Files
sattle/src/lnurlcash/nwc.fencing.cases.ts
T

67 lines
2.5 KiB
TypeScript

// Stale-owner fencing end to end: a service accepted work while its owner
// was still installed, and a second tab replaced the saved key AFTER the
// fence's last safe point - mid-melt, past the boundary where aborting is
// no longer possible. What must not happen is any stale-owner WRITE:
// no bearer changeset, no budget debit, no success response.
import {describe, expect, it} from 'vitest'
import {bytesToHex} from '@noble/hashes/utils.js'
import {readNwcConnections} from './nwc'
import {
methodRequest,
OTHER_LINKING_KEY,
OTHER_OWNER_ID,
OWNER_ID,
waitFor,
} from './nwc.testProtocol'
import {makeBearer, mint, readResponse, startTestService} from './nwc.testService'
describe('service: post-boundary stale-owner fencing', () => {
it('makes no stale-owner writes when the saved key is replaced mid-melt', async () => {
// Given a running service whose next mint call coincides with a second
// tab installing its own wallet (the first fetch is the melt itself:
// the exact-match carve performs no mint call of its own)
const m = await mint()
let swapped = false
const swappingFetch: typeof fetch = (input, init) => {
if (!swapped) {
swapped = true
localStorage.setItem(
'sattle_linking_key',
JSON.stringify({
enc: false,
value: bytesToHex(OTHER_LINKING_KEY),
ownerId: OTHER_OWNER_ID,
version: 1,
}),
)
}
return fetch(input, init)
}
const {relay, walletServicePubkey, state, stop} = await startTestService({
kit: {fetch: swappingFetch},
})
state.bearers = [await makeBearer(m, 'd7'.repeat(32), 21_000)]
const request = methodRequest(walletServicePubkey, 'pay_invoice', {
invoice: 'lnbc210n1pjqrstuvwxyz',
})
// When the pay flows past the irreversible boundary and reaches the
// first owner-bound persistence (the conservative budget debit)
relay.emit(request)
await waitFor(() => state.errors.length > 0)
// Then the melt genuinely happened (we are past the boundary) ...
expect(swapped).toBe(true)
expect(m.state.noteState('d7'.repeat(32))).toBe('burned')
// ... but nothing under the stale owner moved: no budget debit, no
// bearer changeset, no success response (the failure surfaced through
// onError instead)
expect(readNwcConnections(OWNER_ID)[0]?.spent.msat).toBe(0)
expect(state.changesets).toEqual([])
expect(readResponse(relay.published, request.id, 'nip44_v2')).toBeNull()
await stop()
})
})