Skip to content

Commit a8c41f2

Browse files
committed
Fetch data to determine bridge strategy
1 parent a093ceb commit a8c41f2

File tree

8 files changed

+262
-25
lines changed

8 files changed

+262
-25
lines changed

api/_bridges/index.ts

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { getAcrossBridgeStrategy } from "./across/strategy";
22
import { getCctpBridgeStrategy } from "./cctp/strategy";
33
import { getHyperCoreBridgeStrategy } from "./hypercore/strategy";
4-
import { BridgeStrategiesConfig } from "./types";
4+
import { BridgeStrategiesConfig, BridgeStrategyData } from "./types";
55
import { CHAIN_IDs } from "../_constants";
66

77
export const bridgeStrategies: BridgeStrategiesConfig = {
@@ -14,16 +14,47 @@ export const bridgeStrategies: BridgeStrategiesConfig = {
1414
// TODO: Add CCTP routes when ready
1515
};
1616

17-
// TODO: Extend the strategy selection based on more sophisticated logic when we start
18-
// implementing burn/mint bridges.
1917
export function getBridgeStrategy({
2018
originChainId,
2119
destinationChainId,
20+
bridgeStrategyData,
2221
}: {
2322
originChainId: number;
2423
destinationChainId: number;
24+
bridgeStrategyData: BridgeStrategyData;
2525
}) {
26-
const fromToChainOverride =
27-
bridgeStrategies.fromToChains?.[originChainId]?.[destinationChainId];
28-
return fromToChainOverride ?? bridgeStrategies.default;
26+
if (!bridgeStrategyData) {
27+
const fromToChainOverride =
28+
bridgeStrategies.fromToChains?.[originChainId]?.[destinationChainId];
29+
return fromToChainOverride ?? bridgeStrategies.default;
30+
}
31+
if (!bridgeStrategyData.isUsdcToUsdc) {
32+
return getAcrossBridgeStrategy();
33+
}
34+
if (bridgeStrategyData.isUtilizationHigh) {
35+
return getCctpBridgeStrategy();
36+
}
37+
if (bridgeStrategyData.isLineaSource) {
38+
return getAcrossBridgeStrategy();
39+
}
40+
if (bridgeStrategyData.isFastCctpEligible) {
41+
if (bridgeStrategyData.isInThreshold) {
42+
return getAcrossBridgeStrategy();
43+
}
44+
if (bridgeStrategyData.isLargeDeposit) {
45+
return getAcrossBridgeStrategy();
46+
} else {
47+
return getCctpBridgeStrategy();
48+
}
49+
}
50+
if (bridgeStrategyData.canFillInstantly) {
51+
return getAcrossBridgeStrategy();
52+
} else {
53+
if (bridgeStrategyData.isLargeDeposit) {
54+
return getAcrossBridgeStrategy();
55+
} else {
56+
// Use OFT bridge if not CCTP
57+
return getCctpBridgeStrategy();
58+
}
59+
}
2960
}

api/_bridges/types.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BigNumber } from "ethers";
22

33
import { CrossSwap, CrossSwapQuotes, Token } from "../_dexes/types";
44
import { AppFee, CrossSwapType } from "../_dexes/utils";
5+
import { Logger } from "@across-protocol/sdk/dist/types/relayFeeCalculator";
56

67
export type BridgeStrategiesConfig = {
78
default: BridgeStrategy;
@@ -93,3 +94,24 @@ export type BridgeStrategy = {
9394
integratorId?: string;
9495
}) => Promise<OriginTx>;
9596
};
97+
98+
export type BridgeStrategyData =
99+
| {
100+
canFillInstantly: boolean;
101+
isUtilizationHigh: boolean;
102+
isUsdcToUsdc: boolean;
103+
isLargeDeposit: boolean;
104+
isFastCctpEligible: boolean;
105+
isLineaSource: boolean;
106+
isInThreshold: boolean;
107+
}
108+
| undefined;
109+
110+
export type BridgeStrategyDataParams = {
111+
inputToken: { address: string; chainId: number };
112+
outputToken: { address: string; chainId: number };
113+
amount: BigNumber;
114+
recipient?: string;
115+
depositor: string;
116+
logger: Logger;
117+
};

