mirror of
https://github.com/tompro/sattle.git
synced 2026-08-26 23:05:58 +00:00
fix: hide invoice settlement until commit
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
// The NWC wallet service end to end: connection strings and key
|
||||
// derivation, the request/response cycle over an in-memory relay (the
|
||||
// transport is injected - no network), every method against the
|
||||
// conformance mock mint, the legacy NIP-04 path, budget enforcement, and
|
||||
// the error paths. Fund-safety focus: budgets can't be exceeded, stale
|
||||
// requests never execute, and a settled preimage only ever reveals an
|
||||
// already-rotated (burned) note secret.
|
||||
|
||||
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
|
||||
import {bytesToHex, hexToBytes} from '@noble/hashes/utils.js'
|
||||
import {finalizeEvent, getPublicKey} from 'nostr-tools/pure'
|
||||
import {encrypt as nip04Encrypt, decrypt as nip04Decrypt} from 'nostr-tools/nip04'
|
||||
import {v2 as nip44v2} from 'nostr-tools/nip44'
|
||||
import {buildNoteUrl, fetchNoteInfo, noteK1} from 'lnurlcash-kit'
|
||||
import {createMockMint} from 'lnurlcash-conformance/mock-mint'
|
||||
|
||||
import {
|
||||
NWC_INFO_KIND,
|
||||
NWC_REQUEST_KIND,
|
||||
NWC_RESPONSE_KIND,
|
||||
buildConnectionString,
|
||||
connectionInfoOf,
|
||||
createConnection,
|
||||
deriveNwcWalletKey,
|
||||
migrateLegacyNwcStorage,
|
||||
parseConnectionString,
|
||||
readNwcEnabled,
|
||||
readNwcConnections,
|
||||
startService,
|
||||
writeNwcEnabled,
|
||||
writeNwcConnections,
|
||||
} from './nwc'
|
||||
import type {NostrEvent, NwcConnectionRecord, NwcServiceDeps, NwcTransport} from './nwc'
|
||||
import type {NostrFilter} from './nwc/transport'
|
||||
import type {NwcChangeset} from './nwc'
|
||||
import type {Bearer} from './types'
|
||||
import {ensureSavedKeyOwner, linkingPubKeyHex, saveLinkingKey} from './keys'
|
||||
import {requiredString, requiredValue, stubLocalStorage} from './test-utils'
|
||||
|
||||
import {
|
||||
CLIENT_PUBKEY,
|
||||
CLIENT_SECRET,
|
||||
FAST_POLL,
|
||||
LINKING_KEY,
|
||||
OTHER_LINKING_KEY,
|
||||
OTHER_OWNER_ID,
|
||||
OWNER_ID,
|
||||
RELAYS,
|
||||
STRANGER_SECRET,
|
||||
clientRequest,
|
||||
createFakeRelay,
|
||||
deferred,
|
||||
foreignConnectionFixture,
|
||||
methodRequest,
|
||||
nowSeconds,
|
||||
storeForeignConnection,
|
||||
waitFor,
|
||||
} from './nwc.testProtocol'
|
||||
import {call, makeBearer, mint, readResponse, startTestService} from './nwc.testService'
|
||||
describe('service: make_invoice / lookup_invoice', () => {
|
||||
it('issues an invoice, settles it in the background, and reports the preimage', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
})
|
||||
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
description: 'nwc test',
|
||||
expiry: 3600,
|
||||
})
|
||||
expect(made.error).toBeNull()
|
||||
expect(made.result).toMatchObject({
|
||||
type: 'incoming',
|
||||
state: 'pending',
|
||||
amount: 21_000,
|
||||
description: 'nwc test',
|
||||
created_at: nowSeconds(),
|
||||
expires_at: nowSeconds() + 3600,
|
||||
})
|
||||
const invoice = requiredValue(made.result).invoice
|
||||
if (typeof invoice !== 'string') {
|
||||
throw new TypeError('make_invoice did not return an invoice')
|
||||
}
|
||||
const paymentHash = made.result?.payment_hash
|
||||
if (typeof paymentHash !== 'string') {
|
||||
throw new TypeError('make_invoice did not return a payment hash')
|
||||
}
|
||||
expect(invoice).toMatch(/^lnbc/)
|
||||
expect(paymentHash).toMatch(/^[0-9a-f]{64}$/)
|
||||
|
||||
// before settlement the lookup reports the pending invoice
|
||||
const pending = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
expect(pending.error).toBeNull()
|
||||
expect(pending.result?.state).toBe('pending')
|
||||
expect(pending.result?.preimage).toBeUndefined()
|
||||
|
||||
// the "payer" pays the invoice; the background claim settles and
|
||||
// mints the note
|
||||
const settleRes = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||
expect(settleRes.ok).toBe(true)
|
||||
await waitFor(() => state.changesets.some((c) => c.add.length > 0))
|
||||
|
||||
const settled = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
expect(settled.error).toBeNull()
|
||||
expect(settled.result?.state).toBe('settled')
|
||||
expect(settled.result?.settled_at).toBe(nowSeconds())
|
||||
const preimage = requiredString(settled.result?.preimage)
|
||||
expect(preimage).toMatch(/^[0-9a-f]{64}$/)
|
||||
|
||||
// the minted note was claimed AND rotated before settlement was
|
||||
// recorded: the preimage the client just learned is a burned secret,
|
||||
// and the wallet's fresh note is the only live one
|
||||
expect(m.state.noteState(preimage)).toBe('burned')
|
||||
const minted = requiredValue(state.bearers.find((b) => b.id.startsWith('added-')))
|
||||
expect(minted.amount).toBe(21_000)
|
||||
expect(minted.verified).toBe(true)
|
||||
expect(noteK1(minted.url)).not.toBe(preimage)
|
||||
expect(m.state.noteState(requiredValue(noteK1(minted.url)))).toBe('outstanding')
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('keeps a paid invoice pending while its bearer commit is deferred', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const commit = deferred()
|
||||
let commitStarted = false
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
commitChangeset: () => {
|
||||
commitStarted = true
|
||||
return commit.promise
|
||||
},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
const paymentHash = made.result?.payment_hash
|
||||
if (typeof paymentHash !== 'string') {
|
||||
throw new TypeError('make_invoice did not return a payment hash')
|
||||
}
|
||||
|
||||
const settleResponse = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||
expect(settleResponse.ok).toBe(true)
|
||||
await waitFor(() => commitStarted)
|
||||
|
||||
const pending = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
expect(pending.result?.state).toBe('pending')
|
||||
expect(pending.result?.settled_at).toBeUndefined()
|
||||
expect(pending.result?.preimage).toBeUndefined()
|
||||
|
||||
commit.resolve()
|
||||
await waitFor(() => state.changesets.length === 1)
|
||||
const settled = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
expect(settled.result?.state).toBe('settled')
|
||||
expect(settled.result?.settled_at).toBe(nowSeconds())
|
||||
expect(settled.result?.preimage).toMatch(/^[0-9a-f]{64}$/)
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('keeps repeated stops pending until an already-started invoice settlement commits', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const commit = deferred()
|
||||
let commitStarted = false
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
commitChangeset: () => {
|
||||
commitStarted = true
|
||||
return commit.promise
|
||||
},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
const paymentHash = made.result?.payment_hash
|
||||
if (typeof paymentHash !== 'string') {
|
||||
throw new TypeError('make_invoice did not return a payment hash')
|
||||
}
|
||||
const settleResponse = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||
expect(settleResponse.ok).toBe(true)
|
||||
await waitFor(() => commitStarted)
|
||||
|
||||
let stopped = false
|
||||
const firstStop = stop().then(() => {
|
||||
stopped = true
|
||||
return state.changesets.length
|
||||
})
|
||||
const repeatedStop = stop()
|
||||
for (let turn = 0; turn < 10; turn += 1) await Promise.resolve()
|
||||
|
||||
expect(stopped).toBe(false)
|
||||
expect(state.changesets).toHaveLength(0)
|
||||
|
||||
commit.resolve()
|
||||
const [changesetsAtStop] = await Promise.all([firstStop, repeatedStop])
|
||||
expect(changesetsAtStop).toBe(1)
|
||||
expect(state.changesets).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,241 @@
|
||||
// The NWC wallet service end to end: connection strings and key
|
||||
// derivation, the request/response cycle over an in-memory relay (the
|
||||
// transport is injected - no network), every method against the
|
||||
// conformance mock mint, the legacy NIP-04 path, budget enforcement, and
|
||||
// the error paths. Fund-safety focus: budgets can't be exceeded, stale
|
||||
// requests never execute, and a settled preimage only ever reveals an
|
||||
// already-rotated (burned) note secret.
|
||||
|
||||
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
|
||||
import {bytesToHex, hexToBytes} from '@noble/hashes/utils.js'
|
||||
import {finalizeEvent, getPublicKey} from 'nostr-tools/pure'
|
||||
import {encrypt as nip04Encrypt, decrypt as nip04Decrypt} from 'nostr-tools/nip04'
|
||||
import {v2 as nip44v2} from 'nostr-tools/nip44'
|
||||
import {buildNoteUrl, fetchNoteInfo, noteK1} from 'lnurlcash-kit'
|
||||
import {createMockMint} from 'lnurlcash-conformance/mock-mint'
|
||||
|
||||
import {
|
||||
NWC_INFO_KIND,
|
||||
NWC_REQUEST_KIND,
|
||||
NWC_RESPONSE_KIND,
|
||||
buildConnectionString,
|
||||
connectionInfoOf,
|
||||
createConnection,
|
||||
deriveNwcWalletKey,
|
||||
migrateLegacyNwcStorage,
|
||||
parseConnectionString,
|
||||
readNwcEnabled,
|
||||
readNwcConnections,
|
||||
startService,
|
||||
writeNwcEnabled,
|
||||
writeNwcConnections,
|
||||
} from './nwc'
|
||||
import type {NostrEvent, NwcConnectionRecord, NwcServiceDeps, NwcTransport} from './nwc'
|
||||
import type {NostrFilter} from './nwc/transport'
|
||||
import type {NwcChangeset} from './nwc'
|
||||
import type {Bearer} from './types'
|
||||
import {ensureSavedKeyOwner, linkingPubKeyHex, saveLinkingKey} from './keys'
|
||||
import {requiredValue, stubLocalStorage} from './test-utils'
|
||||
|
||||
import {
|
||||
CLIENT_PUBKEY,
|
||||
CLIENT_SECRET,
|
||||
FAST_POLL,
|
||||
LINKING_KEY,
|
||||
OTHER_LINKING_KEY,
|
||||
OTHER_OWNER_ID,
|
||||
OWNER_ID,
|
||||
RELAYS,
|
||||
STRANGER_SECRET,
|
||||
clientRequest,
|
||||
createFakeRelay,
|
||||
deferred,
|
||||
foreignConnectionFixture,
|
||||
methodRequest,
|
||||
nowSeconds,
|
||||
storeForeignConnection,
|
||||
waitFor,
|
||||
} from './nwc.testProtocol'
|
||||
import {call, makeBearer, mint, readResponse, startTestService} from './nwc.testService'
|
||||
describe('service: make_invoice / lookup_invoice (continued)', () => {
|
||||
it('drains a rejected invoice settlement and reports it before stop resolves', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const commit = deferred()
|
||||
const commitError = new Error('invoice commit rejected during stop')
|
||||
let commitStarted = false
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
commitChangeset: () => {
|
||||
commitStarted = true
|
||||
return commit.promise
|
||||
},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
const paymentHash = made.result?.payment_hash
|
||||
if (typeof paymentHash !== 'string') {
|
||||
throw new TypeError('make_invoice did not return a payment hash')
|
||||
}
|
||||
const settleResponse = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||
expect(settleResponse.ok).toBe(true)
|
||||
await waitFor(() => commitStarted)
|
||||
|
||||
let stopped = false
|
||||
const stopping = stop().then(() => {
|
||||
stopped = true
|
||||
return state.errors.length
|
||||
})
|
||||
for (let turn = 0; turn < 10; turn += 1) await Promise.resolve()
|
||||
expect(stopped).toBe(false)
|
||||
|
||||
commit.reject(commitError)
|
||||
expect(await stopping).toBe(1)
|
||||
expect(state.errors).toEqual([commitError])
|
||||
expect(state.changesets).toHaveLength(0)
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('does not block stop on an invoice whose claim is still polling', async () => {
|
||||
// an unpaid invoice's claim poll can legally run for minutes (the
|
||||
// client pays whenever it pays) - stop must interrupt the wait, not
|
||||
// sit on it; a settlement that already REACHED the commit phase is
|
||||
// still awaited (see the drain tests above)
|
||||
const m = await mint({testHooks: true})
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
claimPoll: {intervalMs: 50, intervalCapMs: 50, maxWaitMs: 60_000},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 5_000,
|
||||
})
|
||||
expect(made.error).toBeNull()
|
||||
|
||||
// nobody pays the invoice; the claim keeps polling. stop must resolve
|
||||
// promptly regardless (an un-interrupted stop would wait out the
|
||||
// whole 60s claim budget)
|
||||
await stop()
|
||||
expect(state.changesets).toHaveLength(0)
|
||||
expect(state.errors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('does not start invoice settlement after stop begins during preparation', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const prepare = deferred()
|
||||
let prepareStarted = false
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
kit: {
|
||||
fetch: async (input, init) => {
|
||||
prepareStarted = true
|
||||
await prepare.promise
|
||||
return fetch(input, init)
|
||||
},
|
||||
},
|
||||
})
|
||||
const request = methodRequest(walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
relay.emit(request)
|
||||
await waitFor(() => prepareStarted)
|
||||
|
||||
let stopped = false
|
||||
const stopping = stop().then(() => {
|
||||
stopped = true
|
||||
})
|
||||
for (let turn = 0; turn < 10; turn += 1) await Promise.resolve()
|
||||
expect(stopped).toBe(false)
|
||||
|
||||
prepare.resolve()
|
||||
await stopping
|
||||
|
||||
expect(state.changesets).toHaveLength(0)
|
||||
expect(readResponse(relay.published, request.id, 'nip44_v2')?.error?.code).toBe('INTERNAL')
|
||||
})
|
||||
|
||||
it('marks a paid invoice failed when its bearer commit rejects', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const commit = deferred()
|
||||
const commitError = new Error('invoice bearer commit failed')
|
||||
let commitStarted = false
|
||||
const {relay, walletServicePubkey, state, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
commitChangeset: () => {
|
||||
commitStarted = true
|
||||
return commit.promise
|
||||
},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
const paymentHash = made.result?.payment_hash
|
||||
if (typeof paymentHash !== 'string') {
|
||||
throw new TypeError('make_invoice did not return a payment hash')
|
||||
}
|
||||
|
||||
const settleResponse = await fetch(`${m.url}/_test/settle?payment_hash=${paymentHash}`)
|
||||
expect(settleResponse.ok).toBe(true)
|
||||
await waitFor(() => commitStarted)
|
||||
commit.reject(commitError)
|
||||
await waitFor(() => state.errors.length === 1)
|
||||
|
||||
const failed = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
expect(failed.result?.state).toBe('failed')
|
||||
expect(failed.result?.settled_at).toBeUndefined()
|
||||
expect(failed.result?.preimage).toBeUndefined()
|
||||
expect(state.errors).toEqual([commitError])
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('finds an invoice by its invoice string too', async () => {
|
||||
const m = await mint({testHooks: true})
|
||||
const {relay, walletServicePubkey, stop} = await startTestService({
|
||||
defaultMint: `mint@127.0.0.1:${m.port}`,
|
||||
claimPoll: {intervalMs: 1, intervalCapMs: 2, maxWaitMs: 10},
|
||||
})
|
||||
const made = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 5_000,
|
||||
})
|
||||
const invoice = made.result?.invoice
|
||||
if (typeof invoice !== 'string') {
|
||||
throw new TypeError('make_invoice did not return an invoice')
|
||||
}
|
||||
const found = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
invoice: invoice.toUpperCase(),
|
||||
})
|
||||
expect(found.error).toBeNull()
|
||||
expect(found.result?.payment_hash).toBe(made.result?.payment_hash)
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('answers an unknown invoice with NOT_FOUND', async () => {
|
||||
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||
const response = await call(relay, walletServicePubkey, 'lookup_invoice', {
|
||||
payment_hash: 'ab'.repeat(32),
|
||||
})
|
||||
expect(response.error?.code).toBe('NOT_FOUND')
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('answers make_invoice without a default mint with INTERNAL', async () => {
|
||||
const {relay, walletServicePubkey, stop} = await startTestService({
|
||||
defaultMint: null,
|
||||
})
|
||||
const response = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: 21_000,
|
||||
})
|
||||
expect(response.error?.code).toBe('INTERNAL')
|
||||
await stop()
|
||||
})
|
||||
|
||||
it('answers a make_invoice with a bad amount with OTHER', async () => {
|
||||
const {relay, walletServicePubkey, stop} = await startTestService({})
|
||||
const response = await call(relay, walletServicePubkey, 'make_invoice', {
|
||||
amount: -5,
|
||||
})
|
||||
expect(response.error?.code).toBe('OTHER')
|
||||
await stop()
|
||||
})
|
||||
})
|
||||
@@ -23,10 +23,10 @@ import type {PollOptions} from '../ops/shared'
|
||||
|
||||
import type {PendingInvoice, RequestContext} from './context'
|
||||
|
||||
export const DEFAULT_CLAIM_POLL: Required<PollOptions> = {
|
||||
export const DEFAULT_CLAIM_POLL: Required<Omit<PollOptions, 'signal'>> = {
|
||||
intervalMs: 2000,
|
||||
intervalCapMs: 10_000,
|
||||
maxWaitMs: 15 * 60_000
|
||||
maxWaitMs: 15 * 60_000,
|
||||
}
|
||||
|
||||
// LUD-21 verify URLs end in /verify/<payment_hash> (the protocol's verify
|
||||
@@ -41,9 +41,7 @@ export const resolvePaymentHash = (prepared: PreparedMint): string => {
|
||||
}
|
||||
|
||||
// the NIP-47 transaction object make_invoice and lookup_invoice share
|
||||
export const invoiceResult = (
|
||||
entry: PendingInvoice
|
||||
): Record<string, unknown> => {
|
||||
export const invoiceResult = (entry: PendingInvoice): Record<string, unknown> => {
|
||||
const result: Record<string, unknown> = {
|
||||
type: 'incoming',
|
||||
state: entry.state,
|
||||
@@ -51,7 +49,7 @@ export const invoiceResult = (
|
||||
payment_hash: entry.paymentHash,
|
||||
amount: entry.amountMsat,
|
||||
created_at: entry.createdAt,
|
||||
metadata: {}
|
||||
metadata: {},
|
||||
}
|
||||
if (entry.description) result.description = entry.description
|
||||
if (entry.expiresAt) result.expires_at = entry.expiresAt
|
||||
@@ -64,47 +62,50 @@ export const invoiceResult = (
|
||||
|
||||
// the background half of make_invoice: watch the invoice, and once it
|
||||
// settles claim the note (rotating it immediately) and hand the fresh
|
||||
// bearer to the caller. Settlement is recorded LAST - after the rotate -
|
||||
// so a preimage lookup can only ever reveal an already-burned secret.
|
||||
// bearer to the caller. Settlement is recorded LAST - after the rotate and
|
||||
// bearer commit - so lookup can only reveal durably tracked funds and an
|
||||
// already-burned secret.
|
||||
// Throws on any failure; the caller marks the entry failed and reports
|
||||
// through deps.onError.
|
||||
export const settleAndClaim = async (
|
||||
ctx: RequestContext,
|
||||
entry: PendingInvoice
|
||||
): Promise<void> => {
|
||||
export const settleAndClaim = async (ctx: RequestContext, entry: PendingInvoice): Promise<void> => {
|
||||
if (!entry.prepared.verifyUrl) {
|
||||
throw new Error(
|
||||
'This mint did not advertise a verify URL - the invoice cannot be auto-claimed.'
|
||||
'This mint did not advertise a verify URL - the invoice cannot be auto-claimed.',
|
||||
)
|
||||
}
|
||||
// the observation half is interruptible (the client may never pay, so
|
||||
// the poll can legally outlive the service); once settlement is seen,
|
||||
// everything below - claim, rotate, bearer commit - is fund-critical and
|
||||
// deliberately ignores the stop signal: stop's drain awaits it
|
||||
const result = await pollVerifyUntilSettled(
|
||||
entry.prepared.verifyUrl,
|
||||
ctx.deps.claimPoll ?? DEFAULT_CLAIM_POLL,
|
||||
ctx.deps.kit ?? {}
|
||||
{...(ctx.deps.claimPoll ?? DEFAULT_CLAIM_POLL), signal: ctx.stopSignal},
|
||||
ctx.deps.kit ?? {},
|
||||
)
|
||||
// a settled report only means this wallet's invoice was paid if it's
|
||||
// for the invoice this wallet actually requested
|
||||
if (!sameInvoice(result.pr, entry.prepared.invoice)) {
|
||||
throw new Error(
|
||||
"The service's verify response is for a different invoice than requested."
|
||||
)
|
||||
throw new Error("The service's verify response is for a different invoice than requested.")
|
||||
}
|
||||
const preimage = result.preimage
|
||||
if (!preimage || !isPreimage(preimage)) {
|
||||
throw new Error(
|
||||
'The payment settled but the service did not reveal the preimage.'
|
||||
)
|
||||
throw new Error('The payment settled but the service did not reveal the preimage.')
|
||||
}
|
||||
// claimFromPreimage IS claimMintedNote's claim half (poll above is the
|
||||
// other half) - invoked in two steps here because NWC needs the
|
||||
// preimage, which claimMintedNote deliberately discards
|
||||
const claimed = await claimFromPreimage(
|
||||
entry.prepared,
|
||||
preimage,
|
||||
ctx.deps.kit ?? {}
|
||||
)
|
||||
const claimed = await claimFromPreimage(entry.prepared, preimage, {
|
||||
...(ctx.deps.kit ?? {}),
|
||||
assertOwner: ctx.assertOwner,
|
||||
})
|
||||
const add: NewBearer[] = [claimed.note]
|
||||
if (claimed.possibleCopy) add.push(claimed.possibleCopy)
|
||||
await ctx.deps.applyChangeset(
|
||||
{add, markSpent: []},
|
||||
ctx.connection(),
|
||||
'make_invoice',
|
||||
ctx.assertOwner,
|
||||
)
|
||||
entry.settledAt = ctx.nowSeconds()
|
||||
entry.state = 'settled'
|
||||
if (claimed.rotated) {
|
||||
@@ -112,5 +113,4 @@ export const settleAndClaim = async (
|
||||
// secret, safe to hand out as the settlement receipt
|
||||
entry.preimage = preimage
|
||||
}
|
||||
ctx.deps.applyChangeset({add, markSpent: []}, ctx.connection(), 'make_invoice')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user