Skip to content

Commit

Permalink
feat: spend fidelity bond (#556)
Browse files Browse the repository at this point in the history
* dev: ability to append content to ExistingFidelityBond component

* refactor(i18n): add global.errors keys

* fix: improve resolving of api error messages

* refactor: use generic 'spendUtxosWithDirectSend' method

this method can lator on be used to send a fidelity bond
to an external destination, renew the fidelity bond, or
generally send selected utxos without manually freezing/
unfreezing them before.

* refactor: externalize PaymentConfirmModal

* dev: sort fidelity bond by value and lock state

* refactor: prevent unnecessary rerendering of Balance component

* ui: always display FB utxos with lock icon in JarDetails
  • Loading branch information
theborakompanioni authored Dec 13, 2022
1 parent 4fca8f1 commit 9f42dac
Show file tree
Hide file tree
Showing 24 changed files with 962 additions and 300 deletions.
4 changes: 4 additions & 0 deletions public/sprite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 28 additions & 28 deletions src/components/Balance.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { SATS, BTC, btcToSats, satsToBtc, formatBtc, formatSats } from '../utils'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { SATS, BTC, btcToSats, satsToBtc, formatBtc, formatSats, isValidNumber } from '../utils'
import Sprite from './Sprite'
import * as rb from 'react-bootstrap'
import styles from './Balance.module.css'
Expand Down Expand Up @@ -57,25 +57,14 @@ export default function Balance({
setDisplayMode(getDisplayMode(convertToUnit, isBalanceVisible))
}, [convertToUnit, isBalanceVisible])

if (loading) {
return (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder
data-testid="balance-component-placeholder"
className={styles['balance-component-placeholder']}
/>
</rb.Placeholder>
)
}

const toggleVisibility = (e) => {
const toggleVisibility = useCallback((e) => {
e.preventDefault()
e.stopPropagation()

setIsBalanceVisible((current) => !current)
}
}, [])

const balanceComponent = (() => {
const balanceComponent = useMemo(() => {
if (displayMode === DISPLAY_MODE_HIDDEN) {
return (
<BalanceComponent
Expand All @@ -90,15 +79,15 @@ export default function Balance({
)
}

if (typeof valueString !== 'string') {
console.warn('<Balance /> component expects string input')
if (typeof valueString !== 'string' || !isValidNumber(parseFloat(valueString))) {
console.warn('<Balance /> component expects number input as string')
return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} />
}

// Treat integers as sats.
const valueIsSats = valueString === Number.parseInt(valueString).toString()
const valueIsSats = valueString === parseInt(valueString, 10).toString()
// Treat decimal numbers as btc.
const valueIsBtc = !valueIsSats && !Number.isNaN(Number.parseFloat(valueString)) && valueString.indexOf('.') > -1
const valueIsBtc = !valueIsSats && !isNaN(parseFloat(valueString)) && valueString.indexOf('.') > -1

const btcSymbol = (
<span className="balance-symbol" style={{ paddingRight: '0.1em' }}>
Expand All @@ -118,16 +107,27 @@ export default function Balance({
return <BalanceComponent symbol={btcSymbol} value={formatBtc(satsToBtc(valueString))} symbolIsPrefix={true} />

console.warn('<Balance /> component cannot determine balance format')
return <BalanceComponent symbol={''} value={valueString} symbolIsPrefix={false} />
})()
return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} />
}, [valueString, displayMode])

if (loading) {
return (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder
data-testid="balance-component-placeholder"
className={styles['balance-component-placeholder']}
/>
</rb.Placeholder>
)
}

if (!enableVisibilityToggle) {
return <>{balanceComponent}</>
} else {
return (
<span onClick={toggleVisibility} style={{ cursor: 'pointer' }}>
{balanceComponent}
</span>
)
}

return (
<span onClick={toggleVisibility} style={{ cursor: 'pointer' }}>
{balanceComponent}
</span>
)
}
6 changes: 5 additions & 1 deletion src/components/Balance.test.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react'
import { act } from 'react-dom/test-utils'
import user from '@testing-library/user-event'
import { render, screen } from '../testUtils'
Expand All @@ -12,6 +11,11 @@ describe('<Balance />', () => {
expect(screen.getByTestId('balance-component-placeholder')).toBeInTheDocument()
})

it('should render invalid param as given', () => {
render(<Balance valueString={'NaN'} convertToUnit={BTC} showBalance={true} />)
expect(screen.getByText(`NaN`)).toBeInTheDocument()
})

it('should render BTC using satscomma formatting', () => {
render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} />)
expect(screen.getByText(`123.45 600 000`)).toBeInTheDocument()
Expand Down
62 changes: 54 additions & 8 deletions src/components/Earn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import { useCurrentWalletInfo, useReloadCurrentWalletInfo } from '../context/Wal
import { useServiceInfo, useReloadServiceInfo } from '../context/ServiceInfoContext'
import { factorToPercentage, percentageToFactor } from '../utils'
import * as Api from '../libs/JmWalletApi'
import * as fb from './fb/utils'
import Sprite from './Sprite'
import PageTitle from './PageTitle'
import SegmentedTabs from './SegmentedTabs'
import { CreateFidelityBond } from './fb/CreateFidelityBond'
import { ExistingFidelityBond } from './fb/ExistingFidelityBond'
import { SpendFidelityBondModal } from './fb/SpendFidelityBondModal'
import { EarnReportOverlay } from './EarnReport'
import { OrderbookOverlay } from './Orderbook'
import Balance from './Balance'
Expand Down Expand Up @@ -62,7 +64,7 @@ const persistFormValues = (values) => {
}
}

const initialFormValues = (settings) => ({
const initialFormValues = () => ({
offertype:
window.localStorage.getItem(FORM_INPUT_LOCAL_STORAGE_KEYS.offertype) || FORM_INPUT_DEFAULT_VALUES.offertype,
feeRel:
Expand Down Expand Up @@ -187,6 +189,8 @@ export default function Earn({ wallet }) {
return currentWalletInfo?.fidelityBondSummary.fbOutputs || []
}, [currentWalletInfo])

const [moveToJarFidelityBondId, setMoveToJarFidelityBondId] = useState()

const startMakerService = (ordertype, minsize, cjfee_a, cjfee_r) => {
setIsSending(true)
setIsWaitingMakerStart(true)
Expand Down Expand Up @@ -280,8 +284,8 @@ export default function Earn({ wallet }) {
setIsLoading(true)

new Promise((resolve) => {
setTimeout(() => {
resolve(reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }))
setTimeout(async () => {
resolve(await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }))
}, delay)
})
.catch((err) => {
Expand All @@ -298,7 +302,7 @@ export default function Earn({ wallet }) {
const feeRelMax = 0.1 // 10%
const feeRelPercentageStep = 0.0001

const initialValues = initialFormValues(settings)
const initialValues = initialFormValues()

const validate = (values) => {
const errors = {}
Expand Down Expand Up @@ -396,11 +400,53 @@ export default function Earn({ wallet }) {
subtitle={t('earn.subtitle_fidelity_bonds')}
/>
<div className="d-flex flex-column gap-3">
{fidelityBonds.length > 0 && (
{currentWalletInfo && fidelityBonds.length > 0 && (
<>
{fidelityBonds.map((fidelityBond, index) => (
<ExistingFidelityBond key={index} fidelityBond={fidelityBond} />
))}
{moveToJarFidelityBondId && (
<SpendFidelityBondModal
show={true}
fidelityBondId={moveToJarFidelityBondId}
wallet={wallet}
walletInfo={currentWalletInfo}
destinationJarIndex={0}
onClose={({ mustReload }) => {
setMoveToJarFidelityBondId(undefined)
if (mustReload) {
reloadFidelityBonds({ delay: 0 })
}
}}
/>
)}
{fidelityBonds.map((fidelityBond, index) => {
const isExpired = !fb.utxo.isLocked(fidelityBond)
const actionsEnabled =
isExpired &&
serviceInfo &&
!serviceInfo.coinjoinInProgress &&
!serviceInfo.makerRunning &&
!isWaitingMakerStart &&
!isWaitingMakerStop &&
!isLoading
return (
<ExistingFidelityBond key={index} fidelityBond={fidelityBond}>
{actionsEnabled && (
<div className="mt-4">
<div className="">
<rb.Button
variant={settings.theme === 'dark' ? 'light' : 'dark'}
className="w-50 d-flex justify-content-center align-items-center"
disabled={moveToJarFidelityBondId !== undefined}
onClick={() => setMoveToJarFidelityBondId(fidelityBond.utxo)}
>
<Sprite className="me-1 mb-1" symbol="unlock" width="24" height="24" />
{t('earn.fidelity_bond.existing.button_spend')}
</rb.Button>
</div>
</div>
)}
</ExistingFidelityBond>
)
})}
</>
)}
<>
Expand Down
9 changes: 7 additions & 2 deletions src/components/Jam.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,18 @@ export default function Jam({ wallet }) {
const abortCtrl = new AbortController()
const loadingServiceInfo = reloadServiceInfo({ signal: abortCtrl.signal }).catch((err) => {
if (abortCtrl.signal.aborted) return
const message = err.message || t('send.error_loading_wallet_failed')
// reusing "wallet failed" message here is okay, as session info also contains wallet information
const message = t('global.errors.error_loading_wallet_failed', {
reason: err.message || t('global.errors.reason_unknown'),
})
setAlert({ variant: 'danger', message })
})

const loadingWalletInfo = reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }).catch((err) => {
if (abortCtrl.signal.aborted) return
const message = err.message || t('send.error_loading_wallet_failed')
const message = t('global.errors.error_loading_wallet_failed', {
reason: err.message || t('global.errors.reason_unknown'),
})
setAlert({ variant: 'danger', message })
})

Expand Down
2 changes: 1 addition & 1 deletion src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
import styles from './Modal.module.css'
import Sprite from './Sprite'

interface ConfirmModalProps {
export interface ConfirmModalProps {
isShown: boolean
title: ReactNode | string
onCancel: () => void
Expand Down
6 changes: 6 additions & 0 deletions src/components/PaymentConfirmModal.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.infoIcon {
margin: 2px 0 0 0.25rem;
color: var(--bs-gray-500);
border: 1px solid var(--bs-gray-500);
border-radius: 50%;
}
Loading

0 comments on commit 9f42dac

Please sign in to comment.