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
144 changes: 117 additions & 27 deletions src/monitor/Monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import {
sortEventsAscending,
} from "../utils";
import { MonitorClients, updateMonitorClients } from "./MonitorClientHelper";
import { MonitorConfig } from "./MonitorConfig";
import { MonitorConfig, L2Token } from "./MonitorConfig";
import { getImpliedBundleBlockRanges } from "../dataworker/DataworkerUtils";
import { PUBLIC_NETWORKS, TOKEN_EQUIVALENCE_REMAPPING } from "@across-protocol/constants";
import { utils as sdkUtils, arch } from "@across-protocol/sdk";
Expand Down Expand Up @@ -94,6 +94,7 @@ export class Monitor {
private balanceCache: { [chainId: number]: { [token: string]: { [account: string]: BigNumber } } } = {};
private decimals: { [chainId: number]: { [token: string]: number } } = {};
private additionalL1Tokens: L1Token[] = [];
private l2OnlyTokens: L2Token[] = [];
// Chains for each spoke pool client.
public monitorChains: number[];
// Chains that we care about inventory manager activity on, so doesn't include Ethereum which doesn't
Expand Down Expand Up @@ -126,6 +127,7 @@ export class Monitor {
address: l1TokenInfo.address,
};
});
this.l2OnlyTokens = monitorConfig.l2OnlyTokens;
this.l1Tokens = this.clients.hubPoolClient.getL1Tokens();
this.bundleDataApproxClient = new BundleDataApproxClient(
this.clients.spokePoolClients,
Expand All @@ -136,6 +138,46 @@ export class Monitor {
);
}

/**
* Returns L2-only tokens for a specific chain.
*/
private getL2OnlyTokensForChain(chainId: number): L2Token[] {
return this.l2OnlyTokens.filter((token) => token.chainId === chainId);
}

/**
* Generates markdown report for a token's balances across the specified chains.
* Returns the token markdown section and summary entry.
*/
private generateTokenBalanceMarkdown(
report: RelayerBalanceTable,
token: { symbol: string; decimals: number },
chainNames: string[],
labelSuffix = ""
): { mrkdwn: string; summaryEntry: string } {
let tokenMrkdwn = "";
for (const chainName of chainNames) {
const balancesBN = Object.values(report[token.symbol]?.[chainName] ?? {});
if (balancesBN.find((b) => b.gt(bnZero))) {
const balances = balancesBN.map((balance) =>
balance.gt(bnZero) ? convertFromWei(balance.toString(), token.decimals) : "0"
);
tokenMrkdwn += `${chainName}: ${balances.join(", ")}\n`;
} else {
tokenMrkdwn += `${chainName}: 0\n`;
}
}

const totalBalance = report[token.symbol]?.[ALL_CHAINS_NAME]?.[BalanceType.TOTAL] ?? bnZero;
if (totalBalance.gt(bnZero)) {
return {
mrkdwn: `*[${token.symbol}${labelSuffix}]*\n` + tokenMrkdwn,
summaryEntry: `${token.symbol}: ${convertFromWei(totalBalance.toString(), token.decimals)}\n`,
};
}
return { mrkdwn: "", summaryEntry: `${token.symbol}: 0\n` };
}

