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 a5bf03eccd..bfd6b40cd6 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 diff --git a/apps/coordinator/src/components/Slices/ConfirmAddress.jsx b/apps/coordinator/src/components/Slices/ConfirmAddress.jsx index 70d02fd0f5..6a2a4e2fb8 100644 --- a/apps/coordinator/src/components/Slices/ConfirmAddress.jsx +++ b/apps/coordinator/src/components/Slices/ConfirmAddress.jsx @@ -143,12 +143,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({ @@ -156,6 +156,7 @@ const ConfirmAddress = ({ slice, network }) => { network, bip32Path: fullBip32Path, multisig, + name: walletConfig.name, walletConfig, policyHmac: ledgerPolicyHmac, }), @@ -189,6 +190,7 @@ const ConfirmAddress = ({ slice, network }) => { network, bip32Path: state.bip32Path, multisig, + name: walletConfig.name, walletConfig, policyHmac: state.ledgerPolicyHmac, }), @@ -207,6 +209,10 @@ const ConfirmAddress = ({ slice, network }) => { } dispatch({ type: "SET_ACTIVE" }); + if (interaction.manual) { + return; + } + try { let confirmed = await interaction.run(); if ( @@ -231,6 +237,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 ( @@ -253,9 +272,7 @@ const ConfirmAddress = ({ slice, network }) => { Trezor Ledger BCUR2 - - Coldcard - + Coldcard Hermit @@ -320,12 +337,25 @@ const ConfirmAddress = ({ slice, network }) => { )} + {isManualConfirmationActive() && ( + + + + )} {(state.interactionMessage !== "" || state.interactionError !== "") && ( + {interactionMessage === "" && ( + + + + )} + + {this.isManualConfirmationActive() && ( + + + + )} + {(interactionMessage !== "" || interactionError !== "") && ( - + + + )} )} @@ -327,13 +362,37 @@ class AddressExpander extends React.Component { }); }; + isManualConfirmationActive = () => { + const { interactionState, interactionMessage, interactionError } = + this.state; + return ( + this.interaction?.manual && + interactionState === ACTIVE && + interactionMessage === "" && + interactionError === "" + ); + }; + + completeManualConfirmation = () => { + this.setState({ + interactionState: ACTIVE, + interactionMessage: "Success", + interactionError: "", + }); + }; + confirmOnDevice = async () => { this.setState({ interactionState: ACTIVE }); const { node } = this.props; const { multisig } = node; + if (this.interaction.manual) { + return; + } + try { const confirmed = await this.interaction.run(); if ( + confirmed && confirmed.address === multisig.address && confirmed.serializedPath === this.interaction.bip32Path ) { @@ -343,11 +402,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 +414,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 +422,7 @@ class AddressExpander extends React.Component { network, bip32Path: `${extendedPublicKeyImporter.bip32Path}${bip32Path.slice(1)}`, multisig, + name: walletName, }); this.setState({ hasInteraction: true }); this.resetInteractionState(); @@ -428,11 +484,13 @@ AddressExpander.propTypes = { totalSigners: PropTypes.number.isRequired, setSpendCheckbox: PropTypes.func, feeRate: PropTypes.string, + walletName: PropTypes.string, }; AddressExpander.defaultProps = { network: Network.TESTNET, setSpendCheckbox: () => {}, + walletName: "", }; function mapStateToProps(state) { @@ -443,6 +501,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..2c76700176 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"; @@ -24,7 +25,7 @@ describe("ColdcardExportPublicKey", () => { describe("constructor", () => { it("fails with invalid network", () => { expect(() => interactionBuilder({ network: "foo" })).toThrow( - /Unknown network/i + /Unknown network/i, ); }); it("unknown chroot unsupported", () => { @@ -38,7 +39,7 @@ describe("ColdcardExportPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.unknown_chroot_error", - }) + }), ).toBe(true); }); it("invalid bip32Path unsupported", () => { @@ -52,7 +53,7 @@ describe("ColdcardExportPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.path_error", - }) + }), ).toBe(true); }); it("hardened after unhardened unsupported", () => { @@ -66,7 +67,7 @@ describe("ColdcardExportPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.no_hardened_relative_path_error", - }) + }), ).toBe(true); }); }); @@ -81,13 +82,13 @@ describe("ColdcardExportPublicKey", () => { }); expect(() => interaction.parse(notJSON)).toThrow(/Unable to parse JSON/i); expect(() => interaction.parse(definitelyNotJSON)).toThrow( - /Not valid JSON/i + /Not valid JSON/i, ); expect(() => interaction.parse({})).toThrow(/Empty JSON file/i); expect(() => interaction.parse({ xpubJSONFile: coldcardFixtures.invalidColdcardXpubJSON, - }) + }), ).toThrow(/Missing required params/i); }); @@ -99,7 +100,7 @@ describe("ColdcardExportPublicKey", () => { const missingXpub = { ...coldcardFixtures.validColdcardXpubJSON }; Reflect.deleteProperty(missingXpub, "p2sh"); expect(() => interaction.parse(missingXpub)).toThrow( - /Missing required params/i + /Missing required params/i, ); }); it("missing bip32path", () => { @@ -110,7 +111,7 @@ describe("ColdcardExportPublicKey", () => { const missingb32 = { ...coldcardFixtures.validColdcardXpubJSON }; Reflect.deleteProperty(missingb32, "p2sh_deriv"); expect(() => interaction.parse(missingb32)).toThrow( - /Missing required params/i + /Missing required params/i, ); }); it("xfp in file and computed xfp don't match", () => { @@ -122,7 +123,7 @@ describe("ColdcardExportPublicKey", () => { //set to a valid depth>1 xpub reallyMissingXFP.xfp = "12341234"; expect(() => interaction.parse(reallyMissingXFP)).toThrow( - /Computed fingerprint does not match/i + /Computed fingerprint does not match/i, ); }); it("missing xfp but passes bc depth is 1", () => { @@ -176,7 +177,7 @@ describe("ColdcardExportPublicKey", () => { }); expect(interaction.isSupported()).toEqual(true); const result = interaction.parse( - coldcardFixtures.validColdcardXpubNewFirmwareJSON + coldcardFixtures.validColdcardXpubNewFirmwareJSON, ); expect(result).toEqual({ rootFingerprint: ROOT_FINGERPRINT, @@ -192,7 +193,7 @@ describe("ColdcardExportPublicKey", () => { }); expect(interaction.isSupported()).toEqual(true); const result = interaction.parse( - coldcardFixtures.validColdcardXpubMainnetJSON + coldcardFixtures.validColdcardXpubMainnetJSON, ); expect(result).toEqual({ rootFingerprint: ROOT_FINGERPRINT, @@ -234,7 +235,7 @@ describe("ColdcardExportPublicKey", () => { level: INFO, code: "coldcard.upload_key", text: "Upload the JSON file", - }) + }), ).toBe(true); }); it("has a message about selecting 0 for account ", () => { @@ -247,7 +248,7 @@ describe("ColdcardExportPublicKey", () => { level: INFO, code: "coldcard.select_account", text: "Enter 0 for account", - }) + }), ).toBe(true); }); it("has a message about exporting xpub", () => { @@ -260,7 +261,7 @@ describe("ColdcardExportPublicKey", () => { level: INFO, code: "coldcard.export_xpub", text: "Settings > Multisig Wallets > Export XPUB", - }) + }), ).toBe(true); }); }); @@ -276,7 +277,7 @@ describe("ColdcardExportExtendedPublicKey", () => { describe("constructor", () => { it("fails with invalid network", () => { expect(() => interactionBuilder({ network: "foob" })).toThrow( - /Unknown network/i + /Unknown network/i, ); }); @@ -291,7 +292,7 @@ describe("ColdcardExportExtendedPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.unknown_chroot_error", - }) + }), ).toBe(true); }); it("invalid bip32Path unsupported", () => { @@ -305,7 +306,7 @@ describe("ColdcardExportExtendedPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.path_error", - }) + }), ).toBe(true); }); it("hardened after unhardened unsupported", () => { @@ -319,7 +320,7 @@ describe("ColdcardExportExtendedPublicKey", () => { state: PENDING, level: ERROR, code: "coldcard.bip32_path.no_hardened_relative_path_error", - }) + }), ).toBe(true); }); }); @@ -334,7 +335,7 @@ describe("ColdcardExportExtendedPublicKey", () => { }); expect(() => interaction.parse(notJSON)).toThrow(/Unable to parse JSON/i); expect(() => interaction.parse(definitelyNotJSON)).toThrow( - /Not valid JSON/i + /Not valid JSON/i, ); expect(() => interaction.parse({})).toThrow(/Empty JSON file/i); }); @@ -347,7 +348,7 @@ describe("ColdcardExportExtendedPublicKey", () => { const missingXpub = { ...coldcardFixtures.validColdcardXpubJSON }; Reflect.deleteProperty(missingXpub, "p2sh"); expect(() => interaction.parse(missingXpub)).toThrow( - /Missing required params/i + /Missing required params/i, ); }); it("missing bip32path", () => { @@ -358,7 +359,7 @@ describe("ColdcardExportExtendedPublicKey", () => { const missingb32 = { ...coldcardFixtures.validColdcardXpubJSON }; Reflect.deleteProperty(missingb32, "p2sh_deriv"); expect(() => interaction.parse(missingb32)).toThrow( - /Missing required params/i + /Missing required params/i, ); }); it("xfp in file and computed xfp don't match", () => { @@ -370,7 +371,7 @@ describe("ColdcardExportExtendedPublicKey", () => { //set to a valid depth>1 xpub reallyMissingXFP.xfp = "12341234"; expect(() => interaction.parse(reallyMissingXFP)).toThrow( - /Computed fingerprint does not match/i + /Computed fingerprint does not match/i, ); }); it("missing xfp but passes", () => { @@ -452,7 +453,7 @@ describe("ColdcardExportExtendedPublicKey", () => { level: INFO, code: "coldcard.upload_key", text: "Upload the JSON file", - }) + }), ).toBe(true); }); it("has a message about selecting 0 for account ", () => { @@ -465,7 +466,7 @@ describe("ColdcardExportExtendedPublicKey", () => { level: INFO, code: "coldcard.select_account", text: "Enter 0 for account", - }) + }), ).toBe(true); }); it("has a message about exporting xpub", () => { @@ -478,7 +479,7 @@ describe("ColdcardExportExtendedPublicKey", () => { level: INFO, code: "coldcard.export_xpub", text: "Settings > Multisig Wallets > Export XPUB", - }) + }), ).toBe(true); }); }); @@ -547,7 +548,7 @@ describe("ColdcardSignMultisigTransaction", () => { it("psbt has no signatures", () => { const interaction = interactionBuilder({ psbt: multisigs[0].psbt }); expect(() => interaction.parse(multisigs[0].psbt)).toThrow( - /No signatures found/i + /No signatures found/i, ); }); }); @@ -562,7 +563,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.install_multisig_config", text: "has the multisig wallet installed", - }) + }), ).toBe(true); }); it("has a message about downloading psbt", () => { @@ -575,7 +576,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.download_psbt", text: "Download and save this PSBT", - }) + }), ).toBe(true); }); it("has a message about transferring psbt", () => { @@ -588,7 +589,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.transfer_psbt", text: "Transfer the PSBT", - }) + }), ).toBe(true); }); it("has a message about transferring psbt", () => { @@ -601,7 +602,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.transfer_psbt", text: "Transfer the PSBT", - }) + }), ).toBe(true); }); it("has a message about ready to sign", () => { @@ -614,7 +615,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.select_psbt", text: "Choose 'Ready To Sign'", - }) + }), ).toBe(true); }); it("has a message about verify tx", () => { @@ -627,7 +628,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.sign_psbt", text: "Verify the transaction", - }) + }), ).toBe(true); }); it("has a message about upload PSBT", () => { @@ -640,7 +641,7 @@ describe("ColdcardSignMultisigTransaction", () => { level: INFO, code: "coldcard.upload_signed_psbt", text: "Upload the signed PSBT", - }) + }), ).toBe(true); }); }); @@ -651,7 +652,7 @@ describe("ColdcardMultisigWalletConfig", () => { beforeEach(() => { // runs before each test in this block jsonConfigCopy = JSON.parse( - JSON.stringify(coldcardFixtures.jsonConfigUUID) + JSON.stringify(coldcardFixtures.jsonConfigUUID), ); }); @@ -680,16 +681,16 @@ describe("ColdcardMultisigWalletConfig", () => { const definitelyNotJSON = 77; const jsonConfigBad = { test: 0 }; expect(() => interactionBuilder({ jsonConfig: notJSON })).toThrow( - /Unable to parse JSON/i + /Unable to parse JSON/i, ); expect(() => interactionBuilder({ jsonConfig: definitelyNotJSON })).toThrow( - /Not valid JSON/i + /Not valid JSON/i, ); expect(() => interactionBuilder({ jsonConfig: {} })).toThrow( - /Configuration file needs/i + /Configuration file needs/i, ); expect(() => interactionBuilder({ jsonConfig: jsonConfigBad })).toThrow( - /Configuration file needs/i + /Configuration file needs/i, ); }); @@ -697,7 +698,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingKeys = { ...jsonConfigCopy }; Reflect.deleteProperty(jsonMissingKeys, "extendedPublicKeys"); expect(() => interactionBuilder({ jsonConfig: jsonMissingKeys })).toThrow( - "Configuration file needs extendedPublicKeys." + "Configuration file needs extendedPublicKeys.", ); }); @@ -705,7 +706,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingXFP = { ...jsonConfigCopy }; Reflect.deleteProperty(jsonMissingXFP.extendedPublicKeys[0], "xfp"); expect(() => interactionBuilder({ jsonConfig: jsonMissingXFP })).toThrow( - "ExtendedPublicKeys missing at least one xfp." + "ExtendedPublicKeys missing at least one xfp.", ); }); @@ -713,7 +714,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonUnknownXFP = { ...jsonConfigCopy }; jsonUnknownXFP.extendedPublicKeys[0].xfp = "Unknown"; expect(() => interactionBuilder({ jsonConfig: jsonUnknownXFP })).toThrow( - "ExtendedPublicKeys missing at least one xfp." + "ExtendedPublicKeys missing at least one xfp.", ); }); @@ -721,7 +722,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingMultipleXFP = { ...jsonConfigCopy }; jsonMissingMultipleXFP.extendedPublicKeys[1].xfp = "1234"; expect(() => - interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }) + interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }), ).toThrow("XFP not length 8"); }); @@ -729,7 +730,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingMultipleXFP = { ...jsonConfigCopy }; jsonMissingMultipleXFP.extendedPublicKeys[0].xfp = 1234; expect(() => - interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }) + interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }), ).toThrow("XFP not a string"); }); @@ -737,7 +738,7 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingMultipleXFP = { ...jsonConfigCopy }; jsonMissingMultipleXFP.extendedPublicKeys[0].xfp = "1234567z"; expect(() => - interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }) + interactionBuilder({ jsonConfig: jsonMissingMultipleXFP }), ).toThrow("XFP is invalid hex"); }); @@ -746,7 +747,7 @@ describe("ColdcardMultisigWalletConfig", () => { Reflect.deleteProperty(jsonMissingUUIDandName, "uuid"); Reflect.deleteProperty(jsonMissingUUIDandName, "name"); expect(() => - interactionBuilder({ jsonConfig: jsonMissingUUIDandName }) + interactionBuilder({ jsonConfig: jsonMissingUUIDandName }), ).toThrow("Configuration file needs a UUID or a name."); }); @@ -754,9 +755,9 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingQuorumRequired = { ...jsonConfigCopy }; Reflect.deleteProperty(jsonMissingQuorumRequired.quorum, "requiredSigners"); expect(() => - interactionBuilder({ jsonConfig: jsonMissingQuorumRequired }) + interactionBuilder({ jsonConfig: jsonMissingQuorumRequired }), ).toThrow( - "Configuration file needs quorum.requiredSigners and quorum.totalSigners." + "Configuration file needs quorum.requiredSigners and quorum.totalSigners.", ); }); @@ -764,9 +765,9 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingQuorumTotal = { ...jsonConfigCopy }; Reflect.deleteProperty(jsonMissingQuorumTotal.quorum, "totalSigners"); expect(() => - interactionBuilder({ jsonConfig: jsonMissingQuorumTotal }) + interactionBuilder({ jsonConfig: jsonMissingQuorumTotal }), ).toThrow( - "Configuration file needs quorum.requiredSigners and quorum.totalSigners." + "Configuration file needs quorum.requiredSigners and quorum.totalSigners.", ); }); @@ -774,7 +775,52 @@ describe("ColdcardMultisigWalletConfig", () => { const jsonMissingAddressType = { ...jsonConfigCopy }; Reflect.deleteProperty(jsonMissingAddressType, "addressType"); expect(() => - interactionBuilder({ jsonConfig: jsonMissingAddressType }) + interactionBuilder({ jsonConfig: jsonMissingAddressType }), ).toThrow("Configuration file needs addressType."); }); }); + +describe("ColdcardConfirmMultisigAddress", () => { + it("is marked as a manual confirmation interaction", () => { + const interaction = new ColdcardConfirmMultisigAddress({ + network: Network.TESTNET, + bip32Path: "m/45'/1/0/0/0", + multisig: multisigs[0], + }); + + expect(interaction.manual).toBe(true); + }); + + 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..ebd301b340 100644 --- a/packages/caravan-wallets/src/coldcard.ts +++ b/packages/caravan-wallets/src/coldcard.ts @@ -79,7 +79,7 @@ class ColdcardMultisigSettingsFileParser extends ColdcardInteraction { super(); if ( [Network.MAINNET, Network.TESTNET, Network.REGTEST].find( - (net) => net === network + (net) => net === network, ) ) { this.network = network; @@ -238,7 +238,7 @@ class ColdcardMultisigSettingsFileParser extends ColdcardInteraction { (!data.p2sh_p2wsh_deriv || !data.p2sh_p2wsh)) ) { throw new Error( - "Missing required params. Was this file exported from a Coldcard? If you are using firmware version 4.1.0 please upgrade to 4.1.1 or later." + "Missing required params. Was this file exported from a Coldcard? If you are using firmware version 4.1.0 please upgrade to 4.1.1 or later.", ); } @@ -262,7 +262,7 @@ class ColdcardMultisigSettingsFileParser extends ColdcardInteraction { xfpFromWithinXpub !== data.xfp.toLowerCase() ) { throw new Error( - "Computed fingerprint does not match the one in the file." + "Computed fingerprint does not match the one in the file.", ); } @@ -443,7 +443,7 @@ export class ColdcardSignMultisigTransaction extends ColdcardInteraction { } catch (e) { console.error("Error building PSBT", e); throw new Error( - "Unable to build the PSBT from the provided parameters." + "Unable to build the PSBT from the provided parameters.", ); } } @@ -521,7 +521,7 @@ export class ColdcardSignMultisigTransaction extends ColdcardInteraction { const signatures = parseSignaturesFromPSBT(psbtObject); if (!signatures || Object.keys(signatures).length === 0) { throw new Error( - "No signatures found in the PSBT. Did you upload the right one?" + "No signatures found in the PSBT. Did you upload the right one?", ); } return signatures; @@ -595,7 +595,7 @@ export class ColdcardMultisigWalletConfig { this.totalSigners = this.jsonConfig.quorum.totalSigners; } else { throw new Error( - "Configuration file needs quorum.requiredSigners and quorum.totalSigners." + "Configuration file needs quorum.requiredSigners and quorum.totalSigners.", ); } @@ -654,3 +654,62 @@ Format: ${this.addressType} return output; } } + +export class ColdcardConfirmMultisigAddress extends ColdcardInteraction { + manual: boolean; + + network: string; + + bip32Path: string; + + multisig: any; + + name: string; + + constructor({ network, bip32Path, multisig, name }) { + super(); + this.manual = true; + 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 b343c0e2b9..926ee89df5 100644 --- a/packages/caravan-wallets/src/index.ts +++ b/packages/caravan-wallets/src/index.ts @@ -39,6 +39,7 @@ import { ColdcardExportExtendedPublicKey, ColdcardSignMultisigTransaction, ColdcardMultisigWalletConfig, + ColdcardConfirmMultisigAddress, } from "./coldcard"; import { CUSTOM, @@ -604,6 +605,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";