mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 08:47:33 +00:00
feat(cashu): claim P2PK-locked tokens and clear errors when we can't
Pasting a P2PK-locked cashu token (NUT-11) into the wallet sent the proofs to /v1/swap with no witness, so the mint rejected them with an opaque `witness is missing for p2pk signature` 400. Only the NIP-61 nutzap path signed witnesses; the generic redeem path had no P2PK support at all. - quartz: add `signP2pkWitnesses` (pure, resolver-driven) + the `P2PKUnredeemableException` it throws when a locked proof's key is unknown, and a `CashuMintOperations.redeemToken` that signs then swaps. - commons: `CashuWalletOps.redeemToken` now takes the wallet P2PK key and (local-signer-only) identity key, indexes them by x-only pubkey, and routes through the P2PK-aware path. Add `describeRedeemError`, which tells a user whose token is locked to their own identity key (e.g. Bey Wallet's P2PK send) — but who is on a bunker/external signer that can't sign a raw witness — to import their nsec elsewhere to claim it. - amethyst: `CashuWalletState.redeemSigningKeys()` surfaces both keys (identity key only for a local NostrSignerInternal); the wallet ViewModel wires them in and reports via `describeRedeemError`. - cli: `amy cashu receive token` passes the same keys and reports a distinct `p2pk_locked` error code. Adds P2PKRedeemTest covering pass-through, x-only + compressed locks, verifiable witnesses, and the unredeemable case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QKeRaX749TYnJ7oR8UpqA4
This commit is contained in:
+19
-1
@@ -26,6 +26,9 @@ import com.vitorpamplona.amethyst.cli.DataDir
|
||||
import com.vitorpamplona.amethyst.cli.Output
|
||||
import com.vitorpamplona.amethyst.cli.commands.route
|
||||
import com.vitorpamplona.quartz.lightning.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip60Cashu.p2pk.P2PKUnredeemableException
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenB64Parser
|
||||
|
||||
/**
|
||||
@@ -156,12 +159,25 @@ object CashuReceiveCommands {
|
||||
Context.open(dataDir).use { ctx ->
|
||||
ctx.prepare()
|
||||
return try {
|
||||
// Keys that can unlock a P2PK-locked token: the wallet's kind:17375
|
||||
// key, plus — for a local key account — the identity key (some
|
||||
// senders, e.g. Bey Wallet, P2PK-lock ecash to the recipient npub).
|
||||
val snap = ctx.cashuSnapshot()
|
||||
val walletKey = snap.walletEvent?.let { runCatching { it.privkey(ctx.signer) }.getOrNull() }
|
||||
val identityKey = (ctx.signer as? NostrSignerInternal)?.keyPair?.privKey?.toHexKey()
|
||||
var total = 0L
|
||||
var lastTokenEventId: String? = null
|
||||
var lastHistoryEventId: String? = null
|
||||
var mint = ""
|
||||
for (t in parsed) {
|
||||
val redeemed = ctx.cashuOps().redeemToken(raw, t.proofs, t.mint)
|
||||
val redeemed =
|
||||
ctx.cashuOps().redeemToken(
|
||||
cashuToken = raw,
|
||||
proofs = t.proofs,
|
||||
mintUrl = t.mint,
|
||||
walletP2pkPrivkeyHex = walletKey,
|
||||
identityPrivkeyHex = identityKey,
|
||||
)
|
||||
total += redeemed.amount
|
||||
lastTokenEventId = redeemed.tokenEvent.id
|
||||
lastHistoryEventId = redeemed.historyEvent.id
|
||||
@@ -176,6 +192,8 @@ object CashuReceiveCommands {
|
||||
),
|
||||
)
|
||||
0
|
||||
} catch (e: P2PKUnredeemableException) {
|
||||
Output.error("p2pk_locked", "token is P2PK-locked to a key this wallet can't sign for (${e.lockPubKeyHex})")
|
||||
} catch (e: Exception) {
|
||||
Output.error("mint_proofs_spent", describe(e))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user