Skip to content

Commit

Permalink
feat(pyth): add Pyth Data plugin
Browse files Browse the repository at this point in the history
- Create new plugin for Pyth Data integration
- Add EventSource client for price feeds
- Configure plugin in agent
- Update dependencies and configuration files

This plugin enables real-time price feed data from Pyth Network,
including price updates, TWAPs, and publisher caps information.
  • Loading branch information
AIFlowML committed Jan 16, 2025
1 parent d55c86c commit d9e1090
Show file tree
Hide file tree
Showing 39 changed files with 6,718 additions and 3 deletions.
2 changes: 2 additions & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"@elizaos/plugin-hyperliquid": "workspace:*",
"@elizaos/plugin-akash": "workspace:*",
"@elizaos/plugin-quai": "workspace:*",
"@elizaos/plugin-pyth-data": "workspace:*",

"readline": "1.3.0",
"ws": "8.18.0",
"yargs": "17.7.2"
Expand Down
5 changes: 3 additions & 2 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import { zgPlugin } from "@elizaos/plugin-0g";
import { bootstrapPlugin } from "@elizaos/plugin-bootstrap";
import createGoatPlugin from "@elizaos/plugin-goat";
// import { intifacePlugin } from "@elizaos/plugin-intiface";
import { DirectClient } from "@elizaos/client-direct";
import { ThreeDGenerationPlugin } from "@elizaos/plugin-3d-generation";
import { abstractPlugin } from "@elizaos/plugin-abstract";
import { alloraPlugin } from "@elizaos/plugin-allora";
Expand Down Expand Up @@ -96,13 +95,14 @@ import { openWeatherPlugin } from "@elizaos/plugin-open-weather";
import { stargazePlugin } from "@elizaos/plugin-stargaze";
import { akashPlugin } from "@elizaos/plugin-akash";
import { quaiPlugin } from "@elizaos/plugin-quai";
import { pythDataPlugin } from "@elizaos/plugin-pyth-data";
import Database from "better-sqlite3";
import fs from "fs";
import net from "net";
import path from "path";
import { fileURLToPath } from "url";
import yargs from "yargs";
import {dominosPlugin} from "@elizaos/plugin-dominos";


const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename); // get the name of the directory
Expand Down Expand Up @@ -854,6 +854,7 @@ export async function createAgent(
getSecret(character, "QUAI_PRIVATE_KEY")
? quaiPlugin
: null,
getSecret(character, "PYTH_GRANULAR_LOG") ? pythDataPlugin : null,
].filter(Boolean),
providers: [],
actions: [],
Expand Down
2 changes: 1 addition & 1 deletion characters/dobby.character.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Dobby",
"clients": [],
"modelProvider": "anthropic",
"modelProvider": "openai",
"settings": {
"voice": {
"model": "en_GB-danny-low"
Expand Down
Binary file added packages/plugin-pyth-data/assets/pyth-data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions packages/plugin-pyth-data/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [
...eslintGlobalConfig,
{
files: ["src/**/*.ts"],
rules: {
// Disable problematic rules
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
},
},
];
58 changes: 58 additions & 0 deletions packages/plugin-pyth-data/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@elizaos/plugin-pyth-data",
"version": "1.0.0",
"description": "Pyth Network data plugin for Eliza",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup --format esm --dts",
"test": "vitest",
"lint": "eslint src --ext .ts",
"clean": "rimraf dist",
"build:schemas": "openapi-zod-client ./schema.json --output src/types/zodSchemas.ts",
"pull:schema": "curl -o schema.json -z schema.json https://hermes.pyth.network/docs/openapi.json",
"prebuild": "pnpm run pull:schema && pnpm run build:schemas"
},
"dependencies": {
"@elizaos/core": "^0.1.7",
"@pythnetwork/client": "^2.22.0",
"@solana/web3.js": "^1.98.0",
"@zodios/core": "^10.9.6",
"ajv": "^8.12.0",
"buffer": "6.0.3",
"chalk": "^5.4.1",
"cli-table3": "^0.6.5",
"cross-fetch": "^4.0.0",
"eventsource": "^3.0.2",
"jstat": "^1.9.6",
"ora": "^8.1.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.8.2",
"@typescript-eslint/eslint-plugin": "^6.7.4",
"@typescript-eslint/parser": "^6.7.4",
"eslint": "^8.50.0",
"openapi-zod-client": "^1.18.1",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.2.2",
"vitest": "^1.0.0"
},
"peerDependencies": {
"@elizaos/core": "^0.1.7"
},
"engines": {
"node": ">=16.0.0"
},
"keywords": [
"eliza",
"plugin",
"pyth",
"oracle",
"price-feed"
],
"author": "Eliza Team",
"license": "MIT"
}
1 change: 1 addition & 0 deletions packages/plugin-pyth-data/schema.json

Large diffs are not rendered by default.

252 changes: 252 additions & 0 deletions packages/plugin-pyth-data/src/actions/actionGetLatestPriceUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import { Action, elizaLogger } from "@elizaos/core";
import { IAgentRuntime, Memory, State, HandlerCallback, Content, ActionExample } from "@elizaos/core";
import { HermesClient } from "../hermes/HermesClient";
import { DataError, ErrorSeverity, DataErrorCode } from "../error";
import { validatePythConfig, getNetworkConfig, getConfig } from "../environment";
import { ValidationSchemas } from "../types/types";
import { validateSchema } from "../utils/validation";
import { schemas } from "../types/zodSchemas";
import { z } from "zod";

// Get configuration for granular logging
const config = getConfig();
const GRANULAR_LOG = config.PYTH_GRANULAR_LOG;

