Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/coldcard-address-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"caravan-coordinator": patch
"@caravan/wallets": patch
---

Add Coldcard manual multisig address verification support.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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
Expand Down
42 changes: 36 additions & 6 deletions apps/coordinator/src/components/Slices/ConfirmAddress.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,19 +143,20 @@ 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({
keystore: extendedPublicKeyImporter.method,
network,
bip32Path: fullBip32Path,
multisig,
name: walletConfig.name,
walletConfig,
policyHmac: ledgerPolicyHmac,
}),
Expand Down Expand Up @@ -189,6 +190,7 @@ const ConfirmAddress = ({ slice, network }) => {
network,
bip32Path: state.bip32Path,
multisig,
name: walletConfig.name,
walletConfig,
policyHmac: state.ledgerPolicyHmac,
}),
Expand All @@ -207,6 +209,10 @@ const ConfirmAddress = ({ slice, network }) => {
}

dispatch({ type: "SET_ACTIVE" });
if (interaction.manual) {
return;
}

try {
let confirmed = await interaction.run();
if (
Expand All @@ -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 (
<Grid item md={12}>
<ExtendedPublicKeySelector number={0} onChange={handleKeySelected} />
Expand All @@ -253,9 +272,7 @@ const ConfirmAddress = ({ slice, network }) => {
<MenuItem value={TREZOR}>Trezor</MenuItem>
<MenuItem value={LEDGER}>Ledger</MenuItem>
<MenuItem value={BCUR2}>BCUR2</MenuItem>
<MenuItem value={COLDCARD} disabled>
Coldcard
</MenuItem>
<MenuItem value={COLDCARD}>Coldcard</MenuItem>
<MenuItem value={HERMIT} disabled>
Hermit
</MenuItem>
Expand Down Expand Up @@ -320,12 +337,25 @@ const ConfirmAddress = ({ slice, network }) => {
)}
<Button
variant="contained"
color="primary"
size="large"
onClick={confirmOnDevice}
disabled={state.interactionState === ACTIVE || !state.bip32Path}
>
Confirm
Confirm on Device
</Button>
{isManualConfirmationActive() && (
<Box mt={2}>
<Button
variant="contained"
color="primary"
size="large"
onClick={completeManualConfirmation}
>
I Verified This Address
</Button>
</Box>
)}
{(state.interactionMessage !== "" ||
state.interactionError !== "") && (
<Button size="large" onClick={() => dispatch({ type: "RESET" })}>
Expand Down
99 changes: 79 additions & 20 deletions apps/coordinator/src/components/Wallet/AddressExpander.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,16 @@ import {
multisigAddressType,
Network,
} from "@caravan/bitcoin";
import { PENDING, ACTIVE, ConfirmMultisigAddress } from "@caravan/wallets";
import {
JADE,
BITBOX,
TREZOR,
LEDGER,
COLDCARD,
PENDING,
ACTIVE,
ConfirmMultisigAddress,
} from "@caravan/wallets";
import LaunchIcon from "@mui/icons-material/Launch";
import UTXOSet from "../ScriptExplorer/UTXOSet";
import MultisigDetails from "../MultisigDetails";
Expand Down Expand Up @@ -158,7 +167,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
);
};
Expand Down Expand Up @@ -276,7 +290,7 @@ class AddressExpander extends React.Component {
{hasInteraction && (
<>
{this.confirmAddressDescription()}
{interactionMessage !== "" && (
{interactionMessage !== "" && !interactionError && (
<Box mt={2} align="center">
<Typography variant="h5" style={{ color: "green" }}>
<SuccessIcon />
Expand All @@ -300,18 +314,39 @@ class AddressExpander extends React.Component {
})}
/>
)}
<Button
variant="contained"
size="large"
onClick={this.confirmOnDevice}
disabled={interactionState === ACTIVE}
>
Confirm
</Button>
{interactionMessage === "" && (
<Box mt={2}>
<Button
variant="contained"
color="primary"
size="large"
onClick={this.confirmOnDevice}
disabled={interactionState === ACTIVE}
>
Confirm on Device
</Button>
</Box>
)}

{this.isManualConfirmationActive() && (
<Box mt={2}>
<Button
variant="contained"
color="primary"
size="large"
onClick={this.completeManualConfirmation}
>
I Verified This Address
</Button>
</Box>
)}

{(interactionMessage !== "" || interactionError !== "") && (
<Button size="large" onClick={this.resetInteractionState}>
Reset
</Button>
<Box mt={2}>
<Button size="large" onClick={this.resetInteractionState}>
Reset
</Button>
</Box>
)}
</>
)}
Expand All @@ -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
) {
Expand All @@ -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({
Expand All @@ -359,14 +414,15 @@ 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({
keystore: extendedPublicKeyImporter.method,
network,
bip32Path: `${extendedPublicKeyImporter.bip32Path}${bip32Path.slice(1)}`,
multisig,
name: walletName,
});
this.setState({ hasInteraction: true });
this.resetInteractionState();
Expand Down Expand Up @@ -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) {
Expand All @@ -443,6 +501,7 @@ function mapStateToProps(state) {
client: state.client,
extendedPublicKeyImporters: state.quorum.extendedPublicKeyImporters,
transaction: state.spend.transaction,
walletName: state.wallet.common.walletName,
};
}

Expand Down
2 changes: 1 addition & 1 deletion apps/coordinator/src/hooks/utxos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/*
Expand Down
Loading