diff --git a/.vscode/settings.json b/.vscode/settings.json index fa9f49187..6c70aba17 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,5 +15,7 @@ }, "editor.codeActionsOnSave": { "source.organizeImports.biome": "explicit" - } + }, + "editor.insertSpaces": true, + "editor.tabSize": 2 } diff --git a/apps/alchemy/package.json b/apps/alchemy/package.json index be4973e23..1c49cd26f 100644 --- a/apps/alchemy/package.json +++ b/apps/alchemy/package.json @@ -1,9 +1,10 @@ { - "name": "alchemy-hooks", + "name": "@repo/alchemy", "module": "src/index.ts", + "types": "src/index.d.ts", "type": "module", "scripts": { - "push": "bun src/index.ts" + "create": "bun src/create.ts" }, "devDependencies": { "@types/bun": "latest" diff --git a/apps/alchemy/src/create.ts b/apps/alchemy/src/create.ts new file mode 100644 index 000000000..9527522be --- /dev/null +++ b/apps/alchemy/src/create.ts @@ -0,0 +1,30 @@ +import { Alchemy, Network, WebhookType } from 'alchemy-sdk' +import { appConfig } from './config' + +async function createAddressActivityNotification() { + try { + const settings = { + authToken: appConfig.alchemyNotifyToken, + network: Network.MATIC_MAINNET, // Replace with your network. + } + + const alchemy = new Alchemy(settings) + const addressActivityWebhook = await alchemy.notify.createWebhook( + appConfig.alchemyActivityWebhookUrl, + WebhookType.ADDRESS_ACTIVITY, + { + addresses: [appConfig.presaleAddress], + network: Network.MATIC_MAINNET, + }, + ) + console.log('Address Activity Webhook Details:') + console.log(JSON.stringify(addressActivityWebhook, null, 2)) + console.log( + 'Alchemy Notify address activity notification created, go to https://dashboard.alchemy.com/notify to see details of your custom hook.', + ) + } catch (error) { + console.error('Failed to create address activity notification:', error) + } +} + +createAddressActivityNotification() diff --git a/apps/alchemy/src/index.ts b/apps/alchemy/src/index.ts index 7b5629cc8..c9f6f047d 100644 --- a/apps/alchemy/src/index.ts +++ b/apps/alchemy/src/index.ts @@ -1,26 +1 @@ -import { Alchemy, Network, WebhookType } from 'alchemy-sdk' -import { appConfig } from './config' - -async function createAddressActivityNotification() { - const settings = { - authToken: appConfig.alchemyNotifyToken, - network: Network.MATIC_MAINNET, // Replace with your network. - } - - const alchemy = new Alchemy(settings) - const addressActivityWebhook = await alchemy.notify.createWebhook( - appConfig.alchemyActivityWebhookUrl, - WebhookType.ADDRESS_ACTIVITY, - { - addresses: [appConfig.presaleAddress], - network: Network.MATIC_MAINNET, - }, - ) - console.log('Address Activity Webhook Details:') - console.log(JSON.stringify(addressActivityWebhook, null, 2)) - console.log( - 'Alchemy Notify address activity notification created, go to https://dashboard.alchemy.com/notify to see details of your custom hook.', - ) -} - -createAddressActivityNotification() +export * from './types' diff --git a/apps/alchemy/src/types.ts b/apps/alchemy/src/types.ts new file mode 100644 index 000000000..7d84c5002 --- /dev/null +++ b/apps/alchemy/src/types.ts @@ -0,0 +1,32 @@ +import { Network } from 'alchemy-sdk' +export interface AlchemyWebhookEvent { + webhookId: string + id: string + createdAt: Date + type: AlchemyWebhookType + event: Record +} + +export type AlchemyWebhookType = + | 'MINED_TRANSACTION' + | 'DROPPED_TRANSACTION' + | 'ADDRESS_ACTIVITY' + +export interface AlchemyActivity { + fromAddress: string + toAddress: string + blockNum: string + hash: string + value: number + asset: string + category: string + rawContract: { + rawValue: string + decimals: number + } +} + +export interface AlchemyActivityEvent { + network: Network + activity: AlchemyActivity[] +} \ No newline at end of file diff --git a/apps/indexer/package.json b/apps/indexer/package.json index 6c4aed519..f660e3a1a 100644 --- a/apps/indexer/package.json +++ b/apps/indexer/package.json @@ -16,6 +16,7 @@ "@dfuse/client": "^0.3.21", "@repo/supabase": "workspace:*", "@repo/trigger": "workspace:*", + "@repo/alchemy": "workspace:*", "@sentry/integrations": "^7.114.0", "@sentry/node": "^8.19.0", "@sentry/profiling-node": "^8.26.0", diff --git a/apps/indexer/src/config.ts b/apps/indexer/src/config.ts index 88dd3f10b..52ba208f8 100644 --- a/apps/indexer/src/config.ts +++ b/apps/indexer/src/config.ts @@ -21,6 +21,13 @@ const envSchema = z.object({ (value): value is Address => isAddress(value), 'Invalid issuer address', ), + PRESALE_ADDRESS: z + .string() + .refine( + (value): value is Address => isAddress(value), + 'Invalid presale address', + ), + ALCHEMY_ACTIVITY_SIGNING_KEY: z.string().min(1), }) @@ -33,6 +40,7 @@ if (!parsedEnv.success) { } export const appConfig = { + presaleAddress: parsedEnv.data.PRESALE_ADDRESS, sentry: { dsn: parsedEnv.data.SENTRY_DSN, }, diff --git a/apps/indexer/src/routes/alchemy.ts b/apps/indexer/src/routes/alchemy.ts index 890e18735..d8d52f5b3 100644 --- a/apps/indexer/src/routes/alchemy.ts +++ b/apps/indexer/src/routes/alchemy.ts @@ -1,8 +1,28 @@ import crypto from 'crypto' +import type { AlchemyWebhookEvent } from '@repo/alchemy' import { addressActivityTask } from '@repo/trigger' +import { Network } from 'alchemy-sdk' +import { prodChains } from 'app-env' import type { Request, Response } from 'express' import { appConfig } from '~/config' import { logger } from '~/lib/logger' +import {AlchemyActivityEvent} from '@/Users/gaboesquivel/Code/smartsale/apps/alchemy/src/types'; + +const chainIdToNetwork: Record = { + 1: Network.ETH_MAINNET, + 137: Network.MATIC_MAINNET, + 42161: Network.ARB_MAINNET, + 10: Network.OPT_MAINNET, + 8453: Network.BASE_MAINNET, + 43114: Network.AVAX_MAINNET, + 56: Network.BNB_MAINNET, +} + +const networks: Network[] = prodChains.map((chain) => { + const network = chainIdToNetwork[chain.id] + if (!network) throw new Error(`Unsupported chain ID: ${chain.id}`) + return network +}) /** * Handles incoming Alchemy webhook requests. @@ -13,15 +33,29 @@ import { logger } from '~/lib/logger' export async function alchemyWebhook(req: Request, res: Response) { const evt = req.body as AlchemyWebhookEvent logger.info(`Alchemy webhook received: ${evt.id}`) - // TODO: restore alchemy signature validation + // TODO: restore alchemy signature validation // if (!validateAlchemySignature(req)) return res.status(401).send('Unauthorized') // logger.info('Validated Alchemy webhook 😀') - // TODO: validate user is whitelisted + + const {network, activity} = evt.event + + // Validate before triggering + if ( + evt.type !== 'ADDRESS_ACTIVITY' || + !networks.includes(network) || + (activity.asset !== 'USDC' && activity.asset !== 'USDT') || + activity.toAddress === appConfig.presaleAddress + ) { + return res.status(401).send('Unauthorized') + } + + + // TODO: validate addres is whitelisted const handle = await addressActivityTask.trigger(req.body) logger.info(`Triggered address activity task: ${JSON.stringify(handle)}`) - res.status(200).send('Webhook processed') + res.status(200).send(`Webhook ${evt.id} processed`) } /** @@ -39,16 +73,3 @@ function validateAlchemySignature(req: Request): boolean { hmac.update(payload) return alchemySignature === hmac.digest('hex') } - -export interface AlchemyWebhookEvent { - webhookId: string - id: string - createdAt: Date - type: AlchemyWebhookType - event: Record -} - -export type AlchemyWebhookType = - | 'MINED_TRANSACTION' - | 'DROPPED_TRANSACTION' - | 'ADDRESS_ACTIVITY' diff --git a/apps/trigger/package.json b/apps/trigger/package.json index 742a2607d..05b948edc 100644 --- a/apps/trigger/package.json +++ b/apps/trigger/package.json @@ -9,10 +9,12 @@ "deploy:prod": "bunx trigger.dev@beta deploy --env prod" }, "dependencies": { + "@repo/alchemy": "workspace:*", + "@trigger.dev/sdk": "3.0.0-beta.55", + "alchemy-sdk": "^3.4.1", "app-contracts": "workspace:*", "app-env": "workspace:*", "app-lib": "workspace:*", - "@trigger.dev/sdk": "3.0.0-beta.55", "viem": "latest" } } diff --git a/apps/trigger/src/trigger/activity.ts b/apps/trigger/src/trigger/activity.ts index 30580bfa8..6edb0d06e 100644 --- a/apps/trigger/src/trigger/activity.ts +++ b/apps/trigger/src/trigger/activity.ts @@ -1,17 +1,19 @@ +import type { AlchemyWebhookEvent, AlchemyActivity } from '@repo/alchemy' import { logger, task } from '@trigger.dev/sdk/v3' -import { getErrorMessage } from 'app-lib' // AlchemyWebhookEvent export const addressActivityTask = task({ id: 'address-activity', - run: async (payload: any, { ctx }) => { + run: async (payload: AlchemyWebhookEvent) => { try { - logger.log('Address activity', { payload, ctx }) + const activity: AlchemyActivity = payload.event.activity[0] + console.log(activity) } catch (error) { logger.error('Error processing address activity', { - error: getErrorMessage(error), + error: (error as Error).message, }) throw error } }, }) + diff --git a/bun.lockb b/bun.lockb index 6876c12e4..10ebdb1fd 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/app-env/src/chains.ts b/packages/app-env/src/chains.ts index ee618c150..f5f9653fb 100644 --- a/packages/app-env/src/chains.ts +++ b/packages/app-env/src/chains.ts @@ -1,23 +1,13 @@ import type { Chain } from 'viem' import { arbitrum, - aurora, avalanche, base, bsc, - celo, - cronos, - fantom, - gnosis, - harmonyOne, - kava, mainnet, - metis, - moonbeam, optimism, polygon, sepolia, - zkSync, } from 'viem/chains' export const eosEvmTestnet: Chain = { @@ -41,26 +31,19 @@ export const eosEvmTestnet: Chain = { testnet: true, } -const prodChains: Chain[] = [ - arbitrum, - avalanche, +export const prodChains: Chain[] = [ base, - celo, - mainnet, + arbitrum, optimism, polygon, - zkSync, - bsc, - fantom, - moonbeam, - cronos, - kava, - metis, - gnosis, - aurora, - harmonyOne, + mainnet, // Ethereum + avalanche, + bsc, // BNB Chain ] -const devChains: Chain[] = [eosEvmTestnet, sepolia] + +// Note: Solana is not included as it's not an EVM-compatible chain and not supported by viem + +export const devChains: Chain[] = [eosEvmTestnet, sepolia] // note: use .entries() to get an array export const appChains = {