Skip to content

Commit b443c30

Browse files
committed
feat(evm/sdk/js): add pyth filler
1 parent 637b6cc commit b443c30

File tree

10 files changed

+1379
-359
lines changed

10 files changed

+1379
-359
lines changed

pnpm-lock.yaml

Lines changed: 89 additions & 346 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Pyth EVM JS
2+
3+
[Pyth](https://pyth.network/) provides real-time pricing data in a variety of asset classes, including cryptocurrency,
4+
equities, FX and commodities. This library allows you to use these real-time prices on EVM-based networks.
5+
6+
## Installation
7+
8+
### npm
9+
10+
```
11+
$ npm install --save @pythnetwork/pyth-evm-js
12+
```
13+
14+
### Yarn
15+
16+
```
17+
$ yarn add @pythnetwork/pyth-evm-js
18+
```
19+
20+
## Quickstart
21+
22+
### Filling Pyth Data for Transactions
23+
24+
The `fillPythUpdate` function helps you automatically determine what Pyth price updates are needed for a transaction and creates the necessary update call.
25+
This function uses the `trace_callMany` method by default but can be used with `debug_traceCall` and a bundler as well. See the example below for more information.
26+
27+
```typescript
28+
import { fillPythUpdate, multicall3Bundler } from "@pythnetwork/pyth-evm-js";
29+
import { createPublicClient, http } from "viem";
30+
import { optimismSepolia } from "viem/chains";
31+
32+
async function example() {
33+
const client = createPublicClient({
34+
chain: optimismSepolia,
35+
transport: http("YOUR_RPC_ENDPOINT"),
36+
});
37+
38+
// Fill Pyth update data using "trace_callMany"
39+
const pythUpdate = await fillPythUpdate(
40+
client,
41+
{
42+
to: "0x3252c2F7962689fA17f892C52555613f36056f22",
43+
data: "0xd09de08a", // Your transaction calldata
44+
from: "0x78357316239040e19fC823372cC179ca75e64b81",
45+
},
46+
"0x0708325268df9f66270f1401206434524814508b", // Pyth contract address
47+
"https://hermes.pyth.network", // Hermes endpoint
48+
{
49+
method: "trace_callMany"
50+
maxIter: 5,
51+
},
52+
);
53+
54+
// Fill Pyth update data using "debug_traceCall"
55+
const pythUpdateWithDebugTrace = await fillPythUpdate(
56+
client,
57+
{
58+
to: "0x3252c2F7962689fA17f892C52555613f36056f22",
59+
data: "0xd09de08a", // Your transaction calldata
60+
from: "0x78357316239040e19fC823372cC179ca75e64b81",
61+
},
62+
"0x0708325268df9f66270f1401206434524814508b", // Pyth contract address
63+
"https://hermes.pyth.network", // Hermes endpoint
64+
{
65+
method: "debug_traceCall",
66+
bundler: multicall3Bundler // or any function that takes a PythUpdate and a CallRequest and produces a CallRequest
67+
maxIter: 5,
68+
},
69+
);
70+
71+
if (pythUpdate) {
72+
console.log("Pyth update needed:", pythUpdate);
73+
// Bundle the calls together, or pass the pythUpdate.updateData to your contract.
74+
} else {
75+
console.log("No Pyth data needed for this transaction");
76+
}
77+
}
78+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { base as default } from "@cprussin/eslint-config";
Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/pyth-evm-js",
3-
"version": "1.83.0",
3+
"version": "2.0.0-alpha",
44
"description": "Pyth Network EVM Utils in JS",
55
"homepage": "https://pyth.network",
66
"author": {
@@ -21,42 +21,37 @@
2121
},
2222
"scripts": {
2323
"build": "tsc",
24-
"example-client": "pnpm run build && node lib/examples/EvmPriceServiceClient.js",
25-
"example-relay": "pnpm run build && node lib/examples/EvmRelay.js",
26-
"example-benchmark": "pnpm run build && node lib/examples/EvmBenchmark.js",
2724
"test:format": "prettier --check \"src/**/*.ts\"",
2825
"test:lint": "eslint src/ --max-warnings 0",
2926
"fix:format": "prettier --write \"src/**/*.ts\"",
3027
"fix:lint": "eslint src/ --fix --max-warnings 0",
3128
"prepublishOnly": "pnpm run build && pnpm run test:lint",
3229
"preversion": "pnpm run test:lint",
33-
"version": "pnpm run format && git add -A src"
30+
"version": "pnpm run test:format && git add -A src"
3431
},
3532
"keywords": [
3633
"pyth",
3734
"oracle"
3835
],
3936
"license": "Apache-2.0",
4037
"devDependencies": {
38+
"@cprussin/eslint-config": "catalog:",
4139
"@pythnetwork/pyth-sdk-solidity": "workspace:*",
4240
"@truffle/hdwallet-provider": "^2.1.5",
4341
"@types/ethereum-protocol": "^1.0.2",
4442
"@types/jest": "^29.4.0",
4543
"@types/node": "^18.11.18",
4644
"@types/web3-provider-engine": "^14.0.1",
4745
"@types/yargs": "^17.0.10",
48-
"@typescript-eslint/eslint-plugin": "^5.21.0",
49-
"@typescript-eslint/parser": "^5.21.0",
50-
"eslint": "^8.14.0",
46+
"eslint": "catalog:",
5147
"jest": "^29.4.1",
5248
"prettier": "catalog:",
5349
"ts-jest": "^29.0.5",
54-
"typescript": "^4.6.3",
55-
"web3": "^1.8.2",
56-
"yargs": "^17.4.1"
50+
"ts-node": "catalog:",
51+
"typescript": "catalog:"
5752
},
5853
"dependencies": {
59-
"@pythnetwork/price-service-client": "workspace:*",
60-
"buffer": "^6.0.3"
54+
"@pythnetwork/hermes-client": "workspace:*",
55+
"viem": "catalog:"
6156
}
6257
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import { HermesClient } from "@pythnetwork/hermes-client";
2+
import {
3+
Address,
4+
PublicClient,
5+
encodeFunctionData,
6+
Hex,
7+
Transport,
8+
Chain,
9+
} from "viem";
10+
11+
import { multicall3Bundler } from "./multicall3-bundler";
12+
import { IPythAbi } from "./pyth-abi";
13+
import {
14+
debugTraceCallAction,
15+
extractPythPriceFeedsFromDebugTraceCall,
16+
} from "./tracer/debug-trace-call";
17+
import {
18+
extractPythPriceFeedsFromTraceCallMany,
19+
traceCallManyAction,
20+
} from "./tracer/trace-call-many";
21+
22+
/**
23+
* Represents a call request to be executed on the blockchain
24+
*/
25+
export type CallRequest = {
26+
/** The address making the call (optional) */
27+
from?: Address;
28+
/** The target contract address */
29+
to: Address;
30+
/** The encoded function call data (optional) */
31+
data?: `0x${string}`;
32+
/** The amount of ETH to send with the call (optional) */
33+
value?: bigint;
34+
};
35+
36+
/**
37+
* Get the update fee for a given set of update data
38+
*
39+
* @param client - The public client instance
40+
* @param pythContractAddress - The Pyth contract address
41+
* @param updateData - Array of hex-encoded update data
42+
* @returns Promise resolving to the update fee in wei
43+
*/
44+
export async function getUpdateFee<
45+
transport extends Transport,
46+
chain extends Chain | undefined,
47+
>(
48+
client: PublicClient<transport, chain>,
49+
pythContractAddress: Address,
50+
updateData: Hex[],
51+
): Promise<bigint> {
52+
return await client.readContract({
53+
address: pythContractAddress,
54+
abi: IPythAbi,
55+
functionName: "getUpdateFee",
56+
args: [updateData],
57+
});
58+
}
59+
60+
/**
61+
* A function that takes a Pyth update and a call request and returns a single bundled call request.
62+
* This is used to combine the Pyth update with the original call.
63+
*/
64+
export type Bundler = (
65+
pythUpdate: PythUpdate,
66+
call: CallRequest,
67+
) => CallRequest;
68+
69+
/**
70+
* Configuration for debug_traceCall method.
71+
* Use this when you want to trace a single bundled transaction that combines the Pyth update with the original call.
72+
* The bundler function is responsible for creating a single transaction that executes both operations.
73+
*
74+
* The bundler is crucial because debug_traceCall can only trace one transaction at a time. The bundler
75+
* must create a single call that includes both the Pyth price update and the original transaction logic.
76+
* This allows the tracer to see all the Pyth price feed calls that would be made in the actual execution.
77+
*/
78+
export type DebugTraceCallConfig = {
79+
/** Must be "debug_traceCall" */
80+
method: "debug_traceCall";
81+
/** Function that takes a Pyth update and original call, returns a single bundled call request.
82+
* Common bundlers include multicall3Bundler for combining calls via Multicall3 contract.
83+
* The bundler must create a single transaction that executes both the Pyth update and the original call. */
84+
bundler: Bundler;
85+
/** Maximum number of iterations to find all required price feeds. Default is 5.
86+
* Each iteration traces the current transaction to find new Pyth price feed calls. */
87+
maxIter: number;
88+
};
89+
90+
/**
91+
* Configuration for trace_callMany method.
92+
* Use this when you want to trace multiple separate transactions (Pyth update + original call).
93+
* This method traces each call independently, which may be more accurate but requires more RPC calls.
94+
*/
95+
export type TraceCallManyConfig = {
96+
/** Must be "trace_callMany" */
97+
method: "trace_callMany";
98+
/** Maximum number of iterations to find all required price feeds. Default is 5.
99+
* Each iteration traces the current set of transactions to find new Pyth price feed calls. */
100+
maxIter: number;
101+
};
102+
103+
/**
104+
* Union type for tracing configuration options
105+
*/
106+
export type Config = DebugTraceCallConfig | TraceCallManyConfig;
107+
108+
/**
109+
* Represents a Pyth price update transaction
110+
*/
111+
export type PythUpdate = {
112+
/** The call request to update Pyth price feeds */
113+
call: CallRequest;
114+
/** Array of hex-encoded price update data */
115+
updateData: Hex[];
116+
/** The fee required for the update in wei */
117+
updateFee: bigint;
118+
};
119+
120+
/**
121+
* Fill the Pyth data for a given call request.
122+
* Requires a client that supports trace_callMany or debug_traceCall with a bundler.
123+
*
124+
* @param client - The public client instance
125+
* @param call - The call request to fill with Pyth data
126+
* @param pythContractAddress - The Pyth contract address
127+
* @param hermesEndpoint - The Hermes endpoint URL for fetching price updates
128+
* @param config - Configuration options for tracing and bundling. Can be either:
129+
* - `DebugTraceCallConfig`: For debug_traceCall method with a bundler function to combine Pyth update with original call.
130+
* The bundler creates a single transaction that executes both the Pyth update and the original call.
131+
* - `TraceCallManyConfig`: For trace_callMany method which traces multiple calls separately.
132+
* This method traces the Pyth update and original call as separate transactions.
133+
* @returns Promise resolving to Pyth update object or undefined if no Pyth data needed
134+
*/
135+
export async function fillPythUpdate<
136+
transport extends Transport,
137+
chain extends Chain | undefined,
138+
>(
139+
client: PublicClient<transport, chain>,
140+
call: CallRequest,
141+
pythContractAddress: Address,
142+
hermesEndpoint: string,
143+
config?: Config,
144+
): Promise<PythUpdate | undefined> {
145+
const defaultConfig: Config = {
146+
method: "debug_traceCall",
147+
bundler: multicall3Bundler,
148+
maxIter: 5,
149+
};
150+
const finalConfig = config ?? defaultConfig;
151+
const traceActionsClient = client
152+
.extend(debugTraceCallAction)
153+
.extend(traceCallManyAction);
154+
const hermesClient = new HermesClient(hermesEndpoint);
155+
156+
let requiredPriceFeeds = new Set<`0x${string}`>();
157+
158+
let pythUpdate: PythUpdate | undefined;
159+
160+
for (let i = 0; i < finalConfig.maxIter; i++) {
161+
let priceFeeds = new Set<`0x${string}`>();
162+
163+
if (finalConfig.method === "debug_traceCall") {
164+
const bundledCall = pythUpdate
165+
? finalConfig.bundler(pythUpdate, call)
166+
: call;
167+
const traceResult = await traceActionsClient.debugTraceCall(bundledCall);
168+
priceFeeds = extractPythPriceFeedsFromDebugTraceCall(
169+
traceResult,
170+
pythContractAddress,
171+
);
172+
} else {
173+
const calls = pythUpdate ? [pythUpdate.call, call] : [call];
174+
const traceResult = await traceActionsClient.traceCallMany(calls);
175+
priceFeeds = extractPythPriceFeedsFromTraceCallMany(
176+
traceResult,
177+
pythContractAddress,
178+
);
179+
}
180+
181+
const oldSize = requiredPriceFeeds.size;
182+
requiredPriceFeeds = new Set([...requiredPriceFeeds, ...priceFeeds]);
183+
184+
if (oldSize === requiredPriceFeeds.size) {
185+
break;
186+
}
187+
188+
const hermesResponse = await hermesClient.getLatestPriceUpdates([
189+
...requiredPriceFeeds,
190+
]);
191+
const updateData = hermesResponse.binary.data.map(
192+
(data) => ("0x" + data) as `0x${string}`,
193+
);
194+
195+
const updateFee = await getUpdateFee(
196+
client,
197+
pythContractAddress,
198+
updateData,
199+
);
200+
201+
pythUpdate = {
202+
call: {
203+
to: pythContractAddress,
204+
data: encodeFunctionData({
205+
abi: IPythAbi,
206+
functionName: "updatePriceFeeds",
207+
args: [updateData],
208+
}),
209+
from: call.from,
210+
value: updateFee,
211+
},
212+
updateData,
213+
updateFee,
214+
};
215+
}
216+
217+
return pythUpdate;
218+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from "./filler";
2+
export * from "./multicall3-bundler";
3+
export * from "./pyth-abi";

0 commit comments

Comments
 (0)