Fix Cashu file redemption and Bitcoin-dependent wallet readiness

This commit is contained in:
archipelago
2026-09-29 14:42:44 -04:00
parent 540639d2c1
commit 0f85f588fb
41 changed files with 1641 additions and 214 deletions
@@ -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)
})
})