feat(quartz): NIP-BC sign_psbt over NIP-55 external signer (Phase B)

Wires the NIP-55 Android external-signer contract for sign_psbt, so
NostrSignerExternal.signPsbt is real instead of a stub.

- CommandType.SIGN_PSBT ("sign_psbt") — also available in NIP-55 `perms`
  lists, since Permission wraps CommandType directly.
- SignPsbtResult result type.
- SignPsbtQuery (background ContentResolver) + SignPsbtRequest /
  SignPsbtResponse (foreground Intent), modeled on the derive_key
  string-in/string-out shape: the PSBT hex rides the `nostrsigner:` URI,
  the signed (not finalized) PSBT comes back in the `result` field.
- BackgroundRequestHandler.signPsbt / ForegroundRequestHandler.signPsbt.
- NostrSignerExternal.signPsbt now runs the background-then-foreground
  query. Signer apps that predate sign_psbt reply with no `result`, which
  surfaces as CouldNotPerformException — the send dialog shows it as a
  failure ("update your signer").

NIP-46 (NostrSignerRemote) still throws UnsupportedMethodException — the
bunker-side command isn't standardized yet.
This commit is contained in:
Claude
2026-05-14 12:50:47 +00:00
parent bb0f41d320
commit 5f16ebc060
9 changed files with 211 additions and 15 deletions
+18 -6
View File
@@ -150,7 +150,7 @@ Lightning fast path. Two minimal hooks:
| **C** | Receive + display: `OnchainSection` in `WalletScreen` (address + live balance), Esplora sync, `LocalCache.consume(OnchainZapEvent)`, `updateZapTotal` fold-in, kind-list edits in all 7 in-scope filter files | **Shipped** | | **C** | Receive + display: `OnchainSection` in `WalletScreen` (address + live balance), Esplora sync, `LocalCache.consume(OnchainZapEvent)`, `updateZapTotal` fold-in, kind-list edits in all 7 in-scope filter files | **Shipped** |
| **A.2** | Quartz foundation (send side): BIP-174 PSBT codec, BIP-341 sighash, `OnchainZapBuilder`, `signPsbt` on the `NostrSigner` hierarchy. All crypto validated against BIP-341 wallet test vectors (31 tests). | **Shipped** | | **A.2** | Quartz foundation (send side): BIP-174 PSBT codec, BIP-341 sighash, `OnchainZapBuilder`, `signPsbt` on the `NostrSigner` hierarchy. All crypto validated against BIP-341 wallet test vectors (31 tests). | **Shipped** |
| **D** | Send flow: `OnchainZapSender` orchestrator, `Account.sendOnchainZap`, `OnchainZapSendDialog`, "Send" button on the wallet `OnchainSection`. | **Shipped** | | **D** | Send flow: `OnchainZapSender` orchestrator, `Account.sendOnchainZap`, `OnchainZapSendDialog`, "Send" button on the wallet `OnchainSection`. | **Shipped** |
| **B** | NIP-55 `sign_psbt` intent contract + `NostrSignerExternal.signPsbt`. Broken until Amber ships matching support — surface "update your signer" in the UI. | Pending | | **B** | NIP-55 `sign_psbt` Intent + ContentResolver contract, wired through `NostrSignerExternal.signPsbt`. Works once the external signer app (Amber etc.) ships `sign_psbt` support — older signers reply with no `result`, surfaced as a send failure. | **Shipped** |
### Phase A.2 — shipped ### Phase A.2 — shipped
@@ -179,15 +179,27 @@ Lightning fast path. Two minimal hooks:
estimates, comment; runs the send and shows progress + result. estimates, comment; runs the send and shows progress + result.
- `OnchainSection`: "Send" button on the wallet-screen Bitcoin card. - `OnchainSection`: "Send" button on the wallet-screen Bitcoin card.
### Phase B — shipped
- `CommandType.SIGN_PSBT` (`sign_psbt`) — also usable in NIP-55 `perms` lists
via `Permission`, which wraps `CommandType` directly.
- `SignPsbtResult` result type; `SignPsbtQuery` (background ContentResolver),
`SignPsbtRequest` / `SignPsbtResponse` (foreground Intent), mirroring the
`derive_key` string-in/string-out shape — the PSBT hex rides the
`nostrsigner:` URI, the signed PSBT comes back in `result`.
- `BackgroundRequestHandler.signPsbt` / `ForegroundRequestHandler.signPsbt`;
`NostrSignerExternal.signPsbt` now does the real background-then-foreground
query instead of throwing. External signers that predate `sign_psbt` reply
with no `result``CouldNotPerformException`, surfaced by the send dialog.
### What's still pending ### What's still pending
1. **Phase B — NIP-55 `sign_psbt` Intent.** The Android external-signer Intent 1. **Note-zap-menu entry point.** `OnchainZapSendDialog` already accepts
contract for PSBT signing, plus `NostrSignerExternal.signPsbt`. Blocked on
Amber shipping support; until then external-signer accounts hit
`UnsupportedMethodException` (the send dialog surfaces it as a failure).
2. **Note-zap-menu entry point.** `OnchainZapSendDialog` already accepts
`recipientPubKey` + `zappedEvent`; wiring an "Onchain" option into the `recipientPubKey` + `zappedEvent`; wiring an "Onchain" option into the
existing `ZapAmountChoicePopup` is the remaining UI hook for event zaps. existing `ZapAmountChoicePopup` is the remaining UI hook for event zaps.
2. **NIP-46 `sign_psbt`.** `NostrSignerRemote.signPsbt` still throws
`UnsupportedMethodException` — the bunker-side command is not standardized
yet.
## Risks / open questions ## Risks / open questions
@@ -31,6 +31,7 @@ enum class CommandType(
GET_PUBLIC_KEY("get_public_key"), GET_PUBLIC_KEY("get_public_key"),
DECRYPT_ZAP_EVENT("decrypt_zap_event"), DECRYPT_ZAP_EVENT("decrypt_zap_event"),
DERIVE_KEY("derive_key"), DERIVE_KEY("derive_key"),
SIGN_PSBT("sign_psbt"),
; ;
companion object { companion object {
@@ -44,6 +45,7 @@ enum class CommandType(
GET_PUBLIC_KEY.code -> GET_PUBLIC_KEY GET_PUBLIC_KEY.code -> GET_PUBLIC_KEY
DECRYPT_ZAP_EVENT.code -> DECRYPT_ZAP_EVENT DECRYPT_ZAP_EVENT.code -> DECRYPT_ZAP_EVENT
DERIVE_KEY.code -> DERIVE_KEY DERIVE_KEY.code -> DERIVE_KEY
SIGN_PSBT.code -> SIGN_PSBT
else -> null else -> null
} }
} }
@@ -89,3 +89,7 @@ data class ZapEventDecryptionResult(
data class DerivationResult( data class DerivationResult(
val newPrivKey: HexKey, val newPrivKey: HexKey,
) : IResult ) : IResult
data class SignPsbtResult(
val signedPsbtHex: String,
) : IResult
@@ -0,0 +1,59 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries
import android.content.ContentResolver
import androidx.core.net.toUri
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignPsbtResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.getStringByName
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.utils.query
/**
* NIP-BC `sign_psbt` background (ContentResolver) query.
*
* Passes the unsigned/partially-signed PSBT (lowercase hex) to the external
* signer app and expects the updated PSBT back in the `result` column. The
* signer signs each input whose `tapInternalKey` matches the user's pubkey;
* it does NOT finalize the PSBT.
*/
class SignPsbtQuery(
val loggedInUser: HexKey,
val packageName: String,
val contentResolver: ContentResolver,
) {
val uri = "content://$packageName.${CommandType.SIGN_PSBT}".toUri()
fun query(psbtHex: String): SignerResult<SignPsbtResult> =
contentResolver.query(
uri,
arrayOf(psbtHex, loggedInUser),
) { cursor ->
val signedPsbtHex = cursor.getStringByName("result")
if (!signedPsbtHex.isNullOrBlank()) {
SignerResult.RequestAddressed.Successful(SignPsbtResult(signedPsbtHex))
} else {
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
}
}
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests
import android.content.Intent
import androidx.core.net.toUri
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip55AndroidSigner.api.CommandType
/**
* NIP-BC `sign_psbt` foreground Intent request.
*
* Carries the PSBT (lowercase hex) as the `nostrsigner:` URI data so the
* signer app can display the inputs/outputs to the user for confirmation.
*/
class SignPsbtRequest {
companion object {
fun assemble(
psbtHex: String,
loggedInUser: HexKey,
packageName: String,
): Intent {
val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$psbtHex".toUri())
intent.`package` = packageName
intent.putExtra("type", CommandType.SIGN_PSBT.code)
intent.putExtra("current_user", loggedInUser)
return intent
}
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignPsbtResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.results.IntentResult
/**
* Parses the external signer's `sign_psbt` Intent reply. The `result` field
* carries the updated (signed, not finalized) PSBT as lowercase hex.
*/
class SignPsbtResponse {
companion object {
fun assemble(signedPsbtHex: String): IntentResult =
IntentResult(
result = signedPsbtHex,
)
fun parse(intent: IntentResult): SignerResult.RequestAddressed<SignPsbtResult> {
if (intent.rejected == true) {
return SignerResult.RequestAddressed.ManuallyRejected()
}
val signedPsbtHex = intent.result
return if (!signedPsbtHex.isNullOrBlank()) {
SignerResult.RequestAddressed.Successful(SignPsbtResult(signedPsbtHex))
} else {
SignerResult.RequestAddressed.ReceivedButCouldNotPerform()
}
}
}
}
@@ -30,6 +30,7 @@ import com.vitorpamplona.quartz.nip01Core.signers.SignerExceptions
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignPsbtResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
@@ -175,16 +176,23 @@ class NostrSignerExternal(
} }
/** /**
* NIP-BC `sign_psbt` over NIP-55. The Android external-signer Intent * NIP-BC `sign_psbt` over NIP-55. Sends the PSBT (lowercase hex) to the
* contract for PSBT signing is not implemented yet (Phase B); Amber and * external signer app, which signs each input whose `tapInternalKey`
* other signer apps must also ship support before this can work. Until * matches the user's pubkey and returns the updated (not finalized) PSBT.
* then, callers should fall back to other signer kinds or surface an *
* "update your signer" message. * Signer apps that predate `sign_psbt` support reply with no `result`,
* which surfaces here as [SignerExceptions.CouldNotPerformException] —
* callers should treat that as "update your signer".
*/ */
override suspend fun signPsbt(psbtHex: String): String = override suspend fun signPsbt(psbtHex: String): String {
throw SignerExceptions.UnsupportedMethodException( val result = backgroundQuery.signPsbt(psbtHex) ?: foregroundQuery.signPsbt(psbtHex)
"This external signer does not support sign_psbt yet",
) if (result is SignerResult.RequestAddressed.Successful<SignPsbtResult>) {
return result.result.signedPsbtHex
}
throw convertExceptions("Could not sign PSBT", result)
}
// always ready // always ready
override fun hasForegroundSupport() = hasForegroundActivity() override fun hasForegroundSupport() = hasForegroundActivity()
@@ -27,6 +27,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.DecryptionResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.DerivationResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.EncryptionResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignPsbtResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.ZapEventDecryptionResult
@@ -37,6 +38,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04D
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04EncryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip04EncryptQuery
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44DecryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44DecryptQuery
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.Nip44EncryptQuery
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignPsbtQuery
import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery import com.vitorpamplona.quartz.nip55AndroidSigner.api.background.queries.SignQuery
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -53,6 +55,7 @@ class BackgroundRequestHandler(
val nip44Decrypt = Nip44DecryptQuery(loggedInUser, packageName, contentResolver) val nip44Decrypt = Nip44DecryptQuery(loggedInUser, packageName, contentResolver)
val decryptZap = DecryptZapQuery(loggedInUser, packageName, contentResolver) val decryptZap = DecryptZapQuery(loggedInUser, packageName, contentResolver)
val deriveKey = DeriveKeyQuery(loggedInUser, packageName, contentResolver) val deriveKey = DeriveKeyQuery(loggedInUser, packageName, contentResolver)
val signPsbt = SignPsbtQuery(loggedInUser, packageName, contentResolver)
fun login() = login.query() as? SignerResult.RequestAddressed<PubKeyResult> fun login() = login.query() as? SignerResult.RequestAddressed<PubKeyResult>
@@ -81,4 +84,6 @@ class BackgroundRequestHandler(
fun decryptZapEvent(event: LnZapRequestEvent) = decryptZap.query(event) as? SignerResult.RequestAddressed<ZapEventDecryptionResult> fun decryptZapEvent(event: LnZapRequestEvent) = decryptZap.query(event) as? SignerResult.RequestAddressed<ZapEventDecryptionResult>
fun deriveKey(nonce: HexKey) = deriveKey.query(nonce) as? SignerResult.RequestAddressed<DerivationResult> fun deriveKey(nonce: HexKey) = deriveKey.query(nonce) as? SignerResult.RequestAddressed<DerivationResult>
fun signPsbt(psbtHex: String) = signPsbt.query(psbtHex) as? SignerResult.RequestAddressed<SignPsbtResult>
} }
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.reques
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04EncryptRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip04EncryptRequest
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44DecryptRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44DecryptRequest
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44EncryptRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.Nip44EncryptRequest
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignPsbtRequest
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignRequest import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.requests.SignRequest
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DecryptZapResponse
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.DeriveKeyResponse
@@ -36,6 +37,7 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.respon
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip04EncryptResponse
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44DecryptResponse
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.Nip44EncryptResponse
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignPsbtResponse
import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.responses.SignResponse
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
@@ -95,4 +97,10 @@ class ForegroundRequestHandler(
requestIntentBuilder = { DeriveKeyRequest.assemble(nonce, loggedInUser, packageName) }, requestIntentBuilder = { DeriveKeyRequest.assemble(nonce, loggedInUser, packageName) },
parser = DeriveKeyResponse::parse, parser = DeriveKeyResponse::parse,
) )
suspend fun signPsbt(psbtHex: String) =
launcher.launchWaitAndParse(
requestIntentBuilder = { SignPsbtRequest.assemble(psbtHex, loggedInUser, packageName) },
parser = SignPsbtResponse::parse,
)
} }