From e0d43ec77d3c40652494f324bdc90431a8be6ae5 Mon Sep 17 00:00:00 2001 From: kasperpawlowski Date: Sat, 23 Aug 2025 22:48:06 +0100 Subject: [PATCH 1/2] feat: comprehensive V8 support with startup sync and market status monitoring --- Dockerfile | 11 +- README.md | 111 +------ app/lib/utils.ts | 21 +- app/routes/_index.tsx | 106 +++++- app/routes/market-status.config.tsx | 148 +++++++++ config-v8-example.yml | 255 ++++++++++++++ package-docker.json | 93 ++++++ package-lock.json | 494 +++++++++++++++++++++++++++- package.json | 4 +- server/index.ts | 82 +++-- server/services/client.ts | 135 +++++++- server/services/clientEvm.ts | 50 ++- server/services/clientSolana.ts | 48 ++- server/services/rest-api.ts | 251 ++++++++++++++ server/store.ts | 117 ++++++- server/types.ts | 31 +- 16 files changed, 1790 insertions(+), 167 deletions(-) create mode 100644 app/routes/market-status.config.tsx create mode 100644 config-v8-example.yml create mode 100644 package-docker.json create mode 100644 server/services/rest-api.ts diff --git a/Dockerfile b/Dockerfile index 5ca1c44..1d3e27b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,11 @@ FROM base as deps WORKDIR /push-engine -ADD package.json ./ +# Install git for GitHub dependencies +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +# Copy Docker-specific package.json (without macOS native dependencies) +COPY package-docker.json ./package.json RUN npm install --include=dev # Setup production node_modules @@ -21,8 +25,11 @@ FROM base as production-deps WORKDIR /push-engine +# Install git for GitHub dependencies in production stage too +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + COPY --from=deps /push-engine/node_modules /push-engine/node_modules -ADD package.json ./ +COPY package-docker.json ./package.json RUN npm prune --omit=dev # Build the app diff --git a/README.md b/README.md index 83ac597..c1722c8 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ Push Engine is a service that bridges off-chain data streams with on-chain smart - Retrieves and verifies price data from Chainlink Data Streams. - Writes verified prices to on-chain smart contracts. +- **Dual monitoring modes**: Price delta or market status monitoring. +- **V8 report support**: Full support for latest Chainlink Data Streams schema. - Supports containerized deployment with Docker. - Configurable through environment variables and Redis-based settings. @@ -117,7 +119,7 @@ Before setting up the , ensure you have the required dependencies installed. 3. Update the `.env` file with your credential details (explained below) and optionally provide a `config.yml` file following the examples below. > [!TIP] -> To ensure you won't miss any of the needed variables to be set - you can copy the provided `.env.example` and `config-example.yml` to `.env` and `config.yml` respectively and only fill in the details. +> To ensure you won't miss any of the needed variables to be set - you can copy the provided `.env.example` to `.env` and optionally use `config-v8-example.yml` as a reference for your `config.yml`. 4. Install dependencies: ```sh @@ -153,7 +155,7 @@ To make setting environment variables easier there is a `.env.example` file in t > [!NOTE] > All other user configurations are stored locally using Redis file eliminating the need for separate configuration files. This ensures fast access and persistence across sessions without manual file handling. Only sensitive configurations, such as API keys and database credentials, are managed separately in the `.env` file. The application automatically loads and updates configurations in Redis as needed. Users do not need to manually edit or maintain configuration files, simplifying setup and deployment. > -> Optional: The initial configurations can also be seeded by providing a `config.yml` file. See the `config-example.yml` and section below for more details. +> Optional: The initial configurations can also be seeded by providing a `config.yml` file. See the `config-v8-example.yml` and section below for more details. --- @@ -230,101 +232,16 @@ targetChains: ### Example YAML Configuration -```yaml -feeds: - - name: 'AVAX/USD' - feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796' - - name: 'ETH/USD' - feedId: '0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782' -chainId: 43113 -gasCap: '150000' -interval: '*/30 * * * * *' -priceDeltaPercentage: 0.01 -chains: - - id: 995 - name: 'đŸ”Ĩ 5ireChain' - currencyName: '5ire Token' - currencySymbol: '5IRE' - currencyDecimals: 18 - rpc: 'https://rpc.5ire.network' - - id: 84532 - name: 'Base Sepolia Custom' - currencyName: 'Sepolia Ether' - currencySymbol: 'ETH' - currencyDecimals: 18 - rpc: 'https://sepolia.base.org' - testnet: true -verifierAddresses: - - chainId: 995 - address: '0x...' - - chainId: 84532 - address: '0x...' -targetChains: - - chainId: 43113 - targetContracts: - - feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796' - address: '0xfa162F0A25b2C2aA32Ddaacda872B6D7b2c38E47' - functionName: 'set' - functionArgs: - - 'feedId' - - 'validFromTimestamp' - - 'observationsTimestamp' - - 'nativeFee' - - 'linkFee' - - 'expiresAt' - - 'price' - - 'bid' - - 'ask' - abi: - [ - { - 'inputs': - [ - { - 'internalType': 'bytes32', - 'name': 'feedId', - 'type': 'bytes32', - }, - { - 'internalType': 'uint32', - 'name': 'validFromTimestamp', - 'type': 'uint32', - }, - { - 'internalType': 'uint32', - 'name': 'observationsTimestamp', - 'type': 'uint32', - }, - { - 'internalType': 'uint192', - 'name': 'nativeFee', - 'type': 'uint192', - }, - { - 'internalType': 'uint192', - 'name': 'linkFee', - 'type': 'uint192', - }, - { - 'internalType': 'uint32', - 'name': 'expiresAt', - 'type': 'uint32', - }, - { - 'internalType': 'int192', - 'name': 'price', - 'type': 'int192', - }, - { 'internalType': 'int192', 'name': 'bid', 'type': 'int192' }, - { 'internalType': 'int192', 'name': 'ask', 'type': 'int192' }, - ], - 'name': 'set', - 'outputs': [], - 'stateMutability': 'nonpayable', - 'type': 'function', - }, - ] -``` +A comprehensive example configuration file is provided at `config-v8-example.yml` that shows all available options including: + +- **Price Delta Monitoring**: Traditional mode that triggers on price changes +- **Market Status Monitoring**: Mode that triggers on market open/close events +- **V3/V4 Schema**: Individual field extraction for bid/ask spreads +- **V8 Schema**: Raw report handling for advanced use cases +- **EVM and SVM**: Support for both Ethereum and Solana chains +- **Startup Sync**: Immediate data synchronization on bot startup using REST API for any report type + +The example file demonstrates how to configure different monitoring modes, contract interaction patterns, and startup synchronization. You can use it as a reference when creating your own `config.yml`. ### Key Configuration Parameters diff --git a/app/lib/utils.ts b/app/lib/utils.ts index c192d0f..1bee52e 100644 --- a/app/lib/utils.ts +++ b/app/lib/utils.ts @@ -19,9 +19,18 @@ export function detectSchemaVersion(feedId: string) { return `v${parseInt(firstTwoBytesHex, 16)}`; } -export const getReportPrice = (report?: StreamReport) => - report?.version === 'v3' - ? report.benchmarkPrice - : report?.version === 'v4' - ? report.price - : BigInt(0); +export const getReportPrice = (report?: StreamReport) => { + if (!report) return BigInt(0); + + // Check if it's a V8 report (has midPrice field) + if ('midPrice' in report && report.midPrice !== undefined) { + return report.midPrice; + } + + // Check if it's a V3/V4 report (has price field) + if ('price' in report && report.price !== undefined) { + return report.price; + } + + return BigInt(0); +}; diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index aad7cf9..565c456 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -9,6 +9,7 @@ import { getInterval, getPriceDelta, getSavedReportBenchmarkPrice, + getMarketStatusMode, } from 'server/store'; import { formatUSD } from 'server/utils'; import { formatEther } from 'viem'; @@ -38,10 +39,10 @@ import { TooltipProvider, TooltipTrigger, } from '~/components/ui/tooltip'; -import { cn, detectSchemaVersion } from '~/lib/utils'; +import { cn, detectSchemaVersion, getReportPrice } from '~/lib/utils'; export async function loader() { - const [feeds, interval, priceDelta, gasCap] = await Promise.all([ + const [feeds, interval, priceDelta, gasCap, marketStatusMode] = await Promise.all([ (async function () { const feedsIds = await getFeeds(); return await Promise.all( @@ -57,17 +58,22 @@ export async function loader() { getInterval(), getPriceDelta(), getGasCap(), + getMarketStatusMode(), ]); - return { feeds, interval, priceDelta, gasCap }; + return { feeds, interval, priceDelta, gasCap, marketStatusMode }; } export default function Index() { - const { feeds, interval, priceDelta, gasCap } = + const { feeds, interval, priceDelta, gasCap, marketStatusMode } = useLoaderData(); const revalidator = useRevalidator(); const [nextThree, setNextThree] = useState([]); + // Parse market status mode configuration + const marketStatusConfig = marketStatusMode ? JSON.parse(marketStatusMode) : null; + const isMarketStatusMode = marketStatusConfig?.enabled || false; + useEffect(() => { const intervalId = setInterval(() => { if (revalidator.state === 'idle') { @@ -115,8 +121,9 @@ export default function Index() { Feed ID Report Schema Contract - Saved price - Last reported + Saved Price + Last Reported + Market Status Status Remove @@ -151,6 +158,9 @@ export default function Index() { {formatUSD(BigInt(feed.latestReport ?? 0))} + + + @@ -310,6 +320,54 @@ export default function Index() { + + + Market Status Monitoring + + Configure market status monitoring mode. When enabled, the bot will + process reports based on market status changes (open/close) instead + of price changes. This mode is exclusive with price delta monitoring. + + + +
+
+
+ {isMarketStatusMode ? ( +
+ ) : ( +
+ )} +
+ + {isMarketStatusMode ? 'Enabled' : 'Disabled'} + +
+ + {isMarketStatusMode && marketStatusConfig && ( +
+

â€ĸ Triggers on market open: {marketStatusConfig.triggerOnMarketOpen ? 'Yes' : 'No'}

+

â€ĸ Triggers on market close: {marketStatusConfig.triggerOnMarketClose ? 'Yes' : 'No'}

+
+ )} + +

