fix(ecash): harden Minibits claim persistence
This commit is contained in:
@@ -242,13 +242,17 @@ const lnClaimedSats = ref(0)
|
||||
// operator should see it rather than have it be a silent, unbounded wait.
|
||||
const lnPendingClaims = ref(0)
|
||||
let lnClaimTimer: ReturnType<typeof setInterval> | null = null
|
||||
let lnClaimInFlight = false
|
||||
|
||||
async function loadLnAddress() {
|
||||
if (lnAddress.value || lnAddressLoading.value) return
|
||||
lnAddressLoading.value = true
|
||||
lnAddressError.value = false
|
||||
try {
|
||||
const res = await rpcClient.call<{ address?: string }>({ method: 'wallet.ecash-lnaddress' })
|
||||
const res = await rpcClient.call<{ address?: string }>({
|
||||
method: 'wallet.ecash-lnaddress',
|
||||
timeout: 60_000,
|
||||
})
|
||||
lnAddress.value = res?.address || ''
|
||||
if (lnAddress.value) {
|
||||
await nextTick()
|
||||
@@ -277,14 +281,23 @@ function startLnClaimPoll() {
|
||||
}
|
||||
|
||||
async function pollLnClaims() {
|
||||
if (lnClaimInFlight) return
|
||||
if (!props.show || !lnAddress.value) {
|
||||
stopLnClaimPoll()
|
||||
return
|
||||
}
|
||||
lnClaimInFlight = true
|
||||
try {
|
||||
const res = await rpcClient.call<{ received_sats?: number; failed_count?: number }>({
|
||||
method: 'wallet.ecash-lnaddress-claim',
|
||||
// Relay collection alone has a ten-second window and redemption may
|
||||
// then contact the mint. Keep the browser request alive long enough for
|
||||
// the backend's bounded work instead of timing out and queuing another.
|
||||
timeout: 90_000,
|
||||
})
|
||||
// The user may have closed the modal while the relay fetch was in flight.
|
||||
// Do not resurrect its status or emit a stale received event afterward.
|
||||
if (!props.show || !lnAddress.value) return
|
||||
if (res?.received_sats && res.received_sats > 0) {
|
||||
lnClaimedSats.value += res.received_sats
|
||||
emit('received')
|
||||
@@ -292,6 +305,8 @@ async function pollLnClaims() {
|
||||
lnPendingClaims.value = res?.failed_count || 0
|
||||
} catch {
|
||||
// Transient poll failure (offline, mint busy) — keep polling.
|
||||
} finally {
|
||||
lnClaimInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: { call: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
toCanvas: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useLightningRequired', () => ({
|
||||
useLightningRequired: () => ({
|
||||
requireLightningReady: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -16,6 +16,10 @@ vi.mock('@/api/rpc-client', () => ({
|
||||
rpcClient: { call: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('qrcode', () => ({
|
||||
toCanvas: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useLightningRequired', () => ({
|
||||
useLightningRequired: () => ({
|
||||
requireLightningReady: vi.fn().mockResolvedValue(true),
|
||||
@@ -70,4 +74,38 @@ describe('ReceiveBitcoinModal — ecash tab click', () => {
|
||||
expect(document.body.querySelector('[role="dialog"]')).toBeTruthy()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('never overlaps slow Lightning-address claim polls', async () => {
|
||||
vi.useFakeTimers()
|
||||
let finishClaim!: (value: unknown) => void
|
||||
const slowClaim = new Promise((resolve) => { finishClaim = resolve })
|
||||
vi.mocked(rpcClient.call).mockImplementation(async ({ method }: { method: string }) => {
|
||||
if (method === 'wallet.ecash-lnaddress') {
|
||||
return { address: 'someone@minibits.cash' } as never
|
||||
}
|
||||
if (method === 'wallet.ecash-lnaddress-claim') return slowClaim as never
|
||||
return {} as never
|
||||
})
|
||||
|
||||
const wrapper = mount(ReceiveBitcoinModal, {
|
||||
props: { show: true },
|
||||
attachTo: document.body,
|
||||
})
|
||||
const ecashTab = Array.from(document.body.querySelectorAll('button')).find((b) =>
|
||||
b.textContent?.toLowerCase().includes('ecash'),
|
||||
)
|
||||
ecashTab!.dispatchEvent(new Event('click', { bubbles: true }))
|
||||
await flushPromises()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(24_000)
|
||||
const claimCalls = vi.mocked(rpcClient.call).mock.calls.filter(
|
||||
([request]) => request.method === 'wallet.ecash-lnaddress-claim',
|
||||
)
|
||||
expect(claimCalls).toHaveLength(1)
|
||||
|
||||
finishClaim({ claimed_count: 0, received_sats: 0, failed_count: 0 })
|
||||
await flushPromises()
|
||||
wrapper.unmount()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -776,7 +776,7 @@
|
||||
"transactionId": "Transaction ID",
|
||||
"pasteEcashToken": "Paste ecash token",
|
||||
"lnAddressTitle": "Or share your Minibits Lightning address",
|
||||
"lnAddressHint": "Anyone can pay you sats with any Lightning wallet by sending to this address — the sats arrive as ecash. Keep this screen open to receive them.",
|
||||
"lnAddressHint": "Anyone can pay you sats with any Lightning wallet by sending to this address — the sats arrive as ecash. Keep this screen open to receive them. Minibits is a third-party beta service; keep balances small.",
|
||||
"lnAddressLabel": "Your {'@'}minibits.cash address:",
|
||||
"lnAddressLoading": "Setting up your Lightning address…",
|
||||
"lnAddressUnavailable": "Lightning address unavailable — you can still paste a token below.",
|
||||
|
||||
@@ -757,7 +757,7 @@
|
||||
"transactionId": "ID de transacci\u00f3n",
|
||||
"pasteEcashToken": "Pegar token Ecash",
|
||||
"lnAddressTitle": "O comparte tu direcci\u00f3n Lightning de Minibits",
|
||||
"lnAddressHint": "Cualquier persona puede pagarte sats con cualquier billetera Lightning enviando a esta direcci\u00f3n \u2014 los sats llegan como ecash. Mant\u00e9n esta pantalla abierta para recibirlos.",
|
||||
"lnAddressHint": "Cualquier persona puede pagarte sats con cualquier billetera Lightning enviando a esta direcci\u00f3n \u2014 los sats llegan como ecash. Mant\u00e9n esta pantalla abierta para recibirlos. Minibits es un servicio beta de terceros; mant\u00e9n saldos peque\u00f1os.",
|
||||
"lnAddressLabel": "Su direcci\u00f3n {'@'}minibits.cash:",
|
||||
"lnAddressLoading": "Configurando su direcci\u00f3n Lightning\u2026",
|
||||
"lnAddressUnavailable": "Direcci\u00f3n Lightning no disponible \u2014 a\u00fan puede pegar un token abajo.",
|
||||
|
||||
Reference in New Issue
Block a user