Skip to content

Commit ad973a7

Browse files
feat: comprehensive V8 support with startup sync and market status monitoring
1 parent d2c9e24 commit ad973a7

File tree

16 files changed

+1714
-174
lines changed

16 files changed

+1714
-174
lines changed

Dockerfile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ FROM base as deps
1212

1313
WORKDIR /push-engine
1414

15-
ADD package.json ./
15+
# Install git for GitHub dependencies
16+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
17+
18+
# Copy Docker-specific package.json (without macOS native dependencies)
19+
COPY package-docker.json ./package.json
1620
RUN npm install --include=dev
1721

1822
# Setup production node_modules
@@ -21,8 +25,11 @@ FROM base as production-deps
2125

2226
WORKDIR /push-engine
2327

28+
# Install git for GitHub dependencies in production stage too
29+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
30+
2431
COPY --from=deps /push-engine/node_modules /push-engine/node_modules
25-
ADD package.json ./
32+
COPY package-docker.json ./package.json
2633
RUN npm prune --omit=dev
2734

2835
# Build the app

README.md

Lines changed: 14 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Push Engine is a service that bridges off-chain data streams with on-chain smart
1818

1919
- Retrieves and verifies price data from Chainlink Data Streams.
2020
- Writes verified prices to on-chain smart contracts.
21+
- **Dual monitoring modes**: Price delta or market status monitoring.
22+
- **V8 report support**: Full support for latest Chainlink Data Streams schema.
2123
- Supports containerized deployment with Docker.
2224
- Configurable through environment variables and Redis-based settings.
2325

@@ -117,7 +119,7 @@ Before setting up the , ensure you have the required dependencies installed.
117119
3. Update the `.env` file with your credential details (explained below) and optionally provide a `config.yml` file following the examples below.
118120

119121
> [!TIP]
120-
> 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.
122+
> 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`.
121123
122124
4. Install dependencies:
123125
```sh
@@ -153,7 +155,7 @@ To make setting environment variables easier there is a `.env.example` file in t
153155
> [!NOTE]
154156
> 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.
155157
>
156-
> 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.
158+
> 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.
157159
158160
---
159161

