Skip to content

Commit cf77941

Browse files
committed
handle errors from cron logs
1 parent 4447b30 commit cf77941

26 files changed

Lines changed: 653 additions & 104 deletions

src/adapters/axelar-satellite/index.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,26 @@ import {
1919
throwIfAborted,
2020
waitWithSignal,
2121
} from "../../utils/errors";
22+
import { ConsecutiveFailureCircuitBreaker } from "../../helpers/circuitBreaker";
2223
const retry = require("async-retry");
2324

2425
const AXELAR_API_URL = "https://api.axelarscan.io/";
2526
const AXELAR_REQUEST_TIMEOUT_MS = 30_000;
2627
const AXELAR_REQUEST_INTERVAL_MS = 350;
2728
const AXELAR_REQUEST_CONCURRENCY = 3;
2829
const AXELAR_REQUEST_RETRIES = 3;
30+
const AXELAR_CIRCUIT_FAILURE_THRESHOLD = 3;
31+
const AXELAR_CIRCUIT_COOLDOWN_MS = 10 * 60 * 1000;
2932

3033
let activeAxelarRequests = 0;
3134
let nextAxelarRequestAt = 0;
3235
const axelarWaiters: Array<() => void> = [];
3336
let assetsPromise: Promise<any[]> | undefined;
37+
const axelarCircuit = new ConsecutiveFailureCircuitBreaker(
38+
"Axelarscan",
39+
AXELAR_CIRCUIT_FAILURE_THRESHOLD,
40+
AXELAR_CIRCUIT_COOLDOWN_MS
41+
);
3442

3543
const acquireAxelarSlot = async (signal?: AbortSignal) => {
3644
throwIfAborted(signal);
@@ -82,8 +90,10 @@ const fetchAxelar = async <T>(body: Record<string, unknown>, signal?: AbortSigna
8290
let acquired = false;
8391
try {
8492
throwIfAborted(signal);
93+
axelarCircuit.assertAvailable();
8594
await acquireAxelarSlot(signal);
8695
acquired = true;
96+
axelarCircuit.assertAvailable();
8797
const response = await fetch(AXELAR_API_URL, {
8898
method: "POST",
8999
headers: {
@@ -95,13 +105,21 @@ const fetchAxelar = async <T>(body: Record<string, unknown>, signal?: AbortSigna
95105
});
96106
if (!response.ok) {
97107
const responseBody = await response.text().catch(() => "");
98-
const error =
99-
response.status !== 429 && response.status < 500
100-
? new NonRetryableError(`Axelarscan HTTP ${response.status}: ${responseBody.slice(0, 300)}`)
101-
: new Error(`Axelarscan HTTP ${response.status}: ${responseBody.slice(0, 300)}`);
108+
const message = `Axelarscan HTTP ${response.status}: ${responseBody.slice(0, 300)}`;
109+
let error: Error;
110+
if (response.status >= 500 && axelarCircuit.recordFailure()) {
111+
error = new NonRetryableError(
112+
`${message}; circuit opened after ${AXELAR_CIRCUIT_FAILURE_THRESHOLD} consecutive server failures.`
113+
);
114+
} else {
115+
error =
116+
response.status !== 429 && response.status < 500 ? new NonRetryableError(message) : new Error(message);
117+
}
102118
throw error;
103119
}
104-
return (await response.json()) as T;
120+
const result = (await response.json()) as T;
121+
axelarCircuit.recordSuccess();
122+
return result;
105123
} catch (error) {
106124
if (isAbortError(error) || signal?.aborted || isNonRetryableError(error)) return bail(error as Error);
107125
throw error;

src/adapters/axelar/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ const InterchainTransferReceivedParams: PartialContractEventParams = {
243243
isDeposit: false,
244244
};
245245

246-
const constructParams = (chain: SupportedChains) => {
246+
const constructParams = (chain: SupportedChains, providerChain: string = chain) => {
247247
const axelarContractAddress = axelarChains[chain];
248248

249249
const gatewayAddy = axelarContractAddress.gateway;
@@ -307,7 +307,7 @@ const constructParams = (chain: SupportedChains) => {
307307
const interchainTransferSentEvent = {
308308
...InterchainTransferSentParams,
309309
target: itsAddy,
310-
chain: chain,
310+
chain: providerChain,
311311
argGetters: {
312312
amount: (log: any) => BigNumber.from(log.amount),
313313
to: (log: any) => log.destinationAddress,
@@ -324,7 +324,7 @@ const constructParams = (chain: SupportedChains) => {
324324
const interchainTransferReceivedEvent = {
325325
...InterchainTransferReceivedParams,
326326
target: itsAddy,
327-
chain: chain,
327+
chain: providerChain,
328328
argGetters: {
329329
amount: (log: any) => BigNumber.from(log.amount),
330330
to: (log: any) => log.destinationAddress,
@@ -348,7 +348,7 @@ const constructParams = (chain: SupportedChains) => {
348348
interchainTransferSentEvent,
349349
interchainTransferReceivedEvent
350350
);
351-
return await getTxDataFromEVMEventLogs("axelar", chain, fromBlock, toBlock, eventParams);
351+
return await getTxDataFromEVMEventLogs("axelar", providerChain as any, fromBlock, toBlock, eventParams);
352352
};
353353
};
354354
const adapter: BridgeAdapter = {
@@ -370,7 +370,7 @@ const adapter: BridgeAdapter = {
370370
mantle: constructParams("mantle"),
371371
moonbeam: constructParams("moonbeam"),
372372
optimism: constructParams("optimism"),
373-
plume: constructParams("plume"),
373+
plume: constructParams("plume", "plume_mainnet"),
374374
polygon: constructParams("polygon"),
375375
scroll: constructParams("scroll"),
376376
monad: constructParams("monad"),

src/adapters/debridgedln/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const withdrawParams: PartialContractEventParams = {
7979
isDeposit: false,
8080
};
8181

82-
const constructParams = (chain: SupportedChains) => {
82+
const constructParams = (chain: SupportedChains, providerChain: string = chain) => {
8383
const eventParams: PartialContractEventParams[] = [];
8484

8585
const token = nativeTokenAddress[chain];
@@ -97,7 +97,7 @@ const constructParams = (chain: SupportedChains) => {
9797
eventParams.push(finalDepositParams, finalWithdrawParams);
9898

9999
return async (fromBlock: number, toBlock: number) =>
100-
getTxDataFromEVMEventLogs("debridgedln", chain, fromBlock, toBlock, eventParams);
100+
getTxDataFromEVMEventLogs("debridgedln", providerChain as any, fromBlock, toBlock, eventParams);
101101
};
102102

103103
type ApiSolanaEvent = {
@@ -158,7 +158,7 @@ const adapter: BridgeAdapter = {
158158
solana: getSolanaEvents,
159159
sonic: constructParams("sonic"),
160160
plasma: constructParams("plasma"),
161-
plume: constructParams("plume"),
161+
plume: constructParams("plume", "plume_mainnet"),
162162
monad: constructParams("monad"),
163163
robinhood: constructParams("robinhood"),
164164
};

src/adapters/hyperlane/index.ts

Lines changed: 65 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Chain } from "@defillama/sdk/build/general";
44
import * as yaml from "js-yaml";
55

66
import { BridgeAdapter } from "../../helpers/bridgeAdapter.type";
7+
import { NonRetryableError } from "../../utils/errors";
78

89
const baseUri = "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/main";
910
const kyveApiBaseUri = "https://data.services.hyperlane.xyz";
@@ -24,7 +25,7 @@ export async function setUp(): Promise<string[]> {
2425
return chains;
2526
}
2627

27-
interface KyveEvent {
28+
export interface KyveEvent {
2829
blockNumber: number;
2930
chain: string;
3031
from: string;
@@ -36,47 +37,75 @@ interface KyveEvent {
3637
usdAmount: string;
3738
}
3839

39-
export const getEvents = async (fromTimestamp: number, toTimestamp: number): Promise<any[]> => {
40+
type HyperlaneFetch = (
41+
input: string,
42+
init?: { headers?: Record<string, string> }
43+
) => Promise<{
44+
ok: boolean;
45+
status: number;
46+
statusText: string;
47+
text(): Promise<string>;
48+
json(): Promise<unknown>;
49+
}>;
50+
51+
export const fetchHyperlaneEvents = async (
52+
fromTimestamp: number,
53+
toTimestamp: number,
54+
fetchImpl: HyperlaneFetch = fetch as HyperlaneFetch
55+
): Promise<KyveEvent[]> => {
4056
const apiUrl = `${kyveApiBaseUri}/events?fromTimestamp=${fromTimestamp}&toTimestamp=${toTimestamp}`;
4157
console.log(apiUrl);
42-
try {
43-
const response = await fetch(apiUrl);
44-
if (!response.ok) {
45-
console.error(`Error fetching data from Kyve API: ${response.statusText}`);
46-
return [];
58+
const response = await fetchImpl(apiUrl, {
59+
headers: {
60+
Accept: "application/json",
61+
"User-Agent": "defillama-bridges-server/1.0",
62+
},
63+
});
64+
if (!response.ok) {
65+
const responseBody = await response.text().catch(() => "");
66+
const detail = responseBody.trim().slice(0, 300);
67+
const message = `Hyperlane events API HTTP ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`;
68+
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
69+
throw new NonRetryableError(message);
4770
}
48-
const events = (await response.json()) as KyveEvent[];
49-
50-
const txData: any[] = events.map((event) => {
51-
let usdAmount: number | undefined = undefined;
52-
try {
53-
usdAmount = parseFloat(event.usdAmount);
54-
if (isNaN(usdAmount)) {
55-
usdAmount = undefined;
56-
}
57-
} catch {
71+
throw new Error(message);
72+
}
73+
74+
const events = await response.json();
75+
if (!Array.isArray(events)) {
76+
throw new NonRetryableError("Hyperlane events API returned a non-array response.");
77+
}
78+
return events as KyveEvent[];
79+
};
80+
81+
export const getEvents = async (fromTimestamp: number, toTimestamp: number): Promise<any[]> => {
82+
const events = await fetchHyperlaneEvents(fromTimestamp, toTimestamp);
83+
const txData: any[] = events.map((event) => {
84+
let usdAmount: number | undefined = undefined;
85+
try {
86+
usdAmount = parseFloat(event.usdAmount);
87+
if (isNaN(usdAmount)) {
5888
usdAmount = undefined;
5989
}
90+
} catch {
91+
usdAmount = undefined;
92+
}
6093

61-
return {
62-
blockNumber: event.blockNumber,
63-
chain: event.chain,
64-
from: event.from,
65-
isDeposit: event.isDeposit,
66-
timestamp: event.timestamp,
67-
to: event.to,
68-
token: event.token,
69-
txHash: event.txHash,
70-
amount: usdAmount,
71-
isUSDVolume: true,
72-
};
73-
});
74-
75-
return txData;
76-
} catch (error) {
77-
console.error(`Error processing Kyve API response:`, error);
78-
return [];
79-
}
94+
return {
95+
blockNumber: event.blockNumber,
96+
chain: event.chain,
97+
from: event.from,
98+
isDeposit: event.isDeposit,
99+
timestamp: event.timestamp,
100+
to: event.to,
101+
token: event.token,
102+
txHash: event.txHash,
103+
amount: usdAmount,
104+
isUSDVolume: true,
105+
};
106+
});
107+
108+
return txData;
80109
};
81110

82111
export async function build(): Promise<BridgeAdapter> {

src/adapters/relay/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,10 @@ export const slugToChainId: Record<string, number> = {
361361
"polygon-zkevm": 1101,
362362
lisk: 1135,
363363
sei: 1329,
364+
hyperliquid: 1337,
364365
hychain: 2911,
366+
ronin: 2020,
367+
somnia: 5031,
365368
mantle: 5000,
366369
ham: 5112,
367370
cyber: 7560,

src/handlers/relayApi.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert/strict";
22
import test from "node:test";
3-
import { makeRequestsUrl, parseRelayRequestsResponse } from "../adapters/relay";
3+
import { makeRequestsUrl, parseRelayRequestsResponse, slugToChainId } from "../adapters/relay";
44

55
test("Relay windows are filtered and sorted by updatedAt", () => {
66
const url = new URL(makeRequestsUrl(100, 200, "next", 1));
@@ -12,6 +12,12 @@ test("Relay windows are filtered and sorted by updatedAt", () => {
1212
assert.equal(url.searchParams.get("chainId"), "1");
1313
});
1414

15+
test("Relay maps currently supported non-EVM and emerging chain IDs", () => {
16+
assert.equal(slugToChainId.hyperliquid, 1337);
17+
assert.equal(slugToChainId.ronin, 2020);
18+
assert.equal(slugToChainId.somnia, 5031);
19+
});
20+
1521
test("Relay rejects malformed successful responses before checkpoint advancement", () => {
1622
assert.throws(() => parseRelayRequestsResponse({}), /requests array/);
1723
assert.throws(() => parseRelayRequestsResponse({ requests: [], continuation: 123 }), /continuation/);

src/handlers/relayProgress.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert/strict";
22
import test from "node:test";
3-
import { requireRelayChainId, resolveRelayWindowFromCheckpoint } from "./relayProgress";
3+
import { getRelayBootstrapCheckpoint, requireRelayChainId, resolveRelayWindowFromCheckpoint } from "./relayProgress";
44

55
const defaults = {
66
now: 10_000,
@@ -29,6 +29,31 @@ test("Relay bootstraps from the configured lookback when the Redis key is missin
2929
});
3030
});
3131

32+
test("Relay persists a stable bootstrap low-water mark across failed hourly runs", () => {
33+
const checkpoint = getRelayBootstrapCheckpoint(defaults.now, defaults.bootstrapLookbackSeconds);
34+
const firstRun = resolveRelayWindowFromCheckpoint({
35+
...defaults,
36+
checkpoint,
37+
source: "redis",
38+
});
39+
const nextHourlyRun = resolveRelayWindowFromCheckpoint({
40+
...defaults,
41+
now: defaults.now + 3_600,
42+
checkpoint,
43+
source: "redis",
44+
});
45+
46+
assert.equal(firstRun.startTs, 7_700);
47+
assert.equal(nextHourlyRun.startTs, firstRun.startTs);
48+
assert.equal(nextHourlyRun.endTs, defaults.now + 3_600);
49+
});
50+
51+
test("Relay validates bootstrap checkpoint inputs", () => {
52+
assert.equal(getRelayBootstrapCheckpoint(10_000, 2_000), 8_000);
53+
assert.throws(() => getRelayBootstrapCheckpoint(0, 2_000), /positive Unix timestamp/);
54+
assert.throws(() => getRelayBootstrapCheckpoint(10_000, 0), /positive integer/);
55+
});
56+
3257
test("Relay fails closed with a far-future checkpoint", () => {
3358
assert.throws(
3459
() => resolveRelayWindowFromCheckpoint({ ...defaults, checkpoint: 10_061, source: "redis" }),

src/handlers/relayProgress.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import { NonRetryableError } from "../utils/errors";
33
export type RelayCheckpointSource = "redis" | "bootstrap";
44
export const RELAY_CHECKPOINT_MAX_FUTURE_SKEW_SECONDS = 60;
55

6+
export const getRelayBootstrapCheckpoint = (now: number, bootstrapLookbackSeconds: number): number => {
7+
if (!Number.isSafeInteger(now) || now <= 0) {
8+
throw new NonRetryableError(`Relay bootstrap time must be a positive Unix timestamp; received ${now}.`);
9+
}
10+
if (!Number.isSafeInteger(bootstrapLookbackSeconds) || bootstrapLookbackSeconds <= 0) {
11+
throw new NonRetryableError(
12+
`Relay bootstrap lookback must be a positive integer; received ${bootstrapLookbackSeconds}.`
13+
);
14+
}
15+
return Math.max(1, now - bootstrapLookbackSeconds);
16+
};
17+
618
export const requireRelayChainId = (leg: "deposit" | "withdrawal", chainId?: number): number => {
719
if (!Number.isInteger(chainId) || Number(chainId) <= 0) {
820
throw new NonRetryableError(`Relay ${leg} is missing a valid chain ID`);

src/handlers/runRelay.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import dayjs from "dayjs";
77
import { formatError, NonRetryableError, throwIfAborted } from "../utils/errors";
88
import {
99
RelayCheckpointSource,
10+
getRelayBootstrapCheckpoint,
1011
requireRelayChainId,
1112
resolveRelayWindowFromCheckpoint,
1213
validateRelayCheckpoint,
@@ -81,9 +82,18 @@ const getLatestRelayCheckpoint = async (
8182
source: RelayCheckpointSource;
8283
}> => {
8384
const durableCheckpoint = await getDurableCheckpoint(RELAY_CHECKPOINT_KEY);
84-
return durableCheckpoint !== null
85-
? { checkpoint: validateRelayCheckpoint(durableCheckpoint, now), source: "redis" }
86-
: { checkpoint: null, source: "bootstrap" };
85+
if (durableCheckpoint !== null) {
86+
return { checkpoint: validateRelayCheckpoint(durableCheckpoint, now), source: "redis" };
87+
}
88+
89+
const bootstrapCheckpoint = getRelayBootstrapCheckpoint(now, BOOTSTRAP_LOOKBACK_HOURS * 60 * 60);
90+
const storedCheckpoint = await advanceDurableCheckpoint(RELAY_CHECKPOINT_KEY, bootstrapCheckpoint);
91+
console.log(
92+
`Relay bootstrap checkpoint initialized in Redis: ${dayjs
93+
.unix(storedCheckpoint)
94+
.toISOString()} (${RELAY_CHECKPOINT_KEY})`
95+
);
96+
return { checkpoint: validateRelayCheckpoint(storedCheckpoint, now), source: "redis" };
8797
};
8898

8999
const resolveWindow = async () => {

0 commit comments

Comments
 (0)