Skip to content

Commit 1cae140

Browse files
committed
fix(gui): use makeMaxSpend in sweep private key flow
Enable the sweep private key scenes to use a single multi-asset spend when memory wallets expose makeMaxSpend, matching migrate wallet behavior. This keeps token selection and post-sweep token enabling consistent when a single transaction represents multiple assets, while preserving the legacy per-asset fallback for wallets without the method.
1 parent f30fe5b commit 1cae140

4 files changed

Lines changed: 165 additions & 88 deletions

File tree

eslint.config.mjs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,6 @@ export default [
287287

288288
'src/components/scenes/SwapSettingsScene.tsx',
289289
'src/components/scenes/SwapSuccessScene.tsx',
290-
'src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx',
291-
'src/components/scenes/SweepPrivateKeyCompletionScene.tsx',
292290

293291
'src/components/scenes/TransactionDetailsScene.tsx',
294292

src/components/scenes/MigrateWalletCalculateFeeScene.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,6 @@ const MigrateWalletCalculateFeeComponent: React.FC<Props> = props => {
274274
item.tokenId == null ? txFee : 'included'
275275
)
276276
}
277-
278-
successCount++
279277
} catch (e: any) {
280278
for (const key of bundlesFeeTotals.keys()) {
281279
const insufficientFundsError =

src/components/scenes/SweepPrivateKeyCalculateFeeScene.tsx

Lines changed: 133 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
type EdgeCurrencyWallet,
55
type EdgeMemoryWallet,
66
type EdgeSpendInfo,
7+
type EdgeTokenId,
78
type EdgeTransaction,
89
InsufficientFundsError
910
} from 'edge-core-js'
@@ -29,6 +30,7 @@ import { CreateWalletSelectCryptoRow } from '../themed/CreateWalletSelectCryptoR
2930
import { EdgeText } from '../themed/EdgeText'
3031
import { SafeSlider } from '../themed/SafeSlider'
3132
import { SceneHeader } from '../themed/SceneHeader'
33+
import type { SweepPrivateKeyCompletionParams } from './SweepPrivateKeyCompletionScene'
3234
import type { SweepPrivateKeyItem } from './SweepPrivateKeyProcessingScene'
3335

3436
export interface SweepPrivateKeyCalculateFeeParams {
@@ -38,8 +40,13 @@ export interface SweepPrivateKeyCalculateFeeParams {
3840
}
3941

4042
type Props = EdgeAppSceneProps<'sweepPrivateKeyCalculateFee'>
43+
type AssetTxState = EdgeTransaction | Error | 'included'
44+
type MakeMaxSpendMethod = (params: {
45+
tokenIds?: EdgeTokenId[]
46+
spendTargets: Array<{ publicAddress: string }>
47+
}) => Promise<EdgeTransaction>
4148

42-
const SweepPrivateKeyCalculateFeeComponent = (props: Props) => {
49+
const SweepPrivateKeyCalculateFeeComponent: React.FC<Props> = props => {
4350
const { navigation, route } = props
4451
const { memoryWallet, receivingWallet, sweepPrivateKeyList } = route.params
4552

@@ -66,7 +73,7 @@ const SweepPrivateKeyCalculateFeeComponent = (props: Props) => {
6673
const mounted = React.useRef<boolean>(true)
6774

6875
const [transactionState, setTransactionState] = React.useState<
69-
Map<string, EdgeTransaction | Error>
76+
Map<string, AssetTxState>
7077
>(new Map())
7178
const [sliderDisabled, setSliderDisabled] = React.useState(true)
7279

@@ -111,6 +118,14 @@ const SweepPrivateKeyCalculateFeeComponent = (props: Props) => {
111118
/>
112119
)
113120
}
121+
} else if (tx === 'included') {
122+
rightSide = (
123+
<EdgeText
124+
style={{ color: theme.secondaryText, fontSize: theme.rem(0.75) }}
125+
>
126+
{lstrings.string_included}
127+
</EdgeText>
128+
)
114129
} else {
115130
const exchangeDenom = getExchangeDenom(
116131
receivingWallet.currencyConfig,
@@ -170,22 +185,26 @@ const SweepPrivateKeyCalculateFeeComponent = (props: Props) => {
170185
const unsignedEdgeTransactions: EdgeTransaction[] = []
171186
for (const item of sweepPrivateKeyList) {
172187
const tx = transactionState.get(item.key)
173-
if (tx == null || tx instanceof Error) continue
188+
if (tx == null || tx instanceof Error || tx === 'included') continue
174189
unsignedEdgeTransactions.push(tx)
175190
}
176-
navigation.push('sweepPrivateKeyCompletion', {
191+
const hasIncludedAssets = sweepPrivateKeyList.some(
192+
item => transactionState.get(item.key) === 'included'
193+
)
194+
const completionParams: SweepPrivateKeyCompletionParams = {
177195
memoryWallet,
178196
receivingWallet,
179-
unsignedEdgeTransactions
180-
})
197+
sweepPrivateKeyList,
198+
unsignedEdgeTransactions,
199+
hasIncludedAssets
200+
}
201+
navigation.push('sweepPrivateKeyCompletion', completionParams)
181202
})
182203

183204
// Create getMaxSpendable/makeSpend promises for each selected asset. We'll group them by wallet first and then execute all of them while keeping
184205
// track of which makeSpends are successful so we can enable the slider. A single failure from any of a wallet's assets will cast them all as failures.
185206
useAsyncEffect(
186207
async () => {
187-
let feeTotal = '0'
188-
189208
const tokenItems = [...sweepPrivateKeyList]
190209
const mainnetItem = tokenItems.splice(
191210
sweepPrivateKeyList.length - 1,
@@ -195,90 +214,124 @@ const SweepPrivateKeyCalculateFeeComponent = (props: Props) => {
195214
await receivingWallet.getReceiveAddress({ tokenId: null })
196215
).publicAddress
197216
let enableSlider = false
198-
199-
const getMax = async (
200-
asset: SweepPrivateKeyItem,
201-
numPendingTxs: number
202-
) => {
203-
const fakeEdgeTransaction: EdgeTransaction = {
204-
blockHeight: 0,
205-
currencyCode: '',
206-
date: 0,
207-
memos: [],
208-
isSend: true,
209-
nativeAmount: '0',
210-
networkFee: '0',
211-
networkFees: [],
212-
ourReceiveAddresses: [],
213-
signedTx: '',
214-
tokenId: null,
215-
txid: '',
216-
walletId: ''
217-
}
218-
219-
const spendInfo: EdgeSpendInfo = {
220-
tokenId: asset.tokenId,
221-
spendTargets: [{ publicAddress }],
222-
networkFeeOption: 'standard',
223-
pendingTxs: Array.from(
224-
{ length: numPendingTxs },
225-
() => fakeEdgeTransaction
226-
)
227-
}
228-
217+
if (
218+
((memoryWallet as any).otherMethods?.makeMaxSpend as
219+
| MakeMaxSpendMethod
220+
| undefined) != null
221+
) {
229222
try {
230-
const maxAmount = await memoryWallet.getMaxSpendable(spendInfo)
231-
if (maxAmount === '0') {
232-
throw new InsufficientFundsError({ tokenId: asset.tokenId })
233-
}
234-
let nativeAmount = maxAmount
235-
if (asset.tokenId === null) {
236-
nativeAmount = sub(nativeAmount, feeTotal)
237-
}
238-
const maxSpendInfo = {
239-
...spendInfo,
240-
spendTargets: [{ publicAddress, nativeAmount }]
241-
}
242-
const edgeTransaction = await memoryWallet.makeSpend(maxSpendInfo)
243-
const txFee =
244-
edgeTransaction.parentNetworkFee ?? edgeTransaction.networkFee
223+
const tokenIds = sweepPrivateKeyList.map(item => item.tokenId)
224+
const edgeTransaction = await (
225+
(memoryWallet as any).otherMethods
226+
.makeMaxSpend as MakeMaxSpendMethod
227+
)({
228+
tokenIds,
229+
spendTargets: [{ publicAddress }]
230+
})
245231
setTransactionState(
246-
prevState => new Map([...prevState, [asset.key, edgeTransaction]])
232+
new Map(
233+
sweepPrivateKeyList.map(item => [
234+
item.key,
235+
item.tokenId == null ? edgeTransaction : 'included'
236+
])
237+
)
247238
)
248-
feeTotal = add(feeTotal, txFee)
249-
// While imperfect, sanity check that the total fee spent so far to send tokens + fee to send mainnet currency is under the total mainnet balance
250-
if (lt(memoryWallet.balanceMap.get(null) ?? '0', feeTotal)) {
251-
throw new InsufficientFundsError({
252-
tokenId: null,
253-
networkFee: feeTotal
254-
})
255-
}
256239
enableSlider = true
257240
} catch (e) {
258241
const insufficientFundsError = asMaybeInsufficientFundsError(e)
259-
if (insufficientFundsError != null) {
260-
setTransactionState(
261-
prevState =>
262-
new Map([...prevState, [asset.key, insufficientFundsError]])
242+
const error =
243+
insufficientFundsError ??
244+
Error(lstrings.migrate_unknown_error_fragment)
245+
setTransactionState(
246+
new Map(sweepPrivateKeyList.map(item => [item.key, error]))
247+
)
248+
}
249+
} else {
250+
let feeTotal = '0'
251+
const getMax = async (
252+
asset: SweepPrivateKeyItem,
253+
numPendingTxs: number
254+
): Promise<void> => {
255+
const fakeEdgeTransaction: EdgeTransaction = {
256+
blockHeight: 0,
257+
currencyCode: '',
258+
date: 0,
259+
memos: [],
260+
isSend: true,
261+
nativeAmount: '0',
262+
networkFee: '0',
263+
networkFees: [],
264+
ourReceiveAddresses: [],
265+
signedTx: '',
266+
tokenId: null,
267+
txid: '',
268+
walletId: ''
269+
}
270+
271+
const spendInfo: EdgeSpendInfo = {
272+
tokenId: asset.tokenId,
273+
spendTargets: [{ publicAddress }],
274+
networkFeeOption: 'standard',
275+
pendingTxs: Array.from(
276+
{ length: numPendingTxs },
277+
() => fakeEdgeTransaction
263278
)
264-
} else {
279+
}
280+
281+
try {
282+
const maxAmount = await memoryWallet.getMaxSpendable(spendInfo)
283+
if (maxAmount === '0') {
284+
throw new InsufficientFundsError({ tokenId: asset.tokenId })
285+
}
286+
let nativeAmount = maxAmount
287+
if (asset.tokenId === null) {
288+
nativeAmount = sub(nativeAmount, feeTotal)
289+
}
290+
const maxSpendInfo = {
291+
...spendInfo,
292+
spendTargets: [{ publicAddress, nativeAmount }]
293+
}
294+
const edgeTransaction = await memoryWallet.makeSpend(maxSpendInfo)
295+
const txFee =
296+
edgeTransaction.parentNetworkFee ?? edgeTransaction.networkFee
265297
setTransactionState(
266-
prevState =>
267-
new Map([
268-
...prevState,
269-
[asset.key, Error(lstrings.migrate_unknown_error_fragment)]
270-
])
298+
prevState => new Map([...prevState, [asset.key, edgeTransaction]])
271299
)
300+
feeTotal = add(feeTotal, txFee)
301+
// While imperfect, sanity check that the total fee spent so far to send tokens + fee to send mainnet currency is under the total mainnet balance
302+
if (lt(memoryWallet.balanceMap.get(null) ?? '0', feeTotal)) {
303+
throw new InsufficientFundsError({
304+
tokenId: null,
305+
networkFee: feeTotal
306+
})
307+
}
308+
enableSlider = true
309+
} catch (e) {
310+
const insufficientFundsError = asMaybeInsufficientFundsError(e)
311+
if (insufficientFundsError != null) {
312+
setTransactionState(
313+
prevState =>
314+
new Map([...prevState, [asset.key, insufficientFundsError]])
315+
)
316+
} else {
317+
setTransactionState(
318+
prevState =>
319+
new Map([
320+
...prevState,
321+
[asset.key, Error(lstrings.migrate_unknown_error_fragment)]
322+
])
323+
)
324+
}
272325
}
273326
}
274-
}
275327

276-
await Promise.all(
277-
tokenItems.map(async (item, index) => {
278-
await getMax(item, index)
279-
})
280-
)
281-
await getMax(mainnetItem, tokenItems.length)
328+
await Promise.all(
329+
tokenItems.map(async (item, index) => {
330+
await getMax(item, index)
331+
})
332+
)
333+
await getMax(mainnetItem, tokenItems.length)
334+
}
282335

283336
if (enableSlider && mounted.current) {
284337
setSliderDisabled(false)

src/components/scenes/SweepPrivateKeyCompletionScene.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,41 @@ import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext'
2222
import { CreateWalletSelectCryptoRow } from '../themed/CreateWalletSelectCryptoRow'
2323
import { MainButton } from '../themed/MainButton'
2424
import { SceneHeader } from '../themed/SceneHeader'
25+
import type { SweepPrivateKeyItem } from './SweepPrivateKeyProcessingScene'
2526

2627
export interface SweepPrivateKeyCompletionParams {
28+
/** Temporary source wallet created from the swept private key(s). */
2729
memoryWallet: EdgeMemoryWallet
30+
/**
31+
* Destination wallet that receives swept funds and enables
32+
* successfully transferred tokens.
33+
*/
2834
receivingWallet: EdgeCurrencyWallet
35+
/**
36+
* Original asset selection order from processing, with token
37+
* and mainnet metadata.
38+
*/
39+
sweepPrivateKeyList: SweepPrivateKeyItem[]
40+
/** Unsent transactions prepared in fee calculation. */
2941
unsignedEdgeTransactions: EdgeTransaction[]
42+
/**
43+
* True when fee calculation marked token rows as included in
44+
* a bundled max-spend transaction.
45+
*/
46+
hasIncludedAssets: boolean
3047
}
3148

3249
interface Props extends EdgeAppSceneProps<'sweepPrivateKeyCompletion'> {}
3350

34-
const SweepPrivateKeyCompletionComponent = (props: Props) => {
51+
const SweepPrivateKeyCompletionComponent: React.FC<Props> = props => {
3552
const { navigation, route } = props
36-
const { memoryWallet, receivingWallet, unsignedEdgeTransactions } =
37-
route.params
53+
const {
54+
memoryWallet,
55+
receivingWallet,
56+
sweepPrivateKeyList,
57+
unsignedEdgeTransactions,
58+
hasIncludedAssets
59+
} = route.params
3860

3961
const theme = useTheme()
4062
const styles = getStyles(theme)
@@ -58,7 +80,7 @@ const SweepPrivateKeyCompletionComponent = (props: Props) => {
5880
const handleTxStatus = (
5981
tx: EdgeTransaction,
6082
status: 'complete' | 'error'
61-
) => {
83+
): void => {
6284
setItemStatus(currentState => {
6385
const newState = new Map(currentState)
6486
newState.set(tx.tokenId, status)
@@ -108,6 +130,12 @@ const SweepPrivateKeyCompletionComponent = (props: Props) => {
108130
mainnetTransaction
109131
)
110132
handleTxStatus(tx, 'complete')
133+
if (hasIncludedAssets) {
134+
for (const item of sweepPrivateKeyList) {
135+
if (item.tokenId == null) continue
136+
successfullyTransferredTokenIds.push(item.tokenId)
137+
}
138+
}
111139
} catch (e) {
112140
showError(e)
113141
handleTxStatus(mainnetTransaction, 'error')

0 commit comments

Comments
 (0)