api/_bridges/utils.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { BigNumber, ethers } from "ethers";
2+
import { LimitsResponse } from "../_types";
3+
import * as sdk from "@across-protocol/sdk";
4+
import {
5+
getTokenByAddress,
6+
getCachedLimits,
7+
HUB_POOL_CHAIN_ID,
8+
} from "../_utils";
9+
import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../_constants";
10+
import {
11+
BridgeStrategyData,
12+
BridgeStrategyDataParams,
13+
} from "../_bridges/types";
14+
15+
export function isFullyUtilized(limits: LimitsResponse): boolean {
16+
// Check if utilization is high (>80%)
17+
const { liquidReserves, utilizedReserves } = limits.reserves;
18+
const _liquidReserves = BigNumber.from(liquidReserves);
19+
const _utilizedReserves = BigNumber.from(utilizedReserves);
20+
21+
const utilizationThreshold = sdk.utils.fixedPointAdjustment.mul(80).div(100); // 80%
22+
23+
// Calculate current utilization percentage
24+
const currentUtilization = _utilizedReserves
25+
.mul(sdk.utils.fixedPointAdjustment)
26+
.div(_liquidReserves.add(_utilizedReserves));
27+
28+
return currentUtilization.gt(utilizationThreshold);
29+
}
30+
31+
/**
32+
* Fetches bridge limits and utilization data in parallel to determine strategy requirements
33+
*/
34+
export async function getBridgeStrategyData({
35+
inputToken,
36+
outputToken,
37+
amount,
38+
recipient,
39+
depositor,
40+
logger,
41+
}: BridgeStrategyDataParams): Promise<BridgeStrategyData> {
42+
logger.debug({
43+
at: "getBridgeStrategyData",
44+
message: "Starting bridge strategy data fetch",
45+
inputToken: inputToken.address,
46+
outputToken: outputToken.address,
47+
amount: amount.toString(),
48+
});
49+
50+
try {
51+
// Get token details for symbol and decimals first
52+
const inputTokenDetails = getTokenByAddress(
53+
inputToken.address,
54+
inputToken.chainId
55+
);
56+
if (!inputTokenDetails) {
57+
throw new Error(
58+
`Input token not found for address ${inputToken.address}`
59+
);
60+
}
61+
62+
// Get L1 token address using TOKEN_SYMBOLS_MAP logic
63+
const l1TokenAddress =
64+
TOKEN_SYMBOLS_MAP[
65+
inputTokenDetails.symbol as keyof typeof TOKEN_SYMBOLS_MAP
66+
]?.addresses[HUB_POOL_CHAIN_ID];
67+
if (!l1TokenAddress) {
68+
throw new Error(
69+
`L1 token not found for symbol ${inputTokenDetails.symbol}`
70+
);
71+
}
72+
73+
const l1Token = getTokenByAddress(l1TokenAddress, HUB_POOL_CHAIN_ID);
74+
if (!l1Token) {
75+
throw new Error(
76+
`L1 token details not found for address ${l1TokenAddress}`
77+
);
78+
}
79+
80+
const inputUnit = BigNumber.from(10).pow(inputTokenDetails.decimals);
81+
const limits =
82+
// Get bridge limits
83+
await getCachedLimits(
84+
inputToken.address,
85+
outputToken.address,
86+
inputToken.chainId,
87+
outputToken.chainId,
88+
inputUnit.toString(),
89+
recipient || depositor
90+
);
91+
92+
// Check if we can fill instantly
93+
const maxDepositInstant = BigNumber.from(limits.maxDepositInstant);
94+
const canFillInstantly = amount.lte(maxDepositInstant);
95+
96+
const isUtilizationHigh = isFullyUtilized(limits);
97+
98+
// Get output token details
99+
const outputTokenDetails = getTokenByAddress(
100+
outputToken.address,
101+
outputToken.chainId
102+
);
103+
104+
// Check if input and output tokens are both USDC
105+
const isUsdcToUsdc =
106+
inputTokenDetails?.symbol === "USDC" &&
107+
outputTokenDetails?.symbol === "USDC";
108+
109+
// Check if deposit is > 1M USD
110+
const depositAmountUsd = parseFloat(
111+
ethers.utils.formatUnits(amount, inputTokenDetails?.decimals || 18)
112+
);
113+
const isInThreshold = depositAmountUsd <= 10_000; // 10K USD
114+
const isLargeDeposit = depositAmountUsd > 1_000_000; // 1M USD
115+
116+
// Check if eligible for Fast CCTP (Polygon, BSC, Solana) and deposit > 10K USD
117+
const fastCctpChains = [CHAIN_IDs.POLYGON, CHAIN_IDs.BSC, CHAIN_IDs.SOLANA];
118+
const isFastCctpChain =
119+
fastCctpChains.includes(inputToken.chainId) ||
120+
fastCctpChains.includes(outputToken.chainId);
121+
const isFastCctpEligible = isFastCctpChain && depositAmountUsd > 10_000; // 10K USD
122+
123+
// Check if Linea is the source chain
124+
const isLineaSource = inputToken.chainId === CHAIN_IDs.LINEA;
125+
126+
logger.debug({
127+
at: "getBridgeStrategyData",
128+
message: "Successfully completed bridge strategy data fetch",
129+
results: {
130+
canFillInstantly,
131+
isUtilizationHigh,
132+
isUsdcToUsdc,
133+
isLargeDeposit,
134+
isFastCctpEligible,
135+
isLineaSource,
136+
},
137+
});
138+
139+
return {
140+
canFillInstantly,
141+
isUtilizationHigh,
142+
isUsdcToUsdc,
143+
isLargeDeposit,
144+
isInThreshold,
145+
isFastCctpEligible,
146+
isLineaSource,
147+
};
148+
} catch (error) {
149+
logger.warn({
150+
at: "getBridgeStrategyData",
151+
message: "Failed to fetch bridge strategy data, using defaults",
152+
error: error instanceof Error ? error.message : String(error),
153+
});
154+
155+
// Safely return undefined if we can't fetch bridge strategy data
156+
return undefined;
157+
}
158+
}

