From ad3a3638ef1ad1db9428ec969f1ac8930d4393a5 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 2 Jun 2024 12:10:26 +0530 Subject: [PATCH 01/35] Implemented UTXO Selection Modal --- public/sprite.svg | 3 + src/components/Send/SendForm.tsx | 8 +- src/components/Send/ShowUtxos.module.css | 99 ++++++ src/components/Send/ShowUtxos.tsx | 293 ++++++++++++++++++ .../Send/SourceJarSelector.module.css | 1 + src/components/Send/SourceJarSelector.tsx | 72 +++-- src/components/Send/index.tsx | 1 + src/components/jar_details/UtxoList.tsx | 2 +- src/components/jars/Jar.tsx | 67 +++- src/i18n/locales/en/translation.json | 9 + 10 files changed, 534 insertions(+), 21 deletions(-) create mode 100644 src/components/Send/ShowUtxos.module.css create mode 100644 src/components/Send/ShowUtxos.tsx diff --git a/public/sprite.svg b/public/sprite.svg index 67f0b222f..6777e65d5 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -361,4 +361,7 @@ + + + diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 7b16481f9..7eeaf130b 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -26,7 +26,7 @@ import { isValidNumCollaborators, } from './helpers' import { AccountBalanceSummary } from '../../context/BalanceSummary' -import { WalletInfo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' @@ -221,6 +221,7 @@ interface InnerSendFormProps { className?: string isLoading: boolean walletInfo?: WalletInfo + wallet: CurrentWallet loadNewWalletAddress: (props: { signal: AbortSignal; jarIndex: JarIndex }) => Promise minNumCollaborators: number feeConfigValues?: FeeValues @@ -233,6 +234,7 @@ const InnerSendForm = ({ className, isLoading, walletInfo, + wallet, loadNewWalletAddress, minNumCollaborators, feeConfigValues, @@ -272,6 +274,7 @@ const InnerSendForm = ({ name="sourceJarIndex" label={t('send.label_source_jar')} walletInfo={walletInfo} + wallet={wallet} isLoading={isLoading} disabled={disabled} variant={showCoinjoinPreconditionViolationAlert ? 'warning' : 'default'} @@ -375,6 +378,7 @@ type SendFormProps = Omit & { onSubmit: (values: SendFormValues) => Promise formRef?: React.Ref> blurred?: boolean + wallet: CurrentWallet } export const SendForm = ({ @@ -383,6 +387,7 @@ export const SendForm = ({ formRef, blurred = false, walletInfo, + wallet, minNumCollaborators, ...innerProps }: SendFormProps) => { @@ -446,6 +451,7 @@ export const SendForm = ({ props={props} className={blurred ? styles.blurred : undefined} walletInfo={walletInfo} + wallet={wallet} minNumCollaborators={minNumCollaborators} {...innerProps} /> diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css new file mode 100644 index 000000000..d10bdef1f --- /dev/null +++ b/src/components/Send/ShowUtxos.module.css @@ -0,0 +1,99 @@ +/* ShowUtxos.module.css */ + +.utxoRowUnfrozen { + background-color: #27ae600d; + padding: 8px 30px; + height: 46px; + color: #27ae60; + margin-bottom: 4px; + cursor: pointer; +} + +.utxoRowFrozen { + background-color: #2d9cdb0d; + padding: 8px 30px; + height: 46px; + color: #2d9cdb; + margin-bottom: 4px; + cursor: pointer; +} + +.iconMixed { + color: #27ae60; +} + +.iconFrozen { + color: #2d9cdb; +} + +.iconConfirmations { + color: #27ae60; + margin-bottom: 4px; +} + +.iconConfirmationsFreeze { + color: #2d9cdb; + margin-bottom: 4px; +} + +.valueColumn { + margin-right: 20px; +} + +.subTitle { + color: #777777; + border: none; + margin-bottom: 1.5rem; +} + +.NextButton { + width: 47%; + height: 48px; + padding: 14px 20px 14px 20px; + margin-right: 19px; + border-radius: 5px; + opacity: 0px; +} + +.BackButton { + width: 47%; + height: 48px; + padding: 14px 20px 14px 20px; + margin-right: 19px; + border-radius: 5px; + opacity: 0px; +} + +.utxoTagUnFreeze { + white-space: nowrap; + border: 1px solid #27ae60; + background-color: #c6eed7; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; +} + +.utxoTagFreeze { + white-space: nowrap; + border: 1px solid #2d9cdb; + background-color: #bce7ff; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; +} + +.parent-class .utxoTag { + border: 1px solid #27ae60 !important; + border-radius: 0.2rem !important; +} + +.squareToggleButton { + height: 22px !important; + border-radius: 3px !important; +} + +.squareFrozenToggleButton { + height: 22px !important; + border-radius: 3px !important; + border: 1px solid #2d9cdb !important; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx new file mode 100644 index 000000000..18f4afdf6 --- /dev/null +++ b/src/components/Send/ShowUtxos.tsx @@ -0,0 +1,293 @@ +import { useState, useEffect, useCallback } from 'react' +import * as rb from 'react-bootstrap' +import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import Sprite from '../Sprite' +import Alert from '../Alert' +import { useTranslation } from 'react-i18next' +import { TFunction } from 'i18next' +import * as Api from '../../libs/JmWalletApi' +import { utxoTags } from '../jar_details/UtxoList' +import mainStyles from '../MainWalletView.module.css' +import styles from './ShowUtxos.module.css' + +type UtxoType = { + address: Api.BitcoinAddress + path: string + label: string + checked: boolean + value: Api.AmountSats + tries: number + tries_remaining: number + external: boolean + mixdepth: number + confirmations: number + frozen: boolean + utxo: Api.UtxoId + locktime?: string +} + +type UtxoList = UtxoType[] + +interface SignModalProps { + walletInfo: WalletInfo + wallet: CurrentWallet + show: boolean + onHide: () => void + index: String +} + +interface UtxoRowProps { + utxo: UtxoType + index: number + onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + walletInfo: WalletInfo + t: TFunction + isFrozen: boolean +} + +interface UtxoListDisplayProps { + utxos: UtxoList + onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + walletInfo: WalletInfo + t: TFunction + isFrozen: boolean +} + +// Utility function to format Bitcoin address +const formatAddress = (address: string) => `${address.slice(0, 10)}...${address.slice(-8)}` + +// Utility function to format the confirmations +const formatConfirmations = (conf: number) => { + if (conf === 0) return { symbol: 'confs-0', confirmations: conf } + if (conf === 1) return { symbol: 'confs-1', confirmations: conf } + if (conf === 2) return { symbol: 'confs-2', confirmations: conf } + if (conf === 3) return { symbol: 'confs-3', confirmations: conf } + if (conf === 4) return { symbol: 'confs-4', confirmations: conf } + if (conf === 5) return { symbol: 'confs-5', confirmations: conf } + if (conf >= 9999) return { symbol: 'confs-full', confirmations: '9999+' } + return { symbol: 'confs-full', confirmations: conf } +} + +// Utility function to convert Satoshi to Bitcoin +const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) + +const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProps) => { + const address = formatAddress(utxo.address) + const conf = formatConfirmations(utxo.confirmations) + const value = satsToBtc(utxo.value) + const tags = utxoTags(utxo, walletInfo, t) + const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen + const icon = isFrozen ? 'snowflake' : 'mixed' + const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze + + return ( + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> + + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} + className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + /> + + + + + {address} + + + {conf.confirmations} + + {`₿${value}`} + +
{tags[0].tag}
+
+
+ ) +} + +const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( +
+ {utxos.map((utxo, index) => ( + + ))} +
+) + +const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) => { + const abortCtrl = new AbortController() + + const { t } = useTranslation() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + + const [alert, setAlert] = useState() + const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) + const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) + const [frozenUtxos, setFrozenUtxos] = useState([]) + + // Effect to load UTXO data when component mounts or index/walletInfo changes + useEffect(() => { + const loadData = () => { + const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) + const utxos = data ? data[1] : [] + + const frozen = utxos.filter((utxo) => utxo.frozen).map((utxo) => ({ ...utxo, checked: false })) + const unfrozen = utxos.filter((utxo) => !utxo.frozen).map((utxo) => ({ ...utxo, checked: true })) + + setFrozenUtxos(frozen) + setUnFrozenUtxos(unfrozen) + + if (utxos && unfrozen.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos'), dismissible: true }) + } else { + setAlert(undefined) + } + } + + loadData() + }, [index, walletInfo.utxosByJar, t]) + + // Handler to toggle UTXO selection + const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { + if (type === 'unfrozen') { + setUnFrozenUtxos((prevUtxos) => + prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), + ) + } else { + setFrozenUtxos((prevUtxos) => + prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), + ) + } + }, []) + + // Handler for the "Next" button click + const handleNext = async () => { + const frozenUtxosToUpdate = frozenUtxos + .filter((utxo) => utxo.checked && !utxo.locktime) + .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) + const unFrozenUtxosToUpdate = unFrozenUtxos + .filter((utxo) => !utxo.checked) + .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) + + for (const utxo of frozenUtxos) { + if (utxo.checked && utxo.locktime) { + setAlert({ + variant: 'danger', + message: `${t('showUtxos.alert_for_time_locked')} ${utxo.locktime}`, + dismissible: true, + }) + return + } + } + + if (frozenUtxosToUpdate.length >= 1) { + try { + const freezeCalls = frozenUtxosToUpdate.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( + (res) => { + if (!res.ok) { + return Api.Helper.throwError(res) + } + }, + ), + ) + + await Promise.all(freezeCalls) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + return + } + } + + const uncheckedUnfrozen = unFrozenUtxos.filter((utxo) => !utxo.checked) + if (uncheckedUnfrozen.length === unFrozenUtxos.length && frozenUtxosToUpdate.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + return + } + + try { + const unfreezeCalls = unFrozenUtxosToUpdate.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( + (res) => { + if (!res.ok) { + return Api.Helper.throwError(res) + } + }, + ), + ) + + await Promise.all(unfreezeCalls) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + } + + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + onHide() + } + + return ( + + + {t('showUtxos.show_utxo_title')} + + +
{t('showUtxos.show_utxo_subtitle')}
+ {alert && ( + + setAlert(undefined)} + /> + + )} + + + +
+
+ +
+
+
+
+ {showFrozenUtxos && ( + + )} +
+ + + {t('showUtxos.back_button')} + + + {t('showUtxos.next_button')} + + +
+ ) +} + +export default ShowUtxos diff --git a/src/components/Send/SourceJarSelector.module.css b/src/components/Send/SourceJarSelector.module.css index 74f910d85..9dc43eea7 100644 --- a/src/components/Send/SourceJarSelector.module.css +++ b/src/components/Send/SourceJarSelector.module.css @@ -7,6 +7,7 @@ gap: 1rem; color: var(--bs-body-color); margin-bottom: 1.5rem; + margin-top: 2rem; } .sourceJarsPlaceholder { diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 3f77374ae..952c54085 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -1,10 +1,11 @@ -import { useMemo } from 'react' +import { useState, useMemo } from 'react' import { useField, useFormikContext } from 'formik' import * as rb from 'react-bootstrap' -import { jarFillLevel, SelectableJar } from '../jars/Jar' +import { jarFillLevel, SelectableSendJar } from '../jars/Jar' import { noop } from '../../utils' -import { WalletInfo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' +import ShowUtxos from './ShowUtxos' export type SourceJarSelectorProps = { name: string @@ -12,20 +13,31 @@ export type SourceJarSelectorProps = { className?: string variant: 'default' | 'warning' walletInfo?: WalletInfo + wallet: CurrentWallet isLoading: boolean disabled?: boolean } +interface showingUtxosProps { + index: String + show: boolean +} + export const SourceJarSelector = ({ name, label, walletInfo, + wallet, variant, isLoading, disabled = false, }: SourceJarSelectorProps) => { const [field] = useField(name) const form = useFormikContext() + const [showingUTXOS, setshowingUTXOS] = useState({ + index: '', + show: false, + }) const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -44,22 +56,46 @@ export const SourceJarSelector = ({ ) : (
- {jarBalances.map((it) => ( - 0} - isSelected={it.accountIndex === field.value} - fillLevel={jarFillLevel( - it.calculatedTotalBalanceInSats, - walletInfo.balanceSummary.calculatedTotalBalanceInSats, - )} - variant={it.accountIndex === field.value ? variant : undefined} - onClick={(jarIndex) => form.setFieldValue(field.name, jarIndex, true)} + {showingUTXOS.show && ( + { + setshowingUTXOS({ + index: '', + show: false, + }) + }} + index={showingUTXOS.index} /> - ))} + )} + {jarBalances.map((it) => { + return ( +
+ 0} + isSelected={it.accountIndex === field.value} + fillLevel={jarFillLevel( + it.calculatedTotalBalanceInSats, + walletInfo.balanceSummary.calculatedTotalBalanceInSats, + )} + variant={it.accountIndex === field.value ? variant : undefined} + onClick={(jarIndex) => { + form.setFieldValue(field.name, jarIndex, true) + setshowingUTXOS({ + index: jarIndex.toString(), + show: true, + }) + }} + /> +
+ ) + })}
)} diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 3fa33fd5d..e5f920062 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -480,6 +480,7 @@ export default function Send({ wallet }: SendProps) { disabled={isOperationDisabled} isLoading={isLoading} walletInfo={walletInfo} + wallet={wallet} minNumCollaborators={minNumCollaborators} loadNewWalletAddress={loadNewWalletAddress} feeConfigValues={feeConfigValues} diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index 32219a307..e782f0800 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -37,7 +37,7 @@ const ADDRESS_STATUS_COLORS: { [key: string]: string } = { type Tag = { tag: string; color: string } -const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction): Tag[] => { +export const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction): Tag[] => { const rawStatus = walletInfo.addressSummary[utxo.address]?.status let status: string | null = null diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index 7da0271ac..bf97fd26f 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -27,6 +27,14 @@ export type SelectableJarProps = JarProps & { onClick: (index: JarIndex) => void } +export type SelectableSendJarProps = JarProps & { + tooltipText: string + isSelectable: boolean + isSelected: boolean + variant?: 'default' | 'warning' + onClick: (index: JarIndex) => void +} + export type OpenableJarProps = Omit & { tooltipText: string onClick: () => void @@ -225,4 +233,61 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } -export { SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } +const SelectableSendJar = ({ + tooltipText, + isSelectable, + isSelected, + onClick, + index, + variant = 'default', + ...jarProps +}: SelectableSendJarProps) => { + const [jarIsOpen, setJarIsOpen] = useState(false) + const onMouseOver = () => setJarIsOpen(true) + const onMouseOut = () => setJarIsOpen(false) + + return ( +
isSelectable && onClick(index)} onMouseOver={onMouseOver} onMouseOut={onMouseOut}> + { + return isSelectable ? {tooltipText} : <> + }} + > +
+ +
+ isSelectable && onClick(index)} + className={styles.selectionCircle} + disabled={!isSelectable} + /> + {variant === 'warning' && ( +
+ +
+ )} +
+
+
+
+ ) +} + +export { SelectableSendJar, SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 18e909ef0..dbe9c304e 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -698,5 +698,14 @@ "utxo_detail_label_locktime": "Locktime", "utxo_detail_label_status": "Address status" } + }, + "showUtxos": { + "show_utxo_title": "Select UTXOS to send", + "show_utxo_subtitle": "The following UTXOs are selected to be sent. Modify if needed.", + "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", + "alert_for_time_locked": "Selected UTXO is Time Locked till", + "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", + "back_button": "Back", + "next_button": "Next" } } From e4e62da093a6119abfe9d5c3cb591d0dc6e87ee2 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Mon, 3 Jun 2024 21:01:02 +0530 Subject: [PATCH 02/35] Done with the suggested changes in ShowUtxos --- src/components/Send/ShowUtxos.module.css | 5 ++ src/components/Send/ShowUtxos.tsx | 86 +++++++----------------- 2 files changed, 31 insertions(+), 60 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index d10bdef1f..25720d45c 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -97,3 +97,8 @@ border-radius: 3px !important; border: 1px solid #2d9cdb !important; } + +.utxoListDisplay { + margin-left: -20px; + margin-right: 20px; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 18f4afdf6..8f53b79b7 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -115,7 +115,7 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp } const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( -
+
{utxos.map((utxo, index) => ( { + const frozenUtxosToUpdate = frozenUtxos.filter((utxo) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo) => utxo.checked && utxo.locktime) + const noUnfrozenUtxos = unFrozenUtxos.length === 0 + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo) => !utxo.checked) + + if (timeLockedUtxo) { + setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + } else if (noUnfrozenUtxos) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { + setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + } else { + setAlert(undefined) + } + }, [unFrozenUtxos, frozenUtxos, t]) + // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { if (type === 'unfrozen') { @@ -185,64 +197,18 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) .filter((utxo) => !utxo.checked) .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) - for (const utxo of frozenUtxos) { - if (utxo.checked && utxo.locktime) { - setAlert({ - variant: 'danger', - message: `${t('showUtxos.alert_for_time_locked')} ${utxo.locktime}`, - dismissible: true, - }) - return - } - } - - if (frozenUtxosToUpdate.length >= 1) { - try { - const freezeCalls = frozenUtxosToUpdate.map((utxo) => - Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( - (res) => { - if (!res.ok) { - return Api.Helper.throwError(res) - } - }, - ), - ) - - await Promise.all(freezeCalls) - } catch (err: any) { - if (!abortCtrl.signal.aborted) { - setAlert({ variant: 'danger', message: err.message, dismissible: true }) - } - return - } - } - - const uncheckedUnfrozen = unFrozenUtxos.filter((utxo) => !utxo.checked) - if (uncheckedUnfrozen.length === unFrozenUtxos.length && frozenUtxosToUpdate.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - return - } - try { - const unfreezeCalls = unFrozenUtxosToUpdate.map((utxo) => - Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( - (res) => { - if (!res.ok) { - return Api.Helper.throwError(res) - } - }, - ), - ) - - await Promise.all(unfreezeCalls) + await Promise.all([ + ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ]) + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { setAlert({ variant: 'danger', message: err.message, dismissible: true }) } } - - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) - onHide() } return ( @@ -282,7 +248,7 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) {t('showUtxos.back_button')} - + {t('showUtxos.next_button')} From 49081855ba03ba33936632e3ecc4b4386ab8ad30 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Mon, 3 Jun 2024 21:42:57 +0530 Subject: [PATCH 03/35] minor bug fixing --- src/components/Send/ShowUtxos.module.css | 7 +-- src/components/Send/ShowUtxos.tsx | 67 ++++++++++++++---------- src/components/jars/Jar.tsx | 8 +-- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 25720d45c..12fa86c08 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -48,20 +48,17 @@ .NextButton { width: 47%; - height: 48px; + height: 3.15rem; padding: 14px 20px 14px 20px; - margin-right: 19px; border-radius: 5px; - opacity: 0px; } .BackButton { width: 47%; - height: 48px; + height: 3.15rem; padding: 14px 20px 14px 20px; margin-right: 19px; border-radius: 5px; - opacity: 0px; } .utxoTagUnFreeze { diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 8f53b79b7..b3e660808 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -82,40 +82,49 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp return ( onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> - - onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} - className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} - /> - - - - - {address} - - - {conf.confirmations} - - {`₿${value}`} - -
{tags[0].tag}
-
+
+ + + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} + className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + /> + + + + + {address} + + + {conf.confirmations} + + {`₿${value}`} + +
{tags[0].tag}
+
+
+
) } const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( -
+
{utxos.map((utxo, index) => (
- isSelectable && onClick(index)} - className={styles.selectionCircle} - disabled={!isSelectable} - /> + {variant === 'warning' && (
From fa05a0192aa0993228755ac3e49789bae4a49606 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sat, 8 Jun 2024 18:12:46 +0530 Subject: [PATCH 04/35] Fixed Undefined tag issue , Refactor Jar Selection logic & some minor css --- src/components/Balance.tsx | 19 +- src/components/Send/ShowUtxos.module.css | 55 +++--- src/components/Send/ShowUtxos.tsx | 210 +++++++++++++--------- src/components/Send/SourceJarSelector.tsx | 12 +- src/components/jars/Jar.module.css | 4 + src/components/jars/Jar.tsx | 88 +++++---- src/context/SettingsContext.tsx | 2 +- src/i18n/locales/en/translation.json | 9 +- 8 files changed, 247 insertions(+), 152 deletions(-) diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 880d0f67e..645ee00a0 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -39,23 +39,28 @@ interface BalanceComponentProps { symbol?: JSX.Element showSymbol?: boolean frozen?: boolean + isColorChange?: boolean + frozenSymbol?: boolean } const BalanceComponent = ({ symbol, showSymbol = true, frozen = false, + isColorChange = false, + frozenSymbol = true, children, }: PropsWithChildren) => { return ( {children} {showSymbol && symbol} - {frozen && FROZEN_SYMBOL} + {frozen && frozenSymbol && FROZEN_SYMBOL} ) } @@ -75,7 +80,9 @@ const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => { return ( & { value: number const SatsBalance = ({ value, ...props }: SatsBalanceProps) => { return ( - + {formatSats(value)} diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 12fa86c08..11dded072 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -1,10 +1,8 @@ -/* ShowUtxos.module.css */ - .utxoRowUnfrozen { background-color: #27ae600d; padding: 8px 30px; height: 46px; - color: #27ae60; + color: #27ae60 !important; margin-bottom: 4px; cursor: pointer; } @@ -13,7 +11,7 @@ background-color: #2d9cdb0d; padding: 8px 30px; height: 46px; - color: #2d9cdb; + color: #2d9cdb !important; margin-bottom: 4px; cursor: pointer; } @@ -47,18 +45,18 @@ } .NextButton { - width: 47%; + width: 47% !important; height: 3.15rem; - padding: 14px 20px 14px 20px; - border-radius: 5px; + padding: 14px 20px 14px 20px !important; + border-radius: 5px !important; } .BackButton { - width: 47%; + width: 47% !important; height: 3.15rem; - padding: 14px 20px 14px 20px; - margin-right: 19px; - border-radius: 5px; + padding: 14px 20px 14px 20px !important; + margin-right: 33px !important; + border-radius: 5px !important; } .utxoTagUnFreeze { @@ -68,6 +66,7 @@ border-radius: 0.35rem; padding: 0rem 0.25rem; display: inline-block; + margin-left: 4px; } .utxoTagFreeze { @@ -77,6 +76,7 @@ border-radius: 0.35rem; padding: 0rem 0.25rem; display: inline-block; + margin-left: 4px; } .parent-class .utxoTag { @@ -84,18 +84,33 @@ border-radius: 0.2rem !important; } +.utxoListDisplay { + margin-left: -27px !important; +} + +.modalBody { + padding: 40px !important; +} + .squareToggleButton { - height: 22px !important; - border-radius: 3px !important; + appearance: none; + width: 22px; + height: 22px; + border-radius: 3px; + border: 1px solid var(--bs-body-color); + margin-top: 3.5px; } -.squareFrozenToggleButton { - height: 22px !important; - border-radius: 3px !important; - border: 1px solid #2d9cdb !important; +.selected { + visibility: visible !important; + background-color: var(--bs-body-color); } -.utxoListDisplay { - margin-left: -20px; - margin-right: 20px; +.squareFrozenToggleButton { + appearance: none; + width: 22px; + height: 22px; + border-radius: 3px; + border: 1px solid #2d9cdb; + margin-top: 3.5px; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index b3e660808..586ad22c4 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,14 +1,16 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import { useSettings, Settings } from '../../context/SettingsContext' import Sprite from '../Sprite' import Alert from '../Alert' import { useTranslation } from 'react-i18next' -import { TFunction } from 'i18next' import * as Api from '../../libs/JmWalletApi' import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' +import Balance from '../Balance' +import classNames from 'classnames' type UtxoType = { address: Api.BitcoinAddress @@ -24,12 +26,12 @@ type UtxoType = { frozen: boolean utxo: Api.UtxoId locktime?: string + _tags: { tag: string; color: string }[] } type UtxoList = UtxoType[] -interface SignModalProps { - walletInfo: WalletInfo +interface ShowUtxosProps { wallet: CurrentWallet show: boolean onHide: () => void @@ -40,17 +42,15 @@ interface UtxoRowProps { utxo: UtxoType index: number onToggle: (index: number, type: 'frozen' | 'unfrozen') => void - walletInfo: WalletInfo - t: TFunction isFrozen: boolean + settings: Settings } interface UtxoListDisplayProps { utxos: UtxoList onToggle: (index: number, type: 'frozen' | 'unfrozen') => void - walletInfo: WalletInfo - t: TFunction isFrozen: boolean + settings: Settings } // Utility function to format Bitcoin address @@ -71,11 +71,12 @@ const formatConfirmations = (conf: number) => { // Utility function to convert Satoshi to Bitcoin const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) -const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProps) => { +// UTXO row component +const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - const tags = utxoTags(utxo, walletInfo, t) + const tags = utxo._tags const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen const icon = isFrozen ? 'snowflake' : 'mixed' const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze @@ -85,14 +86,13 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp
- onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} - className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + defaultChecked={utxo.checked} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} /> @@ -113,7 +113,16 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp /> {conf.confirmations} - {`₿${value}`} + + +
{tags[0].tag}
@@ -121,68 +130,93 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp
) -} +}) -const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( +// UTXO list display component +const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => (
{utxos.map((utxo, index) => ( - + ))}
-) - -const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) => { - const abortCtrl = new AbortController() +)) +// Main component to show UTXOs +const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + const settings = useSettings() - const [alert, setAlert] = useState() + const isHandleReloadExecuted = useRef(false) + + const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) + const [isLoading, setisLoading] = useState(true) - // Effect to load UTXO data when component mounts or index/walletInfo changes - useEffect(() => { - const loadData = () => { + // Load data from wallet info + const loadData = useCallback( + (walletInfo: WalletInfo) => { const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) - const utxos = data ? data[1] : [] + const utxos: any = data ? data[1] : [] + + const newUtxos = utxos.map((utxo: any) => ({ + ...utxo, + id: utxo.utxo, + _tags: utxoTags(utxo, walletInfo, t), + })) - const frozen = utxos.filter((utxo) => utxo.frozen).map((utxo) => ({ ...utxo, checked: false })) - const unfrozen = utxos.filter((utxo) => !utxo.frozen).map((utxo) => ({ ...utxo, checked: true })) + const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) + const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) setFrozenUtxos(frozen) setUnFrozenUtxos(unfrozen) - } - loadData() - }, [index, walletInfo.utxosByJar, t]) + if (unfrozen.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + } else { + setAlert(undefined) + } + + setisLoading(false) + }, + [index, t], + ) - //Effect to set Warnings & Alert useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo) => utxo.checked && utxo.locktime) - const noUnfrozenUtxos = unFrozenUtxos.length === 0 - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo) => !utxo.checked) + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) if (timeLockedUtxo) { setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (noUnfrozenUtxos) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else { + } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } - }, [unFrozenUtxos, frozenUtxos, t]) + }, [frozenUtxos, unFrozenUtxos, t]) + + // Reload wallet info + const handleReload = useCallback(async () => { + const abortCtrl = new AbortController() + try { + const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) + loadData(walletInfo) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + } + }, [reloadCurrentWalletInfo, loadData]) + + useEffect(() => { + if (!isHandleReloadExecuted.current) { + handleReload() + isHandleReloadExecuted.current = true + } + }, [handleReload]) // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { @@ -199,6 +233,8 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) // Handler for the "Next" button click const handleNext = async () => { + const abortCtrl = new AbortController() + const frozenUtxosToUpdate = frozenUtxos .filter((utxo) => utxo.checked && !utxo.locktime) .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) @@ -211,7 +247,7 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ]) - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { @@ -225,34 +261,46 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) {t('showUtxos.show_utxo_title')} - -
{t('showUtxos.show_utxo_subtitle')}
- {alert && ( - - setAlert(undefined)} - /> - - )} - - - -
-
- -
-
-
-
- {showFrozenUtxos && ( - - )} -
+ {!isLoading ? ( + +
{t('showUtxos.show_utxo_subtitle')}
+ {alert && ( + + setAlert(undefined)} + /> + + )} + + {frozenUtxos.length > 0 && ( + + +
+
+ +
+
+
+
+ )} + {showFrozenUtxos && ( + + )} +
+ ) : ( +
+
+ )} {t('showUtxos.back_button')} diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 952c54085..157c613c1 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -6,6 +6,7 @@ import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' import ShowUtxos from './ShowUtxos' +import { useTranslation } from 'react-i18next' export type SourceJarSelectorProps = { name: string @@ -32,6 +33,8 @@ export const SourceJarSelector = ({ isLoading, disabled = false, }: SourceJarSelectorProps) => { + const { t } = useTranslation() + const [field] = useField(name) const form = useFormikContext() const [showingUTXOS, setshowingUTXOS] = useState({ @@ -58,7 +61,6 @@ export const SourceJarSelector = ({
{showingUTXOS.show && ( { @@ -74,7 +76,7 @@ export const SourceJarSelector = ({ return (
{ form.setFieldValue(field.name, jarIndex, true) - setshowingUTXOS({ - index: jarIndex.toString(), - show: true, - }) }} />
diff --git a/src/components/jars/Jar.module.css b/src/components/jars/Jar.module.css index d95b72761..dc52e1793 100644 --- a/src/components/jars/Jar.module.css +++ b/src/components/jars/Jar.module.css @@ -95,3 +95,7 @@ .tooltipJarContainer { cursor: zoom-in; } + +.custom_tooltip { + z-index: auto !important; +} diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index a57e4b2bc..f3f0d4d10 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useRef } from 'react' import * as rb from 'react-bootstrap' import classnamesBind from 'classnames/bind' import { useSettings } from '../../context/SettingsContext' @@ -26,12 +26,14 @@ export type SelectableJarProps = JarProps & { variant?: 'default' | 'warning' onClick: (index: JarIndex) => void } - +type showingUTXOS = { index: String; show: boolean } export type SelectableSendJarProps = JarProps & { tooltipText: string isSelectable: boolean isSelected: boolean variant?: 'default' | 'warning' + showingUTXOS: showingUTXOS + setshowingUTXOS: (val: showingUTXOS) => void onClick: (index: JarIndex) => void } @@ -233,6 +235,10 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } +/* + * A jar with index, balance, and a radio-style selection button. + * The jar symbol opens on onClick of radio button. + */ const SelectableSendJar = ({ tooltipText, isSelectable, @@ -240,46 +246,56 @@ const SelectableSendJar = ({ onClick, index, variant = 'default', + showingUTXOS, + setshowingUTXOS, ...jarProps }: SelectableSendJarProps) => { - const [jarIsOpen, setJarIsOpen] = useState(false) - const onMouseOver = () => setJarIsOpen(true) - const onMouseOut = () => setJarIsOpen(false) + const target = useRef(null) + + const handleClick = () => { + if (isSelected && isSelectable) { + setshowingUTXOS({ + index: index.toString(), + show: true, + }) + } + } return ( -
isSelectable && onClick(index)} onMouseOver={onMouseOver} onMouseOut={onMouseOut}> - { - return isSelectable ? {tooltipText} : <> - }} +
+
-
- -
- - {variant === 'warning' && ( -
- -
- )} -
+ + + +
+ isSelectable && onClick(index)} + className={styles.selectionCircle} + disabled={!isSelectable} + /> + {variant === 'warning' && ( +
+ +
+ )}
- +
+ {isSelected && ( + + {(props) => ( + + {tooltipText} + + )} + + )}
) } diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index 33f4e0074..6f4d125f1 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -64,4 +64,4 @@ const useSettingsDispatch = () => { return context.dispatch } -export { SettingsProvider, useSettings, useSettingsDispatch } +export { SettingsProvider, useSettings, useSettingsDispatch, Settings } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index dbe9c304e..e3a81810d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -700,12 +700,13 @@ } }, "showUtxos": { - "show_utxo_title": "Select UTXOS to send", - "show_utxo_subtitle": "The following UTXOs are selected to be sent. Modify if needed.", + "Select_UTXOs": "Select UTXOs", + "show_utxo_title": "Select UTXOs to be considered", + "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", "alert_for_time_locked": "Selected UTXO is Time Locked till", "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", - "back_button": "Back", - "next_button": "Next" + "back_button": "Cancel", + "next_button": "Confirm" } } From e00269086ad45bd1dfbebe260f3b5ba5ea01f380 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 9 Jun 2024 09:29:23 +0530 Subject: [PATCH 05/35] Updated deposit tag , Refactor ShowUtxos & added few comments --- public/sprite.svg | 3 + src/components/Send/ShowUtxos.module.css | 29 ++++++++ src/components/Send/ShowUtxos.tsx | 87 +++++++++++++----------- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/public/sprite.svg b/public/sprite.svg index 6777e65d5..c520136c1 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -364,4 +364,7 @@ + + + diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 11dded072..3cbfb4211 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -16,10 +16,23 @@ cursor: pointer; } +.utxoRowUnMixed { + background-color: none; + padding: 8px 30px; + height: 46px; + color: var(--bs-modal-color) !important; + margin-bottom: 4px; + cursor: pointer; +} + .iconMixed { color: #27ae60; } +.iconUnMixed { + color: var(--bs-modal-color); +} + .iconFrozen { color: #2d9cdb; } @@ -29,6 +42,11 @@ margin-bottom: 4px; } +.iconConfirmationsUnmixed { + color: var(--bs-modal-color); + margin-bottom: 4px; +} + .iconConfirmationsFreeze { color: #2d9cdb; margin-bottom: 4px; @@ -59,6 +77,17 @@ border-radius: 5px !important; } +.utxoTagUnMixed { + white-space: nowrap; + color: #999999; + border: 1px solid #bbbbbb; + background-color: #dedede; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; + margin-left: 4px; +} + .utxoTagUnFreeze { white-space: nowrap; border: 1px solid #27ae60; diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 586ad22c4..724a2e9ca 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -12,6 +12,8 @@ import styles from './ShowUtxos.module.css' import Balance from '../Balance' import classNames from 'classnames' +type tags = 'deposit' | 'non-cj-change' | 'bond' + type UtxoType = { address: Api.BitcoinAddress path: string @@ -26,7 +28,7 @@ type UtxoType = { frozen: boolean utxo: Api.UtxoId locktime?: string - _tags: { tag: string; color: string }[] + _tags: { tag: tags; color: string }[] } type UtxoList = UtxoType[] @@ -71,15 +73,32 @@ const formatConfirmations = (conf: number) => { // Utility function to convert Satoshi to Bitcoin const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) +// Utility function to Identifies Icons +const utxoIcon = (tag: tags, isFrozen: boolean) => { + if (isFrozen && tag === 'bond') return 'timelock' + else if (isFrozen) return 'snowflake' + else if (tag === 'deposit') return 'Unmixed' + else if (tag === 'bond') return 'timelock' + else return 'mixed' +} + // UTXO row component const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - const tags = utxo._tags - const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen - const icon = isFrozen ? 'snowflake' : 'mixed' - const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze + + const tag = utxo._tags[0].tag + const icon = utxoIcon(tag, isFrozen) + + const rowClass = isFrozen ? styles.utxoRowFrozen : icon === 'mixed' ? styles.utxoRowUnfrozen : styles.utxoRowUnMixed + const iconClass = isFrozen ? styles.iconFrozen : icon === 'mixed' ? styles.iconMixed : styles.iconUnMixed + const confirmationClass = isFrozen + ? styles.iconConfirmationsFreeze + : icon === 'mixed' + ? styles.iconConfirmations + : styles.iconConfirmationsUnmixed + const tagClass = isFrozen ? styles.utxoTagFreeze : icon === 'mixed' ? styles.utxoTagUnFreeze : styles.utxoTagUnMixed return ( onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> @@ -96,21 +115,11 @@ const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProp /> - + {address} - + {conf.confirmations} @@ -124,7 +133,7 @@ const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProp /> -
{tags[0].tag}
+
{tag}
@@ -143,18 +152,18 @@ const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListD // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { - const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const settings = useSettings() - - const isHandleReloadExecuted = useRef(false) - const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) const [isLoading, setisLoading] = useState(true) + const { t } = useTranslation() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + const settings = useSettings() + + const isHandleReloadExecuted = useRef(false) + // Load data from wallet info const loadData = useCallback( (walletInfo: WalletInfo) => { @@ -184,20 +193,6 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { [index, t], ) - useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) - - if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else if (unFrozenUtxos.length !== 0) { - setAlert(undefined) - } - }, [frozenUtxos, unFrozenUtxos, t]) - // Reload wallet info const handleReload = useCallback(async () => { const abortCtrl = new AbortController() @@ -211,6 +206,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, [reloadCurrentWalletInfo, loadData]) + //Effect to Reload walletInfo useEffect(() => { if (!isHandleReloadExecuted.current) { handleReload() @@ -218,6 +214,21 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, [handleReload]) + //Effect to set Alert according to the walletInfo + useEffect(() => { + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) + + if (timeLockedUtxo) { + setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { + setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + } else if (unFrozenUtxos.length !== 0) { + setAlert(undefined) + } + }, [frozenUtxos, unFrozenUtxos, t]) + // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { if (type === 'unfrozen') { From 827fff236c423d19329aced476e2a40792b9b0f6 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Fri, 14 Jun 2024 01:34:15 +0530 Subject: [PATCH 06/35] Fixed recommended changes --- src/components/Modal.tsx | 22 ++- src/components/Send/ShowUtxos.module.css | 114 +++-------- src/components/Send/ShowUtxos.tsx | 227 ++++++++++++---------- src/components/Send/SourceJarSelector.tsx | 22 ++- src/components/jars/Jar.tsx | 84 ++------ src/i18n/locales/en/translation.json | 14 +- 6 files changed, 205 insertions(+), 278 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 71dd98de8..1c04322c0 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -10,6 +10,7 @@ type BaseModalProps = { onCancel: () => void backdrop?: rb.ModalProps['backdrop'] size?: rb.ModalProps['size'] + showCloseButtonAndRemoveClassName?: boolean } const BaseModal = ({ isShown, @@ -18,6 +19,7 @@ const BaseModal = ({ onCancel, size, backdrop = 'static', + showCloseButtonAndRemoveClassName = false, }: PropsWithChildren) => { return ( - - {title} + + {title} {children} @@ -65,9 +70,18 @@ const InfoModal = ({ export type ConfirmModalProps = BaseModalProps & { onConfirm: () => void + disabled?: boolean + confirmVariant?: string } -const ConfirmModal = ({ children, onCancel, onConfirm, ...baseModalProps }: PropsWithChildren) => { +const ConfirmModal = ({ + children, + onCancel, + onConfirm, + disabled = false, + confirmVariant = 'outline-dark', + ...baseModalProps +}: PropsWithChildren) => { const { t } = useTranslation() return ( @@ -82,7 +96,7 @@ const ConfirmModal = ({ children, onCancel, onConfirm, ...baseModalProps }: Prop
{t('modal.confirm_button_reject')}
- onConfirm()}> + onConfirm()} disabled={disabled}> {t('modal.confirm_button_accept')} diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 3cbfb4211..45f867279 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -1,124 +1,54 @@ -.utxoRowUnfrozen { - background-color: #27ae600d; - padding: 8px 30px; - height: 46px; +.joinedUtxoAndCjout { + background-color: #27ae600d !important; color: #27ae60 !important; - margin-bottom: 4px; - cursor: pointer; } -.utxoRowFrozen { - background-color: #2d9cdb0d; - padding: 8px 30px; - height: 46px; +.frozenUtxo { + background-color: #2d9cdb0d !important; color: #2d9cdb !important; - margin-bottom: 4px; - cursor: pointer; } -.utxoRowUnMixed { - background-color: none; - padding: 8px 30px; - height: 46px; +.depositUtxo { + background-color: none !important; color: var(--bs-modal-color) !important; - margin-bottom: 4px; - cursor: pointer; } -.iconMixed { - color: #27ae60; -} - -.iconUnMixed { - color: var(--bs-modal-color); -} - -.iconFrozen { - color: #2d9cdb; -} - -.iconConfirmations { - color: #27ae60; - margin-bottom: 4px; -} - -.iconConfirmationsUnmixed { - color: var(--bs-modal-color); - margin-bottom: 4px; -} - -.iconConfirmationsFreeze { - color: #2d9cdb; - margin-bottom: 4px; -} - -.valueColumn { - margin-right: 20px; +.changeAndReuseUtxo { + background-color: #eb57570d !important; + color: #eb5757 !important; } .subTitle { - color: #777777; - border: none; - margin-bottom: 1.5rem; -} - -.NextButton { - width: 47% !important; - height: 3.15rem; - padding: 14px 20px 14px 20px !important; - border-radius: 5px !important; + color: #777777 !important; } -.BackButton { - width: 47% !important; - height: 3.15rem; - padding: 14px 20px 14px 20px !important; - margin-right: 33px !important; - border-radius: 5px !important; -} - -.utxoTagUnMixed { - white-space: nowrap; +.utxoTagDeposit { color: #999999; border: 1px solid #bbbbbb; - background-color: #dedede; + background-color: #dedede !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } -.utxoTagUnFreeze { - white-space: nowrap; +.utxoTagJoinedAndCjout { border: 1px solid #27ae60; - background-color: #c6eed7; + background-color: #c6eed7 !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } .utxoTagFreeze { - white-space: nowrap; border: 1px solid #2d9cdb; - background-color: #bce7ff; + background-color: #bce7ff !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } -.parent-class .utxoTag { - border: 1px solid #27ae60 !important; - border-radius: 0.2rem !important; -} - -.utxoListDisplay { - margin-left: -27px !important; -} - -.modalBody { - padding: 40px !important; +.utxoTagChangeAndReuse { + border: 1px solid #eb5757; + background-color: #fac7c7 !important; + border-radius: 0.35rem; + padding: 0rem 0.25rem; } .squareToggleButton { @@ -127,7 +57,7 @@ height: 22px; border-radius: 3px; border: 1px solid var(--bs-body-color); - margin-top: 3.5px; + margin-top: 0.45rem; } .selected { @@ -141,5 +71,5 @@ height: 22px; border-radius: 3px; border: 1px solid #2d9cdb; - margin-top: 3.5px; + margin-top: 0.45rem; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 724a2e9ca..99565ce8b 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,34 +1,39 @@ import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import classNames from 'classnames' +import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' +import { useTheme } from '@table-library/react-table-library/theme' +import * as TableTypes from '@table-library/react-table-library/types/table' +import * as Api from '../../libs/JmWalletApi' import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' -import Sprite from '../Sprite' import Alert from '../Alert' -import { useTranslation } from 'react-i18next' -import * as Api from '../../libs/JmWalletApi' +import Balance from '../Balance' +import { ConfirmModal } from '../Modal' +import Sprite from '../Sprite' import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' -import Balance from '../Balance' -import classNames from 'classnames' -type tags = 'deposit' | 'non-cj-change' | 'bond' +type Tags = 'deposit' | 'non-cj-change' | 'bond' | 'reused' | 'joined' | 'cj-out' type UtxoType = { address: Api.BitcoinAddress path: string label: string + id: string checked: boolean value: Api.AmountSats tries: number - tries_remaining: number + triesRemaining: number external: boolean mixdepth: number confirmations: number frozen: boolean utxo: Api.UtxoId locktime?: string - _tags: { tag: tags; color: string }[] + tags: { tag: Tags; color: string }[] } type UtxoList = UtxoType[] @@ -43,14 +48,14 @@ interface ShowUtxosProps { interface UtxoRowProps { utxo: UtxoType index: number - onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings } interface UtxoListDisplayProps { utxos: UtxoList - onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings } @@ -74,81 +79,104 @@ const formatConfirmations = (conf: number) => { const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) // Utility function to Identifies Icons -const utxoIcon = (tag: tags, isFrozen: boolean) => { +const utxoIcon = (tag: Tags, isFrozen: boolean) => { if (isFrozen && tag === 'bond') return 'timelock' - else if (isFrozen) return 'snowflake' - else if (tag === 'deposit') return 'Unmixed' - else if (tag === 'bond') return 'timelock' - else return 'mixed' + if (isFrozen) return 'snowflake' + if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'Unmixed' + if (tag === 'bond') return 'timelock' + return 'mixed' +} + +// Utility function to allot classes +const allotClasses = (tag: Tags, isFrozen: boolean) => { + if (isFrozen) return { row: styles.frozenUtxo, tag: styles.utxoTagFreeze } + if (tag === 'deposit') return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } + if (tag === 'joined' || tag === 'cj-out') return { row: styles.joinedUtxoAndCjout, tag: styles.utxoTagJoinedAndCjout } + if (tag === 'non-cj-change' || tag === 'reused') + return { row: styles.changeAndReuseUtxo, tag: styles.utxoTagChangeAndReuse } + return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } -// UTXO row component +// Utxos row component const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - - const tag = utxo._tags[0].tag + const tag = utxo.tags[0].tag const icon = utxoIcon(tag, isFrozen) - - const rowClass = isFrozen ? styles.utxoRowFrozen : icon === 'mixed' ? styles.utxoRowUnfrozen : styles.utxoRowUnMixed - const iconClass = isFrozen ? styles.iconFrozen : icon === 'mixed' ? styles.iconMixed : styles.iconUnMixed - const confirmationClass = isFrozen - ? styles.iconConfirmationsFreeze - : icon === 'mixed' - ? styles.iconConfirmations - : styles.iconConfirmationsUnmixed - const tagClass = isFrozen ? styles.utxoTagFreeze : icon === 'mixed' ? styles.utxoTagUnFreeze : styles.utxoTagUnMixed + const rowAndTagClass = allotClasses(tag, isFrozen) return ( - onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> -
- - - - - - - - {address} - - - {conf.confirmations} - - - - - -
{tag}
-
-
-
-
+ + + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} + /> + + + + + {address} + + + {conf.confirmations} + + + + + +
{tag}
+
+
) }) -// UTXO list display component -const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => ( -
- {utxos.map((utxo, index) => ( - - ))} -
-)) +//Table theme to manage view +const TABLE_THEME = { + Table: ` + font-size: 1rem; + --data-table-library_grid-template-columns: 3.5rem 2.5rem 12rem 2fr 3fr 10rem ; + @media only screen and (min-width: 768px) { + --data-table-library_grid-template-columns: 3.5rem 2.5rem 14rem 5fr 3fr 10rem ; + } + `, + BaseCell: ` + padding: 0.55rem 0.35rem !important; + margin: 0.15rem 0px !important; + `, +} + +//Utxo list display component +const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => { + const tableTheme = useTheme(TABLE_THEME) + + return ( + + {(utxosList: TableTypes.TableProps) => ( + + {utxosList.map((utxo: UtxoType, index: number) => ( + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> + + + ))} + + )} +
+ ) +} // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { @@ -157,7 +185,6 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) const [isLoading, setisLoading] = useState(true) - const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const settings = useSettings() @@ -173,9 +200,8 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const newUtxos = utxos.map((utxo: any) => ({ ...utxo, id: utxo.utxo, - _tags: utxoTags(utxo, walletInfo, t), + tags: utxoTags(utxo, walletInfo, t), })) - const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) @@ -183,7 +209,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { setUnFrozenUtxos(unfrozen) if (unfrozen.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + setAlert({ variant: 'danger', message: t('showUtxos.alertForEmptyUtxos') }) } else { setAlert(undefined) } @@ -195,6 +221,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { // Reload wallet info const handleReload = useCallback(async () => { + setisLoading(true) const abortCtrl = new AbortController() try { const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) @@ -221,17 +248,17 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + setAlert({ variant: 'danger', message: `${t('showUtxos.alertForTimeLocked')} ${timeLockedUtxo.locktime}` }) } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + setAlert({ variant: 'warning', message: t('showUtxos.alertForUnfreezeUtxos'), dismissible: true }) } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } }, [frozenUtxos, unFrozenUtxos, t]) // Handler to toggle UTXO selection - const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { - if (type === 'unfrozen') { + const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unFrozen') => { + if (type === 'unFrozen') { setUnFrozenUtxos((prevUtxos) => prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), ) @@ -242,8 +269,8 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, []) - // Handler for the "Next" button click - const handleNext = async () => { + // Handler for the "confirm" button click + const handleConfirm = async () => { const abortCtrl = new AbortController() const frozenUtxosToUpdate = frozenUtxos @@ -258,7 +285,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ]) - await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) + await handleReload() onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { @@ -268,26 +295,32 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } return ( - - - {t('showUtxos.show_utxo_title')} - + {!isLoading ? ( - -
{t('showUtxos.show_utxo_subtitle')}
+ <> +
{t('showUtxos.showUtxoSubtitle')}
{alert && ( setAlert(undefined)} /> )} {frozenUtxos.length > 0 && ( - +

@@ -305,22 +338,14 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { {showFrozenUtxos && ( )} - + ) : (
)} - - - {t('showUtxos.back_button')} - - - {t('showUtxos.next_button')} - - - + ) } diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 157c613c1..db60388e8 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from 'react' import { useField, useFormikContext } from 'formik' import * as rb from 'react-bootstrap' -import { jarFillLevel, SelectableSendJar } from '../jars/Jar' +import { jarFillLevel, SelectableJar } from '../jars/Jar' import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' @@ -75,8 +75,9 @@ export const SourceJarSelector = ({ {jarBalances.map((it) => { return (
- { + onClick={(jarIndex: number) => { form.setFieldValue(field.name, jarIndex, true) + if ( + it.accountIndex === field.value && + !disabled && + !isLoading && + it.calculatedTotalBalanceInSats > 0 + ) { + setshowingUTXOS({ + index: it.accountIndex.toString(), + show: true, + }) + } }} />
diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index f3f0d4d10..8c267e7b2 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -21,6 +21,8 @@ export type JarProps = { } export type SelectableJarProps = JarProps & { + tooltipText?: string + isOpen?: boolean isSelectable: boolean isSelected: boolean variant?: 'default' | 'warning' @@ -164,6 +166,8 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false, size }: * A jar with index, balance, and a radio-style selection button. */ const SelectableJar = ({ + tooltipText, + isOpen = false, isSelectable, isSelected, onClick, @@ -171,6 +175,7 @@ const SelectableJar = ({ variant = 'default', ...jarProps }: SelectableJarProps) => { + const target = useRef(null) return (
isSelectable && onClick(index)} + ref={target} > - +
)}
+ {isOpen && isSelectable && ( + + {(props) => ( + + {tooltipText} + + )} + + )}
) } @@ -235,69 +250,4 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } -/* - * A jar with index, balance, and a radio-style selection button. - * The jar symbol opens on onClick of radio button. - */ -const SelectableSendJar = ({ - tooltipText, - isSelectable, - isSelected, - onClick, - index, - variant = 'default', - showingUTXOS, - setshowingUTXOS, - ...jarProps -}: SelectableSendJarProps) => { - const target = useRef(null) - - const handleClick = () => { - if (isSelected && isSelectable) { - setshowingUTXOS({ - index: index.toString(), - show: true, - }) - } - } - - return ( -
-
- - - -
- isSelectable && onClick(index)} - className={styles.selectionCircle} - disabled={!isSelectable} - /> - {variant === 'warning' && ( -
- -
- )} -
-
- {isSelected && ( - - {(props) => ( - - {tooltipText} - - )} - - )} -
- ) -} - -export { SelectableSendJar, SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } +export { SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index e3a81810d..c672ba8f5 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -700,13 +700,11 @@ } }, "showUtxos": { - "Select_UTXOs": "Select UTXOs", - "show_utxo_title": "Select UTXOs to be considered", - "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", - "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", - "alert_for_time_locked": "Selected UTXO is Time Locked till", - "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", - "back_button": "Cancel", - "next_button": "Confirm" + "selectUTXOs": "Select UTXOs", + "showUtxoTitle": "Select UTXOs to be considered", + "showUtxoSubtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", + "alertForUnfreezeUtxos": "At least one UTXO is required to perform a transaction", + "alertForTimeLocked": "Selected UTXO is Time Locked till", + "alertForEmptyUtxos": "Please Unfreeze UTXOs to send" } } From 6bebf84efefdd2d817b985d615590ab7021d4cc2 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 26 Jun 2024 13:53:27 +0530 Subject: [PATCH 07/35] Made UTXO list scrollable after 5 entries for better usability --- src/components/Send/ShowUtxos.module.css | 4 +++ src/components/Send/ShowUtxos.tsx | 35 +++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 45f867279..7477f4e60 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -73,3 +73,7 @@ border: 1px solid #2d9cdb; margin-top: 0.45rem; } + +.utxoListDisplayHeight { + max-height: 18rem; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 99565ce8b..cacf31d50 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -47,7 +47,7 @@ interface ShowUtxosProps { interface UtxoRowProps { utxo: UtxoType - index: number + utxoIndex: number onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings @@ -98,22 +98,21 @@ const allotClasses = (tag: Tags, isFrozen: boolean) => { } // Utxos row component -const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { +const UtxoRow = memo(({ utxo, utxoIndex, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) const tag = utxo.tags[0].tag const icon = utxoIcon(tag, isFrozen) const rowAndTagClass = allotClasses(tag, isFrozen) - return ( onToggle(index, isFrozen ? 'frozen' : 'unFrozen')} + onChange={() => onToggle(utxoIndex, isFrozen ? 'frozen' : 'unFrozen')} className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { [styles.selected]: utxo.checked, })} @@ -164,17 +163,21 @@ const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDispla const tableTheme = useTheme(TABLE_THEME) return ( - - {(utxosList: TableTypes.TableProps) => ( - - {utxosList.map((utxo: UtxoType, index: number) => ( - onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> - - - ))} - - )} -
+
+ + {(utxosList: TableTypes.TableProps) => ( + + {utxosList.map((utxo: UtxoType, index: number) => { + return ( + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> + + + ) + })} + + )} +
+
) } From e8623d1864e03e01767aa495af0ef3463597f3f0 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:11:52 +0530 Subject: [PATCH 08/35] Fixed ShowUtxos and reused it in PaymentConfirmModal for UTXOs review --- src/components/PaymentConfirmModal.tsx | 14 +- src/components/Send/SendForm.tsx | 3 +- src/components/Send/ShowUtxos.module.css | 2 +- src/components/Send/ShowUtxos.tsx | 296 ++++++++++++---------- src/components/Send/SourceJarSelector.tsx | 4 +- src/components/Send/index.tsx | 21 +- src/components/jar_details/UtxoList.tsx | 2 +- src/context/WalletContext.tsx | 4 + src/i18n/locales/en/translation.json | 15 +- 9 files changed, 216 insertions(+), 145 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 68c466726..d6af10890 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -64,6 +64,7 @@ interface PaymentDisplayInfo { numCollaborators?: number feeConfigValues?: FeeValues showPrivacyInfo?: boolean + showSelectedUtxos?: boolean } interface PaymentConfirmModalProps extends ConfirmModalProps { @@ -80,6 +81,7 @@ export function PaymentConfirmModal({ numCollaborators, feeConfigValues, showPrivacyInfo = true, + showSelectedUtxos = false, }, children, ...confirmModalProps @@ -207,7 +209,17 @@ export function PaymentConfirmModal({ )} - {children && ( + {showSelectedUtxos && ( + + + {t('show_utxos.selected_utxos')} + + + {children} + + + )} + {!showSelectedUtxos && children && ( {children} diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 7eeaf130b..9f387fbea 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -26,7 +26,7 @@ import { isValidNumCollaborators, } from './helpers' import { AccountBalanceSummary } from '../../context/BalanceSummary' -import { WalletInfo, CurrentWallet } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet, Utxo } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' @@ -214,6 +214,7 @@ export interface SendFormValues { txFee?: TxFee isCoinJoin: boolean numCollaborators?: number + selectedUtxos?: Array } interface InnerSendFormProps { diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 7477f4e60..b97bd0616 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -75,5 +75,5 @@ } .utxoListDisplayHeight { - max-height: 18rem; + max-height: 17.6rem; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index cacf31d50..dd84d3cd3 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,12 +1,19 @@ import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' +import type { TFunction } from 'i18next' import classNames from 'classnames' import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' import { useTheme } from '@table-library/react-table-library/theme' import * as TableTypes from '@table-library/react-table-library/types/table' import * as Api from '../../libs/JmWalletApi' -import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import { + WalletInfo, + CurrentWallet, + useReloadCurrentWalletInfo, + Utxo, + useCurrentWalletInfo, +} from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' import Alert from '../Alert' import Balance from '../Balance' @@ -16,27 +23,7 @@ import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' -type Tags = 'deposit' | 'non-cj-change' | 'bond' | 'reused' | 'joined' | 'cj-out' - -type UtxoType = { - address: Api.BitcoinAddress - path: string - label: string - id: string - checked: boolean - value: Api.AmountSats - tries: number - triesRemaining: number - external: boolean - mixdepth: number - confirmations: number - frozen: boolean - utxo: Api.UtxoId - locktime?: string - tags: { tag: Tags; color: string }[] -} - -type UtxoList = UtxoType[] +type UtxoList = Array interface ShowUtxosProps { wallet: CurrentWallet @@ -46,18 +33,27 @@ interface ShowUtxosProps { } interface UtxoRowProps { - utxo: UtxoType + utxo: Utxo utxoIndex: number - onToggle: (index: number, type: 'frozen' | 'unFrozen') => void + onToggle?: (index: number, isFrozen: boolean) => void isFrozen: boolean settings: Settings + showRadioAndBg: boolean + walletInfo: WalletInfo + t: TFunction } interface UtxoListDisplayProps { - utxos: UtxoList - onToggle: (index: number, type: 'frozen' | 'unFrozen') => void - isFrozen: boolean + utxos: Array + onToggle?: (index: number, isFrozen: boolean) => void settings: Settings + showRadioAndBg?: boolean +} + +interface DividerProps { + isState: boolean + setIsState: (arg: boolean) => void + className?: string } // Utility function to format Bitcoin address @@ -79,7 +75,7 @@ const formatConfirmations = (conf: number) => { const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) // Utility function to Identifies Icons -const utxoIcon = (tag: Tags, isFrozen: boolean) => { +const utxoIcon = (tag: string, isFrozen: boolean) => { if (isFrozen && tag === 'bond') return 'timelock' if (isFrozen) return 'snowflake' if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'Unmixed' @@ -88,7 +84,7 @@ const utxoIcon = (tag: Tags, isFrozen: boolean) => { } // Utility function to allot classes -const allotClasses = (tag: Tags, isFrozen: boolean) => { +const allotClasses = (tag: string, isFrozen: boolean) => { if (isFrozen) return { row: styles.frozenUtxo, tag: styles.utxoTagFreeze } if (tag === 'deposit') return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } if (tag === 'joined' || tag === 'cj-out') return { row: styles.joinedUtxoAndCjout, tag: styles.utxoTagJoinedAndCjout } @@ -98,82 +94,110 @@ const allotClasses = (tag: Tags, isFrozen: boolean) => { } // Utxos row component -const UtxoRow = memo(({ utxo, utxoIndex, onToggle, isFrozen, settings }: UtxoRowProps) => { - const address = formatAddress(utxo.address) - const conf = formatConfirmations(utxo.confirmations) - const value = satsToBtc(utxo.value) - const tag = utxo.tags[0].tag - const icon = utxoIcon(tag, isFrozen) - const rowAndTagClass = allotClasses(tag, isFrozen) - return ( - - - onToggle(utxoIndex, isFrozen ? 'frozen' : 'unFrozen')} - className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { - [styles.selected]: utxo.checked, - })} - /> - - - - - {address} - - - {conf.confirmations} - - - - - -
{tag}
-
-
- ) -}) +const UtxoRow = memo( + ({ utxo, utxoIndex, onToggle, isFrozen, showRadioAndBg, settings, walletInfo, t }: UtxoRowProps) => { + const address = formatAddress(utxo.address) + const conf = formatConfirmations(utxo.confirmations) + const value = satsToBtc(utxo.value) + const tag = utxoTags(utxo, walletInfo, t) + const icon = utxoIcon(tag[0].tag, isFrozen) + const rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + return ( + onToggle && onToggle(utxoIndex, utxo.frozen)} + > + {showRadioAndBg && ( + + { + onToggle && onToggle(utxoIndex, isFrozen) + }} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} + /> + + )} + + + + {address} + + + {conf.confirmations} + + + + + +
{tag[0].tag}
+
+
+ ) + }, +) + +//Utxo list display component +const UtxoListDisplay = ({ utxos, onToggle, settings, showRadioAndBg = true }: UtxoListDisplayProps) => { + const { t } = useTranslation() + const walletInfo = useCurrentWalletInfo() -//Table theme to manage view -const TABLE_THEME = { - Table: ` - font-size: 1rem; - --data-table-library_grid-template-columns: 3.5rem 2.5rem 12rem 2fr 3fr 10rem ; + //Table theme to manage view + const TABLE_THEME = { + Table: ` + font-size: ${showRadioAndBg ? '1rem' : '0.87rem'}; + --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 12rem 2fr 3fr 10rem ' : '2.5rem 10rem 5fr 3fr 7.5rem'}; @media only screen and (min-width: 768px) { - --data-table-library_grid-template-columns: 3.5rem 2.5rem 14rem 5fr 3fr 10rem ; + --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 14rem 5fr 3fr 10rem' : '2.5rem 11rem 5fr 3fr 7.5rem'}; } `, - BaseCell: ` - padding: 0.55rem 0.35rem !important; + BaseCell: ` + padding:${showRadioAndBg ? '0.5rem' : '0.55rem'} 0.35rem !important; margin: 0.15rem 0px !important; `, -} - -//Utxo list display component -const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => { + } const tableTheme = useTheme(TABLE_THEME) return ( -
- - {(utxosList: TableTypes.TableProps) => ( +
+
+ {(utxosList: TableTypes.TableProps) => ( - {utxosList.map((utxo: UtxoType, index: number) => { - return ( - onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> - - - ) - })} + {walletInfo && + utxosList.map((utxo: Utxo, index: number) => { + return ( + + ) + })} )}
@@ -181,6 +205,30 @@ const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDispla ) } +//Component to show the Divider line +const Divider = ({ isState, setIsState, className }: DividerProps) => { + //Effect for getting back to it's original state when components unMounts + useEffect(() => { + return () => { + setIsState(false) + } + }, [setIsState]) + + return ( + + +
+
+ +
+
+
+
+ ) +} + // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const [alert, setAlert] = useState(undefined) @@ -200,19 +248,18 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) const utxos: any = data ? data[1] : [] - const newUtxos = utxos.map((utxo: any) => ({ - ...utxo, - id: utxo.utxo, - tags: utxoTags(utxo, walletInfo, t), - })) - const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) - const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) + const frozen = utxos + .filter((utxo: any) => utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) + const unfrozen = utxos + .filter((utxo: any) => !utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) setFrozenUtxos(frozen) setUnFrozenUtxos(unfrozen) if (unfrozen.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alertForEmptyUtxos') }) + setAlert({ variant: 'danger', message: t('show_utxos.alert_for_empty_utxos') }) } else { setAlert(undefined) } @@ -246,22 +293,22 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { //Effect to set Alert according to the walletInfo useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: Utxo) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: Utxo) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alertForTimeLocked')} ${timeLockedUtxo.locktime}` }) + setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alertForUnfreezeUtxos'), dismissible: true }) + setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } }, [frozenUtxos, unFrozenUtxos, t]) // Handler to toggle UTXO selection - const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unFrozen') => { - if (type === 'unFrozen') { + const handleToggle = useCallback((utxoIndex: number, isFrozen: boolean) => { + if (!isFrozen) { setUnFrozenUtxos((prevUtxos) => prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), ) @@ -303,14 +350,14 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { onConfirm={handleConfirm} disabled={alert?.dismissible || isLoading} isShown={show} - title={t('showUtxos.showUtxoTitle')} + title={t('show_utxos.show_utxo_title')} size="lg" showCloseButtonAndRemoveClassName={true} confirmVariant={'dark'} > {!isLoading ? ( <> -
{t('showUtxos.showUtxoSubtitle')}
+
{t('show_utxos.show_utxo_subtitle')}
{alert && ( { /> )} - + {frozenUtxos.length > 0 && ( - - -
-
- -
-
-
-
+ )} {showFrozenUtxos && ( - + )} ) : ( @@ -352,4 +390,4 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { ) } -export default ShowUtxos +export { ShowUtxos, Divider, UtxoListDisplay, UtxoRow } diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index db60388e8..60c059e0c 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -5,7 +5,7 @@ import { jarFillLevel, SelectableJar } from '../jars/Jar' import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' -import ShowUtxos from './ShowUtxos' +import { ShowUtxos } from './ShowUtxos' import { useTranslation } from 'react-i18next' export type SourceJarSelectorProps = { @@ -76,7 +76,7 @@ export const SourceJarSelector = ({ return (
serviceInfo?.coinjoinInProgress === true, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo?.makerRunning === true, [serviceInfo]) @@ -134,6 +137,7 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() + const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues), @@ -383,8 +387,13 @@ export default function Send({ wallet }: SendProps) { } if (showConfirmSendModal === undefined) { - setShowConfirmSendModal(values) - return + if (walletInfo?.utxosByJar && values.sourceJarIndex !== undefined) { + values.selectedUtxos = walletInfo.utxosByJar[values.sourceJarIndex].filter((utxo) => { + return utxo.frozen === false + }) + setShowConfirmSendModal(values) + return + } } setShowConfirmSendModal(undefined) @@ -517,8 +526,14 @@ export default function Send({ wallet }: SendProps) { isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, + showSelectedUtxos: true, }} - /> + > + + {showSelectedUtxos && showConfirmSendModal.selectedUtxos && ( + + )} + )}
) diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index e782f0800..f6be3702b 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -156,7 +156,7 @@ const toUtxo = (tableNode: TableTypes.TableNode): Utxo => { return utxo as Utxo } -interface UtxoTableRow extends Utxo, TableTypes.TableNode { +interface UtxoTableRow extends Utxo { _icon: JSX.Element _tags: Tag[] _confs: JSX.Element diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 5bfce6abc..5632f7975 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -53,6 +53,9 @@ export type Utxo = { // `locktime` in format "yyyy-MM-dd 00:00:00" // NOTE: it is unparsable with safari Date constructor locktime?: string + id: string + checked?: boolean + tags?: { tag: string; color: string }[] } export type Utxos = Utxo[] @@ -192,6 +195,7 @@ export const groupByJar = (utxos: Utxos): UtxosByJar => { return utxos.reduce((res, utxo) => { const { mixdepth } = utxo res[mixdepth] = res[mixdepth] || [] + utxo.id = utxo.utxo res[mixdepth].push(utxo) return res }, {} as UtxosByJar) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index c672ba8f5..437e3f4cc 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -699,12 +699,13 @@ "utxo_detail_label_status": "Address status" } }, - "showUtxos": { - "selectUTXOs": "Select UTXOs", - "showUtxoTitle": "Select UTXOs to be considered", - "showUtxoSubtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", - "alertForUnfreezeUtxos": "At least one UTXO is required to perform a transaction", - "alertForTimeLocked": "Selected UTXO is Time Locked till", - "alertForEmptyUtxos": "Please Unfreeze UTXOs to send" + "show_utxos": { + "select_utxos": "Select UTXOs", + "selected_utxos": "Selected UTXOs", + "show_utxo_title": "Select UTXOs to be considered", + "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", + "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", + "alert_for_time_locked": "Selected UTXO is Time Locked till", + "alert_for_empty_utxos": "Please Unfreeze UTXOs to send" } } From ba7bb0bc6f998cc3a85fc7663c9746ca3350227f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:25:56 +0530 Subject: [PATCH 09/35] Implemented suggested changes and made it reusable --- src/components/Send/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 558f6eaf5..fcd6b519e 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -526,14 +526,8 @@ export default function Send({ wallet }: SendProps) { isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, - showSelectedUtxos: true, }} - > - - {showSelectedUtxos && showConfirmSendModal.selectedUtxos && ( - - )} - + /> )}
) From 65d573d9d2251980bae2994ce0de3df31f0d64dd Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:31:24 +0530 Subject: [PATCH 10/35] bug fixing --- src/components/Send/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index fcd6b519e..1aae7b658 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -18,8 +18,6 @@ import { useLoadConfigValue } from '../../context/ServiceConfigContext' import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' -import { useSettings } from '../../context/SettingsContext' -import { UtxoListDisplay, Divider } from './ShowUtxos' import { initialNumCollaborators } from './helpers' @@ -92,7 +90,6 @@ export default function Send({ wallet }: SendProps) { const serviceInfo = useServiceInfo() const reloadServiceInfo = useReloadServiceInfo() const loadConfigValue = useLoadConfigValue() - const settings = useSettings() const isCoinjoinInProgress = useMemo(() => serviceInfo?.coinjoinInProgress === true, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo?.makerRunning === true, [serviceInfo]) @@ -137,7 +134,6 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() - const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues), From 1eebac753c0b5b2dbb6efe9fd707e8133d609f11 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 15:22:17 +0530 Subject: [PATCH 11/35] Implemented Show Selected UTXOs before performing transaction --- src/components/Send/index.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 1aae7b658..7a84d22ca 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -18,6 +18,8 @@ import { useLoadConfigValue } from '../../context/ServiceConfigContext' import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' +import { useSettings } from '../../context/SettingsContext' +import { UtxoListDisplay, Divider } from './ShowUtxos' import { initialNumCollaborators } from './helpers' @@ -85,6 +87,7 @@ type SendProps = { export default function Send({ wallet }: SendProps) { const { t } = useTranslation() const walletInfo = useCurrentWalletInfo() + const settings = useSettings() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const serviceInfo = useServiceInfo() @@ -134,6 +137,7 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() + const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues), @@ -522,8 +526,14 @@ export default function Send({ wallet }: SendProps) { isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, + showSelectedUtxos: true, }} - /> + > + + {showSelectedUtxos && showConfirmSendModal.selectedUtxos && ( + + )} + )}
) From dc4bcf4bfbed815df989d08640964835e0cc2c18 Mon Sep 17 00:00:00 2001 From: apX13_ Date: Thu, 27 Jun 2024 21:53:40 +0530 Subject: [PATCH 12/35] empty utxos bug fixed --- src/components/Send/ShowUtxos.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index dd84d3cd3..b56d01aab 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -100,8 +100,14 @@ const UtxoRow = memo( const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) const tag = utxoTags(utxo, walletInfo, t) - const icon = utxoIcon(tag[0].tag, isFrozen) - const rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + let icon, rowAndTagClass + if (tag.length === 0) { + icon = 'Unmixed' + rowAndTagClass = { row: styles.depositUtxo, tag: styles.utxoTagDeposit } + } else { + icon = utxoIcon(tag[0].tag, isFrozen) + rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + } return ( -
{tag[0].tag}
+
{tag.length ? tag[0].tag : ''}
) From 3a0f37844f5e39ff62b649af68dfe2953c13f99f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 2 Jun 2024 12:10:26 +0530 Subject: [PATCH 13/35] Implemented UTXO Selection Modal --- public/sprite.svg | 3 + src/components/Send/SendForm.tsx | 8 +- src/components/Send/ShowUtxos.module.css | 99 ++++++ src/components/Send/ShowUtxos.tsx | 293 ++++++++++++++++++ .../Send/SourceJarSelector.module.css | 1 + src/components/Send/SourceJarSelector.tsx | 72 +++-- src/components/Send/index.tsx | 1 + src/components/jar_details/UtxoList.tsx | 2 +- src/components/jars/Jar.tsx | 67 +++- src/i18n/locales/en/translation.json | 9 + 10 files changed, 534 insertions(+), 21 deletions(-) create mode 100644 src/components/Send/ShowUtxos.module.css create mode 100644 src/components/Send/ShowUtxos.tsx diff --git a/public/sprite.svg b/public/sprite.svg index 67f0b222f..6777e65d5 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -361,4 +361,7 @@ + + + diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 7b16481f9..7eeaf130b 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -26,7 +26,7 @@ import { isValidNumCollaborators, } from './helpers' import { AccountBalanceSummary } from '../../context/BalanceSummary' -import { WalletInfo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' @@ -221,6 +221,7 @@ interface InnerSendFormProps { className?: string isLoading: boolean walletInfo?: WalletInfo + wallet: CurrentWallet loadNewWalletAddress: (props: { signal: AbortSignal; jarIndex: JarIndex }) => Promise minNumCollaborators: number feeConfigValues?: FeeValues @@ -233,6 +234,7 @@ const InnerSendForm = ({ className, isLoading, walletInfo, + wallet, loadNewWalletAddress, minNumCollaborators, feeConfigValues, @@ -272,6 +274,7 @@ const InnerSendForm = ({ name="sourceJarIndex" label={t('send.label_source_jar')} walletInfo={walletInfo} + wallet={wallet} isLoading={isLoading} disabled={disabled} variant={showCoinjoinPreconditionViolationAlert ? 'warning' : 'default'} @@ -375,6 +378,7 @@ type SendFormProps = Omit & { onSubmit: (values: SendFormValues) => Promise formRef?: React.Ref> blurred?: boolean + wallet: CurrentWallet } export const SendForm = ({ @@ -383,6 +387,7 @@ export const SendForm = ({ formRef, blurred = false, walletInfo, + wallet, minNumCollaborators, ...innerProps }: SendFormProps) => { @@ -446,6 +451,7 @@ export const SendForm = ({ props={props} className={blurred ? styles.blurred : undefined} walletInfo={walletInfo} + wallet={wallet} minNumCollaborators={minNumCollaborators} {...innerProps} /> diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css new file mode 100644 index 000000000..d10bdef1f --- /dev/null +++ b/src/components/Send/ShowUtxos.module.css @@ -0,0 +1,99 @@ +/* ShowUtxos.module.css */ + +.utxoRowUnfrozen { + background-color: #27ae600d; + padding: 8px 30px; + height: 46px; + color: #27ae60; + margin-bottom: 4px; + cursor: pointer; +} + +.utxoRowFrozen { + background-color: #2d9cdb0d; + padding: 8px 30px; + height: 46px; + color: #2d9cdb; + margin-bottom: 4px; + cursor: pointer; +} + +.iconMixed { + color: #27ae60; +} + +.iconFrozen { + color: #2d9cdb; +} + +.iconConfirmations { + color: #27ae60; + margin-bottom: 4px; +} + +.iconConfirmationsFreeze { + color: #2d9cdb; + margin-bottom: 4px; +} + +.valueColumn { + margin-right: 20px; +} + +.subTitle { + color: #777777; + border: none; + margin-bottom: 1.5rem; +} + +.NextButton { + width: 47%; + height: 48px; + padding: 14px 20px 14px 20px; + margin-right: 19px; + border-radius: 5px; + opacity: 0px; +} + +.BackButton { + width: 47%; + height: 48px; + padding: 14px 20px 14px 20px; + margin-right: 19px; + border-radius: 5px; + opacity: 0px; +} + +.utxoTagUnFreeze { + white-space: nowrap; + border: 1px solid #27ae60; + background-color: #c6eed7; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; +} + +.utxoTagFreeze { + white-space: nowrap; + border: 1px solid #2d9cdb; + background-color: #bce7ff; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; +} + +.parent-class .utxoTag { + border: 1px solid #27ae60 !important; + border-radius: 0.2rem !important; +} + +.squareToggleButton { + height: 22px !important; + border-radius: 3px !important; +} + +.squareFrozenToggleButton { + height: 22px !important; + border-radius: 3px !important; + border: 1px solid #2d9cdb !important; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx new file mode 100644 index 000000000..18f4afdf6 --- /dev/null +++ b/src/components/Send/ShowUtxos.tsx @@ -0,0 +1,293 @@ +import { useState, useEffect, useCallback } from 'react' +import * as rb from 'react-bootstrap' +import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import Sprite from '../Sprite' +import Alert from '../Alert' +import { useTranslation } from 'react-i18next' +import { TFunction } from 'i18next' +import * as Api from '../../libs/JmWalletApi' +import { utxoTags } from '../jar_details/UtxoList' +import mainStyles from '../MainWalletView.module.css' +import styles from './ShowUtxos.module.css' + +type UtxoType = { + address: Api.BitcoinAddress + path: string + label: string + checked: boolean + value: Api.AmountSats + tries: number + tries_remaining: number + external: boolean + mixdepth: number + confirmations: number + frozen: boolean + utxo: Api.UtxoId + locktime?: string +} + +type UtxoList = UtxoType[] + +interface SignModalProps { + walletInfo: WalletInfo + wallet: CurrentWallet + show: boolean + onHide: () => void + index: String +} + +interface UtxoRowProps { + utxo: UtxoType + index: number + onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + walletInfo: WalletInfo + t: TFunction + isFrozen: boolean +} + +interface UtxoListDisplayProps { + utxos: UtxoList + onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + walletInfo: WalletInfo + t: TFunction + isFrozen: boolean +} + +// Utility function to format Bitcoin address +const formatAddress = (address: string) => `${address.slice(0, 10)}...${address.slice(-8)}` + +// Utility function to format the confirmations +const formatConfirmations = (conf: number) => { + if (conf === 0) return { symbol: 'confs-0', confirmations: conf } + if (conf === 1) return { symbol: 'confs-1', confirmations: conf } + if (conf === 2) return { symbol: 'confs-2', confirmations: conf } + if (conf === 3) return { symbol: 'confs-3', confirmations: conf } + if (conf === 4) return { symbol: 'confs-4', confirmations: conf } + if (conf === 5) return { symbol: 'confs-5', confirmations: conf } + if (conf >= 9999) return { symbol: 'confs-full', confirmations: '9999+' } + return { symbol: 'confs-full', confirmations: conf } +} + +// Utility function to convert Satoshi to Bitcoin +const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) + +const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProps) => { + const address = formatAddress(utxo.address) + const conf = formatConfirmations(utxo.confirmations) + const value = satsToBtc(utxo.value) + const tags = utxoTags(utxo, walletInfo, t) + const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen + const icon = isFrozen ? 'snowflake' : 'mixed' + const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze + + return ( + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> + + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} + className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + /> + + + + + {address} + + + {conf.confirmations} + + {`₿${value}`} + +
{tags[0].tag}
+
+
+ ) +} + +const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( +
+ {utxos.map((utxo, index) => ( + + ))} +
+) + +const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) => { + const abortCtrl = new AbortController() + + const { t } = useTranslation() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + + const [alert, setAlert] = useState() + const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) + const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) + const [frozenUtxos, setFrozenUtxos] = useState([]) + + // Effect to load UTXO data when component mounts or index/walletInfo changes + useEffect(() => { + const loadData = () => { + const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) + const utxos = data ? data[1] : [] + + const frozen = utxos.filter((utxo) => utxo.frozen).map((utxo) => ({ ...utxo, checked: false })) + const unfrozen = utxos.filter((utxo) => !utxo.frozen).map((utxo) => ({ ...utxo, checked: true })) + + setFrozenUtxos(frozen) + setUnFrozenUtxos(unfrozen) + + if (utxos && unfrozen.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos'), dismissible: true }) + } else { + setAlert(undefined) + } + } + + loadData() + }, [index, walletInfo.utxosByJar, t]) + + // Handler to toggle UTXO selection + const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { + if (type === 'unfrozen') { + setUnFrozenUtxos((prevUtxos) => + prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), + ) + } else { + setFrozenUtxos((prevUtxos) => + prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), + ) + } + }, []) + + // Handler for the "Next" button click + const handleNext = async () => { + const frozenUtxosToUpdate = frozenUtxos + .filter((utxo) => utxo.checked && !utxo.locktime) + .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) + const unFrozenUtxosToUpdate = unFrozenUtxos + .filter((utxo) => !utxo.checked) + .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) + + for (const utxo of frozenUtxos) { + if (utxo.checked && utxo.locktime) { + setAlert({ + variant: 'danger', + message: `${t('showUtxos.alert_for_time_locked')} ${utxo.locktime}`, + dismissible: true, + }) + return + } + } + + if (frozenUtxosToUpdate.length >= 1) { + try { + const freezeCalls = frozenUtxosToUpdate.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( + (res) => { + if (!res.ok) { + return Api.Helper.throwError(res) + } + }, + ), + ) + + await Promise.all(freezeCalls) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + return + } + } + + const uncheckedUnfrozen = unFrozenUtxos.filter((utxo) => !utxo.checked) + if (uncheckedUnfrozen.length === unFrozenUtxos.length && frozenUtxosToUpdate.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + return + } + + try { + const unfreezeCalls = unFrozenUtxosToUpdate.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( + (res) => { + if (!res.ok) { + return Api.Helper.throwError(res) + } + }, + ), + ) + + await Promise.all(unfreezeCalls) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + } + + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + onHide() + } + + return ( + + + {t('showUtxos.show_utxo_title')} + + +
{t('showUtxos.show_utxo_subtitle')}
+ {alert && ( + + setAlert(undefined)} + /> + + )} + + + +
+
+ +
+
+
+
+ {showFrozenUtxos && ( + + )} +
+ + + {t('showUtxos.back_button')} + + + {t('showUtxos.next_button')} + + +
+ ) +} + +export default ShowUtxos diff --git a/src/components/Send/SourceJarSelector.module.css b/src/components/Send/SourceJarSelector.module.css index 74f910d85..9dc43eea7 100644 --- a/src/components/Send/SourceJarSelector.module.css +++ b/src/components/Send/SourceJarSelector.module.css @@ -7,6 +7,7 @@ gap: 1rem; color: var(--bs-body-color); margin-bottom: 1.5rem; + margin-top: 2rem; } .sourceJarsPlaceholder { diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 3f77374ae..952c54085 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -1,10 +1,11 @@ -import { useMemo } from 'react' +import { useState, useMemo } from 'react' import { useField, useFormikContext } from 'formik' import * as rb from 'react-bootstrap' -import { jarFillLevel, SelectableJar } from '../jars/Jar' +import { jarFillLevel, SelectableSendJar } from '../jars/Jar' import { noop } from '../../utils' -import { WalletInfo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' +import ShowUtxos from './ShowUtxos' export type SourceJarSelectorProps = { name: string @@ -12,20 +13,31 @@ export type SourceJarSelectorProps = { className?: string variant: 'default' | 'warning' walletInfo?: WalletInfo + wallet: CurrentWallet isLoading: boolean disabled?: boolean } +interface showingUtxosProps { + index: String + show: boolean +} + export const SourceJarSelector = ({ name, label, walletInfo, + wallet, variant, isLoading, disabled = false, }: SourceJarSelectorProps) => { const [field] = useField(name) const form = useFormikContext() + const [showingUTXOS, setshowingUTXOS] = useState({ + index: '', + show: false, + }) const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -44,22 +56,46 @@ export const SourceJarSelector = ({ ) : (
- {jarBalances.map((it) => ( - 0} - isSelected={it.accountIndex === field.value} - fillLevel={jarFillLevel( - it.calculatedTotalBalanceInSats, - walletInfo.balanceSummary.calculatedTotalBalanceInSats, - )} - variant={it.accountIndex === field.value ? variant : undefined} - onClick={(jarIndex) => form.setFieldValue(field.name, jarIndex, true)} + {showingUTXOS.show && ( + { + setshowingUTXOS({ + index: '', + show: false, + }) + }} + index={showingUTXOS.index} /> - ))} + )} + {jarBalances.map((it) => { + return ( +
+ 0} + isSelected={it.accountIndex === field.value} + fillLevel={jarFillLevel( + it.calculatedTotalBalanceInSats, + walletInfo.balanceSummary.calculatedTotalBalanceInSats, + )} + variant={it.accountIndex === field.value ? variant : undefined} + onClick={(jarIndex) => { + form.setFieldValue(field.name, jarIndex, true) + setshowingUTXOS({ + index: jarIndex.toString(), + show: true, + }) + }} + /> +
+ ) + })}
)} diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 3fa33fd5d..e5f920062 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -480,6 +480,7 @@ export default function Send({ wallet }: SendProps) { disabled={isOperationDisabled} isLoading={isLoading} walletInfo={walletInfo} + wallet={wallet} minNumCollaborators={minNumCollaborators} loadNewWalletAddress={loadNewWalletAddress} feeConfigValues={feeConfigValues} diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index 32219a307..e782f0800 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -37,7 +37,7 @@ const ADDRESS_STATUS_COLORS: { [key: string]: string } = { type Tag = { tag: string; color: string } -const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction): Tag[] => { +export const utxoTags = (utxo: Utxo, walletInfo: WalletInfo, t: TFunction): Tag[] => { const rawStatus = walletInfo.addressSummary[utxo.address]?.status let status: string | null = null diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index 7da0271ac..bf97fd26f 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -27,6 +27,14 @@ export type SelectableJarProps = JarProps & { onClick: (index: JarIndex) => void } +export type SelectableSendJarProps = JarProps & { + tooltipText: string + isSelectable: boolean + isSelected: boolean + variant?: 'default' | 'warning' + onClick: (index: JarIndex) => void +} + export type OpenableJarProps = Omit & { tooltipText: string onClick: () => void @@ -225,4 +233,61 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } -export { SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } +const SelectableSendJar = ({ + tooltipText, + isSelectable, + isSelected, + onClick, + index, + variant = 'default', + ...jarProps +}: SelectableSendJarProps) => { + const [jarIsOpen, setJarIsOpen] = useState(false) + const onMouseOver = () => setJarIsOpen(true) + const onMouseOut = () => setJarIsOpen(false) + + return ( +
isSelectable && onClick(index)} onMouseOver={onMouseOver} onMouseOut={onMouseOut}> + { + return isSelectable ? {tooltipText} : <> + }} + > +
+ +
+ isSelectable && onClick(index)} + className={styles.selectionCircle} + disabled={!isSelectable} + /> + {variant === 'warning' && ( +
+ +
+ )} +
+
+
+
+ ) +} + +export { SelectableSendJar, SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 1657b0003..24c217c7d 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -697,5 +697,14 @@ "utxo_detail_label_locktime": "Locktime", "utxo_detail_label_status": "Address status" } + }, + "showUtxos": { + "show_utxo_title": "Select UTXOS to send", + "show_utxo_subtitle": "The following UTXOs are selected to be sent. Modify if needed.", + "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", + "alert_for_time_locked": "Selected UTXO is Time Locked till", + "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", + "back_button": "Back", + "next_button": "Next" } } From 27fca6df7a37e7dc830ee5d6b0e7394959253a22 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Mon, 3 Jun 2024 21:01:02 +0530 Subject: [PATCH 14/35] Done with the suggested changes in ShowUtxos --- src/components/Send/ShowUtxos.module.css | 5 ++ src/components/Send/ShowUtxos.tsx | 86 +++++++----------------- 2 files changed, 31 insertions(+), 60 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index d10bdef1f..25720d45c 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -97,3 +97,8 @@ border-radius: 3px !important; border: 1px solid #2d9cdb !important; } + +.utxoListDisplay { + margin-left: -20px; + margin-right: 20px; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 18f4afdf6..8f53b79b7 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -115,7 +115,7 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp } const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( -
+
{utxos.map((utxo, index) => ( { + const frozenUtxosToUpdate = frozenUtxos.filter((utxo) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo) => utxo.checked && utxo.locktime) + const noUnfrozenUtxos = unFrozenUtxos.length === 0 + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo) => !utxo.checked) + + if (timeLockedUtxo) { + setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + } else if (noUnfrozenUtxos) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { + setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + } else { + setAlert(undefined) + } + }, [unFrozenUtxos, frozenUtxos, t]) + // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { if (type === 'unfrozen') { @@ -185,64 +197,18 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) .filter((utxo) => !utxo.checked) .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) - for (const utxo of frozenUtxos) { - if (utxo.checked && utxo.locktime) { - setAlert({ - variant: 'danger', - message: `${t('showUtxos.alert_for_time_locked')} ${utxo.locktime}`, - dismissible: true, - }) - return - } - } - - if (frozenUtxosToUpdate.length >= 1) { - try { - const freezeCalls = frozenUtxosToUpdate.map((utxo) => - Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( - (res) => { - if (!res.ok) { - return Api.Helper.throwError(res) - } - }, - ), - ) - - await Promise.all(freezeCalls) - } catch (err: any) { - if (!abortCtrl.signal.aborted) { - setAlert({ variant: 'danger', message: err.message, dismissible: true }) - } - return - } - } - - const uncheckedUnfrozen = unFrozenUtxos.filter((utxo) => !utxo.checked) - if (uncheckedUnfrozen.length === unFrozenUtxos.length && frozenUtxosToUpdate.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - return - } - try { - const unfreezeCalls = unFrozenUtxosToUpdate.map((utxo) => - Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: utxo.freeze }).then( - (res) => { - if (!res.ok) { - return Api.Helper.throwError(res) - } - }, - ), - ) - - await Promise.all(unfreezeCalls) + await Promise.all([ + ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ]) + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { setAlert({ variant: 'danger', message: err.message, dismissible: true }) } } - - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) - onHide() } return ( @@ -282,7 +248,7 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) {t('showUtxos.back_button')} - + {t('showUtxos.next_button')} From 276e9a83f2fad2a7aa1f40f5a8de6371a7d54056 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Mon, 3 Jun 2024 21:42:57 +0530 Subject: [PATCH 15/35] minor bug fixing --- src/components/Send/ShowUtxos.module.css | 7 +-- src/components/Send/ShowUtxos.tsx | 67 ++++++++++++++---------- src/components/jars/Jar.tsx | 8 +-- 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 25720d45c..12fa86c08 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -48,20 +48,17 @@ .NextButton { width: 47%; - height: 48px; + height: 3.15rem; padding: 14px 20px 14px 20px; - margin-right: 19px; border-radius: 5px; - opacity: 0px; } .BackButton { width: 47%; - height: 48px; + height: 3.15rem; padding: 14px 20px 14px 20px; margin-right: 19px; border-radius: 5px; - opacity: 0px; } .utxoTagUnFreeze { diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 8f53b79b7..b3e660808 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -82,40 +82,49 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp return ( onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> - - onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} - className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} - /> - - - - - {address} - - - {conf.confirmations} - - {`₿${value}`} - -
{tags[0].tag}
-
+
+ + + onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} + className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + /> + + + + + {address} + + + {conf.confirmations} + + {`₿${value}`} + +
{tags[0].tag}
+
+
+
) } const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( -
+
{utxos.map((utxo, index) => (
- isSelectable && onClick(index)} - className={styles.selectionCircle} - disabled={!isSelectable} - /> + {variant === 'warning' && (
From 80c32f9a99c0da7a985aabc83a2e4c33f018e32e Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sat, 8 Jun 2024 18:12:46 +0530 Subject: [PATCH 16/35] Fixed Undefined tag issue , Refactor Jar Selection logic & some minor css --- src/components/Balance.tsx | 19 +- src/components/Send/ShowUtxos.module.css | 55 +++--- src/components/Send/ShowUtxos.tsx | 210 +++++++++++++--------- src/components/Send/SourceJarSelector.tsx | 12 +- src/components/jars/Jar.module.css | 4 + src/components/jars/Jar.tsx | 88 +++++---- src/context/SettingsContext.tsx | 2 +- src/i18n/locales/en/translation.json | 9 +- 8 files changed, 247 insertions(+), 152 deletions(-) diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 880d0f67e..645ee00a0 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -39,23 +39,28 @@ interface BalanceComponentProps { symbol?: JSX.Element showSymbol?: boolean frozen?: boolean + isColorChange?: boolean + frozenSymbol?: boolean } const BalanceComponent = ({ symbol, showSymbol = true, frozen = false, + isColorChange = false, + frozenSymbol = true, children, }: PropsWithChildren) => { return ( {children} {showSymbol && symbol} - {frozen && FROZEN_SYMBOL} + {frozen && frozenSymbol && FROZEN_SYMBOL} ) } @@ -75,7 +80,9 @@ const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => { return ( & { value: number const SatsBalance = ({ value, ...props }: SatsBalanceProps) => { return ( - + {formatSats(value)} diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 12fa86c08..11dded072 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -1,10 +1,8 @@ -/* ShowUtxos.module.css */ - .utxoRowUnfrozen { background-color: #27ae600d; padding: 8px 30px; height: 46px; - color: #27ae60; + color: #27ae60 !important; margin-bottom: 4px; cursor: pointer; } @@ -13,7 +11,7 @@ background-color: #2d9cdb0d; padding: 8px 30px; height: 46px; - color: #2d9cdb; + color: #2d9cdb !important; margin-bottom: 4px; cursor: pointer; } @@ -47,18 +45,18 @@ } .NextButton { - width: 47%; + width: 47% !important; height: 3.15rem; - padding: 14px 20px 14px 20px; - border-radius: 5px; + padding: 14px 20px 14px 20px !important; + border-radius: 5px !important; } .BackButton { - width: 47%; + width: 47% !important; height: 3.15rem; - padding: 14px 20px 14px 20px; - margin-right: 19px; - border-radius: 5px; + padding: 14px 20px 14px 20px !important; + margin-right: 33px !important; + border-radius: 5px !important; } .utxoTagUnFreeze { @@ -68,6 +66,7 @@ border-radius: 0.35rem; padding: 0rem 0.25rem; display: inline-block; + margin-left: 4px; } .utxoTagFreeze { @@ -77,6 +76,7 @@ border-radius: 0.35rem; padding: 0rem 0.25rem; display: inline-block; + margin-left: 4px; } .parent-class .utxoTag { @@ -84,18 +84,33 @@ border-radius: 0.2rem !important; } +.utxoListDisplay { + margin-left: -27px !important; +} + +.modalBody { + padding: 40px !important; +} + .squareToggleButton { - height: 22px !important; - border-radius: 3px !important; + appearance: none; + width: 22px; + height: 22px; + border-radius: 3px; + border: 1px solid var(--bs-body-color); + margin-top: 3.5px; } -.squareFrozenToggleButton { - height: 22px !important; - border-radius: 3px !important; - border: 1px solid #2d9cdb !important; +.selected { + visibility: visible !important; + background-color: var(--bs-body-color); } -.utxoListDisplay { - margin-left: -20px; - margin-right: 20px; +.squareFrozenToggleButton { + appearance: none; + width: 22px; + height: 22px; + border-radius: 3px; + border: 1px solid #2d9cdb; + margin-top: 3.5px; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index b3e660808..586ad22c4 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,14 +1,16 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import { useSettings, Settings } from '../../context/SettingsContext' import Sprite from '../Sprite' import Alert from '../Alert' import { useTranslation } from 'react-i18next' -import { TFunction } from 'i18next' import * as Api from '../../libs/JmWalletApi' import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' +import Balance from '../Balance' +import classNames from 'classnames' type UtxoType = { address: Api.BitcoinAddress @@ -24,12 +26,12 @@ type UtxoType = { frozen: boolean utxo: Api.UtxoId locktime?: string + _tags: { tag: string; color: string }[] } type UtxoList = UtxoType[] -interface SignModalProps { - walletInfo: WalletInfo +interface ShowUtxosProps { wallet: CurrentWallet show: boolean onHide: () => void @@ -40,17 +42,15 @@ interface UtxoRowProps { utxo: UtxoType index: number onToggle: (index: number, type: 'frozen' | 'unfrozen') => void - walletInfo: WalletInfo - t: TFunction isFrozen: boolean + settings: Settings } interface UtxoListDisplayProps { utxos: UtxoList onToggle: (index: number, type: 'frozen' | 'unfrozen') => void - walletInfo: WalletInfo - t: TFunction isFrozen: boolean + settings: Settings } // Utility function to format Bitcoin address @@ -71,11 +71,12 @@ const formatConfirmations = (conf: number) => { // Utility function to convert Satoshi to Bitcoin const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) -const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProps) => { +// UTXO row component +const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - const tags = utxoTags(utxo, walletInfo, t) + const tags = utxo._tags const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen const icon = isFrozen ? 'snowflake' : 'mixed' const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze @@ -85,14 +86,13 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp
- onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} - className={isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton} + defaultChecked={utxo.checked} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} /> @@ -113,7 +113,16 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp /> {conf.confirmations} - {`₿${value}`} + + +
{tags[0].tag}
@@ -121,68 +130,93 @@ const UtxoRow = ({ utxo, index, onToggle, walletInfo, t, isFrozen }: UtxoRowProp
) -} +}) -const UtxoListDisplay = ({ utxos, onToggle, walletInfo, t, isFrozen }: UtxoListDisplayProps) => ( +// UTXO list display component +const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => (
{utxos.map((utxo, index) => ( - + ))}
-) - -const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) => { - const abortCtrl = new AbortController() +)) +// Main component to show UTXOs +const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + const settings = useSettings() - const [alert, setAlert] = useState() + const isHandleReloadExecuted = useRef(false) + + const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) + const [isLoading, setisLoading] = useState(true) - // Effect to load UTXO data when component mounts or index/walletInfo changes - useEffect(() => { - const loadData = () => { + // Load data from wallet info + const loadData = useCallback( + (walletInfo: WalletInfo) => { const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) - const utxos = data ? data[1] : [] + const utxos: any = data ? data[1] : [] + + const newUtxos = utxos.map((utxo: any) => ({ + ...utxo, + id: utxo.utxo, + _tags: utxoTags(utxo, walletInfo, t), + })) - const frozen = utxos.filter((utxo) => utxo.frozen).map((utxo) => ({ ...utxo, checked: false })) - const unfrozen = utxos.filter((utxo) => !utxo.frozen).map((utxo) => ({ ...utxo, checked: true })) + const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) + const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) setFrozenUtxos(frozen) setUnFrozenUtxos(unfrozen) - } - loadData() - }, [index, walletInfo.utxosByJar, t]) + if (unfrozen.length === 0) { + setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + } else { + setAlert(undefined) + } + + setisLoading(false) + }, + [index, t], + ) - //Effect to set Warnings & Alert useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo) => utxo.checked && utxo.locktime) - const noUnfrozenUtxos = unFrozenUtxos.length === 0 - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo) => !utxo.checked) + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) if (timeLockedUtxo) { setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (noUnfrozenUtxos) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else { + } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } - }, [unFrozenUtxos, frozenUtxos, t]) + }, [frozenUtxos, unFrozenUtxos, t]) + + // Reload wallet info + const handleReload = useCallback(async () => { + const abortCtrl = new AbortController() + try { + const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) + loadData(walletInfo) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + } + }, [reloadCurrentWalletInfo, loadData]) + + useEffect(() => { + if (!isHandleReloadExecuted.current) { + handleReload() + isHandleReloadExecuted.current = true + } + }, [handleReload]) // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { @@ -199,6 +233,8 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) // Handler for the "Next" button click const handleNext = async () => { + const abortCtrl = new AbortController() + const frozenUtxosToUpdate = frozenUtxos .filter((utxo) => utxo.checked && !utxo.locktime) .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) @@ -211,7 +247,7 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ]) - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { @@ -225,34 +261,46 @@ const ShowUtxos = ({ walletInfo, wallet, show, onHide, index }: SignModalProps) {t('showUtxos.show_utxo_title')} - -
{t('showUtxos.show_utxo_subtitle')}
- {alert && ( - - setAlert(undefined)} - /> - - )} - - - -
-
- -
-
-
-
- {showFrozenUtxos && ( - - )} -
+ {!isLoading ? ( + +
{t('showUtxos.show_utxo_subtitle')}
+ {alert && ( + + setAlert(undefined)} + /> + + )} + + {frozenUtxos.length > 0 && ( + + +
+
+ +
+
+
+
+ )} + {showFrozenUtxos && ( + + )} +
+ ) : ( +
+
+ )} {t('showUtxos.back_button')} diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 952c54085..157c613c1 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -6,6 +6,7 @@ import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' import ShowUtxos from './ShowUtxos' +import { useTranslation } from 'react-i18next' export type SourceJarSelectorProps = { name: string @@ -32,6 +33,8 @@ export const SourceJarSelector = ({ isLoading, disabled = false, }: SourceJarSelectorProps) => { + const { t } = useTranslation() + const [field] = useField(name) const form = useFormikContext() const [showingUTXOS, setshowingUTXOS] = useState({ @@ -58,7 +61,6 @@ export const SourceJarSelector = ({
{showingUTXOS.show && ( { @@ -74,7 +76,7 @@ export const SourceJarSelector = ({ return (
{ form.setFieldValue(field.name, jarIndex, true) - setshowingUTXOS({ - index: jarIndex.toString(), - show: true, - }) }} />
diff --git a/src/components/jars/Jar.module.css b/src/components/jars/Jar.module.css index d95b72761..dc52e1793 100644 --- a/src/components/jars/Jar.module.css +++ b/src/components/jars/Jar.module.css @@ -95,3 +95,7 @@ .tooltipJarContainer { cursor: zoom-in; } + +.custom_tooltip { + z-index: auto !important; +} diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index a57e4b2bc..f3f0d4d10 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useRef } from 'react' import * as rb from 'react-bootstrap' import classnamesBind from 'classnames/bind' import { useSettings } from '../../context/SettingsContext' @@ -26,12 +26,14 @@ export type SelectableJarProps = JarProps & { variant?: 'default' | 'warning' onClick: (index: JarIndex) => void } - +type showingUTXOS = { index: String; show: boolean } export type SelectableSendJarProps = JarProps & { tooltipText: string isSelectable: boolean isSelected: boolean variant?: 'default' | 'warning' + showingUTXOS: showingUTXOS + setshowingUTXOS: (val: showingUTXOS) => void onClick: (index: JarIndex) => void } @@ -233,6 +235,10 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } +/* + * A jar with index, balance, and a radio-style selection button. + * The jar symbol opens on onClick of radio button. + */ const SelectableSendJar = ({ tooltipText, isSelectable, @@ -240,46 +246,56 @@ const SelectableSendJar = ({ onClick, index, variant = 'default', + showingUTXOS, + setshowingUTXOS, ...jarProps }: SelectableSendJarProps) => { - const [jarIsOpen, setJarIsOpen] = useState(false) - const onMouseOver = () => setJarIsOpen(true) - const onMouseOut = () => setJarIsOpen(false) + const target = useRef(null) + + const handleClick = () => { + if (isSelected && isSelectable) { + setshowingUTXOS({ + index: index.toString(), + show: true, + }) + } + } return ( -
isSelectable && onClick(index)} onMouseOver={onMouseOver} onMouseOut={onMouseOut}> - { - return isSelectable ? {tooltipText} : <> - }} +
+
-
- -
- - {variant === 'warning' && ( -
- -
- )} -
+ + + +
+ isSelectable && onClick(index)} + className={styles.selectionCircle} + disabled={!isSelectable} + /> + {variant === 'warning' && ( +
+ +
+ )}
- +
+ {isSelected && ( + + {(props) => ( + + {tooltipText} + + )} + + )}
) } diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index 33f4e0074..6f4d125f1 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -64,4 +64,4 @@ const useSettingsDispatch = () => { return context.dispatch } -export { SettingsProvider, useSettings, useSettingsDispatch } +export { SettingsProvider, useSettings, useSettingsDispatch, Settings } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 24c217c7d..2f9aad87e 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -699,12 +699,13 @@ } }, "showUtxos": { - "show_utxo_title": "Select UTXOS to send", - "show_utxo_subtitle": "The following UTXOs are selected to be sent. Modify if needed.", + "Select_UTXOs": "Select UTXOs", + "show_utxo_title": "Select UTXOs to be considered", + "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", "alert_for_time_locked": "Selected UTXO is Time Locked till", "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", - "back_button": "Back", - "next_button": "Next" + "back_button": "Cancel", + "next_button": "Confirm" } } From b4df045c1a9f98747934e7717fed4284577959a5 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Sun, 9 Jun 2024 09:29:23 +0530 Subject: [PATCH 17/35] Updated deposit tag , Refactor ShowUtxos & added few comments --- public/sprite.svg | 3 + src/components/Send/ShowUtxos.module.css | 29 ++++++++ src/components/Send/ShowUtxos.tsx | 87 +++++++++++++----------- 3 files changed, 81 insertions(+), 38 deletions(-) diff --git a/public/sprite.svg b/public/sprite.svg index 6777e65d5..c520136c1 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -364,4 +364,7 @@ + + + diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 11dded072..3cbfb4211 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -16,10 +16,23 @@ cursor: pointer; } +.utxoRowUnMixed { + background-color: none; + padding: 8px 30px; + height: 46px; + color: var(--bs-modal-color) !important; + margin-bottom: 4px; + cursor: pointer; +} + .iconMixed { color: #27ae60; } +.iconUnMixed { + color: var(--bs-modal-color); +} + .iconFrozen { color: #2d9cdb; } @@ -29,6 +42,11 @@ margin-bottom: 4px; } +.iconConfirmationsUnmixed { + color: var(--bs-modal-color); + margin-bottom: 4px; +} + .iconConfirmationsFreeze { color: #2d9cdb; margin-bottom: 4px; @@ -59,6 +77,17 @@ border-radius: 5px !important; } +.utxoTagUnMixed { + white-space: nowrap; + color: #999999; + border: 1px solid #bbbbbb; + background-color: #dedede; + border-radius: 0.35rem; + padding: 0rem 0.25rem; + display: inline-block; + margin-left: 4px; +} + .utxoTagUnFreeze { white-space: nowrap; border: 1px solid #27ae60; diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 586ad22c4..724a2e9ca 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -12,6 +12,8 @@ import styles from './ShowUtxos.module.css' import Balance from '../Balance' import classNames from 'classnames' +type tags = 'deposit' | 'non-cj-change' | 'bond' + type UtxoType = { address: Api.BitcoinAddress path: string @@ -26,7 +28,7 @@ type UtxoType = { frozen: boolean utxo: Api.UtxoId locktime?: string - _tags: { tag: string; color: string }[] + _tags: { tag: tags; color: string }[] } type UtxoList = UtxoType[] @@ -71,15 +73,32 @@ const formatConfirmations = (conf: number) => { // Utility function to convert Satoshi to Bitcoin const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) +// Utility function to Identifies Icons +const utxoIcon = (tag: tags, isFrozen: boolean) => { + if (isFrozen && tag === 'bond') return 'timelock' + else if (isFrozen) return 'snowflake' + else if (tag === 'deposit') return 'Unmixed' + else if (tag === 'bond') return 'timelock' + else return 'mixed' +} + // UTXO row component const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - const tags = utxo._tags - const rowClass = isFrozen ? styles.utxoRowFrozen : styles.utxoRowUnfrozen - const icon = isFrozen ? 'snowflake' : 'mixed' - const tagClass = isFrozen ? styles.utxoTagFreeze : styles.utxoTagUnFreeze + + const tag = utxo._tags[0].tag + const icon = utxoIcon(tag, isFrozen) + + const rowClass = isFrozen ? styles.utxoRowFrozen : icon === 'mixed' ? styles.utxoRowUnfrozen : styles.utxoRowUnMixed + const iconClass = isFrozen ? styles.iconFrozen : icon === 'mixed' ? styles.iconMixed : styles.iconUnMixed + const confirmationClass = isFrozen + ? styles.iconConfirmationsFreeze + : icon === 'mixed' + ? styles.iconConfirmations + : styles.iconConfirmationsUnmixed + const tagClass = isFrozen ? styles.utxoTagFreeze : icon === 'mixed' ? styles.utxoTagUnFreeze : styles.utxoTagUnMixed return ( onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> @@ -96,21 +115,11 @@ const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProp /> - + {address} - + {conf.confirmations} @@ -124,7 +133,7 @@ const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProp /> -
{tags[0].tag}
+
{tag}
@@ -143,18 +152,18 @@ const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListD // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { - const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const settings = useSettings() - - const isHandleReloadExecuted = useRef(false) - const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) const [isLoading, setisLoading] = useState(true) + const { t } = useTranslation() + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + const settings = useSettings() + + const isHandleReloadExecuted = useRef(false) + // Load data from wallet info const loadData = useCallback( (walletInfo: WalletInfo) => { @@ -184,20 +193,6 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { [index, t], ) - useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) - - if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else if (unFrozenUtxos.length !== 0) { - setAlert(undefined) - } - }, [frozenUtxos, unFrozenUtxos, t]) - // Reload wallet info const handleReload = useCallback(async () => { const abortCtrl = new AbortController() @@ -211,6 +206,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, [reloadCurrentWalletInfo, loadData]) + //Effect to Reload walletInfo useEffect(() => { if (!isHandleReloadExecuted.current) { handleReload() @@ -218,6 +214,21 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, [handleReload]) + //Effect to set Alert according to the walletInfo + useEffect(() => { + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) + + if (timeLockedUtxo) { + setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { + setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + } else if (unFrozenUtxos.length !== 0) { + setAlert(undefined) + } + }, [frozenUtxos, unFrozenUtxos, t]) + // Handler to toggle UTXO selection const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { if (type === 'unfrozen') { From 40edf9e35a692875d9107f96c224b3802a5f330f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Fri, 14 Jun 2024 01:34:15 +0530 Subject: [PATCH 18/35] Fixed recommended changes --- src/components/Modal.tsx | 22 ++- src/components/Send/ShowUtxos.module.css | 114 +++-------- src/components/Send/ShowUtxos.tsx | 227 ++++++++++++---------- src/components/Send/SourceJarSelector.tsx | 22 ++- src/components/jars/Jar.tsx | 84 ++------ src/i18n/locales/en/translation.json | 14 +- 6 files changed, 205 insertions(+), 278 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 71dd98de8..1c04322c0 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -10,6 +10,7 @@ type BaseModalProps = { onCancel: () => void backdrop?: rb.ModalProps['backdrop'] size?: rb.ModalProps['size'] + showCloseButtonAndRemoveClassName?: boolean } const BaseModal = ({ isShown, @@ -18,6 +19,7 @@ const BaseModal = ({ onCancel, size, backdrop = 'static', + showCloseButtonAndRemoveClassName = false, }: PropsWithChildren) => { return ( - - {title} + + {title} {children} @@ -65,9 +70,18 @@ const InfoModal = ({ export type ConfirmModalProps = BaseModalProps & { onConfirm: () => void + disabled?: boolean + confirmVariant?: string } -const ConfirmModal = ({ children, onCancel, onConfirm, ...baseModalProps }: PropsWithChildren) => { +const ConfirmModal = ({ + children, + onCancel, + onConfirm, + disabled = false, + confirmVariant = 'outline-dark', + ...baseModalProps +}: PropsWithChildren) => { const { t } = useTranslation() return ( @@ -82,7 +96,7 @@ const ConfirmModal = ({ children, onCancel, onConfirm, ...baseModalProps }: Prop
{t('modal.confirm_button_reject')}
- onConfirm()}> + onConfirm()} disabled={disabled}> {t('modal.confirm_button_accept')} diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 3cbfb4211..45f867279 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -1,124 +1,54 @@ -.utxoRowUnfrozen { - background-color: #27ae600d; - padding: 8px 30px; - height: 46px; +.joinedUtxoAndCjout { + background-color: #27ae600d !important; color: #27ae60 !important; - margin-bottom: 4px; - cursor: pointer; } -.utxoRowFrozen { - background-color: #2d9cdb0d; - padding: 8px 30px; - height: 46px; +.frozenUtxo { + background-color: #2d9cdb0d !important; color: #2d9cdb !important; - margin-bottom: 4px; - cursor: pointer; } -.utxoRowUnMixed { - background-color: none; - padding: 8px 30px; - height: 46px; +.depositUtxo { + background-color: none !important; color: var(--bs-modal-color) !important; - margin-bottom: 4px; - cursor: pointer; } -.iconMixed { - color: #27ae60; -} - -.iconUnMixed { - color: var(--bs-modal-color); -} - -.iconFrozen { - color: #2d9cdb; -} - -.iconConfirmations { - color: #27ae60; - margin-bottom: 4px; -} - -.iconConfirmationsUnmixed { - color: var(--bs-modal-color); - margin-bottom: 4px; -} - -.iconConfirmationsFreeze { - color: #2d9cdb; - margin-bottom: 4px; -} - -.valueColumn { - margin-right: 20px; +.changeAndReuseUtxo { + background-color: #eb57570d !important; + color: #eb5757 !important; } .subTitle { - color: #777777; - border: none; - margin-bottom: 1.5rem; -} - -.NextButton { - width: 47% !important; - height: 3.15rem; - padding: 14px 20px 14px 20px !important; - border-radius: 5px !important; + color: #777777 !important; } -.BackButton { - width: 47% !important; - height: 3.15rem; - padding: 14px 20px 14px 20px !important; - margin-right: 33px !important; - border-radius: 5px !important; -} - -.utxoTagUnMixed { - white-space: nowrap; +.utxoTagDeposit { color: #999999; border: 1px solid #bbbbbb; - background-color: #dedede; + background-color: #dedede !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } -.utxoTagUnFreeze { - white-space: nowrap; +.utxoTagJoinedAndCjout { border: 1px solid #27ae60; - background-color: #c6eed7; + background-color: #c6eed7 !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } .utxoTagFreeze { - white-space: nowrap; border: 1px solid #2d9cdb; - background-color: #bce7ff; + background-color: #bce7ff !important; border-radius: 0.35rem; padding: 0rem 0.25rem; - display: inline-block; - margin-left: 4px; } -.parent-class .utxoTag { - border: 1px solid #27ae60 !important; - border-radius: 0.2rem !important; -} - -.utxoListDisplay { - margin-left: -27px !important; -} - -.modalBody { - padding: 40px !important; +.utxoTagChangeAndReuse { + border: 1px solid #eb5757; + background-color: #fac7c7 !important; + border-radius: 0.35rem; + padding: 0rem 0.25rem; } .squareToggleButton { @@ -127,7 +57,7 @@ height: 22px; border-radius: 3px; border: 1px solid var(--bs-body-color); - margin-top: 3.5px; + margin-top: 0.45rem; } .selected { @@ -141,5 +71,5 @@ height: 22px; border-radius: 3px; border: 1px solid #2d9cdb; - margin-top: 3.5px; + margin-top: 0.45rem; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 724a2e9ca..99565ce8b 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,34 +1,39 @@ import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import classNames from 'classnames' +import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' +import { useTheme } from '@table-library/react-table-library/theme' +import * as TableTypes from '@table-library/react-table-library/types/table' +import * as Api from '../../libs/JmWalletApi' import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' -import Sprite from '../Sprite' import Alert from '../Alert' -import { useTranslation } from 'react-i18next' -import * as Api from '../../libs/JmWalletApi' +import Balance from '../Balance' +import { ConfirmModal } from '../Modal' +import Sprite from '../Sprite' import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' -import Balance from '../Balance' -import classNames from 'classnames' -type tags = 'deposit' | 'non-cj-change' | 'bond' +type Tags = 'deposit' | 'non-cj-change' | 'bond' | 'reused' | 'joined' | 'cj-out' type UtxoType = { address: Api.BitcoinAddress path: string label: string + id: string checked: boolean value: Api.AmountSats tries: number - tries_remaining: number + triesRemaining: number external: boolean mixdepth: number confirmations: number frozen: boolean utxo: Api.UtxoId locktime?: string - _tags: { tag: tags; color: string }[] + tags: { tag: Tags; color: string }[] } type UtxoList = UtxoType[] @@ -43,14 +48,14 @@ interface ShowUtxosProps { interface UtxoRowProps { utxo: UtxoType index: number - onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings } interface UtxoListDisplayProps { utxos: UtxoList - onToggle: (index: number, type: 'frozen' | 'unfrozen') => void + onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings } @@ -74,81 +79,104 @@ const formatConfirmations = (conf: number) => { const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) // Utility function to Identifies Icons -const utxoIcon = (tag: tags, isFrozen: boolean) => { +const utxoIcon = (tag: Tags, isFrozen: boolean) => { if (isFrozen && tag === 'bond') return 'timelock' - else if (isFrozen) return 'snowflake' - else if (tag === 'deposit') return 'Unmixed' - else if (tag === 'bond') return 'timelock' - else return 'mixed' + if (isFrozen) return 'snowflake' + if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'Unmixed' + if (tag === 'bond') return 'timelock' + return 'mixed' +} + +// Utility function to allot classes +const allotClasses = (tag: Tags, isFrozen: boolean) => { + if (isFrozen) return { row: styles.frozenUtxo, tag: styles.utxoTagFreeze } + if (tag === 'deposit') return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } + if (tag === 'joined' || tag === 'cj-out') return { row: styles.joinedUtxoAndCjout, tag: styles.utxoTagJoinedAndCjout } + if (tag === 'non-cj-change' || tag === 'reused') + return { row: styles.changeAndReuseUtxo, tag: styles.utxoTagChangeAndReuse } + return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } -// UTXO row component +// Utxos row component const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) - - const tag = utxo._tags[0].tag + const tag = utxo.tags[0].tag const icon = utxoIcon(tag, isFrozen) - - const rowClass = isFrozen ? styles.utxoRowFrozen : icon === 'mixed' ? styles.utxoRowUnfrozen : styles.utxoRowUnMixed - const iconClass = isFrozen ? styles.iconFrozen : icon === 'mixed' ? styles.iconMixed : styles.iconUnMixed - const confirmationClass = isFrozen - ? styles.iconConfirmationsFreeze - : icon === 'mixed' - ? styles.iconConfirmations - : styles.iconConfirmationsUnmixed - const tagClass = isFrozen ? styles.utxoTagFreeze : icon === 'mixed' ? styles.utxoTagUnFreeze : styles.utxoTagUnMixed + const rowAndTagClass = allotClasses(tag, isFrozen) return ( - onToggle(index, isFrozen ? 'frozen' : 'unfrozen')} className={rowClass}> -
- - - - - - - - {address} - - - {conf.confirmations} - - - - - -
{tag}
-
-
-
-
+ + + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} + /> + + + + + {address} + + + {conf.confirmations} + + + + + +
{tag}
+
+
) }) -// UTXO list display component -const UtxoListDisplay = memo(({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => ( -
- {utxos.map((utxo, index) => ( - - ))} -
-)) +//Table theme to manage view +const TABLE_THEME = { + Table: ` + font-size: 1rem; + --data-table-library_grid-template-columns: 3.5rem 2.5rem 12rem 2fr 3fr 10rem ; + @media only screen and (min-width: 768px) { + --data-table-library_grid-template-columns: 3.5rem 2.5rem 14rem 5fr 3fr 10rem ; + } + `, + BaseCell: ` + padding: 0.55rem 0.35rem !important; + margin: 0.15rem 0px !important; + `, +} + +//Utxo list display component +const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => { + const tableTheme = useTheme(TABLE_THEME) + + return ( + + {(utxosList: TableTypes.TableProps) => ( + + {utxosList.map((utxo: UtxoType, index: number) => ( + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> + + + ))} + + )} +
+ ) +} // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { @@ -157,7 +185,6 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) const [isLoading, setisLoading] = useState(true) - const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const settings = useSettings() @@ -173,9 +200,8 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const newUtxos = utxos.map((utxo: any) => ({ ...utxo, id: utxo.utxo, - _tags: utxoTags(utxo, walletInfo, t), + tags: utxoTags(utxo, walletInfo, t), })) - const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) @@ -183,7 +209,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { setUnFrozenUtxos(unfrozen) if (unfrozen.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alert_for_empty_utxos') }) + setAlert({ variant: 'danger', message: t('showUtxos.alertForEmptyUtxos') }) } else { setAlert(undefined) } @@ -195,6 +221,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { // Reload wallet info const handleReload = useCallback(async () => { + setisLoading(true) const abortCtrl = new AbortController() try { const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) @@ -221,17 +248,17 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + setAlert({ variant: 'danger', message: `${t('showUtxos.alertForTimeLocked')} ${timeLockedUtxo.locktime}` }) } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alert_for_unfreeze_utxos'), dismissible: true }) + setAlert({ variant: 'warning', message: t('showUtxos.alertForUnfreezeUtxos'), dismissible: true }) } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } }, [frozenUtxos, unFrozenUtxos, t]) // Handler to toggle UTXO selection - const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unfrozen') => { - if (type === 'unfrozen') { + const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unFrozen') => { + if (type === 'unFrozen') { setUnFrozenUtxos((prevUtxos) => prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), ) @@ -242,8 +269,8 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, []) - // Handler for the "Next" button click - const handleNext = async () => { + // Handler for the "confirm" button click + const handleConfirm = async () => { const abortCtrl = new AbortController() const frozenUtxosToUpdate = frozenUtxos @@ -258,7 +285,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ]) - await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) + await handleReload() onHide() } catch (err: any) { if (!abortCtrl.signal.aborted) { @@ -268,26 +295,32 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } return ( - - - {t('showUtxos.show_utxo_title')} - + {!isLoading ? ( - -
{t('showUtxos.show_utxo_subtitle')}
+ <> +
{t('showUtxos.showUtxoSubtitle')}
{alert && ( setAlert(undefined)} /> )} {frozenUtxos.length > 0 && ( - +

@@ -305,22 +338,14 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { {showFrozenUtxos && ( )} - + ) : (
)} - - - {t('showUtxos.back_button')} - - - {t('showUtxos.next_button')} - - - + ) } diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 157c613c1..db60388e8 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from 'react' import { useField, useFormikContext } from 'formik' import * as rb from 'react-bootstrap' -import { jarFillLevel, SelectableSendJar } from '../jars/Jar' +import { jarFillLevel, SelectableJar } from '../jars/Jar' import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' @@ -75,8 +75,9 @@ export const SourceJarSelector = ({ {jarBalances.map((it) => { return (
- { + onClick={(jarIndex: number) => { form.setFieldValue(field.name, jarIndex, true) + if ( + it.accountIndex === field.value && + !disabled && + !isLoading && + it.calculatedTotalBalanceInSats > 0 + ) { + setshowingUTXOS({ + index: it.accountIndex.toString(), + show: true, + }) + } }} />
diff --git a/src/components/jars/Jar.tsx b/src/components/jars/Jar.tsx index f3f0d4d10..8c267e7b2 100644 --- a/src/components/jars/Jar.tsx +++ b/src/components/jars/Jar.tsx @@ -21,6 +21,8 @@ export type JarProps = { } export type SelectableJarProps = JarProps & { + tooltipText?: string + isOpen?: boolean isSelectable: boolean isSelected: boolean variant?: 'default' | 'warning' @@ -164,6 +166,8 @@ const Jar = ({ index, balance, frozenBalance, fillLevel, isOpen = false, size }: * A jar with index, balance, and a radio-style selection button. */ const SelectableJar = ({ + tooltipText, + isOpen = false, isSelectable, isSelected, onClick, @@ -171,6 +175,7 @@ const SelectableJar = ({ variant = 'default', ...jarProps }: SelectableJarProps) => { + const target = useRef(null) return (
isSelectable && onClick(index)} + ref={target} > - +
)}
+ {isOpen && isSelectable && ( + + {(props) => ( + + {tooltipText} + + )} + + )}
) } @@ -235,69 +250,4 @@ const OpenableJar = ({ tooltipText, onClick, ...jarProps }: OpenableJarProps) => ) } -/* - * A jar with index, balance, and a radio-style selection button. - * The jar symbol opens on onClick of radio button. - */ -const SelectableSendJar = ({ - tooltipText, - isSelectable, - isSelected, - onClick, - index, - variant = 'default', - showingUTXOS, - setshowingUTXOS, - ...jarProps -}: SelectableSendJarProps) => { - const target = useRef(null) - - const handleClick = () => { - if (isSelected && isSelectable) { - setshowingUTXOS({ - index: index.toString(), - show: true, - }) - } - } - - return ( -
-
- - - -
- isSelectable && onClick(index)} - className={styles.selectionCircle} - disabled={!isSelectable} - /> - {variant === 'warning' && ( -
- -
- )} -
-
- {isSelected && ( - - {(props) => ( - - {tooltipText} - - )} - - )} -
- ) -} - -export { SelectableSendJar, SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } +export { SelectableJar, OpenableJar, jarName, jarInitial, jarFillLevel } diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 2f9aad87e..a2b515ea9 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -699,13 +699,11 @@ } }, "showUtxos": { - "Select_UTXOs": "Select UTXOs", - "show_utxo_title": "Select UTXOs to be considered", - "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", - "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", - "alert_for_time_locked": "Selected UTXO is Time Locked till", - "alert_for_empty_utxos": "Please Unfreeze UTXOs to send", - "back_button": "Cancel", - "next_button": "Confirm" + "selectUTXOs": "Select UTXOs", + "showUtxoTitle": "Select UTXOs to be considered", + "showUtxoSubtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", + "alertForUnfreezeUtxos": "At least one UTXO is required to perform a transaction", + "alertForTimeLocked": "Selected UTXO is Time Locked till", + "alertForEmptyUtxos": "Please Unfreeze UTXOs to send" } } From 3d17216feffc8540dd75194273386999c77e536f Mon Sep 17 00:00:00 2001 From: amitx13 Date: Wed, 26 Jun 2024 13:53:27 +0530 Subject: [PATCH 19/35] Made UTXO list scrollable after 5 entries for better usability --- src/components/Send/ShowUtxos.module.css | 4 +++ src/components/Send/ShowUtxos.tsx | 35 +++++++++++++----------- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 45f867279..7477f4e60 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -73,3 +73,7 @@ border: 1px solid #2d9cdb; margin-top: 0.45rem; } + +.utxoListDisplayHeight { + max-height: 18rem; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 99565ce8b..cacf31d50 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -47,7 +47,7 @@ interface ShowUtxosProps { interface UtxoRowProps { utxo: UtxoType - index: number + utxoIndex: number onToggle: (index: number, type: 'frozen' | 'unFrozen') => void isFrozen: boolean settings: Settings @@ -98,22 +98,21 @@ const allotClasses = (tag: Tags, isFrozen: boolean) => { } // Utxos row component -const UtxoRow = memo(({ utxo, index, onToggle, isFrozen, settings }: UtxoRowProps) => { +const UtxoRow = memo(({ utxo, utxoIndex, onToggle, isFrozen, settings }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) const tag = utxo.tags[0].tag const icon = utxoIcon(tag, isFrozen) const rowAndTagClass = allotClasses(tag, isFrozen) - return ( onToggle(index, isFrozen ? 'frozen' : 'unFrozen')} + onChange={() => onToggle(utxoIndex, isFrozen ? 'frozen' : 'unFrozen')} className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { [styles.selected]: utxo.checked, })} @@ -164,17 +163,21 @@ const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDispla const tableTheme = useTheme(TABLE_THEME) return ( - - {(utxosList: TableTypes.TableProps) => ( - - {utxosList.map((utxo: UtxoType, index: number) => ( - onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> - - - ))} - - )} -
+
+ + {(utxosList: TableTypes.TableProps) => ( + + {utxosList.map((utxo: UtxoType, index: number) => { + return ( + onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> + + + ) + })} + + )} +
+
) } From d024caa4cbedaa9a9ff3281a4e5aadb326f7ef30 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:11:52 +0530 Subject: [PATCH 20/35] Fixed ShowUtxos and reused it in PaymentConfirmModal for UTXOs review --- src/components/PaymentConfirmModal.tsx | 14 +- src/components/Send/SendForm.tsx | 3 +- src/components/Send/ShowUtxos.module.css | 2 +- src/components/Send/ShowUtxos.tsx | 296 ++++++++++++---------- src/components/Send/SourceJarSelector.tsx | 4 +- src/components/Send/index.tsx | 21 +- src/components/jar_details/UtxoList.tsx | 2 +- src/context/WalletContext.tsx | 4 + src/i18n/locales/en/translation.json | 15 +- 9 files changed, 216 insertions(+), 145 deletions(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index 68c466726..d6af10890 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -64,6 +64,7 @@ interface PaymentDisplayInfo { numCollaborators?: number feeConfigValues?: FeeValues showPrivacyInfo?: boolean + showSelectedUtxos?: boolean } interface PaymentConfirmModalProps extends ConfirmModalProps { @@ -80,6 +81,7 @@ export function PaymentConfirmModal({ numCollaborators, feeConfigValues, showPrivacyInfo = true, + showSelectedUtxos = false, }, children, ...confirmModalProps @@ -207,7 +209,17 @@ export function PaymentConfirmModal({ )} - {children && ( + {showSelectedUtxos && ( + + + {t('show_utxos.selected_utxos')} + + + {children} + + + )} + {!showSelectedUtxos && children && ( {children} diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 7eeaf130b..9f387fbea 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -26,7 +26,7 @@ import { isValidNumCollaborators, } from './helpers' import { AccountBalanceSummary } from '../../context/BalanceSummary' -import { WalletInfo, CurrentWallet } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet, Utxo } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' @@ -214,6 +214,7 @@ export interface SendFormValues { txFee?: TxFee isCoinJoin: boolean numCollaborators?: number + selectedUtxos?: Array } interface InnerSendFormProps { diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 7477f4e60..b97bd0616 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -75,5 +75,5 @@ } .utxoListDisplayHeight { - max-height: 18rem; + max-height: 17.6rem; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index cacf31d50..dd84d3cd3 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,12 +1,19 @@ import { useState, useEffect, useCallback, memo, useRef } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' +import type { TFunction } from 'i18next' import classNames from 'classnames' import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' import { useTheme } from '@table-library/react-table-library/theme' import * as TableTypes from '@table-library/react-table-library/types/table' import * as Api from '../../libs/JmWalletApi' -import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo } from '../../context/WalletContext' +import { + WalletInfo, + CurrentWallet, + useReloadCurrentWalletInfo, + Utxo, + useCurrentWalletInfo, +} from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' import Alert from '../Alert' import Balance from '../Balance' @@ -16,27 +23,7 @@ import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' -type Tags = 'deposit' | 'non-cj-change' | 'bond' | 'reused' | 'joined' | 'cj-out' - -type UtxoType = { - address: Api.BitcoinAddress - path: string - label: string - id: string - checked: boolean - value: Api.AmountSats - tries: number - triesRemaining: number - external: boolean - mixdepth: number - confirmations: number - frozen: boolean - utxo: Api.UtxoId - locktime?: string - tags: { tag: Tags; color: string }[] -} - -type UtxoList = UtxoType[] +type UtxoList = Array interface ShowUtxosProps { wallet: CurrentWallet @@ -46,18 +33,27 @@ interface ShowUtxosProps { } interface UtxoRowProps { - utxo: UtxoType + utxo: Utxo utxoIndex: number - onToggle: (index: number, type: 'frozen' | 'unFrozen') => void + onToggle?: (index: number, isFrozen: boolean) => void isFrozen: boolean settings: Settings + showRadioAndBg: boolean + walletInfo: WalletInfo + t: TFunction } interface UtxoListDisplayProps { - utxos: UtxoList - onToggle: (index: number, type: 'frozen' | 'unFrozen') => void - isFrozen: boolean + utxos: Array + onToggle?: (index: number, isFrozen: boolean) => void settings: Settings + showRadioAndBg?: boolean +} + +interface DividerProps { + isState: boolean + setIsState: (arg: boolean) => void + className?: string } // Utility function to format Bitcoin address @@ -79,7 +75,7 @@ const formatConfirmations = (conf: number) => { const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) // Utility function to Identifies Icons -const utxoIcon = (tag: Tags, isFrozen: boolean) => { +const utxoIcon = (tag: string, isFrozen: boolean) => { if (isFrozen && tag === 'bond') return 'timelock' if (isFrozen) return 'snowflake' if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'Unmixed' @@ -88,7 +84,7 @@ const utxoIcon = (tag: Tags, isFrozen: boolean) => { } // Utility function to allot classes -const allotClasses = (tag: Tags, isFrozen: boolean) => { +const allotClasses = (tag: string, isFrozen: boolean) => { if (isFrozen) return { row: styles.frozenUtxo, tag: styles.utxoTagFreeze } if (tag === 'deposit') return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } if (tag === 'joined' || tag === 'cj-out') return { row: styles.joinedUtxoAndCjout, tag: styles.utxoTagJoinedAndCjout } @@ -98,82 +94,110 @@ const allotClasses = (tag: Tags, isFrozen: boolean) => { } // Utxos row component -const UtxoRow = memo(({ utxo, utxoIndex, onToggle, isFrozen, settings }: UtxoRowProps) => { - const address = formatAddress(utxo.address) - const conf = formatConfirmations(utxo.confirmations) - const value = satsToBtc(utxo.value) - const tag = utxo.tags[0].tag - const icon = utxoIcon(tag, isFrozen) - const rowAndTagClass = allotClasses(tag, isFrozen) - return ( - - - onToggle(utxoIndex, isFrozen ? 'frozen' : 'unFrozen')} - className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { - [styles.selected]: utxo.checked, - })} - /> - - - - - {address} - - - {conf.confirmations} - - - - - -
{tag}
-
-
- ) -}) +const UtxoRow = memo( + ({ utxo, utxoIndex, onToggle, isFrozen, showRadioAndBg, settings, walletInfo, t }: UtxoRowProps) => { + const address = formatAddress(utxo.address) + const conf = formatConfirmations(utxo.confirmations) + const value = satsToBtc(utxo.value) + const tag = utxoTags(utxo, walletInfo, t) + const icon = utxoIcon(tag[0].tag, isFrozen) + const rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + return ( + onToggle && onToggle(utxoIndex, utxo.frozen)} + > + {showRadioAndBg && ( + + { + onToggle && onToggle(utxoIndex, isFrozen) + }} + className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} + /> + + )} + + + + {address} + + + {conf.confirmations} + + + + + +
{tag[0].tag}
+
+
+ ) + }, +) + +//Utxo list display component +const UtxoListDisplay = ({ utxos, onToggle, settings, showRadioAndBg = true }: UtxoListDisplayProps) => { + const { t } = useTranslation() + const walletInfo = useCurrentWalletInfo() -//Table theme to manage view -const TABLE_THEME = { - Table: ` - font-size: 1rem; - --data-table-library_grid-template-columns: 3.5rem 2.5rem 12rem 2fr 3fr 10rem ; + //Table theme to manage view + const TABLE_THEME = { + Table: ` + font-size: ${showRadioAndBg ? '1rem' : '0.87rem'}; + --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 12rem 2fr 3fr 10rem ' : '2.5rem 10rem 5fr 3fr 7.5rem'}; @media only screen and (min-width: 768px) { - --data-table-library_grid-template-columns: 3.5rem 2.5rem 14rem 5fr 3fr 10rem ; + --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 14rem 5fr 3fr 10rem' : '2.5rem 11rem 5fr 3fr 7.5rem'}; } `, - BaseCell: ` - padding: 0.55rem 0.35rem !important; + BaseCell: ` + padding:${showRadioAndBg ? '0.5rem' : '0.55rem'} 0.35rem !important; margin: 0.15rem 0px !important; `, -} - -//Utxo list display component -const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDisplayProps) => { + } const tableTheme = useTheme(TABLE_THEME) return ( -
- - {(utxosList: TableTypes.TableProps) => ( +
+
+ {(utxosList: TableTypes.TableProps) => ( - {utxosList.map((utxo: UtxoType, index: number) => { - return ( - onToggle(index, isFrozen ? 'frozen' : 'unFrozen')}> - - - ) - })} + {walletInfo && + utxosList.map((utxo: Utxo, index: number) => { + return ( + + ) + })} )}
@@ -181,6 +205,30 @@ const UtxoListDisplay = ({ utxos, onToggle, isFrozen, settings }: UtxoListDispla ) } +//Component to show the Divider line +const Divider = ({ isState, setIsState, className }: DividerProps) => { + //Effect for getting back to it's original state when components unMounts + useEffect(() => { + return () => { + setIsState(false) + } + }, [setIsState]) + + return ( + + +
+
+ +
+
+
+
+ ) +} + // Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const [alert, setAlert] = useState(undefined) @@ -200,19 +248,18 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) const utxos: any = data ? data[1] : [] - const newUtxos = utxos.map((utxo: any) => ({ - ...utxo, - id: utxo.utxo, - tags: utxoTags(utxo, walletInfo, t), - })) - const frozen = newUtxos.filter((utxo: any) => utxo.frozen).map((utxo: any) => ({ ...utxo, checked: false })) - const unfrozen = newUtxos.filter((utxo: any) => !utxo.frozen).map((utxo: any) => ({ ...utxo, checked: true })) + const frozen = utxos + .filter((utxo: any) => utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) + const unfrozen = utxos + .filter((utxo: any) => !utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) setFrozenUtxos(frozen) setUnFrozenUtxos(unfrozen) if (unfrozen.length === 0) { - setAlert({ variant: 'danger', message: t('showUtxos.alertForEmptyUtxos') }) + setAlert({ variant: 'danger', message: t('show_utxos.alert_for_empty_utxos') }) } else { setAlert(undefined) } @@ -246,22 +293,22 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { //Effect to set Alert according to the walletInfo useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo: UtxoType) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo: UtxoType) => utxo.checked && utxo.locktime) - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: UtxoType) => !utxo.checked) + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: Utxo) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: Utxo) => utxo.checked && utxo.locktime) + const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('showUtxos.alertForTimeLocked')} ${timeLockedUtxo.locktime}` }) + setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { - setAlert({ variant: 'warning', message: t('showUtxos.alertForUnfreezeUtxos'), dismissible: true }) + setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) } else if (unFrozenUtxos.length !== 0) { setAlert(undefined) } }, [frozenUtxos, unFrozenUtxos, t]) // Handler to toggle UTXO selection - const handleToggle = useCallback((utxoIndex: number, type: 'frozen' | 'unFrozen') => { - if (type === 'unFrozen') { + const handleToggle = useCallback((utxoIndex: number, isFrozen: boolean) => { + if (!isFrozen) { setUnFrozenUtxos((prevUtxos) => prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), ) @@ -303,14 +350,14 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { onConfirm={handleConfirm} disabled={alert?.dismissible || isLoading} isShown={show} - title={t('showUtxos.showUtxoTitle')} + title={t('show_utxos.show_utxo_title')} size="lg" showCloseButtonAndRemoveClassName={true} confirmVariant={'dark'} > {!isLoading ? ( <> -
{t('showUtxos.showUtxoSubtitle')}
+
{t('show_utxos.show_utxo_subtitle')}
{alert && ( { /> )} - + {frozenUtxos.length > 0 && ( - - -
-
- -
-
-
-
+ )} {showFrozenUtxos && ( - + )} ) : ( @@ -352,4 +390,4 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { ) } -export default ShowUtxos +export { ShowUtxos, Divider, UtxoListDisplay, UtxoRow } diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index db60388e8..60c059e0c 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -5,7 +5,7 @@ import { jarFillLevel, SelectableJar } from '../jars/Jar' import { noop } from '../../utils' import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' -import ShowUtxos from './ShowUtxos' +import { ShowUtxos } from './ShowUtxos' import { useTranslation } from 'react-i18next' export type SourceJarSelectorProps = { @@ -76,7 +76,7 @@ export const SourceJarSelector = ({ return (
serviceInfo?.coinjoinInProgress === true, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo?.makerRunning === true, [serviceInfo]) @@ -134,6 +137,7 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() + const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues), @@ -383,8 +387,13 @@ export default function Send({ wallet }: SendProps) { } if (showConfirmSendModal === undefined) { - setShowConfirmSendModal(values) - return + if (walletInfo?.utxosByJar && values.sourceJarIndex !== undefined) { + values.selectedUtxos = walletInfo.utxosByJar[values.sourceJarIndex].filter((utxo) => { + return utxo.frozen === false + }) + setShowConfirmSendModal(values) + return + } } setShowConfirmSendModal(undefined) @@ -517,8 +526,14 @@ export default function Send({ wallet }: SendProps) { isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, + showSelectedUtxos: true, }} - /> + > + + {showSelectedUtxos && showConfirmSendModal.selectedUtxos && ( + + )} + )}
) diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index e782f0800..f6be3702b 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -156,7 +156,7 @@ const toUtxo = (tableNode: TableTypes.TableNode): Utxo => { return utxo as Utxo } -interface UtxoTableRow extends Utxo, TableTypes.TableNode { +interface UtxoTableRow extends Utxo { _icon: JSX.Element _tags: Tag[] _confs: JSX.Element diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 5bfce6abc..5632f7975 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -53,6 +53,9 @@ export type Utxo = { // `locktime` in format "yyyy-MM-dd 00:00:00" // NOTE: it is unparsable with safari Date constructor locktime?: string + id: string + checked?: boolean + tags?: { tag: string; color: string }[] } export type Utxos = Utxo[] @@ -192,6 +195,7 @@ export const groupByJar = (utxos: Utxos): UtxosByJar => { return utxos.reduce((res, utxo) => { const { mixdepth } = utxo res[mixdepth] = res[mixdepth] || [] + utxo.id = utxo.utxo res[mixdepth].push(utxo) return res }, {} as UtxosByJar) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index a2b515ea9..796c247a0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -698,12 +698,13 @@ "utxo_detail_label_status": "Address status" } }, - "showUtxos": { - "selectUTXOs": "Select UTXOs", - "showUtxoTitle": "Select UTXOs to be considered", - "showUtxoSubtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", - "alertForUnfreezeUtxos": "At least one UTXO is required to perform a transaction", - "alertForTimeLocked": "Selected UTXO is Time Locked till", - "alertForEmptyUtxos": "Please Unfreeze UTXOs to send" + "show_utxos": { + "select_utxos": "Select UTXOs", + "selected_utxos": "Selected UTXOs", + "show_utxo_title": "Select UTXOs to be considered", + "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", + "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", + "alert_for_time_locked": "Selected UTXO is Time Locked till", + "alert_for_empty_utxos": "Please Unfreeze UTXOs to send" } } From abe5adf5d77374a839da8e2f58752fd361a85565 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:25:56 +0530 Subject: [PATCH 21/35] Implemented suggested changes and made it reusable --- src/components/Send/index.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 558f6eaf5..fcd6b519e 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -526,14 +526,8 @@ export default function Send({ wallet }: SendProps) { isCoinjoin: showConfirmSendModal.isCoinJoin, numCollaborators: showConfirmSendModal.numCollaborators!, feeConfigValues: { ...feeConfigValues, tx_fees: showConfirmSendModal.txFee }, - showSelectedUtxos: true, }} - > - - {showSelectedUtxos && showConfirmSendModal.selectedUtxos && ( - - )} - + /> )}
) From 1228c99b5c6e948c9573437af1d950c7c7b73ef0 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:31:24 +0530 Subject: [PATCH 22/35] bug fixing --- src/components/Send/index.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index fcd6b519e..1aae7b658 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -18,8 +18,6 @@ import { useLoadConfigValue } from '../../context/ServiceConfigContext' import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' -import { useSettings } from '../../context/SettingsContext' -import { UtxoListDisplay, Divider } from './ShowUtxos' import { initialNumCollaborators } from './helpers' @@ -92,7 +90,6 @@ export default function Send({ wallet }: SendProps) { const serviceInfo = useServiceInfo() const reloadServiceInfo = useReloadServiceInfo() const loadConfigValue = useLoadConfigValue() - const settings = useSettings() const isCoinjoinInProgress = useMemo(() => serviceInfo?.coinjoinInProgress === true, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo?.makerRunning === true, [serviceInfo]) @@ -137,7 +134,6 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() - const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues), From 3ae26ea77c1c57d78a6799ad24419e1f7cca1510 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 21:39:59 +0530 Subject: [PATCH 23/35] empty utxos bug fixed --- src/components/Send/ShowUtxos.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index dd84d3cd3..b56d01aab 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -100,8 +100,14 @@ const UtxoRow = memo( const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) const tag = utxoTags(utxo, walletInfo, t) - const icon = utxoIcon(tag[0].tag, isFrozen) - const rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + let icon, rowAndTagClass + if (tag.length === 0) { + icon = 'Unmixed' + rowAndTagClass = { row: styles.depositUtxo, tag: styles.utxoTagDeposit } + } else { + icon = utxoIcon(tag[0].tag, isFrozen) + rowAndTagClass = allotClasses(tag[0].tag, isFrozen) + } return (
-
{tag[0].tag}
+
{tag.length ? tag[0].tag : ''}
) From 67fd5bf95f1f0e0dfbf40262dcbcc67599bc0f55 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Fri, 5 Jul 2024 12:48:43 +0530 Subject: [PATCH 24/35] Implemented suggested changes - Fixed sweep amount - Refactored code (naming) - Removed unnecessary warnings - Added minor improvements - Fixed a few bugs --- src/components/Modal.tsx | 13 ++-- src/components/PaymentConfirmModal.tsx | 14 +--- src/components/Send/AmountInputField.tsx | 21 +++++- src/components/Send/ShowUtxos.module.css | 2 +- src/components/Send/ShowUtxos.tsx | 80 +++++++++++++++-------- src/components/Send/SourceJarSelector.tsx | 14 ++-- src/i18n/locales/en/translation.json | 1 + 7 files changed, 87 insertions(+), 58 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 1c04322c0..efece7e78 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -10,7 +10,8 @@ type BaseModalProps = { onCancel: () => void backdrop?: rb.ModalProps['backdrop'] size?: rb.ModalProps['size'] - showCloseButtonAndRemoveClassName?: boolean + showCloseButton?: boolean + removeClassName?: boolean } const BaseModal = ({ isShown, @@ -19,7 +20,8 @@ const BaseModal = ({ onCancel, size, backdrop = 'static', - showCloseButtonAndRemoveClassName = false, + showCloseButton = false, + removeClassName = false, }: PropsWithChildren) => { return ( - - {title} + + {title} {children} diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index d6af10890..68c466726 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -64,7 +64,6 @@ interface PaymentDisplayInfo { numCollaborators?: number feeConfigValues?: FeeValues showPrivacyInfo?: boolean - showSelectedUtxos?: boolean } interface PaymentConfirmModalProps extends ConfirmModalProps { @@ -81,7 +80,6 @@ export function PaymentConfirmModal({ numCollaborators, feeConfigValues, showPrivacyInfo = true, - showSelectedUtxos = false, }, children, ...confirmModalProps @@ -209,17 +207,7 @@ export function PaymentConfirmModal({ )} - {showSelectedUtxos && ( - - - {t('show_utxos.selected_utxos')} - - - {children} - - - )} - {!showSelectedUtxos && children && ( + {children && ( {children} diff --git a/src/components/Send/AmountInputField.tsx b/src/components/Send/AmountInputField.tsx index e16a89340..522f53812 100644 --- a/src/components/Send/AmountInputField.tsx +++ b/src/components/Send/AmountInputField.tsx @@ -1,4 +1,4 @@ -import { useRef } from 'react' +import { useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import { useField, useFormikContext } from 'formik' @@ -35,6 +35,25 @@ export const AmountInputField = ({ const form = useFormikContext() const ref = useRef(null) + //Effect to change the field value whenever the sourceJarBalance changes (sourceJarBalance will change when quick freeze/unfreeze is performed or different source jar is selected) + useEffect(() => { + if (!sourceJarBalance) return + + const currentValue = formatBtcDisplayValue(sourceJarBalance.calculatedAvailableBalanceInSats) + + if (field.value?.isSweep && field.value.displayValue !== currentValue) { + form.setFieldValue( + field.name, + { + value: 0, + isSweep: true, + displayValue: formatBtcDisplayValue(sourceJarBalance.calculatedAvailableBalanceInSats), + }, + true, + ) + } + }, [sourceJarBalance, field, form]) + return ( <> diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index b97bd0616..ccd1bca0f 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -9,7 +9,7 @@ } .depositUtxo { - background-color: none !important; + background-color: var(--bs-body-bg) !important; color: var(--bs-modal-color) !important; } diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index b56d01aab..5db3627af 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -38,7 +38,8 @@ interface UtxoRowProps { onToggle?: (index: number, isFrozen: boolean) => void isFrozen: boolean settings: Settings - showRadioAndBg: boolean + showRadioButton: boolean + showBackground: boolean walletInfo: WalletInfo t: TFunction } @@ -47,7 +48,8 @@ interface UtxoListDisplayProps { utxos: Array onToggle?: (index: number, isFrozen: boolean) => void settings: Settings - showRadioAndBg?: boolean + showRadioButton: boolean + showBackground: boolean } interface DividerProps { @@ -67,7 +69,7 @@ const formatConfirmations = (conf: number) => { if (conf === 3) return { symbol: 'confs-3', confirmations: conf } if (conf === 4) return { symbol: 'confs-4', confirmations: conf } if (conf === 5) return { symbol: 'confs-5', confirmations: conf } - if (conf >= 9999) return { symbol: 'confs-full', confirmations: '9999+' } + if (conf > 9999) return { symbol: 'confs-full', confirmations: '9999+' } return { symbol: 'confs-full', confirmations: conf } } @@ -93,9 +95,8 @@ const allotClasses = (tag: string, isFrozen: boolean) => { return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } -// Utxos row component const UtxoRow = memo( - ({ utxo, utxoIndex, onToggle, isFrozen, showRadioAndBg, settings, walletInfo, t }: UtxoRowProps) => { + ({ utxo, utxoIndex, onToggle, isFrozen, showRadioButton, showBackground, settings, walletInfo, t }: UtxoRowProps) => { const address = formatAddress(utxo.address) const conf = formatConfirmations(utxo.confirmations) const value = satsToBtc(utxo.value) @@ -112,11 +113,11 @@ const UtxoRow = memo( onToggle && onToggle(utxoIndex, utxo.frozen)} > - {showRadioAndBg && ( + {showRadioButton && ( { +const UtxoListDisplay = ({ + utxos, + onToggle, + settings, + showRadioButton = true, + showBackground = true, +}: UtxoListDisplayProps) => { const { t } = useTranslation() const walletInfo = useCurrentWalletInfo() //Table theme to manage view const TABLE_THEME = { Table: ` - font-size: ${showRadioAndBg ? '1rem' : '0.87rem'}; - --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 12rem 2fr 3fr 10rem ' : '2.5rem 10rem 5fr 3fr 7.5rem'}; + font-size: ${showRadioButton ? '1rem' : '0.87rem'}; + --data-table-library_grid-template-columns: ${showRadioButton ? '3.5rem 2.5rem 12rem 2fr 3fr 10rem ' : '2.5rem 10rem 5fr 3fr 7.5rem'}; @media only screen and (min-width: 768px) { - --data-table-library_grid-template-columns: ${showRadioAndBg ? '3.5rem 2.5rem 14rem 5fr 3fr 10rem' : '2.5rem 11rem 5fr 3fr 7.5rem'}; + --data-table-library_grid-template-columns: ${showRadioButton ? '3.5rem 2.5rem 14rem 5fr 3fr 10rem' : '2.5rem 11rem 5fr 3fr 7.5rem'}; } `, BaseCell: ` - padding:${showRadioAndBg ? '0.5rem' : '0.55rem'} 0.35rem !important; + padding:${showRadioButton ? '0.5rem' : '0.55rem'} 0.35rem !important; margin: 0.15rem 0px !important; `, } @@ -197,7 +203,8 @@ const UtxoListDisplay = ({ utxos, onToggle, settings, showRadioAndBg = true }: U utxoIndex={index} onToggle={onToggle} isFrozen={utxo.frozen} - showRadioAndBg={showRadioAndBg} + showRadioButton={showRadioButton} + showBackground={showBackground} settings={settings} walletInfo={walletInfo} t={t} @@ -211,7 +218,6 @@ const UtxoListDisplay = ({ utxos, onToggle, settings, showRadioAndBg = true }: U ) } -//Component to show the Divider line const Divider = ({ isState, setIsState, className }: DividerProps) => { //Effect for getting back to it's original state when components unMounts useEffect(() => { @@ -235,13 +241,12 @@ const Divider = ({ isState, setIsState, className }: DividerProps) => { ) } -// Main component to show UTXOs const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) const [frozenUtxos, setFrozenUtxos] = useState([]) - const [isLoading, setisLoading] = useState(true) + const [isLoading, setIsLoading] = useState(true) const { t } = useTranslation() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const settings = useSettings() @@ -265,23 +270,23 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { setUnFrozenUtxos(unfrozen) if (unfrozen.length === 0) { - setAlert({ variant: 'danger', message: t('show_utxos.alert_for_empty_utxos') }) + setShowFrozenUtxos(true) + setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) } else { setAlert(undefined) } - - setisLoading(false) }, [index, t], ) // Reload wallet info const handleReload = useCallback(async () => { - setisLoading(true) const abortCtrl = new AbortController() try { + setIsLoading(true) const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) loadData(walletInfo) + setIsLoading(false) } catch (err: any) { if (!abortCtrl.signal.aborted) { setAlert({ variant: 'danger', message: err.message, dismissible: true }) @@ -289,7 +294,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { } }, [reloadCurrentWalletInfo, loadData]) - //Effect to Reload walletInfo + //Effect to Reload walletInfo only once useEffect(() => { if (!isHandleReloadExecuted.current) { handleReload() @@ -305,9 +310,9 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { if (timeLockedUtxo) { setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0 && unFrozenUtxos.length > 0) { + } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else if (unFrozenUtxos.length !== 0) { + } else if (unFrozenUtxos.length !== 0 || frozenUtxosToUpdate.length !== 0) { setAlert(undefined) } }, [frozenUtxos, unFrozenUtxos, t]) @@ -358,12 +363,17 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { isShown={show} title={t('show_utxos.show_utxo_title')} size="lg" - showCloseButtonAndRemoveClassName={true} + showCloseButton={true} + removeClassName={true} confirmVariant={'dark'} > {!isLoading ? ( <> -
{t('show_utxos.show_utxo_subtitle')}
+
+ {unFrozenUtxos.length !== 0 + ? t('show_utxos.show_utxo_subtitle') + : t('show_utxos.show_utxo_subtitle_when_allutxos_are_frozen')} +
{alert && ( { /> )} - - {frozenUtxos.length > 0 && ( + + {frozenUtxos.length > 0 && unFrozenUtxos.length > 0 && ( { /> )} {showFrozenUtxos && ( - + )} ) : ( diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 60c059e0c..6cfec3fb0 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -19,7 +19,7 @@ export type SourceJarSelectorProps = { disabled?: boolean } -interface showingUtxosProps { +interface ShowUtxosProps { index: String show: boolean } @@ -37,7 +37,7 @@ export const SourceJarSelector = ({ const [field] = useField(name) const form = useFormikContext() - const [showingUTXOS, setshowingUTXOS] = useState({ + const [showUtxos, setShowUtxos] = useState({ index: '', show: false, }) @@ -59,17 +59,17 @@ export const SourceJarSelector = ({ ) : (
- {showingUTXOS.show && ( + {showUtxos.show && ( { - setshowingUTXOS({ + setShowUtxos({ index: '', show: false, }) }} - index={showingUTXOS.index} + index={showUtxos.index} /> )} {jarBalances.map((it) => { @@ -96,7 +96,7 @@ export const SourceJarSelector = ({ !isLoading && it.calculatedTotalBalanceInSats > 0 ) { - setshowingUTXOS({ + setShowUtxos({ index: it.accountIndex.toString(), show: true, }) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 796c247a0..023d14730 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -703,6 +703,7 @@ "selected_utxos": "Selected UTXOs", "show_utxo_title": "Select UTXOs to be considered", "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", + "show_utxo_subtitle_when_allutxos_are_frozen": "The following UTXOs are frozen. Please select them to be considered in the transaction.", "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", "alert_for_time_locked": "Selected UTXO is Time Locked till", "alert_for_empty_utxos": "Please Unfreeze UTXOs to send" From 38f5d58a2433c62b22e6cdc4db41a685e1da43ec Mon Sep 17 00:00:00 2001 From: amitx13 Date: Fri, 5 Jul 2024 14:18:52 +0530 Subject: [PATCH 25/35] Added default sorting i.e by Date(Confirmations) --- src/components/Send/ShowUtxos.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 5db3627af..f079215f3 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -184,6 +184,9 @@ const UtxoListDisplay = ({ } const tableTheme = useTheme(TABLE_THEME) + //Default sort is by date the older ones at the bottom, newer ones at the top. + utxos.sort((a, b) => a.confirmations - b.confirmations) + return (
Date: Fri, 5 Jul 2024 15:36:05 +0530 Subject: [PATCH 26/35] Added tooltip for confirmation --- src/components/App.tsx | 2 + src/components/Send/ShowUtxos.tsx | 66 ++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 5c1cd49c8..1edbd5e67 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -87,6 +87,8 @@ export default function App() { [reloadCurrentWalletInfo], ) + debugger + const router = createBrowserRouter( createRoutesFromElements( { const UtxoRow = memo( ({ utxo, utxoIndex, onToggle, isFrozen, showRadioButton, showBackground, settings, walletInfo, t }: UtxoRowProps) => { - const address = formatAddress(utxo.address) - const conf = formatConfirmations(utxo.confirmations) - const value = satsToBtc(utxo.value) - const tag = utxoTags(utxo, walletInfo, t) - let icon, rowAndTagClass - if (tag.length === 0) { - icon = 'Unmixed' - rowAndTagClass = { row: styles.depositUtxo, tag: styles.utxoTagDeposit } - } else { - icon = utxoIcon(tag[0].tag, isFrozen) - rowAndTagClass = allotClasses(tag[0].tag, isFrozen) - } + const { address: utxoAddress, confirmations, value, checked, frozen } = utxo + + const address = useMemo(() => formatAddress(utxoAddress), [utxoAddress]) + const conf = useMemo(() => formatConfirmations(confirmations), [confirmations]) + const valueString = useMemo(() => satsToBtc(value).toString(), [value]) + const tag = useMemo(() => utxoTags(utxo, walletInfo, t), [utxo, walletInfo, t]) + + const { icon, rowAndTagClass } = useMemo(() => { + if (tag.length === 0) { + return { icon: 'Unmixed', rowAndTagClass: { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } + } + return { icon: utxoIcon(tag[0].tag, isFrozen), rowAndTagClass: allotClasses(tag[0].tag, isFrozen) } + }, [tag, isFrozen]) + + const ConfirmationCell = () => + confirmations > 9999 ? ( + {confirmations}} + > +
+ + {conf.confirmations} +
+
+ ) : ( +
+ + {conf.confirmations} +
+ ) + return ( onToggle && onToggle(utxoIndex, utxo.frozen)} + onClick={() => onToggle && onToggle(utxoIndex, frozen)} > {showRadioButton && ( { onToggle && onToggle(utxoIndex, isFrozen) }} className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { - [styles.selected]: utxo.checked, + [styles.selected]: checked, })} /> @@ -137,12 +166,11 @@ const UtxoRow = memo( {address} - - {conf.confirmations} + Date: Mon, 8 Jul 2024 19:41:06 +0530 Subject: [PATCH 27/35] Update src/components/App.tsx Co-authored-by: Thebora Kompanioni --- src/components/App.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 1edbd5e67..5c1cd49c8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -87,8 +87,6 @@ export default function App() { [reloadCurrentWalletInfo], ) - debugger - const router = createBrowserRouter( createRoutesFromElements( Date: Sun, 14 Jul 2024 11:47:24 +0530 Subject: [PATCH 28/35] Improved loading logic, Improved Modal.tsx, Reviewed and enhanced text wording --- src/components/Modal.tsx | 10 ++-- src/components/Send/ShowUtxos.module.css | 9 ++++ src/components/Send/ShowUtxos.tsx | 66 +++++++++++++---------- src/components/Send/SourceJarSelector.tsx | 25 ++++----- src/context/WalletContext.tsx | 4 +- 5 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index efece7e78..40e7c4b36 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -11,7 +11,8 @@ type BaseModalProps = { backdrop?: rb.ModalProps['backdrop'] size?: rb.ModalProps['size'] showCloseButton?: boolean - removeClassName?: boolean + headerClassName?: string + titleClassName?: string } const BaseModal = ({ isShown, @@ -21,7 +22,8 @@ const BaseModal = ({ size, backdrop = 'static', showCloseButton = false, - removeClassName = false, + headerClassName, + titleClassName, }: PropsWithChildren) => { return ( - - {title} + + {title} {children} diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index ccd1bca0f..38998a9df 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -77,3 +77,12 @@ .utxoListDisplayHeight { max-height: 17.6rem; } + +.customHeaderClass { + background-color: var(--bs-gray-800) !important; + padding: var(--bs-modal-header-padding) !important; +} + +.customTitleClass { + color: var(--bs-heading-color) !important; +} diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 509997c0f..f8325f78c 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -26,10 +26,11 @@ import styles from './ShowUtxos.module.css' type UtxoList = Array interface ShowUtxosProps { + walletInfo: WalletInfo wallet: CurrentWallet - show: boolean - onHide: () => void - index: String + isOpen: boolean + onCancel: () => void + jarIndex: String } interface UtxoRowProps { @@ -39,7 +40,7 @@ interface UtxoRowProps { isFrozen: boolean settings: Settings showRadioButton: boolean - showBackground: boolean + showBackgroundColor: boolean walletInfo: WalletInfo t: TFunction } @@ -49,7 +50,7 @@ interface UtxoListDisplayProps { onToggle?: (index: number, isFrozen: boolean) => void settings: Settings showRadioButton: boolean - showBackground: boolean + showBackgroundColor: boolean } interface DividerProps { @@ -96,7 +97,17 @@ const allotClasses = (tag: string, isFrozen: boolean) => { } const UtxoRow = memo( - ({ utxo, utxoIndex, onToggle, isFrozen, showRadioButton, showBackground, settings, walletInfo, t }: UtxoRowProps) => { + ({ + utxo, + utxoIndex, + onToggle, + isFrozen, + showRadioButton, + showBackgroundColor, + settings, + walletInfo, + t, + }: UtxoRowProps) => { const { address: utxoAddress, confirmations, value, checked, frozen } = utxo const address = useMemo(() => formatAddress(utxoAddress), [utxoAddress]) @@ -142,7 +153,7 @@ const UtxoRow = memo( onToggle && onToggle(utxoIndex, frozen)} > @@ -191,7 +202,7 @@ const UtxoListDisplay = ({ onToggle, settings, showRadioButton = true, - showBackground = true, + showBackgroundColor = true, }: UtxoListDisplayProps) => { const { t } = useTranslation() const walletInfo = useCurrentWalletInfo() @@ -235,7 +246,7 @@ const UtxoListDisplay = ({ onToggle={onToggle} isFrozen={utxo.frozen} showRadioButton={showRadioButton} - showBackground={showBackground} + showBackgroundColor={showBackgroundColor} settings={settings} walletInfo={walletInfo} t={t} @@ -272,7 +283,7 @@ const Divider = ({ isState, setIsState, className }: DividerProps) => { ) } -const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { +const ShowUtxos = ({ walletInfo, wallet, isOpen, onCancel, jarIndex }: ShowUtxosProps) => { const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) @@ -287,27 +298,27 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { // Load data from wallet info const loadData = useCallback( (walletInfo: WalletInfo) => { - const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === index) + const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === jarIndex) const utxos: any = data ? data[1] : [] - const frozen = utxos + const frozenUtxoList = utxos .filter((utxo: any) => utxo.frozen) .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) - const unfrozen = utxos + const unFrozenUtxosList = utxos .filter((utxo: any) => !utxo.frozen) .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) - setFrozenUtxos(frozen) - setUnFrozenUtxos(unfrozen) + setFrozenUtxos(frozenUtxoList) + setUnFrozenUtxos(unFrozenUtxosList) - if (unfrozen.length === 0) { + if (unFrozenUtxosList.length === 0) { setShowFrozenUtxos(true) setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) } else { setAlert(undefined) } }, - [index, t], + [jarIndex, t], ) // Reload wallet info @@ -315,7 +326,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { const abortCtrl = new AbortController() try { setIsLoading(true) - const walletInfo = await reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }) + await reloadCurrentWalletInfo.reloadDisplay({ signal: abortCtrl.signal }) loadData(walletInfo) setIsLoading(false) } catch (err: any) { @@ -323,7 +334,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { setAlert({ variant: 'danger', message: err.message, dismissible: true }) } } - }, [reloadCurrentWalletInfo, loadData]) + }, [reloadCurrentWalletInfo, loadData, walletInfo]) //Effect to Reload walletInfo only once useEffect(() => { @@ -337,11 +348,11 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { useEffect(() => { const frozenUtxosToUpdate = frozenUtxos.filter((utxo: Utxo) => utxo.checked && !utxo.locktime) const timeLockedUtxo = frozenUtxos.find((utxo: Utxo) => utxo.checked && utxo.locktime) - const allUnfrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) + const allUnFrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) if (timeLockedUtxo) { setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (allUnfrozenUnchecked && frozenUtxosToUpdate.length === 0) { + } else if (allUnFrozenUnchecked && frozenUtxosToUpdate.length === 0) { setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) } else if (unFrozenUtxos.length !== 0 || frozenUtxosToUpdate.length !== 0) { setAlert(undefined) @@ -378,7 +389,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), ]) await handleReload() - onHide() + onCancel() } catch (err: any) { if (!abortCtrl.signal.aborted) { setAlert({ variant: 'danger', message: err.message, dismissible: true }) @@ -388,15 +399,16 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { return ( {!isLoading ? ( <> @@ -420,7 +432,7 @@ const ShowUtxos = ({ wallet, show, onHide, index }: ShowUtxosProps) => { onToggle={handleToggle} settings={settings} showRadioButton={true} - showBackground={true} + showBackgroundColor={true} /> {frozenUtxos.length > 0 && unFrozenUtxos.length > 0 && ( { onToggle={handleToggle} settings={settings} showRadioButton={true} - showBackground={true} + showBackgroundColor={true} /> )} diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 6cfec3fb0..7fb2fb17d 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -20,8 +20,8 @@ export type SourceJarSelectorProps = { } interface ShowUtxosProps { - index: String - show: boolean + jarIndex: String + isOpen: boolean } export const SourceJarSelector = ({ @@ -38,8 +38,8 @@ export const SourceJarSelector = ({ const [field] = useField(name) const form = useFormikContext() const [showUtxos, setShowUtxos] = useState({ - index: '', - show: false, + jarIndex: '', + isOpen: false, }) const jarBalances = useMemo(() => { @@ -59,17 +59,18 @@ export const SourceJarSelector = ({ ) : (
- {showUtxos.show && ( + {showUtxos.isOpen && ( { + isOpen={showUtxos.isOpen} + onCancel={() => { setShowUtxos({ - index: '', - show: false, + jarIndex: '', + isOpen: false, }) }} - index={showUtxos.index} + jarIndex={showUtxos.jarIndex} /> )} {jarBalances.map((it) => { @@ -97,8 +98,8 @@ export const SourceJarSelector = ({ it.calculatedTotalBalanceInSats > 0 ) { setShowUtxos({ - index: it.accountIndex.toString(), - show: true, + jarIndex: it.accountIndex.toString(), + isOpen: true, }) } }} diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 5632f7975..54dd4f3e4 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -147,6 +147,7 @@ interface WalletContextEntry { reloadCurrentWalletInfo: { reloadAll: ({ signal }: { signal: AbortSignal }) => Promise reloadUtxos: ({ signal }: { signal: AbortSignal }) => Promise + reloadDisplay: ({ signal }: { signal: AbortSignal }) => Promise } } @@ -306,8 +307,9 @@ const WalletProvider = ({ children }: PropsWithChildren) => { () => ({ reloadAll, reloadUtxos, + reloadDisplay, }), - [reloadAll, reloadUtxos], + [reloadAll, reloadUtxos, reloadDisplay], ) useEffect(() => { From 9996488e79d7506d4cb82fc7c5bd1e93519c1794 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 16 Jul 2024 10:41:19 +0530 Subject: [PATCH 29/35] Prevent unnecessary /display requests each time the modal is opened --- src/components/Send/SendForm.tsx | 8 ++++++++ src/components/Send/ShowUtxos.tsx | 20 +++++++++++++++++--- src/components/Send/SourceJarSelector.tsx | 6 ++++++ src/components/Send/index.tsx | 5 +++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 9f387fbea..51c937388 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -228,6 +228,8 @@ interface InnerSendFormProps { feeConfigValues?: FeeValues reloadFeeConfigValues: () => void disabled?: boolean + isDisplayReloadInShowUtxos: boolean + setIsDisplayReloadInShowUtxos: (arg: boolean) => void } const InnerSendForm = ({ @@ -241,6 +243,8 @@ const InnerSendForm = ({ feeConfigValues, reloadFeeConfigValues, disabled = false, + isDisplayReloadInShowUtxos, + setIsDisplayReloadInShowUtxos, }: InnerSendFormProps) => { const { t } = useTranslation() const serviceInfo = useServiceInfo() @@ -279,6 +283,8 @@ const InnerSendForm = ({ isLoading={isLoading} disabled={disabled} variant={showCoinjoinPreconditionViolationAlert ? 'warning' : 'default'} + isDisplayReloadInShowUtxos={isDisplayReloadInShowUtxos} + setIsDisplayReloadInShowUtxos={setIsDisplayReloadInShowUtxos} /> {showCoinjoinPreconditionViolationAlert && (
@@ -380,6 +386,8 @@ type SendFormProps = Omit & { formRef?: React.Ref> blurred?: boolean wallet: CurrentWallet + isDisplayReloadInShowUtxos: boolean + setIsDisplayReloadInShowUtxos: (arg: boolean) => void } export const SendForm = ({ diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index f8325f78c..4d480a153 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -31,6 +31,8 @@ interface ShowUtxosProps { isOpen: boolean onCancel: () => void jarIndex: String + isDisplayReloadInShowUtxos: boolean + setIsDisplayReloadInShowUtxos: (arg: boolean) => void } interface UtxoRowProps { @@ -283,7 +285,15 @@ const Divider = ({ isState, setIsState, className }: DividerProps) => { ) } -const ShowUtxos = ({ walletInfo, wallet, isOpen, onCancel, jarIndex }: ShowUtxosProps) => { +const ShowUtxos = ({ + walletInfo, + wallet, + isOpen, + onCancel, + jarIndex, + isDisplayReloadInShowUtxos, + setIsDisplayReloadInShowUtxos, +}: ShowUtxosProps) => { const [alert, setAlert] = useState(undefined) const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) @@ -326,7 +336,11 @@ const ShowUtxos = ({ walletInfo, wallet, isOpen, onCancel, jarIndex }: ShowUtxos const abortCtrl = new AbortController() try { setIsLoading(true) - await reloadCurrentWalletInfo.reloadDisplay({ signal: abortCtrl.signal }) + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + if (isDisplayReloadInShowUtxos) { + await reloadCurrentWalletInfo.reloadDisplay({ signal: abortCtrl.signal }) + setIsDisplayReloadInShowUtxos(false) + } loadData(walletInfo) setIsLoading(false) } catch (err: any) { @@ -334,7 +348,7 @@ const ShowUtxos = ({ walletInfo, wallet, isOpen, onCancel, jarIndex }: ShowUtxos setAlert({ variant: 'danger', message: err.message, dismissible: true }) } } - }, [reloadCurrentWalletInfo, loadData, walletInfo]) + }, [isDisplayReloadInShowUtxos, setIsDisplayReloadInShowUtxos, reloadCurrentWalletInfo, loadData, walletInfo]) //Effect to Reload walletInfo only once useEffect(() => { diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index 7fb2fb17d..e2b1adf5e 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -17,6 +17,8 @@ export type SourceJarSelectorProps = { wallet: CurrentWallet isLoading: boolean disabled?: boolean + isDisplayReloadInShowUtxos: boolean + setIsDisplayReloadInShowUtxos: (arg: boolean) => void } interface ShowUtxosProps { @@ -32,6 +34,8 @@ export const SourceJarSelector = ({ variant, isLoading, disabled = false, + isDisplayReloadInShowUtxos, + setIsDisplayReloadInShowUtxos, }: SourceJarSelectorProps) => { const { t } = useTranslation() @@ -71,6 +75,8 @@ export const SourceJarSelector = ({ }) }} jarIndex={showUtxos.jarIndex} + isDisplayReloadInShowUtxos={isDisplayReloadInShowUtxos} + setIsDisplayReloadInShowUtxos={setIsDisplayReloadInShowUtxos} /> )} {jarBalances.map((it) => { diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 1aae7b658..52850be62 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -91,6 +91,8 @@ export default function Send({ wallet }: SendProps) { const reloadServiceInfo = useReloadServiceInfo() const loadConfigValue = useLoadConfigValue() + const [isDisplayReloadInShowUtxos, setIsDisplayReloadInShowUtxos] = useState(true) + const isCoinjoinInProgress = useMemo(() => serviceInfo?.coinjoinInProgress === true, [serviceInfo]) const isMakerRunning = useMemo(() => serviceInfo?.makerRunning === true, [serviceInfo]) const isRescanningInProgress = useMemo(() => serviceInfo?.rescanning === true, [serviceInfo]) @@ -266,6 +268,7 @@ export default function Send({ wallet }: SendProps) { txid, }), }) + setIsDisplayReloadInShowUtxos(true) setWaitForUtxosToBeSpent(inputs.map((it: any) => it.outpoint)) success = true } else { @@ -490,6 +493,8 @@ export default function Send({ wallet }: SendProps) { loadNewWalletAddress={loadNewWalletAddress} feeConfigValues={feeConfigValues} reloadFeeConfigValues={reloadFeeConfigValues} + isDisplayReloadInShowUtxos={isDisplayReloadInShowUtxos} + setIsDisplayReloadInShowUtxos={setIsDisplayReloadInShowUtxos} /> {showConfirmAbortModal && ( From f5f228a8cc75667dde6a0f1bce3197d2b53cb2e0 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Tue, 16 Jul 2024 11:48:28 +0530 Subject: [PATCH 30/35] UTXOs are only visible in case of sweep --- src/components/PaymentConfirmModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/PaymentConfirmModal.tsx b/src/components/PaymentConfirmModal.tsx index d6af10890..7efb74912 100644 --- a/src/components/PaymentConfirmModal.tsx +++ b/src/components/PaymentConfirmModal.tsx @@ -209,7 +209,7 @@ export function PaymentConfirmModal({ )} - {showSelectedUtxos && ( + {showSelectedUtxos && isSweep && ( {t('show_utxos.selected_utxos')} From d06ba0679dc6ed24a39805316548a74ef65ecae2 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Tue, 16 Jul 2024 15:26:17 +0200 Subject: [PATCH 31/35] feat(config): ability to customize "max sweep fee change" setting (#793) --- src/components/settings/FeeConfigModal.tsx | 72 +++++++++++++++++++++- src/constants/config.ts | 3 + src/hooks/Fees.ts | 4 ++ src/i18n/locales/en/translation.json | 3 + 4 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 36490405b..581be1bcd 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -12,6 +12,7 @@ import ToggleSwitch from '../ToggleSwitch' import { isValidNumber, factorToPercentage, percentageToFactor } from '../../utils' import styles from './FeeConfigModal.module.css' import BitcoinAmountInput, { AmountValue, toAmountValue } from '../BitcoinAmountInput' +import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT } from '../../constants/config' const __dev_allowFeeValuesReset = isDebugFeatureEnabled('allowFeeValuesReset') @@ -27,6 +28,8 @@ const CJ_FEE_ABS_MIN = 1 const CJ_FEE_ABS_MAX = 1_000_000 // 0.01 BTC - no enforcement by JM - this should be a "sane" max value const CJ_FEE_REL_MIN = 0.000001 // 0.0001% const CJ_FEE_REL_MAX = 0.05 // 5% - no enforcement by JM - this should be a "sane" max value +const MAX_SWEEP_FEE_CHANGE_MIN = 0.5 // 50% +const MAX_SWEEP_FEE_CHANGE_MAX = 1 // 100% interface FeeConfigModalProps { show: boolean @@ -40,7 +43,7 @@ export type FeeConfigSectionKey = 'tx_fee' | 'cj_fee' const TX_FEE_SECTION_KEY: FeeConfigSectionKey = 'tx_fee' const CJ_FEE_SECTION_KEY: FeeConfigSectionKey = 'cj_fee' -type FeeFormValues = Pick & { +type FeeFormValues = Pick & { max_cj_fee_abs?: AmountValue enableValidation?: boolean } @@ -167,7 +170,7 @@ const FeeConfigForm = forwardRef( {t('settings.fees.title_general_fee_settings')} @@ -217,6 +220,55 @@ const FeeConfigForm = forwardRef( {errors.tx_fees_factor} + + + {t('settings.fees.label_max_sweep_fee_change', { + fee: isValidNumber(values.max_sweep_fee_change) + ? `(${factorToPercentage(values.max_sweep_fee_change!)}%)` + : '', + })} + + + {t('settings.fees.description_max_sweep_fee_change', { + defaultValue: `${factorToPercentage(JM_MAX_SWEEP_FEE_CHANGE_DEFAULT)}%`, + })} + + + + % + + { + const value = parseFloat(e.target.value) + setFieldValue('max_sweep_fee_change', percentageToFactor(value), true) + }} + isValid={touched.max_sweep_fee_change && !errors.max_sweep_fee_change} + isInvalid={touched.max_sweep_fee_change && !!errors.max_sweep_fee_change} + min={factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN)} + max={factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX)} + step={1} + /> + + {errors.max_sweep_fee_change} + + + @@ -323,6 +375,17 @@ export default function FeeConfigModal({ max: `${factorToPercentage(CJ_FEE_REL_MAX)}%`, }) } + + if ( + !isValidNumber(values.max_sweep_fee_change) || + values.max_sweep_fee_change! < MAX_SWEEP_FEE_CHANGE_MIN || + values.max_sweep_fee_change! > MAX_SWEEP_FEE_CHANGE_MAX + ) { + errors.max_sweep_fee_change = t('settings.fees.feedback_invalid_max_sweep_fee_change', { + min: `${factorToPercentage(MAX_SWEEP_FEE_CHANGE_MIN)}%`, + max: `${factorToPercentage(MAX_SWEEP_FEE_CHANGE_MAX)}%`, + }) + } return errors }, [t], @@ -346,6 +409,10 @@ export default function FeeConfigModal({ key: FEE_CONFIG_KEYS.max_cj_fee_rel, value: String(values.max_cj_fee_rel ?? ''), }, + { + key: FEE_CONFIG_KEYS.max_sweep_fee_change, + value: String(values.max_sweep_fee_change ?? ''), + }, ] setSaveErrorMessage(undefined) @@ -452,6 +519,7 @@ export default function FeeConfigModal({ formRef.current?.setFieldValue('max_cj_fee_rel', undefined, false) formRef.current?.setFieldValue('tx_fees', undefined, false) formRef.current?.setFieldValue('tx_fees_factor', undefined, false) + formRef.current?.setFieldValue('max_sweep_fee_change', undefined, false) setTimeout(() => formRef.current?.validateForm(), 4) }} disabled={isLoading || isSubmitting} diff --git a/src/constants/config.ts b/src/constants/config.ts index b91f47960..f80c07e50 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -23,3 +23,6 @@ export const JM_API_AUTH_TOKEN_EXPIRY: Milliseconds = Math.round(0.5 * 60 * 60 * // cap of dusty offer minsizes ("has dusty minsize, capping at 27300") // See: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.9.11/src/jmclient/configure.py#L70 (last check on 2024-04-22 of v0.9.11) export const JM_DUST_THRESHOLD = 27_300 + +// See: https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/v0.9.11/src/jmclient/configure.py#L321 (last check on 2024-07-09 of v0.9.11) +export const JM_MAX_SWEEP_FEE_CHANGE_DEFAULT = 0.8 diff --git a/src/hooks/Fees.ts b/src/hooks/Fees.ts index 339cc3b1c..fac10dbce 100644 --- a/src/hooks/Fees.ts +++ b/src/hooks/Fees.ts @@ -20,6 +20,7 @@ export const FEE_CONFIG_KEYS = { tx_fees_factor: { section: 'POLICY', field: 'tx_fees_factor' }, max_cj_fee_abs: { section: 'POLICY', field: 'max_cj_fee_abs' }, max_cj_fee_rel: { section: 'POLICY', field: 'max_cj_fee_rel' }, + max_sweep_fee_change: { section: 'POLICY', field: 'max_sweep_fee_change' }, } export interface FeeValues { @@ -27,6 +28,7 @@ export interface FeeValues { tx_fees_factor?: number max_cj_fee_abs?: number max_cj_fee_rel?: number + max_sweep_fee_change?: number } export const useLoadFeeConfigValues = () => { @@ -45,6 +47,7 @@ export const useLoadFeeConfigValues = () => { const parsedTxFeesFactor = parseFloat(policy.tx_fees_factor || '') const parsedMaxFeeAbs = parseInt(policy.max_cj_fee_abs || '', 10) const parsedMaxFeeRel = parseFloat(policy.max_cj_fee_rel || '') + const parsedMaxSweepFeeChange = parseFloat(policy.max_sweep_fee_change || '') const feeValues: FeeValues = { tx_fees: isValidNumber(parsedTxFees) @@ -56,6 +59,7 @@ export const useLoadFeeConfigValues = () => { tx_fees_factor: isValidNumber(parsedTxFeesFactor) ? parsedTxFeesFactor : undefined, max_cj_fee_abs: isValidNumber(parsedMaxFeeAbs) ? parsedMaxFeeAbs : undefined, max_cj_fee_rel: isValidNumber(parsedMaxFeeRel) ? parsedMaxFeeRel : undefined, + max_sweep_fee_change: isValidNumber(parsedMaxSweepFeeChange) ? parsedMaxSweepFeeChange : undefined, } return feeValues }, diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index 023d14730..fcbdf99f4 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -260,6 +260,9 @@ "label_max_cj_fee_rel": "Relative limit (per collaborator)", "description_max_cj_fee_rel": "The maximum fee you are willing to pay per collaborator, as a percentage of the transaction amount. Example: if you send 2 million sats and the maximum fee is set to 0.1%, you will at most pay 2,000 sats to a single collaborator.", "feedback_invalid_max_cj_fee_rel": "Please provide a valid maximum relative fee between {{ min }} and {{ max }}.", + "label_max_sweep_fee_change": "Collaborative sweep fee change", + "description_max_sweep_fee_change": "The maximum fee increase you are willing to pay on collaborative sweep transactions, as a percentage of the estimated transaction fee. Example: when set to 60% with 10 sats/vByte, the actual fee for collaborative sweeps can be between 4 and 16 sats/vByte. Default: {{ defaultValue }}", + "feedback_invalid_max_sweep_fee_change": "Please provide a valid maximum sweep fee change between {{ min }} and {{ max }}.", "text_button_cancel": "Cancel", "text_button_submit": "Save", "text_button_submitting": "Saving...", From a2c52110a7e6db3d4bf13d48f6c094fb9da66b31 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Fri, 19 Jul 2024 13:56:17 +0200 Subject: [PATCH 32/35] dev(regtest): update bitcoin core from v26 to v27 (#795) * dev(regtest): use polarlightning/bitcoind docker image * dev(regtest): update bitcoin core from v26 to v27 --- docker/regtest/docker-compose.yml | 73 +++++++++++++++---------------- docker/regtest/mine-block.sh | 2 +- 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/docker/regtest/docker-compose.yml b/docker/regtest/docker-compose.yml index f6becb41d..cea4802cc 100644 --- a/docker/regtest/docker-compose.yml +++ b/docker/regtest/docker-compose.yml @@ -92,51 +92,50 @@ services: bitcoind: container_name: jm_regtest_bitcoind restart: unless-stopped - image: btcpayserver/bitcoin:26.0 - environment: - BITCOIN_NETWORK: regtest - BITCOIN_WALLETDIR: "/walletdata" - BITCOIN_EXTRA_ARGS: | - rpcport=43782 - rpcbind=0.0.0.0 - rpcallowip=0.0.0.0/0 - port=39388 - whitelist=0.0.0.0/0 - maxmempool=500 - debug=rpc - logips=0 - networkactive=1 - dnsseed=0 - uacomment=jmdevbitcoindregtest - printpriority=1 - logtimemicros=1 - zmqpubrawblock=tcp://0.0.0.0:28332 - zmqpubrawtx=tcp://0.0.0.0:28333 - zmqpubhashblock=tcp://0.0.0.0:28334 - dns=0 - # do not automatically create tor hidden service - listenonion=0 - # rpcauth (user=regtest; password=regtest) - rpcauth=regtest:20b58677979ad9d3cf4b78b1d6e85e44$$2ec3e1e1c00c7c58d7aff1d4bf96e4a984ea1af5d676d862fd0faa857a1d4d7c - # rpcauth (user=joinmarket; password=joinmarket) - rpcauth=joinmarket:260b4c5b1fbd09d75a4aabf90226282f$$76e170af088d43a588992cdd5e7bae2242b03c33aa672cccfd1fb75f9281299e - # rpcauth (user=joinmarket2; password=joinmarket2) - rpcauth=joinmarket2:521bf9f4468529d49c0a41f9c9f8fdbf$$63ae94a73d2aa45e7ee756945d9b1e469f9873ce026b815d676a748f777e0b8d - # rpcauth (user=joinmarket3; password=joinmarket3) - rpcauth=joinmarket3:85d4beaa74540c3b08f4fef50d74a59e$$3033c779ea4bfd02a1f3403bc4d012f3e6d19b355f74c5e8de1d3439979d5e4b - # legacy wallet is being deprecated in v26: see https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-26.0.md#wallet - deprecatedrpc=create_bdb + image: polarlightning/bitcoind:27.0 + command: + # rpcauth (user=regtest; password=regtest) + # rpcauth (user=joinmarket; password=joinmarket) + # rpcauth (user=joinmarket2; password=joinmarket2) + # rpcauth (user=joinmarket3; password=joinmarket3) + -rpcauth=regtest:20b58677979ad9d3cf4b78b1d6e85e44$$2ec3e1e1c00c7c58d7aff1d4bf96e4a984ea1af5d676d862fd0faa857a1d4d7c + -rpcauth=joinmarket:260b4c5b1fbd09d75a4aabf90226282f$$76e170af088d43a588992cdd5e7bae2242b03c33aa672cccfd1fb75f9281299e + -rpcauth=joinmarket2:521bf9f4468529d49c0a41f9c9f8fdbf$$63ae94a73d2aa45e7ee756945d9b1e469f9873ce026b815d676a748f777e0b8d + -rpcauth=joinmarket3:85d4beaa74540c3b08f4fef50d74a59e$$3033c779ea4bfd02a1f3403bc4d012f3e6d19b355f74c5e8de1d3439979d5e4b + -datadir=/home/bitcoin/data + -walletdir=/home/bitcoin/walletdata + -regtest=1 + -server=1 + -port=39388 + -rpcport=43782 + -rpcbind=0.0.0.0 + -rpcallowip=0.0.0.0/0 + -whitelist=0.0.0.0/0 + -maxmempool=500 + -debug=rpc + -logips=0 + -networkactive=1 + -dnsseed=0 + -uacomment=jmdevbitcoindregtest + -printpriority=1 + -logtimemicros=1 + -zmqpubrawblock=tcp://0.0.0.0:28332 + -zmqpubrawtx=tcp://0.0.0.0:28333 + -zmqpubhashblock=tcp://0.0.0.0:28334 + -dns=0 + -listenonion=0 + -deprecatedrpc=create_bdb expose: - - 43782 # RPC - 39388 # P2P + - 43782 # RPC - 28332 # ZMQ - 28333 # ZMQ - 28334 # ZMQ ports: - "17782:43782" volumes: - - "bitcoin_datadir:/data" - - "bitcoin_wallet_datadir:/walletdata" + - "bitcoin_datadir:/home/bitcoin/data" + - "bitcoin_wallet_datadir:/home/bitcoin/walletdata" bitcoind_regtest_initializer: container_name: jm_regtest_bitcoind_initializer diff --git a/docker/regtest/mine-block.sh b/docker/regtest/mine-block.sh index f140b1d84..b1d446734 100755 --- a/docker/regtest/mine-block.sh +++ b/docker/regtest/mine-block.sh @@ -15,4 +15,4 @@ ADDRESS=${2:-bcrt1qrnz0thqslhxu86th069r9j6y7ldkgs2tzgf5wx} # default to a "rando [ "$BLOCKS" -ge 1 ] || die "Invalid parameter: 'blocks' must be a positve integer" [ -z "${ADDRESS-}" ] && die "Missing required parameter: 'address'" -docker exec -t jm_regtest_bitcoind bitcoin-cli -datadir=/data generatetoaddress "$BLOCKS" "$ADDRESS" +docker exec -t jm_regtest_bitcoind bitcoin-cli -datadir=/home/bitcoin/data -regtest -rpcport=43782 generatetoaddress "$BLOCKS" "$ADDRESS" From 8b29431155fc26124be1cba8b7a917dd107e8deb Mon Sep 17 00:00:00 2001 From: apX13_ Date: Mon, 22 Jul 2024 15:38:30 +0530 Subject: [PATCH 33/35] feat: quick freeze/unfreeze UTXOs on send page (#771) --- src/components/Send/SendForm.tsx | 7 +- src/components/Send/ShowUtxos.tsx | 175 +++++----------------- src/components/Send/SourceJarSelector.tsx | 114 +++++++++++--- src/components/Send/index.tsx | 14 +- 4 files changed, 142 insertions(+), 168 deletions(-) diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 51c937388..9f5bfb8af 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -26,7 +26,7 @@ import { isValidNumCollaborators, } from './helpers' import { AccountBalanceSummary } from '../../context/BalanceSummary' -import { WalletInfo, CurrentWallet, Utxo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet } from '../../context/WalletContext' import { useSettings } from '../../context/SettingsContext' import styles from './SendForm.module.css' import { TxFeeInputField, validateTxFee } from '../settings/TxFeeInputField' @@ -223,6 +223,7 @@ interface InnerSendFormProps { isLoading: boolean walletInfo?: WalletInfo wallet: CurrentWallet + wallet: CurrentWallet loadNewWalletAddress: (props: { signal: AbortSignal; jarIndex: JarIndex }) => Promise minNumCollaborators: number feeConfigValues?: FeeValues @@ -238,6 +239,7 @@ const InnerSendForm = ({ isLoading, walletInfo, wallet, + wallet, loadNewWalletAddress, minNumCollaborators, feeConfigValues, @@ -280,6 +282,7 @@ const InnerSendForm = ({ label={t('send.label_source_jar')} walletInfo={walletInfo} wallet={wallet} + wallet={wallet} isLoading={isLoading} disabled={disabled} variant={showCoinjoinPreconditionViolationAlert ? 'warning' : 'default'} @@ -386,8 +389,6 @@ type SendFormProps = Omit & { formRef?: React.Ref> blurred?: boolean wallet: CurrentWallet - isDisplayReloadInShowUtxos: boolean - setIsDisplayReloadInShowUtxos: (arg: boolean) => void } export const SendForm = ({ diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 4d480a153..03eacd75b 100644 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, memo, useRef, useMemo } from 'react' +import { useState, useEffect, useCallback, memo, useMemo } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import type { TFunction } from 'i18next' @@ -6,14 +6,7 @@ import classNames from 'classnames' import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' import { useTheme } from '@table-library/react-table-library/theme' import * as TableTypes from '@table-library/react-table-library/types/table' -import * as Api from '../../libs/JmWalletApi' -import { - WalletInfo, - CurrentWallet, - useReloadCurrentWalletInfo, - Utxo, - useCurrentWalletInfo, -} from '../../context/WalletContext' +import { WalletInfo, Utxo, useCurrentWalletInfo } from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' import Alert from '../Alert' import Balance from '../Balance' @@ -22,17 +15,18 @@ import Sprite from '../Sprite' import { utxoTags } from '../jar_details/UtxoList' import mainStyles from '../MainWalletView.module.css' import styles from './ShowUtxos.module.css' - -type UtxoList = Array +import { UtxoList } from './SourceJarSelector' interface ShowUtxosProps { - walletInfo: WalletInfo - wallet: CurrentWallet isOpen: boolean onCancel: () => void - jarIndex: String - isDisplayReloadInShowUtxos: boolean - setIsDisplayReloadInShowUtxos: (arg: boolean) => void + onConfirm: () => void + alert: SimpleAlert | undefined + isLoading: boolean + frozenUtxos: UtxoList + unFrozenUtxos: UtxoList + setFrozenUtxos: (arg: UtxoList) => void + setUnFrozenUtxos: (arg: UtxoList) => void } interface UtxoRowProps { @@ -286,135 +280,49 @@ const Divider = ({ isState, setIsState, className }: DividerProps) => { } const ShowUtxos = ({ - walletInfo, - wallet, isOpen, onCancel, - jarIndex, - isDisplayReloadInShowUtxos, - setIsDisplayReloadInShowUtxos, + onConfirm, + alert, + isLoading, + frozenUtxos, + unFrozenUtxos, + setFrozenUtxos, + setUnFrozenUtxos, }: ShowUtxosProps) => { - const [alert, setAlert] = useState(undefined) - const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) - const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) - const [frozenUtxos, setFrozenUtxos] = useState([]) - const [isLoading, setIsLoading] = useState(true) const { t } = useTranslation() - const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() const settings = useSettings() + const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) - const isHandleReloadExecuted = useRef(false) - - // Load data from wallet info - const loadData = useCallback( - (walletInfo: WalletInfo) => { - const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === jarIndex) - const utxos: any = data ? data[1] : [] - - const frozenUtxoList = utxos - .filter((utxo: any) => utxo.frozen) - .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) - const unFrozenUtxosList = utxos - .filter((utxo: any) => !utxo.frozen) - .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) - - setFrozenUtxos(frozenUtxoList) - setUnFrozenUtxos(unFrozenUtxosList) - - if (unFrozenUtxosList.length === 0) { - setShowFrozenUtxos(true) - setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) + // Handler to toggle UTXO selection + const handleUtxoCheckedState = useCallback( + (utxoIndex: number, isFrozen: boolean) => { + if (!isFrozen) { + const utxos = unFrozenUtxos.map((utxo: Utxo, i: number) => + i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo, + ) + setUnFrozenUtxos(utxos) } else { - setAlert(undefined) + const utxos = frozenUtxos.map((utxo: Utxo, i: number) => + i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo, + ) + setFrozenUtxos(utxos) } }, - [jarIndex, t], + [frozenUtxos, unFrozenUtxos, setUnFrozenUtxos, setFrozenUtxos], ) - // Reload wallet info - const handleReload = useCallback(async () => { - const abortCtrl = new AbortController() - try { - setIsLoading(true) - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) - if (isDisplayReloadInShowUtxos) { - await reloadCurrentWalletInfo.reloadDisplay({ signal: abortCtrl.signal }) - setIsDisplayReloadInShowUtxos(false) - } - loadData(walletInfo) - setIsLoading(false) - } catch (err: any) { - if (!abortCtrl.signal.aborted) { - setAlert({ variant: 'danger', message: err.message, dismissible: true }) - } - } - }, [isDisplayReloadInShowUtxos, setIsDisplayReloadInShowUtxos, reloadCurrentWalletInfo, loadData, walletInfo]) - - //Effect to Reload walletInfo only once - useEffect(() => { - if (!isHandleReloadExecuted.current) { - handleReload() - isHandleReloadExecuted.current = true - } - }, [handleReload]) - - //Effect to set Alert according to the walletInfo + //Effect to hide the Divider line when there is no unFrozen-UTXOs present useEffect(() => { - const frozenUtxosToUpdate = frozenUtxos.filter((utxo: Utxo) => utxo.checked && !utxo.locktime) - const timeLockedUtxo = frozenUtxos.find((utxo: Utxo) => utxo.checked && utxo.locktime) - const allUnFrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) - - if (timeLockedUtxo) { - setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) - } else if (allUnFrozenUnchecked && frozenUtxosToUpdate.length === 0) { - setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) - } else if (unFrozenUtxos.length !== 0 || frozenUtxosToUpdate.length !== 0) { - setAlert(undefined) - } - }, [frozenUtxos, unFrozenUtxos, t]) - - // Handler to toggle UTXO selection - const handleToggle = useCallback((utxoIndex: number, isFrozen: boolean) => { - if (!isFrozen) { - setUnFrozenUtxos((prevUtxos) => - prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), - ) - } else { - setFrozenUtxos((prevUtxos) => - prevUtxos.map((utxo, i) => (i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo)), - ) + if (unFrozenUtxos.length === 0 && frozenUtxos.length > 0) { + setShowFrozenUtxos(true) } - }, []) - - // Handler for the "confirm" button click - const handleConfirm = async () => { - const abortCtrl = new AbortController() - - const frozenUtxosToUpdate = frozenUtxos - .filter((utxo) => utxo.checked && !utxo.locktime) - .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) - const unFrozenUtxosToUpdate = unFrozenUtxos - .filter((utxo) => !utxo.checked) - .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) - - try { - await Promise.all([ - ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), - ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), - ]) - await handleReload() - onCancel() - } catch (err: any) { - if (!abortCtrl.signal.aborted) { - setAlert({ variant: 'danger', message: err.message, dismissible: true }) - } - } - } + }, [unFrozenUtxos.length, frozenUtxos.length]) return ( {alert && ( - setAlert(undefined)} - /> + )} void } interface ShowUtxosProps { - jarIndex: String - isOpen: boolean + jarIndex?: string + isOpen?: boolean } +export type UtxoList = Utxo[] + export const SourceJarSelector = ({ name, label, walletInfo, wallet, + wallet, variant, isLoading, disabled = false, @@ -38,13 +41,15 @@ export const SourceJarSelector = ({ setIsDisplayReloadInShowUtxos, }: SourceJarSelectorProps) => { const { t } = useTranslation() - const [field] = useField(name) const form = useFormikContext() - const [showUtxos, setShowUtxos] = useState({ - jarIndex: '', - isOpen: false, - }) + const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() + + const [showUtxos, setShowUtxos] = useState(undefined) + const [alert, setAlert] = useState(undefined) + const [isUtxosLoading, setIsUtxosLoading] = useState(false) + const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) + const [frozenUtxos, setFrozenUtxos] = useState([]) const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -53,6 +58,74 @@ export const SourceJarSelector = ({ ) }, [walletInfo]) + useEffect(() => { + if (showUtxos?.jarIndex && walletInfo?.utxosByJar) { + const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === showUtxos.jarIndex) + const utxos: any = data ? data[1] : [] + + const frozenUtxoList = utxos + .filter((utxo: any) => utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) + const unFrozenUtxosList = utxos + .filter((utxo: any) => !utxo.frozen) + .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) + + setFrozenUtxos(frozenUtxoList) + setUnFrozenUtxos(unFrozenUtxosList) + } + }, [walletInfo, showUtxos?.jarIndex, t]) + + useEffect(() => { + if (frozenUtxos.length === 0 && unFrozenUtxos.length === 0) { + return + } + const frozenUtxosToUpdate = frozenUtxos.filter((utxo: Utxo) => utxo.checked && !utxo.locktime) + const timeLockedUtxo = frozenUtxos.find((utxo: Utxo) => utxo.checked && utxo.locktime) + const allUnFrozenUnchecked = unFrozenUtxos.every((utxo: Utxo) => !utxo.checked) + + if (frozenUtxos.length > 0 && timeLockedUtxo) { + setAlert({ variant: 'danger', message: `${t('show_utxos.alert_for_time_locked')} ${timeLockedUtxo.locktime}` }) + } else if ( + (frozenUtxos.length > 0 || unFrozenUtxos.length > 0) && + allUnFrozenUnchecked && + frozenUtxosToUpdate.length === 0 + ) { + setAlert({ variant: 'warning', message: t('show_utxos.alert_for_unfreeze_utxos'), dismissible: true }) + } else { + setAlert(undefined) + } + }, [frozenUtxos, unFrozenUtxos, t, setAlert]) + + const handleUtxosFrozenState = useCallback(async () => { + const abortCtrl = new AbortController() + const frozenUtxosToUpdate = frozenUtxos + .filter((utxo) => utxo.checked && !utxo.locktime) + .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) + const unFrozenUtxosToUpdate = unFrozenUtxos + .filter((utxo) => !utxo.checked) + .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) + + try { + const res = await Promise.all([ + ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), + ]) + + if (res.length !== 0) { + setIsUtxosLoading(true) + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + } + + setShowUtxos(undefined) + } catch (err: any) { + if (!abortCtrl.signal.aborted) { + setAlert({ variant: 'danger', message: err.message, dismissible: true }) + } + } finally { + setIsUtxosLoading(false) + } + }, [frozenUtxos, unFrozenUtxos, wallet, reloadCurrentWalletInfo]) + return ( <> @@ -63,20 +136,19 @@ export const SourceJarSelector = ({ ) : (
- {showUtxos.isOpen && ( + {showUtxos?.isOpen && ( { - setShowUtxos({ - jarIndex: '', - isOpen: false, - }) + setShowUtxos(undefined) }} - jarIndex={showUtxos.jarIndex} - isDisplayReloadInShowUtxos={isDisplayReloadInShowUtxos} - setIsDisplayReloadInShowUtxos={setIsDisplayReloadInShowUtxos} + alert={alert} + isLoading={isUtxosLoading} + frozenUtxos={frozenUtxos} + unFrozenUtxos={unFrozenUtxos} + setFrozenUtxos={setFrozenUtxos} + setUnFrozenUtxos={setUnFrozenUtxos} /> )} {jarBalances.map((it) => { diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 3101f8d32..153559887 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -207,15 +207,13 @@ export default function Send({ wallet }: SendProps) { setAlert({ variant: 'danger', message }) }) - const loadingWalletInfoAndUtxos = reloadCurrentWalletInfo - .reloadUtxos({ signal: abortCtrl.signal }) - .catch((err) => { - if (abortCtrl.signal.aborted) return - const message = t('global.errors.error_loading_wallet_failed', { - reason: err.message || t('global.errors.reason_unknown'), - }) - setAlert({ variant: 'danger', message }) + const loadingWalletInfoAndUtxos = reloadCurrentWalletInfo.reloadAll({ signal: abortCtrl.signal }).catch((err) => { + if (abortCtrl.signal.aborted) return + const message = t('global.errors.error_loading_wallet_failed', { + reason: err.message || t('global.errors.reason_unknown'), }) + setAlert({ variant: 'danger', message }) + }) const loadingMinimumMakerConfig = loadConfigValue({ signal: abortCtrl.signal, From 4d74ab262abece19160eb7cfbc7c17bd8bc140bd Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Tue, 6 Aug 2024 13:32:45 +0200 Subject: [PATCH 34/35] feat: unit and visibility toggle on main wallet view (#806) --- src/components/MainWalletView.tsx | 33 ++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/MainWalletView.tsx b/src/components/MainWalletView.tsx index 4b636c310..efadf1d18 100644 --- a/src/components/MainWalletView.tsx +++ b/src/components/MainWalletView.tsx @@ -148,18 +148,29 @@ export default function MainWalletView({ wallet }: MainWalletViewProps) { + ) : !currentWalletInfo || isLoading ? ( + + + ) : ( - settingsDispatch({ showBalance: !settings.showBalance })} className="cursor-pointer"> - {!currentWalletInfo || isLoading ? ( - - ) : ( - - )} + { + if (!settings.showBalance) { + settingsDispatch({ unit: 'BTC', showBalance: true }) + } else if (settings.unit === 'BTC') { + settingsDispatch({ unit: 'sats', showBalance: true }) + } else { + settingsDispatch({ unit: 'BTC', showBalance: false }) + } + }} + > + )}
From 76d090366b4dc6f1de5293acf8ccfbf6132aead5 Mon Sep 17 00:00:00 2001 From: amitx13 Date: Thu, 27 Jun 2024 14:31:24 +0530 Subject: [PATCH 35/35] bug fixing --- src/components/Send/index.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/Send/index.tsx b/src/components/Send/index.tsx index 153559887..3d030fb70 100644 --- a/src/components/Send/index.tsx +++ b/src/components/Send/index.tsx @@ -18,8 +18,6 @@ import { useLoadConfigValue } from '../../context/ServiceConfigContext' import { useWaitForUtxosToBeSpent } from '../../hooks/WaitForUtxosToBeSpent' import { routes } from '../../constants/routes' import { JM_MINIMUM_MAKERS_DEFAULT } from '../../constants/config' -import { useSettings } from '../../context/SettingsContext' -import { UtxoListDisplay, Divider } from './ShowUtxos' import { initialNumCollaborators } from './helpers' @@ -139,7 +137,6 @@ export default function Send({ wallet }: SendProps) { const [showConfirmAbortModal, setShowConfirmAbortModal] = useState(false) const [showConfirmSendModal, setShowConfirmSendModal] = useState() - const [showSelectedUtxos, setShowSelectedUtxos] = useState(false) const initialValues = useMemo( () => createInitialValues(initNumCollaborators, feeConfigValues),