Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
111 changes: 14 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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

Expand Down
21 changes: 15 additions & 6 deletions app/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
106 changes: 100 additions & 6 deletions app/routes/_index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getInterval,
getPriceDelta,
getSavedReportBenchmarkPrice,
getMarketStatusMode,
} from 'server/store';
import { formatUSD } from 'server/utils';
import { formatEther } from 'viem';
Expand Down Expand Up @@ -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(
Expand 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<typeof loader>();

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

// 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') {
Expand Down Expand Up @@ -115,8 +121,9 @@ export default function Index() {
<TableHead>Feed ID</TableHead>
<TableHead>Report Schema</TableHead>
<TableHead>Contract</TableHead>
<TableHead>Saved price</TableHead>
<TableHead>Last reported</TableHead>
<TableHead>Saved Price</TableHead>
<TableHead>Last Reported</TableHead>
<TableHead>Market Status</TableHead>
<TableHead>Status</TableHead>
<TableHead>Remove</TableHead>
</TableRow>
Expand Down Expand Up @@ -151,6 +158,9 @@ export default function Index() {
<TableCell>
{formatUSD(BigInt(feed.latestReport ?? 0))}
</TableCell>
<TableCell>
<MarketStatusIndicator feedId={feed.feedId} />
</TableCell>
<TableCell>
<Status status={feed.status} />
</TableCell>
Expand Down Expand Up @@ -310,6 +320,54 @@ export default function Index() {
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Market Status Monitoring</CardTitle>
<CardDescription>
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.
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center space-x-2">
<div className="w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center">
{isMarketStatusMode ? (
<div className="w-2 h-2 rounded-full bg-green-500"></div>
) : (
<div className="w-2 h-2 rounded-full bg-gray-400"></div>
)}
</div>
<span className="text-sm font-medium">
{isMarketStatusMode ? 'Enabled' : 'Disabled'}
</span>
</div>

{isMarketStatusMode && marketStatusConfig && (
<div className="text-sm text-muted-foreground space-y-1">
<p>• Triggers on market open: {marketStatusConfig.triggerOnMarketOpen ? 'Yes' : 'No'}</p>
<p>• Triggers on market close: {marketStatusConfig.triggerOnMarketClose ? 'Yes' : 'No'}</p>
</div>
)}

<p className="text-xs text-muted-foreground">
{isMarketStatusMode
? 'Currently monitoring market status changes. Price delta monitoring is disabled.'
: 'Currently using price delta monitoring. Market status monitoring is disabled.'
}
</p>
</div>
</CardContent>
<CardFooter>
<Link
to="/market-status/config"
className={cn(buttonVariants({ variant: 'outline' }), 'w-fit')}
>
<Pencil /> Configure Market Status Mode
</Link>
</CardFooter>
</Card>
<Card>
<CardHeader>
<CardTitle>Gas cap</CardTitle>
Expand Down Expand Up @@ -345,6 +403,42 @@ export default function Index() {
);
}

function MarketStatusIndicator({ feedId }: { feedId: string }) {
const schemaVersion = detectSchemaVersion(feedId);

if (schemaVersion === 'v8') {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<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">
V8
</span>
</TooltipTrigger>
<TooltipContent>
<p>V8 Report Schema - Supports market status monitoring</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<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">
{schemaVersion}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{schemaVersion} Report Schema - Traditional price monitoring</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

function Status({ status }: { status?: number | string }) {
if (status === 0)
return (
Expand Down
Loading