@@ -230,101 +232,16 @@ targetChains:
230232
231233
### Example YAML Configuration
232234
233-
```yaml
234-
feeds:
235-
- name: 'AVAX/USD'
236-
feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796'
237-
- name: 'ETH/USD'
238-
feedId: '0x000359843a543ee2fe414dc14c7e7920ef10f4372990b79d6361cdc0dd1ba782'
239-
chainId: 43113
240-
gasCap: '150000'
241-
interval: '*/30 * * * * *'
242-
priceDeltaPercentage: 0.01
243-
chains:
244-
- id: 995
245-
name: '🔥 5ireChain'
246-
currencyName: '5ire Token'
247-
currencySymbol: '5IRE'
248-
currencyDecimals: 18
249-
rpc: 'https://rpc.5ire.network'
250-
- id: 84532
251-
name: 'Base Sepolia Custom'
252-
currencyName: 'Sepolia Ether'
253-
currencySymbol: 'ETH'
254-
currencyDecimals: 18
255-
rpc: 'https://sepolia.base.org'
256-
testnet: true
257-
verifierAddresses:
258-
- chainId: 995
259-
address: '0x...'
260-
- chainId: 84532
261-
address: '0x...'
262-
targetChains:
263-
- chainId: 43113
264-
targetContracts:
265-
- feedId: '0x0003735a076086936550bd316b18e5e27fc4f280ee5b6530ce68f5aad404c796'
266-
address: '0xfa162F0A25b2C2aA32Ddaacda872B6D7b2c38E47'
267-
functionName: 'set'
268-
functionArgs:
269-
- 'feedId'
270-
- 'validFromTimestamp'
271-
- 'observationsTimestamp'
272-
- 'nativeFee'
273-
- 'linkFee'
274-
- 'expiresAt'
275-
- 'price'
276-
- 'bid'
277-
- 'ask'
278-
abi:
279-
[
280-
{
281-
'inputs':
282-
[
283-
{
284-
'internalType': 'bytes32',
285-
'name': 'feedId',
286-
'type': 'bytes32',
287-
},
288-
{
289-
'internalType': 'uint32',
290-
'name': 'validFromTimestamp',
291-
'type': 'uint32',
292-
},
293-
{
294-
'internalType': 'uint32',
295-
'name': 'observationsTimestamp',
296-
'type': 'uint32',
297-
},
298-
{
299-
'internalType': 'uint192',
300-
'name': 'nativeFee',
301-
'type': 'uint192',
302-
},
303-
{
304-
'internalType': 'uint192',
305-
'name': 'linkFee',
306-
'type': 'uint192',
307-
},
308-
{
309-
'internalType': 'uint32',
310-
'name': 'expiresAt',
311-
'type': 'uint32',
312-
},
313-
{
314-
'internalType': 'int192',
315-
'name': 'price',
316-
'type': 'int192',
317-
},
318-
{ 'internalType': 'int192', 'name': 'bid', 'type': 'int192' },
319-
{ 'internalType': 'int192', 'name': 'ask', 'type': 'int192' },
320-
],
321-
'name': 'set',
322-
'outputs': [],
323-
'stateMutability': 'nonpayable',
324-
'type': 'function',
325-
},
326-
]
327-
```
235+
A comprehensive example configuration file is provided at `config-v8-example.yml` that shows all available options including:
236+
237+
- **Price Delta Monitoring**: Traditional mode that triggers on price changes
238+
- **Market Status Monitoring**: Mode that triggers on market open/close events
239+
- **V3/V4 Schema**: Individual field extraction for bid/ask spreads
240+
- **V8 Schema**: Raw report handling for advanced use cases
241+
- **EVM and SVM**: Support for both Ethereum and Solana chains
242+
- **Startup Sync**: Immediate data synchronization on bot startup using REST API for any report type
243+
244+
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`.
328245

329246
### Key Configuration Parameters
330247

app/lib/utils.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,18 @@ export function detectSchemaVersion(feedId: string) {
1919
return `v${parseInt(firstTwoBytesHex, 16)}`;
2020
}
2121

22-
export const getReportPrice = (report?: StreamReport) =>
23-
report?.version === 'v3'
24-
? report.benchmarkPrice
25-
: report?.version === 'v4'
26-
? report.price
27-
: BigInt(0);
22+
export const getReportPrice = (report?: StreamReport) => {
23+
if (!report) return BigInt(0);
24+
25+
// Check if it's a V8 report (has midPrice field)
26+
if ('midPrice' in report && report.midPrice !== undefined) {
27+
return report.midPrice;
28+
}
29+
30+
// Check if it's a V3/V4 report (has price field)
31+
if ('price' in report && report.price !== undefined) {
32+
return report.price;
33+
}
34+
35+
return BigInt(0);
36+
};

app/routes/_index.tsx

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getInterval,
1010
getPriceDelta,
1111
getSavedReportBenchmarkPrice,
12+
getMarketStatusMode,
1213
} from 'server/store';
1314
import { formatUSD } from 'server/utils';
1415
import { formatEther } from 'viem';
@@ -38,10 +39,10 @@ import {
3839
TooltipProvider,
3940
TooltipTrigger,
4041
} from '~/components/ui/tooltip';
41-
import { cn, detectSchemaVersion } from '~/lib/utils';
42+
import { cn, detectSchemaVersion, getReportPrice } from '~/lib/utils';
4243

4344
export async function loader() {
44-
const [feeds, interval, priceDelta, gasCap] = await Promise.all([
45+
const [feeds, interval, priceDelta, gasCap, marketStatusMode] = await Promise.all([
4546
(async function () {
4647
const feedsIds = await getFeeds();
4748
return await Promise.all(
@@ -57,17 +58,22 @@ export async function loader() {
5758
getInterval(),
5859
getPriceDelta(),
5960
getGasCap(),
61+
getMarketStatusMode(),
6062
]);
61-
return { feeds, interval, priceDelta, gasCap };
63+
return { feeds, interval, priceDelta, gasCap, marketStatusMode };
6264
}
6365

6466
export default function Index() {
65-
const { feeds, interval, priceDelta, gasCap } =
67+
const { feeds, interval, priceDelta, gasCap, marketStatusMode } =
6668
useLoaderData<typeof loader>();
6769

6870
const revalidator = useRevalidator();
6971
const [nextThree, setNextThree] = useState<string[]>([]);
7072

73+
// Parse market status mode configuration
74+
const marketStatusConfig = marketStatusMode ? JSON.parse(marketStatusMode) : null;
75+
const isMarketStatusMode = marketStatusConfig?.enabled || false;
76+
7177
useEffect(() => {
7278
const intervalId = setInterval(() => {
7379
if (revalidator.state === 'idle') {
@@ -115,8 +121,9 @@ export default function Index() {
115121
<TableHead>Feed ID</TableHead>
116122
<TableHead>Report Schema</TableHead>
117123
<TableHead>Contract</TableHead>
118-
<TableHead>Saved price</TableHead>
119-
<TableHead>Last reported</TableHead>
124+
<TableHead>Saved Price</TableHead>
125+
<TableHead>Last Reported</TableHead>
126+
<TableHead>Market Status</TableHead>
120127
<TableHead>Status</TableHead>
121128
<TableHead>Remove</TableHead>
122129
</TableRow>
@@ -151,6 +158,9 @@ export default function Index() {
151158
<TableCell>
152159
{formatUSD(BigInt(feed.latestReport ?? 0))}
153160
</TableCell>
161+
<TableCell>
162+
<MarketStatusIndicator feedId={feed.feedId} />
163+
</TableCell>
154164
<TableCell>
155165
<Status status={feed.status} />
156166
</TableCell>
@@ -310,6 +320,54 @@ export default function Index() {
310320
</Form>
311321
</CardContent>
312322
</Card>
323+
<Card>
324+
<CardHeader>
325+
<CardTitle>Market Status Monitoring</CardTitle>
326+
<CardDescription>
327+
Configure market status monitoring mode. When enabled, the bot will
328+
process reports based on market status changes (open/close) instead
329+
of price changes. This mode is exclusive with price delta monitoring.
330+
</CardDescription>
331+
</CardHeader>
332+
<CardContent>
333+
<div className="space-y-4">
334+
<div className="flex items-center space-x-2">
335+
<div className="w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center">
336+
{isMarketStatusMode ? (
337+
<div className="w-2 h-2 rounded-full bg-green-500"></div>
338+
) : (
339+
<div className="w-2 h-2 rounded-full bg-gray-400"></div>
340+
)}
341+
</div>
342+
<span className="text-sm font-medium">
343+
{isMarketStatusMode ? 'Enabled' : 'Disabled'}
344+
</span>
345+
</div>
346+
347+
{isMarketStatusMode && marketStatusConfig && (
348+
<div className="text-sm text-muted-foreground space-y-1">
349+
<p>• Triggers on market open: {marketStatusConfig.triggerOnMarketOpen ? 'Yes' : 'No'}</p>
350+
<p>• Triggers on market close: {marketStatusConfig.triggerOnMarketClose ? 'Yes' : 'No'}</p>
351+
</div>
352+
)}
353+
354+
<p className="text-xs text-muted-foreground">
355+
{isMarketStatusMode
356+
? 'Currently monitoring market status changes. Price delta monitoring is disabled.'
357+
: 'Currently using price delta monitoring. Market status monitoring is disabled.'
358+
}
359+
</p>
360+
</div>
361+
</CardContent>
362+
<CardFooter>
363+
<Link
364+
to="/market-status/config"
365+
className={cn(buttonVariants({ variant: 'outline' }), 'w-fit')}
366+
>
367+
<Pencil /> Configure Market Status Mode
368+
</Link>
369+
</CardFooter>
370+
</Card>
313371
<Card>
314372
<CardHeader>
315373
<CardTitle>Gas cap</CardTitle>
@@ -345,6 +403,42 @@ export default function Index() {
345403
);
346404
}
347405

406+
function MarketStatusIndicator({ feedId }: { feedId: string }) {
407+
const schemaVersion = detectSchemaVersion(feedId);
408+
409+
if (schemaVersion === 'v8') {
410+
return (
411+
<TooltipProvider>
412+
<Tooltip>
413+
<TooltipTrigger>
414+
<span className="inline-flex items-center rounded-md bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 ring-1 ring-blue-600/20 ring-inset">
415+
V8
416+
</span>
417+
</TooltipTrigger>
418+
<TooltipContent>
419+
<p>V8 Report Schema - Supports market status monitoring</p>
420+
</TooltipContent>
421+
</Tooltip>
422+
</TooltipProvider>
423+
);
424+
}
425+
426+
return (
427+
<TooltipProvider>
428+
<Tooltip>
429+
<TooltipTrigger>
430+
<span className="inline-flex items-center rounded-md bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700 ring-1 ring-gray-600/20 ring-inset">
431+
{schemaVersion}
432+
</span>
433+
</TooltipTrigger>
434+
<TooltipContent>
435+
<p>{schemaVersion} Report Schema - Traditional price monitoring</p>
436+
</TooltipContent>
437+
</Tooltip>
438+
</TooltipProvider>
439+
);
440+
}
441+
348442
function Status({ status }: { status?: number | string }) {
349443
if (status === 0)
350444
return (

0 commit comments

Comments
 (0)