// Enhanced logging helper
const logGranular = (message: string, data?: unknown) => {
if (GRANULAR_LOG) {
elizaLogger.info(`[PriceUpdates] ${message}`, data);
console.log(`[PriceUpdates] ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
};

interface GetLatestPriceUpdatesContent extends Content {
text: string;
priceIds: string[];
options?: {
encoding?: "hex" | "base64";
parsed?: boolean;
};
success?: boolean;
data?: {
updates?: Array<{
price_feed_id: string;
price: number;
conf: number;
expo: number;
publish_time: number;
ema_price?: {
price: number;
conf: number;
expo: number;
};
}>;
error?: string;
};
}

export const getLatestPriceUpdatesAction: Action = {
name: "GET_LATEST_PRICE_UPDATES",
similes: ["FETCH_LATEST_PRICES", "GET_CURRENT_PRICES", "CHECK_PRICE_FEED"],
description: "Retrieve latest price updates from Pyth Network",
examples: [[
{
user: "user",
content: {
text: "Get latest BTC/USD price updates",
priceIds: ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"],
options: {
encoding: "base64",
parsed: true
}
} as GetLatestPriceUpdatesContent
} as ActionExample,
{
user: "assistant",
content: {
text: "Here is the latest BTC/USD price",
success: true,
priceIds: ["ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"],
data: {
updates: [{
price_feed_id: "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
price: 42000000000,
conf: 100000000,
expo: -8,
publish_time: 1641034800,
ema_price: {
price: 41950000000,
conf: 95000000,
expo: -8
}
}]
}
} as GetLatestPriceUpdatesContent
} as ActionExample
]],

validate: async (_runtime: IAgentRuntime, message: Memory): Promise<boolean> => {
logGranular("Validating GET_LATEST_PRICE_UPDATES action", {
content: message.content
});

try {
const content = message.content as GetLatestPriceUpdatesContent;

// Validate against schema
try {
await validateSchema(content, ValidationSchemas.GET_LATEST_PRICE);
logGranular("Schema validation passed");
} catch (error) {
logGranular("Schema validation failed", { error });
if (error instanceof DataError) {
elizaLogger.error("Schema validation failed", {
errors: error.details?.errors
});
throw error;
}
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
"Schema validation failed",
ErrorSeverity.HIGH,
{ error }
);
}

// Validate priceIds array
if (!content.priceIds || !Array.isArray(content.priceIds)) {
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
"priceIds must be an array of strings",
ErrorSeverity.HIGH
);
}

if (content.priceIds.length === 0) {
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
"priceIds array cannot be empty",
ErrorSeverity.HIGH
);
}

// Validate each price ID is a valid hex string
content.priceIds.forEach((id, index) => {
if (!/^[0-9a-fA-F]+$/.test(id)) {
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
`Invalid price ID at index ${index}: ${id}`,
ErrorSeverity.HIGH
);
}
});

return true;
} catch (error) {
logGranular("Validation failed", { error });
elizaLogger.error("Validation failed for GET_LATEST_PRICE_UPDATES", {
error: error instanceof Error ? error.message : String(error)
});
return false;
}
},

handler: async (
runtime: IAgentRuntime,
message: Memory,
_state?: State,
_options: { [key: string]: unknown } = {},
callback?: HandlerCallback
): Promise<boolean> => {
logGranular("Executing GET_LATEST_PRICE_UPDATES action");

try {
const messageContent = message.content as GetLatestPriceUpdatesContent;
const { priceIds, options = {} } = messageContent;

// Get Pyth configuration
const config = await validatePythConfig(runtime);
if (!config) {
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
"Invalid Pyth configuration",
ErrorSeverity.HIGH
);
}

// Get network configuration
const networkConfig = getNetworkConfig(config.PYTH_NETWORK_ENV);

// Initialize Hermes client
const hermesClient = new HermesClient(networkConfig.hermes);

logGranular("Initialized HermesClient", {
endpoint: networkConfig.hermes
});

try {
// Get latest price updates
const updates = await hermesClient.getLatestPriceUpdates(priceIds, {
parsed: true,
encoding: options?.encoding as "hex" | "base64" | undefined
});

logGranular("Successfully retrieved price updates", {
count: updates.parsed?.length || 0
});

// Create callback content
const callbackContent: GetLatestPriceUpdatesContent = {
text: `Retrieved ${updates.parsed?.length || 0} price updates`,
success: true,
priceIds,
data: {
updates: (updates as z.infer<typeof schemas.PriceUpdate>).parsed?.map((update) => ({
price_feed_id: update.id,
price: Number(update.price.price),
conf: Number(update.price.conf),
expo: Number(update.price.expo),
publish_time: update.price.publish_time,
ema_price: update.ema_price ? {
price: Number(update.ema_price.price),
conf: Number(update.ema_price.conf),
expo: Number(update.ema_price.expo)
} : undefined
})) || []
}
};

// Call callback with results
if (callback) {
await callback(callbackContent);
}

return true;
} catch (error) {
logGranular("Failed to process price updates request", { error });
if (error instanceof DataError) {
throw error;
}
throw new DataError(
DataErrorCode.VALIDATION_FAILED,
"Failed to process price updates request",
ErrorSeverity.HIGH,
{ originalError: error }
);
}
} catch (error) {
logGranular("Failed to get latest price updates", { error });
if (error instanceof DataError) {
throw error;
}
throw new DataError(
DataErrorCode.NETWORK_ERROR,
"Failed to get latest price updates",
ErrorSeverity.HIGH,
{ originalError: error }
);
}
}
};

export default getLatestPriceUpdatesAction;
Loading

0 comments on commit d9e1090

Please sign in to comment.