diff --git a/.changeset/twelve-eagles-admire.md b/.changeset/twelve-eagles-admire.md
new file mode 100644
index 0000000000..77c7727c52
--- /dev/null
+++ b/.changeset/twelve-eagles-admire.md
@@ -0,0 +1,5 @@
+---
+"caravan-coordinator": minor
+---
+
+Added a backwards compatible message signing functionality for ledger and tenzor wallet using caravan-wallets
diff --git a/apps/coordinator/package.json b/apps/coordinator/package.json
index 30f82e4611..a1ef42c8eb 100644
--- a/apps/coordinator/package.json
+++ b/apps/coordinator/package.json
@@ -114,6 +114,7 @@
"base58check": "^2.0.0",
"bignumber.js": "^9.0.0",
"bip32": "^2.0.4",
+ "bip322-js": "^2.0.0",
"bitcoin-address-validation": "^1.0.2",
"bitcoinjs-lib": "^5.1.7",
"bowser": "^2.6.1",
diff --git a/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx b/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx
index 1c5f4332ae..0662bc87c8 100644
--- a/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx
+++ b/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx
@@ -1,4 +1,4 @@
-import React from "react";
+import React, { useState } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import {
@@ -19,7 +19,7 @@ import {
} from "@mui/material";
import { downloadFile } from "utils";
import { externalLink } from "utils/ExternalLink";
-
+import SignMessageModal from "./SignMessageModal";
// Actions
import {
sortPublicKeyImporters as sortPublicKeyImportersAction,
@@ -39,6 +39,7 @@ const AddressGenerator = ({
requiredSigners,
setMultisigAddress,
}) => {
+ const [showSignMessageModal, setShowSignMessageModal] = useState(false);
const isInConflict = () => {
return Object.values(publicKeyImporters).some(
(importer) => importer.conflict,
@@ -212,6 +213,23 @@ ${redeemScriptLine}${scriptsSpacer}${witnessScriptLine}
Download Address Details
+
+
+
+
+ {showSignMessageModal && (
+ setShowSignMessageModal(false)}
+ />
+ )}
);
}
diff --git a/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx b/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx
new file mode 100644
index 0000000000..39a5ba27a2
--- /dev/null
+++ b/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx
@@ -0,0 +1,173 @@
+import React, { useState } from "react";
+import PropTypes from "prop-types";
+import { SignMessage, LEDGER, TREZOR } from "@caravan/wallets";
+import { verifyMessageSignature } from "utils/verifyMessage";
+import Copyable from "../Copyable";
+import {
+ Modal,
+ Card,
+ Typography,
+ TextField,
+ Box,
+ Button,
+ CircularProgress,
+ MenuItem,
+ Grid,
+} from "@mui/material";
+const SUPPORTED_KEYSTORES = {
+ ledger: LEDGER,
+ trezor: TREZOR,
+};
+
+const SignMessageModal = ({ onClose, multisig, publicKeyImporters }) => {
+ const [keystoreType, setKeystoreType] = useState("");
+ const [selectedKeyIndex, setSelectedKeyIndex] = useState("");
+ const [message, setMessage] = useState(
+ "Sign to prove ownership of this multisig address.",
+ );
+ const [signatureResult, setSignatureResult] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSignerChange = (e) => {
+ const index = e.target.value;
+ setSelectedKeyIndex(index);
+ };
+
+ const selectedImporter = publicKeyImporters?.[selectedKeyIndex] || {};
+ const bip32Path = selectedImporter?.bip32Path || "";
+ const publicKey = selectedImporter?.publicKey || "";
+
+ const handleSign = async () => {
+ setLoading(true);
+ setSignatureResult(null);
+ setError(null);
+
+ try {
+ const keystore = SUPPORTED_KEYSTORES[keystoreType];
+ if (!keystore) throw new Error("Unsupported or missing keystore");
+ if (!bip32Path || !publicKey) throw new Error("No signer selected");
+
+ const interaction = SignMessage({ keystore, bip32Path, message });
+ const result = await interaction.run();
+
+ const verified = verifyMessageSignature(
+ multisig.address,
+ message,
+ result.signature,
+ );
+
+ setSignatureResult({
+ signature: result.signature,
+ verified,
+ });
+ } catch (e) {
+ console.error("Signing failed:", e);
+ setError(e.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ Sign Message
+
+ setKeystoreType(e.target.value)}
+ margin="normal"
+ >
+
+
+
+
+
+ {Object.entries(publicKeyImporters).map(([index, importer]) => (
+
+ ))}
+
+
+
+
+ setMessage(e.target.value)}
+ multiline
+ margin="normal"
+ />
+
+
+
+
+
+ {signatureResult && (
+
+
+ {signatureResult.verified
+ ? "✅ Signature verified!"
+ : "❌ Signature invalid"}
+
+
+
+ Signature (base64):
+
+
+
+
+
+
+ )}
+
+ {error && (
+
+ ❌ Error: {error}
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+SignMessageModal.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ multisig: PropTypes.object,
+ publicKeyImporters: PropTypes.object.isRequired,
+};
+
+export default SignMessageModal;
diff --git a/apps/coordinator/src/utils/verifyMessage.js b/apps/coordinator/src/utils/verifyMessage.js
new file mode 100644
index 0000000000..aef6a1aa40
--- /dev/null
+++ b/apps/coordinator/src/utils/verifyMessage.js
@@ -0,0 +1,10 @@
+import { Verifier } from "bip322-js";
+
+export const verifyMessageSignature = (address, message, signatureBase64) => {
+ try {
+ return Verifier.verifySignature(address, message, signatureBase64, false);
+ } catch (err) {
+ console.log("Signature verification failed:", err);
+ return false;
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index 4b9ea13f71..5c8a2a1cae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -55,6 +55,7 @@
"base58check": "^2.0.0",
"bignumber.js": "^9.0.0",
"bip32": "^2.0.4",
+ "bip322-js": "^2.0.0",
"bitcoin-address-validation": "^1.0.2",
"bitcoinjs-lib": "^5.1.7",
"bowser": "^2.6.1",
@@ -2875,12 +2876,27 @@
}
},
"node_modules/@bitcoinerlab/secp256k1": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz",
- "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.2.0.tgz",
+ "integrity": "sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==",
+ "license": "MIT",
"dependencies": {
- "@noble/hashes": "^1.1.5",
- "@noble/secp256k1": "^1.7.1"
+ "@noble/curves": "^1.7.0"
+ }
+ },
+ "node_modules/@bitcoinerlab/secp256k1/node_modules/@noble/curves": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz",
+ "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "1.7.1"
+ },
+ "engines": {
+ "node": "^14.21.3 || >=16"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
}
},
"node_modules/@caravan/bip32": {
@@ -6075,17 +6091,6 @@
"url": "https://paulmillr.com/funding/"
}
},
- "node_modules/@noble/secp256k1": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz",
- "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==",
- "funding": [
- {
- "type": "individual",
- "url": "https://paulmillr.com/funding/"
- }
- ]
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -9735,6 +9740,69 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz",
"integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ=="
},
+ "node_modules/bip322-js": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/bip322-js/-/bip322-js-2.0.0.tgz",
+ "integrity": "sha512-wyewxyCLl+wudZWiyvA46SaNQL41dVDJ+sx4HvD6zRXScHzAycwuKEMmbvr2qN+P/IIYArF4XVqlyZVnjutELQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@bitcoinerlab/secp256k1": "^1.1.1",
+ "bitcoinjs-lib": "^6.1.5",
+ "bitcoinjs-message": "^2.2.0",
+ "ecpair": "^2.1.0",
+ "elliptic": "^6.5.5",
+ "fast-sha256": "^1.3.0",
+ "secp256k1": "^5.0.0"
+ }
+ },
+ "node_modules/bip322-js/node_modules/base-x": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz",
+ "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==",
+ "license": "MIT"
+ },
+ "node_modules/bip322-js/node_modules/bech32": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
+ "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==",
+ "license": "MIT"
+ },
+ "node_modules/bip322-js/node_modules/bitcoinjs-lib": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz",
+ "integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.2.0",
+ "bech32": "^2.0.0",
+ "bip174": "^2.1.1",
+ "bs58check": "^3.0.1",
+ "typeforce": "^1.11.3",
+ "varuint-bitcoin": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/bip322-js/node_modules/bs58": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz",
+ "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base-x": "^4.0.0"
+ }
+ },
+ "node_modules/bip322-js/node_modules/bs58check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz",
+ "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^1.2.0",
+ "bs58": "^5.0.0"
+ }
+ },
"node_modules/bip66": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
@@ -9861,6 +9929,43 @@
"bs58": "^5.0.0"
}
},
+ "node_modules/bitcoinjs-message": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz",
+ "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==",
+ "license": "MIT",
+ "dependencies": {
+ "bech32": "^1.1.3",
+ "bs58check": "^2.1.2",
+ "buffer-equals": "^1.0.3",
+ "create-hash": "^1.1.2",
+ "secp256k1": "^3.0.1",
+ "varuint-bitcoin": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/bitcoinjs-message/node_modules/secp256k1": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz",
+ "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "bindings": "^1.5.0",
+ "bip66": "^1.1.5",
+ "bn.js": "^4.11.8",
+ "create-hash": "^1.2.0",
+ "drbg.js": "^1.0.1",
+ "elliptic": "^6.5.7",
+ "nan": "^2.14.0",
+ "safe-buffer": "^5.1.2"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/blake-hash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz",
@@ -9940,7 +10045,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
- "dev": true,
"dependencies": {
"buffer-xor": "^1.0.3",
"cipher-base": "^1.0.0",
@@ -10142,6 +10246,15 @@
"ieee754": "^1.2.1"
}
},
+ "node_modules/buffer-equals": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz",
+ "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -10151,8 +10264,7 @@
"node_modules/buffer-xor": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
- "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
- "dev": true
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ=="
},
"node_modules/bufferutil": {
"version": "4.0.8",
@@ -11769,6 +11881,20 @@
"node": ">=4"
}
},
+ "node_modules/drbg.js": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
+ "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==",
+ "license": "MIT",
+ "dependencies": {
+ "browserify-aes": "^1.0.6",
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -13131,7 +13257,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
"integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
- "dev": true,
"dependencies": {
"md5.js": "^1.3.4",
"safe-buffer": "^5.1.1"
@@ -13275,6 +13400,12 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
+ "node_modules/fast-sha256": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
+ "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
+ "license": "Unlicense"
+ },
"node_modules/fastestsmallesttextencoderdecoder": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz",
@@ -21941,6 +22072,27 @@
"object-assign": "^4.1.1"
}
},
+ "node_modules/secp256k1": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz",
+ "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "elliptic": "^6.5.7",
+ "node-addon-api": "^5.0.0",
+ "node-gyp-build": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/secp256k1/node_modules/node-addon-api": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
+ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",