88 Chain ,
99} from "viem" ;
1010
11- import { multicall3Bundler } from "./multicall3-bundler" ;
1211import { IPythAbi } from "./pyth-abi" ;
1312import {
1413 debugTraceCallAction ,
@@ -28,7 +27,7 @@ export type CallRequest = {
2827 /** The target contract address */
2928 to : Address ;
3029 /** The encoded function call data (optional) */
31- data ?: `0x${ string } ` ;
30+ data ?: Hex ;
3231 /** The amount of ETH to send with the call (optional) */
3332 value ?: bigint ;
3433} ;
@@ -67,43 +66,36 @@ export type Bundler = (
6766) => CallRequest ;
6867
6968/**
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
69+ * Tracing configuration options
10570 */
106- export type Config = DebugTraceCallConfig | TraceCallManyConfig ;
71+ export type Config = {
72+ /** Maximum number of iterations to find all required price feeds. Default is 5. */
73+ maxIter ?: number ;
74+ } & (
75+ | {
76+ /**
77+ * Use this when you want to trace multiple separate transactions (Pyth update + original call).
78+ * This method traces each call independently, which may be more accurate but requires more RPC calls.
79+ */
80+ method : "trace_callMany" ;
81+ }
82+ | {
83+ /**
84+ * Use this when you want to trace a single bundled transaction that combines the Pyth update with the original call.
85+ * The bundler function is responsible for creating a single transaction that executes both operations.
86+ *
87+ * The bundler is crucial because debug_traceCall can only trace one transaction at a time.
88+ * The bundler must create a single call that includes both the Pyth price update and the original transaction logic.
89+ * This allows the tracer to see all the Pyth price feed calls that would be made in the actual execution.
90+ */
91+ method : "debug_traceCall" ;
92+ /**
93+ * Function that takes a Pyth update and original call, returns a single bundled call request.
94+ * Common bundlers include multicall3Bundler for combining calls via Multicall3 contract.
95+ */
96+ bundler : Bundler ;
97+ }
98+ ) ;
10799
108100/**
109101 * Represents a Pyth price update transaction
@@ -120,16 +112,23 @@ export type PythUpdate = {
120112/**
121113 * Fill the Pyth data for a given call request.
122114 * Requires a client that supports trace_callMany or debug_traceCall with a bundler.
115+ * This function will trace the call and find all the Pyth price feeds that are needed to fill the call in multiple
116+ * iterations because a single call might revert if it requires a price feed that is not available and we need to
117+ * trace the call again with the new price feeds until we have all the price feeds.
123118 *
124119 * @param client - The public client instance
125120 * @param call - The call request to fill with Pyth data
126121 * @param pythContractAddress - The Pyth contract address
127122 * @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.
123+ * @param config - Configuration options for tracing and bundling. Default is `{ method: "trace_callMany" }`.
124+ * - `Config` with `method: "trace_callMany"`: For trace_callMany method which traces multiple calls separately.
132125 * This method traces the Pyth update and original call as separate transactions.
126+ * - `Config` with `method: "debug_traceCall"` and `bundler`: For debug_traceCall method with a bundler function to
127+ * combine Pyth update with the original call. The bundler creates a single transaction that executes both the
128+ * Pyth update and the original call.
129+ * - `maxIter`: Maximum number of iterations to find all required price feeds. Each iteration traces the current
130+ * transaction(s) to find new Pyth price feed calls. The process stops when no new price feeds are found
131+ * or when maxIter is reached. Default is 5.
133132 * @returns Promise resolving to Pyth update object or undefined if no Pyth data needed
134133 */
135134export async function fillPythUpdate <
@@ -142,77 +141,102 @@ export async function fillPythUpdate<
142141 hermesEndpoint : string ,
143142 config ?: Config ,
144143) : Promise < PythUpdate | undefined > {
145- const defaultConfig : Config = {
146- method : "debug_traceCall" ,
147- bundler : multicall3Bundler ,
148- maxIter : 5 ,
144+ config = {
145+ method : "trace_callMany" ,
146+ ...config ,
149147 } ;
150- const finalConfig = config ?? defaultConfig ;
151- const traceActionsClient = client
152- . extend ( debugTraceCallAction )
153- . extend ( traceCallManyAction ) ;
154- const hermesClient = new HermesClient ( hermesEndpoint ) ;
155148
156- let requiredPriceFeeds = new Set < `0x${ string } ` > ( ) ;
149+ const hermesClient = new HermesClient ( hermesEndpoint ) ;
157150
151+ let requiredPriceFeeds = new Set < Address > ( ) ;
158152 let pythUpdate : PythUpdate | undefined ;
159153
160- for ( let i = 0 ; i < finalConfig . maxIter ; i ++ ) {
161- let priceFeeds = new Set < `0x${string } `> ( ) ;
154+ for ( let i = 0 ; i < ( config . maxIter ?? 5 ) ; i ++ ) {
155+ const priceFeeds = await getPriceFeeds (
156+ client ,
157+ pythContractAddress ,
158+ call ,
159+ config ,
160+ pythUpdate ,
161+ ) ;
162162
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- ) ;
163+ if ( priceFeeds . isSubsetOf ( requiredPriceFeeds ) ) {
164+ break ;
172165 } else {
173- const calls = pythUpdate ? [ pythUpdate . call , call ] : [ call ] ;
174- const traceResult = await traceActionsClient . traceCallMany ( calls ) ;
175- priceFeeds = extractPythPriceFeedsFromTraceCallMany (
176- traceResult ,
166+ requiredPriceFeeds = requiredPriceFeeds . union ( priceFeeds ) ;
167+ pythUpdate = await getPythUpdate (
168+ client ,
169+ hermesClient ,
170+ requiredPriceFeeds ,
177171 pythContractAddress ,
172+ call ,
178173 ) ;
179174 }
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- } ;
215175 }
216176
217177 return pythUpdate ;
218178}
179+
180+ const getPythUpdate = async <
181+ transport extends Transport ,
182+ chain extends Chain | undefined ,
183+ > (
184+ client : PublicClient < transport , chain > ,
185+ hermesClient : HermesClient ,
186+ priceFeeds : Set < Address > ,
187+ pythContractAddress : Address ,
188+ call : CallRequest ,
189+ ) => {
190+ const hermesResponse = await hermesClient . getLatestPriceUpdates ( [
191+ ...priceFeeds ,
192+ ] ) ;
193+ const updateData = hermesResponse . binary . data . map < Hex > ( ( data ) => `0x${ data } ` ) ;
194+ const updateFee = await getUpdateFee ( client , pythContractAddress , updateData ) ;
195+ return {
196+ call : {
197+ to : pythContractAddress ,
198+ data : encodeFunctionData ( {
199+ abi : IPythAbi ,
200+ functionName : "updatePriceFeeds" ,
201+ args : [ updateData ] ,
202+ } ) ,
203+ from : call . from ,
204+ value : updateFee ,
205+ } ,
206+ updateData,
207+ updateFee,
208+ } ;
209+ } ;
210+
211+ /**
212+ * Get the price feeds from the trace of the given call.
213+ */
214+ const getPriceFeeds = async <
215+ transport extends Transport ,
216+ chain extends Chain | undefined ,
217+ > (
218+ client : PublicClient < transport , chain > ,
219+ pythContractAddress : Address ,
220+ call : CallRequest ,
221+ config : Config ,
222+ pythUpdate : PythUpdate | undefined ,
223+ ) => {
224+ switch ( config . method ) {
225+ case "debug_traceCall" : {
226+ return extractPythPriceFeedsFromDebugTraceCall (
227+ await client
228+ . extend ( debugTraceCallAction )
229+ . debugTraceCall ( pythUpdate ? config . bundler ( pythUpdate , call ) : call ) ,
230+ pythContractAddress ,
231+ ) ;
232+ }
233+ case "trace_callMany" : {
234+ return extractPythPriceFeedsFromTraceCallMany (
235+ await client
236+ . extend ( traceCallManyAction )
237+ . traceCallMany ( pythUpdate ? [ pythUpdate . call , call ] : [ call ] ) ,
238+ pythContractAddress ,
239+ ) ;
240+ }
241+ }
242+ } ;
0 commit comments