Skip to content

Commit 9037b9c

Browse files
committed
fix(gui): migrate selected tokens with parent wallet
Motivation: keep token migrations bundled with their parent wallet so multi-asset chains like Zano do not split into separate wallet bundles. This also enables the makeMaxSpend migration path to run as a single routine for a wallet bundle while preserving the per-asset fallback.
1 parent 7119d79 commit 9037b9c

6 files changed

Lines changed: 210 additions & 68 deletions

File tree

eslint.config.mjs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@ export default [
238238
'src/components/scenes/ConfirmScene.tsx',
239239
'src/components/scenes/CreateWalletAccountSelectScene.tsx',
240240
'src/components/scenes/CreateWalletAccountSetupScene.tsx',
241-
'src/components/scenes/CreateWalletCompletionScene.tsx',
242241

243242
'src/components/scenes/CurrencyNotificationScene.tsx',
244243
'src/components/scenes/DefaultFiatSettingScene.tsx',

src/components/scenes/CreateWalletCompletionScene.tsx

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -252,23 +252,44 @@ const CreateWalletCompletionComponent: React.FC<Props> = props => {
252252
})
253253

254254
const handleMigrate = useHandler(() => {
255-
// Transform filtered items into the structure expected by the migration component
256-
const migrateWalletList: MigrateWalletItem[] = newWalletItems.map(
257-
createWallet => {
258-
const { key, pluginId } = createWallet
259-
const wallet = wallets.find(
260-
wallet => wallet.currencyInfo.pluginId === pluginId
261-
)
255+
// Build migrate items from successfully created parent wallets, then attach
256+
// selected tokens for the same plugin to that same wallet id.
257+
const successfulNewWalletItems = newWalletItems.filter(
258+
item => itemStatus[item.key] === 'complete'
259+
)
260+
const addedTokenKeys = new Set<string>()
261+
const migrateWalletList: MigrateWalletItem[] = []
262+
263+
for (const createWallet of successfulNewWalletItems) {
264+
const { key, pluginId, walletType } = createWallet
265+
const wallet = wallets.find(
266+
wallet => wallet.currencyInfo.pluginId === pluginId
267+
)
268+
const createWalletId = wallet?.id ?? ''
269+
const displayName = walletNames[key]
270+
271+
migrateWalletList.push({
272+
...createWallet,
273+
createWalletId,
274+
displayName,
275+
key,
276+
type: 'create'
277+
})
278+
279+
for (const tokenItem of newTokenItems) {
280+
if (tokenItem.pluginId !== pluginId) continue
281+
if (addedTokenKeys.has(tokenItem.key)) continue
282+
addedTokenKeys.add(tokenItem.key)
262283

263-
return {
264-
...createWallet,
265-
createWalletId: wallet == null ? '' : wallet.id,
266-
displayName: walletNames[key],
267-
key,
284+
migrateWalletList.push({
285+
...tokenItem,
286+
createWalletId,
287+
displayName,
288+
walletType,
268289
type: 'create'
269-
}
290+
})
270291
}
271-
)
292+
}
272293

273294
// Navigate to the migration screen with the prepared list
274295
if (migrateWalletList.length > 0) {

src/components/scenes/MigrateWalletCalculateFeeScene.tsx

Lines changed: 117 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export interface MigrateWalletCalculateFeeParams {
3939

4040
type Props = EdgeAppSceneProps<'migrateWalletCalculateFee'>
4141

42-
type AssetRowState = string | Error
42+
type AssetRowState = string | Error | 'included'
43+
type MakeMaxSpendMethod = (params: {
44+
tokenIds?: Array<string | null>
45+
spendTargets: Array<{ publicAddress: string }>
46+
}) => Promise<EdgeTransaction>
4347

4448
const MigrateWalletCalculateFeeComponent: React.FC<Props> = props => {
4549
const { navigation, route } = props
@@ -117,6 +121,14 @@ const MigrateWalletCalculateFeeComponent: React.FC<Props> = props => {
117121
/>
118122
)
119123
}
124+
} else if (fee === 'included') {
125+
rightSide = (
126+
<EdgeText
127+
style={{ color: theme.secondaryText, fontSize: theme.rem(0.75) }}
128+
>
129+
{lstrings.string_included}
130+
</EdgeText>
131+
)
120132
} else {
121133
const fakeEdgeTransaction: EdgeTransaction = {
122134
blockHeight: 0,
@@ -222,73 +234,125 @@ const MigrateWalletCalculateFeeComponent: React.FC<Props> = props => {
222234
}, [])
223235

224236
let successCount = 0
225-
const walletPromises = []
237+
const walletRoutines = []
226238
for (const bundle of bundledWalletAssets) {
227239
const wallet = currencyWallets[bundle[bundle.length - 1].createWalletId]
228240
const {
229241
currencyInfo: { pluginId }
230242
} = wallet
231-
232243
let feeTotal = '0'
233244
const bundlesFeeTotals = new Map<string, AssetRowState>(
234245
bundle.map(item => [item.key, '0'])
235246
)
236247

237-
const assetPromises = bundle.map((asset, i) => {
238-
return async () => {
239-
const publicAddress =
240-
SPECIAL_CURRENCY_INFO[pluginId].dummyPublicAddress ??
241-
(await wallet.getReceiveAddress({ tokenId: null })).publicAddress
242-
const spendInfo: EdgeSpendInfo = {
243-
tokenId: asset.tokenId,
244-
spendTargets: [{ publicAddress }],
245-
networkFeeOption: 'standard'
246-
}
247-
248-
try {
249-
const maxAmount = await wallet.getMaxSpendable(spendInfo)
250-
if (maxAmount === '0') {
251-
throw new InsufficientFundsError({ tokenId: asset.tokenId })
252-
}
253-
const maxSpendInfo = {
254-
...spendInfo,
255-
spendTargets: [{ publicAddress, nativeAmount: maxAmount }]
256-
}
257-
const edgeTransaction = await wallet.makeSpend(maxSpendInfo)
258-
const txFee =
259-
edgeTransaction.parentNetworkFee ?? edgeTransaction.networkFee
260-
bundlesFeeTotals.set(asset.key, txFee)
261-
feeTotal = add(feeTotal, txFee)
262-
263-
// 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
264-
if (
265-
i === bundle.length - 1 &&
266-
lt(wallet.balanceMap.get(null) ?? '0', feeTotal)
267-
) {
268-
throw new InsufficientFundsError({
269-
tokenId: null,
270-
networkFee: feeTotal
248+
let assetSpendRoutines: Array<() => Promise<void>>
249+
if (
250+
(wallet.otherMethods.makeMaxSpend as
251+
| MakeMaxSpendMethod
252+
| undefined) != null
253+
) {
254+
assetSpendRoutines = [
255+
async () => {
256+
const publicAddress =
257+
SPECIAL_CURRENCY_INFO[pluginId].dummyPublicAddress ??
258+
(await wallet.getReceiveAddress({ tokenId: null }))
259+
.publicAddress
260+
261+
try {
262+
const tokenIds = bundle.map(item => item.tokenId)
263+
const edgeTransaction = await (
264+
wallet.otherMethods.makeMaxSpend as MakeMaxSpendMethod
265+
)({
266+
tokenIds,
267+
spendTargets: [{ publicAddress }]
271268
})
272-
}
273-
} catch (e: any) {
274-
for (const key of bundlesFeeTotals.keys()) {
275-
const insufficientFundsError = asMaybeInsufficientFundsError(e)
276-
if (insufficientFundsError != null) {
277-
bundlesFeeTotals.set(key, e)
278-
} else {
269+
const txFee =
270+
edgeTransaction.parentNetworkFee ?? edgeTransaction.networkFee
271+
for (const item of bundle) {
279272
bundlesFeeTotals.set(
280-
key,
281-
Error(lstrings.migrate_unknown_error_fragment)
273+
item.key,
274+
item.tokenId == null ? txFee : 'included'
282275
)
283276
}
277+
278+
successCount++
279+
} catch (e: any) {
280+
for (const key of bundlesFeeTotals.keys()) {
281+
const insufficientFundsError =
282+
asMaybeInsufficientFundsError(e)
283+
if (insufficientFundsError != null) {
284+
bundlesFeeTotals.set(key, e)
285+
} else {
286+
bundlesFeeTotals.set(
287+
key,
288+
Error(lstrings.migrate_unknown_error_fragment)
289+
)
290+
}
291+
}
284292
}
285293
}
286-
}
287-
})
294+
]
295+
} else {
296+
// Create an array of async functions to call that will spend each
297+
// asset in the bundle.
298+
assetSpendRoutines = bundle.map((asset, i) => {
299+
return async () => {
300+
const publicAddress =
301+
SPECIAL_CURRENCY_INFO[pluginId].dummyPublicAddress ??
302+
(await wallet.getReceiveAddress({ tokenId: null }))
303+
.publicAddress
304+
const spendInfo: EdgeSpendInfo = {
305+
tokenId: asset.tokenId,
306+
spendTargets: [{ publicAddress }],
307+
networkFeeOption: 'standard'
308+
}
309+
310+
try {
311+
const maxAmount = await wallet.getMaxSpendable(spendInfo)
312+
if (maxAmount === '0') {
313+
throw new InsufficientFundsError({ tokenId: asset.tokenId })
314+
}
315+
const maxSpendInfo = {
316+
...spendInfo,
317+
spendTargets: [{ publicAddress, nativeAmount: maxAmount }]
318+
}
319+
const edgeTransaction = await wallet.makeSpend(maxSpendInfo)
320+
const txFee =
321+
edgeTransaction.parentNetworkFee ?? edgeTransaction.networkFee
322+
bundlesFeeTotals.set(asset.key, txFee)
323+
feeTotal = add(feeTotal, txFee)
324+
325+
// 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
326+
if (
327+
i === bundle.length - 1 &&
328+
lt(wallet.balanceMap.get(null) ?? '0', feeTotal)
329+
) {
330+
throw new InsufficientFundsError({
331+
tokenId: null,
332+
networkFee: feeTotal
333+
})
334+
}
335+
} catch (e: any) {
336+
for (const key of bundlesFeeTotals.keys()) {
337+
const insufficientFundsError =
338+
asMaybeInsufficientFundsError(e)
339+
if (insufficientFundsError != null) {
340+
bundlesFeeTotals.set(key, e)
341+
} else {
342+
bundlesFeeTotals.set(
343+
key,
344+
Error(lstrings.migrate_unknown_error_fragment)
345+
)
346+
}
347+
}
348+
}
349+
}
350+
})
351+
}
288352

289-
walletPromises.push(async () => {
290-
for (const promise of assetPromises) {
291-
await promise()
353+
walletRoutines.push(async () => {
354+
for (const spendRoutine of assetSpendRoutines) {
355+
await spendRoutine()
292356
}
293357

294358
const success = [...bundlesFeeTotals.values()].some(
@@ -305,8 +369,8 @@ const MigrateWalletCalculateFeeComponent: React.FC<Props> = props => {
305369
}
306370

307371
await Promise.all(
308-
walletPromises.map(async promise => {
309-
await promise()
372+
walletRoutines.map(async routine => {
373+
await routine()
310374
})
311375
)
312376

src/components/scenes/MigrateWalletCompletionScene.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ interface Props extends EdgeAppSceneProps<'migrateWalletCompletion'> {}
4141
interface MigrateWalletTokenItem extends MigrateWalletItem {
4242
tokenId: string
4343
}
44+
type MakeMaxSpendMethod = (params: {
45+
tokenIds?: Array<string | null>
46+
spendTargets: Array<{ publicAddress: string }>
47+
metadata?: EdgeSpendInfo['metadata']
48+
}) => Promise<EdgeTransaction>
4449

4550
const MigrateWalletCompletionComponent: React.FC<Props> = props => {
4651
const { navigation, route } = props
@@ -181,6 +186,57 @@ const MigrateWalletCompletionComponent: React.FC<Props> = props => {
181186
]
182187
await newWallet.changeEnabledTokenIds(tokenIdsToEnable)
183188

189+
if (
190+
(oldWallet.otherMethods.makeMaxSpend as
191+
| MakeMaxSpendMethod
192+
| undefined) != null
193+
) {
194+
try {
195+
const tokenIds = bundle.map(item => item.tokenId)
196+
const unsignedTx = await (
197+
oldWallet.otherMethods.makeMaxSpend as MakeMaxSpendMethod
198+
)({
199+
tokenIds,
200+
spendTargets: [{ publicAddress: newPublicAddress }],
201+
metadata: {
202+
category: 'Transfer',
203+
name: newWalletName,
204+
notes: sprintf(
205+
lstrings.migrate_wallet_tx_notes,
206+
newWalletName
207+
)
208+
}
209+
})
210+
const signedTx = await oldWallet.signTx(unsignedTx)
211+
const broadcastedTx = await oldWallet.broadcastTx(signedTx)
212+
await oldWallet.saveTx(broadcastedTx)
213+
214+
for (const item of bundle) {
215+
handleItemStatus(item, 'complete')
216+
}
217+
const successfullyTransferredTokenIds = tokenIds.filter(
218+
(id): id is string => id != null
219+
)
220+
await oldWallet.changeEnabledTokenIds(
221+
tokenIdsToEnable.filter(
222+
tokenId => !successfullyTransferredTokenIds.includes(tokenId)
223+
)
224+
)
225+
226+
const { modalShown } = securityCheckedWallets[oldWalletId]
227+
securityCheckedWallets[oldWalletId] = {
228+
checked: true,
229+
modalShown
230+
}
231+
} catch (e) {
232+
showError(e)
233+
for (const item of bundle) {
234+
handleItemStatus(item, 'error')
235+
}
236+
}
237+
return
238+
}
239+
184240
// Send tokens
185241
let feeTotal = '0'
186242
const hasError = false

src/locales/en_US.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ const strings = {
690690
gasPrice: 'Gas Price (Gwei)',
691691
string_disable: 'Disabled',
692692
string_done_cap: 'Done',
693+
string_included: 'Included',
693694
string_first_amoy_wallet_name: 'My Amoy',
694695
string_first_ethereum_wallet_name: 'My Ether',
695696
string_first_ethereum_classic_wallet_name: 'My Ethereum Classic',

src/locales/strings/enUS.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@
523523
"gasPrice": "Gas Price (Gwei)",
524524
"string_disable": "Disabled",
525525
"string_done_cap": "Done",
526+
"string_included": "Included",
526527
"string_first_amoy_wallet_name": "My Amoy",
527528
"string_first_ethereum_wallet_name": "My Ether",
528529
"string_first_ethereum_classic_wallet_name": "My Ethereum Classic",

0 commit comments

Comments
 (0)