public async update(): Promise<void> {
// Clear balance cache at the start of each update.
// Note: decimals don't need to be cleared because they shouldn't ever change.
Expand Down Expand Up @@ -454,11 +496,11 @@ export class Monitor {
async reportRelayerBalances(): Promise<void> {
const relayers = this.monitorConfig.monitoredRelayers;
const allL1Tokens = this.getL1TokensForRelayerBalancesReport();
const l2OnlyTokens = this.l2OnlyTokens;

// @dev TODO: Handle special case for tokens that do not have an L1 token mapped to them via PoolRebalanceRoutes
const chainIds = this.monitorChains;
const allChainNames = chainIds.map(getNetworkName).concat([ALL_CHAINS_NAME]);
const reports = this.initializeBalanceReports(relayers, allL1Tokens, allChainNames);
const reports = this.initializeBalanceReports(relayers, allL1Tokens, l2OnlyTokens, allChainNames);

await this.updateCurrentRelayerBalances(reports);
await this.updateLatestAndFutureRelayerRefunds(reports);
Expand All @@ -467,30 +509,24 @@ export class Monitor {
const report = reports[relayer.toNative()];
let summaryMrkdwn = "*[Summary]*\n";
let mrkdwn = "Token amounts: current, pending execution, cross-chain transfers, total\n";

// Report L1 tokens (all chains)
for (const token of allL1Tokens) {
let tokenMrkdwn = "";
for (const chainName of allChainNames) {
const balancesBN = Object.values(report[token.symbol][chainName]);
if (balancesBN.find((b) => b.gt(bnZero))) {
// Human-readable balances
const balances = balancesBN.map((balance) =>
balance.gt(bnZero) ? convertFromWei(balance.toString(), token.decimals) : "0"
);
tokenMrkdwn += `${chainName}: ${balances.join(", ")}\n`;
} else {
// Shorten balances in the report if everything is 0.
tokenMrkdwn += `${chainName}: 0\n`;
}
}
const { mrkdwn: tokenMrkdwn, summaryEntry } = this.generateTokenBalanceMarkdown(report, token, allChainNames);
mrkdwn += tokenMrkdwn;
summaryMrkdwn += summaryEntry;
}

const totalBalance = report[token.symbol][ALL_CHAINS_NAME][BalanceType.TOTAL];
// Update corresponding summary section for current token.
if (totalBalance.gt(bnZero)) {
mrkdwn += `*[${token.symbol}]*\n` + tokenMrkdwn;
summaryMrkdwn += `${token.symbol}: ${convertFromWei(totalBalance.toString(), token.decimals)}\n`;
} else {
summaryMrkdwn += `${token.symbol}: 0\n`;
}
// Report L2-only tokens (only their specific chain)
for (const token of l2OnlyTokens) {
const { mrkdwn: tokenMrkdwn, summaryEntry } = this.generateTokenBalanceMarkdown(
report,
token,
[getNetworkName(token.chainId)],
" (L2-only)"
);
mrkdwn += tokenMrkdwn;
summaryMrkdwn += summaryEntry;
}

mrkdwn += summaryMrkdwn;
Expand All @@ -500,9 +536,15 @@ export class Monitor {
mrkdwn,
});
}

// Build a combined token list for decimal lookups in the debug logging
const allTokensWithDecimals = new Map<string, number>();
allL1Tokens.forEach((token) => allTokensWithDecimals.set(token.symbol, token.decimals));
l2OnlyTokens.forEach((token) => allTokensWithDecimals.set(token.symbol, token.decimals));

Object.entries(reports).forEach(([relayer, balanceTable]) => {
Object.entries(balanceTable).forEach(([tokenSymbol, columns]) => {
const decimals = allL1Tokens.find((token) => token.symbol === tokenSymbol)?.decimals;
const decimals = allTokensWithDecimals.get(tokenSymbol);
if (!decimals) {
throw new Error(`No decimals found for ${tokenSymbol}`);
}
Expand Down Expand Up @@ -535,6 +577,7 @@ export class Monitor {
// Update current balances of all tokens on each supported chain for each relayer.
async updateCurrentRelayerBalances(relayerBalanceReport: RelayerBalanceReport): Promise<void> {
const l1Tokens = this.getL1TokensForRelayerBalancesReport();

for (const relayer of this.monitorConfig.monitoredRelayers) {
for (const chainId of this.monitorChains) {
// If the monitored relayer address is invalid on the monitored chain (e.g. the monitored relayer is a base58 address while the chain ID is mainnet),
Expand Down Expand Up @@ -566,6 +609,30 @@ export class Monitor {
decimalConverter(tokenBalances[i])
);
}

// Handle L2-only tokens for this chain
const l2OnlyTokensForChain = this.getL2OnlyTokensForChain(chainId);
if (l2OnlyTokensForChain.length > 0) {
const l2OnlyBalances = await this._getBalances(
l2OnlyTokensForChain.map((token) => ({
token: token.address,
chainId: chainId,
account: relayer,
}))
);

for (let i = 0; i < l2OnlyTokensForChain.length; i++) {
const token = l2OnlyTokensForChain[i];
// L2-only tokens don't need decimal conversion since they don't map to L1
this.updateRelayerBalanceTable(
relayerBalanceReport[relayer.toNative()],
token.symbol,
getNetworkName(chainId),
BalanceType.CURRENT,
l2OnlyBalances[i]
);
}
}
}
}
}
Expand Down Expand Up @@ -1253,10 +1320,17 @@ export class Monitor {
return transfers.map((transfer) => transfer.value).reduce((a, b) => a.add(b));
}

initializeBalanceReports(relayers: Address[], allL1Tokens: L1Token[], allChainNames: string[]): RelayerBalanceReport {
initializeBalanceReports(
relayers: Address[],
allL1Tokens: L1Token[],
l2OnlyTokens: L2Token[],
allChainNames: string[]
): RelayerBalanceReport {
const reports: RelayerBalanceReport = {};
for (const relayer of relayers) {
reports[relayer.toNative()] = {};

// Initialize L1 tokens for all chains
for (const token of allL1Tokens) {
reports[relayer.toNative()][token.symbol] = {};
for (const chainName of allChainNames) {
Expand All @@ -1266,6 +1340,22 @@ export class Monitor {
}
}
}

// Initialize L2-only tokens for their specific chain and the "All chains" summary
for (const token of l2OnlyTokens) {
const tokenChainName = getNetworkName(token.chainId);
reports[relayer.toNative()][token.symbol] = {};
// Initialize for the specific chain the token exists on
reports[relayer.toNative()][token.symbol][tokenChainName] = {};
for (const balanceType of ALL_BALANCE_TYPES) {
reports[relayer.toNative()][token.symbol][tokenChainName][balanceType] = bnZero;
}
// Initialize for "All chains" summary
reports[relayer.toNative()][token.symbol][ALL_CHAINS_NAME] = {};
for (const balanceType of ALL_BALANCE_TYPES) {
reports[relayer.toNative()][token.symbol][ALL_CHAINS_NAME][balanceType] = bnZero;
}
}
}
return reports;
}
Expand Down
36 changes: 36 additions & 0 deletions src/monitor/MonitorConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import winston from "winston";
import { MAINNET_CHAIN_IDs } from "@across-protocol/constants";
import { CommonConfig, ProcessEnv } from "../common";
import {
CHAIN_IDs,
Expand All @@ -7,8 +8,18 @@ import {
TOKEN_SYMBOLS_MAP,
Address,
toAddressType,
EvmAddress,
} from "../utils";

// Interface for tokens that exist only on L2 (no L1 equivalent)
// @TODO: Move this to SDK
export interface L2Token {
symbol: string;
chainId: number;
address: EvmAddress;
decimals: number;
}

// Set modes to true that you want to enable in the AcrossMonitor bot.
export interface BotModes {
balancesEnabled: boolean;
Expand Down Expand Up @@ -45,6 +56,7 @@ export class MonitorConfig extends CommonConfig {
token: Address;
}[] = [];
readonly additionalL1NonLpTokens: string[] = [];
readonly l2OnlyTokens: L2Token[] = [];
readonly binanceWithdrawWarnThreshold: number;
readonly binanceWithdrawAlertThreshold: number;
readonly hyperliquidOrderMaximumLifetime: number;
Expand Down Expand Up @@ -77,6 +89,7 @@ export class MonitorConfig extends CommonConfig {
CLOSE_PDAS_ENABLED,
HYPERLIQUID_ORDER_MAXIMUM_LIFETIME,
HYPERLIQUID_SUPPORTED_TOKENS,
L2_ONLY_TOKENS,
} = env;

this.botModes = {
Expand Down Expand Up @@ -109,6 +122,29 @@ export class MonitorConfig extends CommonConfig {
return TOKEN_SYMBOLS_MAP[token]?.addresses?.[CHAIN_IDs.MAINNET];
}
});

// Parse L2-only tokens: tokens that exist only on L2 chains (no L1 equivalent).
// Format: ["USDH", "OTHER_TOKEN"] - array of token symbols
// - will look up token info from TOKEN_SYMBOLS_MAP
// - will create entries for all chains in MAINNET_CHAIN_IDs where the token has an address
// - all monitored relayers (MONITORED_RELAYERS) will be tracked for these tokens
const l2OnlySymbols: string[] = JSON.parse(L2_ONLY_TOKENS ?? "[]");
const mainnetChainIds = Object.values(MAINNET_CHAIN_IDs) as number[];
this.l2OnlyTokens = l2OnlySymbols.flatMap((symbol) => {
const tokenInfo = TOKEN_SYMBOLS_MAP[symbol];
if (!tokenInfo?.addresses) {
return [];
}
return mainnetChainIds
.filter((chainId) => isDefined(tokenInfo.addresses[chainId]))
.map((chainId) => ({
symbol,
chainId,
address: EvmAddress.from(tokenInfo.addresses[chainId]),
decimals: tokenInfo.decimals,
}));
});

this.binanceWithdrawWarnThreshold = Number(BINANCE_WITHDRAW_WARN_THRESHOLD ?? 1);
this.binanceWithdrawAlertThreshold = Number(BINANCE_WITHDRAW_ALERT_THRESHOLD ?? 1);

Expand Down
2 changes: 2 additions & 0 deletions test/Monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ describe("Monitor", async function () {
const reports = monitorInstance.initializeBalanceReports(
monitorInstance.monitorConfig.monitoredRelayers,
monitorInstance.clients.hubPoolClient.getL1Tokens(),
[], // No L2-only tokens in test
TEST_NETWORK_NAMES
);
await monitorInstance.updateCurrentRelayerBalances(reports);
Expand Down Expand Up @@ -284,6 +285,7 @@ describe("Monitor", async function () {
const reports = monitorInstance.initializeBalanceReports(
monitorInstance.monitorConfig.monitoredRelayers,
monitorInstance.clients.hubPoolClient.getL1Tokens(),
[], // No L2-only tokens in test
TEST_NETWORK_NAMES
);
await monitorInstance.updateLatestAndFutureRelayerRefunds(reports);
Expand Down