Skip to content

Commit 65d8e2d

Browse files
committed
fix: normalize LiFi EVM quote addresses
1 parent 1cc49d8 commit 65d8e2d

5 files changed

Lines changed: 99 additions & 7 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
## Highlights
2+
3+
- Fixed Li.Fi EVM quote normalization so Base → Polygon USDC bridge requests strip CAIP-10 `eip155:<chain>:` prefixes before sending `fromAddress` / `toAddress` to Li.Fi.
4+
- Added regression coverage for Base → Polygon native USDC quote construction, matching Mony’s Polymarket buffer top-up path.
5+
6+
## Validation
7+
8+
- `npm test -- test/bridge/lifi.test.ts`
9+
- `npm run typecheck`
10+
- `npm run build`
11+
- live bridge quote smoke: Base USDC → Polygon native USDC with CAIP-10 `fromAddress`
12+
13+
## Notes
14+
15+
- This unblocks autonomous Polymarket cash-buffer top-ups where the wallet needs to bridge small Base USDC balances onto Polygon before PM collateral conversion.
16+
- Scope is intentionally narrow: quote address normalization only, with no behavior change for already-flat EVM addresses.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "evalanche",
3-
"version": "1.9.2",
3+
"version": "1.9.5",
44
"description": "Multi-EVM agent wallet SDK with unified holdings discovery, onchain identity (ERC-8004), x402 payments, cross-chain liquidity (Li.Fi + Gas.zip), prediction markets, DeFi, and perpetual futures",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",

src/bridge/lifi.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,19 @@ export class LiFiClient {
212212
const decimals = await this.resolveFromDecimals(params);
213213
const fromAmount = parseUnits(String(params.fromAmount), decimals).toString();
214214
const routeOptions = this.resolveRouteOptions(params);
215+
const fromAddress = this.normalizeEvmQuoteAddress(params.fromAddress, params.fromChainId, 'fromAddress');
216+
const toAddress = params.toAddress
217+
? this.normalizeEvmQuoteAddress(params.toAddress, params.toChainId, 'toAddress')
218+
: fromAddress;
215219

216220
const searchParams = new URLSearchParams({
217221
fromChain: params.fromChainId.toString(),
218222
toChain: params.toChainId.toString(),
219223
fromToken: params.fromToken,
220224
toToken: params.toToken,
221225
fromAmount,
222-
fromAddress: params.fromAddress,
223-
toAddress: params.toAddress ?? params.fromAddress,
226+
fromAddress,
227+
toAddress,
224228
slippage: (routeOptions.slippage ?? 0.03).toString(),
225229
integrator: 'evalanche',
226230
});
@@ -262,15 +266,19 @@ export class LiFiClient {
262266
const decimals = await this.resolveFromDecimals(params);
263267
const fromAmount = parseUnits(String(params.fromAmount), decimals).toString();
264268
const routeOptions = this.resolveRouteOptions(params);
269+
const fromAddress = this.normalizeEvmQuoteAddress(params.fromAddress, params.fromChainId, 'fromAddress');
270+
const toAddress = params.toAddress
271+
? this.normalizeEvmQuoteAddress(params.toAddress, params.toChainId, 'toAddress')
272+
: fromAddress;
265273

266274
const body = {
267275
fromChainId: params.fromChainId,
268276
toChainId: params.toChainId,
269277
fromTokenAddress: params.fromToken,
270278
toTokenAddress: params.toToken,
271279
fromAmount,
272-
fromAddress: params.fromAddress,
273-
toAddress: params.toAddress ?? params.fromAddress,
280+
fromAddress,
281+
toAddress,
274282
options: {
275283
slippage: routeOptions.slippage ?? 0.03,
276284
integrator: 'evalanche',
@@ -733,6 +741,31 @@ export class LiFiClient {
733741
});
734742
}
735743

744+
private normalizeEvmQuoteAddress(address: string, chainId: number, field: string): string {
745+
const value = String(address ?? '').trim();
746+
const caip10 = value.match(/^eip155:(\d+):(0x[0-9a-fA-F]{40})$/);
747+
748+
if (caip10) {
749+
const prefixedChainId = Number(caip10[1]);
750+
if (prefixedChainId !== chainId) {
751+
throw new EvalancheError(
752+
`${field} chain prefix ${prefixedChainId} does not match chain ${chainId}`,
753+
EvalancheErrorCode.INVALID_PARAMS,
754+
);
755+
}
756+
return caip10[2].toLowerCase();
757+
}
758+
759+
if (/^0x[0-9a-fA-F]{40}$/.test(value)) {
760+
return value.toLowerCase();
761+
}
762+
763+
throw new EvalancheError(
764+
`${field} must be a single EVM address for Li.Fi EVM quote requests`,
765+
EvalancheErrorCode.INVALID_PARAMS,
766+
);
767+
}
768+
736769
private expectString(value: unknown, field: string): string {
737770
if (typeof value === 'string' && value.length > 0) return value;
738771
throw new EvalancheError(

test/bridge/lifi.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,49 @@ describe('LiFiClient', () => {
7878
expect(quote.estimatedTime).toBe(120);
7979
});
8080

81+
it('should flatten CAIP-style EVM sender addresses for Base to Polygon USDC quotes', async () => {
82+
const walletAddress = '0x0fE61780BD5508b3C99E420662050E5560608cA4';
83+
const baseUsdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
84+
const polygonNativeUsdc = '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359';
85+
86+
mockFetch.mockResolvedValueOnce({
87+
ok: true,
88+
json: async () => ({
89+
id: 'base-polygon-usdc',
90+
tool: 'across',
91+
action: {
92+
fromChainId: 8453,
93+
toChainId: 137,
94+
fromToken: { address: baseUsdc },
95+
toToken: { address: polygonNativeUsdc },
96+
fromAmount: '5000000',
97+
},
98+
estimate: {
99+
toAmount: '4990000',
100+
gasCosts: [{ amountUSD: '0.02' }],
101+
executionDuration: 45,
102+
},
103+
}),
104+
});
105+
106+
await client.getQuote({
107+
fromChainId: 8453,
108+
toChainId: 137,
109+
fromToken: baseUsdc,
110+
toToken: polygonNativeUsdc,
111+
fromAmount: '5',
112+
fromDecimals: 6,
113+
fromAddress: `eip155:8453:${walletAddress}`,
114+
});
115+
116+
const callUrl = new URL(mockFetch.mock.calls[0][0] as string);
117+
expect(callUrl.searchParams.get('fromChain')).toBe('8453');
118+
expect(callUrl.searchParams.get('toChain')).toBe('137');
119+
expect(callUrl.searchParams.get('fromAmount')).toBe('5000000');
120+
expect(callUrl.searchParams.get('fromAddress')).toBe(walletAddress.toLowerCase());
121+
expect(callUrl.searchParams.get('toAddress')).toBe(walletAddress.toLowerCase());
122+
});
123+
81124
it('should throw on API error', async () => {
82125
mockFetch.mockResolvedValueOnce({
83126
ok: false,

0 commit comments

Comments
 (0)