Skip to content

Commit 4e0992c

Browse files
authored
Merge pull request #155 from oasisprotocol/lw/cancel-on-navigate
move: Cancel long running promises on react-router navigation
2 parents c7bea11 + 93df517 commit 4e0992c

File tree

8 files changed

+141
-66
lines changed

8 files changed

+141
-66
lines changed

.changelog/155.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
move: Fix withdrawing/depositing after navigating away and back without reload

move/src/App.tsx

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import { ConnectButton } from '@rainbow-me/rainbowkit'
22
import classes from './App.module.css'
33
import { MoveButton as Button, PrivateKeyHelpModal, VideoModal } from '@oasisprotocol/rose-app-ui/move'
44
import { Layout } from './components/Layout'
5-
import { useDeposit } from './useDeposit'
6-
import { useWithdraw } from './useWithdraw'
75
import { useIsRpcResponding } from './utils/useIsRpcResponding'
86

97
import videocam_svg from '@material-design-icons/svg/filled/videocam.svg'
@@ -17,13 +15,16 @@ import { Deposit } from './deposit/Deposit'
1715
import { useReloadIfAccountSwitched } from './utils/useReloadIfAccountSwitched'
1816
import { Withdraw } from './withdraw/Withdraw'
1917
import { Header } from '@oasisprotocol/rose-app-ui/core'
18+
import { trackEvent } from 'fathom-client'
19+
import { useGenerateConsensusAccount } from './deposit/useGenerateConsensusAccount'
20+
import { useGenerateSapphireAccount } from './withdraw/useGenerateSapphireAccount'
2021

