Skip to content

Commit

Permalink
feat(orderbook): show fidelity bond value and locktime (#766)
Browse files Browse the repository at this point in the history
* fix(orderbook): do not follow redirects on refresh orderbook

* fix(orderbook): display loading state correctly

* feat(orderbook): display fidelity bond amount and locktime
  • Loading branch information
theborakompanioni authored May 28, 2024
1 parent 056c9d4 commit 3bcbdee
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 22 deletions.
2 changes: 0 additions & 2 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ export default function App() {
[reloadCurrentWalletInfo],
)

debugger

const router = createBrowserRouter(
createRoutesFromElements(
<Route
Expand Down
90 changes: 71 additions & 19 deletions src/components/Orderbook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ import { useSort, HeaderCellSort, SortToggleType } from '@table-library/react-ta
import * as TableTypes from '@table-library/react-table-library/types/table'
import { useTheme } from '@table-library/react-table-library/theme'
import * as rb from 'react-bootstrap'
import { TFunction } from 'i18next'
import { TFunction, i18n } from 'i18next'
import { useTranslation } from 'react-i18next'
import { Helper as ApiHelper } from '../libs/JmWalletApi'
import { AmountSats, Helper as ApiHelper } from '../libs/JmWalletApi'
import * as ObwatchApi from '../libs/JmObwatchApi'
import { useSettings } from '../context/SettingsContext'
import Balance from './Balance'
import Sprite from './Sprite'
import TablePagination from './TablePagination'
import { factorToPercentage, isAbsoluteOffer, isRelativeOffer } from '../utils'
import { BTC, factorToPercentage, isAbsoluteOffer, isRelativeOffer } from '../utils'
import { isDebugFeatureEnabled, isDevMode } from '../constants/debugFeatures'
import ToggleSwitch from './ToggleSwitch'
import { pseudoRandomNumber } from './Send/helpers'
import { JM_DUST_THRESHOLD } from '../constants/config'
import * as fb from './fb/utils'
import styles from './Orderbook.module.css'

const TABLE_THEME = {
Expand Down Expand Up @@ -102,6 +103,10 @@ interface OrderTableEntry {
bondValue: {
value: number
displayValue: string // example: "0" (no fb) or "114557102085.28133"
locktime?: number
displayLocktime?: string
displayExpiresIn?: string
amount?: AmountSats
}
}

Expand Down Expand Up @@ -170,7 +175,34 @@ const renderOrderAsRow = (item: OrderTableRow, settings: any) => {
<Cell hide={true}>
<Balance valueString={item.minerFeeContribution} convertToUnit={settings.unit} showBalance={true} />
</Cell>
<Cell className="font-monospace">{item.bondValue.displayValue}</Cell>
<Cell className="font-monospace">
{item.bondValue.value > 0 ? (
<rb.OverlayTrigger
popperConfig={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 10],
},
},
],
}}
overlay={(props) => (
<rb.Tooltip {...props}>
<Balance valueString={String(item.bondValue.amount)} convertToUnit={BTC} showBalance={true} />
<div className="small">
{item.bondValue.displayLocktime} ({item.bondValue.displayExpiresIn})
</div>
</rb.Tooltip>
)}
>
<span>{item.bondValue.displayValue}</span>
</rb.OverlayTrigger>
) : (
<>{item.bondValue.displayValue}</>
)}
</Cell>
</Row>
)
}
Expand Down Expand Up @@ -290,9 +322,13 @@ const OrderbookTable = ({ data }: OrderbookTableProps) => {
)
}

