From 540dfaafbaacf59539a88770b4a6d18e939dc25d Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 29 Jun 2022 17:48:52 +0200 Subject: [PATCH 01/13] feat: move fidelity bonds to earn screen --- public/sprite.svg | 39 ++ src/components/Earn.jsx | 55 ++- src/components/Earn.module.css | 5 + src/components/fb/CreateFidelityBond.jsx | 354 ++++++++++++++++++ .../fb/CreateFidelityBond.module.css | 56 +++ src/components/fb/ExistingFidelityBond.jsx | 52 +++ .../fb/ExistingFidelityBond.module.css | 36 ++ .../fb/FidelityBondSteps.module.css | 192 ++++++++++ src/components/fb/FidelityBondSteps.tsx | 293 +++++++++++++++ src/components/fb/utils.ts | 18 + src/components/fidelity_bond/LockdateForm.tsx | 4 +- src/components/jars/Jar.module.css | 1 + src/hooks/BalanceSummary.ts | 2 +- 13 files changed, 1095 insertions(+), 12 deletions(-) create mode 100644 src/components/fb/CreateFidelityBond.jsx create mode 100644 src/components/fb/CreateFidelityBond.module.css create mode 100644 src/components/fb/ExistingFidelityBond.jsx create mode 100644 src/components/fb/ExistingFidelityBond.module.css create mode 100644 src/components/fb/FidelityBondSteps.module.css create mode 100644 src/components/fb/FidelityBondSteps.tsx create mode 100644 src/components/fb/utils.ts diff --git a/public/sprite.svg b/public/sprite.svg index b96d804ac..0eb0f694c 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -249,4 +249,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Earn.jsx b/src/components/Earn.jsx index 31701b1d8..3d5ba0d18 100644 --- a/src/components/Earn.jsx +++ b/src/components/Earn.jsx @@ -4,11 +4,14 @@ import { Formik } from 'formik' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import { useSettings } from '../context/SettingsContext' -import { useCurrentWallet } from '../context/WalletContext' +import { useCurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo } from '../context/WalletContext' import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext' +import { useBalanceSummary } from '../hooks/BalanceSummary' import Sprite from './Sprite' import PageTitle from './PageTitle' import SegmentedTabs from './SegmentedTabs' +import { CreateFidelityBond } from './fb/CreateFidelityBond' +import { ExistingFidelityBond } from './fb/ExistingFidelityBond' import { EarnReportOverlay } from './EarnReport' import * as Api from '../libs/JmWalletApi' import styles from './Earn.module.css' @@ -82,8 +85,11 @@ export default function Earn() { const { t } = useTranslation() const settings = useSettings() const currentWallet = useCurrentWallet() + const currentWalletInfo = useCurrentWalletInfo() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const serviceInfo = useServiceInfo() const reloadServiceInfo = useReloadServiceInfo() + const balanceSummary = useBalanceSummary(currentWalletInfo) const [isAdvancedView, setIsAdvancedView] = useState(settings.useAdvancedWalletMode) const [alert, setAlert] = useState(null) @@ -93,6 +99,7 @@ export default function Earn() { const [isWaitingMakerStart, setIsWaitingMakerStart] = useState(false) const [isWaitingMakerStop, setIsWaitingMakerStop] = useState(false) const [isShowReport, setIsShowReport] = useState(false) + const [fidelityBonds, setFidelityBonds] = useState([]) const startMakerService = (ordertype, minsize, cjfee_a, cjfee_r) => { setIsSending(true) @@ -146,14 +153,23 @@ export default function Earn() { setIsLoading(true) - reloadServiceInfo({ signal: abortCtrl.signal }) + const reloadingServiceInfo = reloadServiceInfo({ signal: abortCtrl.signal }) + const reloadingCurrentWalletInfo = reloadCurrentWalletInfo({ signal: abortCtrl.signal }).then((info) => { + if (info && !abortCtrl.signal.aborted) { + const unspentOutputs = info.data.utxos.utxos + const fbOutputs = unspentOutputs.filter((utxo) => utxo.locktime) + setFidelityBonds(fbOutputs) + } + }) + + Promise.all([reloadingServiceInfo, reloadingCurrentWalletInfo]) .catch((err) => { !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message }) }) .finally(() => !abortCtrl.signal.aborted && setIsLoading(false)) return () => abortCtrl.abort() - }, [currentWallet, isSending, reloadServiceInfo]) + }, [currentWallet, isSending, reloadServiceInfo, reloadCurrentWalletInfo]) useEffect(() => { if (isSending) return @@ -255,16 +271,39 @@ export default function Earn() { {t('earn.alert_coinjoin_in_progress')} - {alert && {alert.message}} - {serviceInfoAlert && {serviceInfoAlert.message}} - {!serviceInfo?.coinjoinInProgress && !serviceInfo?.makerRunning && !isWaitingMakerStart && !isWaitingMakerStop &&

