Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import wallet #621

Merged
merged 58 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
879ab6c
chore(regtest): do not build tor locally in regtest env
theborakompanioni Jun 5, 2023
99ff1c5
feat(import): add rescanning param to session request
theborakompanioni Jun 5, 2023
a2a0a0c
feat(api): add rescanblockchain request to JmWalletApi
theborakompanioni Jun 5, 2023
dc319c1
feat: add import wallet button to wallets page
theborakompanioni Jun 12, 2023
faa0087
refactor: externalize component PreventLeavingPageByMistake
theborakompanioni Jul 3, 2023
4c83497
refactor: externalize component WalletCreationForm
theborakompanioni Jul 3, 2023
99fd236
wip: use WalletCreationForm in ImportWallet
theborakompanioni Jul 3, 2023
ea3a3b5
refactor: externalize SeedWordInput and rename to MnemonicWordInput
theborakompanioni Jul 3, 2023
fc1f450
feat(api): add wallet recover request to JmWalletApi
theborakompanioni Jul 3, 2023
ed4a8f7
fix(test): verify import button size
theborakompanioni Jul 3, 2023
202e194
fix(build): remove unused hook from component CreateWallet
theborakompanioni Jul 3, 2023
59c349e
build(deps): bump caniuse-lite to v1.0.30001511
theborakompanioni Jul 3, 2023
8ac3bf8
wip(import): recover walet and start rescan
theborakompanioni Jul 4, 2023
52c26b2
wip(import): start wallet after successful import
theborakompanioni Jul 4, 2023
1ffc3e2
refactor: externalize component WalletCreationConfirmation
theborakompanioni Jul 4, 2023
34ff89c
wip(import): use WalletCreationConfirmation view in component WalletI…
theborakompanioni Jul 4, 2023
3441df9
dev(regtest): add funds to dummy wallet during initialization of loca…
theborakompanioni Jul 5, 2023
6214683
wip(import): externalize rescan to own component
theborakompanioni Jul 9, 2023
b87ccea
dev(regtest): fund address beyond default gaplimit (201st address of …
theborakompanioni Jul 9, 2023
4162908
wip(import): add blockheight input to rescan chain form
theborakompanioni Jul 9, 2023
5c9d266
wip(import): update gaplimit before rescanning chain
theborakompanioni Jul 10, 2023
f001cbb
wip(import): start rescanning chain immediately
theborakompanioni Aug 9, 2023
884cace
wip(import): prevent import if rescan is in progress
theborakompanioni Aug 10, 2023
7a03424
wip(import): ability to customize gaplimit and blockheight
theborakompanioni Aug 10, 2023
b445dca
wip(import): show duration hint when importing wallet
theborakompanioni Aug 10, 2023
2affc71
wip(import): hide sensitive info while importing wallet
theborakompanioni Aug 10, 2023
f8e8dc9
wip(import): reset gaplimit to original value after importing
theborakompanioni Aug 10, 2023
380b2bf
wip(import): input all needed values in single form
theborakompanioni Aug 11, 2023
70187e7
wip(import): add description for blockheight and gaplimit
theborakompanioni Aug 11, 2023
53189b5
wip(import): add back button
theborakompanioni Aug 11, 2023
2220710
wip(import): add cancel button to WalletCreationForm
theborakompanioni Aug 11, 2023
4f6df6d
refactor: externalize component MnemonicPhraseInput
theborakompanioni Aug 11, 2023
bdb7754
wip(import): prevent user from accidentally leaving the page
theborakompanioni Aug 11, 2023
0dbaabe
wip(import): display blockheight and gaplimit as text on confirm page
theborakompanioni Aug 12, 2023
d16e5bd
wip(import): update gaplimit only if necessary
theborakompanioni Aug 13, 2023
e0607ab
wip(import): show rescan button only in dev mode
theborakompanioni Aug 13, 2023
8eb8be2
wip(import): set rescanning service info flag
theborakompanioni Aug 13, 2023
e702246
wip(import): show rescanning indicator on main wallet view
theborakompanioni Aug 13, 2023
56bb845
wip(import): show rescanning indicator in navbar
theborakompanioni Aug 13, 2023
6410da5
wip(import): disable sending when rescanning is in progress
theborakompanioni Aug 13, 2023
4964616
wip(import): disable navbar/earn/jam when rescanning is in progress
theborakompanioni Aug 13, 2023
0de0aa4
wip(import): improve error reporting of wallet import form
theborakompanioni Aug 13, 2023
fa4c410
wip(import): improve rescan page and hide with dev feature flag
theborakompanioni Aug 14, 2023
8b3a47e
Update src/i18n/locales/en/translation.json
theborakompanioni Aug 15, 2023
317714b
fix(import): realign footer position at page bottom
theborakompanioni Aug 16, 2023
8a356ac
fix(import): disable viewing jars on main wallet if rescanning is active
theborakompanioni Aug 16, 2023
e909458
wip(import): prevent creating new address when rescanning
theborakompanioni Aug 16, 2023
0243edb
fix(import): reload wallet info after rescanning finishes
theborakompanioni Aug 17, 2023
caf2dff
refactor: move auto reloading code from WalletContext to WalletInfoAu…
theborakompanioni Aug 17, 2023
2ca4e0d
refactor: remove unused method reloadDisplay
theborakompanioni Aug 17, 2023
81da668
wip(import): reset alert when navigating to previous step
theborakompanioni Aug 17, 2023
0f06fc0
wip(import): reload recursively till balance is found
theborakompanioni Aug 17, 2023
c5362f4
fix(ui): set cursor on unelectable to not-allowed
theborakompanioni Aug 17, 2023
bca5b63
wip(import): improve wording of gaplimit label and rescanning subtitle
theborakompanioni Aug 17, 2023
89a8fa7
wip(import): expand import options by default
theborakompanioni Aug 17, 2023
3224ff9
fix(import): fix param order for recursive wallet reload
theborakompanioni Sep 7, 2023
875ac2e
ui: add 'dev' badge to buttons only visible in dev env
theborakompanioni Sep 7, 2023
ce95273
refactor: remove translation of page rescan chain
theborakompanioni Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ RUN apt-get update \
&& apt-get install -qq --no-install-recommends gnupg tini procps vim git iproute2 supervisor \
# joinmarket dependencies
curl build-essential automake pkg-config libtool python3-dev python3-venv python3-pip python3-setuptools libltdl-dev \
# tor dependencies
libevent-dev libssl-dev zlib1g-dev \
tor \
&& rm -rf /var/lib/apt/lists/*

ENV REPO https://github.com/JoinMarket-Org/joinmarket-clientserver
Expand All @@ -15,7 +14,7 @@ ENV REPO_REF master
WORKDIR /src
RUN git clone "$REPO" . --depth=10 --branch "$REPO_BRANCH" && git checkout "$REPO_REF"

RUN ./install.sh --docker-install --with-local-tor --disable-secp-check --without-qt
RUN ./install.sh --docker-install --disable-secp-check --without-qt

ENV DATADIR /root/.joinmarket
ENV CONFIG ${DATADIR}/joinmarket.cfg
Expand Down
10 changes: 9 additions & 1 deletion docker/regtest/init-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,16 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P)
. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket2 --unmatured --blocks 50
. "$script_dir/fund-wallet.sh" --container jm_regtest_joinmarket3 --unmatured --blocks 50

# fund addresses of seed 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
# this is useful if you "import an existing wallet" and verify rescanning the chain works as expected.
dummy_wallet_address1='bcrt1q6rz28mcfaxtmd6v789l9rrlrusdprr9pz3cppk' # 1st address of jar A (m/84'/1'/0'/0/0)
dummy_wallet_address2='bcrt1qt5yxk3xzrx66q9wd5sdyynklqynqcyf7uh74j3' # 8th address of jar C (m/84'/1'/2'/0/7)
dummy_wallet_address3='bcrt1qn8804dw5fahuc5cwqteuq5j4xlhk2cnkq7a8kw' # 21st change address of jar E (m/84'/1'/4'/1/21)
# make block rewards spendable: 100 + 5 (default of `taker_utxo_age`) + 1 = 106
. "$script_dir/mine-block.sh" 106 &>/dev/null
. "$script_dir/mine-block.sh" 2 "$dummy_wallet_address1" &>/dev/null
. "$script_dir/mine-block.sh" 2 "$dummy_wallet_address2" &>/dev/null
. "$script_dir/mine-block.sh" 2 "$dummy_wallet_address3" &>/dev/null
. "$script_dir/mine-block.sh" 100 &>/dev/null

start_maker() {
local base_url; base_url=${1:-}
Expand Down
16 changes: 10 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions public/sprite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions src/components/Accordion.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { PropsWithChildren, useState } from 'react'
import React, { PropsWithChildren, useState } from 'react'
import { useSettings } from '../context/SettingsContext'
import * as rb from 'react-bootstrap'
import Sprite from './Sprite'

interface AccordionProps {
title: string
title: string | React.ReactNode
defaultOpen?: boolean
disabled?: boolean
}

const Accordion = ({ title, defaultOpen = false, children }: PropsWithChildren<AccordionProps>) => {
const Accordion = ({ title, defaultOpen = false, disabled = false, children }: PropsWithChildren<AccordionProps>) => {
const settings = useSettings()
const [isOpen, setIsOpen] = useState(defaultOpen)

Expand All @@ -18,6 +19,7 @@ const Accordion = ({ title, defaultOpen = false, children }: PropsWithChildren<A
variant={settings.theme}
className="d-flex align-items-center bg-transparent border-0 w-100 px-0 py-2"
onClick={() => setIsOpen((current) => !current)}
disabled={disabled}
>
{title}
<Sprite symbol={`caret-${isOpen ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
Expand Down
163 changes: 157 additions & 6 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import * as rb from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import {
Expand All @@ -9,13 +9,22 @@ import {
RouterProvider,
Outlet,
} from 'react-router-dom'
import classNames from 'classnames'
import * as Api from '../libs/JmWalletApi'
import { routes } from '../constants/routes'
import { useSessionConnectionError } from '../context/ServiceInfoContext'
import { useServiceInfo, useSessionConnectionError } from '../context/ServiceInfoContext'
import { useSettings } from '../context/SettingsContext'
import { useCurrentWallet, useSetCurrentWallet } from '../context/WalletContext'
import {
WalletInfo,
CurrentWallet,
useCurrentWallet,
useSetCurrentWallet,
useReloadCurrentWalletInfo,
} from '../context/WalletContext'
import { clearSession, setSession } from '../session'
import { isDebugFeatureEnabled } from '../constants/debugFeatures'
import CreateWallet from './CreateWallet'
import ImportWallet from './ImportWallet'
import Earn from './Earn'
import ErrorPage, { ErrorThrowingComponent } from './ErrorPage'
import Footer from './Footer'
Expand All @@ -26,6 +35,7 @@ import Navbar from './Navbar'
import Onboarding from './Onboarding'
import Receive from './Receive'
import Send from './Send'
import RescanChain from './RescanChain'
import Settings from './Settings'
import Wallets from './Wallets'

Expand All @@ -34,10 +44,14 @@ export default function App() {
const settings = useSettings()
const currentWallet = useCurrentWallet()
const setCurrentWallet = useSetCurrentWallet()
const reloadCurrentWalletInfo = useReloadCurrentWalletInfo()
const serviceInfo = useServiceInfo()
const sessionConnectionError = useSessionConnectionError()
const [reloadingWalletInfoCounter, setReloadingWalletInfoCounter] = useState(0)
const isReloadingWalletInfo = useMemo(() => reloadingWalletInfoCounter > 0, [reloadingWalletInfoCounter])

const startWallet = useCallback(
(name, token) => {
(name: Api.WalletName, token: Api.ApiToken) => {
setSession({ name, token })
setCurrentWallet({ name, token })
},
Expand All @@ -49,6 +63,27 @@ export default function App() {
setCurrentWallet(null)
}, [setCurrentWallet])

const reloadWalletInfo = useCallback(
(delay: Milliseconds) => {
setReloadingWalletInfoCounter((current) => current + 1)
console.info('Reloading wallet info...')
return new Promise<WalletInfo>((resolve, reject) =>
setTimeout(() => {
const abortCtrl = new AbortController()
reloadCurrentWalletInfo
.reloadAll({ signal: abortCtrl.signal })
.then((result) => resolve(result))
.catch((error) => reject(error))
.finally(() => {
console.info('Finished reloading wallet info.')
setReloadingWalletInfoCounter((current) => current - 1)
})
}, delay)
)
},
[reloadCurrentWalletInfo]
)

const router = createBrowserRouter(
createRoutesFromElements(
<Route
Expand Down Expand Up @@ -82,7 +117,11 @@ export default function App() {
* to the backend is down, e.g. "create-wallet" shows the seed quiz and it is important
* that it stays visible in case the backend becomes unavailable.
*/}
<Route id="create-wallet" path={routes.createWallet} element={<CreateWallet startWallet={startWallet} />} />
<Route
id="create-wallet"
path={routes.createWallet}
element={<CreateWallet parentRoute={'home'} startWallet={startWallet} />}
/>

