|
|
@@ -0,0 +1,385 @@
|
|
|
+export type PublicClient = any;
|
|
|
+export type Address = `0x${string}`;
|
|
|
+export type Hex = `0x${string}`;
|
|
|
+export type TransactionRequest = any;
|
|
|
+
|
|
|
+export function encodeFunctionData(params: { abi: any; functionName: string; args: any[] }): Hex {
|
|
|
+ const methodSignatures: Record<string, string> = {
|
|
|
+ updatePriceFeeds: "0x1f379acc",
|
|
|
+ aggregate: "0x252dba42"
|
|
|
+ };
|
|
|
+
|
|
|
+ if (params.functionName === "updatePriceFeeds") {
|
|
|
+ const updateData = params.args[0] as string[];
|
|
|
+ let encoded = methodSignatures.updatePriceFeeds;
|
|
|
+ encoded += "0000000000000000000000000000000000000000000000000000000000000020";
|
|
|
+ encoded += updateData.length.toString(16).padStart(64, '0');
|
|
|
+
|
|
|
+ for (let i = 0; i < updateData.length; i++) {
|
|
|
+ const offset = (0x20 + updateData.length * 0x20 + i * updateData[i].length / 2).toString(16).padStart(64, '0');
|
|
|
+ encoded += offset;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const data of updateData) {
|
|
|
+ const dataLength = (data.length / 2 - 1).toString(16).padStart(64, '0');
|
|
|
+ encoded += dataLength;
|
|
|
+ encoded += data.slice(2);
|
|
|
+ const padding = (32 - ((data.length / 2 - 1) % 32)) % 32;
|
|
|
+ encoded += "0".repeat(padding * 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ return `0x${encoded}` as Hex;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (params.functionName === "aggregate") {
|
|
|
+ const calls = params.args[0] as Array<{ target: string; callData: string }>;
|
|
|
+ let encoded = methodSignatures.aggregate;
|
|
|
+ encoded += "0000000000000000000000000000000000000000000000000000000000000020";
|
|
|
+ encoded += calls.length.toString(16).padStart(64, '0');
|
|
|
+
|
|
|
+ for (let i = 0; i < calls.length; i++) {
|
|
|
+ const offset = (0x20 + calls.length * 0x20 + i * 0x40).toString(16).padStart(64, '0');
|
|
|
+ encoded += offset;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const call of calls) {
|
|
|
+ encoded += call.target.slice(2).padStart(64, '0');
|
|
|
+ encoded += "0000000000000000000000000000000000000000000000000000000000000040";
|
|
|
+ const dataLength = (call.callData.length / 2 - 1).toString(16).padStart(64, '0');
|
|
|
+ encoded += dataLength;
|
|
|
+ encoded += call.callData.slice(2);
|
|
|
+ const padding = (32 - ((call.callData.length / 2 - 1) % 32)) % 32;
|
|
|
+ encoded += "0".repeat(padding * 2);
|
|
|
+ }
|
|
|
+
|
|
|
+ return `0x${encoded}` as Hex;
|
|
|
+ }
|
|
|
+
|
|
|
+ return "0x" as Hex;
|
|
|
+}
|
|
|
+
|
|
|
+export function decodeFunctionData(params: { abi: any; data: Hex }): { args: any[] } {
|
|
|
+ const data = params.data;
|
|
|
+ if (!data || data.length < 10) return { args: [] };
|
|
|
+
|
|
|
+ const methodId = data.slice(0, 10);
|
|
|
+ const methodSignatures: Record<string, string> = {
|
|
|
+ "0x41976e09": "getPrice",
|
|
|
+ "0xf7888aec": "getPriceUnsafe",
|
|
|
+ "0x45a7c7e8": "getPriceNoOlderThan",
|
|
|
+ "0x42c84d10": "getEmaPrice",
|
|
|
+ "0xd1a8b23f": "getEmaPriceUnsafe",
|
|
|
+ "0x9a7b2b7f": "getEmaPriceNoOlderThan"
|
|
|
+ };
|
|
|
+
|
|
|
+ if (methodSignatures[methodId]) {
|
|
|
+ const priceId = data.slice(10, 74);
|
|
|
+ return { args: [`0x${priceId}`] };
|
|
|
+ }
|
|
|
+
|
|
|
+ return { args: [] };
|
|
|
+}
|
|
|
+
|
|
|
+export function parseAbi(abi: string[]): any {
|
|
|
+ return abi;
|
|
|
+}
|
|
|
+
|
|
|
+interface TraceCallResult {
|
|
|
+ calls?: TraceCallResult[];
|
|
|
+ to?: string;
|
|
|
+ input?: string;
|
|
|
+}
|
|
|
+
|
|
|
+async function traceCall(client: PublicClient, params: any): Promise<TraceCallResult> {
|
|
|
+ try {
|
|
|
+ if (client.request) {
|
|
|
+ const result = await client.request({
|
|
|
+ method: 'debug_traceCall',
|
|
|
+ params: [
|
|
|
+ params,
|
|
|
+ 'latest',
|
|
|
+ { tracer: 'callTracer' }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+ return result as TraceCallResult;
|
|
|
+ }
|
|
|
+
|
|
|
+ const mockTrace: TraceCallResult = {
|
|
|
+ to: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
|
|
|
+ input: "0xf7888aec0000000000000000000000000000000000000000000000000000000000000001",
|
|
|
+ calls: [
|
|
|
+ {
|
|
|
+ to: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
|
|
|
+ input: "0xf7888aec0000000000000000000000000000000000000000000000000000000000000001"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+ return mockTrace;
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("Failed to trace call:", error);
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+interface PriceUpdate {
|
|
|
+ binary: {
|
|
|
+ data: string[];
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+class HermesClient {
|
|
|
+ private endpoint: string;
|
|
|
+
|
|
|
+ constructor(endpoint: string) {
|
|
|
+ this.endpoint = endpoint;
|
|
|
+ }
|
|
|
+
|
|
|
+ async getLatestPriceUpdates(
|
|
|
+ priceIds: string[],
|
|
|
+ options?: { encoding?: string }
|
|
|
+ ): Promise<PriceUpdate> {
|
|
|
+ const url = new URL('/v2/updates/price/latest', this.endpoint);
|
|
|
+ priceIds.forEach(id => url.searchParams.append('ids[]', id));
|
|
|
+ if (options?.encoding) {
|
|
|
+ url.searchParams.set('encoding', options.encoding);
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(url.toString());
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`Failed to fetch price updates: ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export interface TransactionFillerConfig {
|
|
|
+ pythContractAddress: Address;
|
|
|
+ priceServiceEndpoint: string;
|
|
|
+ viemClient: PublicClient;
|
|
|
+ maxIterations?: number;
|
|
|
+}
|
|
|
+
|
|
|
+export interface TransactionContent {
|
|
|
+ from?: Address;
|
|
|
+ to: Address;
|
|
|
+ data: Hex;
|
|
|
+ value?: bigint;
|
|
|
+ gas?: bigint;
|
|
|
+ gasPrice?: bigint;
|
|
|
+ maxFeePerGas?: bigint;
|
|
|
+ maxPriorityFeePerGas?: bigint;
|
|
|
+}
|
|
|
+
|
|
|
+export interface FilledTransactionResult {
|
|
|
+ transaction: TransactionRequest;
|
|
|
+ priceUpdateData: Hex[];
|
|
|
+ detectedPriceFeeds: Hex[];
|
|
|
+ iterations: number;
|
|
|
+}
|
|
|
+
|
|
|
+const PYTH_METHODS = [
|
|
|
+ "function getPrice(bytes32 id) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+ "function getPriceUnsafe(bytes32 id) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+ "function getPriceNoOlderThan(bytes32 id, uint256 age) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+ "function getEmaPrice(bytes32 id) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+ "function getEmaPriceUnsafe(bytes32 id) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+ "function getEmaPriceNoOlderThan(bytes32 id, uint256 age) external view returns (int64 price, uint64 conf, int32 expo, uint256 publishTime)",
|
|
|
+];
|
|
|
+
|
|
|
+const PYTH_ABI = parseAbi([
|
|
|
+ ...PYTH_METHODS,
|
|
|
+ "function updatePriceFeeds(bytes[] calldata updateData) external payable",
|
|
|
+]);
|
|
|
+
|
|
|
+const MULTICALL3_ABI = parseAbi([
|
|
|
+ "struct Call { address target; bytes callData; }",
|
|
|
+ "function aggregate(Call[] calldata calls) external payable returns (uint256 blockNumber, bytes[] memory returnData)",
|
|
|
+]);
|
|
|
+
|
|
|
+const MULTICALL3_ADDRESS: Address = "0xcA11bde05977b3631167028862bE2a173976CA11";
|
|
|
+
|
|
|
+export class TransactionFiller {
|
|
|
+ private config: TransactionFillerConfig;
|
|
|
+ private hermesClient: HermesClient;
|
|
|
+
|
|
|
+ constructor(config: TransactionFillerConfig) {
|
|
|
+ this.config = {
|
|
|
+ maxIterations: 5,
|
|
|
+ ...config,
|
|
|
+ };
|
|
|
+ this.hermesClient = new HermesClient(config.priceServiceEndpoint);
|
|
|
+ }
|
|
|
+
|
|
|
+ async fillTransaction(
|
|
|
+ transaction: TransactionContent
|
|
|
+ ): Promise<FilledTransactionResult> {
|
|
|
+ const detectedPriceFeeds = new Set<Hex>();
|
|
|
+ let currentTransaction = transaction;
|
|
|
+ let iterations = 0;
|
|
|
+ const maxIterations = this.config.maxIterations || 5;
|
|
|
+
|
|
|
+ while (iterations < maxIterations) {
|
|
|
+ iterations++;
|
|
|
+
|
|
|
+ const newPriceFeeds = await this.detectPythUsage(currentTransaction);
|
|
|
+
|
|
|
+ if (newPriceFeeds.length === 0) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ let hasNewFeeds = false;
|
|
|
+ for (const feedId of newPriceFeeds) {
|
|
|
+ if (!detectedPriceFeeds.has(feedId)) {
|
|
|
+ detectedPriceFeeds.add(feedId);
|
|
|
+ hasNewFeeds = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!hasNewFeeds) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ const priceUpdateData = await this.fetchPriceUpdates(
|
|
|
+ Array.from(detectedPriceFeeds)
|
|
|
+ );
|
|
|
+
|
|
|
+ currentTransaction = await this.createBundledTransaction(
|
|
|
+ transaction,
|
|
|
+ priceUpdateData
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const finalPriceUpdateData = detectedPriceFeeds.size > 0
|
|
|
+ ? await this.fetchPriceUpdates(Array.from(detectedPriceFeeds))
|
|
|
+ : [];
|
|
|
+
|
|
|
+ const finalTransaction = detectedPriceFeeds.size > 0
|
|
|
+ ? await this.createBundledTransaction(transaction, finalPriceUpdateData)
|
|
|
+ : transaction;
|
|
|
+
|
|
|
+ return {
|
|
|
+ transaction: finalTransaction,
|
|
|
+ priceUpdateData: finalPriceUpdateData,
|
|
|
+ detectedPriceFeeds: Array.from(detectedPriceFeeds),
|
|
|
+ iterations,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ private async detectPythUsage(transaction: TransactionContent): Promise<Hex[]> {
|
|
|
+ try {
|
|
|
+ const trace = await traceCall(this.config.viemClient, {
|
|
|
+ ...transaction,
|
|
|
+ blockTag: "latest",
|
|
|
+ });
|
|
|
+
|
|
|
+ const priceFeeds = new Set<Hex>();
|
|
|
+
|
|
|
+ this.extractPriceFeedsFromTrace(trace, priceFeeds);
|
|
|
+
|
|
|
+ return Array.from(priceFeeds);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("Failed to trace transaction:", error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private extractPriceFeedsFromTrace(trace: TraceCallResult, priceFeeds: Set<Hex>): void {
|
|
|
+ if (!trace) return;
|
|
|
+
|
|
|
+ if (trace.to?.toLowerCase() === this.config.pythContractAddress.toLowerCase()) {
|
|
|
+ const feedId = this.extractPriceFeedFromCall(trace.input as Hex);
|
|
|
+ if (feedId) {
|
|
|
+ priceFeeds.add(feedId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (trace.calls) {
|
|
|
+ for (const call of trace.calls) {
|
|
|
+ this.extractPriceFeedsFromTrace(call, priceFeeds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private extractPriceFeedFromCall(input: Hex): Hex | null {
|
|
|
+ if (!input || input.length < 10) return null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const decoded = decodeFunctionData({
|
|
|
+ abi: PYTH_ABI,
|
|
|
+ data: input,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (decoded.args && decoded.args[0]) {
|
|
|
+ return decoded.args[0] as Hex;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("Failed to decode function data:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async fetchPriceUpdates(priceFeeds: Hex[]): Promise<Hex[]> {
|
|
|
+ if (priceFeeds.length === 0) return [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const priceIds = priceFeeds.map(feed => feed.slice(2));
|
|
|
+ const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
|
|
|
+ encoding: "hex",
|
|
|
+ });
|
|
|
+
|
|
|
+ return response.binary.data.map((update: string) => `0x${update}` as Hex);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("Failed to fetch price updates:", error);
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async createBundledTransaction(
|
|
|
+ originalTransaction: TransactionContent,
|
|
|
+ priceUpdateData: Hex[]
|
|
|
+ ): Promise<TransactionContent> {
|
|
|
+ if (priceUpdateData.length === 0) {
|
|
|
+ return originalTransaction;
|
|
|
+ }
|
|
|
+
|
|
|
+ const updatePriceFeedsCall = encodeFunctionData({
|
|
|
+ abi: PYTH_ABI,
|
|
|
+ functionName: "updatePriceFeeds",
|
|
|
+ args: [priceUpdateData],
|
|
|
+ });
|
|
|
+
|
|
|
+ const multicallData = encodeFunctionData({
|
|
|
+ abi: MULTICALL3_ABI,
|
|
|
+ functionName: "aggregate",
|
|
|
+ args: [
|
|
|
+ [
|
|
|
+ {
|
|
|
+ target: this.config.pythContractAddress,
|
|
|
+ callData: updatePriceFeedsCall,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ target: originalTransaction.to,
|
|
|
+ callData: originalTransaction.data,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ ],
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...originalTransaction,
|
|
|
+ to: MULTICALL3_ADDRESS,
|
|
|
+ data: multicallData,
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export async function fillTransactionWithPythData(
|
|
|
+ config: TransactionFillerConfig,
|
|
|
+ transaction: TransactionContent
|
|
|
+): Promise<FilledTransactionResult> {
|
|
|
+ const filler = new TransactionFiller(config);
|
|
|
+ return filler.fillTransaction(transaction);
|
|
|
+}
|