From 6bb07ff9ec5c69b01c1822471c24312b44707927 Mon Sep 17 00:00:00 2001 From: protom Date: Mon, 31 Aug 2026 12:54:46 +0200 Subject: [PATCH] fix(lnurlcash): assert named claims land at the wallet's own secret Review feedback: bind the claim answer to the secret that was polled before building a note from it. The kit already guarantees this - claimMintedNote returns the queried secret by construction and fetchNoteInfo rejects a mismatched k1 echo - so the assert cannot trip through the kit (and is therefore not separately testable); it exists so a future kit regression fails loudly instead of tracking a wrong secret. --- src/lnurlcash/ops/mint.ts | 7 +++++++ src/lnurlcash/ops/transfer.ts | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/lnurlcash/ops/mint.ts b/src/lnurlcash/ops/mint.ts index ad04023..5abd23b 100644 --- a/src/lnurlcash/ops/mint.ts +++ b/src/lnurlcash/ops/mint.ts @@ -245,6 +245,13 @@ export const claimFromSecret = async ( // is lying about the note it just credited throw new Error('The mint reports the freshly minted note as already spent.') } + if (claim.k1 !== target.noteSecret) { + // belt and suspenders: the kit's claimMintedNote returns the queried + // secret by construction and fetchNoteInfo rejects a mismatched echo, + // so this cannot trip through the kit - if it ever does, building a + // note from the answer would track the wrong secret + throw new Error('The mint answered the claim for a different note secret.') + } if (claim.amountMsat === null || !claim.callback) { throw new Error('The mint did not return the minted note details.') } diff --git a/src/lnurlcash/ops/transfer.ts b/src/lnurlcash/ops/transfer.ts index a72abc6..6141214 100644 --- a/src/lnurlcash/ops/transfer.ts +++ b/src/lnurlcash/ops/transfer.ts @@ -215,10 +215,18 @@ export const transferBetweenMints = async ( // proof - the target only credits it once the melt's payment landed try { const claim = await pollMintClaim(prepared.withdrawLink, noteSecret, poll, options) - if (claim.state !== 'minted' || claim.amountMsat === null || !claim.callback) { - // 'spent' on a fresh wallet-chosen secret, or an incomplete - // answer: the target misbehaved - the way back to the note is - // already in the claim material + if ( + claim.state !== 'minted' || + claim.k1 !== noteSecret || + claim.amountMsat === null || + !claim.callback + ) { + // 'spent' on a fresh wallet-chosen secret, a claim naming a + // secret other than the one polled (impossible through the kit, + // which returns the queried secret and rejects mismatched + // echoes - asserted anyway: building from it would track the + // wrong note), or an incomplete answer: the target misbehaved - + // the way back to the note is already in the claim material return {...base, outcome: 'settled-claim-failed', claimMaterial} } const note: NewBearer = {