Description
SignPsbtOptionsLike (packages/core/src/signer/btc/psbt.ts) declares:
autoFinalized?: boolean — documented as "Default is true."
inputsToSign?: InputToSignLike[]
However, the BTC wallet signer implementations handle this inconsistently, so a dApp calling signer.signPsbt(psbtHex) (or with an incomplete options) can get different results depending on which wallet is connected — even though the caller wrote identical code.
Where the inconsistency comes from
-
JoyID (packages/joy-id/src/btc/index.ts) correctly normalizes options via ccc.SignPsbtOptions.from(options) before use, so autoFinalized reliably defaults to true as documented.
-
UniSat (packages/uni-sat/src/signer.ts) and OKX (packages/okx/src/btc/index.ts) pass the raw options argument straight through to the injected wallet provider's signPsbt(psbtHex, options):
async signPsbt(psbtHex: ccc.HexLike, options?: ccc.SignPsbtOptionsLike): Promise<ccc.Hex> {
return ccc.hexFrom(
await this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options),
);
}
When options (or options.autoFinalized) is undefined, the actual default is whatever UniSat's/OKX's own extension implements — not guaranteed to match CCC's documented default of true.
Worse, UniSat's and OKX's real APIs expect the field name toSignInputs, not inputsToSign (see UniSat docs, OKX docs). Since options is forwarded unmodified, a caller who sets options.inputsToSign on these two wallets has it silently ignored — the wallet instead falls back to signing all inputs matching the connected address.
-
Xverse (packages/xverse/src/signer.ts) never sends any autoFinalized/finalize-equivalent field in its RPC request at all:
this.provider.request("signPsbt", {
psbt: psbtBase64,
signInputs,
broadcast: false,
})
So options.autoFinalized is silently dropped for Xverse regardless of what the caller sets.
Impact
A dApp built against the ccc.SignerBtc abstraction that calls signPsbt(psbtHex) (relying on the documented default) or signPsbt(psbtHex, { autoFinalized: false, inputsToSign: [...] }) can get:
- A fully-finalized PSBT from one wallet and a non-finalized one from another (or vice-versa), even though no option was changed.
- Silently-ignored
inputsToSign on UniSat/OKX, causing unintended inputs to be signed.
This breaks the abstraction's promise that identical SignerBtc calls behave consistently across wallets.
Suggested fix
- In every
SignerBtc implementation, normalize the incoming options via ccc.SignPsbtOptions.from(options) first (as JoyID already does), so the documented default (autoFinalized: true) is always honored regardless of wallet.
- For UniSat/OKX, translate the normalized
SignPsbtOptions into the wallet's actual expected shape ({ autoFinalized, toSignInputs }) instead of forwarding the CCC-level object as-is.
- For Xverse, actually forward
autoFinalized (mapped to whatever Xverse's signPsbt RPC method supports) instead of dropping it, or throw/warn if Xverse has no equivalent so callers aren't silently misled.
- Consider adding a shared test (e.g. against mocked providers) asserting that all
SignerBtc implementations apply the same default/behavior for a given SignPsbtOptionsLike.
Relevant files
packages/core/src/signer/btc/psbt.ts
packages/joy-id/src/btc/index.ts (signPsbt)
packages/uni-sat/src/signer.ts (signPsbt)
packages/okx/src/btc/index.ts (signPsbt)
packages/xverse/src/signer.ts (signPsbt, signAndBroadcastPsbt, prepareSignPsbtParams)
Description
SignPsbtOptionsLike(packages/core/src/signer/btc/psbt.ts) declares:autoFinalized?: boolean— documented as "Default is true."inputsToSign?: InputToSignLike[]However, the BTC wallet signer implementations handle this inconsistently, so a dApp calling
signer.signPsbt(psbtHex)(or with an incompleteoptions) can get different results depending on which wallet is connected — even though the caller wrote identical code.Where the inconsistency comes from
JoyID (
packages/joy-id/src/btc/index.ts) correctly normalizes options viaccc.SignPsbtOptions.from(options)before use, soautoFinalizedreliably defaults totrueas documented.UniSat (
packages/uni-sat/src/signer.ts) and OKX (packages/okx/src/btc/index.ts) pass the rawoptionsargument straight through to the injected wallet provider'ssignPsbt(psbtHex, options):When
options(oroptions.autoFinalized) isundefined, the actual default is whatever UniSat's/OKX's own extension implements — not guaranteed to match CCC's documented default oftrue.Worse, UniSat's and OKX's real APIs expect the field name
toSignInputs, notinputsToSign(see UniSat docs, OKX docs). Sinceoptionsis forwarded unmodified, a caller who setsoptions.inputsToSignon these two wallets has it silently ignored — the wallet instead falls back to signing all inputs matching the connected address.Xverse (
packages/xverse/src/signer.ts) never sends anyautoFinalized/finalize-equivalent field in its RPC request at all:So
options.autoFinalizedis silently dropped for Xverse regardless of what the caller sets.Impact
A dApp built against the
ccc.SignerBtcabstraction that callssignPsbt(psbtHex)(relying on the documented default) orsignPsbt(psbtHex, { autoFinalized: false, inputsToSign: [...] })can get:inputsToSignon UniSat/OKX, causing unintended inputs to be signed.This breaks the abstraction's promise that identical
SignerBtccalls behave consistently across wallets.Suggested fix
SignerBtcimplementation, normalize the incoming options viaccc.SignPsbtOptions.from(options)first (as JoyID already does), so the documented default (autoFinalized: true) is always honored regardless of wallet.SignPsbtOptionsinto the wallet's actual expected shape ({ autoFinalized, toSignInputs }) instead of forwarding the CCC-level object as-is.autoFinalized(mapped to whatever Xverse'ssignPsbtRPC method supports) instead of dropping it, or throw/warn if Xverse has no equivalent so callers aren't silently misled.SignerBtcimplementations apply the same default/behavior for a givenSignPsbtOptionsLike.Relevant files
packages/core/src/signer/btc/psbt.tspackages/joy-id/src/btc/index.ts(signPsbt)packages/uni-sat/src/signer.ts(signPsbt)packages/okx/src/btc/index.ts(signPsbt)packages/xverse/src/signer.ts(signPsbt,signAndBroadcastPsbt,prepareSignPsbtParams)