diff --git a/.eslintrc b/.eslintrc index 7a580420..c1b08fab 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,7 +18,8 @@ "prettier/prettier": "error", "react/jsx-props-no-spreading": "off", "react/jsx-no-bind": "off", - "react/forbid-prop-types": "off" + "react/forbid-prop-types": "off", + "import/prefer-default-export": "off" }, "settings": { "import/core-modules": ["test-utils"] diff --git a/package-lock.json b/package-lock.json index 803af36a..8d851861 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "bitcoinjs-lib": "^5.1.7", "bowser": "^2.6.1", "buffer": "^6.0.3", + "caravan-descriptors": "file:../../caravan-descriptors", "classnames": "^2.2.6", "diff": "^4.0.2", "fs-extra": "^9.0.0", @@ -87,12 +88,33 @@ "prettier": "^2.0.2", "standard-version": "^9.0.0", "vite": "^4.2.3", - "vite-plugin-node-polyfills": "^0.7.0" + "vite-plugin-node-polyfills": "^0.7.0", + "vite-plugin-wasm": "^3.2.2" }, "engines": { "node": ">=16" } }, + "../../caravan-descriptors": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "unchained-bitcoin": "^0.6.0", + "unchained-wallets": "^0.6.0" + }, + "devDependencies": { + "@jest/globals": "^29.7.0", + "@jest/transform": "^29.7.0", + "@types/jest": "^29.5.4", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "eslint": "^8.49.0", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + } + }, "node_modules/@adobe/css-tools": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.2.0.tgz", @@ -5902,6 +5924,10 @@ "node": ">=10.0.0" } }, + "node_modules/caravan-descriptors": { + "resolved": "../../caravan-descriptors", + "link": true + }, "node_modules/cashaddrjs": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.4.4.tgz", @@ -16483,6 +16509,16 @@ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0" } }, + "node_modules/vite-plugin-wasm": { + "version": "3.2.2", + "resolved": "https://artifacts.unchained-capital.us/repository/npm-dev-group/vite-plugin-wasm/-/vite-plugin-wasm-3.2.2.tgz", + "integrity": "sha512-cdbBUNR850AEoMd5nvLmnyeq63CSfoP1ctD/L2vLk/5+wsgAPlAVAzUK5nGKWO/jtehNlrSSHLteN+gFQw7VOA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4" + } + }, "node_modules/vite/node_modules/rollup": { "version": "3.20.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.20.0.tgz", @@ -21180,6 +21216,23 @@ "svg-pathdata": "^6.0.3" } }, + "caravan-descriptors": { + "version": "file:../../caravan-descriptors", + "requires": { + "@jest/globals": "^29.7.0", + "@jest/transform": "^29.7.0", + "@types/jest": "^29.5.4", + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "eslint": "^8.49.0", + "jest": "^29.7.0", + "prettier": "^3.0.3", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2", + "unchained-bitcoin": "^0.6.0", + "unchained-wallets": "^0.6.0" + } + }, "cashaddrjs": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/cashaddrjs/-/cashaddrjs-0.4.4.tgz", @@ -29292,6 +29345,13 @@ "node-stdlib-browser": "^1.2.0" } }, + "vite-plugin-wasm": { + "version": "3.2.2", + "resolved": "https://artifacts.unchained-capital.us/repository/npm-dev-group/vite-plugin-wasm/-/vite-plugin-wasm-3.2.2.tgz", + "integrity": "sha512-cdbBUNR850AEoMd5nvLmnyeq63CSfoP1ctD/L2vLk/5+wsgAPlAVAzUK5nGKWO/jtehNlrSSHLteN+gFQw7VOA==", + "dev": true, + "requires": {} + }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", diff --git a/package.json b/package.json index 03cad0d7..fecd4a30 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "prettier": "^2.0.2", "standard-version": "^9.0.0", "vite": "^4.2.3", - "vite-plugin-node-polyfills": "^0.7.0" + "vite-plugin-node-polyfills": "^0.7.0", + "vite-plugin-wasm": "^3.2.2" }, "scripts": { "build": "npm run check && VITE_GIT_SHA=`git rev-parse --short HEAD` vite build", @@ -100,6 +101,7 @@ "bitcoinjs-lib": "^5.1.7", "bowser": "^2.6.1", "buffer": "^6.0.3", + "caravan-descriptors": "file:../../caravan-descriptors", "classnames": "^2.2.6", "diff": "^4.0.2", "fs-extra": "^9.0.0", diff --git a/src/components/Wallet/DownloadDescriptors.jsx b/src/components/Wallet/DownloadDescriptors.jsx new file mode 100644 index 00000000..2a634fb9 --- /dev/null +++ b/src/components/Wallet/DownloadDescriptors.jsx @@ -0,0 +1,52 @@ +import React, { useEffect, useState } from "react"; + +import { useSelector } from "react-redux"; +import { Button } from "@mui/material"; +import { getMaskedDerivation } from "unchained-bitcoin"; +import { encodeDescriptors } from "caravan-descriptors"; +import { getWalletConfig } from "../../selectors/wallet"; +import { downloadFile } from "../../utils"; + +export const DownloadDescriptors = () => { + const walletConfig = useSelector(getWalletConfig); + const [descriptors, setDescriptors] = useState({ change: "", receive: "" }); + + useEffect(() => { + const loadAsync = async () => { + const multisigConfig = { + requiredSigners: walletConfig.quorum.requiredSigners, + keyOrigins: walletConfig.extendedPublicKeys.map( + ({ xfp, bip32Path, xpub }) => ({ + xfp, + bip32Path: getMaskedDerivation({ xpub, bip32Path }), + xpub, + }) + ), + addressType: walletConfig.addressType, + network: walletConfig.network, + }; + const { change, receive } = await encodeDescriptors(multisigConfig); + setDescriptors({ change, receive }); + }; + loadAsync(); + }, []); + + const handleDownload = () => { + if (descriptors.change) { + downloadFile( + JSON.stringify(descriptors, null, 2), + `${walletConfig.uuid}.txt` + ); + } + }; + + return ( + + ); +}; diff --git a/src/components/Wallet/WalletConfigInteractionButtons.jsx b/src/components/Wallet/WalletConfigInteractionButtons.jsx index 8d38b147..9d9d20ca 100644 --- a/src/components/Wallet/WalletConfigInteractionButtons.jsx +++ b/src/components/Wallet/WalletConfigInteractionButtons.jsx @@ -2,6 +2,7 @@ import React from "react"; import PropTypes from "prop-types"; import { Button, Grid } from "@mui/material"; import { CARAVAN_CONFIG } from "./constants"; +import { DownloadDescriptors } from "./DownloadDescriptors"; const WalletConfigInteractionButtons = ({ onClearFn, onDownloadFn }) => { const handleClearClick = (e) => { @@ -26,6 +27,9 @@ const WalletConfigInteractionButtons = ({ onClearFn, onDownloadFn }) => { Clear Wallet + + + ); }; diff --git a/src/components/Wallet/WalletDescriptorImporter.jsx b/src/components/Wallet/WalletDescriptorImporter.jsx new file mode 100644 index 00000000..c61b7745 --- /dev/null +++ b/src/components/Wallet/WalletDescriptorImporter.jsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from "react"; +import { Button } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; + +import { getWalletFromDescriptor, getChecksum } from "caravan-descriptors"; +import { + setRequiredSigners, + setTotalSigners, +} from "../../actions/transactionActions"; +import { setAddressType } from "../../actions/settingsActions"; +import { + setExtendedPublicKeyImporterBIP32Path, + setExtendedPublicKeyImporterExtendedPublicKey, + setExtendedPublicKeyImporterExtendedPublicKeyRootFingerprint, + setExtendedPublicKeyImporterFinalized, + setExtendedPublicKeyImporterMethod, + setExtendedPublicKeyImporterName, +} from "../../actions/extendedPublicKeyImporterActions"; +import { updateWalletUuidAction } from "../../actions/walletActions"; + +const importWalletDetails = ( + { keyOrigins, requiredSigners, addressType }, + dispatch +) => { + dispatch(setTotalSigners(keyOrigins.length)); + dispatch(setRequiredSigners(requiredSigners)); + dispatch(setAddressType(addressType)); + keyOrigins.forEach(({ xfp, bip32Path, xpub }, index) => { + const number = index + 1; + dispatch(setExtendedPublicKeyImporterName(number, `key_${number}_${xfp}`)); + dispatch(setExtendedPublicKeyImporterMethod(number, "text")); + dispatch(setExtendedPublicKeyImporterBIP32Path(number, bip32Path)); + dispatch( + setExtendedPublicKeyImporterExtendedPublicKeyRootFingerprint(number, xfp) + ); + dispatch(setExtendedPublicKeyImporterExtendedPublicKey(number, xpub)); + dispatch(setExtendedPublicKeyImporterFinalized(number, true)); + }); +}; + +export const WalletDescriptorImporter = () => { + const [walletConfig, setWalletConfig] = useState(); + const network = useSelector((state) => state.quorum.network); + const dispatch = useDispatch(); + useEffect(() => { + if (walletConfig) { + importWalletDetails(walletConfig, dispatch); + } + }, [walletConfig]); + + const handleClick = async () => { + // eslint-disable-next-line no-alert + const descriptor = window.prompt( + `Please enter one of the wallet's descriptors (change or receive). +Make sure any settings that are not in a descriptor are set before submitting.` + ); + + if (descriptor) { + try { + const config = await getWalletFromDescriptor(descriptor, network); + const checksum = await getChecksum(descriptor); + dispatch(updateWalletUuidAction(checksum)); + setWalletConfig(config); + } catch (e) { + // eslint-disable-next-line no-alert + window.alert(e.message); + } + } + }; + + return ( + + ); +}; diff --git a/src/components/Wallet/index.jsx b/src/components/Wallet/index.jsx index 76c8150b..b3cd26b4 100644 --- a/src/components/Wallet/index.jsx +++ b/src/components/Wallet/index.jsx @@ -58,6 +58,7 @@ import { SET_CLIENT_USERNAME, } from "../../actions/clientActions"; import { clientPropTypes, slicePropTypes } from "../../proptypes"; +import { WalletDescriptorImporter } from "./WalletDescriptorImporter"; class CreateWallet extends React.Component { static validateProperties(config, properties, key) { @@ -356,12 +357,7 @@ class CreateWallet extends React.Component { type="file" /> - @@ -544,7 +540,12 @@ class CreateWallet extends React.Component { - {this.renderWalletImporter()} + + {this.renderWalletImporter()} + + + + {this.renderExtendedPublicKeyImporters()} diff --git a/vite.config.js b/vite.config.js index dd80654f..7ff115b7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,11 +1,14 @@ import { defineConfig } from "vite"; +import wasm from "vite-plugin-wasm"; import react from "@vitejs/plugin-react"; import { nodePolyfills } from "vite-plugin-node-polyfills"; // https://vitejs.dev/config/ export default defineConfig({ base: "/caravan/#", + assetsInclude: ["**/*.wasm"], plugins: [ + wasm(), react(), nodePolyfills({ protocolImports: true, @@ -17,4 +20,14 @@ export default defineConfig({ define: { VITE_GIT_SHA: JSON.stringify(process.env.VITE_GIT_SHA), }, + optimizeDeps: { + // needed for local development to support proper handling of wasm + exclude: ["caravan-descriptors"], + }, + server: { + fs: { + // Allow serving files from one level up to the project root + allow: ["../.."], + }, + }, });