fix(neode-ui): instant payments no longer stuck as 'Incoming' forever
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s
Some checks failed
Demo images / Build & push demo images (push) Failing after 1m42s
The Home wallet card counted every incoming tx with num_confirmations < 3 in the pulsing 'Incoming N' badge. Lightning history hardcodes 1 conf and ecash/fedimint/ark receives are normalized to 1 conf, so instant payments qualified permanently — old receives showed as 'incoming' forever (seen on .228 with week-old lightning orders). - On-chain keeps confirmation-based logic (< 3 confs, expires naturally). - Instant rails (lightning/cashu/fedimint/ark) now surface in the incoming panel only for 5 minutes after receipt — the nice arrival moment without the perpetual pending state. - Panel rows: 'Unconfirmed / N conf' badge and mempool link are now on-chain-only; instant rows get a rail badge instead. - Home now polls wallet balances+transactions every 30s (like Web5.vue), so pending on-chain receives actually flip to confirmed and the badge appears/expires without a manual wallet action. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1931371058
commit
7e7b6bd474
@ -359,6 +359,7 @@ const line2Text = computed(() => showWelcomeBlock.value ? displayLine2.value : L
|
||||
onBeforeUnmount(() => {
|
||||
if (typingInterval) clearInterval(typingInterval)
|
||||
if (systemStatsInterval) clearInterval(systemStatsInterval)
|
||||
if (walletRefreshInterval) clearInterval(walletRefreshInterval)
|
||||
})
|
||||
|
||||
watch(() => loginTransition.pendingWelcomeTyping, (pending) => { if (pending) showWelcomeBlock.value = true })
|
||||
@ -519,6 +520,10 @@ const cloudFolderDisplay = computed(() => cloudFolderCount.value !== null ? Stri
|
||||
onMounted(async () => {
|
||||
try { const usage = await fileBrowserClient.getUsage(); cloudStorageUsed.value = usage.totalSize; cloudFolderCount.value = usage.folderCount } catch { /* not running */ }
|
||||
loadSystemStats(); systemStatsInterval = setInterval(loadSystemStats, 10000); checkUpdateStatus(); loadWeb5Status()
|
||||
// Poll wallet balances/transactions like Web5.vue does — without this a
|
||||
// pending on-chain receive (or a fresh instant payment) only shows up
|
||||
// after a manual wallet action or a remount.
|
||||
walletRefreshInterval = setInterval(loadWeb5Status, 30000)
|
||||
})
|
||||
|
||||
// Wallet modals
|
||||
@ -594,6 +599,7 @@ const systemStats = computed(() => ({
|
||||
const systemUptimeDisplay = computed(() => { if (homeStatus.stats.uptimeSecs === 0) return t('home.systemMonitoring'); const days = Math.floor(homeStatus.stats.uptimeSecs / 86400); const hours = Math.floor((homeStatus.stats.uptimeSecs % 86400) / 3600); if (days > 0) return `Uptime: ${days}d ${hours}h`; const mins = Math.floor((homeStatus.stats.uptimeSecs % 3600) / 60); return `Uptime: ${hours}h ${mins}m` })
|
||||
|
||||
let systemStatsInterval: ReturnType<typeof setInterval> | null = null
|
||||
let walletRefreshInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
async function loadSystemStats() {
|
||||
await homeStatus.refresh(packages.value)
|
||||
|
||||
@ -67,15 +67,16 @@
|
||||
<div
|
||||
v-for="tx in incomingTransactions"
|
||||
:key="tx.tx_hash"
|
||||
class="flex items-center justify-between gap-3 px-4 py-3 hover:bg-white/5 cursor-pointer transition-colors"
|
||||
@click="$emit('openInMempool', tx.tx_hash)"
|
||||
class="flex items-center justify-between gap-3 px-4 py-3 hover:bg-white/5 transition-colors"
|
||||
:class="isOnchain(tx) ? 'cursor-pointer' : ''"
|
||||
@click="isOnchain(tx) && $emit('openInMempool', tx.tx_hash)"
|
||||
>
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div
|
||||
class="w-7 h-7 rounded-full flex items-center justify-center shrink-0"
|
||||
:class="tx.num_confirmations === 0 ? 'bg-yellow-500/15' : 'bg-green-500/15'"
|
||||
:class="isOnchain(tx) && tx.num_confirmations === 0 ? 'bg-yellow-500/15' : 'bg-green-500/15'"
|
||||
>
|
||||
<svg class="w-3.5 h-3.5" :class="tx.num_confirmations === 0 ? 'text-yellow-400' : 'text-green-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg class="w-3.5 h-3.5" :class="isOnchain(tx) && tx.num_confirmations === 0 ? 'text-yellow-400' : 'text-green-400'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</div>
|
||||
@ -83,18 +84,25 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium text-green-400">+{{ tx.amount_sats.toLocaleString() }} sats</span>
|
||||
<span
|
||||
v-if="isOnchain(tx)"
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium"
|
||||
:class="tx.num_confirmations === 0 ? 'bg-yellow-500/15 text-yellow-400' : 'bg-green-500/15 text-green-400'"
|
||||
>
|
||||
{{ tx.num_confirmations === 0 ? 'Unconfirmed' : tx.num_confirmations + ' conf' }}
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="text-[10px] px-1.5 py-0.5 rounded-full font-medium bg-green-500/15 text-green-400"
|
||||
>
|
||||
{{ railBadge(tx) }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-white/40 font-mono truncate mt-0.5">{{ tx.tx_hash }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="text-[11px] text-white/40">{{ formatTxTime(tx.time_stamp) }}</span>
|
||||
<svg class="w-3.5 h-3.5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-if="isOnchain(tx)" class="w-3.5 h-3.5 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</div>
|
||||
@ -181,7 +189,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
@ -224,11 +232,36 @@ defineEmits<{
|
||||
|
||||
const showIncomingTxPanel = ref(false)
|
||||
|
||||
function isOnchain(tx: WalletTransaction): boolean {
|
||||
return !tx.kind || tx.kind === 'onchain'
|
||||
}
|
||||
|
||||
// Instant rails (lightning/cashu/fedimint/ark) settle immediately — there is
|
||||
// no confirmation to wait for, so they only get the "incoming" treatment for
|
||||
// a short window after receipt instead of sitting in the badge forever.
|
||||
const INSTANT_INCOMING_WINDOW_SECS = 5 * 60
|
||||
const nowSecs = ref(Math.floor(Date.now() / 1000))
|
||||
let nowTimer: ReturnType<typeof setInterval> | null = null
|
||||
onMounted(() => { nowTimer = setInterval(() => { nowSecs.value = Math.floor(Date.now() / 1000) }, 30000) })
|
||||
onUnmounted(() => { if (nowTimer) clearInterval(nowTimer) })
|
||||
|
||||
const incomingTransactions = computed(() =>
|
||||
props.walletTransactions.filter(tx => tx.direction === 'incoming' && tx.num_confirmations < 3)
|
||||
props.walletTransactions.filter(tx => {
|
||||
if (tx.direction !== 'incoming') return false
|
||||
if (isOnchain(tx)) return tx.num_confirmations < 3
|
||||
return nowSecs.value - tx.time_stamp < INSTANT_INCOMING_WINDOW_SECS
|
||||
})
|
||||
)
|
||||
const incomingTxCount = computed(() => incomingTransactions.value.length)
|
||||
|
||||
function railBadge(tx: WalletTransaction): string {
|
||||
if (tx.kind === 'lightning') return '⚡ Instant'
|
||||
if (tx.kind === 'cashu') return 'Cashu'
|
||||
if (tx.kind === 'fedimint') return 'Fedimint'
|
||||
if (tx.kind === 'ark') return 'Ark'
|
||||
return ''
|
||||
}
|
||||
|
||||
function formatTxTime(timestamp: number): string {
|
||||
if (!timestamp) return ''
|
||||
const date = new Date(timestamp * 1000)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user