Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: spend fidelity bond #556

Merged
merged 32 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4a0d3a3
dev: ability to append content to ExistingFidelityBond component
theborakompanioni Oct 29, 2022
bcb09cb
send-fb - wip
theborakompanioni Oct 30, 2022
c0bb836
refactor: remove most code and revert to simplest method
theborakompanioni Oct 31, 2022
be21ad4
refactor: rename SpendFidelityBond to MoveFidelityBondModal
theborakompanioni Oct 31, 2022
71aac85
refactor: rename successfulSendData to txInfo
theborakompanioni Oct 31, 2022
466d1b2
dev: report to parent if data has changed and should be reloaded
theborakompanioni Oct 31, 2022
5bf8795
refactor(i18n): add global.errors keys
theborakompanioni Oct 31, 2022
d35fb58
chore(i18n): proper translation strings in move fb modal
theborakompanioni Oct 31, 2022
846cc66
fix: keep on unfreezing previously frozen coins on errors
theborakompanioni Oct 31, 2022
0672475
fix: improve resolving of api error messages
theborakompanioni Nov 1, 2022
a5de13d
dev: freeze previously unfrozen fidelity bond on error
theborakompanioni Nov 1, 2022
23b7dc2
refactor: use generic 'spendUtxosWithDirectSend' method
theborakompanioni Nov 1, 2022
ada3fce
refactor: externalize PaymentConfirmModal
theborakompanioni Nov 1, 2022
e69717d
feat: payment summary in confirm modal before moving fb
theborakompanioni Nov 1, 2022
e3230fb
ui: hide move button when maker/taker is running
theborakompanioni Nov 1, 2022
866948e
refactor: simplify code and fix typos in MoveFidelityBondModal
theborakompanioni Nov 1, 2022
03118cd
ui: align with figma wording and style
theborakompanioni Nov 1, 2022
07e97e4
dev: sort fidelity bond by value and lock state
theborakompanioni Nov 2, 2022
93b6078
refactor: PaymentConfimModal from jsx to tsx
theborakompanioni Nov 2, 2022
d6ac7de
refactor: indicate sweep tx when moving fb
theborakompanioni Nov 2, 2022
ed91ae4
refactor: prevent unnecessary rerendering of Balance component
theborakompanioni Nov 2, 2022
d572d51
dev: prevent modals from being rendered unnecessarily
theborakompanioni Nov 2, 2022
81bcb6c
ui: always display FB utxos with lock icon in JarDetails
theborakompanioni Nov 2, 2022
938b9e7
Merge branch 'master' into spend-fidelity-bond
theborakompanioni Nov 9, 2022
7553755
Merge branch 'master' into spend-fidelity-bond
theborakompanioni Nov 15, 2022
d0a380c
Merge branch 'master' into spend-fidelity-bond
theborakompanioni Nov 23, 2022
5f881cb
Merge branch 'master' into spend-fidelity-bond
theborakompanioni Dec 9, 2022
ab8a602
review(fb): change wording from 'moving' to 'unlocking'
theborakompanioni Dec 9, 2022
fbf0a59
fix: resolve promise value when reloading utxos
theborakompanioni Dec 9, 2022
55db232
review(fb): preselect destination jar
theborakompanioni Dec 9, 2022
5bf9d05
review(fb): hide/show privacy info in payment confirm modal
theborakompanioni Dec 9, 2022
3bf14f5
review(fb): ability to skip jar selection when spending bond
theborakompanioni Dec 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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