Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
"@solana-program/compute-budget": "^0.8.0",
"@solana/kit": "^5.4.0",
"google-auth-library": "^8.5.1",
"axios": "^1.7.4",
"binance-api-node": "0.12.7",
"dotenv": "^16.3.1",
"ethers": "^5.7.2",
Expand Down
24 changes: 13 additions & 11 deletions scripts/fetchInventoryConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { writeFile } from "node:fs/promises";
import { config } from "dotenv";
import axios from "axios";
import { GoogleAuth } from "google-auth-library";
import { Logger, waitForLogger, delay } from "../src/utils";

Expand All @@ -26,8 +25,14 @@ async function fetchWithRetry(
): Promise<string> {
for (let i = 0; i < retries; i++) {
try {
const response = await axios.get(url, { headers, responseType: "text", timeout: 30000 });
return response.data as string;
const response = await fetch(url, {
headers,
signal: AbortSignal.timeout(30000),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.text();
} catch (error) {
Comment on lines 27 to 36
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about getting rid of this try/catch construction? fetch won't throw by default so it's basically artificial that we're setting up the try/catch loop and then are the sole reason for it to throw. i.e. it should be possible to just evaluate response.status directly, rather than doing string comparisons on error.message.

if (i === retries - 1) {
throw error;
Expand Down Expand Up @@ -162,17 +167,14 @@ async function run(): Promise<number> {
}

function getErrorMessage(error: unknown): string {
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
if (error instanceof Error) {
const msg = error.message;
if (msg.includes("HTTP 404")) {
return "File not found in Configurama";
} else if (error.response?.status === 401 || error.response?.status === 403) {
} else if (msg.includes("HTTP 401") || msg.includes("HTTP 403")) {
return "Authentication failed. Ensure ADC is configured to call the Configurama API.";
} else {
return `Configurama API error: ${error.response?.status} - ${error.message}`;
}
}
if (error instanceof Error) {
return error.message;
return msg;
}
return String(error);
}
Expand Down
93 changes: 43 additions & 50 deletions scripts/simulateFill.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import axios from "axios";
import minimist from "minimist";
import { config } from "dotenv";
import { LogDescription } from "@ethersproject/abi";
Expand Down Expand Up @@ -170,58 +169,52 @@ async function createTenderlySimulation(
public: true, // Make simulation publicly accessible
};

try {
const response = await axios.post(tenderlyUrl, simulationPayload, {
headers: {
"X-Access-Key": tenderlyAccessKey,
"Content-Type": "application/json",
},
});
const tenderlyHeaders = {
"X-Access-Key": tenderlyAccessKey,
"Content-Type": "application/json",
};

const simulationId = response.data.simulation.id;

console.log(`\nDebug: Simulation created with ID: ${simulationId}`);

// Enable sharing by calling the share endpoint
const shareUrl = `https://api.tenderly.co/api/v1/account/${tenderlyUser}/project/${tenderlyProject}/simulations/${simulationId}/share`;

try {
await axios.post(
shareUrl,
{}, // Empty body
{
headers: {
"X-Access-Key": tenderlyAccessKey,
"Content-Type": "application/json",
},
}
);

console.log("Debug: Share enabled for simulation");

// Once sharing is enabled, construct the public share URL
const publicShareUrl = `https://www.tdly.co/shared/simulation/${simulationId}`;
console.log(`Debug: Using public share URL: ${publicShareUrl}`);

return publicShareUrl;
} catch (shareError) {
// If share endpoint fails, fall back to the regular dashboard URL
console.warn("Could not enable sharing, using dashboard URL instead");
if (axios.isAxiosError(shareError)) {
console.warn("Share endpoint error:", shareError.response?.status, shareError.response?.data);
}
}
const response = await fetch(tenderlyUrl, {
method: "POST",
headers: tenderlyHeaders,
body: JSON.stringify(simulationPayload),
});

// Fallback to dashboard URL
return `https://dashboard.tenderly.co/${tenderlyUser}/${tenderlyProject}/simulator/${simulationId}`;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(
`Tenderly simulation failed: ${error.response?.status} - ${JSON.stringify(error.response?.data)}`
);
}
throw error instanceof Error ? error : new Error(String(error));
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Tenderly simulation failed: ${response.status} - ${errorBody}`);
}

const responseData = (await response.json()) as { simulation: { id: string } };
const simulationId = responseData.simulation.id;

console.log(`\nDebug: Simulation created with ID: ${simulationId}`);

// Enable sharing by calling the share endpoint
const shareUrl = `https://api.tenderly.co/api/v1/account/${tenderlyUser}/project/${tenderlyProject}/simulations/${simulationId}/share`;

const shareResponse = await fetch(shareUrl, {
method: "POST",
headers: tenderlyHeaders,
body: JSON.stringify({}),
});

if (shareResponse.ok) {
console.log("Debug: Share enabled for simulation");

// Once sharing is enabled, construct the public share URL
const publicShareUrl = `https://www.tdly.co/shared/simulation/${simulationId}`;
console.log(`Debug: Using public share URL: ${publicShareUrl}`);

return publicShareUrl;
} else {
// If share endpoint fails, fall back to the regular dashboard URL
console.warn("Could not enable sharing, using dashboard URL instead");
console.warn("Share endpoint error:", shareResponse.status, await shareResponse.text());
}

// Fallback to dashboard URL
return `https://dashboard.tenderly.co/${tenderlyUser}/${tenderlyProject}/simulator/${simulationId}`;
}

async function getBlockNumberForTimestamp(destinationChainId: number, targetTimestamp: number): Promise<number> {
Expand Down
32 changes: 21 additions & 11 deletions src/clients/AcrossAPIClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import _ from "lodash";
import axios, { AxiosError } from "axios";
import {
bnZero,
winston,
Expand Down Expand Up @@ -132,28 +131,39 @@ export class AcrossApiClient {
}
}

const params = { l1Tokens: l1Tokens.join(",") };
const params = new URLSearchParams({ l1Tokens: l1Tokens.join(",") });
let liquidReserves: BigNumber[] = [];
try {
const result = await axios(url, { timeout, params });
if (!result?.data) {
const response = await fetch(`${url}?${params}`, {
signal: AbortSignal.timeout(timeout),
});
if (!response.ok) {
this.logger.error({
at: "AcrossAPIClient",
message: `Invalid response from /${path}, expected maxDeposit field.`,
url,
params,
result,
params: Object.fromEntries(params),
status: response.status,
});
return l1Tokens.map(() => bnZero);
}
liquidReserves = l1Tokens.map((l1Token) => BigNumber.from(result.data[l1Token.toEvmAddress()] ?? bnZero));
const data = (await response.json()) as Record<string, string>;
liquidReserves = l1Tokens.map((l1Token) => BigNumber.from(data[l1Token.toEvmAddress()] ?? bnZero));
} catch (err) {
const msg = _.get(err, "response.data", _.get(err, "response.statusText", (err as AxiosError).message));
this.logger.warn({ at: "AcrossAPIClient", message: `Failed to get ${path},`, url, params, msg });
const msg = (err as Error).message;
this.logger.warn({
at: "AcrossAPIClient",
message: `Failed to get ${path},`,
url,
params: Object.fromEntries(params),
msg,
});
return l1Tokens.map(() => bnZero);
}

if (redis) {
// Cache limit for 5 minutes.
if (redis && liquidReserves.some((r) => r.gt(bnZero))) {
// Cache limit for 5 minutes. Only cache if at least one reserve is non-zero to avoid
// persisting bad data from transient API issues.
const baseTtl = 300;
// Apply a random margin to spread expiry over a larger time window.
const ttl = baseTtl + Math.ceil(_.random(-0.5, 0.5, true) * baseTtl);
Expand Down
37 changes: 27 additions & 10 deletions src/clients/AcrossApiBaseClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import axios, { AxiosError } from "axios";
import { isDefined } from "../utils";
import winston from "winston";

/**
Expand Down Expand Up @@ -34,18 +34,35 @@ export abstract class BaseAcrossApiClient {

protected async _get<T>(endpoint: string, params: Record<string, unknown>): Promise<T | undefined> {
try {
const config: { timeout: number; params: Record<string, unknown>; headers?: Record<string, string> } = {
timeout: this.apiResponseTimeout,
params,
};
const url = new URL(`${this.urlBase}/${endpoint}`);
for (const [key, value] of Object.entries(params)) {
if (isDefined(value)) {
url.searchParams.set(key, String(value));
}
}

const headers: Record<string, string> = {};
if (this.apiKey) {
config.headers = { Authorization: `Bearer ${this.apiKey}` };
headers["Authorization"] = `Bearer ${this.apiKey}`;
}

const response = await axios.get<T>(`${this.urlBase}/${endpoint}`, config);
const response = await fetch(url.toString(), {
headers,
signal: AbortSignal.timeout(this.apiResponseTimeout),
});

if (!response.ok) {
this.logger.warn({
at: this.logContext,
message: `Invalid response from ${this.urlBase}`,
endpoint,
params,
});
return;
}

if (!response?.data) {
const data = (await response.json()) as T;
if (!data) {
this.logger.warn({
at: this.logContext,
message: `Invalid response from ${this.urlBase}`,
Expand All @@ -54,14 +71,14 @@ export abstract class BaseAcrossApiClient {
});
return;
}
return response.data;
return data;
} catch (err) {
this.logger.warn({
at: this.logContext,
message: `Failed to get from ${this.urlBase}`,
endpoint,
params,
error: (err as AxiosError).message,
error: (err as Error).message,
});
return;
}
Expand Down
55 changes: 32 additions & 23 deletions src/finalizer/utils/helios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ import {
} from "../../utils";
import { spreadEventWithBlockNumber } from "../../utils/EventUtils";
import { FinalizerPromise, CrossChainMessage } from "../types";
import axios from "axios";
import UNIVERSAL_SPOKE_ABI from "../../common/abi/Universal_SpokePool.json";
import { RelayedCallDataEvent, StoredCallDataEvent } from "../../interfaces/Universal";
import { ApiProofRequest, ProofOutputs, ProofStateResponse, SP1HeliosProofData } from "../../interfaces/ZkApi";
import { StorageSlotVerifiedEvent, HeadUpdateEvent } from "../../interfaces/Helios";
import { calculateProofId, decodeProofOutputs } from "../../utils/ZkApiUtils";
import { calculateHubPoolStoreStorageSlot, getHubPoolStoreContract } from "../../utils/UniversalUtils";
import { stringifyThrownValue } from "../../utils/LogUtils";
import { getSp1HeliosContractEVM } from "../../utils/HeliosUtils";
import { getBlockFinder, getBlockForTimestamp } from "../../utils/BlockUtils";

Expand Down Expand Up @@ -344,34 +342,36 @@ async function enrichHeliosActions(

let proofState: ProofStateResponse | null = null;

// @dev We need try - catch here because of how API responds to non-existing proofs: with NotFound status
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let getError: any = null;
try {
const response = await axios.get<ProofStateResponse>(getProofUrl);
proofState = response.data;
logger.debug({ ...logContext, message: "Proof state received", proofId, status: proofState.status });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
getError = error;
}

// Axios error. Handle based on whether was a NOTFOUND or another error
if (getError) {
const isNotFoundError = axios.isAxiosError(getError) && getError.response?.status === 404;
if (isNotFoundError) {
// NOTFOUND error -> Request proof
// @dev The API responds to non-existing proofs with 404, so we handle that specially
const getResponse = await fetch(getProofUrl);
if (!getResponse.ok) {
if (getResponse.status === 404) {
// NOTFOUND -> Request proof
logger.debug({ ...logContext, message: "Proof not found (404), requesting...", proofId });
await axios.post(`${apiBaseUrl}/v1/api/proofs`, apiRequest);
const postResponse = await fetch(`${apiBaseUrl}/v1/api/proofs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(apiRequest),
});
if (!postResponse.ok) {
throw new Error(
`Failed to request proof for proofId ${proofId}: ${postResponse.status} ${postResponse.statusText}`
);
}
logger.debug({ ...logContext, message: "Proof requested successfully.", proofId });
continue;
} else {
// If other error is returned -- throw and alert PD; this shouldn't happen
throw new Error(`Failed to get proof state for proofId ${proofId}: ${stringifyThrownValue(getError)}`);
throw new Error(
`Failed to get proof state for proofId ${proofId}: ${getResponse.status} ${getResponse.statusText}`
);
}
}

// No axios error, process `proofState`
proofState = (await getResponse.json()) as ProofStateResponse;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to follow up and check how to validate types on this. I'm not really sure why it wasn't required for; perhaps the return was typed as any 🤔

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ProofStateResponse type from src/interfaces/ZkApi.ts is already well-defined with typed status, update_calldata, and error_message fields. The as ProofStateResponse cast on the response.json() result gives us the same type safety as the prior axios.get<ProofStateResponse> generic — both are essentially trust-the-server assertions since neither validates at runtime. If we want true runtime validation we'd need a schema library (zod, etc), but that's a bigger lift.

logger.debug({ ...logContext, message: "Proof state received", proofId, status: proofState.status });

// Process `proofState`
switch (proofState.status) {
case "pending":
// If proof generation is pending -- there's nothing for us to do yet. Will check this proof next run
Expand All @@ -391,7 +391,16 @@ async function enrichHeliosActions(
errorMessage: proofState.error_message,
});

await axios.post(`${apiBaseUrl}/v1/api/proofs`, apiRequest);
const retryResponse = await fetch(`${apiBaseUrl}/v1/api/proofs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(apiRequest),
});
if (!retryResponse.ok) {
throw new Error(
`Failed to re-request errored proof for proofId ${proofId}: ${retryResponse.status} ${retryResponse.statusText}`
);
}
logger.debug({ ...logContext, message: "Errored proof requested again successfully.", proofId });
break;
}
Expand Down
Loading
Loading