Skip to content
This repository was archived by the owner on Jan 22, 2026. It is now read-only.

Commit e3f3d21

Browse files
authored
Merge pull request #5933 from blockchain/release/v4.88
Release/v4.88
2 parents c9ad0f1 + 2fafab1 commit e3f3d21

7 files changed

Lines changed: 102 additions & 75 deletions

File tree

config/mocks/wallet-options-v4.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"themeEnabled": true,
7878
"unifiedAccountLogin": true,
7979
"useAgentHotWalletAddress": true,
80+
"useAgentHotWalletAddressForSell": false,
8081
"useLoqateService": true,
8182
"useNewPaymentProviders": true,
8283
"useVgsProvider": true,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "blockchain-wallet-v4",
3-
"version": "4.88.4",
3+
"version": "4.88.8",
44
"license": "AGPL-3.0-or-later",
55
"private": true,
66
"author": {

packages/blockchain-wallet-v4-frontend/src/data/components/buySell/sagas.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,10 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
339339
selectors.form.getFormValues(FORM_BS_CHECKOUT)
340340
)
341341
try {
342+
const useAgentHotWalletAddressForSell = selectors.core.walletOptions
343+
.getUseAgentHotWalletAddressForSell(yield select())
344+
.getOrElse(true)
345+
342346
const pair = S.getBSPair(yield select())
343347

344348
if (!pair) throw new Error(BS_ERROR.NO_PAIR_SELECTED)
@@ -367,6 +371,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
367371
direction === 'FROM_USERKEY'
368372
? yield call(selectReceiveAddress, from, networks, api, coreSagas)
369373
: undefined
374+
370375
const sellOrder: SwapOrderType = yield call(
371376
api.createSwapOrder,
372377
direction,
@@ -376,13 +381,23 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
376381
undefined,
377382
refundAddr
378383
)
384+
const paymentAccount: ReturnType<typeof api.getPaymentAccount> = yield call(
385+
api.getPaymentAccount,
386+
coin
387+
)
388+
// Should generally use sellOrder deposit address, have this just in case
389+
// we need to fall back to paymentAccount address
390+
const sellOrderDepositAddress: string = useAgentHotWalletAddressForSell
391+
? paymentAccount.agent.address || paymentAccount.address
392+
: sellOrder.kind.depositAddress
393+
379394
// on chain
380395
if (direction === 'FROM_USERKEY') {
381396
const paymentR = S.getPayment(yield select())
382397
// @ts-ignore
383398
const payment = paymentGetOrElse(from.coin, paymentR)
384399
try {
385-
yield call(buildAndPublishPayment, payment.coin, payment, sellOrder.kind.depositAddress)
400+
yield call(buildAndPublishPayment, payment.coin, payment, sellOrderDepositAddress)
386401
yield call(api.updateSwapOrder, sellOrder.id, 'DEPOSIT_SENT')
387402
} catch (e) {
388403
yield call(api.updateSwapOrder, sellOrder.id, 'CANCEL')

packages/blockchain-wallet-v4-frontend/src/data/components/dex/mocks/quote.json

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

packages/blockchain-wallet-v4-frontend/src/data/components/dex/sagas.ts

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ const taskToPromise = (t) => new Promise((resolve, reject) => t.fork(reject, res
2626
const REFRESH_INTERVAL = 15000
2727
const TOKEN_ALLOWANCE_POLL_INTERVAL = 5000
2828
const provider = ethers.providers.getDefaultProvider(`https://api.blockchain.info/eth/nodes/rpc`)
29+
const ENTER_DETAILS = 'ENTER_DETAILS'
2930
const COMPLETE_SWAP = 'COMPLETE_SWAP'
30-
const NATIVE_CURRENCY = 'ETH'
31+
const NATIVE_TOKEN = 'ETH'
3132

3233
export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; networks: any }) => {
3334
const { waitForUserData } = profileSagas({
@@ -43,19 +44,19 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
4344
const state = yield select()
4445
const nonCustodialCoinAccounts = yield* select(() =>
4546
selectors.coins.getCoinAccounts(state, {
46-
coins: [NATIVE_CURRENCY],
47+
coins: [NATIVE_TOKEN],
4748
nonCustodialAccounts: true
4849
})
4950
)
5051

51-
if (!nonCustodialCoinAccounts[NATIVE_CURRENCY]?.length) {
52+
if (!nonCustodialCoinAccounts[NATIVE_TOKEN]?.length) {
5253
yield put(actions.core.data.eth.fetchData())
5354
yield put(actions.core.data.eth.fetchErc20Data())
5455
}
5556
const walletCurrency = yield select(selectors.core.settings.getCurrency)
5657
const coins = yield select(selectors.core.data.coins.getCoins)
5758
const erc20Coins: CoinType[] = yield select(selectors.core.data.coins.getErc20Coins)
58-
const tokens = [NATIVE_CURRENCY, ...erc20Coins]
59+
const tokens = [NATIVE_TOKEN, ...erc20Coins]
5960
.map((coin) => {
6061
const { name, precision, symbol, type } = coins[coin].coinfig
6162

@@ -86,7 +87,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
8687
symbol
8788
}
8889

89-
if (coin === 'ETH') {
90+
if (coin === NATIVE_TOKEN) {
9091
return {
9192
...tokenObj,
9293
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'
@@ -188,7 +189,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
188189
}
189190

190191
const nonCustodialCoinAccounts = selectors.coins.getCoinAccounts(yield* select(), {
191-
coins: [baseToken],
192+
coins: [baseToken, NATIVE_TOKEN],
192193
nonCustodialAccounts: true
193194
})
194195

@@ -199,8 +200,6 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
199200
throw Error('No user wallet address')
200201
}
201202

202-
// const quoteResponse = quoteMock
203-
204203
const quoteResponse = yield* call(api.getDexSwapQuote, {
205204
fromCurrency: {
206205
address: baseTokenInfo.address,
@@ -239,9 +238,23 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
239238

240239
// We have a list of quotes but it's valid only for cross chains transactions that we currently don't have
241240
// Also we consider to return to the FE only one quote in that case
242-
const { quote, quoteTtl } = quoteResponse
241+
const { quote, quoteTtl, transaction } = quoteResponse
243242

244243
if (quote) {
244+
const nonEthCustodialbalance = nonCustodialCoinAccounts[NATIVE_TOKEN][0].balance
245+
const { gasLimit, gasPrice } = transaction
246+
const gasLimitBn = ethers.BigNumber.from(gasLimit)
247+
const gasPriceBn = ethers.BigNumber.from(gasPrice)
248+
const gasFee = gasLimitBn.mul(gasPriceBn)
249+
250+
if (gasFee.gt(nonEthCustodialbalance)) {
251+
// eslint-disable-next-line no-throw-literal
252+
throw {
253+
message: 'Not enough ETH to cover gas.',
254+
title: 'Insufficient ETH'
255+
}
256+
}
257+
245258
yield* put(
246259
actions.form.change(
247260
DEX_SWAP_FORM,
@@ -287,24 +300,31 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
287300
// exit whenever the counterTokenAmount changes, to avoid infinitely calling fetchSwapQuote
288301
if (field === 'counterTokenAmount' || field === 'step') return
289302

303+
// reset error if user changes token
304+
if (field === 'baseToken') {
305+
const error = yield select(selectors.components.dex.getSwapQuote)
306+
if (error) yield put(A.clearCurrentSwapQuote())
307+
}
308+
290309
// exit if incorrect form changed or the form values were modified by a saga (avoid infinite loop)
291310
if (form !== DEX_SWAP_FORM || action['@@redux-saga/SAGA_ACTION'] === true) return
292311
const formValues = selectors.form.getFormValues(DEX_SWAP_FORM)(yield* select()) as DexSwapForm
293-
const { baseToken, baseTokenAmount, counterToken, counterTokenAmount } = formValues
312+
const { baseToken, baseTokenAmount, counterToken } = formValues
294313

295314
// if one of the values is 0 set another one to 0 and clear a quote
296315
if (field === 'baseTokenAmount' && getValidSwapAmount(baseTokenAmount) === 0) {
297316
yield* put(actions.form.change(DEX_SWAP_FORM, 'counterTokenAmount', ''))
298317
yield* put(A.clearCurrentSwapQuote())
299-
return
318+
return yield put(A.stopPollSwapQuote())
300319
}
301320

302321
if (
303-
(field === 'baseTokenAmount' && baseToken) ||
304-
(field === 'baseToken' && getValidSwapAmount(baseTokenAmount) !== 0 && baseToken)
322+
(field === 'baseTokenAmount' ||
323+
(field === 'baseToken' && getValidSwapAmount(baseTokenAmount) !== 0) ||
324+
field === 'counterToken') &&
325+
baseToken
305326
) {
306327
const token = selectors.components.dex.getTokenInfo(yield* select(), baseToken)
307-
308328
if (!token) return
309329

310330
const { balance } = token
@@ -321,7 +341,8 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
321341
title: 'Insufficient Balance'
322342
})
323343
)
324-
return
344+
345+
return yield put(A.stopPollSwapQuote())
325346
}
326347
}
327348

@@ -355,7 +376,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
355376
const response = yield call(api.getDexTokenAllowance, {
356377
addressOwner: nonCustodialAddress,
357378
currency: tokenAddress,
358-
network: 'ETH',
379+
network: NATIVE_TOKEN,
359380
spender: 'ZEROX_EXCHANGE'
360381
})
361382
const isTokenAllowed = response?.result.allowance !== '0'
@@ -394,7 +415,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
394415
const response = yield call(api.getDexTokenAllowance, {
395416
addressOwner: nonCustodialAddress,
396417
currency: tokenAddress,
397-
network: 'ETH',
418+
network: NATIVE_TOKEN,
398419
spender: 'ZEROX_EXCHANGE'
399420
})
400421
const isTokenAllowed = response?.result.allowance !== '0'
@@ -446,7 +467,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
446467
spender: 'ZEROX_EXCHANGE',
447468
type: 'TOKEN_APPROVAL'
448469
},
449-
network: 'ETH'
470+
network: NATIVE_TOKEN
450471
} as BuildDexTxParams
451472

452473
while (true) {
@@ -464,7 +485,17 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
464485
yield delay(REFRESH_INTERVAL)
465486
}
466487
} catch (e) {
467-
yield put(A.pollTokenAllowanceTxFailure(e))
488+
if (e?.error === 'Unable to fetch gas estimate') {
489+
yield put(
490+
A.fetchSwapQuoteFailure({
491+
message: 'Not enough ETH to cover gas.',
492+
title: 'Insufficient ETH'
493+
})
494+
)
495+
yield put(actions.modals.closeAllModals())
496+
} else {
497+
yield put(A.pollTokenAllowanceTxFailure(e))
498+
}
468499
yield put(A.stopPollTokenAllowanceTx())
469500
}
470501
}
@@ -514,7 +545,7 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
514545
},
515546
type: 'SWAP'
516547
},
517-
network: 'ETH'
548+
network: NATIVE_TOKEN
518549
} as BuildDexTxParams
519550

520551
// build dex tx by call api
@@ -526,10 +557,21 @@ export default ({ api, coreSagas, networks }: { api: APIType; coreSagas: any; ne
526557
// send tx
527558
const tx = yield call(() => taskToPromise(Task.of(provider.sendTransaction(signedTx))))
528559
yield put(A.sendSwapQuoteSuccess({ tx: tx.hash }))
560+
yield put(actions.form.change(DEX_SWAP_FORM, 'step', COMPLETE_SWAP))
529561
} catch (e) {
530-
yield put(A.sendSwapQuoteFailure(e))
562+
if (e.error === 'Insufficient funds for transaction fees') {
563+
yield put(
564+
A.fetchSwapQuoteFailure({
565+
message: 'Not enough ETH to cover gas.',
566+
title: 'Insufficient ETH'
567+
})
568+
)
569+
yield put(actions.form.change(DEX_SWAP_FORM, 'step', ENTER_DETAILS))
570+
} else {
571+
yield put(A.sendSwapQuoteFailure(e))
572+
yield put(actions.form.change(DEX_SWAP_FORM, 'step', COMPLETE_SWAP))
573+
}
531574
}
532-
yield put(actions.form.change(DEX_SWAP_FORM, 'step', COMPLETE_SWAP))
533575
}
534576

535577
return {

packages/blockchain-wallet-v4-frontend/src/scenes/Dex/Swap/EnterSwapDetails/EnterSwapDetails.tsx

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,15 +87,25 @@ export const EnterSwapDetails = ({ walletCurrency }: Props) => {
8787
selectors.components.dex.getDexCoinBalanceToDisplay(counterToken)
8888
)
8989

90+
const showAllowanceCheck =
91+
baseToken &&
92+
baseToken !== NATIVE_TOKEN &&
93+
!isTokenAllowed &&
94+
!isTokenAllowedLoading &&
95+
!isTokenAllowanceNotAsked
96+
const isInsufficientBalance = !!quoteError?.title.includes('Balance')
97+
const isInsufficientGas = !!quoteError?.message.includes('gas')
98+
9099
const baseTokenAccount = useSelector((state: RootState) => {
91-
if (!baseToken) return undefined
100+
const token = isInsufficientGas ? NATIVE_TOKEN : baseToken
101+
if (!token) return undefined
92102

93103
const accounts = selectors.coins.getCoinAccounts(state, {
94-
coins: [baseToken],
104+
coins: [token],
95105
nonCustodialAccounts: true
96106
})
97107

98-
return accounts[baseToken][0]
108+
return accounts[token] && accounts[token][0]
99109
}) as SwapAccountType | undefined
100110

101111
const onViewSettings = () => {
@@ -151,7 +161,7 @@ export const EnterSwapDetails = ({ walletCurrency }: Props) => {
151161
{
152162
origin: 'Dex'
153163
},
154-
{ account: baseTokenAccount, coin: baseToken }
164+
{ account: baseTokenAccount, coin: isInsufficientBalance ? NATIVE_TOKEN : baseToken }
155165
)
156166
)
157167
}
@@ -170,15 +180,6 @@ export const EnterSwapDetails = ({ walletCurrency }: Props) => {
170180
}, 400)
171181
}
172182

173-
const showAllowanceCheck =
174-
baseToken &&
175-
baseToken !== NATIVE_TOKEN &&
176-
!isTokenAllowed &&
177-
!isTokenAllowedLoading &&
178-
!isTokenAllowanceNotAsked
179-
180-
const isInsufficientBalance = !!quoteError?.title.includes('Balance')
181-
182183
return (
183184
<FormWrapper>
184185
<Header onClickSettings={onViewSettings} />
@@ -279,7 +280,7 @@ export const EnterSwapDetails = ({ walletCurrency }: Props) => {
279280
isInsufficientBalance={isInsufficientBalance}
280281
/>
281282
)}
282-
{showAllowanceCheck ? (
283+
{showAllowanceCheck && !quoteError ? (
283284
<Padding bottom={1.5}>
284285
<AllowanceCheck baseToken={baseToken} onApprove={onViewTokenAllowance} />
285286
</Padding>
@@ -320,7 +321,7 @@ export const EnterSwapDetails = ({ walletCurrency }: Props) => {
320321
<FormattedMessage
321322
id='dex.enter-swap-details.deposit-more'
322323
defaultMessage='Deposit more {token}'
323-
values={{ token: baseToken }}
324+
values={{ token: isInsufficientGas ? NATIVE_TOKEN : baseToken }}
324325
/>
325326
}
326327
/>

packages/blockchain-wallet-v4/src/redux/walletOptions/selectors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,7 @@ export const getShowProveFlow = (state: RootState) =>
210210
// which deposit address to use for swap
211211
export const getUseAgentHotWalletAddress = (state: RootState) =>
212212
getWebOptions(state).map(path(['featureFlags', 'useAgentHotWalletAddress']))
213+
214+
// which deposit address to use for non custodial sell
215+
export const getUseAgentHotWalletAddressForSell = (state: RootState) =>
216+
getWebOptions(state).map(path(['featureFlags', 'useAgentHotWalletAddressForSell']))

0 commit comments

Comments
 (0)