Skip to content

Commit a557f38

Browse files
feat: show change address when selling utxo (#10386)
* feat: show change address for UTXO transactions in the swapper * polish design * rework to pull in change address directly where needed * ditch that idea and pull in in verify instead * Update src/assets/translations/en/main.json Co-authored-by: Apotheosis <[email protected]> --------- Co-authored-by: Apotheosis <[email protected]>
1 parent 84ae575 commit a557f38

File tree

6 files changed

+113
-12
lines changed

6 files changed

+113
-12
lines changed

src/assets/translations/en/main.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,8 @@
966966
"intoAssetSymbolBody": "You can proceed with this trade, but transaction history for receiving %{assetSymbol} is temporarily unavailable",
967967
"manualReceiveAddress": "Receive address",
968968
"recipientAddress": "Recipient address",
969+
"changeAddress": "Change address",
970+
"changeAddressExplainer": "When sending cryptocurrencies on UTXO-based networks, any leftover funds from your transaction are sent back to you at this address.",
969971
"customRecipientAddress": "Custom recipient address",
970972
"customRecipientAddressDescription": "Enter a custom recipient address for this trade",
971973
"thisIsYourCustomRecipientAddress": "This is your custom recipient address",
@@ -1772,6 +1774,7 @@
17721774
"failure": {
17731775
"body": "Unable to connect Ledger wallet"
17741776
},
1777+
"gettingChangeAddress": "Getting change address...",
17751778
"connectWarning": "Before connecting a chain, make sure you have the app open on your device.",
17761779
"signWarning": "Before signing, make sure you have the %{chain} app open on your device."
17771780
},

src/components/MultiHopTrade/components/TradeConfirm/TradeConfirmFooter.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { HStack, Skeleton, Stack, Switch } from '@chakra-ui/react'
1+
import { ExternalLinkIcon } from '@chakra-ui/icons'
2+
import { HStack, Icon, Link, Skeleton, Stack, Switch } from '@chakra-ui/react'
23
import type { TradeQuoteStep } from '@shapeshiftoss/swapper'
34
import { TransactionExecutionState } from '@shapeshiftoss/swapper'
45
import type { FC } from 'react'
56
import { useMemo } from 'react'
7+
import { TbArrowsSplit2 } from 'react-icons/tb'
8+
import { useTranslate } from 'react-polyglot'
69

710
import { SharedConfirmFooter } from '../SharedConfirm/SharedConfirmFooter'
811
import { TradeConfirmSummary } from './components/TradeConfirmSummary'
@@ -14,15 +17,21 @@ import { useTradeNetworkFeeCryptoBaseUnit } from './hooks/useTradeNetworkFeeCryp
1417
import { TradeFooterButton } from './TradeFooterButton'
1518