+ {isMarketStatusMode + ? 'Currently monitoring market status changes. Price delta monitoring is disabled.' + : 'Currently using price delta monitoring. Market status monitoring is disabled.' + } +

+
+
+ + + Configure Market Status Mode + + +
Gas cap @@ -345,6 +403,42 @@ export default function Index() { ); } +function MarketStatusIndicator({ feedId }: { feedId: string }) { + const schemaVersion = detectSchemaVersion(feedId); + + if (schemaVersion === 'v8') { + return ( + + + + + V8 + + + +

V8 Report Schema - Supports market status monitoring

+
+
+
+ ); + } + + return ( + + + + + {schemaVersion} + + + +

{schemaVersion} Report Schema - Traditional price monitoring

+
+
+
+ ); +} + function Status({ status }: { status?: number | string }) { if (status === 0) return ( diff --git a/app/routes/market-status.config.tsx b/app/routes/market-status.config.tsx new file mode 100644 index 0000000..e56dd2f --- /dev/null +++ b/app/routes/market-status.config.tsx @@ -0,0 +1,148 @@ +import { ActionFunctionArgs, LoaderFunctionArgs, redirect } from '@remix-run/node'; +import { Form, useLoaderData } from '@remix-run/react'; +import { ArrowLeft } from 'lucide-react'; +import { getMarketStatusMode, setMarketStatusMode } from 'server/store'; +import { Button, buttonVariants } from '~/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card'; +import { Input } from '~/components/ui/input'; +import { Label } from '~/components/ui/label'; +import { Switch } from '~/components/ui/switch'; +import { Link } from '@remix-run/react'; + +export async function loader({}: LoaderFunctionArgs) { + const marketStatusMode = await getMarketStatusMode(); + return { marketStatusMode }; +} + +export async function action({ request }: ActionFunctionArgs) { + const formData = await request.formData(); + const enabled = formData.get('enabled') === 'true'; + const triggerOnMarketOpen = formData.get('triggerOnMarketOpen') === 'true'; + const triggerOnMarketClose = formData.get('triggerOnMarketClose') === 'true'; + + const config = { + enabled, + triggerOnMarketOpen, + triggerOnMarketClose, + }; + + await setMarketStatusMode(JSON.stringify(config)); + + return redirect('/'); +} + +export default function MarketStatusConfig() { + const { marketStatusMode } = useLoaderData(); + const currentConfig = marketStatusMode ? JSON.parse(marketStatusMode) : null; + + return ( +
+
+ + + +

Market Status Monitoring Configuration

+
+ + + + Configure Market Status Monitoring + + Enable market status monitoring to process reports based on market open/close events + instead of price changes. This mode is exclusive with price delta monitoring. + + + +
+
+
+
+ +

+ When enabled, the bot will monitor market status changes instead of price changes. +

+
+ +
+ +
+
+
+ +

+ Process reports when market status changes to "Open" (status 2). +

+
+ +
+ +
+
+ +

+ Process reports when market status changes to "Closed" (status 1). +

+
+ +
+
+
+ +
+ +
+
+
+
+ + + + How It Works + + +
+

Market Status Values:

+
    +
  • Status 0: Unknown/Connecting
  • +
  • Status 1: Market Closed
  • +
  • Status 2: Market Open
  • +
+
+ +
+

V8 Report Schema:

+

+ Market status monitoring requires V8 report schema feeds. The bot will automatically + detect V8 feeds and enable market status monitoring for them. +

+
+ +
+

Exclusive Mode:

+

+ When market status monitoring is enabled, price delta monitoring is automatically + disabled to avoid conflicts and ensure clear monitoring behavior. +

+
+
+
+
+ ); +} \ No newline at end of file diff --git a/config-v8-example.yml b/config-v8-example.yml new file mode 100644 index 0000000..698f01e --- /dev/null +++ b/config-v8-example.yml @@ -0,0 +1,255 @@ +# Comprehensive Chainlink Data Streams Transmitter Configuration Example +# This file shows all available configuration options and monitoring modes + +# ============================================================================= +# REPORT SCHEMA OVERVIEW +# ============================================================================= +# V3/V4 Schema: Cryptocurrency streams with bid/ask spreads +# V8 Schema: Real World Asset (RWA) streams with market status +# V9 Schema: Net Asset Value (NAV) streams +# V10 Schema: Backed xStock streams + +# âš ī¸ IMPORTANT: Market status monitoring is ONLY available for V8+ schemas +# See: https://docs.chain.link/data-streams/rwa-streams +# +# For V8 reports, you'll need the appropriate verifier proxy address for your network +# See: https://docs.chain.link/data-streams/rwa-streams?page=1&testnetPage=1 + +feeds: + - name: 'AVAX/USD' + feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796' + # V3 Schema: Cryptocurrency stream with bid/ask spreads + - name: 'ETH/USD' + feedId: '0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782' + # V3 Schema: Cryptocurrency stream with bid/ask spreads + - name: 'WLD/USD' + feedId: '0x0004b9905d8337c34e00f8dbe31619428bac5c3937e73e6af75c71780f1770ce' + # V8 Schema: RWA stream with market status monitoring capability + +chainId: 43113 +gasCap: '150000' +interval: '*/30 * * * * *' # Check every 30 seconds + +# ============================================================================= +# MONITORING MODE CONFIGURATION +# ============================================================================= + +# Option 1: Price Delta Monitoring (Default Mode) +# Process reports when price changes by specified percentage +# Available for ALL report schemas (V3, V4, V8) +priceDeltaPercentage: 0.01 # Process when price changes by 1% or more + +# Global startup configuration +# This ensures the bot syncs current data on startup before starting cron jobs +# Works for both market status monitoring and price delta monitoring +startup: + syncOnStartup: true # Enable startup sync for all monitoring modes + +# Option 2: Market Status Monitoring +# Process reports only when market status changes (open/close) +# âš ī¸ IMPORTANT: Only available for V8 report schemas (RWA assets) +# âš ī¸ NOT available for V3/V4 cryptocurrency streams +# Note: Only one monitoring mode can be active at a time +marketStatusMode: + enabled: false # Set to true to enable market status monitoring (V8 only) + triggerOnMarketOpen: true # Trigger when market opens (status 2) + triggerOnMarketClose: true # Trigger when market closes (status 1) + +# ============================================================================= +# CHAIN CONFIGURATIONS +# ============================================================================= + +chains: + - id: 995 + name: 'đŸ”Ĩ 5ireChain' + currencyName: '5ire Token' + currencySymbol: '5IRE' + currencyDecimals: 18 + rpc: 'https://rpc.5ire.network' + - id: 84532 + name: 'Base Sepolia Custom' + currencyName: 'Sepolia Ether' + currencySymbol: 'ETH' + currencyDecimals: 18 + rpc: 'https://sepolia.base.org' + testnet: true + +verifierAddresses: + - chainId: 995 + address: '0x...' # Replace with actual verifier contract address + - chainId: 84532 + address: '0x...' # Replace with actual verifier contract address + # For V8 RWA feeds, you'll also need the verifier proxy address for your network + # See the Chainlink documentation for the correct address per network + +# ============================================================================= +# TARGET CONTRACT CONFIGURATIONS +# ============================================================================= +# Different contract patterns for different use cases: +# 1. V3/V4 Schema: Extract individual price fields (bid/ask spreads) +# 2. V8 Schema: Handle raw reports with market status monitoring +# 3. Market Status: Use HookTargetMarketStatus pattern for V8 RWA feeds + +targetChains: + - chainId: 43113 + targetContracts: + # Configuration 1: V3 Schema (Individual Fields) + - feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796' + address: '0xfa162F0A25b2C2aA32Ddaacda872B6D7b2c38E47' + functionName: 'set' + functionArgs: + - 'feedId' + - 'validFromTimestamp' + - 'observationsTimestamp' + - 'nativeFee' + - 'linkFee' + - 'expiresAt' + - 'price' + - 'bid' + - 'ask' + skipVerify: false # Bot verifies first, then contract verifies again + abi: + [ + { + 'inputs': + [ + { + 'internalType': 'bytes32', + 'name': 'feedId', + 'type': 'bytes32', + }, + { + 'internalType': 'uint32', + 'name': 'validFromTimestamp', + 'type': 'uint32', + }, + { + 'internalType': 'uint32', + 'name': 'observationsTimestamp', + 'type': 'uint32', + }, + { + 'internalType': 'uint192', + 'name': 'nativeFee', + 'type': 'uint192', + }, + { + 'internalType': 'uint192', + 'name': 'linkFee', + 'type': 'uint192', + }, + { + 'internalType': 'uint32', + 'name': 'expiresAt', + 'type': 'uint32', + }, + { + 'internalType': 'int192', + 'name': 'price', + 'type': 'int192', + }, + { 'internalType': 'int192', 'name': 'bid', 'type': 'int192' }, + { 'internalType': 'int192', 'name': 'ask', 'type': 'int192' }, + ], + 'name': 'set', + 'outputs': [], + 'stateMutability': 'nonpayable', + 'type': 'function', + }, + ] + + # Configuration 2: V8 Schema (Raw Report) + - feedId: '0x0004b9905d8337c34e00f8dbe31619428bac5c3937e73e6af75c71780f1770ce' + address: '0xYourV8ContractAddress' + functionName: 'updatePriceData' + functionArgs: + - 'rawReport' # Raw V8 report for on-chain verification + - 'parameterPayload' # Parameters for verification (usually empty for V8) + skipVerify: true # Skip bot verification, verify on-chain + abi: + - name: 'updatePriceData' + type: 'function' + stateMutability: 'nonpayable' + inputs: + - { + 'internalType': 'bytes', + 'name': 'verifySignedReportRequest', + 'type': 'bytes', + } + - { + 'internalType': 'bytes', + 'name': 'parameterPayload', + 'type': 'bytes', + } + outputs: + - { + 'internalType': 'bytes', + 'name': '', + 'type': 'bytes', + } + + # Configuration 3: Market Status Monitoring (V8 Schema Only) + # âš ī¸ This configuration only works with V8 report schemas (RWA assets) + # âš ī¸ Do NOT use with V3/V4 cryptocurrency streams + # This example uses the HookTargetMarketStatus contract pattern + - feedId: '0x0004b9905d8337c34e00f8dbe31619428bac5c3937e73e6af75c71780f1770ce' + address: '0xYourHookTargetMarketStatusContractAddress' + functionName: 'updateMarketStatus' + functionArgs: + - 'rawReport' # Raw V8 report for on-chain verification + skipVerify: false # Bot verifies first, then contract verifies again + abi: + - name: 'update' + type: 'function' + stateMutability: 'nonpayable' + inputs: + - { + 'internalType': 'bytes', + 'name': '_rawReport', + 'type': 'bytes', + } + outputs: [] + +# ============================================================================= +# VIRTUAL MACHINE CONFIGURATIONS +# ============================================================================= + +vm: 'evm' # Options: 'evm' or 'svm' + +# Solana configuration (when vm: 'svm') +svm: + cluster: 'devnet' # Options: 'devnet', 'testnet', 'mainnet-beta' + chains: + - cluster: 'devnet' + name: 'Solana Devnet' + rpcUrl: 'https://api.devnet.solana.com' + +# ============================================================================= +# DEPENDENCY NOTES +# ============================================================================= +# This project uses a custom fork of @hackbg/chainlink-datastreams-consumer with V8 support +# The dependency is configured in package.json as: +# "@hackbg/chainlink-datastreams-consumer": "github:euler-xyz/chainlink-datastreams-consumer#main" +# +# This fork adds support for V8 report schemas (RWA assets) which is not available in the original package +# See: https://github.com/euler-xyz/chainlink-datastreams-consumer + +# ============================================================================= +# USAGE NOTES +# ============================================================================= + +# IMPORTANT: Choose ONE monitoring mode: +# 1. Price Delta Monitoring: Set priceDeltaPercentage (default) +# - Available for ALL schemas: V3, V4, V8 +# - Works with cryptocurrency and RWA streams +# 2. Market Status Monitoring: Set marketStatusMode.enabled = true +# - âš ī¸ ONLY available for V8 report schemas (RWA assets) +# - âš ī¸ NOT available for V3/V4 cryptocurrency streams +# - See: https://docs.chain.link/data-streams/rwa-streams + +# For V3/V4 Schema: Use individual field names in functionArgs +# For V8 Schema: Use 'rawReport' and 'parameterPayload' in functionArgs + +# The bot will automatically detect the report schema and process accordingly +# V8 reports include market status and median pricing (RWA assets) +# V3/V4 reports include bid/ask spreads (cryptocurrency) \ No newline at end of file diff --git a/package-docker.json b/package-docker.json new file mode 100644 index 0000000..2a92e31 --- /dev/null +++ b/package-docker.json @@ -0,0 +1,93 @@ +{ + "name": "chainlink-datastreams-scheduler", + "private": true, + "sideEffects": false, + "type": "module", + "scripts": { + "build": "remix vite:build", + "dev": "tsx server/index.ts", + "lint": "prettier --write .", + "start": "cross-env NODE_ENV=production node ./build/server/index.js", + "typecheck": "tsc", + "test": "jest", + "test:coverage": "jest --coverage" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.31.0", + "@hackbg/chainlink-datastreams-consumer": "github:euler-xyz/chainlink-datastreams-consumer#main", + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-label": "^2.1.1", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-scroll-area": "^1.2.2", + "@radix-ui/react-select": "^2.1.5", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-visually-hidden": "^1.1.2", + "@remix-run/express": "^2.15.2", + "@remix-run/node": "^2.15.2", + "@remix-run/react": "^2.15.2", + "@solana/web3.js": "^1.98.0", + "@types/react-lazylog": "^4.5.4", + "bn.js": "^5.2.1", + "bottleneck": "^2.19.5", + "bs58": "^6.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "compression": "^1.7.4", + "cron": "^3.5.0", + "cron-parser": "^5.0.3", + "cross-env": "^7.0.3", + "dotenv": "^16.5.0", + "express": "^4.19.2", + "ioredis": "^5.5.0", + "ioredis-mock": "^8.9.0", + "isbot": "^4.1.0", + "jest": "^29.7.0", + "js-yaml": "^4.1.0", + "lucide-react": "^0.473.0", + "morgan": "^1.10.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.2", + "react-lazylog": "^4.5.3", + "snappy": "^7.2.2", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "ts-jest": "^29.2.6", + "viem": "^2.22.10", + "winston": "^3.17.0", + "winston-daily-rotate-file": "^5.0.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@remix-run/dev": "^2.15.2", + "@types/bn.js": "^5.1.6", + "@types/compression": "^1.7.5", + "@types/express": "^4.17.20", + "@types/js-yaml": "^4.0.9", + "@types/morgan": "^1.9.9", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10.4.19", + "esbuild": "^0.24.2", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "tsx": "^4.19.2", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" + }, + "engines": { + "node": ">=20.0.0" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7f2ba97..ecb5aa0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,8 +7,9 @@ "name": "chainlink-datastreams-scheduler", "dependencies": { "@coral-xyz/anchor": "^0.31.0", - "@hackbg/chainlink-datastreams-consumer": "^2.1.0", + "@hackbg/chainlink-datastreams-consumer": "github:euler-xyz/chainlink-datastreams-consumer#main", "@hookform/resolvers": "^5.0.1", + "@napi-rs/snappy-darwin-arm64": "^7.3.2", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-navigation-menu": "^1.2.5", @@ -21,6 +22,7 @@ "@remix-run/express": "^2.15.2", "@remix-run/node": "^2.15.2", "@remix-run/react": "^2.15.2", + "@rollup/rollup-darwin-arm64": "^4.48.0", "@solana/web3.js": "^1.98.0", "@types/react-lazylog": "^4.5.4", "bn.js": "^5.2.1", @@ -1070,9 +1072,8 @@ "license": "MIT" }, "node_modules/@hackbg/chainlink-datastreams-consumer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@hackbg/chainlink-datastreams-consumer/-/chainlink-datastreams-consumer-2.1.0.tgz", - "integrity": "sha512-t6Cdt6nbHxnFe1TzV1hqJShf8WSTeqVG/qvjHXWjd2Bm8OfPqPlD4a3OBHJ2A+3ejJsWm+KKV5fK8VmX0f9cUg==", + "version": "2.2.0", + "resolved": "git+ssh://git@github.com/euler-xyz/chainlink-datastreams-consumer.git#01b9ed73c6612c9f05e85dd69b727c523391a954", "license": "BSD-3-Clause-No-Military-License", "dependencies": { "@noble/hashes": "^1.3.2", @@ -1744,6 +1745,53 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@napi-rs/snappy-android-arm-eabi": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm-eabi/-/snappy-android-arm-eabi-7.2.2.tgz", + "integrity": "sha512-H7DuVkPCK5BlAr1NfSU8bDEN7gYs+R78pSHhDng83QxRnCLmVIZk33ymmIwurmoA1HrdTxbkbuNl+lMvNqnytw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-android-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-android-arm64/-/snappy-android-arm64-7.2.2.tgz", + "integrity": "sha512-2R/A3qok+nGtpVK8oUMcrIi5OMDckGYNoBLFyli3zp8w6IArPRfg1yOfVUcHvpUDTo9T7LOS1fXgMOoC796eQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.3.2.tgz", + "integrity": "sha512-2j5cHSb34BdnQxBITdXa4NkbPrVc00yI/8BT0laD29Qm7nTXKoUNKYEpD8KYV3IvP2UxFDK7n0nMRqhuo2+fug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@napi-rs/snappy-darwin-x64": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-x64/-/snappy-darwin-x64-7.2.2.tgz", @@ -1760,6 +1808,150 @@ "node": ">= 10" } }, + "node_modules/@napi-rs/snappy-freebsd-x64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-freebsd-x64/-/snappy-freebsd-x64-7.2.2.tgz", + "integrity": "sha512-mRTCJsuzy0o/B0Hnp9CwNB5V6cOJ4wedDTWEthsdKHSsQlO7WU9W1yP7H3Qv3Ccp/ZfMyrmG98Ad7u7lG58WXA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm-gnueabihf": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm-gnueabihf/-/snappy-linux-arm-gnueabihf-7.2.2.tgz", + "integrity": "sha512-v1uzm8+6uYjasBPcFkv90VLZ+WhLzr/tnfkZ/iD9mHYiULqkqpRuC8zvc3FZaJy5wLQE9zTDkTJN1IvUcZ+Vcg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-gnu/-/snappy-linux-arm64-gnu-7.2.2.tgz", + "integrity": "sha512-LrEMa5pBScs4GXWOn6ZYXfQ72IzoolZw5txqUHVGs8eK4g1HR9HTHhb2oY5ySNaKakG5sOgMsb1rwaEnjhChmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-arm64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-arm64-musl/-/snappy-linux-arm64-musl-7.2.2.tgz", + "integrity": "sha512-3orWZo9hUpGQcB+3aTLW7UFDqNCQfbr0+MvV67x8nMNYj5eAeUtMmUE/HxLznHO4eZ1qSqiTwLbVx05/Socdlw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-gnu": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-gnu/-/snappy-linux-x64-gnu-7.2.2.tgz", + "integrity": "sha512-jZt8Jit/HHDcavt80zxEkDpH+R1Ic0ssiVCoueASzMXa7vwPJeF4ZxZyqUw4qeSy7n8UUExomu8G8ZbP6VKhgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-linux-x64-musl": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-linux-x64-musl/-/snappy-linux-x64-musl-7.2.2.tgz", + "integrity": "sha512-Dh96IXgcZrV39a+Tej/owcd9vr5ihiZ3KRix11rr1v0MWtVb61+H1GXXlz6+Zcx9y8jM1NmOuiIuJwkV4vZ4WA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-arm64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-arm64-msvc/-/snappy-win32-arm64-msvc-7.2.2.tgz", + "integrity": "sha512-9No0b3xGbHSWv2wtLEn3MO76Yopn1U2TdemZpCaEgOGccz1V+a/1d16Piz3ofSmnA13HGFz3h9NwZH9EOaIgYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-ia32-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-ia32-msvc/-/snappy-win32-ia32-msvc-7.2.2.tgz", + "integrity": "sha512-QiGe+0G86J74Qz1JcHtBwM3OYdTni1hX1PFyLRo3HhQUSpmi13Bzc1En7APn+6Pvo7gkrcy81dObGLDSxFAkQQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/snappy-win32-x64-msvc": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-win32-x64-msvc/-/snappy-win32-x64-msvc-7.2.2.tgz", + "integrity": "sha512-a43cyx1nK0daw6BZxVcvDEXxKMFLSBSDTAhsFD0VqSKcC7MGUBMaqyoWUcMiI7LBSz4bxUmxDWKfCYzpEmeb3w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@noble/curves": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", @@ -3240,6 +3432,46 @@ "web-streams-polyfill": "^3.1.1" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", + "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", + "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.48.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.0.tgz", + "integrity": "sha512-QhR2KA18fPlJWFefySJPDYZELaVqIUVnYgAOdtJ+B/uH96CFg2l1TQpX19XpUMWUqMyIiyY45wje8K6F4w4/CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "os": [ + "darwin" + ] + }, "node_modules/@rollup/rollup-darwin-x64": { "version": "4.40.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", @@ -3254,6 +3486,230 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", + "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", + "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", + "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", + "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", + "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", + "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", + "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", + "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", + "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", + "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", + "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", + "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", + "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", + "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", + "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", + "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -14155,6 +14611,20 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.40.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", + "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, "node_modules/rpc-websockets": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.1.1.tgz", @@ -14637,6 +15107,22 @@ "@napi-rs/snappy-win32-x64-msvc": "7.2.2" } }, + "node_modules/snappy/node_modules/@napi-rs/snappy-darwin-arm64": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/snappy-darwin-arm64/-/snappy-darwin-arm64-7.2.2.tgz", + "integrity": "sha512-USgArHbfrmdbuq33bD5ssbkPIoT7YCXCRLmZpDS6dMDrx+iM7eD2BecNbOOo7/v1eu6TRmQ0xOzeQ6I/9FIi5g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", diff --git a/package.json b/package.json index ed24d34..71f73ab 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,9 @@ }, "dependencies": { "@coral-xyz/anchor": "^0.31.0", - "@hackbg/chainlink-datastreams-consumer": "^2.1.0", + "@hackbg/chainlink-datastreams-consumer": "github:euler-xyz/chainlink-datastreams-consumer#main", "@hookform/resolvers": "^5.0.1", + "@napi-rs/snappy-darwin-arm64": "^7.3.2", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-navigation-menu": "^1.2.5", @@ -28,6 +29,7 @@ "@remix-run/express": "^2.15.2", "@remix-run/node": "^2.15.2", "@remix-run/react": "^2.15.2", + "@rollup/rollup-darwin-arm64": "^4.48.0", "@solana/web3.js": "^1.98.0", "@types/react-lazylog": "^4.5.4", "bn.js": "^5.2.1", diff --git a/server/index.ts b/server/index.ts index 2acad04..af0c82b 100644 --- a/server/index.ts +++ b/server/index.ts @@ -24,6 +24,7 @@ import { setInterval, setLatestReport, } from './store.js'; + import { schedule } from './services/limiter.js'; import { createDatastream } from './services/datastreams.js'; import { getReportPrice } from '~/lib/utils.js'; @@ -304,32 +305,63 @@ function createCronJob(feedId: string, interval: string) { return; } await reconnectDataStreamIfStale(report); - const latestBenchmarkPrice = getReportPrice(report); - if (!latestBenchmarkPrice) return; - const savedBenchmarkPrice = await getSavedReportBenchmarkPrice(feedId); - const diff = latestBenchmarkPrice - BigInt(savedBenchmarkPrice ?? 0); - const percentDiff = - !savedBenchmarkPrice || - isNaN(Number(savedBenchmarkPrice)) || - Number(savedBenchmarkPrice) === 0 - ? 100 - : Number( - ((latestBenchmarkPrice - BigInt(savedBenchmarkPrice)) * - 1000000n) / - BigInt(savedBenchmarkPrice) - ) / 10000; - const priceDelta = await getPriceDelta(); - if (Math.abs(percentDiff) < Number(priceDelta ?? 0)) return; - logger.info( - `🚨 Price deviation detected | ${await getFeedName( - report.feedId - )}: ${formatUSD(latestBenchmarkPrice)}$ | ${ - isPositive(diff) ? '📈' : '📉' - } ${isPositive(diff) ? '+' : ''}${percentDiff}% (${formatUSD(diff)}$)`, - report - ); + + // Check if this report should be processed based on monitoring mode + // Import the monitoring logic from store + const { getMarketStatusMode, getSavedReportMarketStatus } = await import('./store.js'); + + const marketStatusMode = await getMarketStatusMode(); + if (marketStatusMode && JSON.parse(marketStatusMode).enabled) { + // Market status mode enabled - check for market status changes + if ('marketStatus' in report && report.marketStatus !== undefined) { + const currentStatus = Number(report.marketStatus); + const lastStatusNum = await getSavedReportMarketStatus(report.feedId); + + // Check if status changed + if (lastStatusNum !== null && currentStatus !== lastStatusNum) { + const config = JSON.parse(marketStatusMode); + + // Check specific status triggers + if ((config.triggerOnMarketOpen && currentStatus === 2) || + (config.triggerOnMarketClose && currentStatus === 1)) { + logger.info(`🔄 Market status changed to ${currentStatus} for feed ${feedId}`); + await schedule(() => dataUpdater({ report })); + return; + } + } + } + // No market status change detected, skip processing + logger.info(`â­ī¸ Report skipped - no market status change detected`); + return; + } else { + // Price delta mode - use existing logic + const latestBenchmarkPrice = getReportPrice(report); + if (!latestBenchmarkPrice) return; + const savedBenchmarkPrice = await getSavedReportBenchmarkPrice(feedId); + const diff = latestBenchmarkPrice - BigInt(savedBenchmarkPrice ?? 0); + const percentDiff = + !savedBenchmarkPrice || + isNaN(Number(savedBenchmarkPrice)) || + Number(savedBenchmarkPrice) === 0 + ? 100 + : Number( + ((latestBenchmarkPrice - BigInt(savedBenchmarkPrice)) * + 1000000n) / + BigInt(savedBenchmarkPrice) + ) / 10000; + const priceDelta = await getPriceDelta(); + if (Math.abs(percentDiff) < Number(priceDelta ?? 0)) return; + logger.info( + `🚨 Price deviation detected | ${await getFeedName( + report.feedId + )}: ${formatUSD(latestBenchmarkPrice)}$ | ${ + isPositive(diff) ? '📈' : '📉' + } ${isPositive(diff) ? '+' : ''}${percentDiff}% (${formatUSD(diff)}$)`, + report + ); - await schedule(() => dataUpdater({ report })); + await schedule(() => dataUpdater({ report })); + } }, null, true diff --git a/server/services/client.ts b/server/services/client.ts index 7d453df..54704bd 100644 --- a/server/services/client.ts +++ b/server/services/client.ts @@ -27,6 +27,10 @@ import { getSkipVerify, getVm, setSavedReport, + getMarketStatusMode, + getSavedReportMarketStatus, + getPriceDelta, + getSavedReportBenchmarkPrice, } from 'server/store'; import { zeroAddress } from 'viem'; import { StreamReport } from 'server/types'; @@ -78,6 +82,15 @@ export async function dataUpdater({ report }: { report: StreamReport }) { return; } + // Validate monitoring configuration consistency + await validateMonitoringConfiguration(); + + // Check if we should process this report based on monitoring mode + if (!shouldProcessReport(report)) { + logger.info(`â­ī¸ Skipping report - no trigger conditions met`, { feedId }); + return; + } + const vm = await getVm(); if (vm === 'svm') { const cluster = await getCluster(); @@ -119,10 +132,8 @@ export async function dataUpdater({ report }: { report: StreamReport }) { ? report : await solanaVerifyReport(report); if (!reportPayload) { - if (!reportPayload) { - logger.warn(`🛑 Verified report is missing | Aborting`); - return; - } + logger.warn(`🛑 Verified report is missing | Aborting`); + return; } const transaction = await executeSolanaProgram({ @@ -148,10 +159,10 @@ export async function dataUpdater({ report }: { report: StreamReport }) { transaction, }); await setSavedReport(report); + + // Log success consistently logger.info( - `💾 Price stored | ${await getFeedName(report.feedId)}: ${formatUSD( - getReportPrice(report) - )}$`, + `💾 Report processed successfully | ${await getFeedName(report.feedId)}`, { report } ); return; @@ -181,10 +192,8 @@ export async function dataUpdater({ report }: { report: StreamReport }) { const reportPayload = skipVerify ? report : await evmVerifyReport(report); if (!reportPayload) { - if (!reportPayload) { - logger.warn(`🛑 Verified report is missing | Aborting`); - return; - } + logger.warn(`🛑 Verified report is missing | Aborting`); + return; } const transaction = await executeEVMContract({ @@ -194,16 +203,14 @@ export async function dataUpdater({ report }: { report: StreamReport }) { functionArgs: await getFunctionArgs(feedId, chainId), }); if (transaction?.status) { - logger.info(`â„šī¸ Transaction status: ${transaction?.status}`, { - transaction, - }); + logger.info(`â„šī¸ Transaction status: ${transaction?.status}`, { transaction }); } if (transaction?.status === 'success') { await setSavedReport(report); + + // Log success consistently logger.info( - `💾 Price stored | ${await getFeedName(report.feedId)}: ${formatUSD( - getReportPrice(report) - )}$`, + `💾 Report processed successfully | ${await getFeedName(report.feedId)}`, { report } ); } @@ -212,3 +219,97 @@ export async function dataUpdater({ report }: { report: StreamReport }) { console.error(error); } } + +/** + * Validate configuration consistency between monitoring modes + */ +async function validateMonitoringConfiguration(): Promise { + const marketStatusMode = await getMarketStatusMode(); + const priceDelta = await getPriceDelta(); + + if (marketStatusMode && JSON.parse(marketStatusMode).enabled) { + // Market status mode is enabled + if (priceDelta && Number(priceDelta) > 0) { + logger.warn( + `âš ī¸ Configuration inconsistency detected: Market status mode is enabled but priceDeltaPercentage is ${priceDelta}%. ` + + `The bot will use market status monitoring and ignore price delta. ` + + `Consider setting priceDeltaPercentage to 0 or removing it to avoid confusion.` + ); + } + } else { + // Market status mode is disabled or not configured + if (!priceDelta || Number(priceDelta) === 0) { + logger.warn( + `âš ī¸ Configuration inconsistency detected: Market status mode is disabled but priceDeltaPercentage is ${priceDelta || 'not set'}. ` + + `The bot will process ALL reports without filtering. ` + + `Consider setting priceDeltaPercentage > 0 for efficient monitoring or enabling market status mode.` + ); + } + } +} + +/** + * Determine if a report should be processed based on monitoring mode + */ +async function shouldProcessReport(report: StreamReport): Promise { + const marketStatusMode = await getMarketStatusMode(); + + if (marketStatusMode && JSON.parse(marketStatusMode).enabled) { + // Market status mode enabled - use market status logic + return shouldProcessMarketStatus(report); + } + + // Default: always use price delta mode (existing behavior) + return shouldProcessPriceDelta(report); +} + +/** + * Check if report should be processed based on market status + */ +async function shouldProcessMarketStatus(report: StreamReport): Promise { + const marketStatusMode = await getMarketStatusMode(); + if (!marketStatusMode) return false; + + const config = JSON.parse(marketStatusMode); + + if ('marketStatus' in report && report.marketStatus !== undefined) { + const currentStatus = Number(report.marketStatus); + const lastStatusNum = await getSavedReportMarketStatus(report.feedId); + + // Check if status changed + if (lastStatusNum !== null && currentStatus !== lastStatusNum) { + // Check specific status triggers + if (config.triggerOnMarketOpen && currentStatus === 2) { + return true; + } + + if (config.triggerOnMarketClose && currentStatus === 1) { + return true; + } + } + } + + return false; +} + +/** + * Check if report should be processed based on price delta + */ +async function shouldProcessPriceDelta(report: StreamReport): Promise { + // Get price delta from config + const priceDelta = await getPriceDelta(); + if (!priceDelta) return true; + + const delta = Number(priceDelta); + if (delta <= 0) return true; + + // Get last saved price + const lastPrice = await getSavedReportBenchmarkPrice(report.feedId); + if (!lastPrice) return true; // First report, always process + + const currentPrice = getReportPrice(report); + if (currentPrice === 0n) return false; + + const priceChange = Math.abs(Number(currentPrice - lastPrice) / Number(lastPrice)); + return priceChange >= delta; +} diff --git a/server/services/clientEvm.ts b/server/services/clientEvm.ts index 9d509e6..6939e02 100644 --- a/server/services/clientEvm.ts +++ b/server/services/clientEvm.ts @@ -22,7 +22,7 @@ import { readContract, getBalance, } from 'viem/actions'; -import { ReportV3, ReportV4, StreamReport } from '../types'; +import { StreamReport, ReportV3, ReportV4, ReportV8 } from '../types'; import { feeManagerAbi, verifierProxyAbi } from '../config/abi'; import { logger } from './logger'; import { @@ -236,7 +236,7 @@ export async function verifyReport(report: StreamReport) { ); const reportVersion = parseInt(reportData.slice(0, 6), 16); - if (reportVersion !== 3 && reportVersion !== 4) { + if (reportVersion !== 3 && reportVersion !== 4 && reportVersion !== 8) { logger.warn('âš ī¸ Invalid report version', { report }); return; } @@ -382,6 +382,52 @@ export async function verifyReport(report: StreamReport) { return; } + if (reportVersion === 8) { + const [ + feedId, + validFromTimestamp, + observationsTimestamp, + nativeFee, + linkFee, + expiresAt, + lastUpdateTimestamp, + midPrice, + marketStatus, + ] = decodeAbiParameters( + [ + { type: 'bytes32', name: 'feedId' }, + { type: 'uint32', name: 'validFromTimestamp' }, + { type: 'uint32', name: 'observationsTimestamp' }, + { type: 'uint192', name: 'nativeFee' }, + { type: 'uint192', name: 'linkFee' }, + { type: 'uint32', name: 'expiresAt' }, + { type: 'uint64', name: 'lastUpdateTimestamp' }, + { type: 'int192', name: 'midPrice' }, + { type: 'uint32', name: 'marketStatus' }, + ], + verifiedReportData + ); + + const verifiedReport: ReportV8 = { + reportVersion, + verifiedReport: verifiedReportData as Hex, + feedId, + validFromTimestamp, + observationsTimestamp, + nativeFee, + linkFee, + expiresAt, + lastUpdateTimestamp, + midPrice, + marketStatus, + rawReport: report.rawReport, + parameterPayload: feeTokenAddressEncoded, + }; + + logger.info('✅ V8 Report verified', { verifiedReport }); + return verifiedReport; + } + if (reportVersion === 3) { const [ feedId, diff --git a/server/services/clientSolana.ts b/server/services/clientSolana.ts index 5e45e90..c1c515c 100644 --- a/server/services/clientSolana.ts +++ b/server/services/clientSolana.ts @@ -11,7 +11,7 @@ import * as snappy from 'snappy'; import { logger } from './logger'; import { getCluster, setCluster } from '../store'; import { getAllSolanaChains } from '../config/chains'; -import { ReportV3, ReportV4, StreamReport } from '../types'; +import { StreamReport, ReportV3, ReportV4, ReportV8 } from '../types'; import idl from '../config/idl.json'; import { Verifier } from '../config/idlType'; import { decodeAbiParameters, Hex } from 'viem'; @@ -150,7 +150,7 @@ export async function verifyReport(report: StreamReport) { ); const reportVersion = parseInt(reportData.slice(0, 6), 16); - if (reportVersion !== 3 && reportVersion !== 4) { + if (reportVersion !== 3 && reportVersion !== 4 && reportVersion !== 8) { logger.warn('âš ī¸ Invalid report version', { report }); return; } @@ -221,6 +221,50 @@ export async function verifyReport(report: StreamReport) { for (const log of txDetails.meta.logMessages) { if (log.includes('Program return') || log.includes('Program consumed')) { const verifiedReportData = log.split(' ')[3]; + if (verifiedReportData && reportVersion === 8) { + const [ + feedId, + validFromTimestamp, + observationsTimestamp, + nativeFee, + linkFee, + expiresAt, + lastUpdateTimestamp, + midPrice, + marketStatus, + ] = decodeAbiParameters( + [ + { type: 'bytes32', name: 'feedId' }, + { type: 'uint32', name: 'validFromTimestamp' }, + { type: 'uint32', name: 'observationsTimestamp' }, + { type: 'uint192', name: 'nativeFee' }, + { type: 'uint32', name: 'expiresAt' }, + { type: 'uint64', name: 'lastUpdateTimestamp' }, + { type: 'int192', name: 'midPrice' }, + { type: 'uint32', name: 'marketStatus' }, + ], + `0x${base64ToHex(verifiedReportData)}` + ); + + const verifiedReport: ReportV8 = { + reportVersion, + verifiedReport: `0x${base64ToHex(verifiedReportData)}` as Hex, + feedId, + validFromTimestamp, + observationsTimestamp, + nativeFee, + linkFee, + expiresAt, + lastUpdateTimestamp, + midPrice, + marketStatus, + rawReport: report.rawReport, + parameterPayload: undefined, + }; + + return verifiedReport; + } + if (verifiedReportData && reportVersion === 3) { const [ feedId, diff --git a/server/services/rest-api.ts b/server/services/rest-api.ts new file mode 100644 index 0000000..571b92f --- /dev/null +++ b/server/services/rest-api.ts @@ -0,0 +1,251 @@ +import { logger } from './logger.js'; +import { ReportV3, ReportV4, ReportV8 } from '../types.js'; +import crypto from 'crypto'; + +// Chainlink Data Streams REST API endpoints +const DATASTREAMS_BASE_URL = process.env.DATASTREAMS_HOSTNAME || 'https://api.testnet-dataengine.chain.link'; + +// Authentication credentials from environment variables +const API_KEY = process.env.DATASTREAMS_CLIENT_ID; +const API_SECRET = process.env.DATASTREAMS_CLIENT_SECRET; + +export interface ChainlinkFeedData { + feedId: string; + report: ReportV3 | ReportV4 | ReportV8 | null; + error?: string; +} + +/** + * Generate Chainlink Data Streams API authentication headers + * Based on official documentation: https://docs.chain.link/data-streams/reference/data-streams-api/authentication + */ +function generateAuthHeaders(feedId: string): { [key: string]: string } { + if (!API_KEY || !API_SECRET) { + throw new Error('DATASTREAMS_CLIENT_ID and DATASTREAMS_CLIENT_SECRET environment variables are required'); + } + + // Generate timestamp in milliseconds (as required by documentation) + const timestamp = Date.now().toString(); + + // API parameters + const method = 'GET'; + const basePath = '/api/v1/reports/latest'; + const fullPath = `${basePath}?feedID=${feedId}`; + + // Calculate body hash for empty string (GET request) + const bodyHash = crypto.createHash('sha256').update('').digest('hex'); + + // Create string to sign: "METHOD FULL_PATH BODY_HASH API_KEY TIMESTAMP" + const stringToSign = `${method} ${fullPath} ${bodyHash} ${API_KEY} ${timestamp}`; + + // Generate HMAC-SHA256 signature + const signature = crypto.createHmac('sha256', API_SECRET).update(stringToSign).digest('hex'); + + return { + 'Authorization': API_KEY, + 'X-Authorization-Timestamp': timestamp, + 'X-Authorization-Signature-SHA256': signature, + 'Content-Type': 'application/json', + }; +} + +/** + * Fetch current market data for a specific feed from Chainlink's REST API + * Supports V3, V4, and V8 report schemas + */ +export async function fetchCurrentFeedData(feedId: string): Promise { + try { + logger.info(`🔍 Fetching current data for feed ${feedId} via REST API`); + + // Check if authentication credentials are available + if (!API_KEY || !API_SECRET) { + logger.error(`❌ Missing authentication credentials for feed ${feedId}`); + return { + feedId, + report: null, + error: 'Missing DATASTREAMS_CLIENT_ID or DATASTREAMS_CLIENT_SECRET environment variables' + }; + } + + // Generate authentication headers + const authHeaders = generateAuthHeaders(feedId); + + // Try to get the latest report from the feed + const response = await fetch(`${DATASTREAMS_BASE_URL}/api/v1/reports/latest?feedID=${feedId}`, { + method: 'GET', + headers: authHeaders, + }); + + if (!response.ok) { + if (response.status === 401) { + logger.error(`❌ Authentication failed for feed ${feedId} - check API credentials`); + return { feedId, report: null, error: 'Authentication failed - invalid API key/secret' }; + } else if (response.status === 403) { + logger.error(`❌ Access forbidden for feed ${feedId} - check API permissions`); + return { feedId, report: null, error: 'Access forbidden - insufficient permissions' }; + } else if (response.status === 404) { + logger.warn(`âš ī¸ No reports found for feed ${feedId} - feed may be inactive`); + return { feedId, report: null, error: 'No reports available' }; + } + + const errorText = await response.text(); + logger.error(`❌ Failed to fetch feed data for ${feedId}: ${response.status} ${errorText}`); + return { feedId, report: null, error: `HTTP ${response.status}: ${errorText}` }; + } + + const data = await response.json(); + + // Extract the report object from the response + const reportData = data.report; + if (!reportData) { + logger.error(`❌ No report data in API response for feed ${feedId}`); + return { feedId, report: null, error: 'No report data in API response' }; + } + + // Parse the report based on its structure + const report = parseReportFromAPI(reportData, feedId); + + if (report) { + logger.info(`✅ Successfully fetched current data for feed ${feedId}`); + return { feedId, report }; + } else { + logger.warn(`âš ī¸ Could not parse report data for feed ${feedId}`); + return { feedId, report: null, error: 'Invalid report format' }; + } + + } catch (error) { + logger.error(`❌ Error fetching feed data for ${feedId}:`, error); + return { feedId, report: null, error: error instanceof Error ? error.message : 'Unknown error' }; + } +} + +/** + * Parse report data from Chainlink API response + * Handles V3, V4, and V8 report schemas dynamically + */ +function parseReportFromAPI(apiData: any, feedId: string): ReportV3 | ReportV4 | ReportV8 | null { + try { + // Check if this is a V8 report (RWA assets) + if (apiData.reportVersion === 8 || apiData.midPrice !== undefined) { + const v8Report: ReportV8 = { + reportVersion: apiData.reportVersion || 8, + verifiedReport: apiData.verifiedReport || '0x', + feedId: feedId as `0x${string}`, + validFromTimestamp: apiData.validFromTimestamp || Math.floor(Date.now() / 1000), + observationsTimestamp: apiData.observationsTimestamp || Math.floor(Date.now() / 1000), + nativeFee: BigInt(apiData.nativeFee || 0), + linkFee: BigInt(apiData.linkFee || 0), + expiresAt: apiData.expiresAt || Math.floor(Date.now() / 1000) + 3600, + lastUpdateTimestamp: BigInt(apiData.lastUpdateTimestamp || Math.floor(Date.now() / 1000)), + midPrice: BigInt(apiData.midPrice || 0), + marketStatus: apiData.marketStatus || 1, // Default to closed if not specified + rawReport: apiData.rawReport || '0x', + parameterPayload: apiData.parameterPayload || '0x', + }; + return v8Report; + } + + // Check if this is a V4 report + if (apiData.reportVersion === 4 || apiData.marketStatus !== undefined) { + const v4Report: ReportV4 = { + reportVersion: apiData.reportVersion || 4, + verifiedReport: apiData.verifiedReport || '0x', + feedId: feedId as `0x${string}`, + validFromTimestamp: apiData.validFromTimestamp || Math.floor(Date.now() / 1000), + observationsTimestamp: apiData.observationsTimestamp || Math.floor(Date.now() / 1000), + nativeFee: BigInt(apiData.nativeFee || 0), + linkFee: BigInt(apiData.linkFee || 0), + expiresAt: apiData.expiresAt || Math.floor(Date.now() / 1000) + 3600, + price: BigInt(apiData.price || 0), + marketStatus: apiData.marketStatus || 1, + rawReport: apiData.rawReport || '0x', + parameterPayload: apiData.parameterPayload || '0x', + }; + return v4Report; + } + + // Check if this is a V3 report (crypto assets) + if (apiData.reportVersion === 3 || apiData.price !== undefined) { + const v3Report: ReportV3 = { + reportVersion: apiData.reportVersion || 3, + verifiedReport: apiData.verifiedReport || '0x', + feedId: feedId as `0x${string}`, + validFromTimestamp: apiData.validFromTimestamp || Math.floor(Date.now() / 1000), + observationsTimestamp: apiData.observationsTimestamp || Math.floor(Date.now() / 1000), + nativeFee: BigInt(apiData.nativeFee || 0), + linkFee: BigInt(apiData.linkFee || 0), + expiresAt: apiData.expiresAt || Math.floor(Date.now() / 1000) + 3600, + price: BigInt(apiData.price || 0), + bid: BigInt(apiData.bid || 0), + ask: BigInt(apiData.ask || 0), + rawReport: apiData.rawReport || '0x', + parameterPayload: apiData.parameterPayload || '0x', + }; + return v3Report; + } + + // If we can't determine the type, try to create a generic report + logger.warn(`âš ī¸ Unknown report format for feed ${feedId}, attempting generic parsing`); + + const genericReport: any = { + reportVersion: apiData.reportVersion || 8, + verifiedReport: apiData.verifiedReport || '0x', + feedId: feedId as `0x${string}`, + validFromTimestamp: apiData.validFromTimestamp || Math.floor(Date.now() / 1000), + observationsTimestamp: apiData.observationsTimestamp || Math.floor(Date.now() / 1000), + nativeFee: BigInt(apiData.nativeFee || 0), + linkFee: BigInt(apiData.linkFee || 0), + expiresAt: apiData.expiresAt || Math.floor(Date.now() / 1000) + 3600, + rawReport: apiData.fullReport || '0x', // Use fullReport as rawReport (same as WebSocket) + parameterPayload: apiData.parameterPayload || '0x', + }; + + // Add type-specific fields if available + if (apiData.midPrice !== undefined) { + genericReport.midPrice = BigInt(apiData.midPrice || 0); + genericReport.marketStatus = apiData.marketStatus || 1; + genericReport.lastUpdateTimestamp = BigInt(apiData.lastUpdateTimestamp || Math.floor(Date.now() / 1000)); + } else if (apiData.price !== undefined) { + genericReport.price = BigInt(apiData.price || 0); + if (apiData.marketStatus !== undefined) { + genericReport.marketStatus = apiData.marketStatus; + } + } + + return genericReport; + + } catch (error) { + logger.error(`❌ Error parsing report data for feed ${feedId}:`, error); + return null; + } +} + +/** + * Get market status description for logging + */ +export function getMarketStatusDescription(marketStatus: number): string { + switch (marketStatus) { + case 0: return 'unknown'; + case 1: return 'closed'; + case 2: return 'open'; + default: return `unknown (${marketStatus})`; + } +} + +/** + * Check if a feed is active and has recent data + */ +export async function isFeedActive(feedId: string): Promise { + try { + const feedData = await fetchCurrentFeedData(feedId); + if (!feedData.report) return false; + + const now = Math.floor(Date.now() / 1000); + const reportAge = now - Number(feedData.report.validFromTimestamp); + + // Consider feed active if report is less than 1 hour old + return reportAge < 3600; + } catch { + return false; + } +} \ No newline at end of file diff --git a/server/store.ts b/server/store.ts index 63d4404..82fcf4a 100644 --- a/server/store.ts +++ b/server/store.ts @@ -11,7 +11,6 @@ import { setList, setValue, } from './services/redis'; -import { getReportPrice } from '../app/lib/utils'; import { logger } from './services/logger'; import { CronExpressionParser } from 'cron-parser'; import { isValidSolanaId, printError } from './utils'; @@ -31,8 +30,32 @@ const removeLatestReport = (feedId: string) => { }; const getSavedReportBenchmarkPrice = async (feedId: string) => await getValue(`price:${feedId}`); -const setSavedReport = async (report: StreamReport) => - await setValue(`price:${report.feedId}`, getReportPrice(report).toString()); +const setSavedReport = async (report: StreamReport) => { + // Convert BigInt values to strings for JSON serialization + const serializableReport = JSON.parse(JSON.stringify(report, (key, value) => { + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + })); + + // Store the complete report for access to all fields including market status + await setValue(`savedReport:${report.feedId}`, JSON.stringify(serializableReport)); + + // Also store the price separately for backward compatibility + let price: bigint; + if ('midPrice' in report && report.midPrice !== undefined) { + // V8 report + price = report.midPrice; + } else if ('price' in report && report.price !== undefined) { + // V3/V4 report + price = report.price; + } else { + price = 0n; + } + + await setValue(`price:${report.feedId}`, price.toString()); +}; const getFeeds = async () => await getSet('feeds'); const getFeedName = async (feedId: string) => await getValue(`name:${feedId}`); const addFeed = async (feed: { feedId: string; name: string }) => { @@ -419,6 +442,74 @@ const seedConfig = async (config: Config) => { ); } + // Handle market status monitoring mode configuration + if (config.marketStatusMode) { + await setMarketStatusMode(JSON.stringify(config.marketStatusMode)); + logger.info( + `đŸ“ĸ Market status monitoring mode has been set: ${config.marketStatusMode.enabled ? 'enabled' : 'disabled'}`, + { + marketStatusMode: config.marketStatusMode, + } + ); + } + + // Check if global startup sync is enabled (independent of monitoring mode) + if (config.startup?.syncOnStartup) { + logger.info('🔄 Global startup sync is enabled - executing immediately with REST API'); + + // Execute startup sync immediately for each configured feed + let successfulSyncs = 0; + let skippedSyncs = 0; + + if (config.feeds && config.feeds.length > 0) { + for (const feed of config.feeds) { + try { + logger.info(`📊 Executing startup sync for feed ${feed.feedId} (${feed.name}) via REST API`); + + // Import and execute dataUpdater with current market data from REST API + const { dataUpdater } = await import('./services/client.js'); + const { fetchCurrentFeedData, getMarketStatusDescription } = await import('./services/rest-api.js'); + + // Fetch current market data from Chainlink's REST API + const feedData = await fetchCurrentFeedData(feed.feedId); + + if (feedData.report) { + // Check if report has market status (V4 or V8) + const marketStatus = 'marketStatus' in feedData.report ? feedData.report.marketStatus : 1; + logger.info(`📊 Current market status: ${getMarketStatusDescription(marketStatus)} for feed ${feed.feedId}`); + + // Store the fetched report so cron jobs can use it later + await setLatestReport(feedData.report as any); + logger.info(`💾 Stored current report for feed ${feed.feedId} - cron jobs will use this data`); + + // Execute startup sync with real current data immediately + await dataUpdater({ report: feedData.report as any }); + logger.info(`✅ Startup sync completed for feed ${feed.feedId} with real data`); + successfulSyncs++; + } else { + logger.warn(`âš ī¸ No current data available for feed ${feed.feedId}: ${feedData.error}`); + logger.info(`â­ī¸ Skipping startup sync for feed ${feed.feedId} - will wait for WebSocket reports`); + skippedSyncs++; + continue; // Skip this feed and continue with the next one + } + + } catch (error) { + logger.error(`❌ Startup sync failed for feed ${feed.feedId}:`, error); + } + } + } else { + logger.warn('âš ī¸ No feeds configured - startup sync skipped'); + } + + // Mark startup sync as completed + await setValue('startupSync', 'completed'); + if (config.feeds && config.feeds.length > 0) { + logger.info(`đŸŽ¯ Startup sync summary: ${successfulSyncs} feeds synced successfully, ${skippedSyncs} feeds skipped (waiting for WebSocket reports)`); + } else { + logger.info('đŸŽ¯ Startup sync completed - no feeds configured'); + } + } + if (config.svm) { config.svm.chains && (await Promise.all( @@ -585,6 +676,23 @@ const seedConfig = async (config: Config) => { } }; +// Market status and price utility functions +const getMarketStatusMode = async () => await getValue('marketStatusMode'); + +const setMarketStatusMode = async (marketStatusMode: string) => await setValue('marketStatusMode', marketStatusMode); + +const getSavedReportMarketStatus = async (feedId: string) => { + const savedReport = await getValue(`savedReport:${feedId}`); + if (!savedReport) return null; + + try { + const report = JSON.parse(savedReport); + return report.marketStatus ? Number(report.marketStatus) : null; + } catch { + return null; + } +}; + export { getFunctionName, setFunctionName, @@ -642,4 +750,7 @@ export { setInstructionArgs, getInstructionPDA, setInstructionPDA, + getMarketStatusMode, + setMarketStatusMode, + getSavedReportMarketStatus, }; diff --git a/server/types.ts b/server/types.ts index 53d2747..d1bbfcf 100644 --- a/server/types.ts +++ b/server/types.ts @@ -7,8 +7,9 @@ export type StreamReport = Report & { nativeFee: bigint; linkFee: bigint; expiresAt: bigint; - bid: bigint; - ask: bigint; + lastUpdateTimestamp: bigint; + midPrice: bigint; + marketStatus: bigint; rawReport: Hex; parameterPayload?: Hex; }; @@ -44,6 +45,22 @@ export type ReportV4 = { parameterPayload?: Hex; }; +export type ReportV8 = { + reportVersion: number; + verifiedReport: Hex; + feedId: Hex; + validFromTimestamp: number; + observationsTimestamp: number; + nativeFee: bigint; + linkFee: bigint; + expiresAt: number; + lastUpdateTimestamp: bigint; + midPrice: bigint; + marketStatus: number; + rawReport: Hex; + parameterPayload?: Hex; +}; + export type Feed = { name: string; feedId: string }; export type Interval = { interval: string }; @@ -77,6 +94,16 @@ export type Config = { gasCap: string; interval: string; priceDeltaPercentage: number | string; + // Global startup configuration + startup?: { + syncOnStartup?: boolean; // Enable startup sync for all monitoring modes + }; + // New: Market status monitoring mode + marketStatusMode?: { + enabled: boolean; + triggerOnMarketOpen?: boolean; // Trigger when market opens (status 2) + triggerOnMarketClose?: boolean; // Trigger when market closes (status 1) + }; vm?: 'evm' | 'svm'; svm?: { cluster?: string; From f05c1aa65ac73b22f788832a0bd8b9af20b37237 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 06:18:54 +0000 Subject: [PATCH 2/2] chore(deps-dev): bump vite from 5.4.18 to 5.4.20 Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.18 to 5.4.20. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.4.20/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.4.20/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 5.4.20 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ecb5aa0..0c47b13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "tailwindcss": "^3.4.4", "tsx": "^4.19.2", "typescript": "^5.1.6", - "vite": "^5.1.0", + "vite": "^5.4.20", "vite-tsconfig-paths": "^4.2.1" }, "engines": { @@ -17029,9 +17029,9 @@ } }, "node_modules/vite": { - "version": "5.4.18", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", - "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 71f73ab..b5b54fd 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "tailwindcss": "^3.4.4", "tsx": "^4.19.2", "typescript": "^5.1.6", - "vite": "^5.1.0", + "vite": "^5.4.20", "vite-tsconfig-paths": "^4.2.1" }, "engines": {