8
8
Chain ,
9
9
} from "viem" ;
10
10
11
- import { multicall3Bundler } from "./multicall3-bundler" ;
12
11
import { IPythAbi } from "./pyth-abi" ;
13
12
import {
14
13
debugTraceCallAction ,
@@ -28,7 +27,7 @@ export type CallRequest = {
28
27
/** The target contract address */
29
28
to : Address ;
30
29
/** The encoded function call data (optional) */
31
- data ?: `0x${ string } ` ;
30
+ data ?: Hex ;
32
31
/** The amount of ETH to send with the call (optional) */
33
32
value ?: bigint ;
34
33
} ;
@@ -67,43 +66,36 @@ export type Bundler = (
67
66
) => CallRequest ;
68
67
69
68
/**
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
105
70
*/
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
+ ) ;
107
99
108
100
/**
109
101
* Represents a Pyth price update transaction
@@ -120,16 +112,23 @@ export type PythUpdate = {
120
112
/**
121
113
* Fill the Pyth data for a given call request.
122
114
* 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.
123
118
*
124
119
* @param client - The public client instance
125
120
* @param call - The call request to fill with Pyth data
126
121
* @param pythContractAddress - The Pyth contract address
127
122
* @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.
132
125
* 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.
133
132
* @returns Promise resolving to Pyth update object or undefined if no Pyth data needed
134
133
*/
135
134
export async function fillPythUpdate <
@@ -142,77 +141,102 @@ export async function fillPythUpdate<
142
141
hermesEndpoint : string ,
143
142
config ?: Config ,
144
143
) : 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 ,
149
147
} ;
150
- const finalConfig = config ?? defaultConfig ;
151
- const traceActionsClient = client
152
- . extend ( debugTraceCallAction )
153
- . extend ( traceCallManyAction ) ;
154
- const hermesClient = new HermesClient ( hermesEndpoint ) ;
155
148
156
- let requiredPriceFeeds = new Set < `0x${ string } ` > ( ) ;
149
+ const hermesClient = new HermesClient ( hermesEndpoint ) ;
157
150
151
+ let requiredPriceFeeds = new Set < Address > ( ) ;
158
152
let pythUpdate : PythUpdate | undefined ;
159
153
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
+ ) ;
162
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
- ) ;
163
+ if ( priceFeeds . isSubsetOf ( requiredPriceFeeds ) ) {
164
+ break ;
172
165
} 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 ,
177
171
pythContractAddress ,
172
+ call ,
178
173
) ;
179
174
}
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
175
}
216
176
217
177
return pythUpdate ;
218
178
}
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