2122
export function App() {
2223
useReloadIfAccountSwitched()
2324
const isRpcResponding = useIsRpcResponding()
2425
const sapphireAddress = useAccount().address
25-
const deposit = useDeposit()
26-
const withdraw = useWithdraw()
26+
const deposit = useGenerateConsensusAccount()
27+
const withdraw = useGenerateSapphireAccount()
2728

2829
const [isMoveWalkthroughVideoModalOpen, setIsMoveWalkthroughVideoModalOpen] = useState(false)
2930
const [isPrivateKeyHelpModalOpen, setIsPrivateKeyHelpModalOpen] = useState(false)
@@ -113,7 +114,16 @@ export function App() {
113114
</div>
114115
<div className={classes.cardContent}>
115116
<p>Easily move your ROSE from a crypto exchange or consensus account to use on Sapphire.</p>
116-
<Button onClick={deposit.step2}>Select and sign-in</Button>
117+
<Button
118+
onClick={async () => {
119+
const generatedConsensusAccount = await deposit.generateConsensusAccount(sapphireAddress)
120+
if (generatedConsensusAccount.isFresh) {
121+
trackEvent('deposit account created')
122+
}
123+
}}
124+
>
125+
Select and sign-in
126+
</Button>
117127
</div>
118128
</div>
119129

@@ -123,7 +133,17 @@ export function App() {
123133
</div>
124134
<div className={classes.cardContent}>
125135
<p>Move your ROSE from Sapphire back to a crypto exchange or consensus account.</p>
126-
<Button onClick={withdraw.step2}>Select and sign-in</Button>
136+
<Button
137+
onClick={async () => {
138+
const { generatedConsensusAccount } =
139+
await withdraw.generateSapphireAccount(sapphireAddress)
140+
if (generatedConsensusAccount.isFresh) {
141+
trackEvent('withdrawal account created')
142+
}
143+
}}
144+
>
145+
Select and sign-in
146+
</Button>
127147
</div>
128148
</div>
129149
</div>
@@ -133,9 +153,14 @@ export function App() {
133153
}
134154

135155
if (deposit.generatedConsensusAccount) {
136-
return <Deposit deposit={deposit} />
156+
return <Deposit generatedConsensusAccount={deposit.generatedConsensusAccount} />
137157
}
138-
if (withdraw.generatedConsensusAccount) {
139-
return <Withdraw withdraw={withdraw} />
158+
if (withdraw.generatedSapphireAccount && withdraw.generatedConsensusAccount) {
159+
return (
160+
<Withdraw
161+
generatedSapphireAccount={withdraw.generatedSapphireAccount}
162+
generatedConsensusAccount={withdraw.generatedConsensusAccount}
163+
/>
164+
)
140165
}
141166
}

move/src/deposit/Deposit.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,18 @@ import loader_blocks_svg from '/move/loader_blocks.svg?url'
2121
import logo_rose_move_svg from '/move/logo_rose_move.svg?url'
2222
import symbol_check_circle_svg from '/move/symbol_check_circle.svg?url'
2323
import symbol_warning_svg from '/move/symbol_warning.svg?url'
24+
import { ConsensusAccount } from './useGenerateConsensusAccount'
2425

25-
export function Deposit(props: { deposit: ReturnType<typeof useDeposit> }) {
26-
const { generatedConsensusAccount, transferMore, progress, isBlockingNavigatingAway, isPrevError } =
27-
props.deposit // Parent useDeposit
26+
export function Deposit({ generatedConsensusAccount }: { generatedConsensusAccount: ConsensusAccount }) {
27+
const { transferMore, progress, isBlockingNavigatingAway, isPrevError } = useDeposit({
28+
generatedConsensusAccount,
29+
})
2830

2931
const isError = progress.percentage === undefined
3032

3133
const [isMoveTransferVideoModalOpen, setIsMoveTransferVideoModalOpen] = useState(false)
3234
const [isCopyPrivateKeyModalOpen, setIsCopyPrivateKeyModalOpen] = useState(false)
3335

34-
if (!generatedConsensusAccount) throw new Error('<Deposit> used before SIWE')
3536
return (
3637
<>
3738
<Layout

move/src/useDeposit.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { useState } from 'react'
1+
import { useEffect, useState } from 'react'
22
import { useAccount, useBalance } from 'wagmi'
33
import { depositToSapphireStep1, depositToSapphireStep2 } from './deposit/depositToSapphire'
4-
import { ConsensusAccount, useGenerateConsensusAccount } from './deposit/useGenerateConsensusAccount'
4+
import { ConsensusAccount } from './deposit/useGenerateConsensusAccount'
55
import { usePrevious } from './hooks/usePrevious.ts'
66
import {
77
fromBaseUnitsToTrackEventCents,
@@ -12,34 +12,33 @@ import {
1212
import { useBlockNavigatingAway } from './utils/useBlockNavigatingAway'
1313
import { trackEvent } from 'fathom-client'
1414
import { consensusConfig } from './utils/oasisConfig.ts'
15+
import { UnmountedAbortError, useUnmountSignal } from './utils/useUnmountSignal'
1516

1617
/** any consensus -> generatedConsensusAccount -> sapphireAddress */
17-
export function useDeposit() {
18+
export function useDeposit({ generatedConsensusAccount }: { generatedConsensusAccount: ConsensusAccount }) {
19+
const unmountSignal = useUnmountSignal()
1820
const { isBlockingNavigatingAway, blockNavigatingAway, allowNavigatingAway } = useBlockNavigatingAway()
1921
const sapphireAddress = useAccount().address
20-
const { generatedConsensusAccount, generateConsensusAccount } = useGenerateConsensusAccount()
2122
const [progress, setProgress] = useState({ percentage: 0 as number | undefined, message: '' })
2223
const { refetch: updateBalanceInsideConnectButton } = useBalance({ address: sapphireAddress })
2324
const isPrevError = usePrevious(progress.percentage === undefined)
2425

25-
// Long running promise, doesn't get canceled if this component is destroyed
26-
async function step2() {
27-
if (!sapphireAddress) return
28-
const generatedConsensusAccount = await generateConsensusAccount(sapphireAddress)
29-
30-
if (generatedConsensusAccount.isFresh) {
31-
trackEvent('deposit account created')
32-
}
33-
26+
// Automatically start listening, and only cancel if unmounted.
27+
useEffect(() => {
28+
if (!sapphireAddress) throw new Error('useDeposit used before wallet connected')
3429
blockNavigatingAway() // Start blocking early for the first transfer
35-
await step3(generatedConsensusAccount, sapphireAddress)
36-
}
30+
step3(generatedConsensusAccount, sapphireAddress)
31+
// eslint-disable-next-line react-hooks/exhaustive-deps
32+
}, [])
3733

3834
async function step3(consensusAccount: ConsensusAccount, sapphireAddress: `0x${string}`) {
3935
// Note: don't use outside state vars. They are outdated.
4036
try {
37+
await new Promise(r => setTimeout(r, 1000)) // Handle React StrictMode: step3 is called by useEffect on mount
38+
if (unmountSignal.aborted) throw new UnmountedAbortError()
39+
4140
setProgress({ percentage: 0.05, message: 'Waiting to move your ROSE…' })
42-
const amountToDeposit = await waitForConsensusBalance(consensusAccount.address, 0n)
41+
const amountToDeposit = await waitForConsensusBalance(consensusAccount.address, 0n, unmountSignal)
4342

4443
trackEvent('deposit flow started', {
4544
_value: fromBaseUnitsToTrackEventCents(amountToDeposit.raw, consensusConfig.decimals),
@@ -63,7 +62,7 @@ export function useDeposit() {
6362
sapphireAddress: sapphireAddress,
6463
})
6564
setProgress({ percentage: 0.75, message: 'ROSE transfer initiated' })
66-
await waitForSapphireBalance(sapphireAddress, preDepositSapphireBalance.raw)
65+
await waitForSapphireBalance(sapphireAddress, preDepositSapphireBalance.raw, unmountSignal)
6766
// TODO: handle probable failure if balance doesn't change after ~10 seconds of depositing
6867
setProgress({
6968
percentage: 1.0,
@@ -77,11 +76,14 @@ export function useDeposit() {
7776
allowNavigatingAway() // Stop blocking unless new transfer comes in
7877
await updateBalanceInsideConnectButton()
7978

79+
if (unmountSignal.aborted) throw new UnmountedAbortError()
8080
await new Promise(r => setTimeout(r, 6000))
8181
// Stay on "Deposited" screen unless new transfer comes in
82-
await waitForConsensusBalance(consensusAccount.address, 0n)
82+
await waitForConsensusBalance(consensusAccount.address, 0n, unmountSignal)
83+
if (unmountSignal.aborted) throw new UnmountedAbortError()
8384
if (window.mock) throw 'mock error'
8485
} catch (err) {
86+
if (err instanceof UnmountedAbortError) return // Ignore and stop looping
8587
console.error(err)
8688
setProgress({ percentage: undefined, message: `Error. Retrying…` })
8789
await new Promise(r => setTimeout(r, 6000))
@@ -101,8 +103,6 @@ export function useDeposit() {
101103

102104
return {
103105
sapphireAddress,
104-
generatedConsensusAccount,
105-
step2,
106106
transferMore,
107107
progress,
108108
isBlockingNavigatingAway,

move/src/useWithdraw.ts

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,26 @@ import {
99
} from './utils/getBalances'
1010
import { useBlockNavigatingAway } from './utils/useBlockNavigatingAway'
1111
import { transferToConsensus } from './withdraw/transferToConsensus'
12-
import { useGenerateSapphireAccount } from './withdraw/useGenerateSapphireAccount'
12+
import { ConsensusAccount, SapphireAccount } from './withdraw/useGenerateSapphireAccount'
1313
import { minimalWithdrawableAmount, withdrawToConsensus } from './withdraw/withdrawToConsensus'
1414
import { trackEvent } from 'fathom-client'
1515
import { consensusConfig, sapphireConfig } from './utils/oasisConfig.ts'
16+
import { UnmountedAbortError, useUnmountSignal } from './utils/useUnmountSignal'
1617

1718
/**
1819
* sapphireAddress -> generatedSapphireAccount -> generatedConsensusAccount -> consensusAddress
1920
*/
20-
export function useWithdraw() {
21+
export function useWithdraw({
22+
generatedSapphireAccount,
23+
generatedConsensusAccount,
24+
}: {
25+
generatedSapphireAccount: SapphireAccount
26+
generatedConsensusAccount: ConsensusAccount
27+
}) {
28+
const unmountSignal = useUnmountSignal()
2129
const { isBlockingNavigatingAway, blockNavigatingAway, allowNavigatingAway } = useBlockNavigatingAway()
2230
const sapphireAddress = useAccount().address
23-
const { generatedSapphireAccount, generatedConsensusAccount, generateSapphireAccount } =
24-
useGenerateSapphireAccount()
31+
2532
const [consensusAddress, setConsensusAddress] = useState<`oasis1${string}`>()
2633
const [progress, setProgress] = useState({ percentage: 0 as number | undefined, message: '' })
2734
const [isInputMode, setIsInputMode] = useState(true)
@@ -31,15 +38,6 @@ export function useWithdraw() {
3138
const { sendTransactionAsync } = useSendTransaction()
3239
const isPrevError = usePrevious(progress.percentage === undefined)
3340

34-
async function step2() {
35-
if (!sapphireAddress) return
36-
await generateSapphireAccount(sapphireAddress)
37-
38-
if (generatedConsensusAccount?.isFresh) {
39-
trackEvent('withdrawal account created')
40-
}
41-
}
42-
4341
async function step3(value: bigint) {
4442
if (!generatedSapphireAccount) return
4543

@@ -53,7 +51,6 @@ export function useWithdraw() {
5351
})
5452
}
5553

56-
// Long running promise, doesn't get canceled if this component is destroyed
5754
async function step4(consensusAddress: `oasis1${string}`, retryingAfterError: number) {
5855
// Note: outside state var consensusAddress is outdated. Use param.
5956
if (!sapphireAddress) return
@@ -67,7 +64,8 @@ export function useWithdraw() {
6764
setProgress({ percentage: 0.05, message: 'Waiting to move your ROSE…' })
6865
const availableAmountToWithdraw = await waitForSapphireBalance(
6966
generatedSapphireAccount.address,
70-
minimalWithdrawableAmount
67+
minimalWithdrawableAmount,
68+
unmountSignal
7169
)
7270
setProgress({ percentage: 0.25, message: 'ROSE transfer initiated' })
7371

@@ -84,21 +82,26 @@ export function useWithdraw() {
8482
}
8583

8684
// TODO: handle probable failure if balance doesn't change after ~10 seconds of withdraw
87-
const amountToWithdraw2 = await waitForConsensusBalance(generatedConsensusAccount.address, 0n)
85+
const amountToWithdraw2 = await waitForConsensusBalance(
86+
generatedConsensusAccount.address,
87+
0n,
88+
unmountSignal
89+
)
8890

8991
trackEvent('withdrawal flow started', {
9092
_value: fromBaseUnitsToTrackEventCents(amountToWithdraw2.raw, consensusConfig.decimals),
9193
})
9294

9395
const preWithdrawConsensusBalance = await getConsensusBalance(consensusAddress)
96+
if (unmountSignal.aborted) throw new UnmountedAbortError()
9497
await transferToConsensus({
9598
amount: amountToWithdraw2.raw,
9699
fromConsensusAccount: generatedConsensusAccount,
97100
toConsensusAddress: consensusAddress,
98101
})
99102
setProgress({ percentage: 0.75, message: `Withdrawing ${amountToWithdraw2.formatted} ROSE` })
100103
if (window.mock && !retryingAfterError) throw 'mock error'
101-
await waitForConsensusBalance(consensusAddress, preWithdrawConsensusBalance.raw)
104+
await waitForConsensusBalance(consensusAddress, preWithdrawConsensusBalance.raw, unmountSignal)
102105
setProgress({
103106
percentage: 1.0,
104107
message: 'Your ROSE transfer is complete!',
@@ -110,6 +113,7 @@ export function useWithdraw() {
110113

111114
allowNavigatingAway() // Stop blocking unless new transfer comes in
112115
} catch (err) {
116+
if (err instanceof UnmountedAbortError) return // Ignore and stop looping
113117
console.error(err)
114118
setProgress({ percentage: undefined, message: `Error. Retrying…` })
115119
await new Promise(r => setTimeout(r, 6000))
@@ -118,11 +122,17 @@ export function useWithdraw() {
118122
return
119123
}
120124

121-
await new Promise(r => setTimeout(r, 6000))
122-
// Stay on "Withdrawn" screen unless new transfer comes in
123-
await waitForSapphireBalance(generatedSapphireAccount.address, 0n)
124-
// Don't loop, force user to input destination again
125-
transferMore()
125+
try {
126+
await new Promise(r => setTimeout(r, 6000))
127+
if (unmountSignal.aborted) throw new UnmountedAbortError()
128+
// Stay on "Withdrawn" screen unless new transfer comes in
129+
await waitForSapphireBalance(generatedSapphireAccount.address, 0n, unmountSignal)
130+
if (unmountSignal.aborted) throw new UnmountedAbortError()
131+
// Don't loop, force user to input destination again
132+
transferMore()
133+
} catch (err) {
134+
return // Ignore
135+
}
126136
}
127137

128138
function transferMore() {
@@ -131,11 +141,8 @@ export function useWithdraw() {
131141

132142
return {
133143
sapphireAddress,
134-
generatedSapphireAccount,
135-
generatedConsensusAccount,
136144
consensusAddress,
137145
setConsensusAddress,
138-
step2,
139146
step3,
140147
step4,
141148
transferMore,

move/src/utils/getBalances.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as oasisRT from '@oasisprotocol/client-rt'
44
import BigNumber from 'bignumber.js'
55
import { getConsensusAccountsWrapper, getNodeInternal } from './client.ts'
66
import { consensusConfig, sapphireConfig } from './oasisConfig'
7+
import { UnmountedAbortError } from './useUnmountSignal'
78

89
export async function getBalances(props: {
910
consensusAddress: `oasis1${string}`
@@ -15,25 +16,37 @@ export async function getBalances(props: {
1516
}
1617

1718
/** Continuously fetches gRPC balance until it is > minBalance */
18-
export async function waitForConsensusBalance(consensusAddress: `oasis1${string}`, moreThan: bigint) {
19+
export async function waitForConsensusBalance(
20+
consensusAddress: `oasis1${string}`,
21+
moreThan: bigint,
22+
unmountSignal: undefined | AbortSignal
23+
) {
1924
// eslint-disable-next-line no-constant-condition
2025
while (true) {
2126
const balance = await getConsensusBalance(consensusAddress)
27+
if (unmountSignal?.aborted) throw new UnmountedAbortError()
2228
console.log('waitForConsensusBalance', balance, '>', moreThan)
2329
if (balance.raw > moreThan) return balance
2430
await new Promise(r => setTimeout(r, 6000))
31+
if (unmountSignal?.aborted) throw new UnmountedAbortError()
2532
if (window.mock) return balance
2633
}
2734
}
2835

2936
/** Continuously fetches gRPC balance until it is > minBalance */
30-
export async function waitForSapphireBalance(sapphireAddress: `0x${string}`, moreThan: bigint) {
37+
export async function waitForSapphireBalance(
38+
sapphireAddress: `0x${string}`,
39+
moreThan: bigint,
40+
unmountSignal: undefined | AbortSignal
41+
) {
3142
// eslint-disable-next-line no-constant-condition
3243
while (true) {
3344
const balance = await getSapphireBalance(sapphireAddress)
45+
if (unmountSignal?.aborted) throw new UnmountedAbortError()
3446
console.log('waitForSapphireBalance', balance, '>', moreThan)
3547
if (balance.raw > moreThan) return balance
3648
await new Promise(r => setTimeout(r, 6000))
49+
if (unmountSignal?.aborted) throw new UnmountedAbortError()
3750
if (window.mock) return balance
3851
}
3952
}

0 commit comments

Comments
 (0)