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(orderbook): show fidelity bond value and locktime #766

Merged
merged 5 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
theborakompanioni marked this conversation as resolved.
Show resolved Hide resolved

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part looks fine! Should amount also be explicitly converted to string?

Copy link
Collaborator Author

@theborakompanioni theborakompanioni May 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I think I know what you mean.. amount is passed as String to the Balance component. I think I'd rather want to have the type info in the object rather than storing it as plain string here. Okay for you?

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