From f3898ef8181c3956db1f1d2dba0897ba4c371f5f Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Tue, 3 Mar 2026 11:45:02 +0530 Subject: [PATCH 1/2] feat: implement Coldcard address verification --- .../Hermit/HermitSignatureImporter.tsx | 10 +-- .../src/components/Wallet/AddressExpander.jsx | 61 +++++++++++++------ apps/coordinator/src/hooks/utxos.ts | 2 +- packages/caravan-wallets/src/coldcard.test.ts | 36 +++++++++++ packages/caravan-wallets/src/coldcard.ts | 59 +++++++++++++++++- packages/caravan-wallets/src/index.ts | 8 +++ packages/caravan-wallets/src/trezor.ts | 1 - 7 files changed, 149 insertions(+), 28 deletions(-) diff --git a/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx b/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx index a5bf03eccd..006fa3d701 100644 --- a/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx +++ b/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx @@ -12,7 +12,7 @@ import { HermitSignMultisigTransaction, } from "@caravan/wallets"; import { Grid, Box, TextField, Button, FormHelperText } from "@mui/material"; -import { Psbt } from "bitcoinjs-lib"; +import { Psbt } from "bitcoinjs-lib-v6"; import HermitReader from "./HermitReader"; import HermitDisplayer from "./HermitDisplayer"; import InteractionMessages from "../InteractionMessages"; @@ -94,7 +94,7 @@ class HermitSignatureImporter extends React.Component< const childNum = Buffer.from(pathData.slice(0, 4)).readUIntLE(0, 4); path += this.childToPath(childNum); - pathData = pathData.subarray(4) as unknown as Buffer; + pathData = Buffer.from(pathData.subarray(4)); } return path; }; @@ -159,7 +159,9 @@ class HermitSignatureImporter extends React.Component< .split("de") .map((p) => [ Buffer.from(p.slice(0, 8), "hex"), - this.parseBinaryPath(Buffer.from(p.slice(8), "hex")), + this.parseBinaryPath( + Buffer.from(p.slice(8), "hex").buffer as ArrayBuffer, + ), ]); // TODO: these need to be fixed with our new types for PSBT inputs and outputs @@ -341,7 +343,7 @@ class HermitSignatureImporter extends React.Component< const bip32Path = event.target.value; validateAndSetBIP32Path( bip32Path, - () => {}, + () => { }, (bip32PathError: any) => { this.setState({ bip32PathError }); }, diff --git a/apps/coordinator/src/components/Wallet/AddressExpander.jsx b/apps/coordinator/src/components/Wallet/AddressExpander.jsx index 404fb371de..01e349e72c 100644 --- a/apps/coordinator/src/components/Wallet/AddressExpander.jsx +++ b/apps/coordinator/src/components/Wallet/AddressExpander.jsx @@ -158,7 +158,12 @@ class AddressExpander extends React.Component { const { extendedPublicKeyImporters } = this.props; return ( Object.values(extendedPublicKeyImporters).filter( - (importer) => importer.method === "trezor", + (importer) => + importer.method === "trezor" || + importer.method === "ledger" || + importer.method === "bitbox" || + importer.method === "jade" || + importer.method === "coldcard", ).length > 0 ); }; @@ -276,7 +281,7 @@ class AddressExpander extends React.Component { {hasInteraction && ( <> {this.confirmAddressDescription()} - {interactionMessage !== "" && ( + {interactionMessage !== "" && !interactionError && ( @@ -300,18 +305,26 @@ class AddressExpander extends React.Component { })} /> )} - + {interactionMessage === "" && ( + + + + )} + {(interactionMessage !== "" || interactionError !== "") && ( - + + + )} )} @@ -333,7 +346,17 @@ class AddressExpander extends React.Component { const { multisig } = node; try { const confirmed = await this.interaction.run(); + if (confirmed && confirmed.manual) { + this.setState({ + interactionState: ACTIVE, + interactionMessage: confirmed.successMessage || "Verify the address on your device.", + interactionError: "", + }); + return; + } + if ( + confirmed && confirmed.address === multisig.address && confirmed.serializedPath === this.interaction.bip32Path ) { @@ -343,11 +366,7 @@ class AddressExpander extends React.Component { interactionError: "", }); } else { - this.setState({ - interactionState: ACTIVE, - interactionError: "An unknown error occurred", - interactionMessage: "", - }); + throw new Error("Address or path does not match."); } } catch (error) { this.setState({ @@ -359,7 +378,7 @@ class AddressExpander extends React.Component { }; keySelected = (event, extendedPublicKeyImporter) => { - const { network, node } = this.props; + const { network, node, walletName } = this.props; const { multisig, bip32Path } = node; this.interaction = ConfirmMultisigAddress({ @@ -367,6 +386,7 @@ class AddressExpander extends React.Component { network, bip32Path: `${extendedPublicKeyImporter.bip32Path}${bip32Path.slice(1)}`, multisig, + name: walletName, }); this.setState({ hasInteraction: true }); this.resetInteractionState(); @@ -432,7 +452,7 @@ AddressExpander.propTypes = { AddressExpander.defaultProps = { network: Network.TESTNET, - setSpendCheckbox: () => {}, + setSpendCheckbox: () => { }, }; function mapStateToProps(state) { @@ -443,6 +463,7 @@ function mapStateToProps(state) { client: state.client, extendedPublicKeyImporters: state.quorum.extendedPublicKeyImporters, transaction: state.spend.transaction, + walletName: state.wallet.common.walletName, }; } diff --git a/apps/coordinator/src/hooks/utxos.ts b/apps/coordinator/src/hooks/utxos.ts index f48b1d5060..3788effae3 100644 --- a/apps/coordinator/src/hooks/utxos.ts +++ b/apps/coordinator/src/hooks/utxos.ts @@ -26,7 +26,7 @@ import { ReconstructedUtxos, matchPsbtInputsToUtxos, } from "utils/uxtoReconstruction"; -import { Psbt } from "bitcoinjs-lib"; +import { Psbt } from "bitcoinjs-lib-v6"; import { getInputIdentifiersFromPsbt } from "utils/psbtUtils"; /* diff --git a/packages/caravan-wallets/src/coldcard.test.ts b/packages/caravan-wallets/src/coldcard.test.ts index 326029a209..c0261ca4a6 100644 --- a/packages/caravan-wallets/src/coldcard.test.ts +++ b/packages/caravan-wallets/src/coldcard.test.ts @@ -5,6 +5,7 @@ import { ColdcardExportExtendedPublicKey, ColdcardSignMultisigTransaction, ColdcardMultisigWalletConfig, + ColdcardConfirmMultisigAddress, } from "./coldcard"; import { coldcardFixtures } from "./fixtures/coldcard.fixtures"; import { INFO, PENDING, ACTIVE, ERROR } from "./interaction"; @@ -778,3 +779,38 @@ describe("ColdcardMultisigWalletConfig", () => { ).toThrow("Configuration file needs addressType."); }); }); + +describe("ColdcardConfirmMultisigAddress", () => { + it("provides correct messages for address confirmation", () => { + const interaction = new ColdcardConfirmMultisigAddress({ + network: Network.TESTNET, + bip32Path: "m/45'/1/0/0/0", + multisig: multisigs[0], + }); + + expect( + interaction.hasMessagesFor({ + state: PENDING, + level: INFO, + code: "coldcard.install_multisig_config", + }) + ).toBe(true); + + expect( + interaction.hasMessagesFor({ + state: ACTIVE, + level: INFO, + code: "coldcard.address_explorer", + }) + ).toBe(true); + + expect( + interaction.hasMessagesFor({ + state: ACTIVE, + level: INFO, + code: "coldcard.verify_address", + text: "m/45'/1/0/0/0", + }) + ).toBe(true); + }); +}); diff --git a/packages/caravan-wallets/src/coldcard.ts b/packages/caravan-wallets/src/coldcard.ts index f15f8ddf75..30dfeab913 100644 --- a/packages/caravan-wallets/src/coldcard.ts +++ b/packages/caravan-wallets/src/coldcard.ts @@ -54,7 +54,7 @@ export const COLDCARD_WALLET_CONFIG_VERSION = "1.0.0"; /** * Base class for interactions with Coldcard */ -export class ColdcardInteraction extends IndirectKeystoreInteraction {} +export class ColdcardInteraction extends IndirectKeystoreInteraction { } /** * Base class for JSON Multisig file-based interactions with Coldcard @@ -252,7 +252,7 @@ class ColdcardMultisigSettingsFileParser extends ColdcardInteraction { let xfpFromWithinXpub = xpubClass.depth === 1 ? xpubClass.parentFingerprint && - fingerprintToFixedLengthHex(xpubClass.parentFingerprint) + fingerprintToFixedLengthHex(xpubClass.parentFingerprint) : null; // Sanity check if you send in a depth one xpub, we should get the same fingerprint @@ -654,3 +654,58 @@ Format: ${this.addressType} return output; } } + +export class ColdcardConfirmMultisigAddress extends ColdcardInteraction { + network: string; + + bip32Path: string; + + multisig: any; + + name: string; + + constructor({ network, bip32Path, multisig, name }) { + super(); + this.network = network; + this.bip32Path = bip32Path; + this.multisig = multisig; + this.name = name; + } + + messages() { + const messages = super.messages(); + + messages.push({ + state: PENDING, + level: INFO, + code: "coldcard.install_multisig_config", + text: "Ensure your Coldcard has the multisig wallet installed.", + }); + + messages.push({ + state: ACTIVE, + level: INFO, + code: "coldcard.address_explorer", + text: `On your Coldcard, go to 'Address Explorer' and select the multisig wallet${this.name ? ` "${this.name}"` : "" + }.`, + }); + + messages.push({ + state: ACTIVE, + level: INFO, + code: "coldcard.verify_address", + text: `Verify the address at path ${this.bip32Path} matches the one shown here.`, + }); + + return messages; + } + + async run(): Promise { + return { + address: this.multisig.address, + serializedPath: this.bip32Path, + manual: true, + successMessage: `Confirm the address on your Coldcard${this.name ? ` "${this.name}"` : ""}.`, + }; + } +} diff --git a/packages/caravan-wallets/src/index.ts b/packages/caravan-wallets/src/index.ts index 094a561d50..c648bb1301 100644 --- a/packages/caravan-wallets/src/index.ts +++ b/packages/caravan-wallets/src/index.ts @@ -37,6 +37,7 @@ import { ColdcardExportExtendedPublicKey, ColdcardSignMultisigTransaction, ColdcardMultisigWalletConfig, + ColdcardConfirmMultisigAddress, } from "./coldcard"; import { CUSTOM, @@ -602,6 +603,13 @@ export function ConfirmMultisigAddress({ walletConfig: _walletConfig, }); } + case COLDCARD: + return new ColdcardConfirmMultisigAddress({ + network, + bip32Path, + multisig, + name, + }); case TREZOR: return new TrezorConfirmMultisigAddress({ network, diff --git a/packages/caravan-wallets/src/trezor.ts b/packages/caravan-wallets/src/trezor.ts index 851fb63047..d7d061ab9c 100644 --- a/packages/caravan-wallets/src/trezor.ts +++ b/packages/caravan-wallets/src/trezor.ts @@ -52,7 +52,6 @@ import TrezorConnectDefault, { Params, BundledParams, TrezorConnect as TrezorConnectType, - TrezorConnect, } from "@trezor/connect-web"; import { BigNumber } from "bignumber.js"; import { ECPair, payments, Payment } from "bitcoinjs-lib"; From fac76491a4fdbeb20c59f6fdb8eeda797189d081 Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Wed, 29 Apr 2026 00:41:16 +0530 Subject: [PATCH 2/2] Address Coldcard verification review feedback --- .changeset/coldcard-address-verification.md | 6 + .../Hermit/HermitSignatureImporter.tsx | 2 +- .../src/components/Slices/ConfirmAddress.jsx | 42 ++++++- .../src/components/Wallet/AddressExpander.jsx | 70 ++++++++--- packages/caravan-wallets/src/coldcard.test.ts | 116 ++++++++++-------- packages/caravan-wallets/src/coldcard.ts | 24 ++-- 6 files changed, 174 insertions(+), 86 deletions(-) create mode 100644 .changeset/coldcard-address-verification.md diff --git a/.changeset/coldcard-address-verification.md b/.changeset/coldcard-address-verification.md new file mode 100644 index 0000000000..5b56964d82 --- /dev/null +++ b/.changeset/coldcard-address-verification.md @@ -0,0 +1,6 @@ +--- +"caravan-coordinator": patch +"@caravan/wallets": patch +--- + +Add Coldcard manual multisig address verification support. diff --git a/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx b/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx index 006fa3d701..bfd6b40cd6 100644 --- a/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx +++ b/apps/coordinator/src/components/Hermit/HermitSignatureImporter.tsx @@ -343,7 +343,7 @@ class HermitSignatureImporter extends React.Component< const bip32Path = event.target.value; validateAndSetBIP32Path( bip32Path, - () => { }, + () => {}, (bip32PathError: any) => { this.setState({ bip32PathError }); }, diff --git a/apps/coordinator/src/components/Slices/ConfirmAddress.jsx b/apps/coordinator/src/components/Slices/ConfirmAddress.jsx index cd46b27f9f..f1256ffa62 100644 --- a/apps/coordinator/src/components/Slices/ConfirmAddress.jsx +++ b/apps/coordinator/src/components/Slices/ConfirmAddress.jsx @@ -139,12 +139,12 @@ const ConfirmAddress = ({ slice, network }) => { value: "", }); } - // FIXME - hardcoded to just show up for trezor if ( extendedPublicKeyImporter.method === JADE || extendedPublicKeyImporter.method === BITBOX || extendedPublicKeyImporter.method === TREZOR || - extendedPublicKeyImporter.method === LEDGER + extendedPublicKeyImporter.method === LEDGER || + extendedPublicKeyImporter.method === COLDCARD ) { setInteraction( ConfirmMultisigAddress({ @@ -152,6 +152,7 @@ const ConfirmAddress = ({ slice, network }) => { network, bip32Path: fullBip32Path, multisig, + name: walletConfig.name, walletConfig, policyHmac: ledgerPolicyHmac, }), @@ -185,6 +186,7 @@ const ConfirmAddress = ({ slice, network }) => { network, bip32Path: state.bip32Path, multisig, + name: walletConfig.name, walletConfig, policyHmac: state.ledgerPolicyHmac, }), @@ -197,6 +199,10 @@ const ConfirmAddress = ({ slice, network }) => { async function confirmOnDevice() { dispatch({ type: "SET_ACTIVE" }); const { multisig } = slice; + if (interaction.manual) { + return; + } + try { let confirmed = await interaction.run(); if ( @@ -221,6 +227,19 @@ const ConfirmAddress = ({ slice, network }) => { } } + function completeManualConfirmation() { + dispatch({ type: "SET_MESSAGE", value: "Success" }); + } + + function isManualConfirmationActive() { + return ( + interaction?.manual && + state.interactionState === ACTIVE && + state.interactionMessage === "" && + state.interactionError === "" + ); + } + return ( @@ -242,9 +261,7 @@ const ConfirmAddress = ({ slice, network }) => { Jade Trezor Ledger - - Coldcard - + Coldcard Hermit @@ -309,12 +326,25 @@ const ConfirmAddress = ({ slice, network }) => { )} + {isManualConfirmationActive() && ( + + + + )} {(state.interactionMessage !== "" || state.interactionError !== "") && ( + + )} + {(interactionMessage !== "" || interactionError !== "") && (