mirror of
https://github.com/joinmarket-webui/jam.git
synced 2026-08-13 12:33:26 +02:00
chore(fb): freeze utxo after creating fb
This commit is contained in:
parent
edf97c30a7
commit
25246e1690
8 changed files with 129 additions and 31 deletions
3
.env
3
.env
|
|
@ -23,3 +23,6 @@ VITE_JM_WEBSOCKET_RECONNECT_INTERVAL_MAX=60000
|
|||
|
||||
VITE_JAM_SWEEP_MIN_ROUNDING_CHANCE_PERCENT=10
|
||||
VITE_JAM_SWEEP_MAX_ROUNDING_CHANCE_PERCENT=90
|
||||
|
||||
VITE_JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS=true
|
||||
VITE_JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY=1000
|
||||
|
|
|
|||
|
|
@ -3,3 +3,5 @@ VITE_JAM_SWEEP_DESTINATION_ADDRESSES_DEFAULT_COUNT=3
|
|||
|
||||
VITE_JAM_SWEEP_MAKER_SESSION_IDLE_MIN_TIMEOUT_SECONDS=1
|
||||
VITE_JAM_SWEEP_MAKER_SESSION_IDLE_TIMEOUT_SECONDS=1
|
||||
|
||||
VITE_JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY=1
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@ import { useQuery } from '@tanstack/react-query'
|
|||
import { useForm, useWatch } from 'react-hook-form'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS,
|
||||
JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY,
|
||||
} from '@/constants/jam'
|
||||
import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext'
|
||||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import type { Utxo } from '@/hooks/useQueryUtxos'
|
||||
import * as fb from '@/lib/fidelityBondUtils'
|
||||
import type { WalletFileName } from '@/lib/utils'
|
||||
import { delayedPromise, type WalletFileName } from '@/lib/utils'
|
||||
import { useDeveloperMode } from '@/store/jamSettingsStore'
|
||||
import type { JarIndex } from '@/types/global'
|
||||
import { useFidelityBondMutations } from '../fidelity-bond/useFidelityBondMutations'
|
||||
|
|
@ -283,6 +287,7 @@ export function useCreateFidelityBondWizard(
|
|||
setStep('creating')
|
||||
|
||||
try {
|
||||
// TODO: refactor: use the sweep functionality from `useFidelityBondSweep` here
|
||||
const result = await directSend.mutateAsync({
|
||||
path: { walletname: walletFileName },
|
||||
body: {
|
||||
|
|
@ -291,9 +296,6 @@ export function useCreateFidelityBondWizard(
|
|||
destination: address,
|
||||
},
|
||||
})
|
||||
setTxResult(result)
|
||||
setStep('success')
|
||||
toast.success(t('earn.fidelity_bond.create_fidelity_bond.success_text'))
|
||||
|
||||
// Best-effort cleanup — tx already broadcast, don't throw on unfreeze failure
|
||||
for (const utxo of frozenUtxos) {
|
||||
|
|
@ -301,12 +303,33 @@ export function useCreateFidelityBondWizard(
|
|||
await unfreezeUtxo.mutateAsync({
|
||||
path: { walletname: walletFileName },
|
||||
body: { 'utxo-string': utxo.utxo, freeze: false },
|
||||
throwOnError: true,
|
||||
})
|
||||
} catch {
|
||||
// logged via onError
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
// only debug output
|
||||
console.debug('Error while unfreezing previously frozen UTXO.')
|
||||
}
|
||||
}
|
||||
|
||||
// freeze resulting fidelity bond output
|
||||
if (JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS) {
|
||||
try {
|
||||
await delayedPromise(JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY)
|
||||
const fbUtxoId = `${result.txinfo.txid}:0` // sent with a sweep, so it is a single output
|
||||
await freezeUtxo.mutateAsync({
|
||||
path: { walletname: walletFileName },
|
||||
body: { 'utxo-string': fbUtxoId, freeze: true },
|
||||
throwOnError: true,
|
||||
})
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
console.warn('Error while freezing Fidelity Bond UTXO - continuing.')
|
||||
}
|
||||
}
|
||||
|
||||
setTxResult(result)
|
||||
setStep('success')
|
||||
toast.success(t('earn.fidelity_bond.create_fidelity_bond.success_text'))
|
||||
|
||||
await walletInfo.refetch()
|
||||
} catch {
|
||||
// Best-effort rollback — unfreeze UTXOs that were frozen for this operation
|
||||
|
|
@ -316,8 +339,9 @@ export function useCreateFidelityBondWizard(
|
|||
path: { walletname: walletFileName },
|
||||
body: { 'utxo-string': utxo.utxo, freeze: false },
|
||||
})
|
||||
} catch {
|
||||
// logged via onError
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
// only debug output
|
||||
console.debug('Error while unfreezing previously frozen UTXO in error handling.')
|
||||
}
|
||||
}
|
||||
setFrozenUtxos([])
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { toast } from 'sonner'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS } from '@/constants/jam'
|
||||
import { useApiClient } from '@/hooks/useApiClient'
|
||||
import type { FidelityBondUtxo } from '@/hooks/useQueryUtxos'
|
||||
import * as fb from '@/lib/fidelityBondUtils'
|
||||
|
|
@ -113,7 +114,7 @@ export function RenewBondDialog({ open, onOpenChange, walletFileName, utxo }: Re
|
|||
|
||||
const txResult = await sweep({
|
||||
destination: destinationAddress,
|
||||
tryFreezeAfterBroadcast: true,
|
||||
tryFreezeAfterBroadcast: JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS,
|
||||
})
|
||||
|
||||
setTxResult(txResult)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo } from 'react'
|
||||
import type { DirectSendResponse } from '@joinmarket-webui/joinmarket-ng-api-ts/jm'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY } from '@/constants/jam'
|
||||
import { useJamWalletInfoContext } from '@/context/JamWalletInfoContext'
|
||||
import type { FidelityBondUtxo, Utxo } from '@/hooks/useQueryUtxos'
|
||||
import { delayedPromise, type WalletFileName } from '@/lib/utils'
|
||||
|
|
@ -81,26 +82,6 @@ export function useFidelityBondSweep({
|
|||
},
|
||||
})
|
||||
|
||||
if (tryFreezeAfterBroadcast) {
|
||||
try {
|
||||
await delayedPromise(1_000) // provide some time for the backend to catch up, small delay of 1s seems to be enough
|
||||
const fbUtxoId = `${result.txinfo.txid}:0` // sent with a sweep, so it is a single output
|
||||
await freezeUtxo.mutateAsync({
|
||||
path: { walletname: walletFileName },
|
||||
body: { 'utxo-string': fbUtxoId, freeze: true },
|
||||
throwOnError: true,
|
||||
})
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
console.warn('Error while freezing Fidelity Bond UTXO - continueing.')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await onBroadcastSuccess?.(result)
|
||||
} catch (error: unknown) {
|
||||
console.warn('onBroadcastSuccess failed', error)
|
||||
}
|
||||
|
||||
// Best-effort cleanup — tx already broadcast, don't throw on unfreeze failure
|
||||
for (const u of frozen) {
|
||||
try {
|
||||
|
|
@ -115,6 +96,27 @@ export function useFidelityBondSweep({
|
|||
}
|
||||
}
|
||||
|
||||
if (tryFreezeAfterBroadcast) {
|
||||
try {
|
||||
await delayedPromise(JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY)
|
||||
|
||||
const fbUtxoId = `${result.txinfo.txid}:0` // sent with a sweep, so it is a single output
|
||||
await freezeUtxo.mutateAsync({
|
||||
path: { walletname: walletFileName },
|
||||
body: { 'utxo-string': fbUtxoId, freeze: true },
|
||||
throwOnError: true,
|
||||
})
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
console.warn('Error while freezing Fidelity Bond UTXO - continuing.')
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await onBroadcastSuccess?.(result)
|
||||
} catch (error: unknown) {
|
||||
console.warn('onBroadcastSuccess failed', error)
|
||||
}
|
||||
|
||||
await walletInfoRefetch()
|
||||
return result
|
||||
} catch (_ignoredOnPurpose: unknown) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
JM_NG_DEFAULT_TUMBLER_PARAMS,
|
||||
JM_WALLET_FILE_EXTENSION,
|
||||
} from './jm'
|
||||
import { parseAsIntOrDefault } from './meta-env-utils'
|
||||
import { parseAsBooleanOrDefault, parseAsIntOrDefault } from './meta-env-utils'
|
||||
|
||||
export const APP_DISPLAY_VERSION = (() => {
|
||||
return parseSemanticVersion(packageInfoVersion)
|
||||
|
|
@ -183,6 +183,23 @@ export const JAM_SWEEP_MAX_ROUNDING_CHANCE_PERCENT = Math.max(
|
|||
JAM_SWEEP_DEFAULT_MIN_ROUNDING_CHANCE_PERCENT,
|
||||
)
|
||||
|
||||
const JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DEFAULT = true
|
||||
export const JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS = parseAsBooleanOrDefault(
|
||||
import.meta.env.VITE_JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS,
|
||||
JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DEFAULT,
|
||||
)
|
||||
|
||||
// provide some time for the backend to catch up after fb is created, small delay of ~1s seems to be enough
|
||||
export const JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DEFAULT_DELAY: Milliseconds = 1000
|
||||
export const JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_MIN_DELAY: Milliseconds = 21
|
||||
export const JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY = Math.max(
|
||||
parseAsIntOrDefault(
|
||||
import.meta.env.VITE_JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DELAY,
|
||||
JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_DEFAULT_DELAY,
|
||||
),
|
||||
JAM_TRY_FREEZE_CREATED_FIDELITY_BOND_OUTPUTS_MIN_DELAY,
|
||||
)
|
||||
|
||||
/**
|
||||
* A gaplimit threshold at which a warning is displayed that with the given value a
|
||||
* decline in performance is to be expected. Importing 500 addresses (per jar!) leads to
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { parseAsIntOrDefault } from './meta-env-utils'
|
||||
import { parseAsIntOrDefault, parseAsBooleanOrDefault } from './meta-env-utils'
|
||||
|
||||
describe('parseAsIntOrDefault', () => {
|
||||
it('should parse input as int or default', () => {
|
||||
|
|
@ -23,3 +23,27 @@ describe('parseAsIntOrDefault', () => {
|
|||
expect(parseAsIntOrDefault(Number.MIN_SAFE_INTEGER - 1, 21)).toBe(21)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseAsBooleanOrDefault', () => {
|
||||
it('should parse input as boolean or default', () => {
|
||||
expect(parseAsBooleanOrDefault('true', false)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault('yes', false)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault('1', false)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault('false', true)).toBe(false)
|
||||
expect(parseAsBooleanOrDefault('no', true)).toBe(false)
|
||||
expect(parseAsBooleanOrDefault('0', true)).toBe(false)
|
||||
|
||||
expect(parseAsBooleanOrDefault(true, false)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(false, true)).toBe(false)
|
||||
|
||||
expect(parseAsBooleanOrDefault(1, false)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(0, true)).toBe(false)
|
||||
|
||||
expect(parseAsBooleanOrDefault(undefined, true)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault('', true)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(null, true)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(String(Number.NaN), true)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(Number.MAX_SAFE_INTEGER + 1, true)).toBe(true)
|
||||
expect(parseAsBooleanOrDefault(Number.MIN_SAFE_INTEGER - 1, true)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,3 +3,28 @@ export const parseAsIntOrDefault = (value: any, defaultValue: number): number =>
|
|||
const parsed = Number.parseInt(`${value ?? Number.NaN}`, 10)
|
||||
return Number.isSafeInteger(parsed) ? parsed : defaultValue
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- okay when trying to parse arbitrary value on purpose
|
||||
export const parseAsBooleanOrDefault = (value: any, defaultValue: boolean): boolean => {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
} else if (typeof value === 'string') {
|
||||
const lowercased = value.trim().toLowerCase()
|
||||
if (['true', 'yes', '1'].includes(lowercased)) {
|
||||
return true
|
||||
}
|
||||
if (['false', 'no', '0'].includes(lowercased)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const asNumber = parseAsIntOrDefault(value, Number.NaN)
|
||||
if (asNumber === 1) {
|
||||
return true
|
||||
}
|
||||
if (asNumber === 0) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue