Repository: joinmarket-webui/joinmarket-webui (js)

Issue:
Expired fidelity bonds are still shown as locked in UTXO selector
Check if the `utxo.lockdate` is in the past and show the "locked" label only when the UTXO is in fact still locked.

Pull request:
fix: Expired fidelity bonds are still shown as locked in UTXO selector
Resolves #373.

Small refactoring: Move function `isLocked` to check expiry date of timelocked utxo to proper place from `BalanceSummary` to `fb/utils`.


Patch:
diff --git a/src/components/DisplayUTXOs.jsx b/src/components/DisplayUTXOs.jsx
index 3cd9e0d..c457c59 100644
--- a/src/components/DisplayUTXOs.jsx
+++ b/src/components/DisplayUTXOs.jsx
@@ -7,7 +7,7 @@ import Alert from './Alert'
 import { useSettings } from '../context/SettingsContext'
 import { useCurrentWallet } from '../context/WalletContext'
 import { useServiceInfo } from '../context/ServiceInfoContext'
-import { isLocked } from '../hooks/BalanceSummary'
+import * as fb from './fb/utils'
 import * as Api from '../libs/JmWalletApi'
 
 const Utxo = ({ utxo, ...props }) => {
@@ -22,7 +22,7 @@ const Utxo = ({ utxo, ...props }) => {
   const isOperationEnabled = useCallback(() => {
     const noServiceIsRunning = serviceInfo && !serviceInfo.makerRunning && !serviceInfo.coinjoinInProgress
 
-    const isUnfreezeEnabled = !isLocked(utxo)
+    const isUnfreezeEnabled = !fb.utxo.isLocked(utxo)
     const allowedToExecute = !utxo.frozen || isUnfreezeEnabled
 
     return noServiceIsRunning && allowedToExecute
diff --git a/src/components/fb/FidelityBondSteps.tsx b/src/components/fb/FidelityBondSteps.tsx
index 6e03f0f..a2e0676 100644
--- a/src/components/fb/FidelityBondSteps.tsx
+++ b/src/components/fb/FidelityBondSteps.tsx
@@ -108,6 +108,8 @@ const UtxoCard = ({
 }: UtxoCardProps) => {
   const settings = useSettings()
 
+  const utxoIsLocked = useMemo(() => fb.utxo.isLocked(utxo), [utxo])
+
   return (
     <div
       className={cx('utxoCard', { selected: isSelected, selectable: isSelectable })}
@@ -130,13 +132,13 @@ const UtxoCard = ({
           <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" />
         </div>
       )}
-      {!isLoading && utxo.frozen && !utxo.locktime && (
+      {!isLoading && utxo.frozen && !utxoIsLocked && (
         <div className={cx('utxoLabel', 'utxoFrozen')}>
           <Sprite symbol="lock" width="18" height="18" />
           <div>frozen</div>
         </div>
       )}
-      {!isLoading && utxo.locktime && (
+      {!isLoading && utxoIsLocked && (
         <div className={cx('utxoLabel', 'utxoFidelityBond')}>
           <Sprite symbol="lock" width="18" height="18" />
           <div>locked</div>
diff --git a/src/components/fb/utils.ts b/src/components/fb/utils.ts
index 8e7d7c5..0477be5 100644
--- a/src/components/fb/utils.ts
+++ b/src/components/fb/utils.ts
@@ -2,6 +2,7 @@ import { Lockdate } from '../../libs/JmWalletApi'
 import { Utxo } from '../../context/WalletContext'
 
 type Milliseconds = number
+type Seconds = number
 
 export type YearsRange = {
   min: number
@@ -91,5 +92,17 @@ export const utxo = (() => {
 
   const allAreFrozen = (utxos: Array<Utxo>) => utxos.every((utxo) => utxo.frozen)
 
-  return { isEqual, isInList, utxosToFreeze, allAreFrozen }
+  const isLocked = (utxo: Utxo, refTime: Milliseconds = Date.now()) => {
+    if (!utxo.locktime) return false
+
+    const pathAndLocktime = utxo.path.split(':')
+    if (pathAndLocktime.length !== 2) return false
+
+    const locktimeUnixTimestamp: Seconds = parseInt(pathAndLocktime[1], 10)
+    if (Number.isNaN(locktimeUnixTimestamp)) return false
+
+    return locktimeUnixTimestamp * 1_000 >= refTime
+  }
+
+  return { isEqual, isInList, utxosToFreeze, allAreFrozen, isLocked }
 })()
diff --git a/src/hooks/BalanceSummary.ts b/src/hooks/BalanceSummary.ts
index 29ff500..a797bc0 100644
--- a/src/hooks/BalanceSummary.ts
+++ b/src/hooks/BalanceSummary.ts
@@ -1,10 +1,10 @@
 import { useEffect, useMemo, useState } from 'react'
-import { WalletInfo, BalanceString, Utxos, Utxo } from '../context/WalletContext'
+import { WalletInfo, BalanceString, Utxos } from '../context/WalletContext'
 
+import * as fb from '../components/fb/utils'
 import { AmountSats } from '../libs/JmWalletApi'
 
 type Milliseconds = number
-type Seconds = number
 
 interface BalanceSummary {
   totalBalance: BalanceString
@@ -49,20 +49,8 @@ export type WalletBalanceSummary = BalanceSummarySupport & {
   accountBalances: AccountBalances
 }
 
-export const isLocked = (utxo: Utxo, refTime: Milliseconds = Date.now()) => {
-  if (!utxo.locktime) return false
-
-  const pathAndLocktime = utxo.path.split(':')
-  if (pathAndLocktime.length !== 2) return false
-
-  const locktimeUnixTimestamp: Seconds = parseInt(pathAndLocktime[1], 10)
-  if (Number.isNaN(locktimeUnixTimestamp)) return false
-
-  return locktimeUnixTimestamp * 1_000 >= refTime
-}
-
 const calculateFrozenOrLockedBalance = (utxos: Utxos, refTime: Milliseconds = Date.now()) => {
-  const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || isLocked(utxo, refTime))
+  const frozenOrLockedUtxos = utxos.filter((utxo) => utxo.frozen || fb.utxo.isLocked(utxo, refTime))
   return frozenOrLockedUtxos.reduce((acc, utxo) => acc + utxo.value, 0)
 }
 
diff --git a/src/hooks/CoinjoinPrecondition.ts b/src/hooks/CoinjoinPrecondition.ts
index a931aeb..4c2d47d 100644
--- a/src/hooks/CoinjoinPrecondition.ts
+++ b/src/hooks/CoinjoinPrecondition.ts
@@ -1,5 +1,5 @@
 import { useMemo } from 'react'
-import { isLocked } from '../hooks/BalanceSummary'
+import * as fb from '../components/fb/utils'
 import { Utxos } from '../context/WalletContext'
 
 export const COINJOIN_PRECONDITIONS = {
@@ -18,7 +18,7 @@ export interface CoinjoinPreconditionSummary {
 
 export const useCoinjoinPreconditionSummary = (utxos: Utxos): CoinjoinPreconditionSummary => {
   const eligibleUtxos = useMemo(() => {
-    return utxos.filter((it) => !it.frozen).filter((it) => !isLocked(it))
+    return utxos.filter((it) => !it.frozen).filter((it) => !fb.utxo.isLocked(it))
   }, [utxos])
 
   return useMemo(() => {


Test patch:
diff --git a/src/components/fb/utils.test.ts b/src/components/fb/utils.test.ts
index 69fa812..6486776 100644
--- a/src/components/fb/utils.test.ts
+++ b/src/components/fb/utils.test.ts
@@ -165,5 +165,35 @@ describe('utils', () => {
       ]
       expect(fb.utxo.allAreFrozen(utxos)).toBe(false)
     })
+
+    describe('isLocked', () => {
+      const now = Date.UTC(2009, 0, 3)
+
+      it('should detect timelocked utxo as locked', () => {
+        const utxo = {
+          // timelocked, not yet expired
+          locktime: 'any',
+          path: `m/84'/1'/0'/0/1:${now / 1_000 + 1}`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(true)
+      })
+
+      it('should detect expired timelocked utxo as unlocked', () => {
+        const utxo = {
+          // timelocked, but expired
+          locktime: 'any',
+          path: `m/84'/1'/0'/0/1:${now / 1_000 - 1}`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(false)
+      })
+
+      it('should detect non-timelocked utxo as unlocked', () => {
+        const utxo = {
+          // not timelocked
+          path: `m/84'/1'/0'/0/1`,
+        } as Utxo
+        expect(fb.utxo.isLocked(utxo, now)).toBe(false)
+      })
+    })
   })
 })
diff --git a/src/hooks/BalanceSummary.test.tsx b/src/hooks/BalanceSummary.test.tsx
index 83391de..0b556e1 100644
--- a/src/hooks/BalanceSummary.test.tsx
+++ b/src/hooks/BalanceSummary.test.tsx
@@ -2,39 +2,11 @@ import React from 'react'
 import { render } from '../testUtils'
 import { act } from 'react-dom/test-utils'
 
-import { useBalanceSummary, WalletBalanceSummary, isLocked } from './BalanceSummary'
+import { useBalanceSummary, WalletBalanceSummary } from './BalanceSummary'
 import { WalletInfo, Utxo } from '../context/WalletContext'
 
 const now = Date.UTC(2009, 0, 3)
 
-describe('isLocked', () => {
-  it('should detect timelocked utxo as locked', () => {
-    const utxo = {
-      // timelocked, not yet expired
-      locktime: 'any',
-      path: `m/84'/1'/0'/0/1:${now / 1_000 + 1}`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(true)
-  })
-
-  it('should detect expired timelocked utxo as unlocked', () => {
-    const utxo = {
-      // timelocked, but expired
-      locktime: 'any',
-      path: `m/84'/1'/0'/0/1:${now / 1_000 - 1}`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(false)
-  })
-
-  it('should detect non-timelocked utxo as unlocked', () => {
-    const utxo = {
-      // not timelocked
-      path: `m/84'/1'/0'/0/1`,
-    } as Utxo
-    expect(isLocked(utxo, now)).toBe(false)
-  })
-})
-
 describe('BalanceSummary', () => {
   function setup(walletInfo: WalletInfo | null, refTime: number) {
     const returnVal: { data: WalletBalanceSummary | null | undefined } = { data: undefined }