const offerToTableEntry = (offer: ObwatchApi.Offer, t: TFunction): OrderTableEntry => {
const offerToTableEntry = (
offer: ObwatchApi.Offer,
fidelityBond: ObwatchApi.FidelityBond | undefined,
i18n: i18n,
): OrderTableEntry => {
return {
type: orderTypeProps(offer, t),
type: orderTypeProps(offer, i18n.t),
counterparty: offer.counterparty,
orderId: String(offer.oid),
fee:
Expand All @@ -314,21 +350,32 @@ const offerToTableEntry = (offer: ObwatchApi.Offer, t: TFunction): OrderTableEnt
bondValue: {
value: offer.fidelity_bond_value,
displayValue: String(offer.fidelity_bond_value.toFixed(0)),
locktime: fidelityBond?.locktime,
displayLocktime:
fidelityBond?.locktime !== undefined ? new Date(fidelityBond.locktime * 1_000).toDateString() : undefined,
displayExpiresIn:
fidelityBond?.locktime !== undefined
? fb.time.humanReadableDuration({
to: fidelityBond.locktime * 1_000,
locale: i18n.resolvedLanguage || i18n.language,
})
: undefined,
amount: fidelityBond?.amount,
},
}
}

interface OrderbookProps {
entries: OrderTableEntry[]
refresh: (signal: AbortSignal) => Promise<void>
isLoading: boolean
nickname?: string
}

export function Orderbook({ entries, refresh, nickname }: OrderbookProps) {
export function Orderbook({ entries, refresh, isLoading: isLoadingRefresh, nickname }: OrderbookProps) {
const { t } = useTranslation()
const settings = useSettings()
const [search, setSearch] = useState('')
const [isLoadingRefresh, setIsLoadingRefresh] = useState(false)
const [isHighlightOwnOffers, setIsHighlightOwnOffers] = useState(false)
const [isPinToTopOwnOffers, setIsPinToTopOwnOffers] = useState(false)
const [highlightedOrders, setHighlightedOrders] = useState<OrderTableEntry[]>([])
Expand Down Expand Up @@ -389,12 +436,9 @@ export function Orderbook({ entries, refresh, nickname }: OrderbookProps) {
onClick={() => {
if (isLoadingRefresh) return

setIsLoadingRefresh(true)

const abortCtrl = new AbortController()
refresh(abortCtrl.signal).finally(() => {
// as refreshing is fast most of the time, add a short delay to avoid flickering
setTimeout(() => setIsLoadingRefresh(false), 250)
console.log('Finished reloading orderbook.')
})
}}
>
Expand Down Expand Up @@ -482,13 +526,19 @@ type OrderbookOverlayProps = rb.OffcanvasProps & {
}

export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayProps) {
const { t } = useTranslation()
const { t, i18n } = useTranslation()
const [alert, setAlert] = useState<SimpleAlert>()
const [isInitialized, setIsInitialized] = useState(false)
const [isLoading, setIsLoading] = useState(true)
const [offers, setOffers] = useState<ObwatchApi.Offer[]>()
const tableEntries = useMemo(() => offers && offers.map((offer) => offerToTableEntry(offer, t)), [offers, t])
const [fidelityBonds, setFidelityBonds] = useState<Map<string, ObwatchApi.FidelityBond>>()
const [__dev_showGenerateDemoOfferButton] = useState(isDebugFeatureEnabled('enableDemoOrderbook'))
const tableEntries = useMemo(() => {
return (
offers &&
offers.map((offer) => offerToTableEntry(offer, fidelityBonds && fidelityBonds.get(offer.counterparty), i18n))
)
}, [offers, fidelityBonds, i18n])

const __dev_generateDemoReportEntryButton = () => {
const randomMinsize = pseudoRandomNumber(JM_DUST_THRESHOLD, JM_DUST_THRESHOLD + 100_000)
Expand All @@ -511,9 +561,10 @@ export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayPro

const refresh = useCallback(
(signal: AbortSignal) => {
return ObwatchApi.refreshOrderbook({ signal })
setIsLoading(true)
return ObwatchApi.refreshOrderbook({ signal, redirect: 'manual' })
.then((res) => {
if (!res.ok) {
if (!res.ok && res.type !== 'opaqueredirect') {
// e.g. error is raised if ob-watcher is not running
return ApiHelper.throwError(res)
}
Expand All @@ -523,15 +574,18 @@ export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayPro
.then((orderbook) => {
if (signal.aborted) return

setIsLoading(false)
setAlert(undefined)
setOffers(orderbook.offers || [])
setFidelityBonds(new Map((orderbook.fidelitybonds || []).map((it) => [it.counterparty, it])))

if (isDevMode()) {
console.table(orderbook.offers)
}
})
.catch((e) => {
if (signal.aborted) return
setIsLoading(false)
const message = t('orderbook.error_loading_orderbook_failed', {
reason: e.message || t('global.errors.reason_unknown'),
})
Expand All @@ -546,10 +600,8 @@ export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayPro

const abortCtrl = new AbortController()

setIsLoading(true)
refresh(abortCtrl.signal).finally(() => {
if (abortCtrl.signal.aborted) return
setIsLoading(false)
setIsInitialized(true)
})

Expand Down Expand Up @@ -617,7 +669,7 @@ export function OrderbookOverlay({ nickname, show, onHide }: OrderbookOverlayPro
{tableEntries && (
<rb.Row>
<rb.Col className="px-0">
<Orderbook nickname={nickname} entries={tableEntries} refresh={refresh} />
<Orderbook nickname={nickname} entries={tableEntries} refresh={refresh} isLoading={isLoading} />
</rb.Col>
</rb.Row>
)}
Expand Down
16 changes: 15 additions & 1 deletion src/libs/JmObwatchApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,21 @@ export interface Offer {
fidelity_bond_value: number // example: 0 (no fb) or 114557102085.28133
}

export interface FidelityBond {
counterparty: string // example: "J5Bv3JSxPFWm2Yjb"
bond_value: number // example: 82681607.26848702,
locktime: number // example: 1725148800
amount: AmountSats // example: 312497098
script: string // example: 002059e6f4a2afbb87c9967530955d091d7954f9364cd829b6012bdbcb38fab5e383
utxo_confirmations: number // example: 10
utxo_confirmation_timestamp: number // example: 1716876653
utxo_pub: string // example: 02d46a9001a5430c0aa1e3ad0c004b409a932d3ae99b19617f0ab013b12076c082
cert_expiry: number // example: 1
}

export interface OrderbookJson {
offers?: Offer[]
fidelitybonds?: FidelityBond[]
}

const orderbookJson = async ({ signal }: { signal: AbortSignal }) => {
Expand All @@ -27,9 +40,10 @@ const fetchOrderbook = async (options: { signal: AbortSignal }): Promise<Orderbo
return orderbookJson(options).then((res) => (res.ok ? res.json() : ApiHelper.throwError(res)))
}

const refreshOrderbook = async ({ signal }: { signal: AbortSignal }) => {
const refreshOrderbook = async ({ signal, redirect }: { signal: AbortSignal; redirect: RequestRedirect }) => {
return await fetch(`${basePath()}/refreshorderbook`, {
method: 'POST',
redirect,
signal,
})
}
Expand Down

0 comments on commit 3bcbdee

Please sign in to comment.