api/_types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./generic.types";
22
export * from "./utility.types";
3+
export * from "./response.types";

api/_types/response.types.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export type LimitsResponse = {
2+
minDeposit: string;
3+
maxDeposit: string;
4+
maxDepositInstant: string;
5+
maxDepositShortDelay: string;
6+
recommendedDepositInstant: string;
7+
relayerFeeDetails: {
8+
relayFeeTotal: string;
9+
relayFeePercent: string;
10+
capitalFeePercent: string;
11+
capitalFeeTotal: string;
12+
gasFeePercent: string;
13+
gasFeeTotal: string;
14+
};
15+
reserves: {
16+
liquidReserves: string;
17+
utilizedReserves: string;
18+
};
19+
};

api/_utils.ts

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ import {
5151
relayerFeeCapitalCostConfig,
5252
TOKEN_EQUIVALENCE_REMAPPING,
5353
} from "./_constants";
54-
import { PoolStateOfUser, PoolStateResult, TokenInfo } from "./_types";
54+
import {
55+
LimitsResponse,
56+
PoolStateOfUser,
57+
PoolStateResult,
58+
TokenInfo,
59+
} from "./_types";
5560
import {
5661
buildInternalCacheKey,
5762
getCachedValue,
@@ -1043,21 +1048,7 @@ export const getCachedLimits = async (
10431048
relayer?: string,
10441049
message?: string,
10451050
allowUnmatchedDecimals?: boolean
1046-
): Promise<{
1047-
minDeposit: string;
1048-
maxDeposit: string;
1049-
maxDepositInstant: string;
1050-
maxDepositShortDelay: string;
1051-
recommendedDepositInstant: string;
1052-
relayerFeeDetails: {
1053-
relayFeeTotal: string;
1054-
relayFeePercent: string;
1055-
capitalFeePercent: string;
1056-
capitalFeeTotal: string;
1057-
gasFeePercent: string;
1058-
gasFeeTotal: string;
1059-
};
1060-
}> => {
1051+
): Promise<LimitsResponse> => {
10611052
const messageTooLong = isMessageTooLong(message ?? "");
10621053

10631054
const params = {

api/limits.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,8 @@ const handler = async (
326326
relayerFeeDetails,
327327
});
328328

329-
const { liquidReserves: _liquidReserves } = multicallOutput[1];
329+
const { liquidReserves: _liquidReserves, utilizedReserves } =
330+
multicallOutput[1];
330331
const [liteChainIdsEncoded] = multicallOutput[2];
331332
const [poolRebalanceRouteOrigin] = multicallOutput[3];
332333
const [poolRebalanceRouteDestination] = multicallOutput[4];
@@ -557,6 +558,10 @@ const handler = async (
557558
tokenGasCost: gasFeeDetails.tokenGasCost.toString(),
558559
}
559560
: undefined,
561+
reserves: {
562+
liquidReserves: String(_liquidReserves),
563+
utilizedReserves: String(utilizedReserves),
564+
},
560565
};
561566
logger.debug({
562567
at: "Limits",

api/swap/approval/_service.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { quoteFetchStrategies } from "../_configs";
2727
import { getBridgeStrategy } from "../../_bridges";
2828
import { TypedVercelRequest } from "../../_types";
2929
import { AcrossErrorCode } from "../../_errors";
30+
import { getBridgeStrategyData } from "../../_bridges/utils";
3031

3132
const logger = getLogger();
3233

@@ -80,11 +81,20 @@ export async function handleApprovalSwap(
8081

8182
const slippageTolerance = _slippageTolerance ?? slippage * 100;
8283

83-
// TODO: Extend the strategy selection based on more sophisticated logic when we start
84-
// implementing burn/mint bridges.
84+
// Get bridge strategy data
85+
const bridgeStrategyData = await getBridgeStrategyData({
86+
inputToken,
87+
outputToken,
88+
amount,
89+
recipient,
90+
depositor,
91+
logger,
92+
});
93+
8594
const bridgeStrategy = getBridgeStrategy({
8695
originChainId: inputToken.chainId,
8796
destinationChainId: outputToken.chainId,
97+
bridgeStrategyData,
8898
});
8999
const crossSwapQuotes = await getCrossSwapQuotes(
90100
{

0 commit comments

Comments
 (0)