{sessionConnectionError ? (
<Route
Expand All @@ -104,13 +143,19 @@ export default function App() {
path={routes.home}
element={<Wallets currentWallet={currentWallet} startWallet={startWallet} stopWallet={stopWallet} />}
/>
<Route
id="import-wallet"
path={routes.importWallet}
element={<ImportWallet parentRoute={'home'} startWallet={startWallet} />}
/>
{currentWallet && (
<>
<Route id="wallet" path={routes.wallet} element={<MainWalletView wallet={currentWallet} />} />
<Route id="jam" path={routes.jam} element={<Jam wallet={currentWallet} />} />
<Route id="send" path={routes.send} element={<Send wallet={currentWallet} />} />
<Route id="earn" path={routes.earn} element={<Earn wallet={currentWallet} />} />
<Route id="receive" path={routes.receive} element={<Receive wallet={currentWallet} />} />
<Route id="rescan" path={routes.rescanChain} element={<RescanChain wallet={currentWallet} />} />
<Route
id="settings"
path={routes.settings}
Expand Down Expand Up @@ -144,5 +189,111 @@ export default function App() {
)
}

return <RouterProvider router={router} />
return (
<>
<div
className={classNames('app', {
'jam-reload-wallet-info-in-progress': isReloadingWalletInfo,
'jm-coinjoin-in-progress': serviceInfo?.coinjoinInProgress === true,
'jm-rescan-in-progress': serviceInfo?.rescanning === true,
'jm-maker-running': serviceInfo?.makerRunning === true,
})}
>
<RouterProvider router={router} />
</div>
<WalletInfoAutoReload currentWallet={currentWallet} reloadWalletInfo={reloadWalletInfo} />
</>
)
}

const RELOAD_WALLET_INFO_DELAY: {
AFTER_RESCAN: Milliseconds
AFTER_UNLOCK: Milliseconds
} = {
// After rescanning, it is necessary to give the JM backend some time to synchronize.
// A couple of seconds should be enough, however, this depends on the user hardware
// and the delay might need to be increased if users encounter problems, e.g. the
// balance changes again when switching views.
// As reference: 4 seconds was not enough, even on regtest. But keep in mind, this only
// takes effect after rescanning the chain, which should happen quite infrequently.
AFTER_RESCAN: 8_000,

// No delay is needed after normal unlock of wallet
AFTER_UNLOCK: 0,
}

const MAX_RECURSIVE_WALLET_INFO_RELOADS = 10

interface WalletInfoAutoReloadProps {
currentWallet: CurrentWallet | null
reloadWalletInfo: (delay: Milliseconds) => Promise<WalletInfo>
}

/**
* A component that automatically reloads wallet information on certain state changes,
* e.g. when the wallet is unlocked or rescanning the chain finished successfully.
*
* If the auto-reloading on wallet change fails, the error can currently
* only be logged and cannot be displayed to the user satisfactorily.
* This might change in the future but is okay for now - components can
* always trigger a reload on demand and inform the user as they see fit.
*/
const WalletInfoAutoReload = ({ currentWallet, reloadWalletInfo }: WalletInfoAutoReloadProps) => {
const serviceInfo = useServiceInfo()
const [previousRescanning, setPreviousRescanning] = useState(serviceInfo?.rescanning || false)
const [currentRescanning, setCurrentRescanning] = useState(serviceInfo?.rescanning || false)
const rescanningFinished = useMemo(
() => previousRescanning === true && currentRescanning === false,
[previousRescanning, currentRescanning]
)

useEffect(() => {
setPreviousRescanning(currentRescanning)
setCurrentRescanning(serviceInfo?.rescanning || false)
}, [serviceInfo, currentRescanning])

useEffect(
function reloadAfterUnlock() {
if (!currentWallet) return

reloadWalletInfo(RELOAD_WALLET_INFO_DELAY.AFTER_UNLOCK).catch((err) => console.error(err))
},
[currentWallet, reloadWalletInfo]
)

useEffect(
function reloadAfterRescan() {
if (!currentWallet || !rescanningFinished) return

// Hacky: If the balance changes after a reload, the backend might still not have been fully synchronized - try again!
// Hint 1: Wallet might be empty after the first attempt
// Hint 2: Just because wallet balance did not change, it does not mean everything has been found.
const reloadWhileBalanceChangesRecursively = async (
currentBalance: Api.AmountSats,
delay: Milliseconds,
maxCalls: number,
callCounter: number = 0
) => {
if (callCounter >= maxCalls) return
const info = await reloadWalletInfo(delay)
const newBalance = info.balanceSummary.calculatedTotalBalanceInSats
if (newBalance > currentBalance) {
await reloadWhileBalanceChangesRecursively(newBalance, delay, maxCalls, callCounter++)
}
}

reloadWalletInfo(RELOAD_WALLET_INFO_DELAY.AFTER_RESCAN)
.then((info) =>
reloadWhileBalanceChangesRecursively(
info.balanceSummary.calculatedTotalBalanceInSats,
RELOAD_WALLET_INFO_DELAY.AFTER_RESCAN,
MAX_RECURSIVE_WALLET_INFO_RELOADS
)
)
.catch((err) => console.error(err))
},
[currentWallet, rescanningFinished, reloadWalletInfo]
)

return <></>
}
Loading