{t('earn.market_explainer')}

} - + {!serviceInfo?.coinjoinInProgress && ( + <> + +
+ {fidelityBonds.length > 0 && + fidelityBonds.map((utxo, index) => )} + {!isLoading && balanceSummary ? ( + 0} + accountBalances={balanceSummary?.accountBalances} + totalBalance={balanceSummary?.totalBalance} + wallet={currentWallet} + walletInfo={currentWalletInfo} + /> + ) : ( + + + + )} +
+ + )} {!serviceInfo?.coinjoinInProgress && ( {({ handleSubmit, setFieldValue, handleChange, handleBlur, values, touched, errors, isSubmitting }) => ( @@ -423,7 +462,6 @@ export default function Earn() {
)} - - setIsShowReport(false)} /> diff --git a/src/components/Earn.module.css b/src/components/Earn.module.css index 4f0ec453c..8f53024c4 100644 --- a/src/components/Earn.module.css +++ b/src/components/Earn.module.css @@ -3,6 +3,11 @@ border-radius: 0.25rem; } +.earn .fb-loader { + height: 11rem; + border-radius: 0.25rem; +} + .earn form input:not([type='checkbox']) { height: 3.5rem; } diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx new file mode 100644 index 000000000..ebc30a857 --- /dev/null +++ b/src/components/fb/CreateFidelityBond.jsx @@ -0,0 +1,354 @@ +import React, { useState, useEffect, useMemo } from 'react' +import * as rb from 'react-bootstrap' +import * as Api from '../../libs/JmWalletApi' +import { useReloadCurrentWalletInfo } from '../../context/WalletContext' +import Sprite from '../Sprite' +import { ConfirmModal } from '../Modal' +import { SelectJar, SelectUtxos, SelectDate, FreezeUtxos, ReviewInputs } from './FidelityBondSteps' +import { utxosToFreeze, allUtxosAreFrozen } from './utils' +import { toYearsRange, DEFAULT_MAX_TIMELOCK_YEARS } from '../fidelity_bond/fb_utils' +import { isDebugFeatureEnabled } from '../../constants/debugFeatures' +import styles from './CreateFidelityBond.module.css' + +const steps = { + selectDate: 0, + selectJar: 1, + selectUtxos: 2, + freezeUtxos: 3, + reviewInputs: 4, + createFidelityBond: 5, + done: 6, +} + +const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBalance, wallet, walletInfo }) => { + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + + const [isExpanded, setIsExpanded] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [step, setStep] = useState(steps.selectDate) + const [showConfirmInputsModal, setShowConfirmInputsModal] = useState(false) + + const [utxos, setUtxos] = useState({}) + + const [lockDate, setLockDate] = useState(null) + const [selectedJar, setSelectedJar] = useState(null) + const [selectedUtxos, setSelectedUtxos] = useState([]) + const [timelockedAddress, setTimelockedAddress] = useState(null) + + const yearsRange = useMemo(() => { + if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { + return toYearsRange(-1, DEFAULT_MAX_TIMELOCK_YEARS) + } + return toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS) + }, []) + + const reset = () => { + setIsExpanded(false) + setStep(steps.selectDate) + setSelectedJar(null) + setSelectedUtxos([]) + setLockDate(null) + setTimelockedAddress(null) + } + + const freezeUtxos = async (utxos) => { + setIsLoading(true) + + const abortCtrl = new AbortController() + + const { name: walletName, token } = wallet + const freezeCalls = utxos.map((utxo) => + Api.postFreeze({ walletName, token }, { utxo: utxo.utxo, freeze: true }).then((res) => { + if (!res.ok) { + return Api.Helper.throwError(res, 'current_wallet_advanced.error_freeze_failed') + } + }) + ) + + await Promise.all(freezeCalls) + .then((_) => reloadCurrentWalletInfo({ signal: abortCtrl.signal })) + .catch((err) => { + console.error(err.message) + }) + .finally(() => setIsLoading(false)) + } + + const loadTimeLockedAddress = async (lockDate) => { + setIsLoading(true) + + const abortCtrl = new AbortController() + + await Api.getAddressTimelockNew({ + walletName: wallet.name, + token: wallet.token, + signal: abortCtrl.signal, + lockdate: lockDate, + }) + .then((res) => + res.ok ? res.json() : Api.Helper.throwError(res, 'fidelity_bond.error_loading_timelock_address_failed') + ) + .then((data) => setTimelockedAddress(data.address)) + .catch((err) => { + console.error(err.message) + }) + .finally(() => setIsLoading(false)) + } + + const directSweepToFidelityBond = async (jar, address) => { + setIsLoading(true) + + const res = await Api.postDirectSend( + { walletName: wallet.name, token: wallet.token }, + { + mixdepth: jar, + destination: address, + amount_sats: 0, // sweep + } + ) + + if (!res.ok) { + await Api.Helper.throwError(res) + } + + setIsLoading(false) + } + + useEffect(() => { + const utxos = walletInfo.data.utxos.utxos + + const utxosByAccount = utxos.reduce((res, utxo) => { + const { mixdepth } = utxo + res[mixdepth] = res[mixdepth] || [] + + // todo: remove + if (utxo.value === 100000000) { + utxo.label = 'cj-out' + } + + res[mixdepth].push(utxo) + + return res + }, {}) + + setUtxos(utxosByAccount) + }, [walletInfo]) + + const stepComponent = (currentStep) => { + switch (currentStep) { + case steps.selectDate: + return setLockDate(date)} /> + case steps.selectJar: + return ( + setSelectedJar(accountIndex)} + /> + ) + case steps.selectUtxos: + return ( + setSelectedUtxos([...selectedUtxos, utxo])} + onUtxoDeselected={(utxo) => setSelectedUtxos(selectedUtxos.filter((it) => it !== utxo))} + /> + ) + case steps.freezeUtxos: + return ( + + ) + case steps.reviewInputs: + return isLoading || timelockedAddress === null ? ( +
Loading...
+ ) : ( + + ) + case steps.createFidelityBond: + return isLoading ? ( +
+
+ ) : ( +
+ + Fidelity Bond Created! +
+ ) + default: + return null + } + } + + const buttonText = (currentStep) => { + switch (currentStep) { + case steps.selectDate: + return 'Next' + case steps.selectJar: + return 'Next' + case steps.selectUtxos: + return 'Next' + case steps.freezeUtxos: + return allUtxosAreFrozen({ + utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), + }) + ? 'Next' + : 'Freeze UTXOs' + case steps.reviewInputs: + return 'Create Fidelity Bond' + case steps.createFidelityBond: + return 'Close' + default: + return null + } + } + + const nextStep = (currentStep) => { + if (currentStep === steps.selectDate) { + if (lockDate !== null) { + return steps.selectJar + } + } + + if (currentStep === steps.selectJar) { + if (selectedJar !== null) { + return steps.selectUtxos + } + } + + if (currentStep === steps.selectUtxos) { + if (selectedUtxos.length > 0 && selectedUtxos.every((utxo) => !utxo.frozen)) { + return steps.freezeUtxos + } + } + + if (currentStep === steps.freezeUtxos) { + if (isLoading) { + return null + } + + if ( + allUtxosAreFrozen({ + utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), + }) + ) { + return steps.reviewInputs + } + + return steps.freezeUtxos + } + + if (currentStep === steps.reviewInputs) { + return steps.createFidelityBond + } + + if (currentStep === steps.createFidelityBond) { + if (!isLoading) { + return steps.done + } + } + + return null + } + + const onButtonClicked = () => { + if (nextStep(step) === null) { + return + } + + if ( + step === steps.freezeUtxos && + !allUtxosAreFrozen({ + utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), + }) + ) { + freezeUtxos(utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos })) + } + + if (nextStep(step) === steps.reviewInputs) { + loadTimeLockedAddress(lockDate) + } + + if (nextStep(step) === steps.createFidelityBond) { + setShowConfirmInputsModal(true) + return + } + + if (nextStep(step) === steps.done) { + reset() + } + + setStep(nextStep(step)) + } + + return ( +
+ setShowConfirmInputsModal(false)} + onConfirm={() => { + setStep(steps.createFidelityBond) + setShowConfirmInputsModal(false) + directSweepToFidelityBond(selectedJar, timelockedAddress) + }} + /> +
setIsExpanded(!isExpanded)}> +
+
+ {otherFidelityBondExists ? 'Create another Fidelity Bond' : 'Long-term deposit with a Fidelity Bond'} +
+ +
+ {!otherFidelityBondExists && ( +
+ +
+ You increase your chance to be chosen as a market maker in a collaborative transaction by locking up some + funds for a certain amount of time. Be aware that your bitcoin can only be moved again when the bond is + expired. +
+
+ )} +
+ +
+
+
{stepComponent(step)}
+
+ {buttonText(step) !== null && ( + + {buttonText(step)} + + )} + {step !== steps.createFidelityBond && ( + + Cancel + + )} +
+
+
+
+ ) +} + +export { CreateFidelityBond } diff --git a/src/components/fb/CreateFidelityBond.module.css b/src/components/fb/CreateFidelityBond.module.css new file mode 100644 index 000000000..f25ea7874 --- /dev/null +++ b/src/components/fb/CreateFidelityBond.module.css @@ -0,0 +1,56 @@ +.container { + border: 1px solid var(--bs-gray-200); + border-radius: 0.3rem; + padding: 1.25rem; +} + +.header { + cursor: pointer; +} + +.header .subtitleJar { + flex-shrink: 0; +} + +.header .subtitle { + flex-shrink: 1; + font-size: 0.9rem; + color: var(--bs-gray-600); +} + +.header .title { + width: 100%; + font-size: 1.2rem; + color: var(--bs-body-color); +} + +.header svg { + color: var(--bs-body-color); +} + +.header :global .accordion-button:after { + display: none; +} + +.buttons { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1rem; +} + +.buttons > button { + display: flex; + justify-content: center; + align-items: center; + height: 2.2rem; + font-size: 1rem; +} + +.successCheckmark { + width: '2rem'; + height: '2rem'; + background-color: 'rgba(39, 174, 96, 1)'; + color: 'white'; + border-radius: '50%'; +} diff --git a/src/components/fb/ExistingFidelityBond.jsx b/src/components/fb/ExistingFidelityBond.jsx new file mode 100644 index 000000000..917365e41 --- /dev/null +++ b/src/components/fb/ExistingFidelityBond.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import { useSettings } from '../../context/SettingsContext' +import Sprite from '../Sprite' +import Balance from '../Balance' +import { CopyButton } from '../CopyButton' +import styles from './ExistingFidelityBond.module.css' + +const ExistingFidelityBond = ({ utxo }) => { + const settings = useSettings() + + console.log(utxo) + return ( +
+
+
Fidelity Bond
+
+ + +
+
+
+ +
+
+ +
+
Locked until
+
{utxo.locktime}
+
+
+
+ } + successText={} + value={utxo.address} + className={styles.icon} + /> +
+
Timelocked address
+
+ {utxo.address} +
+
+
+
+
+
+ ) +} + +export { ExistingFidelityBond } diff --git a/src/components/fb/ExistingFidelityBond.module.css b/src/components/fb/ExistingFidelityBond.module.css new file mode 100644 index 000000000..2bb2fbda3 --- /dev/null +++ b/src/components/fb/ExistingFidelityBond.module.css @@ -0,0 +1,36 @@ +.container { + border: 1px solid var(--bs-gray-200); + border-radius: 0.3rem; + padding: 1.25rem; +} + +.title { + width: 100%; + font-size: 1.2rem; + color: var(--bs-body-color); +} + +.jar { + flex-shrink: 0; +} + +.jar > svg { + color: var(--bs-body-color); +} + +.label { + color: var(--bs-gray-600); + font-size: 0.8rem; +} + +.content { + font-size: 0.8rem; + word-break: break-all; +} + +.icon { + flex-shrink: 0; + padding: 0 !important; + width: 18px; + height: 18px; +} diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css new file mode 100644 index 000000000..ae8cf9d62 --- /dev/null +++ b/src/components/fb/FidelityBondSteps.module.css @@ -0,0 +1,192 @@ +.stepDescription { + font-size: 0.8rem; + color: var(--bs-gray-600); +} + +.utxoCard { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border: 2px solid var(--bs-gray-100); + border-radius: 0.5rem; +} + +.utxoCard.selectable { + cursor: pointer; +} + +.utxoCard:not(.selected):not(.selectable) { + color: var(--bs-gray-600); +} + +.utxoCard.selected { + background-color: rgb(45, 156, 219, 0.03); + border: 2px solid rgb(45, 156, 219, 1); +} + +.utxoCard.selected:not(.selectable) { + background-color: var(--bs-gray-100); + border: 2px solid var(--bs-gray-200); +} + +.utxoCard > .utxoSelectionMarker { + width: 1.25rem; + height: 1.25rem; + border-radius: 50%; + border: 2px solid var(--bs-gray-200); +} + +.utxoCard:not(.selected):not(.selectable) > .utxoSelectionMarker { + visibility: hidden; +} + +.utxoCard.selected > .utxoSelectionMarker { + border: 6px solid #2d9cdb; + background-color: white; +} + +.utxoCard.selected:not(.selectable) > .utxoSelectionMarker { + border: 6px solid var(--bs-gray-200); + background-color: white; +} + +.utxoCard > .utxoBody { + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.utxoCard > .utxoBody > .utxoAddress { + font-size: 0.8rem; + color: var(--bs-gray-600); + word-break: break-all; + margin-bottom: 0.1rem; +} + +.utxoCard > .utxoBody > .utxoDetails { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.2rem; + font-size: 0.6rem; + color: var(--bs-gray-600); +} + +.utxoCard > .utxoLabel { + display: flex; + justify-content: center; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + background-color: var(--bs-gray-100); + border-radius: 0.3rem; + color: var(--bs-gray-600); + min-width: 5.5rem; + font-size: 0.9rem; +} + +.utxoCard.selected:not(.selectable) > .utxoLabel { + background-color: var(--bs-gray-200); +} + +.utxoCard > .utxoLabel.utxoFrozen > svg { + color: #2d9cdb; + margin-bottom: 0.1rem; +} + +.utxoCard > .utxoLabel.utxoFidelityBond > svg { + color: #f7cf09; +} + +.utxoCard > .utxoLabel.utxoCjOut > svg { + color: #27ae60; +} + +.utxoCard > .utxoLoadingSpinner { + color: var(--bs-gray-600); + margin-right: 1rem; +} + +.fbIcon { + margin-top: -0.8rem; + flex-shrink: 0; +} + +.confirmationStepIcon { + flex-shrink: 0; + padding: 0 !important; + width: 18px; + height: 18px; +} + +.confirmationStepLabel { + color: var(--bs-gray-600); + font-size: 0.8rem; +} + +.confirmationStepContent { + font-size: 0.8rem; + word-break: break-all; +} + +.timelockedAddress { + text-align: left; + padding: 0; + font-size: 0.8rem; +} + +.utxoSummaryIcon { + margin-bottom: 3px; +} + +.utxoSummaryTitle { + font-size: 0.8rem; + color: var(--bs-gray-800); +} + +.utxoSummaryCard { + display: flex; + flex-direction: column; + flex-shrink: 0; + padding: 0.3rem 0.5rem; + border: 1px solid var(--bs-gray-200); + background-color: var(--bs-gray-100); + border-radius: 0.3rem; +} + +.utxoSummaryCardTitleContainer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.utxoSummaryCard > .utxoSummaryCardTitleContainer > .utxoSummaryCardTitle { + font-size: 0.8rem; +} + +.utxoSummaryCard > .utxoSummaryCardTitleContainer > .utxoSummaryCardTitleLabel { + display: flex; + justify-content: center; + align-items: center; + gap: 0.15rem; + border-radius: 0.1rem; + color: var(--bs-gray-600); + font-size: 0.55rem; +} + +.utxoSummaryCard > .utxoSummaryCardTitleContainer > .utxoSummaryCardTitleLabel.utxoFrozen > svg { + color: #2d9cdb; + margin-bottom: 0.1rem; +} + +.utxoSummaryCard > .utxoSummaryCardTitleContainer > .utxoSummaryCardTitleLabel.utxoCjOut > svg { + color: #27ae60; +} + +.utxoSummaryCard > .utxoSummaryCardSubtitle { + font-size: 0.55rem; + color: var(--bs-gray-600); + word-break: break-all; +} diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx new file mode 100644 index 000000000..a6854a04d --- /dev/null +++ b/src/components/fb/FidelityBondSteps.tsx @@ -0,0 +1,293 @@ +import React, { useMemo } from 'react' +import * as rb from 'react-bootstrap' +import classnamesBind from 'classnames/bind' +import * as Api from '../../libs/JmWalletApi' +import { useSettings } from '../../context/SettingsContext' +import { AccountBalances, AccountBalanceSummary } from '../../hooks/BalanceSummary' +import { Utxo } from '../../context/WalletContext' +import { calculateFillLevel, SelectableJar } from '../jars/Jar' +import Sprite from '../Sprite' +import Balance from '../Balance' +import { CopyButton } from '../CopyButton' +import LockdateForm from '../fidelity_bond/LockdateForm' +import * as fb from '../fidelity_bond/fb_utils' +import { utxosToFreeze, utxoIsInList } from './utils' +import styles from './FidelityBondSteps.module.css' + +const cx = classnamesBind.bind(styles) + +interface SelectDateProps { + selectableYearsRange: fb.YearsRange + onDateSelected: (lockdate: Api.Lockdate | null) => void +} + +interface SelectJarProps { + accountBalances: AccountBalances + totalBalance: number + utxos: { [accountIndex: number]: Array } + selectedJar: number | null + onJarSelected: (accountIndex: number) => void +} + +interface UtxoCardProps { + utxo: Utxo + isSelectable?: boolean + isSelected?: boolean + isLoading?: boolean + onClick?: () => void +} + +interface SelectUtxosProps { + jar: number + utxos: Array + selectedUtxos: Array + onUtxoSelected: (utxo: Utxo) => void + onUtxoDeselected: (utxo: Utxo) => void +} + +interface FreezeUtxosProps { + jar: number + utxos: Array + selectedUtxos: Array + isLoading?: boolean +} + +interface ReviewInputsProps { + lockDate: Api.Lockdate + jar: number + utxos: Array + selectedUtxos: Array + timelockedAddress: string +} + +const SelectDate = ({ selectableYearsRange, onDateSelected }: SelectDateProps) => { + return ( +
+
+ Fidelity bonds are a feature of JoinMarket which improves the resistance to sybil attacks, and therefore + improves the privacy of the system. This should be a better and easier to understand description and let the + user know that they should pick a date until which the fidelity bond will be valid. Maybe also a recommendation + on the length. +
+ onDateSelected(date)} yearsRange={selectableYearsRange} /> +
+ ) +} + +const SelectJar = ({ accountBalances, totalBalance, utxos, selectedJar, onJarSelected }: SelectJarProps) => { + const sortedAccountBalances: Array = useMemo(() => { + if (!accountBalances) return [] + return Object.values(accountBalances).sort((lhs, rhs) => lhs.accountIndex - rhs.accountIndex) + }, [accountBalances]) + + return ( +
+
Select a jar to fund the fidelity bond from.
+
+ {sortedAccountBalances.map((account, index) => ( + 0} + isSelected={selectedJar === account.accountIndex} + fillLevel={calculateFillLevel(Number.parseFloat(account.totalBalance), totalBalance)} + onClick={() => onJarSelected(account.accountIndex)} + /> + ))} +
+
+ ) +} + +const UtxoCard = ({ + utxo, + isSelectable = true, + isSelected = false, + isLoading = false, + onClick = () => {}, +}: UtxoCardProps) => { + const settings = useSettings() + + return ( +
isSelectable && onClick()} + > +
+
+ + {utxo.address} +
+
{utxo.path}
+
·
+
{utxo.confirmations} Confirmations
+
+
+ {isLoading && ( +
+
+ )} + {!isLoading && utxo.frozen && !utxo.locktime && ( +
+ +
frozen
+
+ )} + {!isLoading && utxo.locktime && ( +
+ +
locked
+
+ )} + {!isLoading && !utxo.frozen && utxo.label === 'cj-out' && ( +
+ +
cj-out
+
+ )} +
+ ) +} + +const SelectUtxos = ({ jar, utxos, selectedUtxos, onUtxoSelected, onUtxoDeselected }: SelectUtxosProps) => { + return ( +
+
+ Select one or more UTXOs from jar #{jar} to use for the fidelity bond. +
+ {utxos.map((utxo, index) => ( + { + if (utxoIsInList({ utxo, list: selectedUtxos })) { + onUtxoDeselected(utxo) + } else { + onUtxoSelected(utxo) + } + }} + /> + ))} +
+ ) +} + +const FreezeUtxos = ({ jar, utxos, selectedUtxos, isLoading = false }: FreezeUtxosProps) => { + return ( +
+
Selected UTXOs:
+ {selectedUtxos.map((utxo, index) => ( + + ))} +
+ The following UTXOs will not be used for the fidelity bond. They will be frozen in order to remain in jar #{jar} + . You can unfreeze them anytime after creating the fidelity bond. +
+ {utxosToFreeze({ allUtxos: utxos, selectedUtxosForFidelityBond: selectedUtxos }).map((utxo, index) => ( + + ))} +
+ ) +} + +const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress }: ReviewInputsProps) => { + const settings = useSettings() + + const UtxoSummary = ({ title, icon, utxos }: { title: string; icon: string; utxos: Array }) => { + const settings = useSettings() + + return ( +
+
+ +
{title}
+
+
+ {utxos.map((utxo, index) => ( +
+
+
+ +
+ {utxo.label === 'cj-out' && ( +
+ + {utxo.label} +
+ )} +
+
+ {utxo.address} +
+
+ ))} +
+
+ ) + } + + const confirmationItems = [ + { + icon: , + label: 'Locked until', + content: <>{new Date(lockDate).toUTCString()}, + }, + { + icon: , + label: 'Funds will be spend from', + content: `Jar #${jar}`, + }, + { + icon: , + label: 'Amount to be locked up', + content: ( + acc + utxo.value, 0).toString()} + convertToUnit={settings.unit} + showBalance={true} + /> + ), + }, + { + icon: ( + } + successText={} + value={timelockedAddress} + className={styles.confirmationStepIcon} + /> + ), + label: 'Funds will be lockd up on this address', + content: {timelockedAddress}, + }, + ] + + return ( +
+
You configured the fidelity bond as follows.
+
+ +
+ {confirmationItems.map((item, index) => ( +
+ {item.icon} +
+
{item.label}
+
{item.content}
+
+
+ ))} +
+
+
+ +
+ ) +} + +export { SelectJar, SelectUtxos, SelectDate, FreezeUtxos, ReviewInputs } diff --git a/src/components/fb/utils.ts b/src/components/fb/utils.ts new file mode 100644 index 000000000..7b6172ebd --- /dev/null +++ b/src/components/fb/utils.ts @@ -0,0 +1,18 @@ +import { Utxo } from '../../context/WalletContext' + +const isEqual = ({ lhs, rhs }: { lhs: Utxo; rhs: Utxo }) => lhs.utxo === rhs.utxo + +const utxoIsInList = ({ utxo, list }: { utxo: Utxo; list: Array }) => + list.findIndex((it) => isEqual({ lhs: it, rhs: utxo })) !== -1 + +const utxosToFreeze = ({ + allUtxos, + selectedUtxosForFidelityBond, +}: { + allUtxos: Array + selectedUtxosForFidelityBond: Array +}) => allUtxos.filter((utxo) => !utxoIsInList({ utxo, list: selectedUtxosForFidelityBond })) + +const allUtxosAreFrozen = ({ utxos }: { utxos: Array }) => utxos.every((utxo) => utxo.frozen) + +export { utxosToFreeze, utxoIsInList, allUtxosAreFrozen } diff --git a/src/components/fidelity_bond/LockdateForm.tsx b/src/components/fidelity_bond/LockdateForm.tsx index b8e37f58d..011a0ba15 100644 --- a/src/components/fidelity_bond/LockdateForm.tsx +++ b/src/components/fidelity_bond/LockdateForm.tsx @@ -106,7 +106,7 @@ const LockdateForm = ({ onChange, now, yearsRange }: LockdateFormProps) => { - + Year @@ -132,7 +132,7 @@ const LockdateForm = ({ onChange, now, yearsRange }: LockdateFormProps) => { - + Month diff --git a/src/components/jars/Jar.module.css b/src/components/jars/Jar.module.css index 1f14bbd42..efe117b65 100644 --- a/src/components/jars/Jar.module.css +++ b/src/components/jars/Jar.module.css @@ -28,6 +28,7 @@ .selectableJarContainer:not(.selectable) { color: var(--bs-gray-600); + cursor: unset; } .selectableJarContainer:not(.selectable) > .selectionCircle { diff --git a/src/hooks/BalanceSummary.ts b/src/hooks/BalanceSummary.ts index 15a1b99c1..29ff500a8 100644 --- a/src/hooks/BalanceSummary.ts +++ b/src/hooks/BalanceSummary.ts @@ -152,4 +152,4 @@ const useBalanceSummary = (currentWalletInfo: WalletInfo | null, now?: Milliseco return balanceSummary } -export { useBalanceSummary } +export { useBalanceSummary, AccountBalances, AccountBalanceSummary } From a8da8835e4374435dde2f9860c9885528b449dfa Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 5 Jul 2022 11:04:50 +0200 Subject: [PATCH 02/13] feat: error handling --- src/components/Alert.jsx | 7 +- src/components/fb/CreateFidelityBond.jsx | 85 ++++++++++++++++++------ 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/src/components/Alert.jsx b/src/components/Alert.jsx index ba4909a7f..52cc9f099 100644 --- a/src/components/Alert.jsx +++ b/src/components/Alert.jsx @@ -1,13 +1,16 @@ import React, { useState } from 'react' import { Alert as BsAlert } from 'react-bootstrap' -export default function Alert({ variant, dismissible, message, ...props }) { +export default function Alert({ variant, dismissible, message, onDismissed, ...props }) { const [show, setShow] = useState(true) return ( setShow(false)} + onClose={() => { + setShow(false) + onDismissed && onDismissed() + }} variant={variant} dismissible={dismissible} className="my-3" diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx index ebc30a857..caead31ca 100644 --- a/src/components/fb/CreateFidelityBond.jsx +++ b/src/components/fb/CreateFidelityBond.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react' import * as rb from 'react-bootstrap' import * as Api from '../../libs/JmWalletApi' import { useReloadCurrentWalletInfo } from '../../context/WalletContext' +import Alert from '../Alert' import Sprite from '../Sprite' import { ConfirmModal } from '../Modal' import { SelectJar, SelectUtxos, SelectDate, FreezeUtxos, ReviewInputs } from './FidelityBondSteps' @@ -18,6 +19,7 @@ const steps = { reviewInputs: 4, createFidelityBond: 5, done: 6, + failed: 7, } const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBalance, wallet, walletInfo }) => { @@ -25,6 +27,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal const [isExpanded, setIsExpanded] = useState(false) const [isLoading, setIsLoading] = useState(false) + const [alert, setAlert] = useState(null) const [step, setStep] = useState(steps.selectDate) const [showConfirmInputsModal, setShowConfirmInputsModal] = useState(false) @@ -49,6 +52,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal setSelectedUtxos([]) setLockDate(null) setTimelockedAddress(null) + setAlert(null) } const freezeUtxos = async (utxos) => { @@ -68,9 +72,11 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal await Promise.all(freezeCalls) .then((_) => reloadCurrentWalletInfo({ signal: abortCtrl.signal })) .catch((err) => { - console.error(err.message) + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + }) + .finally(() => { + setIsLoading(false) }) - .finally(() => setIsLoading(false)) } const loadTimeLockedAddress = async (lockDate) => { @@ -84,12 +90,12 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal signal: abortCtrl.signal, lockdate: lockDate, }) - .then((res) => - res.ok ? res.json() : Api.Helper.throwError(res, 'fidelity_bond.error_loading_timelock_address_failed') - ) + .then((res) => { + return res.ok ? res.json() : Api.Helper.throwError(res, 'fidelity_bond.error_loading_timelock_address_failed') + }) .then((data) => setTimelockedAddress(data.address)) .catch((err) => { - console.error(err.message) + setAlert({ variant: 'danger', message: err.message }) }) .finally(() => setIsLoading(false)) } @@ -97,7 +103,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal const directSweepToFidelityBond = async (jar, address) => { setIsLoading(true) - const res = await Api.postDirectSend( + await Api.postDirectSend( { walletName: wallet.name, token: wallet.token }, { mixdepth: jar, @@ -105,12 +111,15 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal amount_sats: 0, // sweep } ) - - if (!res.ok) { - await Api.Helper.throwError(res) - } - - setIsLoading(false) + .then((res) => { + if (!res.ok) { + return Api.Helper.throwError(res, 'Could not create fidelity bond.') + } + }) + .catch((err) => { + setAlert({ variant: 'danger', message: err.message }) + }) + .finally(() => setIsLoading(false)) } useEffect(() => { @@ -167,9 +176,15 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal /> ) case steps.reviewInputs: - return isLoading || timelockedAddress === null ? ( -
Loading...
- ) : ( + if (isLoading) { + return
Loading...
+ } + + if (timelockedAddress === null) { + return
Could not load time locked address.
+ } + + return ( ) + case steps.createFidelityBond: return isLoading ? (
@@ -186,8 +202,14 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal
) : (
- - Fidelity Bond Created! + {alert === null ? ( + <> + + Fidelity Bond Created!{' '} + + ) : ( + <>Couldn't create fidelity bond. + )}
) default: @@ -208,11 +230,13 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), }) ? 'Next' + : alert !== null + ? 'Try again' : 'Freeze UTXOs' case steps.reviewInputs: - return 'Create Fidelity Bond' + return timelockedAddress === null ? 'Try again' : 'Create Fidelity Bond' case steps.createFidelityBond: - return 'Close' + return alert === null ? 'Close' : 'Try Again' default: return null } @@ -254,13 +278,21 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal } if (currentStep === steps.reviewInputs) { + if (isLoading) { + return null + } + return steps.createFidelityBond } if (currentStep === steps.createFidelityBond) { - if (!isLoading) { + if (!isLoading && alert === null) { return steps.done } + + if (alert !== null) { + return steps.failed + } } return null @@ -284,6 +316,11 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal loadTimeLockedAddress(lockDate) } + if (step === steps.reviewInputs && timelockedAddress === null) { + loadTimeLockedAddress(lockDate) + return + } + if (nextStep(step) === steps.createFidelityBond) { setShowConfirmInputsModal(true) return @@ -293,11 +330,17 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal reset() } + if (nextStep(step) === steps.failed) { + reset() + return + } + setStep(nextStep(step)) } return (
+ {alert && setAlert(null)} />} Date: Tue, 5 Jul 2022 11:43:08 +0200 Subject: [PATCH 03/13] fix: dark mode --- public/sprite.svg | 32 +++++++++---------- .../fb/CreateFidelityBond.module.css | 4 +++ .../fb/ExistingFidelityBond.module.css | 4 +++ .../fb/FidelityBondSteps.module.css | 31 ++++++++++++++++++ 4 files changed, 55 insertions(+), 16 deletions(-) diff --git a/public/sprite.svg b/public/sprite.svg index 0eb0f694c..6c7bc0789 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -259,22 +259,22 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/src/components/fb/CreateFidelityBond.module.css b/src/components/fb/CreateFidelityBond.module.css index f25ea7874..8807a7fe5 100644 --- a/src/components/fb/CreateFidelityBond.module.css +++ b/src/components/fb/CreateFidelityBond.module.css @@ -4,6 +4,10 @@ padding: 1.25rem; } +:root[data-theme='dark'] .container { + border-color: var(--bs-gray-700); +} + .header { cursor: pointer; } diff --git a/src/components/fb/ExistingFidelityBond.module.css b/src/components/fb/ExistingFidelityBond.module.css index 2bb2fbda3..cbbcd4756 100644 --- a/src/components/fb/ExistingFidelityBond.module.css +++ b/src/components/fb/ExistingFidelityBond.module.css @@ -4,6 +4,10 @@ padding: 1.25rem; } +:root[data-theme='dark'] .container { + border-color: var(--bs-gray-700); +} + .title { width: 100%; font-size: 1.2rem; diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css index ae8cf9d62..392b796db 100644 --- a/src/components/fb/FidelityBondSteps.module.css +++ b/src/components/fb/FidelityBondSteps.module.css @@ -12,6 +12,10 @@ border-radius: 0.5rem; } +:root[data-theme='dark'] .utxoCard { + border-color: var(--bs-gray-700); +} + .utxoCard.selectable { cursor: pointer; } @@ -25,6 +29,11 @@ border: 2px solid rgb(45, 156, 219, 1); } +:root[data-theme='dark'] .utxoCard.selected { + background-color: var(--bs-gray-800); + border-color: var(--bs-gray-500); +} + .utxoCard.selected:not(.selectable) { background-color: var(--bs-gray-100); border: 2px solid var(--bs-gray-200); @@ -37,6 +46,10 @@ border: 2px solid var(--bs-gray-200); } +:root[data-theme='dark'] .utxoCard > .utxoSelectionMarker { + border-color: var(--bs-gray-500); +} + .utxoCard:not(.selected):not(.selectable) > .utxoSelectionMarker { visibility: hidden; } @@ -51,6 +64,10 @@ background-color: white; } +:root[data-theme='dark'] .utxoCard.selected:not(.selectable) > .utxoSelectionMarker { + border-color: var(--bs-gray-500); +} + .utxoCard > .utxoBody { display: flex; flex-direction: column; @@ -86,6 +103,11 @@ font-size: 0.9rem; } +:root[data-theme='dark'] .utxoCard > .utxoLabel { + background-color: var(--bs-gray-700); + color: var(--bs-gray-400); +} + .utxoCard.selected:not(.selectable) > .utxoLabel { background-color: var(--bs-gray-200); } @@ -145,6 +167,10 @@ color: var(--bs-gray-800); } +:root[data-theme='dark'] .utxoSummaryTitle { + color: var(--bs-gray-600); +} + .utxoSummaryCard { display: flex; flex-direction: column; @@ -155,6 +181,11 @@ border-radius: 0.3rem; } +:root[data-theme='dark'] .utxoSummaryCard { + border-color: var(--bs-gray-600); + background-color: var(--bs-gray-800); +} + .utxoSummaryCardTitleContainer { display: flex; align-items: center; From 25eba67a92cb66812faf98b940f36ac984d71b55 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 5 Jul 2022 13:57:08 +0200 Subject: [PATCH 04/13] fix: consolidate old code --- src/components/App.jsx | 2 - src/components/CurrentWalletAdvanced.tsx | 10 - src/components/FidelityBond.module.css | 4 - src/components/FidelityBond.tsx | 262 --------------- src/components/fb/CreateFidelityBond.jsx | 44 ++- src/components/fb/ExistingFidelityBond.jsx | 1 - src/components/fb/FidelityBondSteps.tsx | 11 +- .../LockdateForm.test.tsx | 2 +- .../{fidelity_bond => fb}/LockdateForm.tsx | 2 +- src/components/fb/utils.test.ts | 169 ++++++++++ src/components/fb/utils.ts | 101 +++++- .../fidelity_bond/AccountCheckbox.tsx | 30 -- .../fidelity_bond/AccountSelector.tsx | 91 ----- .../fidelity_bond/CheckboxCard.module.css | 26 -- src/components/fidelity_bond/CheckboxCard.tsx | 41 --- .../FidelityBondDetailsSetupForm.tsx | 318 ------------------ .../fidelity_bond/PercentageBar.module.css | 17 - .../fidelity_bond/PercentageBar.tsx | 18 - src/components/fidelity_bond/fb_utils.test.ts | 52 --- src/components/fidelity_bond/fb_utils.ts | 79 ----- 20 files changed, 286 insertions(+), 994 deletions(-) delete mode 100644 src/components/FidelityBond.module.css delete mode 100644 src/components/FidelityBond.tsx rename src/components/{fidelity_bond => fb}/LockdateForm.test.tsx (99%) rename src/components/{fidelity_bond => fb}/LockdateForm.tsx (99%) create mode 100644 src/components/fb/utils.test.ts delete mode 100644 src/components/fidelity_bond/AccountCheckbox.tsx delete mode 100644 src/components/fidelity_bond/AccountSelector.tsx delete mode 100644 src/components/fidelity_bond/CheckboxCard.module.css delete mode 100644 src/components/fidelity_bond/CheckboxCard.tsx delete mode 100644 src/components/fidelity_bond/FidelityBondDetailsSetupForm.tsx delete mode 100644 src/components/fidelity_bond/PercentageBar.module.css delete mode 100644 src/components/fidelity_bond/PercentageBar.tsx delete mode 100644 src/components/fidelity_bond/fb_utils.test.ts delete mode 100644 src/components/fidelity_bond/fb_utils.ts diff --git a/src/components/App.jsx b/src/components/App.jsx index 4877d19d8..01eded40c 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -10,7 +10,6 @@ import Earn from './Earn' import Receive from './Receive' import CurrentWalletMagic from './CurrentWalletMagic' import CurrentWalletAdvanced from './CurrentWalletAdvanced' -import FidelityBond from './FidelityBond' import Settings from './Settings' import Navbar from './Navbar' import Layout from './Layout' @@ -133,7 +132,6 @@ export default function App() { } /> } /> } /> - } /> )} diff --git a/src/components/CurrentWalletAdvanced.tsx b/src/components/CurrentWalletAdvanced.tsx index 8a2e3e051..2a9d5c9e1 100644 --- a/src/components/CurrentWalletAdvanced.tsx +++ b/src/components/CurrentWalletAdvanced.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react' -import { Link } from 'react-router-dom' import * as rb from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' // @ts-ignore @@ -10,7 +9,6 @@ import DisplayAccountUTXOs from './DisplayAccountUTXOs' import DisplayUTXOs from './DisplayUTXOs' // @ts-ignore import { useCurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo } from '../context/WalletContext' -import { routes } from '../constants/routes' import styles from './CurrentWalletAdvanced.module.css' type Utxos = any[] @@ -97,14 +95,6 @@ export default function CurrentWalletAdvanced() { No Fidelity Bond present. - <> - {' '} - - - Create a Fidelity Bond. - - - ) : ( diff --git a/src/components/FidelityBond.module.css b/src/components/FidelityBond.module.css deleted file mode 100644 index 273c836f3..000000000 --- a/src/components/FidelityBond.module.css +++ /dev/null @@ -1,4 +0,0 @@ -/* Firefox */ -.fidelity-bond input[type='number'] { - -moz-appearance: unset !important; -} diff --git a/src/components/FidelityBond.tsx b/src/components/FidelityBond.tsx deleted file mode 100644 index 523738893..000000000 --- a/src/components/FidelityBond.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import { Link } from 'react-router-dom' -import * as rb from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' - -import { useServiceInfo } from '../context/ServiceInfoContext' -import { useLoadConfigValue } from '../context/ServiceConfigContext' -import { useCurrentWallet, useCurrentWalletInfo, useReloadCurrentWalletInfo, Account } from '../context/WalletContext' - -import Sprite from './Sprite' -// @ts-ignore -import DisplayUTXOs from './DisplayUTXOs' -// @ts-ignore -import PageTitle from './PageTitle' - -import FidelityBondDetailsSetupForm from './fidelity_bond/FidelityBondDetailsSetupForm' -import * as Api from '../libs/JmWalletApi' -import { isLocked } from '../hooks/BalanceSummary' -import { routes } from '../constants/routes' -import styles from './FidelityBond.module.css' - -type AlertWithMessage = rb.AlertProps & { message: string } - -const collaborativeSweepToFidelityBond = async ( - requestContext: Api.WalletRequestContext, - account: Account, - timelockedDestinationAddress: Api.BitcoinAddress, - counterparties: number -): Promise => { - const res = await Api.postCoinjoin(requestContext, { - mixdepth: parseInt(account.account, 10), - destination: timelockedDestinationAddress, - counterparties, - amount_sats: 0, // sweep - }) - - if (!res.ok) { - await Api.Helper.throwError(res) - } -} - -const directSweepToFidelityBond = async ( - requestContext: Api.WalletRequestContext, - account: Account, - timelockedDestinationAddress: Api.BitcoinAddress -) => { - const res = await Api.postDirectSend(requestContext, { - mixdepth: parseInt(account.account, 10), - destination: timelockedDestinationAddress, - amount_sats: 0, // sweep - }) - - if (!res.ok) { - await Api.Helper.throwError(res) - } -} - -const MakerIsRunningAlert = () => { - const { t } = useTranslation() - return ( - - - - {t('fidelity_bond.text_maker_running')} - - - - - - - ) -} - -const TakerIsRunningAlert = () => { - const { t } = useTranslation() - return ( - - {t('fidelity_bond.text_coinjoin_already_running')} - - ) -} - -export default function FidelityBond() { - const { t } = useTranslation() - const currentWallet = useCurrentWallet() - const currentWalletInfo = useCurrentWalletInfo() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const serviceInfo = useServiceInfo() - const loadConfigValue = useLoadConfigValue() - - const isCoinjoinInProgress = useMemo(() => serviceInfo && serviceInfo.coinjoinInProgress, [serviceInfo]) - const isMakerRunning = useMemo(() => serviceInfo && serviceInfo.makerRunning, [serviceInfo]) - const isOperationDisabled = useMemo( - () => isMakerRunning || isCoinjoinInProgress, - [isMakerRunning, isCoinjoinInProgress] - ) - - const [alert, setAlert] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [isSending, setIsSending] = useState(false) - const [isInitiateTxSuccess, setIsInitiateTxSuccess] = useState(false) - const [initiateTxError, setInitiateTxError] = useState(undefined) - const isInitiateTxError = useMemo(() => initiateTxError !== undefined, [initiateTxError]) - - const utxos = useMemo(() => currentWalletInfo?.data.utxos.utxos, [currentWalletInfo]) - const fidelityBonds = useMemo(() => utxos?.filter((utxo) => utxo.locktime), [utxos]) - const activeFidelityBonds = useMemo(() => fidelityBonds?.filter((it) => isLocked(it)), [fidelityBonds]) - - useEffect(() => { - const abortCtrl = new AbortController() - - setAlert(null) - setIsLoading(true) - - reloadCurrentWalletInfo({ signal: abortCtrl.signal }) - .catch((err) => { - const message = err.message || t('current_wallet.error_loading_failed') - !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message }) - }) - .finally(() => { - !abortCtrl.signal.aborted && setIsLoading(false) - }) - - return () => abortCtrl.abort() - }, [currentWallet, reloadCurrentWalletInfo, t]) - - /** - * Initiate sending funds to a timelocked address. - * Defaults to sweep with a collaborative transaction. - * If the selected utxo is a single expired FB, "direct-send" is used. - * - * The transaction will have no change output. - */ - const onSubmit = async ( - selectedAccount: Account, - selectedLockdate: Api.Lockdate, - timelockedDestinationAddress: Api.BitcoinAddress - ) => { - if (isSending) return - if (!currentWallet) return - if (!utxos) return - - const abortCtrl = new AbortController() - const { name: walletName, token } = currentWallet - const requestContext = { walletName, token, signal: abortCtrl.signal } - - setIsSending(true) - try { - const accountIndex = parseInt(selectedAccount.account, 10) - const spendableUtxos = utxos - .filter((it) => it.mixdepth === accountIndex) - .filter((it) => !it.frozen) - .filter((it) => !isLocked(it)) - - // If the selected utxo is a single expired FB, send via "direct-send". - const useDirectSend = spendableUtxos.length === 1 && !!spendableUtxos[0].locktime - - if (useDirectSend) { - await directSweepToFidelityBond(requestContext, selectedAccount, timelockedDestinationAddress) - } else { - const minimumMakers = await loadConfigValue({ - signal: abortCtrl.signal, - key: { section: 'POLICY', field: 'minimum_makers' }, - }).then((data) => parseInt(data.value, 10)) - - // TODO: how many counterparties to use? is "minimum" for fbs okay? - await collaborativeSweepToFidelityBond( - requestContext, - selectedAccount, - timelockedDestinationAddress, - minimumMakers - ) - } - setIsInitiateTxSuccess(true) - } catch (error) { - setInitiateTxError(error) - throw error - } finally { - setIsSending(false) - } - } - - return ( -
- - -
- - - See the documentation about Fidelity Bonds - {' '} - for more information. - -
- - {alert && {alert.message}} - -
- {isLoading ? ( -
-
- ) : ( - <> - {currentWallet && currentWalletInfo && serviceInfo && activeFidelityBonds && ( - <> - {isInitiateTxSuccess || isInitiateTxError ? ( - <> - {isInitiateTxSuccess ? ( - <> - - The transaction to create your Fidelity Bond has been successfully initiated. - - {isCoinjoinInProgress && } - - ) : ( - - Error while initiating your Fidelity Bond transaction. - - )} - - ) : ( - <> - {isOperationDisabled ? ( - - <> - {isMakerRunning && } - {isCoinjoinInProgress && } - - - ) : ( - <> - {activeFidelityBonds.length === 0 ? ( - - ) : ( -
-
{t('current_wallet_advanced.title_fidelity_bonds')}
- -
- )} - - )} - - )} - - )} - - )} -
-
- ) -} diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx index caead31ca..01cc36271 100644 --- a/src/components/fb/CreateFidelityBond.jsx +++ b/src/components/fb/CreateFidelityBond.jsx @@ -6,8 +6,7 @@ import Alert from '../Alert' import Sprite from '../Sprite' import { ConfirmModal } from '../Modal' import { SelectJar, SelectUtxos, SelectDate, FreezeUtxos, ReviewInputs } from './FidelityBondSteps' -import { utxosToFreeze, allUtxosAreFrozen } from './utils' -import { toYearsRange, DEFAULT_MAX_TIMELOCK_YEARS } from '../fidelity_bond/fb_utils' +import * as fb from './utils' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import styles from './CreateFidelityBond.module.css' @@ -40,9 +39,9 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal const yearsRange = useMemo(() => { if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { - return toYearsRange(-1, DEFAULT_MAX_TIMELOCK_YEARS) + return fb.toYearsRange(-1, fb.DEFAULT_MAX_TIMELOCK_YEARS) } - return toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS) + return fb.toYearsRange(0, fb.DEFAULT_MAX_TIMELOCK_YEARS) }, []) const reset = () => { @@ -226,13 +225,15 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal case steps.selectUtxos: return 'Next' case steps.freezeUtxos: - return allUtxosAreFrozen({ - utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), - }) - ? 'Next' - : alert !== null - ? 'Try again' - : 'Freeze UTXOs' + const utxosAreFrozen = fb.utxo.allAreFrozen(fb.utxo.utxosToFreeze(utxos[selectedJar], selectedUtxos)) + + if (utxosAreFrozen) { + return 'Next' + } else if (alert !== null) { + return 'Try Again' + } + + return 'Freeze UTXOs' case steps.reviewInputs: return timelockedAddress === null ? 'Try again' : 'Create Fidelity Bond' case steps.createFidelityBond: @@ -266,11 +267,8 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal return null } - if ( - allUtxosAreFrozen({ - utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), - }) - ) { + const utxosAreFrozen = fb.utxo.allAreFrozen(fb.utxo.utxosToFreeze(utxos[selectedJar], selectedUtxos)) + if (utxosAreFrozen) { return steps.reviewInputs } @@ -303,13 +301,13 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal return } - if ( - step === steps.freezeUtxos && - !allUtxosAreFrozen({ - utxos: utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos }), - }) - ) { - freezeUtxos(utxosToFreeze({ allUtxos: utxos[selectedJar], selectedUtxosForFidelityBond: selectedUtxos })) + if (step === steps.freezeUtxos) { + const utxosToFreeze = fb.utxo.utxosToFreeze(utxos[selectedJar], selectedUtxos) + const utxosAreFrozen = fb.utxo.allAreFrozen(utxosToFreeze) + + if (!utxosAreFrozen) { + freezeUtxos(utxosToFreeze) + } } if (nextStep(step) === steps.reviewInputs) { diff --git a/src/components/fb/ExistingFidelityBond.jsx b/src/components/fb/ExistingFidelityBond.jsx index 917365e41..69dd371e3 100644 --- a/src/components/fb/ExistingFidelityBond.jsx +++ b/src/components/fb/ExistingFidelityBond.jsx @@ -8,7 +8,6 @@ import styles from './ExistingFidelityBond.module.css' const ExistingFidelityBond = ({ utxo }) => { const settings = useSettings() - console.log(utxo) return (
diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index a6854a04d..28bf69be6 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -9,9 +9,8 @@ import { calculateFillLevel, SelectableJar } from '../jars/Jar' import Sprite from '../Sprite' import Balance from '../Balance' import { CopyButton } from '../CopyButton' -import LockdateForm from '../fidelity_bond/LockdateForm' -import * as fb from '../fidelity_bond/fb_utils' -import { utxosToFreeze, utxoIsInList } from './utils' +import LockdateForm from './LockdateForm' +import * as fb from './utils' import styles from './FidelityBondSteps.module.css' const cx = classnamesBind.bind(styles) @@ -162,9 +161,9 @@ const SelectUtxos = ({ jar, utxos, selectedUtxos, onUtxoSelected, onUtxoDeselect key={index} utxo={utxo} isSelectable={!utxo.frozen} - isSelected={utxoIsInList({ utxo, list: selectedUtxos })} + isSelected={fb.utxo.isInList(utxo, selectedUtxos)} onClick={() => { - if (utxoIsInList({ utxo, list: selectedUtxos })) { + if (fb.utxo.isInList(utxo, selectedUtxos)) { onUtxoDeselected(utxo) } else { onUtxoSelected(utxo) @@ -187,7 +186,7 @@ const FreezeUtxos = ({ jar, utxos, selectedUtxos, isLoading = false }: FreezeUtx The following UTXOs will not be used for the fidelity bond. They will be frozen in order to remain in jar #{jar} . You can unfreeze them anytime after creating the fidelity bond.
- {utxosToFreeze({ allUtxos: utxos, selectedUtxosForFidelityBond: selectedUtxos }).map((utxo, index) => ( + {fb.utxo.utxosToFreeze(utxos, selectedUtxos).map((utxo, index) => ( ))}
diff --git a/src/components/fidelity_bond/LockdateForm.test.tsx b/src/components/fb/LockdateForm.test.tsx similarity index 99% rename from src/components/fidelity_bond/LockdateForm.test.tsx rename to src/components/fb/LockdateForm.test.tsx index 8d320cd28..84a1f19a1 100644 --- a/src/components/fidelity_bond/LockdateForm.test.tsx +++ b/src/components/fb/LockdateForm.test.tsx @@ -3,7 +3,7 @@ import { act } from 'react-dom/test-utils' import user from '@testing-library/user-event' import { render, screen } from '../../testUtils' import * as Api from '../../libs/JmWalletApi' -import * as fb from './fb_utils' +import * as fb from './utils' import LockdateForm, { _minMonth, _selectableMonths, _selectableYears } from './LockdateForm' diff --git a/src/components/fidelity_bond/LockdateForm.tsx b/src/components/fb/LockdateForm.tsx similarity index 99% rename from src/components/fidelity_bond/LockdateForm.tsx rename to src/components/fb/LockdateForm.tsx index 011a0ba15..221e9b9a7 100644 --- a/src/components/fidelity_bond/LockdateForm.tsx +++ b/src/components/fb/LockdateForm.tsx @@ -3,7 +3,7 @@ import * as rb from 'react-bootstrap' import { Trans, useTranslation } from 'react-i18next' import * as Api from '../../libs/JmWalletApi' -import * as fb from './fb_utils' +import * as fb from './utils' const monthFormatter = (locales: string) => new Intl.DateTimeFormat(locales, { month: 'long' }) diff --git a/src/components/fb/utils.test.ts b/src/components/fb/utils.test.ts new file mode 100644 index 000000000..69fa81257 --- /dev/null +++ b/src/components/fb/utils.test.ts @@ -0,0 +1,169 @@ +import * as fb from './utils' + +import { Lockdate } from '../../libs/JmWalletApi' +import { Utxo } from '../../context/WalletContext' + +const makeUtxo = (id: string, address = '', frozen = false) => + ({ + address: address, + path: '', + label: '', + value: 0, + tries: 0, + tries_remaining: 0, + external: false, + mixdepth: 0, + confirmations: 0, + frozen: frozen, + utxo: id, + } as Utxo) + +describe('utils', () => { + describe('lockdate', () => { + it('should convert timestamp to lockdate', () => { + expect(fb.lockdate.fromTimestamp(-1)).toBe('1969-12') + expect(fb.lockdate.fromTimestamp(0)).toBe('1970-01') + expect(fb.lockdate.fromTimestamp(Date.UTC(2008, 9, 31))).toBe('2008-10') + expect(fb.lockdate.fromTimestamp(Date.UTC(2009, 0, 3, 1, 42, 13, 37))).toBe('2009-01') + expect(fb.lockdate.fromTimestamp(Date.UTC(2022, 1, 18))).toBe('2022-02') + expect(fb.lockdate.fromTimestamp(Date.UTC(2999, 11))).toBe('2999-12') + expect(fb.lockdate.fromTimestamp(Date.UTC(10_000, 10))).toBe('10000-11') + + expect(() => fb.lockdate.fromTimestamp(NaN)).toThrowError('Unsupported input: NaN') + }) + + it('should convert lockdate to timestamp', () => { + expect(fb.lockdate.toTimestamp('2008-10')).toBe(Date.UTC(2008, 9, 1)) + expect(fb.lockdate.toTimestamp('2009-01')).toBe(Date.UTC(2009, 0, 1)) + expect(fb.lockdate.toTimestamp('2999-12')).toBe(Date.UTC(2999, 11)) + + expect(() => fb.lockdate.toTimestamp('-1' as Lockdate)).toThrowError('Unsupported format') + expect(() => fb.lockdate.toTimestamp('2008-1' as Lockdate)).toThrowError('Unsupported format') + expect(() => fb.lockdate.toTimestamp('10000-01' as Lockdate)).toThrowError('Unsupported format') + expect(() => fb.lockdate.toTimestamp('' as Lockdate)).toThrowError('Unsupported format') + expect(() => fb.lockdate.toTimestamp('any' as Lockdate)).toThrowError('Unsupported format') + }) + + it('should create an initial lockdate', () => { + const rangeZero = fb.toYearsRange(0, 10) + const rangeMinusOneYear = fb.toYearsRange(-1, 10) + const rangePlusOneYear = fb.toYearsRange(1, 10) + + // verify the default "months ahead" for the initial lockdate to prevent unintentional changes + expect(fb.__INITIAL_LOCKDATE_MONTH_AHEAD).toBe(3) + + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangeZero)).toBe('2009-05') + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangeMinusOneYear)).toBe('2009-05') + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangePlusOneYear)).toBe('2010-01') + + expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangeZero)).toBe('2009-02') + expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangeMinusOneYear)).toBe('2009-02') + expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangePlusOneYear)).toBe('2009-10') + + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangeZero)).toBe('2010-04') + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangeMinusOneYear)).toBe('2010-04') + expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangePlusOneYear)).toBe('2010-12') + }) + }) + describe('utxo', () => { + it('should use utxo ids for equality checks', () => { + expect(fb.utxo.isEqual(makeUtxo('foo:0'), makeUtxo('foo:0'))).toBe(true) + expect(fb.utxo.isEqual(makeUtxo('foo:0', 'abc'), makeUtxo('foo:0', 'xyz'))).toBe(true) + expect(fb.utxo.isEqual(makeUtxo('foo:0'), makeUtxo('foo:1'))).toBe(false) + }) + + it('should determine if a utxo is in a list', () => { + expect( + fb.utxo.isInList(makeUtxo('foo:3'), [ + makeUtxo('foo:0'), + makeUtxo('foo:1'), + makeUtxo('foo:2'), + makeUtxo('foo:3'), + makeUtxo('foo:4'), + ]) + ).toBe(true) + + expect( + fb.utxo.isInList(makeUtxo('foo:0'), [ + makeUtxo('foo:0'), + makeUtxo('foo:1'), + makeUtxo('foo:2'), + makeUtxo('foo:3'), + makeUtxo('foo:4'), + ]) + ).toBe(true) + + expect( + fb.utxo.isInList(makeUtxo('foo:4'), [ + makeUtxo('foo:0'), + makeUtxo('foo:1'), + makeUtxo('foo:2'), + makeUtxo('foo:3'), + makeUtxo('foo:4'), + ]) + ).toBe(true) + + expect( + fb.utxo.isInList(makeUtxo('foo:5'), [ + makeUtxo('foo:0'), + makeUtxo('foo:1'), + makeUtxo('foo:2'), + makeUtxo('foo:3'), + makeUtxo('foo:4'), + ]) + ).toBe(false) + }) + + it('should determine which utxos to freeze', () => { + const allUtxos = [makeUtxo('foo:0'), makeUtxo('foo:1'), makeUtxo('foo:2'), makeUtxo('foo:3'), makeUtxo('foo:4')] + + let utxosToFreeze = fb.utxo.utxosToFreeze(allUtxos, [makeUtxo('foo:1'), makeUtxo('foo:3')]) + expect(fb.utxo.isInList(makeUtxo('foo:0'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:1'), utxosToFreeze)).toBe(false) + expect(fb.utxo.isInList(makeUtxo('foo:2'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:3'), utxosToFreeze)).toBe(false) + expect(fb.utxo.isInList(makeUtxo('foo:4'), utxosToFreeze)).toBe(true) + + utxosToFreeze = fb.utxo.utxosToFreeze(allUtxos, [makeUtxo('foo:0'), makeUtxo('foo:4')]) + expect(fb.utxo.isInList(makeUtxo('foo:0'), utxosToFreeze)).toBe(false) + expect(fb.utxo.isInList(makeUtxo('foo:1'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:2'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:3'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:4'), utxosToFreeze)).toBe(false) + + utxosToFreeze = fb.utxo.utxosToFreeze(allUtxos, [makeUtxo('foo:2')]) + expect(fb.utxo.isInList(makeUtxo('foo:0'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:1'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:2'), utxosToFreeze)).toBe(false) + expect(fb.utxo.isInList(makeUtxo('foo:3'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:4'), utxosToFreeze)).toBe(true) + + utxosToFreeze = fb.utxo.utxosToFreeze(allUtxos, []) + expect(fb.utxo.isInList(makeUtxo('foo:0'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:1'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:2'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:3'), utxosToFreeze)).toBe(true) + expect(fb.utxo.isInList(makeUtxo('foo:4'), utxosToFreeze)).toBe(true) + }) + + it('should check wheter all utxos are frozen', () => { + let utxos = [ + makeUtxo('foo:0', '', true), + makeUtxo('foo:1', '', true), + makeUtxo('foo:2', '', true), + makeUtxo('foo:3', '', true), + makeUtxo('foo:4', '', true), + ] + expect(fb.utxo.allAreFrozen(utxos)).toBe(true) + + utxos = [ + makeUtxo('foo:0', '', true), + makeUtxo('foo:1', '', true), + makeUtxo('foo:2', '', false), + makeUtxo('foo:3', '', true), + makeUtxo('foo:4', '', true), + ] + expect(fb.utxo.allAreFrozen(utxos)).toBe(false) + }) + }) +}) diff --git a/src/components/fb/utils.ts b/src/components/fb/utils.ts index 7b6172ebd..8e7d7c537 100644 --- a/src/components/fb/utils.ts +++ b/src/components/fb/utils.ts @@ -1,18 +1,95 @@ +import { Lockdate } from '../../libs/JmWalletApi' import { Utxo } from '../../context/WalletContext' -const isEqual = ({ lhs, rhs }: { lhs: Utxo; rhs: Utxo }) => lhs.utxo === rhs.utxo +type Milliseconds = number -const utxoIsInList = ({ utxo, list }: { utxo: Utxo; list: Array }) => - list.findIndex((it) => isEqual({ lhs: it, rhs: utxo })) !== -1 +export type YearsRange = { + min: number + max: number +} -const utxosToFreeze = ({ - allUtxos, - selectedUtxosForFidelityBond, -}: { - allUtxos: Array - selectedUtxosForFidelityBond: Array -}) => allUtxos.filter((utxo) => !utxoIsInList({ utxo, list: selectedUtxosForFidelityBond })) +export const toYearsRange = (min: number, max: number): YearsRange => { + if (max <= min) { + throw new Error('Invalid values for range of years.') + } + return { min, max } +} -const allUtxosAreFrozen = ({ utxos }: { utxos: Array }) => utxos.every((utxo) => utxo.frozen) +// A maximum of years for a timelock to be accepted. +// This is useful in simple mode - when it should be prevented that users +// lock up their coins for an awful amount of time by accident. +// In "advanced" mode, this can be dropped or increased substantially. +export const DEFAULT_MAX_TIMELOCK_YEARS = 10 +export const DEFAULT_TIMELOCK_YEARS_RANGE = toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS) -export { utxosToFreeze, utxoIsInList, allUtxosAreFrozen } +// The months ahead for the initial lock date. +// It is recommended to start locking for a period of between 3 months and 1 years initially. +// This value should be at the lower end of this recommendation. +// See https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/fidelity-bonds.md#what-amount-of-bitcoins-to-lock-up-and-for-how-long +// for more information (last checked on 2022-06-13). +// Exported for tests only! +export const __INITIAL_LOCKDATE_MONTH_AHEAD = 3 + +export const lockdate = (() => { + const _fromDate = (date: Date): Lockdate => { + return `${date.getUTCFullYear()}-${date.getUTCMonth() >= 9 ? '' : '0'}${1 + date.getUTCMonth()}` as Lockdate + } + const fromTimestamp = (timestamp: Milliseconds): Lockdate => { + if (Number.isNaN(timestamp)) throw new Error('Unsupported input: NaN') + return _fromDate(new Date(timestamp)) + } + const toTimestamp = (lockdate: Lockdate): Milliseconds => { + const split = lockdate.split('-') + if (split.length !== 2 || split[0].length !== 4 || split[1].length !== 2) { + throw new Error('Unsupported format') + } + + const year = parseInt(split[0], 10) + const month = parseInt(split[1], 10) + if (Number.isNaN(year) || Number.isNaN(month)) { + throw new Error('Unsupported format') + } + return Date.UTC(year, month - 1, 1) + } + + /** + * Returns a lockdate an initial lockdate in the future. + * + * This method tries to provide a date that is at least + * {@link __INITIAL_LOCKDATE_MONTH_AHEAD} months after {@link now}. + * + * @param now the reference date + * @param range a min/max range of years + * @returns an initial lockdate + */ + const initial = (now: Date, range: YearsRange = DEFAULT_TIMELOCK_YEARS_RANGE): Lockdate => { + const year = now.getUTCFullYear() + const month = now.getUTCMonth() + + const minMonthAhead = Math.max(range.min * 12, __INITIAL_LOCKDATE_MONTH_AHEAD + 1) + const initYear = year + Math.floor((month + minMonthAhead) / 12) + const initMonth = (month + minMonthAhead) % 12 + return fromTimestamp(Date.UTC(initYear, initMonth, 1)) + } + + return { + fromTimestamp, + toTimestamp, + initial, + } +})() + +export const utxo = (() => { + const isEqual = (lhs: Utxo, rhs: Utxo) => { + return lhs.utxo === rhs.utxo + } + + const isInList = (utxo: Utxo, list: Array) => list.findIndex((it) => isEqual(it, utxo)) !== -1 + + const utxosToFreeze = (allUtxos: Array, fbUtxos: Array) => + allUtxos.filter((utxo) => !isInList(utxo, fbUtxos)) + + const allAreFrozen = (utxos: Array) => utxos.every((utxo) => utxo.frozen) + + return { isEqual, isInList, utxosToFreeze, allAreFrozen } +})() diff --git a/src/components/fidelity_bond/AccountCheckbox.tsx b/src/components/fidelity_bond/AccountCheckbox.tsx deleted file mode 100644 index 7e44ff62c..000000000 --- a/src/components/fidelity_bond/AccountCheckbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import React, { PropsWithChildren, useCallback } from 'react' -import { Account } from '../../context/WalletContext' -import CheckboxCard, { CheckboxCardProps } from './CheckboxCard' - -interface AccountCheckboxProps extends CheckboxCardProps { - account: Account - onAccountSelected: (account: Account, checked: boolean) => void -} - -const AccountCheckbox = ({ - account, - onAccountSelected, - children, - ...props -}: PropsWithChildren) => { - const onChange = useCallback( - (e: React.ChangeEvent) => { - return onAccountSelected(account, e.target.checked) - }, - [account, onAccountSelected] - ) - - return ( - - {children} - - ) -} - -export default AccountCheckbox diff --git a/src/components/fidelity_bond/AccountSelector.tsx b/src/components/fidelity_bond/AccountSelector.tsx deleted file mode 100644 index 7f882f7f7..000000000 --- a/src/components/fidelity_bond/AccountSelector.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { useEffect, useState, useMemo } from 'react' -import * as rb from 'react-bootstrap' - -import { Account } from '../../context/WalletContext' -// @ts-ignore -import { useSettings } from '../../context/SettingsContext' - -// @ts-ignore -import Balance from '../Balance' -import PercentageBar from './PercentageBar' -import AccountCheckbox from './AccountCheckbox' -import { WalletBalanceSummary } from '../../hooks/BalanceSummary' - -type SelectableAccount = Account & { disabled?: boolean } - -interface AccountSelectorProps { - balanceSummary: WalletBalanceSummary - accounts: SelectableAccount[] - onChange: (selectedAccount: Account | null) => void -} -const AccountSelector = ({ balanceSummary, accounts, onChange }: AccountSelectorProps) => { - const settings = useSettings() - const [selected, setSelected] = useState(null) - - const selectableAccounts = useMemo(() => { - return accounts.filter((it) => !it.disabled) - }, [accounts]) - - useEffect(() => { - setSelected(null) - }, [selectableAccounts]) - - useEffect(() => { - onChange(selected) - }, [selected, onChange]) - - return ( - - {accounts.map((it) => { - const accountIndex = parseInt(it.account, 10) - const availableAccountBalance = balanceSummary.accountBalances[accountIndex].calculatedAvailableBalanceInSats - - const percentageOfTotal = - balanceSummary.calculatedTotalBalanceInSats > 0 - ? (100 * availableAccountBalance) / balanceSummary.calculatedTotalBalanceInSats - : undefined - return ( - - { - setSelected((current) => { - if (current === account) { - return null - } - return account - }) - }} - > - {' '} - <> - {percentageOfTotal !== undefined && ( - - )} - -
Jar #{it.account}
-
- -
- {percentageOfTotal !== undefined && ( -
- {`${percentageOfTotal.toFixed(2)}%`} -
- )} -
- -
-
- ) - })} -
- ) -} - -export default AccountSelector diff --git a/src/components/fidelity_bond/CheckboxCard.module.css b/src/components/fidelity_bond/CheckboxCard.module.css deleted file mode 100644 index cdc709d8f..000000000 --- a/src/components/fidelity_bond/CheckboxCard.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.checkbox-card { - width: 100%; - position: relative; - cursor: pointer; -} - -.checkbox-card.disabled { - cursor: unset; -} - -.sprite-container { - display: flex; - justify-content: center; - align-items: center; - margin: 1rem; - width: 3rem; - height: 3rem; - border-radius: 50%; - background-color: rgba(222, 222, 222, 1); - color: rgba(66, 66, 66, 1); -} - -.checked .sprite-container { - background-color: rgba(39, 174, 96, 1); - color: var(--bs-white); -} diff --git a/src/components/fidelity_bond/CheckboxCard.tsx b/src/components/fidelity_bond/CheckboxCard.tsx deleted file mode 100644 index 89d5c6688..000000000 --- a/src/components/fidelity_bond/CheckboxCard.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { useRef, PropsWithChildren } from 'react' -import * as rb from 'react-bootstrap' - -import Sprite from '../Sprite' -import styles from './CheckboxCard.module.css' - -export interface CheckboxCardProps extends rb.FormCheckProps {} - -const CheckboxCard = ({ children, checked, disabled, ...props }: PropsWithChildren) => { - const ref = useRef(null) - return ( - <> - { - e.preventDefault() - e.stopPropagation() - - !disabled && ref.current?.click() - }} - onKeyDown={(e: React.KeyboardEvent) => { - e.key === ' ' && ref.current?.click() - }} - > - -
-
- {checked && } - {!checked && disabled && } -
- <>{children} -
-
- - ) -} - -export default CheckboxCard diff --git a/src/components/fidelity_bond/FidelityBondDetailsSetupForm.tsx b/src/components/fidelity_bond/FidelityBondDetailsSetupForm.tsx deleted file mode 100644 index d395685a8..000000000 --- a/src/components/fidelity_bond/FidelityBondDetailsSetupForm.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import React, { useEffect, useMemo, useState } from 'react' -import * as rb from 'react-bootstrap' -import { useTranslation } from 'react-i18next' - -import { WalletInfo, CurrentWallet, Account } from '../../context/WalletContext' -// @ts-ignore -import { useSettings } from '../../context/SettingsContext' - -// @ts-ignore -import Balance from '../../components/Balance' -// @ts-ignore -import ToggleSwitch from '../../components/ToggleSwitch' -import { useBalanceSummary, WalletBalanceSummary } from '../../hooks/BalanceSummary' - -import AccountSelector from './AccountSelector' -import LockdateForm from './LockdateForm' -import { toYearsRange, DEFAULT_MAX_TIMELOCK_YEARS } from './fb_utils' - -import * as Api from '../../libs/JmWalletApi' -import { isDebugFeatureEnabled } from '../../constants/debugFeatures' - -type AlertWithMessage = rb.AlertProps & { message: string } - -interface SelectAccountStepProps { - balanceSummary: WalletBalanceSummary - accounts: Account[] - onChange: (account: Account | null) => void -} - -const SelectAccountStep = ({ balanceSummary, accounts, onChange }: SelectAccountStepProps) => { - const [selectedAccount, setSelectedAccount] = useState(null) - - useEffect(() => { - onChange(selectedAccount) - }, [selectedAccount, onChange]) - - return ( - <> -

Select Account

- - - ) -} - -interface SelectLockdateStepProps { - onChange: (lockdate: Api.Lockdate | null) => void -} -const SelectLockdateStep = ({ onChange }: SelectLockdateStepProps) => { - const yearsRange = useMemo(() => { - if (isDebugFeatureEnabled('allowCreatingExpiredFidelityBond')) { - return toYearsRange(-1, DEFAULT_MAX_TIMELOCK_YEARS) - } - return toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS) - }, []) - - return ( - <> -

Select duration

- - - - - - - - - ) -} - -interface ConfirmationStepProps { - balanceSummary: WalletBalanceSummary - account: Account - lockdate: Api.Lockdate - destinationAddress: Api.BitcoinAddress - confirmed: boolean - onChange: (confirmed: boolean) => void -} - -const ConfirmationStep = ({ - balanceSummary, - account, - lockdate, - destinationAddress, - confirmed, - onChange, -}: ConfirmationStepProps) => { - const { t } = useTranslation() - const settings = useSettings() - - const accountAvailableBalanceInSats = useMemo( - () => balanceSummary.accountBalances[parseInt(account.account, 10)].calculatedAvailableBalanceInSats, - [balanceSummary, account] - ) - - const relativeSizeToTotalBalance = useMemo(() => { - if (balanceSummary.calculatedTotalBalanceInSats <= 0) return 0 - return accountAvailableBalanceInSats / balanceSummary.calculatedTotalBalanceInSats - }, [accountAvailableBalanceInSats, balanceSummary]) - - return ( - <> -

Confirmation

-

Please review the summary of your inputs carefully.

- - - - - - - Account - #{account.account} - - - - Fidelity Bond Size -
Excluding transaction fees
- - - - - - - Relative size to your total balance - {(relativeSizeToTotalBalance * 100).toFixed(2)} % - - - Locked until - {lockdate} - - - - Timelocked Destination Address -
{destinationAddress}
- - - -
-
-
- -
- onChange(isToggled)} - /> -
- - ) -} - -interface FidelityBondDetailsSetupFormProps { - currentWallet: CurrentWallet - walletInfo: WalletInfo - onSubmit: (account: Account, lockdate: Api.Lockdate, timelockedAddress: Api.BitcoinAddress) => Promise -} - -const FidelityBondDetailsSetupForm = ({ currentWallet, walletInfo, onSubmit }: FidelityBondDetailsSetupFormProps) => { - const { t } = useTranslation() - const balanceSummary = useBalanceSummary(walletInfo) - const accounts = useMemo(() => walletInfo.data.display.walletinfo.accounts, [walletInfo]) - - const [alert, setAlert] = useState(null) - const [step, setStep] = useState(0) - const [selectedAccount, setSelectedAccount] = useState(null) - const [selectedLockdate, setSelectedLockdate] = useState(null) - const [timelockedDestinationAddress, setTimelockedDestinationAddress] = useState(null) - const [userConfirmed, setUserConfirmed] = useState(false) - - useEffect(() => { - if (selectedAccount === null) { - setStep(0) - } - }, [selectedAccount]) - - useEffect(() => { - // TODO: toggle button has no way to reflect this change currently - setUserConfirmed(false) - }, [step, selectedAccount, selectedLockdate]) - - useEffect(() => { - if (!selectedLockdate) return - - setAlert(null) - setTimelockedDestinationAddress(null) - - const abortCtrl = new AbortController() - Api.getAddressTimelockNew({ - walletName: currentWallet.name, - token: currentWallet.token, - signal: abortCtrl.signal, - lockdate: selectedLockdate, - }) - .then((res) => - res.ok ? res.json() : Api.Helper.throwError(res, t('fidelity_bond.error_loading_timelock_address_failed')) - ) - .then((data) => setTimelockedDestinationAddress(data.address)) - .catch((err) => { - const message = err.message || t('fidelity_bond.error_loading_timelock_address_failed') - setAlert({ variant: 'danger', message }) - }) - - return () => abortCtrl.abort() - }, [currentWallet, selectedLockdate, t]) - - return ( -
-
-

Step {step + 1}

- {step > 0 && ( - setStep(step - 1)} - > - {t('global.back')} - - )} -
- - {balanceSummary && ( -
- - - setStep(1)} - > - {t('global.next')} - -
- )} - - {balanceSummary && selectedAccount && ( -
- - - setStep(2)} - > - {t('global.next')} - - - setStep(0)}> - {t('global.back')} - -
- )} - - {balanceSummary && selectedAccount && selectedLockdate && ( -
- {alert ? ( - {alert.message} - ) : ( - <> - {timelockedDestinationAddress === null ? ( -
-
- ) : ( - - )} - - )} - - - userConfirmed && - timelockedDestinationAddress && - onSubmit(selectedAccount, selectedLockdate, timelockedDestinationAddress) - } - > - {t('fidelity_bond.create_form.button_create')} - - - setStep(1)}> - {t('global.back')} - -
- )} -
- ) -} - -export default FidelityBondDetailsSetupForm diff --git a/src/components/fidelity_bond/PercentageBar.module.css b/src/components/fidelity_bond/PercentageBar.module.css deleted file mode 100644 index 9da599626..000000000 --- a/src/components/fidelity_bond/PercentageBar.module.css +++ /dev/null @@ -1,17 +0,0 @@ -.percentage-bar { - position: absolute; - height: 100%; - background-color: rgba(22, 22, 22, 0.1); -} - -:root[data-theme='dark'] .percentage-bar { - background-color: rgba(222, 222, 222, 0.1); -} - -.percentage-bar.highlight { - background-color: rgba(39, 174, 96, 0.2); -} - -:root[data-theme='dark'] .percentage-bar.highlight { - background-color: rgba(39, 174, 96, 0.1); -} diff --git a/src/components/fidelity_bond/PercentageBar.tsx b/src/components/fidelity_bond/PercentageBar.tsx deleted file mode 100644 index 28e9f2051..000000000 --- a/src/components/fidelity_bond/PercentageBar.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import styles from './PercentageBar.module.css' - -interface PercentageBarProps { - percentage: number - highlight?: boolean -} - -const PercentageBar = ({ percentage, highlight = false }: PercentageBarProps) => { - return ( -
- ) -} - -export default PercentageBar diff --git a/src/components/fidelity_bond/fb_utils.test.ts b/src/components/fidelity_bond/fb_utils.test.ts deleted file mode 100644 index 6ece7990e..000000000 --- a/src/components/fidelity_bond/fb_utils.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as fb from './fb_utils' - -import { Lockdate } from '../../libs/JmWalletApi' - -describe('fb_utils', () => { - describe('lockdate', () => { - it('should convert timestamp to lockdate', () => { - expect(fb.lockdate.fromTimestamp(-1)).toBe('1969-12') - expect(fb.lockdate.fromTimestamp(0)).toBe('1970-01') - expect(fb.lockdate.fromTimestamp(Date.UTC(2008, 9, 31))).toBe('2008-10') - expect(fb.lockdate.fromTimestamp(Date.UTC(2009, 0, 3, 1, 42, 13, 37))).toBe('2009-01') - expect(fb.lockdate.fromTimestamp(Date.UTC(2022, 1, 18))).toBe('2022-02') - expect(fb.lockdate.fromTimestamp(Date.UTC(2999, 11))).toBe('2999-12') - expect(fb.lockdate.fromTimestamp(Date.UTC(10_000, 10))).toBe('10000-11') - - expect(() => fb.lockdate.fromTimestamp(NaN)).toThrowError('Unsupported input: NaN') - }) - - it('should convert lockdate to timestamp', () => { - expect(fb.lockdate.toTimestamp('2008-10')).toBe(Date.UTC(2008, 9, 1)) - expect(fb.lockdate.toTimestamp('2009-01')).toBe(Date.UTC(2009, 0, 1)) - expect(fb.lockdate.toTimestamp('2999-12')).toBe(Date.UTC(2999, 11)) - - expect(() => fb.lockdate.toTimestamp('-1' as Lockdate)).toThrowError('Unsupported format') - expect(() => fb.lockdate.toTimestamp('2008-1' as Lockdate)).toThrowError('Unsupported format') - expect(() => fb.lockdate.toTimestamp('10000-01' as Lockdate)).toThrowError('Unsupported format') - expect(() => fb.lockdate.toTimestamp('' as Lockdate)).toThrowError('Unsupported format') - expect(() => fb.lockdate.toTimestamp('any' as Lockdate)).toThrowError('Unsupported format') - }) - - it('should create an initial lockdate', () => { - const rangeZero = fb.toYearsRange(0, 10) - const rangeMinusOneYear = fb.toYearsRange(-1, 10) - const rangePlusOneYear = fb.toYearsRange(1, 10) - - // verify the default "months ahead" for the initial lockdate to prevent unintentional changes - expect(fb.__INITIAL_LOCKDATE_MONTH_AHEAD).toBe(3) - - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangeZero)).toBe('2009-05') - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangeMinusOneYear)).toBe('2009-05') - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 0, 3)), rangePlusOneYear)).toBe('2010-01') - - expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangeZero)).toBe('2009-02') - expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangeMinusOneYear)).toBe('2009-02') - expect(fb.lockdate.initial(new Date(Date.UTC(2008, 9, 31)), rangePlusOneYear)).toBe('2009-10') - - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangeZero)).toBe('2010-04') - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangeMinusOneYear)).toBe('2010-04') - expect(fb.lockdate.initial(new Date(Date.UTC(2009, 11, 3)), rangePlusOneYear)).toBe('2010-12') - }) - }) -}) diff --git a/src/components/fidelity_bond/fb_utils.ts b/src/components/fidelity_bond/fb_utils.ts deleted file mode 100644 index bd983aa6d..000000000 --- a/src/components/fidelity_bond/fb_utils.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Lockdate } from '../../libs/JmWalletApi' - -type Milliseconds = number - -export type YearsRange = { - min: number - max: number -} - -export const toYearsRange = (min: number, max: number): YearsRange => { - if (max <= min) { - throw new Error('Invalid values for range of years.') - } - return { min, max } -} - -// A maximum of years for a timelock to be accepted. -// This is useful in simple mode - when it should be prevented that users -// lock up their coins for an awful amount of time by accident. -// In "advanced" mode, this can be dropped or increased substantially. -export const DEFAULT_MAX_TIMELOCK_YEARS = 10 -export const DEFAULT_TIMELOCK_YEARS_RANGE = toYearsRange(0, DEFAULT_MAX_TIMELOCK_YEARS) - -// The months ahead for the initial lock date. -// It is recommended to start locking for a period of between 3 months and 1 years initially. -// This value should be at the lower end of this recommendation. -// See https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/fidelity-bonds.md#what-amount-of-bitcoins-to-lock-up-and-for-how-long -// for more information (last checked on 2022-06-13). -// Exported for tests only! -export const __INITIAL_LOCKDATE_MONTH_AHEAD = 3 - -export const lockdate = (() => { - const _fromDate = (date: Date): Lockdate => { - return `${date.getUTCFullYear()}-${date.getUTCMonth() >= 9 ? '' : '0'}${1 + date.getUTCMonth()}` as Lockdate - } - const fromTimestamp = (timestamp: Milliseconds): Lockdate => { - if (Number.isNaN(timestamp)) throw new Error('Unsupported input: NaN') - return _fromDate(new Date(timestamp)) - } - const toTimestamp = (lockdate: Lockdate): Milliseconds => { - const split = lockdate.split('-') - if (split.length !== 2 || split[0].length !== 4 || split[1].length !== 2) { - throw new Error('Unsupported format') - } - - const year = parseInt(split[0], 10) - const month = parseInt(split[1], 10) - if (Number.isNaN(year) || Number.isNaN(month)) { - throw new Error('Unsupported format') - } - return Date.UTC(year, month - 1, 1) - } - - /** - * Returns a lockdate an initial lockdate in the future. - * - * This method tries to provide a date that is at least - * {@link __INITIAL_LOCKDATE_MONTH_AHEAD} months after {@link now}. - * - * @param now the reference date - * @param range a min/max range of years - * @returns an initial lockdate - */ - const initial = (now: Date, range: YearsRange = DEFAULT_TIMELOCK_YEARS_RANGE): Lockdate => { - const year = now.getUTCFullYear() - const month = now.getUTCMonth() - - const minMonthAhead = Math.max(range.min * 12, __INITIAL_LOCKDATE_MONTH_AHEAD + 1) - const initYear = year + Math.floor((month + minMonthAhead) / 12) - const initMonth = (month + minMonthAhead) % 12 - return fromTimestamp(Date.UTC(initYear, initMonth, 1)) - } - - return { - fromTimestamp, - toTimestamp, - initial, - } -})() From 7b2c1599d3fd7c56fced63f2f2e164f4fed1064a Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 5 Jul 2022 14:06:38 +0200 Subject: [PATCH 05/13] fix: selection box ui --- src/components/fb/FidelityBondSteps.module.css | 17 ++++++++++++++--- src/components/fb/FidelityBondSteps.tsx | 4 +++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css index 392b796db..708d86194 100644 --- a/src/components/fb/FidelityBondSteps.module.css +++ b/src/components/fb/FidelityBondSteps.module.css @@ -42,8 +42,11 @@ .utxoCard > .utxoSelectionMarker { width: 1.25rem; height: 1.25rem; - border-radius: 50%; + border-radius: 0.3rem; border: 2px solid var(--bs-gray-200); + display: flex; + align-items: center; + justify-content: center; } :root[data-theme='dark'] .utxoCard > .utxoSelectionMarker { @@ -55,15 +58,23 @@ } .utxoCard.selected > .utxoSelectionMarker { - border: 6px solid #2d9cdb; + border: 2px solid #2d9cdb; background-color: white; } +.utxoCard.selected > .utxoSelectionMarker > svg { + color: #2d9cdb; +} + .utxoCard.selected:not(.selectable) > .utxoSelectionMarker { - border: 6px solid var(--bs-gray-200); + border: 2px solid var(--bs-gray-200); background-color: white; } +.utxoCard.selected:not(.selectable) > .utxoSelectionMarker > svg { + color: var(--bs-gray-200); +} + :root[data-theme='dark'] .utxoCard.selected:not(.selectable) > .utxoSelectionMarker { border-color: var(--bs-gray-500); } diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index 28bf69be6..dbbc0485d 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -113,7 +113,9 @@ const UtxoCard = ({ className={cx('utxoCard', { selected: isSelected, selectable: isSelectable })} onClick={() => isSelectable && onClick()} > -
+
+ {isSelected && } +
{utxo.address} From 487df34fcbb60a29a9f5820ce4b534c25ae14a14 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Tue, 5 Jul 2022 14:12:12 +0200 Subject: [PATCH 06/13] fix: dark mode fix --- src/components/fb/ExistingFidelityBond.jsx | 2 +- src/components/fb/ExistingFidelityBond.module.css | 4 ++++ src/components/fb/FidelityBondSteps.module.css | 4 ++++ src/components/fb/FidelityBondSteps.tsx | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/fb/ExistingFidelityBond.jsx b/src/components/fb/ExistingFidelityBond.jsx index 69dd371e3..aeed60ce8 100644 --- a/src/components/fb/ExistingFidelityBond.jsx +++ b/src/components/fb/ExistingFidelityBond.jsx @@ -31,7 +31,7 @@ const ExistingFidelityBond = ({ utxo }) => { } - successText={} + successText={} value={utxo.address} className={styles.icon} /> diff --git a/src/components/fb/ExistingFidelityBond.module.css b/src/components/fb/ExistingFidelityBond.module.css index cbbcd4756..0a96e8ce1 100644 --- a/src/components/fb/ExistingFidelityBond.module.css +++ b/src/components/fb/ExistingFidelityBond.module.css @@ -38,3 +38,7 @@ width: 18px; height: 18px; } + +.icon svg { + color: var(--bs-body-color); +} diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css index 708d86194..b177de6ab 100644 --- a/src/components/fb/FidelityBondSteps.module.css +++ b/src/components/fb/FidelityBondSteps.module.css @@ -153,6 +153,10 @@ height: 18px; } +.confirmationStepIcon svg { + color: var(--bs-body-color); +} + .confirmationStepLabel { color: var(--bs-gray-600); font-size: 0.8rem; diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index dbbc0485d..e23236eac 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -258,7 +258,7 @@ const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress } } - successText={} + successText={} value={timelockedAddress} className={styles.confirmationStepIcon} /> From ab03dc27a72a5e33590d9930b99304cd86d37d5a Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:11:04 +0200 Subject: [PATCH 07/13] fix: remove cj-out testing rig --- src/components/fb/CreateFidelityBond.jsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx index 01cc36271..6839e83e2 100644 --- a/src/components/fb/CreateFidelityBond.jsx +++ b/src/components/fb/CreateFidelityBond.jsx @@ -127,12 +127,6 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal const utxosByAccount = utxos.reduce((res, utxo) => { const { mixdepth } = utxo res[mixdepth] = res[mixdepth] || [] - - // todo: remove - if (utxo.value === 100000000) { - utxo.label = 'cj-out' - } - res[mixdepth].push(utxo) return res From 826e7f84c1d90d843bfda5363951f732309cc79d Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:12:51 +0200 Subject: [PATCH 08/13] fix: typos Co-authored-by: Gigi <109058+dergigi@users.noreply.github.com> --- src/components/fb/FidelityBondSteps.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index e23236eac..2d1ca59f0 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -239,7 +239,7 @@ const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress } }, { icon: , - label: 'Funds will be spend from', + label: 'Funds will be spent from', content: `Jar #${jar}`, }, { @@ -263,7 +263,7 @@ const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress } className={styles.confirmationStepIcon} /> ), - label: 'Funds will be lockd up on this address', + label: 'Funds will be locked up on this address', content: {timelockedAddress}, }, ] From 6a1f61677c4603b0b8d7d2380eb693628a5af42c Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 10:27:20 +0200 Subject: [PATCH 09/13] review: resuse settings from parent --- src/components/fb/FidelityBondSteps.tsx | 54 ++++++++++++------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx index 2d1ca59f0..6e03f0f53 100644 --- a/src/components/fb/FidelityBondSteps.tsx +++ b/src/components/fb/FidelityBondSteps.tsx @@ -198,38 +198,34 @@ const FreezeUtxos = ({ jar, utxos, selectedUtxos, isLoading = false }: FreezeUtx const ReviewInputs = ({ lockDate, jar, utxos, selectedUtxos, timelockedAddress }: ReviewInputsProps) => { const settings = useSettings() - const UtxoSummary = ({ title, icon, utxos }: { title: string; icon: string; utxos: Array }) => { - const settings = useSettings() - - return ( -
-
- -
{title}
-
-
- {utxos.map((utxo, index) => ( -
-
-
- -
- {utxo.label === 'cj-out' && ( -
- - {utxo.label} -
- )} -
-
- {utxo.address} + const UtxoSummary = ({ title, icon, utxos }: { title: string; icon: string; utxos: Array }) => ( +
+
+ +
{title}
+
+
+ {utxos.map((utxo, index) => ( +
+
+
+
+ {utxo.label === 'cj-out' && ( +
+ + {utxo.label} +
+ )}
- ))} -
+
+ {utxo.address} +
+
+ ))}
- ) - } +
+ ) const confirmationItems = [ { From 82673f52c2d0bb7e68dc3ee4b9540ad41e1d79f0 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:06:41 +0200 Subject: [PATCH 10/13] review: hide balance on existing fidelity bond --- src/components/fb/ExistingFidelityBond.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/fb/ExistingFidelityBond.jsx b/src/components/fb/ExistingFidelityBond.jsx index aeed60ce8..3b98a91a8 100644 --- a/src/components/fb/ExistingFidelityBond.jsx +++ b/src/components/fb/ExistingFidelityBond.jsx @@ -14,7 +14,11 @@ const ExistingFidelityBond = ({ utxo }) => {
Fidelity Bond
- +
From c7283136c71550152d84d05ec7f563c06dc986be Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:21:05 +0200 Subject: [PATCH 11/13] review: clear alerts on successful requests --- src/components/fb/CreateFidelityBond.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx index 6839e83e2..d4cc8e6ff 100644 --- a/src/components/fb/CreateFidelityBond.jsx +++ b/src/components/fb/CreateFidelityBond.jsx @@ -70,6 +70,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal await Promise.all(freezeCalls) .then((_) => reloadCurrentWalletInfo({ signal: abortCtrl.signal })) + .then((_) => setAlert(null)) .catch((err) => { setAlert({ variant: 'danger', message: err.message, dismissible: true }) }) @@ -93,6 +94,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal return res.ok ? res.json() : Api.Helper.throwError(res, 'fidelity_bond.error_loading_timelock_address_failed') }) .then((data) => setTimelockedAddress(data.address)) + .then((_) => setAlert(null)) .catch((err) => { setAlert({ variant: 'danger', message: err.message }) }) @@ -115,6 +117,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal return Api.Helper.throwError(res, 'Could not create fidelity bond.') } }) + .then((_) => setAlert(null)) .catch((err) => { setAlert({ variant: 'danger', message: err.message }) }) From 38ee904635e15cdcd510a6e48d03768a5de6f3ca Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 12:23:17 +0200 Subject: [PATCH 12/13] review: do not compress selection box --- src/components/fb/FidelityBondSteps.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/fb/FidelityBondSteps.module.css b/src/components/fb/FidelityBondSteps.module.css index b177de6ab..0062b9c60 100644 --- a/src/components/fb/FidelityBondSteps.module.css +++ b/src/components/fb/FidelityBondSteps.module.css @@ -47,6 +47,7 @@ display: flex; align-items: center; justify-content: center; + flex-shrink: 0; } :root[data-theme='dark'] .utxoCard > .utxoSelectionMarker { From 9d58308f71304b77766636fde755dc2bf4dc9319 Mon Sep 17 00:00:00 2001 From: Daniel <10026790+dnlggr@users.noreply.github.com> Date: Wed, 6 Jul 2022 13:39:17 +0200 Subject: [PATCH 13/13] review: fix promise code --- src/components/fb/CreateFidelityBond.jsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/fb/CreateFidelityBond.jsx b/src/components/fb/CreateFidelityBond.jsx index d4cc8e6ff..df082268c 100644 --- a/src/components/fb/CreateFidelityBond.jsx +++ b/src/components/fb/CreateFidelityBond.jsx @@ -54,7 +54,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal setAlert(null) } - const freezeUtxos = async (utxos) => { + const freezeUtxos = (utxos) => { setIsLoading(true) const abortCtrl = new AbortController() @@ -68,7 +68,7 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal }) ) - await Promise.all(freezeCalls) + Promise.all(freezeCalls) .then((_) => reloadCurrentWalletInfo({ signal: abortCtrl.signal })) .then((_) => setAlert(null)) .catch((err) => { @@ -79,12 +79,12 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal }) } - const loadTimeLockedAddress = async (lockDate) => { + const loadTimeLockedAddress = (lockDate) => { setIsLoading(true) const abortCtrl = new AbortController() - await Api.getAddressTimelockNew({ + Api.getAddressTimelockNew({ walletName: wallet.name, token: wallet.token, signal: abortCtrl.signal, @@ -101,10 +101,10 @@ const CreateFidelityBond = ({ otherFidelityBondExists, accountBalances, totalBal .finally(() => setIsLoading(false)) } - const directSweepToFidelityBond = async (jar, address) => { + const directSweepToFidelityBond = (jar, address) => { setIsLoading(true) - await Api.postDirectSend( + Api.postDirectSend( { walletName: wallet.name, token: wallet.token }, { mixdepth: jar,