Skip to content

Commit 4feb9dd

Browse files
nicholaspaiclaude
andauthored
refactor: Simplify Monitor balance reporting and delete TokenTransferClient (#3111)
* improve: Add utility functions for inventory equivalence remapping, Binance deposit matching, and floatToBN fix - TokenUtils: getInventoryEquivalentL1TokenAddress, getInventoryBalanceContributorTokens, isL2OnlyEquivalentToken - RunningBalanceUtils: getLatestRunningBalances extracted for shared use - BinanceUtils: getOutstandingBinanceDeposits for cross-L2 deposit matching, export BinanceDeposit type - BNUtils: Fix floatToBN MAX_SAFE_INTEGER overflow via string manipulation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * improve: Use inventory equivalence remapping in BundleDataApproxClient, InventoryClient, TokenClient - BundleDataApproxClient: returns values in L1 token decimals, groups deposits/refunds across inventory-equivalent L2 tokens, restores requireExecution param, fixes refund decimal conversion - InventoryClient: uses getInventoryEquivalentL1TokenAddress, getInventoryBalanceContributorTokens, getLatestRunningBalances; adds protected getTokenInfo for test mocking - TokenClient: uses getInventoryBalanceContributorTokens for L2 token discovery - Test updates: alias config for USDC, L1-decimal refunds, MockInventoryClient getTokenInfo override Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * improve: Fix Binance CEX pending withdrawal logic and adapter cleanup - BinanceCEXBridge: rewrite getL2PendingWithdrawalAmount to correctly compute outstanding deposits when finalizer batches deposits from multiple L2s into single L1 withdrawal. Uses getOutstandingBinanceDeposits with Redis-cached deposit sender lookups. - BaseChainAdapter: use getInventoryEquivalentL1TokenAddress, skip zero-amount outstanding transfers in getOutstandingCrossChainTransfers - AdapterManager: minor cleanup - MockLineaEvents: accept amount parameter for BridgingInitiatedV2/BridgingFinalizedV2 - Generic adapter tests: updated for zero-amount skip behavior Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update InventoryClient.InventoryRebalance.ts * refactor: Simplify Monitor balance reporting and delete TokenTransferClient - Monitor.reportRelayerBalances: rewritten to use getInventoryBalanceContributorTokens for per-chain L2 token discovery, batch-fetch all balances in parallel, produce aligned table per token. Adds protected getTokenInfo for test mocking. Fixes pending rebalance double-count by only applying to canonical L2 token. - MonitorConfig: removes L2Token interface, l2OnlyTokens, L2_ONLY_TOKENS env var - MonitorClientHelper: removes TokenTransferClient construction - Delete TokenTransferClient (no longer needed) - Delete unused BalanceType/RelayerBalanceReport/TokenTransfer interfaces Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update Monitor.ts * Update Monitor.ts * Update Monitor.ts * Fix various issues: - MonitorClientHelper: Should filter out duplicate spoke pool address strings passed into AdapterManager - AdapterManager: should filter out hub/spoke pool addresses from monitored addresses passed to BaseChainAdapter if the token doesn't have a pool rebalance route to that chain. - BaseChainAdapter: computes outstanding l1->l2 transfer amount using net amount approach rather than marking off events one by one, which better supports adapters with Binance which don't have this 1:1 relationship assumption * Fix tests, change monitor logging to be mobile-friendly, fix base chain adapter codex suggestion * Fix tests * lint --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce39568 commit 4feb9dd

22 files changed

Lines changed: 443 additions & 1254 deletions

src/adapter/BaseChainAdapter.ts

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ import {
3838
sendAndConfirmSolanaTransaction,
3939
getSvmProvider,
4040
submitTransaction,
41+
getTokenInfo,
42+
ConvertDecimals,
43+
toAddressType,
4144
} from "../utils";
4245
import { AugmentedTransaction, TransactionClient } from "../clients/TransactionClient";
4346
import {
@@ -69,7 +72,7 @@ export class BaseChainAdapter {
6972
spokePoolClients: { [chainId: number]: SpokePoolClient },
7073
protected readonly chainId: number,
7174
protected readonly hubChainId: number,
72-
protected readonly monitoredAddresses: Address[],
75+
protected readonly monitoredAddresses: { [l1Token: string]: Address[] },
7376
protected readonly logger: winston.Logger,
7477
public readonly supportedTokens: SupportedTokenSymbol[],
7578
protected readonly bridges: { [l1Token: string]: BaseBridgeAdapter },
@@ -532,8 +535,17 @@ export class BaseChainAdapter {
532535

533536
const outstandingTransfers: OutstandingTransfers = {};
534537

535-
await forEachAsync(this.monitoredAddresses, async (monitoredAddress) => {
536-
await forEachAsync(availableL1Tokens, async (l1Token) => {
538+
this.logger.debug({
539+
at: `${this.adapterName}#getOutstandingCrossChainTransfers`,
540+
message: "Getting outstanding cross chain transfers",
541+
monitoredAddresses: Object.fromEntries(
542+
Object.entries(this.monitoredAddresses).map(([l1Token, addresses]) => [l1Token, addresses])
543+
),
544+
});
545+
546+
await forEachAsync(availableL1Tokens, async (l1Token) => {
547+
const monitoredAddresses = this.monitoredAddresses[l1Token.toNative()];
548+
await forEachAsync(monitoredAddresses, async (monitoredAddress) => {
537549
const bridge = this.bridges[l1Token.toNative()];
538550
const [depositInitiatedResults, depositFinalizedResults] = await Promise.all([
539551
bridge.queryL1BridgeInitiationEvents(l1Token, monitoredAddress, monitoredAddress, l1SearchConfig),
@@ -544,27 +556,37 @@ export class BaseChainAdapter {
544556
this.chainId,
545557
monitoredAddress
546558
);
547-
548559
Object.entries(depositInitiatedResults).forEach(([l2Token, depositInitiatedEvents]) => {
549-
const trackedInitiatedEvents = depositInitiatedEvents.filter(
560+
const filteredDepositEvents = (depositInitiatedEvents ?? []).filter(
550561
(event) => !ignoredPendingBridgeTxnRefs.has(event.txnRef)
551562
);
552-
const finalizedAmounts = depositFinalizedResults?.[l2Token]?.map((event) => event.amount.toString()) ?? [];
553-
const outstandingInitiatedEvents: typeof trackedInitiatedEvents = [];
554-
const totalAmount = trackedInitiatedEvents.reduce((acc, event) => {
555-
// Remove the first match. This handles scenarios where are collisions by amount.
556-
const index = finalizedAmounts.indexOf(event.amount.toString());
557-
if (index > -1) {
558-
finalizedAmounts.splice(index, 1);
559-
return acc;
560-
}
561-
outstandingInitiatedEvents.push(event);
563+
const totalDepositedAmount = filteredDepositEvents.reduce((acc, event) => {
562564
return acc.add(event.amount);
563565
}, bnZero);
566+
const l2TokenDecimals = getTokenInfo(toAddressType(l2Token, this.chainId), this.chainId).decimals;
567+
const l1TokenDecimals = getTokenInfo(l1Token, this.hubChainId).decimals;
568+
const totalFinalizedAmount = (depositFinalizedResults?.[l2Token] ?? []).reduce((acc, event) => {
569+
return acc.add(ConvertDecimals(l2TokenDecimals, l1TokenDecimals)(event.amount));
570+
}, bnZero);
571+
572+
// If there is a net unfinalized amount, go through deposit initiated events in newest to oldest order
573+
// and assume that the newest bridges are the ones that are not yet finalized.
574+
const outstandingInitiatedEvents: string[] = [];
575+
const totalAmount = totalDepositedAmount.sub(totalFinalizedAmount);
576+
let remainingUnfinalizedAmount = totalAmount;
577+
if (remainingUnfinalizedAmount.gt(0)) {
578+
for (const depositEvent of filteredDepositEvents) {
579+
if (remainingUnfinalizedAmount.lte(0)) {
580+
break;
581+
}
582+
outstandingInitiatedEvents.push(depositEvent.txnRef);
583+
remainingUnfinalizedAmount = remainingUnfinalizedAmount.sub(depositEvent.amount);
584+
}
585+
}
564586
if (totalAmount.gt(0) && outstandingInitiatedEvents.length > 0) {
565587
assign(outstandingTransfers, [monitoredAddress.toNative(), l1Token.toNative(), l2Token], {
566588
totalAmount,
567-
depositTxHashes: outstandingInitiatedEvents.map((event) => event.txnRef),
589+
depositTxHashes: outstandingInitiatedEvents,
568590
});
569591
}
570592
});

src/adapter/bridges/BinanceCEXBridge.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getBinanceDepositType,
2525
BinanceTransactionType,
2626
getBinanceWithdrawalType,
27+
toAddressType,
2728
} from "../../utils";
2829
import { BaseBridgeAdapter, BridgeTransactionDetails, BridgeEvents, BridgeEvent } from "./BaseBridgeAdapter";
2930
import ERC20_ABI from "../../common/abi/MinimalERC20.json";
@@ -159,12 +160,13 @@ export class BinanceCEXBridge extends BaseBridgeAdapter {
159160
withdrawalType !== BinanceTransactionType.SWAP
160161
);
161162
});
162-
const { decimals: l1Decimals } = getTokenInfo(l1Token, this.hubChainId);
163+
const l2TokenAddress = this.resolveL2TokenAddress(l1Token);
164+
const { decimals: l2Decimals } = getTokenInfo(toAddressType(l2TokenAddress, this.l2chainId), this.l2chainId);
163165

164166
return {
165-
[this.resolveL2TokenAddress(l1Token)]: await mapAsync(withdrawalHistory, async (withdrawal) => {
167+
[l2TokenAddress]: await mapAsync(withdrawalHistory, async (withdrawal) => {
166168
const txnReceipt = await this.l2Provider.getTransactionReceipt(withdrawal.txId);
167-
return this.toBridgeEvent(floatToBN(withdrawal.amount, l1Decimals), txnReceipt);
169+
return this.toBridgeEvent(floatToBN(withdrawal.amount, l2Decimals), txnReceipt);
168170
}),
169171
};
170172
}

src/adapter/bridges/BridgeApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export class BridgeApi extends BaseBridgeAdapter {
106106
const statusesGrouped = groupObjectCountsByProp(pendingTransfers, (pendingTransfer) => pendingTransfer.state);
107107
this.logger.debug({
108108
at: "BridgeApi#queryL1BridgeInitiationEvents",
109-
message: "Pending transfer statuses",
109+
message: `Pending transfer statuses for ${this.l1TokenInfo.symbol} and ${toAddress}`,
110110
statusesGrouped,
111111
});
112112

src/clients/TokenTransferClient.ts

Lines changed: 0 additions & 103 deletions
This file was deleted.

src/clients/bridges/AdapterManager.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,19 @@ export class AdapterManager {
5656
(client) => client.spokePoolAddress
5757
);
5858

59+
const isHubPoolOrSpokePoolAddress = (chainId: number, address: Address) => {
60+
return (
61+
EvmAddress.from(this.hubPoolClient.hubPool.address).eq(address) ||
62+
this.spokePoolManager.getClient(chainId)?.spokePoolAddress.eq(address)
63+
);
64+
};
65+
5966
// The adapters are only set up to monitor EOA's and the HubPool and SpokePool address, so remove
6067
// spoke pool addresses from other chains.
6168
const filterMonitoredAddresses = (chainId: number) => {
6269
return monitoredAddresses.filter(
6370
(address) =>
64-
EvmAddress.from(this.hubPoolClient.hubPool.address).eq(address) ||
65-
this.spokePoolManager.getClient(chainId)?.spokePoolAddress.eq(address) ||
71+
isHubPoolOrSpokePoolAddress(chainId, address) ||
6672
!spokePoolAddresses.some((spokePoolAddress) => spokePoolAddress.eq(address))
6773
);
6874
};
@@ -129,12 +135,34 @@ export class AdapterManager {
129135
);
130136
};
131137
Object.values(this.spokePoolManager.getSpokePoolClients()).map(({ chainId }) => {
138+
// Filter hub/spoke pool addresses from the monitored addresses for chains that don't have a pool rebalance
139+
// route for the l1 token.
140+
const monitoredAddresses = Object.fromEntries(
141+
(SUPPORTED_TOKENS[chainId] ?? []).map((symbol) => {
142+
const l1Token = TOKEN_SYMBOLS_MAP[symbol].addresses[hubChainId];
143+
return [
144+
l1Token,
145+
filterMonitoredAddresses(chainId).filter((address) => {
146+
if (!l1Token) {
147+
return false;
148+
}
149+
const hasPoolRebalanceRoute = hubPoolClient.l2TokenEnabledForL1Token(EvmAddress.from(l1Token), chainId);
150+
if (!hasPoolRebalanceRoute) {
151+
// Chain does not have a pool rebalance route, only allow EOA's.
152+
return !isHubPoolOrSpokePoolAddress(chainId, address);
153+
}
154+
// Chain has pool rebalance route, all addresses from filterMonitoredAddresses are valid.
155+
return true;
156+
}),
157+
];
158+
})
159+
);
132160
// Instantiate a generic adapter and supply all network-specific configurations.
133161
this.adapters[chainId] = new BaseChainAdapter(
134162
this.spokePoolManager.getSpokePoolClients(),
135163
chainId,
136164
hubChainId,
137-
filterMonitoredAddresses(chainId),
165+
monitoredAddresses,
138166
logger,
139167
SUPPORTED_TOKENS[chainId] ?? [],
140168
constructBridges(chainId),

src/clients/bridges/CrossChainTransferClient.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ export class CrossChainTransferClient {
8585
return;
8686
}
8787

88-
this.log("Updating cross chain transfers", { chainIds });
89-
9088
const outstandingTransfersPerChain = await Promise.all(
9189
chainIds.map(async (chainId) => [
9290
chainId,

src/clients/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export * from "./ConfigStoreClient";
1717
export * from "./MultiCallerClient";
1818
export * from "./ProfitClient";
1919
export * from "./TokenClient";
20-
export * from "./TokenTransferClient";
2120
export * from "./TransactionClient";
2221
export * from "./InventoryClient";
2322
export * from "./AcrossAPIClient";

src/interfaces/Report.ts

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,5 @@
1-
import { BigNumber } from "../utils";
2-
31
export enum BundleAction {
42
PROPOSED = "proposed",
53
DISPUTED = "disputed",
64
CANCELED = "canceled",
75
}
8-
9-
export enum BalanceType {
10-
// Current balance.
11-
CURRENT = "current",
12-
// Balance from any pending bundle's refunds.
13-
PENDING = "pending",
14-
// Balance from next bundle's refunds.
15-
NEXT = "next",
16-
// Balance from pending cross chain transfers.
17-
PENDING_TRANSFERS = "pending transfers",
18-
// Total balance across current, pending, next.
19-
TOTAL = "total",
20-
}
21-
22-
export interface RelayerBalanceCell {
23-
// This should match BalanceType values. We can't use BalanceType directly because interface only accepts static keys,
24-
// not those, such as enums, that are determined at runtime.
25-
[balanceType: string]: BigNumber;
26-
}
27-
28-
export interface RelayerBalanceColumns {
29-
// Including a column for "Total".
30-
[chainName: string]: RelayerBalanceCell;
31-
}
32-
33-
// The relayer balance table is organized as below:
34-
// 1. Rows are token symbols, e.g. WETH, USDC
35-
// 2. Columns are chains, e.g. Optimism.
36-
// 3. Each column is further broken into subcolumns: current (live balance), pending (balance from pending bundle's refunds)
37-
// and next bundle (balance from next bundle's refunds)
38-
export interface RelayerBalanceTable {
39-
[tokenSymbol: string]: RelayerBalanceColumns;
40-
}
41-
42-
export interface RelayerBalanceReport {
43-
[relayer: string]: RelayerBalanceTable;
44-
}

src/interfaces/Token.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +0,0 @@
1-
import { BigNumber } from "../utils";
2-
import { SortableEvent } from ".";
3-
4-
export interface TokenTransfer extends SortableEvent {
5-
value: BigNumber;
6-
from: string;
7-
to: string;
8-
}
9-
10-
export interface TransfersByTokens {
11-
[token: string]: {
12-
incoming: TokenTransfer[];
13-
outgoing: TokenTransfer[];
14-
};
15-
}
16-
17-
export interface TransfersByChain {
18-
[chainId: number]: TransfersByTokens;
19-
}

src/interfaces/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { BigNumber } from "../utils";
33

44
export * from "./InventoryManagement";
55
export * from "./SpokePool";
6-
export * from "./Token";
76
export * from "./Error";
87
export * from "./Report";
98
export * from "./Arweave";

0 commit comments

Comments
 (0)