Skip to content

Commit 1afb095

Browse files
Thunniniclaude
andcommitted
fix: break infinite loop on swap max button for EVM native assets
Max 모드에서 gas simulator cache key와 route query에 fee-dependent한 amountConfig.amount를 사용하면 fee→amount→key/route 변경→re-simulation→fee 무한루프 발생. rawBalance(fee 차감 전)를 사용하여 순환을 차단한다. - SwapAmountConfig에 rawBalance getter 추가, maxAmount가 이를 재사용 - value getter의 route query에 rawBalance 사용 - isFetchingInAmount, uiProperties의 querySwapHelper도 rawBalance로 통일 - gas simulator cache key를 amountConfig.rawBalance로 간소화 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a922900 commit 1afb095

2 files changed

Lines changed: 44 additions & 24 deletions

File tree

apps/extension/src/pages/ibc-swap/index.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,14 @@ export const IBCSwapPage: FunctionComponent = observer(() => {
261261
}
262262

263263
if (inChainType === "evm") {
264-
// EVM 트랜잭션의 gas estimated는 보낼 토큰 수량에 따라 차이가 꽤 클 수 있다.
265-
type = `${type}/${swapConfigs.amountConfig.amount[0].toCoin().amount}`;
264+
// max 모드에서 amountConfig.amount를 cache key에 사용하면
265+
// fee→amount→key 변경→re-simulation→fee 변경 무한루프 발생.
266+
// rawBalance는 fee에 의존하지 않으므로 key가 안정된다.
267+
const amountForKey =
268+
swapConfigs.amountConfig.fraction > 0
269+
? swapConfigs.amountConfig.rawBalance.toCoin().amount
270+
: swapConfigs.amountConfig.amount[0].toCoin().amount;
271+
type = `${type}/${amountForKey}`;
266272
}
267273

268274
return `${swapConfigs.amountConfig.chainId}/${swapConfigs.amountConfig.outChainId}/${swapConfigs.amountConfig.currency.coinMinimalDenom}/${swapConfigs.amountConfig.outCurrency.coinMinimalDenom}/${type}`;

apps/hooks-internal/src/swap/amount.ts

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,20 @@ export class SwapAmountConfig extends AmountConfig {
115115
});
116116
}
117117

118+
// fee 차감 전 순수 잔고. fee에 의존하지 않으므로 fee가 변해도 값이 불변.
119+
// max 모드에서 route query의 amount로 사용하여
120+
// fee→amount→route→simulation→fee 순환을 차단한다.
118121
@computed
119-
get maxAmount(): CoinPretty {
120-
let result = this.queriesStore
122+
get rawBalance(): CoinPretty {
123+
return this.queriesStore
121124
.get(this.chainId)
122-
// CHECK: queryBalances대신 querySpendableBalance를 사용해야 하는지 확인
123125
.queryBalances.getQueryBech32Address(this.senderConfig.sender)
124126
.getBalanceFromCurrency(this.currency);
127+
}
128+
129+
@computed
130+
get maxAmount(): CoinPretty {
131+
let result = this.rawBalance;
125132
if (this.feeConfig && !this.disableSubFeeFromFaction) {
126133
for (const fee of this.feeConfig.fees) {
127134
result = result.sub(fee);
@@ -137,10 +144,14 @@ export class SwapAmountConfig extends AmountConfig {
137144
@override
138145
override get value(): string {
139146
if (this.fraction > 0) {
140-
let result = this.maxAmount;
141-
142-
const queryRoute = this.getQueryRoute(result);
147+
// route query에 rawBalance(fee 차감 전)를 사용한다.
148+
// maxAmount(balance - fee)를 사용하면 fee가 바뀔 때마다 route query 키가 변경되어
149+
// route fetch → simulation → fee 변경 → route fetch ... 무한루프에 빠진다.
150+
// fee 차이에 의한 amount 변동은 전체 잔고 대비 극히 미미하므로 route 결과에 영향 없다.
151+
const queryRoute = this.getQueryRoute(this.rawBalance);
143152
if (queryRoute?.response != null) {
153+
// 실제 value 계산에는 maxAmount(balance - fee)를 사용하여 정확도 유지
154+
let result = this.maxAmount;
144155
const bridgeFee = queryRoute.bridgeFees.reduce(
145156
(acc: CoinPretty, fee: CoinPretty) => {
146157
if (
@@ -155,23 +166,23 @@ export class SwapAmountConfig extends AmountConfig {
155166
if (bridgeFee.toDec().gt(SwapAmountConfig.ZERO_DEC)) {
156167
result = result.sub(bridgeFee);
157168
}
158-
} else {
159-
return this._oldValue;
160-
}
161169

162-
if (result.toDec().lte(SwapAmountConfig.ZERO_DEC)) {
163-
return "0";
164-
}
170+
if (result.toDec().lte(SwapAmountConfig.ZERO_DEC)) {
171+
return "0";
172+
}
165173

166-
const newValue = result
167-
.mul(new Dec(this.fraction))
168-
.trim(true)
169-
.locale(false)
170-
.hideDenom(true)
171-
.toString();
172-
this._oldValue = newValue;
174+
const newValue = result
175+
.mul(new Dec(this.fraction))
176+
.trim(true)
177+
.locale(false)
178+
.hideDenom(true)
179+
.toString();
180+
this._oldValue = newValue;
173181

174-
return newValue;
182+
return newValue;
183+
} else {
184+
return this._oldValue;
185+
}
175186
}
176187

177188
this._oldValue = this._value;
@@ -292,9 +303,10 @@ export class SwapAmountConfig extends AmountConfig {
292303
}
293304

294305
get isFetchingInAmount(): boolean {
306+
// value getter와 동일하게 rawBalance를 사용하여 같은 route query를 참조.
295307
if (this.fraction === 1) {
296308
return (
297-
this.getQueryRoute(this.maxAmount)?.isFetching ??
309+
this.getQueryRoute(this.rawBalance)?.isFetching ??
298310
this.getQueryRoute()?.isFetching ??
299311
false
300312
);
@@ -818,8 +830,10 @@ export class SwapAmountConfig extends AmountConfig {
818830
}
819831

820832
// max amount인 경우엔 route를 두 번 쿼리하기 때문에 첫 번째 쿼리도 체크한다.
833+
// value getter와 동일하게 rawBalance를 사용하여 같은 route query를 참조한다.
834+
// maxAmount를 사용하면 fee 변동 시 다른 쿼리 키가 생성되어 무한 fetch 사이클 유발.
821835
if (this.fraction === 1) {
822-
const querySwapHelper = this.getQuerySwapHelper(this.maxAmount);
836+
const querySwapHelper = this.getQuerySwapHelper(this.rawBalance);
823837
if (!querySwapHelper) {
824838
return {
825839
...prev,

0 commit comments

Comments
 (0)