Fix Cashu file redemption and Bitcoin-dependent wallet readiness
This commit is contained in:
@@ -42,6 +42,7 @@ export interface PackageVersionsResponse {
|
||||
pinnedVersion: string | null
|
||||
autoUpdate: boolean
|
||||
versions: CatalogVersionInfo[]
|
||||
bitcoinPrune?: boolean | null
|
||||
}
|
||||
|
||||
export interface AppGatePortStatus {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div class="mt-5 space-y-2">
|
||||
<label class="flex items-center gap-2 text-sm text-white/80">
|
||||
<input v-model="model" type="checkbox" class="accent-orange-400" />
|
||||
Prune Bitcoin to save disk space
|
||||
</label>
|
||||
<p class="text-xs text-white/50">
|
||||
Keeps about 50 GB of recent blocks, using the same settings as automatic
|
||||
pruning on smaller disks. All blocks are still downloaded and verified.
|
||||
Mempool and other apps that need the full blockchain won’t be available.
|
||||
Turning pruning off later requires downloading the blockchain again.
|
||||
</p>
|
||||
<p v-if="!model" class="text-xs text-white/50">
|
||||
Automatic pruning still applies on disks smaller than 1 TB.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const model = defineModel<boolean>({ default: false })
|
||||
</script>
|
||||
@@ -31,7 +31,7 @@
|
||||
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm font-mono focus:outline-none focus:border-orange-400/60"
|
||||
/>
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
Defaults to tx1138.com. Any Mempool-compatible instance works — you can change this
|
||||
Defaults to mempool.space. Any Mempool-compatible instance works — you can change this
|
||||
any time in Settings → System.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
<p class="text-white/40 text-xs">{{ t('marketplace.installModalHint') }}</p>
|
||||
</div>
|
||||
|
||||
<BitcoinPruningChoice v-if="isBitcoin && !loading" v-model="prune" />
|
||||
|
||||
<template #footer>
|
||||
<div class="flex gap-2 mt-6">
|
||||
<button
|
||||
@@ -58,9 +60,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import BaseModal from './BaseModal.vue'
|
||||
import BitcoinPruningChoice from './BitcoinPruningChoice.vue'
|
||||
import { rpcClient, type CatalogVersionInfo } from '../api/rpc-client'
|
||||
import { displayVersion } from '@/utils/version'
|
||||
|
||||
@@ -73,13 +76,16 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
// Emits the version string the runner chose (e.g. "latest" or "29.3.knots20260508").
|
||||
confirm: [version: string]
|
||||
confirm: [version: string, prune?: boolean]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const versions = ref<CatalogVersionInfo[]>([])
|
||||
const selected = ref('')
|
||||
const prune = ref(false)
|
||||
const pruneKnown = ref(false)
|
||||
const isBitcoin = computed(() => ['bitcoin-core', 'bitcoin-knots'].includes(props.appId))
|
||||
|
||||
// Latest reads as a sentence (no "v" prefix); concrete versions are normalized.
|
||||
function optionLabel(v: CatalogVersionInfo): string {
|
||||
@@ -92,12 +98,16 @@ function optionLabel(v: CatalogVersionInfo): string {
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
prune.value = false
|
||||
pruneKnown.value = false
|
||||
versions.value = []
|
||||
selected.value = ''
|
||||
try {
|
||||
const info = await rpcClient.getPackageVersions(props.appId)
|
||||
// catalog_versions() returns the list default(=latest)-first, so versions[0]
|
||||
// is the latest — pre-select it.
|
||||
pruneKnown.value = typeof info.bitcoinPrune === 'boolean'
|
||||
prune.value = info.bitcoinPrune === true
|
||||
versions.value = info.versions || []
|
||||
selected.value = info.default || versions.value.find((v) => v.default)?.version || versions.value[0]?.version || 'latest'
|
||||
} catch (err) {
|
||||
@@ -111,7 +121,7 @@ async function load() {
|
||||
|
||||
function confirm() {
|
||||
if (!selected.value) return
|
||||
emit('confirm', selected.value)
|
||||
emit('confirm', selected.value, isBitcoin.value && (pruneKnown.value || prune.value) ? prune.value : undefined)
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
@change="saveExplorer"
|
||||
/>
|
||||
<p class="text-[11px] text-white/40 mt-1">
|
||||
Any Mempool-compatible instance works. Default: tx1138.com.
|
||||
Any Mempool-compatible instance works. Default: mempool.space.
|
||||
</p>
|
||||
|
||||
<div class="mt-3 p-3 rounded-lg border border-amber-400/25 bg-amber-500/10 text-amber-200/80 text-xs leading-relaxed">
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import InstallVersionModal from '../InstallVersionModal.vue'
|
||||
const versions = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../../api/rpc-client', () => ({ rpcClient: { getPackageVersions: versions } }))
|
||||
const i18n = createI18n({ legacy: false, locale: 'en', missingWarn: false, fallbackWarn: false, messages: { en: { common: { install: 'Install', cancel: 'Cancel' } } } })
|
||||
function modal(id = 'bitcoin-core') {
|
||||
return mount(InstallVersionModal, {
|
||||
props: { show: true, appId: id, app: { id, title: id } },
|
||||
global: { plugins: [i18n], stubs: { BaseModal: { template: '<div><slot/><slot name="footer"/></div>' } } },
|
||||
})
|
||||
}
|
||||
describe('Bitcoin install storage choice', () => {
|
||||
it.each(['bitcoin-core', 'bitcoin-knots'])('sends chosen version and explicit pruning for %s', async id => {
|
||||
versions.mockResolvedValue({ bitcoinPrune: false, default: 'latest', versions: [{ version: 'latest' }, { version: '28.4' }] })
|
||||
const wrapper = modal(id)
|
||||
await flushPromises()
|
||||
await wrapper.get('select').setValue('28.4')
|
||||
await wrapper.get('input[type=checkbox]').setValue(true)
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('confirm')).toEqual([['28.4', true]])
|
||||
expect(wrapper.text()).toContain('automatic pruning')
|
||||
expect(wrapper.text()).toContain('Mempool')
|
||||
})
|
||||
it('keeps automatic disk selection by default and resets on reopening', async () => {
|
||||
versions.mockResolvedValue({ bitcoinPrune: false, versions: [{ version: 'latest' }] })
|
||||
const wrapper = modal()
|
||||
await flushPromises()
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('confirm')).toEqual([['latest', false]])
|
||||
await wrapper.get('input').setValue(true)
|
||||
await wrapper.setProps({ show: false })
|
||||
await wrapper.setProps({ show: true })
|
||||
await flushPromises()
|
||||
expect((wrapper.get('input').element as HTMLInputElement).checked).toBe(false)
|
||||
})
|
||||
it('still allows choosing pruning when version lookup fails', async () => {
|
||||
versions.mockRejectedValue(new Error('offline'))
|
||||
const wrapper = modal()
|
||||
await flushPromises()
|
||||
await wrapper.get('input').setValue(true)
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('confirm')).toEqual([['latest', true]])
|
||||
})
|
||||
it('remembers the node pruning preference when reinstalling or switching Bitcoin variants', async () => {
|
||||
versions.mockResolvedValue({ bitcoinPrune: true, versions: [{ version: 'latest' }] })
|
||||
const wrapper = modal('bitcoin-knots')
|
||||
await flushPromises()
|
||||
expect((wrapper.get('input').element as HTMLInputElement).checked).toBe(true)
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('confirm')).toEqual([['latest', true]])
|
||||
})
|
||||
it('does not turn off a saved pruning preference when its lookup fails', async () => {
|
||||
versions.mockRejectedValue(new Error('offline'))
|
||||
const wrapper = modal()
|
||||
await flushPromises()
|
||||
await wrapper.get('button').trigger('click')
|
||||
expect(wrapper.emitted('confirm')).toEqual([['latest', undefined]])
|
||||
})
|
||||
it('does not offer Bitcoin settings for other apps', async () => {
|
||||
versions.mockResolvedValue({ bitcoinPrune: false, versions: [{ version: 'latest' }] })
|
||||
const wrapper = modal('other')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('input').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -76,6 +76,24 @@ describe('useTxExplorer.openTx', () => {
|
||||
expect(openSession).toHaveBeenCalledWith('mempool', { path: `/tx/${TX}` })
|
||||
})
|
||||
|
||||
it('does not open an external explorer while container discovery is pending', async () => {
|
||||
const external = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
let finish!: () => void
|
||||
ensureFetched.mockImplementationOnce(() => new Promise<void>(resolve => {
|
||||
finish = () => { fetched = true; resolve() }
|
||||
}))
|
||||
const { openTx, setExplorer } = useTxExplorer()
|
||||
setExplorer(DEFAULT_TX_EXPLORER, true)
|
||||
const opening = openTx(TX)
|
||||
expect(external).not.toHaveBeenCalled()
|
||||
expect(openSession).not.toHaveBeenCalled()
|
||||
finish()
|
||||
await opening
|
||||
expect(openSession).toHaveBeenCalledWith('mempool', { path: `/tx/${TX}` })
|
||||
expect(external).not.toHaveBeenCalled()
|
||||
external.mockRestore()
|
||||
})
|
||||
|
||||
it('asks for consent only when Mempool genuinely is not installed', async () => {
|
||||
containerState = 'not-installed'
|
||||
const { openTx, pendingTx } = useTxExplorer()
|
||||
@@ -84,3 +102,23 @@ describe('useTxExplorer.openTx', () => {
|
||||
expect(pendingTx.value).toBe(TX)
|
||||
})
|
||||
})
|
||||
|
||||
describe('explorer default migration', () => {
|
||||
beforeEach(() => { localStorage.clear(); vi.resetModules() })
|
||||
it('uses mempool.space with consent for a new browser', async () => {
|
||||
const { useTxExplorer } = await import('../useTxExplorer')
|
||||
expect(useTxExplorer().prefs.value).toEqual({ url: 'https://mempool.space', acknowledged: false })
|
||||
})
|
||||
it.each(['https://tx1138.com', 'https://tx1138.com/', 'http://tx1138.com'])('migrates %s and resets consent', async url => {
|
||||
localStorage.setItem('archipelago.tx-explorer.v1', JSON.stringify({ url, acknowledged: true }))
|
||||
const { useTxExplorer } = await import('../useTxExplorer')
|
||||
expect(useTxExplorer().prefs.value).toEqual({ url: 'https://mempool.space', acknowledged: false })
|
||||
expect(JSON.parse(localStorage.getItem('archipelago.tx-explorer.v1')!)).toEqual(useTxExplorer().prefs.value)
|
||||
})
|
||||
it('preserves a custom explorer and its consent', async () => {
|
||||
const prefs = { url: 'https://my-explorer.example', acknowledged: true }
|
||||
localStorage.setItem('archipelago.tx-explorer.v1', JSON.stringify(prefs))
|
||||
const { useTxExplorer } = await import('../useTxExplorer')
|
||||
expect(useTxExplorer().prefs.value).toEqual(prefs)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,8 +17,8 @@ import { ref } from 'vue'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import { useContainerStore } from '@/stores/container'
|
||||
|
||||
export const DEFAULT_TX_EXPLORER = 'https://tx1138.com'
|
||||
export const EXPLORER_PLACEHOLDER = 'https://mempool.guide'
|
||||
export const DEFAULT_TX_EXPLORER = 'https://mempool.space'
|
||||
export const EXPLORER_PLACEHOLDER = DEFAULT_TX_EXPLORER
|
||||
|
||||
const KEY = 'archipelago.tx-explorer.v1'
|
||||
|
||||
@@ -30,7 +30,13 @@ interface TxExplorerPrefs {
|
||||
function loadPrefs(): TxExplorerPrefs {
|
||||
const defaults: TxExplorerPrefs = { url: DEFAULT_TX_EXPLORER, acknowledged: false }
|
||||
try {
|
||||
return { ...defaults, ...JSON.parse(localStorage.getItem(KEY) || '{}') }
|
||||
const stored = { ...defaults, ...JSON.parse(localStorage.getItem(KEY) || '{}') }
|
||||
// Changing operators requires fresh consent, even if the old one was trusted.
|
||||
if (typeof stored.url === 'string' && /^https?:\/\/tx1138\.com\/*$/i.test(stored.url.trim())) {
|
||||
localStorage.setItem(KEY, JSON.stringify(defaults))
|
||||
return defaults
|
||||
}
|
||||
return stored
|
||||
} catch {
|
||||
return defaults
|
||||
}
|
||||
|
||||
@@ -672,6 +672,11 @@ async function handleInstall(app: MarketplaceApp) {
|
||||
return
|
||||
}
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
if (['bitcoin-core', 'bitcoin-knots'].includes(app.id)) {
|
||||
installModalApp.value = app
|
||||
showInstallModal.value = true
|
||||
return
|
||||
}
|
||||
// Multi-version apps (Bitcoin Knots / Core): let the runner pick a version up
|
||||
// front via a full-screen modal (latest pre-selected) instead of silently
|
||||
// installing the default. Best-effort — if the lookup fails we install directly.
|
||||
@@ -686,19 +691,19 @@ async function handleInstall(app: MarketplaceApp) {
|
||||
startInstall(app)
|
||||
}
|
||||
|
||||
function startInstall(app: MarketplaceApp, versionOverride?: string) {
|
||||
function startInstall(app: MarketplaceApp, versionOverride?: string, prune?: boolean) {
|
||||
if (app.source === 'local') {
|
||||
installApp(app, versionOverride)
|
||||
installApp(app, versionOverride, prune)
|
||||
} else {
|
||||
installCommunityApp(app, versionOverride)
|
||||
installCommunityApp(app, versionOverride, prune)
|
||||
}
|
||||
}
|
||||
|
||||
function onInstallModalConfirm(version: string) {
|
||||
function onInstallModalConfirm(version: string, prune?: boolean) {
|
||||
const app = installModalApp.value
|
||||
showInstallModal.value = false
|
||||
installModalApp.value = null
|
||||
if (app) startInstall(app, version)
|
||||
if (app) startInstall(app, version, prune)
|
||||
}
|
||||
|
||||
function viewAppDetails(app: MarketplaceApp) {
|
||||
@@ -774,25 +779,25 @@ function failInstall(app: MarketplaceApp, err: unknown) {
|
||||
trackTimeout(() => { serverStore.clearInstallProgress(app.id) }, 5000)
|
||||
}
|
||||
|
||||
async function installApp(app: MarketplaceApp, versionOverride?: string) {
|
||||
async function installApp(app: MarketplaceApp, versionOverride?: string, prune?: boolean) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
queueInstall(app)
|
||||
installToast(app)
|
||||
try {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: versionOverride || app.version }, timeout: 600000 })
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: versionOverride || app.version, ...(prune === undefined ? {} : { prune }) }, timeout: 600000 })
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Installation failed:', err)
|
||||
failInstall(app, err)
|
||||
}
|
||||
}
|
||||
|
||||
async function installCommunityApp(app: MarketplaceApp, versionOverride?: string) {
|
||||
async function installCommunityApp(app: MarketplaceApp, versionOverride?: string, prune?: boolean) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
queueInstall(app)
|
||||
installToast(app)
|
||||
try {
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: versionOverride || app.version }
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: versionOverride || app.version, ...(prune === undefined ? {} : { prune }) }
|
||||
if ((app as Record<string, unknown>).containerConfig) {
|
||||
installParams.containerConfig = (app as Record<string, unknown>).containerConfig
|
||||
}
|
||||
|
||||
@@ -71,10 +71,11 @@
|
||||
{{ $ver(v.version) }}{{ v.default ? ' — latest' : '' }}{{ v.deprecated ? ' (deprecated)' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<BitcoinPruningChoice v-if="!isInstalled && isBitcoinInstall && prunePrefsLoaded" v-model="pruneOnInstall" />
|
||||
<button
|
||||
v-if="!isInstalled"
|
||||
@click="installApp"
|
||||
:disabled="demoNoInstall || installing || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
||||
:disabled="demoNoInstall || installing || (isBitcoinInstall && !prunePrefsLoaded) || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
||||
:title="demoNoInstall ? 'Not available in the demo' : (installBlockedReason || undefined)"
|
||||
class="glass-button glass-button-sm px-6 py-2.5 rounded-lg text-sm font-semibold flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
@@ -137,6 +138,7 @@
|
||||
{{ $ver(v.version) }}{{ v.default ? ' — latest' : '' }}{{ v.deprecated ? ' (deprecated)' : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<BitcoinPruningChoice v-if="!isInstalled && isBitcoinInstall && prunePrefsLoaded" v-model="pruneOnInstall" />
|
||||
|
||||
<!-- Bottom: Action Buttons -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@@ -153,7 +155,7 @@
|
||||
<button
|
||||
v-else
|
||||
@click="installApp"
|
||||
:disabled="demoNoInstall || installing || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
||||
:disabled="demoNoInstall || installing || (isBitcoinInstall && !prunePrefsLoaded) || (!installBlockedReason && !app.manifestUrl && !app.dockerImage)"
|
||||
:title="demoNoInstall ? 'Not available in the demo' : (installBlockedReason || undefined)"
|
||||
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed col-span-2"
|
||||
>
|
||||
@@ -374,6 +376,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import BitcoinPruningChoice from '@/components/BitcoinPruningChoice.vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { IS_DEMO, isDemoApp } from '@/composables/useDemoIntro'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
@@ -408,6 +411,10 @@ const bitcoinPruned = ref(false)
|
||||
// Hidden when an app offers only one version — install stays one-click.
|
||||
const installVersions = ref<{ version: string; default: boolean; deprecated: boolean; eol: string | null }[]>([])
|
||||
const selectedInstallVersion = ref('')
|
||||
const pruneOnInstall = ref(false)
|
||||
const pruneSettingKnown = ref(false)
|
||||
const prunePrefsLoaded = ref(false)
|
||||
const isBitcoinInstall = computed(() => ['bitcoin-core', 'bitcoin-knots'].includes(app.value?.id || ''))
|
||||
const backButtonLabel = computed(() => route.query.from === 'home' ? t('marketplaceDetails.backToHome') : t('marketplaceDetails.backToStore'))
|
||||
const electrumxArchiveWarning = 'You need a full archival bitcoin node before downloading ElectrumX'
|
||||
|
||||
@@ -587,10 +594,13 @@ onMounted(() => {
|
||||
// cached entry is missing or past its TTL, so a repeat open inside the TTL
|
||||
// paints from cache with no new RPC.
|
||||
async function loadInstallVersions() {
|
||||
if (versionsResource.data.value === null || versionsResource.isStale.value) {
|
||||
if (isBitcoinInstall.value || versionsResource.data.value === null || versionsResource.isStale.value) {
|
||||
await versionsResource.refresh()
|
||||
}
|
||||
const info = versionsResource.data.value
|
||||
pruneSettingKnown.value = !versionsResource.error.value && typeof info?.bitcoinPrune === 'boolean'
|
||||
pruneOnInstall.value = pruneSettingKnown.value && info?.bitcoinPrune === true
|
||||
prunePrefsLoaded.value = true
|
||||
if (!info || !info.supportsVersions || info.versions.length < 2) {
|
||||
installVersions.value = []
|
||||
return
|
||||
@@ -701,6 +711,7 @@ async function installApp() {
|
||||
id: app.value.id,
|
||||
dockerImage: app.value.dockerImage,
|
||||
version: chosenVersion,
|
||||
...(isBitcoinInstall.value && (pruneSettingKnown.value || pruneOnInstall.value) ? { prune: pruneOnInstall.value } : {}),
|
||||
}
|
||||
if (app.value.containerConfig) installParams.containerConfig = app.value.containerConfig
|
||||
await rpcClient.call({
|
||||
@@ -717,6 +728,7 @@ async function installApp() {
|
||||
id: app.value.id,
|
||||
url: installUrl,
|
||||
version: chosenVersion,
|
||||
...(isBitcoinInstall.value && (pruneSettingKnown.value || pruneOnInstall.value) ? { prune: pruneOnInstall.value } : {}),
|
||||
},
|
||||
timeout: 600000,
|
||||
})
|
||||
|
||||
@@ -362,6 +362,22 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.8.20-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.20-alpha</span>
|
||||
<span class="text-xs text-white/40">September 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Fixed Cashu file payments rejected despite a shared mint, and preserved the payment amount when mint fees reduce change.</p>
|
||||
<p>Payment failures now report whether a refund actually succeeded; missing files and unsupported payment methods are rejected before charging.</p>
|
||||
<p>Bitcoin Core and Knots installation offers optional pruning on larger disks, using the same settings as automatic pruning.</p>
|
||||
<p>Fixed false missing-port checks that unnecessarily restarted Bitcoin and LND; recovery now respects managed shutdown timeouts.</p>
|
||||
<p>LND explains when it is waiting for Bitcoin installation, startup, or sync, without treating normal synchronization as a restart-worthy failure.</p>
|
||||
<p>Bitcoin startup messages explain block-index loading without exposing raw RPC errors, and Lightning keeps known balances clearly marked during outages.</p>
|
||||
<p>Changed the public transaction-explorer default to mempool.space while preserving local explorers and custom choices.</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.8.19-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
Reference in New Issue
Block a user