Skip to content
Merged
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
2 changes: 1 addition & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"formatter": {
"enabled": true,
"formatWithErrors": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/block-sync-monitor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"dependencies": {
"@intmax2-function/shared": "workspace:*",
"axios": "^1.11.0",
"viem": "^2.33.3"
"viem": "^2.34.0"
},
"scripts": {
"start": "node dist/index.js",
Expand Down
3 changes: 3 additions & 0 deletions packages/bridge-monitor/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ export const LAYER_ZERO_SCAN_API = {
["mainnet"]: "https://scan.layerzero-api.com/v1",
["testnet"]: "https://scan-testnet.layerzero-api.com/v1",
} as const;

export const MAX_RETRIES = 3;
export const RETRY_DELAY_MS = 10000;
63 changes: 63 additions & 0 deletions packages/bridge-monitor/src/lib/layerzero.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { API_TIMEOUT, config, logger, sleep } from "@intmax2-function/shared";
import axios, { AxiosError } from "axios";
import { LAYER_ZERO_SCAN_API, MAX_RETRIES, RETRY_DELAY_MS } from "../constants";
import type { BridgeGuidTransaction, BridgeGuidTransactionResponse } from "../types";

export const fetchBridgeGuidTransaction = async (
guid: string,
maxRetries: number = MAX_RETRIES,
retryDelayMs: number = RETRY_DELAY_MS,
) => {
const layerZeroMessagesUrl = `${LAYER_ZERO_SCAN_API[config.LAYER_ZERO_NETWORK]}/messages/guid/${guid}`;

let lastError: Error | null = null;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await axios.get<BridgeGuidTransactionResponse>(layerZeroMessagesUrl, {
timeout: API_TIMEOUT,
headers: {
Accept: "application/json",
},
});

if (response.data?.data === undefined) {
throw new Error("Data is missing in the response");
}

const transactions = response.data.data as BridgeGuidTransaction[];
if (transactions.length === 0) {
throw new Error("No transactions found");
}

return transactions[0];
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));

logger.warn(
`Failed to fetch bridge transaction (attempt ${attempt}/${maxRetries}): ${lastError.message}`,
);

if (attempt === maxRetries) {
break;
}

logger.info(`Waiting ${retryDelayMs}ms before retry...`);
await sleep(retryDelayMs);
}
}

logger.error(
`Failed to fetch bridge transaction after ${maxRetries} attempts: ${layerZeroMessagesUrl}`,
);

if (lastError instanceof AxiosError) {
throw new Error(
`Failed to fetch status after ${maxRetries} attempts: ${lastError.response?.status}`,
);
}

throw new Error(
`Unexpected error while fetching bridge transaction status after ${maxRetries} attempts: ${lastError?.message}`,
);
};
96 changes: 68 additions & 28 deletions packages/bridge-monitor/src/service/job.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
BridgeTransaction,
type BridgeTransactionData,
BridgeTransactionStatus,
logger,
} from "@intmax2-function/shared";
import { fetchBridgeGuidTransaction } from "../lib/layerzero";
import type { UpdateParams } from "../types";
import {
fetchBridgeGuidTransaction,
handleFailedStatus,
handleInflightOrConfirming,
handleInflightOrConfirmingStatus,
handlePayloadStored,
handleVerifiedStatus,
} from "./process.service";
Expand All @@ -20,7 +22,7 @@ export const performJob = async () => {
BridgeTransactionStatus.VERIFIED,
BridgeTransactionStatus.BLOCKED,
],
alertSent: false,
// alertSent: not true(include undefined)
});

const sortedTransactions = bridgeTransactions.sort((a, b) => a.nonce - b.nonce);
Expand All @@ -31,30 +33,68 @@ export const performJob = async () => {
};

const processBridgeTransaction = async (bridgeTransaction: BridgeTransactionData) => {
// TODO: sleep and retry
const bridgeGuidTransaction = await fetchBridgeGuidTransaction(bridgeTransaction.guid);
const statusName = bridgeGuidTransaction.status.name;

switch (statusName) {
case BridgeTransactionStatus.FAILED:
await handleFailedStatus(bridgeGuidTransaction);
break;

case BridgeTransactionStatus.INFLIGHT:
case BridgeTransactionStatus.CONFIRMING:
await handleInflightOrConfirming(bridgeTransaction);
break;

case BridgeTransactionStatus.VERIFIED:
await handleVerifiedStatus(bridgeTransaction);
break;

case BridgeTransactionStatus.PAYLOAD_STORED:
await handlePayloadStored(bridgeGuidTransaction);
break;
}
try {
const bridgeGuidTransaction = await fetchBridgeGuidTransaction(bridgeTransaction.guid);
const statusName = bridgeGuidTransaction.status.name as BridgeTransactionStatus;

await BridgeTransaction.getInstance().updateBridgeTransaction(bridgeTransaction.guid, {
status: statusName as BridgeTransactionStatus,
});
let updateParams: UpdateParams = {
status: statusName,
};

switch (statusName) {
case BridgeTransactionStatus.FAILED: {
const result = await handleFailedStatus({ bridgeTransaction, bridgeGuidTransaction });
updateParams = { ...updateParams, ...result };
break;
}
case BridgeTransactionStatus.INFLIGHT:
case BridgeTransactionStatus.CONFIRMING: {
const result = await handleInflightOrConfirmingStatus(bridgeTransaction);
if (result) {
updateParams = { ...updateParams, ...result };
}
break;
}
case BridgeTransactionStatus.VERIFIED: {
const result = await handleVerifiedStatus({
bridgeTransaction,
bridgeGuidTransaction,
});
updateParams = { ...updateParams, ...result };
break;
}
case BridgeTransactionStatus.PAYLOAD_STORED: {
const result = await handlePayloadStored({
bridgeTransaction,
bridgeGuidTransaction,
});
updateParams = { ...updateParams, ...result };
break;
}
}

logger.info(
`Updated bridge transaction ${bridgeTransaction.guid} with status: ${updateParams.status}`,
);

await BridgeTransaction.getInstance().updateBridgeTransaction(
bridgeTransaction.guid,
updateParams,
);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Failed to process bridge transaction ${bridgeTransaction.guid}: ${errorMessage}`);

if (errorMessage.includes("404")) {
logger.warn(
`Updated bridge transaction ${bridgeTransaction.guid} with status: ${BridgeTransactionStatus.NOT_FOUND}`,
);
await BridgeTransaction.getInstance().updateBridgeTransaction(bridgeTransaction.guid, {
status: BridgeTransactionStatus.NOT_FOUND,
});
return;
}

throw error;
}
};
Loading