1619
import { Amount } from '@/components/Amount/Amount'
20+
import { HelperTooltip } from '@/components/HelperTooltip/HelperTooltip'
1721
import { RecipientAddressRow } from '@/components/RecipientAddressRow'
1822
import { Row } from '@/components/Row/Row'
19-
import { Text } from '@/components/Text/Text'
23+
import { RawText, Text } from '@/components/Text'
24+
import { TooltipWithTouch } from '@/components/TooltipWithTouch'
2025
import { useToggle } from '@/hooks/useToggle/useToggle'
2126
import { bnOrZero } from '@/lib/bignumber/bignumber'
2227
import { fromBaseUnit } from '@/lib/math'
23-
import { selectFeeAssetById } from '@/state/slices/assetsSlice/selectors'
28+
import { middleEllipsis } from '@/lib/utils'
29+
import { selectAssetById, selectFeeAssetById } from '@/state/slices/assetsSlice/selectors'
2430
import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/marketDataSlice/selectors'
25-
import { selectInputBuyAsset } from '@/state/slices/tradeInputSlice/selectors'
31+
import {
32+
selectInputBuyAsset,
33+
selectSellAssetUtxoChangeAddress,
34+
} from '@/state/slices/tradeInputSlice/selectors'
2635
import {
2736
selectActiveQuote,
2837
selectHopExecutionMetadata,
@@ -42,10 +51,12 @@ export const TradeConfirmFooter: FC<TradeConfirmFooterProps> = ({
4251
activeTradeId,
4352
}) => {
4453
const [isExactAllowance, toggleIsExactAllowance] = useToggle(true)
54+
const translate = useTranslate()
4555
const { currentTradeStep } = useStepperSteps()
4656
const currentHopIndex = useCurrentHopIndex()
4757
const quoteNetworkFeeCryptoBaseUnit = tradeQuoteStep.feeData.networkFeeCryptoBaseUnit
4858
const feeAsset = useSelectorWithArgs(selectFeeAssetById, tradeQuoteStep.sellAsset.assetId)
59+
const sellAsset = useSelectorWithArgs(selectAssetById, tradeQuoteStep.sellAsset.assetId)
4960
const quoteNetworkFeeCryptoPrecision = fromBaseUnit(
5061
quoteNetworkFeeCryptoBaseUnit,
5162
feeAsset?.precision ?? 0,
@@ -89,6 +100,8 @@ export const TradeConfirmFooter: FC<TradeConfirmFooterProps> = ({
89100
: undefined,
90101
)
91102

103+
const maybeUtxoChangeAddress = useAppSelector(selectSellAssetUtxoChangeAddress)
104+
92105
const {
93106
isLoading: isNetworkFeeCryptoBaseUnitLoading,
94107
isRefetching: isNetworkFeeCryptoBaseUnitRefetching,
@@ -264,17 +277,46 @@ export const TradeConfirmFooter: FC<TradeConfirmFooterProps> = ({
264277
explorerAddressLink={buyAsset.explorerAddressLink}
265278
recipientAddress={receiveAddress ?? ''}
266279
/>
280+
{maybeUtxoChangeAddress && (
281+
<Row>
282+
<Row.Label>
283+
<HelperTooltip label={translate('trade.changeAddressExplainer')}>
284+
<HStack spacing={2}>
285+
<Icon as={TbArrowsSplit2} />
286+
<Text translation='trade.changeAddress' />
287+
</HStack>
288+
</HelperTooltip>
289+
</Row.Label>
290+
<Row.Value>
291+
<HStack>
292+
<TooltipWithTouch label={maybeUtxoChangeAddress}>
293+
<RawText>{middleEllipsis(maybeUtxoChangeAddress)}</RawText>
294+
</TooltipWithTouch>
295+
<Link
296+
href={`${sellAsset?.explorerAddressLink}${maybeUtxoChangeAddress}`}
297+
isExternal
298+
aria-label={translate('common.viewOnExplorer')}
299+
>
300+
<Icon as={ExternalLinkIcon} />
301+
</Link>
302+
</HStack>
303+
</Row.Value>
304+
</Row>
305+
)}
267306
</Stack>
268307
)
269308
}, [
270-
buyAsset,
271-
feeAsset?.symbol,
272309
isActiveSwapperQuoteLoading,
273310
isNetworkFeeCryptoBaseUnitLoading,
274311
isNetworkFeeCryptoBaseUnitRefetching,
312+
feeAsset?.symbol,
275313
networkFeeCryptoPrecision,
276314
networkFeeUserCurrency,
315+
buyAsset.explorerAddressLink,
277316
receiveAddress,
317+
maybeUtxoChangeAddress,
318+
translate,
319+
sellAsset?.explorerAddressLink,
278320
])
279321

280322
const tradeDetail = useMemo(() => {

src/components/MultiHopTrade/components/VerifyAddresses/VerifyAddresses.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ import {
4040
selectInputSellAsset,
4141
selectManualReceiveAddress,
4242
} from '@/state/slices/tradeInputSlice/selectors'
43-
import { useAppSelector } from '@/state/store'
43+
import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice'
44+
import { useAppDispatch, useAppSelector } from '@/state/store'
4445

4546
enum AddressVerificationType {
4647
Sell = 'sell',
@@ -56,13 +57,15 @@ enum AddressVerificationStatus {
5657

5758
export const VerifyAddresses = () => {
5859
const wallet = useWallet().state.wallet
60+
const dispatch = useAppDispatch()
5961
const navigate = useNavigate()
6062
const translate = useTranslate()
6163

6264
const [sellAddress, setSellAddress] = useState<string | undefined>()
6365
const [buyAddress, setBuyAddress] = useState<string | undefined>()
6466
const [isSellVerifying, setIsSellVerifying] = useState(false)
6567
const [isBuyVerifying, setIsBuyVerifying] = useState(false)
68+
const [isFetchingChangeAddress, setIsFetchingChangeAddress] = useState(false)
6669

6770
const [buyAddressVerificationStatus, setBuyAddressVerificationStatus] =
6871
useState<AddressVerificationStatus>(AddressVerificationStatus.Pending)
@@ -266,15 +269,35 @@ export const VerifyAddresses = () => {
266269
})
267270

268271
if (deviceAddress && deviceAddress.toLowerCase() === _address.toLowerCase()) {
269-
setStatus(type, AddressVerificationStatus.Verified)
272+
setIsVerifying(type, false)
273+
274+
// If we're verifying a UTXO sell asset, fetch the change address
275+
if (type === AddressVerificationType.Sell && chainNamespace === CHAIN_NAMESPACE.Utxo) {
276+
setIsFetchingChangeAddress(true)
277+
setStatus(type, AddressVerificationStatus.Verified)
278+
try {
279+
const changeAddress = await adapter.getAddress({
280+
wallet,
281+
showOnDevice: false,
282+
accountType: accountMetadata.accountType,
283+
accountNumber: bip44Params.accountNumber,
284+
isChange: true,
285+
})
286+
dispatch(tradeInput.actions.setSellAssetUtxoChangeAddress(changeAddress))
287+
} catch (e) {
288+
console.error('Failed to fetch change address:', e)
289+
} finally {
290+
setIsFetchingChangeAddress(false)
291+
}
292+
} else {
293+
setStatus(type, AddressVerificationStatus.Verified)
294+
}
270295
} else {
271296
setStatus(type, AddressVerificationStatus.Error)
272297
setErrorMessage(type, translate('walletProvider.ledger.verify.addressMismatch'))
273298
}
274299
} catch (e) {
275300
console.error(e)
276-
} finally {
277-
setIsVerifying(type, false)
278301
}
279302
},
280303
[
@@ -291,6 +314,7 @@ export const VerifyAddresses = () => {
291314
setStatus,
292315
setErrorMessage,
293316
translate,
317+
dispatch,
294318
],
295319
)
296320

@@ -358,6 +382,23 @@ export const VerifyAddresses = () => {
358382
)
359383
}
360384

385+
if (
386+
sellAddressVerificationStatus === AddressVerificationStatus.Verified &&
387+
isFetchingChangeAddress
388+
) {
389+
return (
390+
<Button
391+
size='lg'
392+
colorScheme='blue'
393+
width='full'
394+
isLoading
395+
loadingText={translate('walletProvider.ledger.gettingChangeAddress')}
396+
>
397+
<Text translation='walletProvider.ledger.gettingChangeAddress' />
398+
</Button>
399+
)
400+
}
401+
361402
return (
362403
<Button onClick={handleContinue} size='lg' colorScheme='blue' width='full'>
363404
<Text translation='common.continue' />
@@ -366,6 +407,7 @@ export const VerifyAddresses = () => {
366407
}, [
367408
buyAddressVerificationStatus,
368409
sellAddressVerificationStatus,
410+
isFetchingChangeAddress,
369411
handleContinue,
370412
handleBuyVerify,
371413
isBuyVerifying,
@@ -432,10 +474,10 @@ export const VerifyAddresses = () => {
432474
</Skeleton>
433475
</Flex>
434476
{isSellVerifying && <Spinner boxSize={5} />}
435-
{sellAddressVerificationStatus === 'verified' && (
477+
{sellAddressVerificationStatus === AddressVerificationStatus.Verified && (
436478
<CheckCircleIcon ml='auto' boxSize={5} color='text.success' />
437479
)}
438-
{sellAddressVerificationStatus === 'error' && (
480+
{sellAddressVerificationStatus === AddressVerificationStatus.Error && (
439481
<WarningIcon ml='auto' boxSize={5} color='text.error' />
440482
)}
441483
</Flex>

src/state/slices/tradeInputSlice/selectors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,8 @@ export const selectSecondHopSellAccountId = createSelector(
141141
return chainIdAccountNumbers?.[secondHopSellAssetChainId]
142142
},
143143
)
144+
145+
export const selectSellAssetUtxoChangeAddress = createSelector(
146+
selectBaseSlice,
147+
tradeInput => tradeInput.sellAssetUtxoChangeAddress,
148+
)

src/state/slices/tradeInputSlice/tradeInputSlice.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { localAssetData } from '@/lib/asset-service'
1212

1313
export type TradeInputState = {
1414
slippagePreferencePercentage: string | undefined
15+
sellAssetUtxoChangeAddress: string | undefined
1516
} & TradeInputBaseState
1617

1718
const initialState: TradeInputState = {
@@ -26,6 +27,7 @@ const initialState: TradeInputState = {
2627
isManualReceiveAddressValid: undefined,
2728
isManualReceiveAddressEditing: false,
2829
slippagePreferencePercentage: undefined,
30+
sellAssetUtxoChangeAddress: undefined,
2931
selectedSellAssetChainId: 'All',
3032
selectedBuyAssetChainId: 'All',
3133
}
@@ -41,5 +43,11 @@ export const tradeInput = createTradeInputBaseSlice({
4143
) => {
4244
state.slippagePreferencePercentage = action.payload
4345
},
46+
setSellAssetUtxoChangeAddress: (
47+
state: TradeInputState,
48+
action: PayloadAction<string | undefined>,
49+
) => {
50+
state.sellAssetUtxoChangeAddress = action.payload
51+
},
4452
}),
4553
})

src/test/mocks/store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ export const mockStore: ReduxState = {
276276
isManualReceiveAddressEditing: false,
277277
isManualReceiveAddressValid: undefined,
278278
slippagePreferencePercentage: undefined,
279+
sellAssetUtxoChangeAddress: undefined,
279280
selectedBuyAssetChainId: 'All',
280281
selectedSellAssetChainId: 'All',
281282
},

0 commit comments

Comments
 (0)