From 20983a7f3fa3a817061dd3324d6aeb6496df7646 Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 15:45:40 -0500 Subject: [PATCH 01/15] chore: dedicated hook to get descriptors from the wallet config --- .../components/Wallet/DownloadDescriptors.tsx | 29 +++-------------- apps/coordinator/src/hooks/descriptors.ts | 32 +++++++++++++++++++ apps/coordinator/src/hooks/index.ts | 1 + 3 files changed, 37 insertions(+), 25 deletions(-) create mode 100644 apps/coordinator/src/hooks/descriptors.ts create mode 100644 apps/coordinator/src/hooks/index.ts diff --git a/apps/coordinator/src/components/Wallet/DownloadDescriptors.tsx b/apps/coordinator/src/components/Wallet/DownloadDescriptors.tsx index e1f12837..38cf86f5 100644 --- a/apps/coordinator/src/components/Wallet/DownloadDescriptors.tsx +++ b/apps/coordinator/src/components/Wallet/DownloadDescriptors.tsx @@ -1,36 +1,15 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { useSelector } from "react-redux"; import { Button } from "@mui/material"; -import { getMaskedDerivation } from "@caravan/bitcoin"; -import { encodeDescriptors } from "@caravan/descriptors"; + import { getWalletConfig } from "../../selectors/wallet"; import { downloadFile } from "../../utils"; -import { KeyOrigin } from "@caravan/wallets"; +import { useGetDescriptors } from "../../hooks"; 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 }: KeyOrigin) => ({ - xfp, - bip32Path: getMaskedDerivation({ xpub, bip32Path }), - xpub, - }), - ), - addressType: walletConfig.addressType, - network: walletConfig.network, - }; - const { change, receive } = await encodeDescriptors(multisigConfig); - setDescriptors({ change, receive }); - }; - loadAsync(); - }, []); + const descriptors = useGetDescriptors(); const handleDownload = () => { if (descriptors.change) { diff --git a/apps/coordinator/src/hooks/descriptors.ts b/apps/coordinator/src/hooks/descriptors.ts new file mode 100644 index 00000000..30123af9 --- /dev/null +++ b/apps/coordinator/src/hooks/descriptors.ts @@ -0,0 +1,32 @@ +import { encodeDescriptors } from "@caravan/descriptors"; +import { KeyOrigin } from "@caravan/wallets"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { getWalletConfig } from "../selectors/wallet"; +import { getMaskedDerivation } from "@caravan/bitcoin"; + +export function useGetDescriptors() { + 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 }: KeyOrigin) => ({ + xfp, + bip32Path: getMaskedDerivation({ xpub, bip32Path }), + xpub, + }), + ), + addressType: walletConfig.addressType, + network: walletConfig.network, + }; + const { change, receive } = await encodeDescriptors(multisigConfig); + setDescriptors({ change, receive }); + }; + loadAsync(); + }, [walletConfig]); + return descriptors; +} diff --git a/apps/coordinator/src/hooks/index.ts b/apps/coordinator/src/hooks/index.ts new file mode 100644 index 00000000..724c3fbc --- /dev/null +++ b/apps/coordinator/src/hooks/index.ts @@ -0,0 +1 @@ +export { useGetDescriptors } from "./descriptors"; From 5a466f327d64b7551606e33fc7b4ca94e9055bcb Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 22:00:31 -0500 Subject: [PATCH 02/15] typescript-ify redux store --- apps/coordinator/src/reducers/{index.js => index.ts} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename apps/coordinator/src/reducers/{index.js => index.ts} (93%) diff --git a/apps/coordinator/src/reducers/index.js b/apps/coordinator/src/reducers/index.ts similarity index 93% rename from apps/coordinator/src/reducers/index.js rename to apps/coordinator/src/reducers/index.ts index ff58c349..5c206437 100644 --- a/apps/coordinator/src/reducers/index.js +++ b/apps/coordinator/src/reducers/index.ts @@ -44,7 +44,7 @@ const appReducer = combineReducers({ errorNotification: errorNotificationReducer, }); -const rootReducer = (state, action) => { +const rootReducer = (state: any, action: any) => { let newState = state; if (action.type === RESET_WALLET || action.type === "RESET_APP_STATE") newState = undefined; @@ -53,3 +53,4 @@ const rootReducer = (state, action) => { }; export default rootReducer; +export type RootState = ReturnType; From a25d1233152e2a85233e45abfafb97bb26b092ad Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 22:03:45 -0500 Subject: [PATCH 03/15] cleanup blockchain client interaction with store and add hook for retrieval --- apps/coordinator/src/actions/braidActions.js | 4 +- apps/coordinator/src/actions/clientActions.ts | 82 ++++++++++++++----- apps/coordinator/src/actions/walletActions.js | 4 +- .../src/components/ClientPicker/index.jsx | 16 +--- .../components/ScriptExplorer/OutputsForm.jsx | 4 +- .../components/ScriptExplorer/ScriptEntry.jsx | 4 +- .../components/ScriptExplorer/Transaction.jsx | 4 +- .../src/components/Wallet/WalletDeposit.jsx | 4 +- .../src/components/Wallet/WalletGenerator.jsx | 4 +- apps/coordinator/src/hooks/client.ts | 25 ++++++ apps/coordinator/src/hooks/index.ts | 1 + 11 files changed, 107 insertions(+), 45 deletions(-) create mode 100644 apps/coordinator/src/hooks/client.ts diff --git a/apps/coordinator/src/actions/braidActions.js b/apps/coordinator/src/actions/braidActions.js index 88bebb13..0e0b9e9b 100644 --- a/apps/coordinator/src/actions/braidActions.js +++ b/apps/coordinator/src/actions/braidActions.js @@ -3,7 +3,7 @@ import { updateChangeSliceAction, } from "./walletActions"; import { setErrorNotification } from "./errorNotificationActions"; -import { getBlockchainClientFromStore } from "./clientActions"; +import { updateBlockchainClient } from "./clientActions"; export const UPDATE_BRAID_SLICE = "UPDATE_BRAID_SLICE"; @@ -16,7 +16,7 @@ export const UPDATE_BRAID_SLICE = "UPDATE_BRAID_SLICE"; */ export const fetchSliceData = async (slices) => { return async (dispatch) => { - const blockchainClient = await dispatch(getBlockchainClientFromStore()); + const blockchainClient = dispatch(updateBlockchainClient()); if (!blockchainClient) return; try { diff --git a/apps/coordinator/src/actions/clientActions.ts b/apps/coordinator/src/actions/clientActions.ts index b21f154b..34f4a8ae 100644 --- a/apps/coordinator/src/actions/clientActions.ts +++ b/apps/coordinator/src/actions/clientActions.ts @@ -1,5 +1,6 @@ import { Dispatch } from "react"; import { BlockchainClient, ClientType } from "@caravan/clients"; +import { BitcoinNetwork } from "@caravan/bitcoin"; export const SET_CLIENT_TYPE = "SET_CLIENT_TYPE"; export const SET_CLIENT_URL = "SET_CLIENT_URL"; @@ -12,36 +13,79 @@ export const SET_CLIENT_PASSWORD_ERROR = "SET_CLIENT_PASSWORD_ERROR"; export const SET_BLOCKCHAIN_CLIENT = "SET_BLOCKCHAIN_CLIENT"; -export const getBlockchainClientFromStore = () => { - return async ( +interface Client { + type: string; + url: string; + username: string; + password: string; +} +// Ideally we'd just use the hook to get the client +// and do the comparisons. Because the action creators for the +// other pieces of the client store are not implemented and we +// can't hook into those to update the blockchain client, and +// many components that need the client aren't able to use hooks yet +// we have to do this here. +const matchesClient = ( + blockchainClient: BlockchainClient, + client: Client, + network: BitcoinNetwork, +) => { + return ( + blockchainClient && + blockchainClient.network === network && + blockchainClient.type === client.type && + (client.type === "private" + ? blockchainClient.bitcoindParams.url === client.url && + blockchainClient.bitcoindParams.auth.username === client.username && + blockchainClient.bitcoindParams.auth.password === client.password + : true) + ); +}; + +const getClientType = (client: Client): ClientType => { + switch (client.type) { + case "public": + return ClientType.BLOCKSTREAM; + case "private": + return ClientType.PRIVATE; + default: + return client.type as ClientType; + } +}; + +export const updateBlockchainClient = () => { + return ( dispatch: Dispatch, getState: () => { settings: any; client: any }, ) => { const { network } = getState().settings; const { client } = getState(); - if (!client) return; - if (client.blockchainClient?.type === client.type) - return client.blockchainClient; - let clientType: ClientType; - - switch (client.type) { - case "public": - clientType = ClientType.BLOCKSTREAM; - break; - case "private": - clientType = ClientType.PRIVATE; - break; - default: - clientType = client.type; + const { blockchainClient } = client; + + if (matchesClient(blockchainClient, client, network)) { + return blockchainClient; } + return dispatch(setBlockchainClient()); + }; +}; - const blockchainClient = new BlockchainClient({ +export const setBlockchainClient = () => { + return ( + dispatch: Dispatch, + getState: () => { settings: any; client: any }, + ) => { + const { network } = getState().settings; + const { client } = getState(); + + const clientType = getClientType(client); + const newClient = new BlockchainClient({ client, type: clientType, network, throttled: client.type === ClientType.BLOCKSTREAM, }); - dispatch({ type: SET_BLOCKCHAIN_CLIENT, payload: blockchainClient }); - return blockchainClient; + + dispatch({ type: SET_BLOCKCHAIN_CLIENT, value: newClient }); + return newClient; }; }; diff --git a/apps/coordinator/src/actions/walletActions.js b/apps/coordinator/src/actions/walletActions.js index 64f5d21e..426e2fd0 100644 --- a/apps/coordinator/src/actions/walletActions.js +++ b/apps/coordinator/src/actions/walletActions.js @@ -6,7 +6,7 @@ import { import BigNumber from "bignumber.js"; import { isChange } from "../utils/slices"; import { naiveCoinSelection } from "../utils"; -import { getBlockchainClientFromStore } from "./clientActions"; +import { updateBlockchainClient } from "./clientActions"; import { setBalanceError, setChangeOutput, @@ -181,7 +181,7 @@ export function updateTxSlices( change: { nodes: changeSlices }, }, } = getState(); - const client = await dispatch(getBlockchainClientFromStore()); + const client = dispatch(updateBlockchainClient()); // utility function for getting utxo set of an address // and formatting the result in a way we can use const fetchSliceStatus = async (address, bip32Path) => { diff --git a/apps/coordinator/src/components/ClientPicker/index.jsx b/apps/coordinator/src/components/ClientPicker/index.jsx index 3e251937..a8f47404 100644 --- a/apps/coordinator/src/components/ClientPicker/index.jsx +++ b/apps/coordinator/src/components/ClientPicker/index.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { connect, useDispatch } from "react-redux"; +import { connect } from "react-redux"; import { Grid, Card, @@ -25,10 +25,10 @@ import { SET_CLIENT_URL_ERROR, SET_CLIENT_USERNAME_ERROR, SET_CLIENT_PASSWORD_ERROR, - getBlockchainClientFromStore, } from "../../actions/clientActions"; import PrivateClientSettings from "./PrivateClientSettings"; +import { useGetClient } from "../../hooks"; const ClientPicker = ({ setType, @@ -49,9 +49,8 @@ const ClientPicker = ({ const [urlEdited, setUrlEdited] = useState(false); const [connectError, setConnectError] = useState(""); const [connectSuccess, setConnectSuccess] = useState(false); - const dispatch = useDispatch(); - const [blockchainClient, setClient] = useState(); - + const blockchainClient = useGetClient(); + console.log("blockchainClient", blockchainClient); const validatePassword = () => { return ""; }; @@ -66,17 +65,12 @@ const ClientPicker = ({ return ""; }; - const updateBlockchainClient = async () => { - setClient(await dispatch(getBlockchainClientFromStore())); - }; - const handleTypeChange = async (event) => { const clientType = event.target.value; if (clientType === "private" && !urlEdited) { setUrl(`http://localhost:${network === "mainnet" ? 8332 : 18332}`); } setType(clientType); - await updateBlockchainClient(); }; const handleUrlChange = async (event) => { @@ -85,7 +79,6 @@ const ClientPicker = ({ if (!urlEdited && !error) setUrlEdited(true); setUrl(url); setUrlError(error); - await updateBlockchainClient(); }; const handleUsernameChange = (event) => { @@ -100,7 +93,6 @@ const ClientPicker = ({ const error = validatePassword(password); setPassword(password); setPasswordError(error); - await updateBlockchainClient(); }; const testConnection = async () => { diff --git a/apps/coordinator/src/components/ScriptExplorer/OutputsForm.jsx b/apps/coordinator/src/components/ScriptExplorer/OutputsForm.jsx index 406ef8cc..3bcabfb8 100644 --- a/apps/coordinator/src/components/ScriptExplorer/OutputsForm.jsx +++ b/apps/coordinator/src/components/ScriptExplorer/OutputsForm.jsx @@ -25,7 +25,7 @@ import { finalizeOutputs as finalizeOutputsAction, resetOutputs as resetOutputsAction, } from "../../actions/transactionActions"; -import { getBlockchainClientFromStore } from "../../actions/clientActions"; +import { updateBlockchainClient } from "../../actions/clientActions"; import { MIN_SATS_PER_BYTE_FEE } from "../Wallet/constants"; import OutputEntry from "./OutputEntry"; import styles from "./styles.module.scss"; @@ -514,7 +514,7 @@ const mapDispatchToProps = { setFee: setFeeAction, finalizeOutputs: finalizeOutputsAction, resetOutputs: resetOutputsAction, - getBlockchainClient: getBlockchainClientFromStore, + getBlockchainClient: updateBlockchainClient, }; export default connect(mapStateToProps, mapDispatchToProps)(OutputsForm); diff --git a/apps/coordinator/src/components/ScriptExplorer/ScriptEntry.jsx b/apps/coordinator/src/components/ScriptExplorer/ScriptEntry.jsx index cbdc5145..7488081a 100644 --- a/apps/coordinator/src/components/ScriptExplorer/ScriptEntry.jsx +++ b/apps/coordinator/src/components/ScriptExplorer/ScriptEntry.jsx @@ -47,7 +47,7 @@ import { chooseConfirmOwnership as chooseConfirmOwnershipAction, setOwnershipMultisig as setOwnershipMultisigAction, } from "../../actions/ownershipActions"; -import { getBlockchainClientFromStore } from "../../actions/clientActions"; +import { updateBlockchainClient } from "../../actions/clientActions"; class ScriptEntry extends React.Component { constructor(props) { @@ -441,7 +441,7 @@ const mapDispatchToProps = { setFee: setFeeAction, setUnsignedPSBT: setUnsignedPSBTAction, finalizeOutputs: finalizeOutputsAction, - getBlockchainClient: getBlockchainClientFromStore, + getBlockchainClient: updateBlockchainClient, }; export default connect(mapStateToProps, mapDispatchToProps)(ScriptEntry); diff --git a/apps/coordinator/src/components/ScriptExplorer/Transaction.jsx b/apps/coordinator/src/components/ScriptExplorer/Transaction.jsx index 67bc5e7d..5c8fe36d 100644 --- a/apps/coordinator/src/components/ScriptExplorer/Transaction.jsx +++ b/apps/coordinator/src/components/ScriptExplorer/Transaction.jsx @@ -17,7 +17,7 @@ import { } from "@mui/material"; import { OpenInNew } from "@mui/icons-material"; -import { getBlockchainClientFromStore } from "../../actions/clientActions"; +import { updateBlockchainClient } from "../../actions/clientActions"; import Copyable from "../Copyable"; import { externalLink } from "utils/ExternalLink"; import { setTXID } from "../../actions/transactionActions"; @@ -141,7 +141,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { setTxid: setTXID, - getBlockchainClient: getBlockchainClientFromStore, + getBlockchainClient: updateBlockchainClient, }; export default connect(mapStateToProps, mapDispatchToProps)(Transaction); diff --git a/apps/coordinator/src/components/Wallet/WalletDeposit.jsx b/apps/coordinator/src/components/Wallet/WalletDeposit.jsx index 5567cd19..28824eb5 100644 --- a/apps/coordinator/src/components/Wallet/WalletDeposit.jsx +++ b/apps/coordinator/src/components/Wallet/WalletDeposit.jsx @@ -15,7 +15,7 @@ import { } from "@mui/material"; import { enqueueSnackbar } from "notistack"; -import { getBlockchainClientFromStore } from "../../actions/clientActions"; +import { updateBlockchainClient } from "../../actions/clientActions"; import { updateDepositSliceAction, resetWalletView as resetWalletViewAction, @@ -215,7 +215,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { updateDepositSlice: updateDepositSliceAction, resetWalletView: resetWalletViewAction, - getBlockchainClient: getBlockchainClientFromStore, + getBlockchainClient: updateBlockchainClient, }; export default connect(mapStateToProps, mapDispatchToProps)(WalletDeposit); diff --git a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx index 2474e5c5..fb71fb3e 100644 --- a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx +++ b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx @@ -39,7 +39,7 @@ import { setExtendedPublicKeyImporterVisible } from "../../actions/extendedPubli import { setIsWallet as setIsWalletAction } from "../../actions/transactionActions"; import { wrappedActions } from "../../actions/utils"; import { - getBlockchainClientFromStore, + updateBlockchainClient, SET_CLIENT_PASSWORD, SET_CLIENT_PASSWORD_ERROR, } from "../../actions/clientActions"; @@ -553,7 +553,7 @@ const mapDispatchToProps = { setIsWallet: setIsWalletAction, resetWallet: resetWalletAction, resetNodesFetchErrors: resetNodesFetchErrorsAction, - getBlockchainClient: getBlockchainClientFromStore, + getBlockchainClient: updateBlockchainClient, ...wrappedActions({ setPassword: SET_CLIENT_PASSWORD, setPasswordError: SET_CLIENT_PASSWORD_ERROR, diff --git a/apps/coordinator/src/hooks/client.ts b/apps/coordinator/src/hooks/client.ts new file mode 100644 index 00000000..f4204898 --- /dev/null +++ b/apps/coordinator/src/hooks/client.ts @@ -0,0 +1,25 @@ +import { BlockchainClient } from "@caravan/clients"; +import { setBlockchainClient } from "../actions/clientActions"; +import { useDispatch, useSelector } from "react-redux"; +import { RootState } from "reducers"; +import { useEffect } from "react"; + +export const useGetClient = (): BlockchainClient => { + const dispatch = useDispatch(); + const client = useSelector((state: RootState) => state.client); + const network = useSelector((state: RootState) => state.settings.network); + const blockchainClient = useSelector( + (state: RootState) => state.client.blockchainClient, + ); + useEffect(() => { + dispatch(setBlockchainClient()); + }, [ + client.type, + client.url, + client.username, + client.password, + client.walletName, + network, + ]); + return blockchainClient; +}; diff --git a/apps/coordinator/src/hooks/index.ts b/apps/coordinator/src/hooks/index.ts index 724c3fbc..615f4d81 100644 --- a/apps/coordinator/src/hooks/index.ts +++ b/apps/coordinator/src/hooks/index.ts @@ -1 +1,2 @@ +export { useGetClient } from "./client"; export { useGetDescriptors } from "./descriptors"; From db52c9642b1734047230b1a7cc84c37b7feae2e0 Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 22:40:58 -0500 Subject: [PATCH 04/15] cleanup bitcoind wallet interface and add getWalletInfo --- packages/caravan-clients/src/bitcoind.js | 4 +- packages/caravan-clients/src/blockchain.js | 2 +- packages/caravan-clients/src/client.ts | 75 ++++++++++--- packages/caravan-clients/src/index.ts | 3 +- packages/caravan-clients/src/wallet.ts | 124 +++++++++++++++++++++ 5 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 packages/caravan-clients/src/wallet.ts diff --git a/packages/caravan-clients/src/bitcoind.js b/packages/caravan-clients/src/bitcoind.js index ab2a4b66..da3ac103 100644 --- a/packages/caravan-clients/src/bitcoind.js +++ b/packages/caravan-clients/src/bitcoind.js @@ -41,9 +41,9 @@ export function isWalletAddressNotFoundError(e) { } export function bitcoindParams(client) { - const { url, username, password } = client; + const { url, username, password, walletName } = client; const auth = { username, password }; - return { url, auth }; + return { url, auth, walletName }; } /** diff --git a/packages/caravan-clients/src/blockchain.js b/packages/caravan-clients/src/blockchain.js index f4c9e343..9430cda5 100644 --- a/packages/caravan-clients/src/blockchain.js +++ b/packages/caravan-clients/src/blockchain.js @@ -13,7 +13,7 @@ import { bitcoindParams, bitcoindGetAddressStatus, isWalletAddressNotFoundError, -} from "./bitcoind"; +} from "./wallet"; export const BLOCK_EXPLORER = "public"; export const BITCOIND = "private"; diff --git a/packages/caravan-clients/src/client.ts b/packages/caravan-clients/src/client.ts index 82931638..a7baf991 100644 --- a/packages/caravan-clients/src/client.ts +++ b/packages/caravan-clients/src/client.ts @@ -6,13 +6,17 @@ import axios, { Method } from "axios"; import { Network, satoshisToBitcoins, sortInputs } from "@caravan/bitcoin"; import { bitcoindEstimateSmartFee, - bitcoindGetAddressStatus, bitcoindListUnspent, bitcoindParams, bitcoindSendRawTransaction, isWalletAddressNotFoundError, callBitcoind, } from "./bitcoind"; +import { + bitcoindGetAddressStatus, + bitcoindImportDescriptors, + bitcoindWalletInfo, +} from "./wallet"; import BigNumber from "bignumber.js"; export interface UTXO { @@ -74,16 +78,33 @@ export class ClientBase { } } +export interface BitcoindClientConfig { + url: string; + username: string; + password: string; + walletName?: string; +} + +export interface BitcoindParams { + url: string; + auth: { + username: string; + password: string; + }; + walletName?: string; +} + +export interface BlockchainClientParams { + type: ClientType; + network?: Network; + throttled?: boolean; + client?: BitcoindClientConfig; +} + export class BlockchainClient extends ClientBase { public readonly type: ClientType; public readonly network?: Network; - public readonly bitcoindParams: { - url: string; - auth: { - username: string; - password: string; - }; - }; + public readonly bitcoindParams: BitcoindParams; constructor({ type, @@ -93,17 +114,9 @@ export class BlockchainClient extends ClientBase { url: "", username: "", password: "", + walletName: "", }, - }: { - type: ClientType; - network?: Network; - throttled?: boolean; - client?: { - url: string; - username: string; - password: string; - }; - }) { + }: BlockchainClientParams) { // regtest not supported by public explorers if ( type !== ClientType.PRIVATE && @@ -310,4 +323,30 @@ export class BlockchainClient extends ClientBase { throw new Error(`Failed to get transaction: ${error.message}`); } } + + public async importDescriptors({ + receive, + change, + }: { + receive: string; + change: string; + }): Promise { + if (this.type !== ClientType.PRIVATE) { + throw new Error("Only private clients support descriptor importing"); + } + + return await bitcoindImportDescriptors({ + receive, + change, + ...this.bitcoindParams, + }); + } + + public async getWalletInfo() { + if (this.type !== ClientType.PRIVATE) { + throw new Error("Only private clients support wallet info"); + } + + return await bitcoindWalletInfo({ ...this.bitcoindParams }); + } } diff --git a/packages/caravan-clients/src/index.ts b/packages/caravan-clients/src/index.ts index 26f53e62..a1f928fc 100644 --- a/packages/caravan-clients/src/index.ts +++ b/packages/caravan-clients/src/index.ts @@ -1 +1,2 @@ -export { BlockchainClient, ClientType } from './client'; +export { bitcoindImportDescriptors } from "./wallet"; +export { BlockchainClient, ClientType } from "./client"; diff --git a/packages/caravan-clients/src/wallet.ts b/packages/caravan-clients/src/wallet.ts new file mode 100644 index 00000000..c468a230 --- /dev/null +++ b/packages/caravan-clients/src/wallet.ts @@ -0,0 +1,124 @@ +import { isWalletAddressNotFoundError } from "./bitcoind"; +import { callBitcoind } from "./bitcoind"; + +export interface BitcoindWalletParams { + baseUrl: string; + walletName?: string; + auth: { + username: string; + password: string; + }; + method: string; + params?: any[]; +} + +export function callBitcoindWallet({ + baseUrl, + walletName, + auth, + method, + params, +}: BitcoindWalletParams) { + const url = new URL(baseUrl); + + if (walletName) + url.pathname = url.pathname.replace(/\/$/, "") + `/wallet/${walletName}`; + return callBitcoind(url.toString(), auth, method, params); +} + +export interface BaseBitcoindParams { + url: string; + auth: { + username: string; + password: string; + }; + walletName?: string; +} +export function bitcoindWalletInfo({ + url, + auth, + walletName, +}: BaseBitcoindParams) { + return callBitcoindWallet({ + baseUrl: url, + walletName, + auth, + method: "getwalletinfo", + }); +} + +export function bitcoindImportDescriptors({ + url, + auth, + walletName, + receive, + change, +}: { + url: string; + auth: { + username: string; + password: string; + }; + walletName?: string; + receive: string; + change: string; +}) { + const descriptors = [ + { + desc: receive, + internal: false, + }, + { + desc: change, + internal: true, + }, + ].map((d) => { + return { + ...d, + range: [0, 1005], + timestamp: "now", + watchonly: true, + active: true, + }; + }); + + return callBitcoindWallet({ + baseUrl: url, + walletName, + auth, + method: "importdescriptors", + params: [descriptors], + }); +} + +export async function bitcoindGetAddressStatus({ + url, + auth, + walletName, + address, +}: BaseBitcoindParams & { address: string }) { + try { + const resp: any = await callBitcoindWallet({ + baseUrl: url, + walletName, + auth, + method: "getreceivedbyaddress", + params: [address], + }); + if (typeof resp?.result === "undefined") { + throw new Error(`Error: invalid response from ${url}`); + } + return { + used: resp?.result > 0, + }; + } catch (e) { + const error = e as Error; + if (isWalletAddressNotFoundError(error)) + // eslint-disable-next-line no-console + console.warn( + `Address ${address} not found in bitcoind's wallet. Query failed.`, + ); + else console.error(error.message); // eslint-disable-line no-console + return e; + } +} From d3be6a59a48a9c900b84b3d841b0fc94e5940656 Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 22:42:05 -0500 Subject: [PATCH 05/15] add wallet name support in client config --- apps/coordinator/src/actions/clientActions.ts | 15 +- .../ClientPicker/PrivateClientSettings.jsx | 126 --------------- .../ClientPicker/PrivateClientSettings.tsx | 148 ++++++++++++++++++ .../src/components/ClientPicker/index.jsx | 5 +- .../src/components/Wallet/WalletGenerator.jsx | 8 +- .../src/components/Wallet/index.jsx | 5 + .../coordinator/src/reducers/clientReducer.js | 4 + apps/coordinator/src/selectors/wallet.js | 3 +- 8 files changed, 180 insertions(+), 134 deletions(-) delete mode 100644 apps/coordinator/src/components/ClientPicker/PrivateClientSettings.jsx create mode 100644 apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx diff --git a/apps/coordinator/src/actions/clientActions.ts b/apps/coordinator/src/actions/clientActions.ts index 34f4a8ae..93db2ab5 100644 --- a/apps/coordinator/src/actions/clientActions.ts +++ b/apps/coordinator/src/actions/clientActions.ts @@ -13,11 +13,18 @@ export const SET_CLIENT_PASSWORD_ERROR = "SET_CLIENT_PASSWORD_ERROR"; export const SET_BLOCKCHAIN_CLIENT = "SET_BLOCKCHAIN_CLIENT"; -interface Client { +export const SET_CLIENT_WALLET_NAME = "SET_CLIENT_WALLET_NAME"; + +export const setClientWalletName = (walletName: string) => { + return { type: SET_CLIENT_WALLET_NAME, value: walletName }; +}; + +export interface ClientSettings { type: string; url: string; username: string; password: string; + walletName?: string; } // Ideally we'd just use the hook to get the client // and do the comparisons. Because the action creators for the @@ -27,7 +34,7 @@ interface Client { // we have to do this here. const matchesClient = ( blockchainClient: BlockchainClient, - client: Client, + client: ClientSettings, network: BitcoinNetwork, ) => { return ( @@ -42,7 +49,7 @@ const matchesClient = ( ); }; -const getClientType = (client: Client): ClientType => { +const getClientType = (client: ClientSettings): ClientType => { switch (client.type) { case "public": return ClientType.BLOCKSTREAM; @@ -72,7 +79,7 @@ export const updateBlockchainClient = () => { export const setBlockchainClient = () => { return ( dispatch: Dispatch, - getState: () => { settings: any; client: any }, + getState: () => { settings: any; client: ClientSettings }, ) => { const { network } = getState().settings; const { client } = getState(); diff --git a/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.jsx b/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.jsx deleted file mode 100644 index 152174cd..00000000 --- a/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.jsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -// Components -import { Grid, TextField, Button, FormHelperText, Box } from "@mui/material"; - -import { externalLink } from "utils/ExternalLink"; - -const PrivateClientSettings = ({ - handleUrlChange, - handleUsernameChange, - handlePasswordChange, - client, - urlError, - usernameError, - passwordError, - privateNotes, - connectSuccess, - connectError, - testConnection, -}) => ( -
-

- A bitcoind - -compatible client is required to query UTXO data, estimate fees, and - broadcast transactions. -

-

- - { - "Due to CORS requirements, you must use a proxy around the node. Instructions are available " - } - {externalLink( - "https://github.com/caravan-bitcoin/caravan#adding-cors-headers", - "here", - )} - {"."} - -

-
- - - - - - - - - - - - - - - - - - {connectSuccess && ( - Connection Success! - )} - {connectError !== "" && ( - {connectError} - )} - - - -
- {privateNotes} -
-); - -PrivateClientSettings.propTypes = { - client: PropTypes.shape({ - url: PropTypes.string.isRequired, - username: PropTypes.string.isRequired, - password: PropTypes.string.isRequired, - }).isRequired, - handleUrlChange: PropTypes.func.isRequired, - handleUsernameChange: PropTypes.func.isRequired, - handlePasswordChange: PropTypes.func.isRequired, - testConnection: PropTypes.func.isRequired, - urlError: PropTypes.string, - usernameError: PropTypes.string, - passwordError: PropTypes.string, - privateNotes: PropTypes.node, - connectSuccess: PropTypes.bool.isRequired, - connectError: PropTypes.string.isRequired, -}; - -PrivateClientSettings.defaultProps = { - urlError: "", - usernameError: "", - passwordError: "", - privateNotes: React.createElement("span"), -}; - -export default PrivateClientSettings; diff --git a/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx b/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx new file mode 100644 index 00000000..cbfb98bf --- /dev/null +++ b/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx @@ -0,0 +1,148 @@ +import React, { EventHandler } from "react"; +import PropTypes from "prop-types"; + +// Components +import { Grid, TextField, Button, FormHelperText, Box } from "@mui/material"; + +import { externalLink } from "utils/ExternalLink"; +import { + ClientSettings, + setClientWalletName, +} from "../../actions/clientActions"; +import { useDispatch } from "react-redux"; + +export interface PrivateClientSettingsProps { + handleUrlChange: () => void; + handleUsernameChange: () => void; + handlePasswordChange: () => void; + client: ClientSettings; + urlError: string; + usernameError: string; + passwordError: string; + privateNotes: string; + connectSuccess: boolean; + connectError: string; + testConnection: () => void; +} + +const PrivateClientSettings = ({ + handleUrlChange, + handleUsernameChange, + handlePasswordChange, + client, + urlError, + usernameError, + passwordError, + privateNotes, + connectSuccess, + connectError, + testConnection, +}: PrivateClientSettingsProps) => { + const dispatch = useDispatch(); + + const handleWalletNameChange = ( + event: React.ChangeEvent, + ) => { + dispatch(setClientWalletName(event.target.value)); + }; + return ( +
+

+ A bitcoind + -compatible client is required to query UTXO data, estimate fees, and + broadcast transactions. +

+

+ + { + "Due to CORS requirements, you must use a proxy around the node. Instructions are available " + } + {externalLink( + "https://github.com/caravan-bitcoin/caravan#adding-cors-headers", + "here", + )} + {"."} + +

+
+ + + + + + + + + + + + + + + + + + + + + {connectSuccess && ( + Connection Success! + )} + {connectError !== "" && ( + {connectError} + )} + + + +
+ {privateNotes} +
+ ); +}; + +PrivateClientSettings.defaultProps = { + urlError: "", + usernameError: "", + passwordError: "", + privateNotes: React.createElement("span"), +}; + +export default PrivateClientSettings; diff --git a/apps/coordinator/src/components/ClientPicker/index.jsx b/apps/coordinator/src/components/ClientPicker/index.jsx index a8f47404..be31fe09 100644 --- a/apps/coordinator/src/components/ClientPicker/index.jsx +++ b/apps/coordinator/src/components/ClientPicker/index.jsx @@ -99,13 +99,16 @@ const ClientPicker = ({ setConnectError(""); setConnectSuccess(false); try { + if (blockchainClient.bitcoindParams.walletName) { + await blockchainClient.getWalletInfo(); + } await blockchainClient.getFeeEstimate(); if (onSuccess) { onSuccess(); } setConnectSuccess(true); } catch (e) { - setConnectError(e.message); + setConnectError(e.response?.data?.error?.message || e.message); } }; diff --git a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx index fb71fb3e..051db12e 100644 --- a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx +++ b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx @@ -264,7 +264,11 @@ class WalletGenerator extends React.Component { try { const { getBlockchainClient } = this.props; const client = await getBlockchainClient(); - await client.getFeeEstimate(); + if (client.bitcoindParams.walletName) { + await client.getWalletInfo(); + } else { + await client.getFeeEstimate(); + } setPasswordError(""); this.setState({ connectSuccess: true }, () => { // if testConnection was passed a callback @@ -278,7 +282,7 @@ class WalletGenerator extends React.Component { setPasswordError( "Unauthorized: Incorrect username and password combination", ); - else setPasswordError(e.message); + else setPasswordError(e.response?.data?.error?.message || e.message); } }; diff --git a/apps/coordinator/src/components/Wallet/index.jsx b/apps/coordinator/src/components/Wallet/index.jsx index 5800a283..c060ad41 100644 --- a/apps/coordinator/src/components/Wallet/index.jsx +++ b/apps/coordinator/src/components/Wallet/index.jsx @@ -56,6 +56,7 @@ import { SET_CLIENT_TYPE, SET_CLIENT_URL, SET_CLIENT_USERNAME, + setClientWalletName, } from "../../actions/clientActions"; import { clientPropTypes, slicePropTypes } from "../../proptypes"; import { ExtendedPublicKeyImporters } from "./ExtendedPublicKeyImporters"; @@ -269,6 +270,7 @@ class CreateWallet extends React.Component { setClientType, setClientUrl, setClientUsername, + setWalletName, updateWalletPolicyRegistrations, } = this.props; @@ -290,6 +292,7 @@ class CreateWallet extends React.Component { if (walletConfiguration.client.type === "private") { setClientUrl(walletConfiguration.client.url); setClientUsername(walletConfiguration.client.username); + setWalletName(walletConfiguration.client.walletName); } } else { setClientType("unknown"); @@ -586,6 +589,7 @@ CreateWallet.propTypes = { setExtendedPublicKeyImporterVisible: PropTypes.func.isRequired, setClientType: PropTypes.func.isRequired, setClientUrl: PropTypes.func.isRequired, + setWalletName: PropTypes.func.isRequired, setClientUsername: PropTypes.func.isRequired, totalSigners: PropTypes.number.isRequired, updateWalletNameAction: PropTypes.func.isRequired, @@ -650,6 +654,7 @@ const mapDispatchToProps = { setClientUsername: SET_CLIENT_USERNAME, setClientPassword: SET_CLIENT_PASSWORD, }), + setWalletName: setClientWalletName, updateDepositNode: updateDepositSliceAction, updateChangeNode: updateChangeSliceAction, }; diff --git a/apps/coordinator/src/reducers/clientReducer.js b/apps/coordinator/src/reducers/clientReducer.js index 453f0b56..f733a5d9 100644 --- a/apps/coordinator/src/reducers/clientReducer.js +++ b/apps/coordinator/src/reducers/clientReducer.js @@ -8,6 +8,7 @@ import { SET_CLIENT_USERNAME_ERROR, SET_CLIENT_PASSWORD_ERROR, SET_BLOCKCHAIN_CLIENT, + SET_CLIENT_WALLET_NAME, } from "../actions/clientActions"; const initialState = { @@ -16,6 +17,7 @@ const initialState = { username: "", password: "", urlError: "", + walletName: "", usernameError: "", passwordError: "", status: "unknown", @@ -38,6 +40,8 @@ export default (state = initialState, action) => { return updateState(state, { usernameError: action.value }); case SET_CLIENT_PASSWORD_ERROR: return updateState(state, { passwordError: action.value }); + case SET_CLIENT_WALLET_NAME: + return updateState(state, { walletName: action.value }); case SET_BLOCKCHAIN_CLIENT: return updateState(state, { blockchainClient: action.value }); diff --git a/apps/coordinator/src/selectors/wallet.js b/apps/coordinator/src/selectors/wallet.js index 54c1bd35..106ae6f9 100644 --- a/apps/coordinator/src/selectors/wallet.js +++ b/apps/coordinator/src/selectors/wallet.js @@ -24,7 +24,8 @@ const getClientDetails = (state) => { return `{ "type": "private", "url": "${state.client.url}", - "username": "${state.client.username}" + "username": "${state.client.username}", + "walletName": "${state.client.walletName}" }`; } return `{ From 2b38f89742c52f4c8f8bca1f4a3d031ea70e8166 Mon Sep 17 00:00:00 2001 From: buck Date: Wed, 24 Apr 2024 22:46:13 -0500 Subject: [PATCH 06/15] import descriptors --- .../src/components/ImportAddressesButton.tsx | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/coordinator/src/components/ImportAddressesButton.tsx b/apps/coordinator/src/components/ImportAddressesButton.tsx index 0e6ff592..cd7755f0 100644 --- a/apps/coordinator/src/components/ImportAddressesButton.tsx +++ b/apps/coordinator/src/components/ImportAddressesButton.tsx @@ -13,13 +13,11 @@ import { } from "@mui/material"; import { makeStyles } from "@mui/styles"; import InfoIcon from "@mui/icons-material/Info"; -import { - bitcoindImportMulti, - bitcoindGetAddressStatus, - bitcoindParams, -} from "../clients/bitcoind"; +import { bitcoindGetAddressStatus, bitcoindParams } from "../clients/bitcoind"; import { ClientType } from "./types/client"; +import { useGetClient, useGetDescriptors } from "../hooks"; + const useStyles = makeStyles(() => ({ tooltip: { fontSize: ".8rem", @@ -49,6 +47,8 @@ function ImportAddressesButton({ const [rescan, setRescanPreference] = useState(false); const [addressesError, setAddressesError] = useState(""); const [enableImport, setEnableImport] = useState(false); + const descriptors = useGetDescriptors(); + const blockchainClient = useGetClient(); const classes = useStyles(); // when addresses prop has changed, we want to check its status @@ -114,16 +114,12 @@ function ImportAddressesButton({ }`; async function importAddresses() { - const label = ""; // TODO: do we want to allow to set? or set to "caravan"? - try { - // TODO: remove any after converting bitcoind - const response: any = await bitcoindImportMulti({ - ...bitcoindParams(client), - ...{ addresses, label, rescan }, - }); + const response = (await blockchainClient.importDescriptors( + descriptors, + )) as any; - const responseError = response.result.reduce((e: any, c: any) => { + const responseError = response?.result?.reduce((e: any, c: any) => { return (c.error && c.error.message) || e; }, ""); From 909d3e57a947e48059d3bc07c0a7e5ee1f138f0e Mon Sep 17 00:00:00 2001 From: buck Date: Thu, 25 Apr 2024 14:42:22 -0500 Subject: [PATCH 07/15] more wallet enabled bitcoind calls --- packages/caravan-clients/src/bitcoind.js | 3 +- packages/caravan-clients/src/client.ts | 2 +- packages/caravan-clients/src/wallet.ts | 61 +++++++++++++++++++++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/packages/caravan-clients/src/bitcoind.js b/packages/caravan-clients/src/bitcoind.js index da3ac103..37f2beba 100644 --- a/packages/caravan-clients/src/bitcoind.js +++ b/packages/caravan-clients/src/bitcoind.js @@ -2,7 +2,8 @@ import axios from "axios"; import BigNumber from "bignumber.js"; import { bitcoinsToSatoshis } from "@caravan/bitcoin"; -export async function callBitcoind(url, auth, method, params = []) { +export async function callBitcoind(url, auth, method, params) { + if (!params) params = []; // FIXME // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { diff --git a/packages/caravan-clients/src/client.ts b/packages/caravan-clients/src/client.ts index a7baf991..a9f015a4 100644 --- a/packages/caravan-clients/src/client.ts +++ b/packages/caravan-clients/src/client.ts @@ -6,7 +6,6 @@ import axios, { Method } from "axios"; import { Network, satoshisToBitcoins, sortInputs } from "@caravan/bitcoin"; import { bitcoindEstimateSmartFee, - bitcoindListUnspent, bitcoindParams, bitcoindSendRawTransaction, isWalletAddressNotFoundError, @@ -15,6 +14,7 @@ import { import { bitcoindGetAddressStatus, bitcoindImportDescriptors, + bitcoindListUnspent, bitcoindWalletInfo, } from "./wallet"; import BigNumber from "bignumber.js"; diff --git a/packages/caravan-clients/src/wallet.ts b/packages/caravan-clients/src/wallet.ts index c468a230..8ac953e1 100644 --- a/packages/caravan-clients/src/wallet.ts +++ b/packages/caravan-clients/src/wallet.ts @@ -1,5 +1,7 @@ +import { bitcoinsToSatoshis } from "@caravan/bitcoin"; import { isWalletAddressNotFoundError } from "./bitcoind"; import { callBitcoind } from "./bitcoind"; +import BigNumber from "bignumber.js"; export interface BitcoindWalletParams { baseUrl: string; @@ -9,7 +11,7 @@ export interface BitcoindWalletParams { password: string; }; method: string; - params?: any[]; + params?: any[] | Record; } export function callBitcoindWallet({ @@ -122,3 +124,60 @@ export async function bitcoindGetAddressStatus({ return e; } } + +/** + * Fetch unspent outputs for a single or set of addresses + * @param {Object} options - what is needed to communicate with the RPC + * @param {string} options.url - where to connect + * @param {AxiosBasicCredentials} options.auth - username and password + * @param {string} options.address - The address from which to obtain the information + * @returns {UTXO} object for signing transaction inputs + */ +export async function bitcoindListUnspent({ + url, + auth, + walletName, + address, + addresses, +}: BaseBitcoindParams & { address?: string; addresses?: string[] }) { + try { + const addressParam = addresses || [address]; + const resp = await callBitcoindWallet({ + baseUrl: url, + auth, + walletName, + method: "listunspent", + // params: [0, 9999999, addressParam], + params: { minconf: 0, maxconf: 9999999, addresses: addressParam }, + }); + const promises: Promise[] = []; + resp.result.forEach((utxo) => { + promises.push( + callBitcoindWallet({ + baseUrl: url, + walletName: walletName, + auth, + method: "gettransaction", + params: { txid: utxo.txid }, + }), + ); + }); + const previousTransactions = await Promise.all(promises); + return resp.result.map((utxo, mapindex) => { + const amount = new BigNumber(utxo.amount); + return { + confirmed: (utxo.confirmations || 0) > 0, + txid: utxo.txid, + index: utxo.vout, + amount: amount.toFixed(8), + amountSats: bitcoinsToSatoshis(amount.toString()), + transactionHex: previousTransactions[mapindex].result.hex, + time: previousTransactions[mapindex].result.blocktime, + }; + }); + } catch (e) { + // eslint-disable-next-line no-console + console.error("There was a problem:", (e as Error).message); + throw e; + } +} From 1f4593316b99627cbcb936b4b9a673d52f7562a3 Mon Sep 17 00:00:00 2001 From: buck Date: Thu, 25 Apr 2024 14:43:26 -0500 Subject: [PATCH 08/15] update client calls for getting address balances from bitcoind wallet --- apps/coordinator/src/actions/braidActions.js | 2 +- apps/coordinator/src/actions/walletActions.js | 4 ++-- apps/coordinator/src/components/ImportAddressesButton.tsx | 8 +------- .../coordinator/src/components/Wallet/WalletGenerator.jsx | 2 +- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/coordinator/src/actions/braidActions.js b/apps/coordinator/src/actions/braidActions.js index 0e0b9e9b..a8b79d33 100644 --- a/apps/coordinator/src/actions/braidActions.js +++ b/apps/coordinator/src/actions/braidActions.js @@ -27,7 +27,7 @@ export const fetchSliceData = async (slices) => { // creating a tuple of async calls that will need to be resolved // for each slice we're querying for return Promise.all([ - blockchainClient.fetchAddressUTXOs(address), + blockchainClient.fetchAddressUtxos(address), blockchainClient.getAddressStatus(address), ]); }); diff --git a/apps/coordinator/src/actions/walletActions.js b/apps/coordinator/src/actions/walletActions.js index 426e2fd0..9f961d9d 100644 --- a/apps/coordinator/src/actions/walletActions.js +++ b/apps/coordinator/src/actions/walletActions.js @@ -180,12 +180,12 @@ export function updateTxSlices( deposits: { nextNode: nextDepositSlice, nodes: depositSlices }, change: { nodes: changeSlices }, }, + client: { blockchainClient }, } = getState(); - const client = dispatch(updateBlockchainClient()); // utility function for getting utxo set of an address // and formatting the result in a way we can use const fetchSliceStatus = async (address, bip32Path) => { - const utxos = await client.fetchAddressUTXOs(address); + const utxos = await blockchainClient.fetchAddressUtxos(address); return { addressUsed: true, change: isChange(bip32Path), diff --git a/apps/coordinator/src/components/ImportAddressesButton.tsx b/apps/coordinator/src/components/ImportAddressesButton.tsx index cd7755f0..d51ed037 100644 --- a/apps/coordinator/src/components/ImportAddressesButton.tsx +++ b/apps/coordinator/src/components/ImportAddressesButton.tsx @@ -13,7 +13,6 @@ import { } from "@mui/material"; import { makeStyles } from "@mui/styles"; import InfoIcon from "@mui/icons-material/Info"; -import { bitcoindGetAddressStatus, bitcoindParams } from "../clients/bitcoind"; import { ClientType } from "./types/client"; import { useGetClient, useGetDescriptors } from "../hooks"; @@ -64,12 +63,7 @@ function ImportAddressesButton({ const address = addresses[addresses.length - 1]; // TODO: loop, or maybe just check one if (!address) return; try { - // TODO: remove any after converting bitcoind - const status: any = await bitcoindGetAddressStatus({ - // TODO: use this to warn if spent - ...bitcoindParams(client), - address, - }); + const status: any = await blockchainClient.getAddressStatus(address); // if there is a problem querying the address, then enable the button // once enabled, we won't run checkAddress effect anymore if (!status || typeof status.used === "undefined") { diff --git a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx index 051db12e..584afe44 100644 --- a/apps/coordinator/src/components/Wallet/WalletGenerator.jsx +++ b/apps/coordinator/src/components/Wallet/WalletGenerator.jsx @@ -166,7 +166,7 @@ class WalletGenerator extends React.Component { fetchUTXOs = async (isChange, multisig, attemptToKeepGenerating) => { const { getBlockchainClient } = this.props; const { network } = this.props; - const client = await getBlockchainClient(); + const client = getBlockchainClient(); let updates = await client.fetchAddressUtxos(multisig.address); let addressStatus; From b14e9c4770c108bf76d50a20db4650771c5c4d81 Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 13:48:05 -0500 Subject: [PATCH 09/15] fix broken mocks --- packages/caravan-clients/src/client.test.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/caravan-clients/src/client.test.ts b/packages/caravan-clients/src/client.test.ts index b2639faa..b3f6c71c 100644 --- a/packages/caravan-clients/src/client.test.ts +++ b/packages/caravan-clients/src/client.test.ts @@ -1,6 +1,7 @@ import { Network, satoshisToBitcoins } from "@caravan/bitcoin"; import { BlockchainClient, ClientType, ClientBase, UTXO } from "./client"; import * as bitcoind from "./bitcoind"; +import * as wallet from "./wallet"; import BigNumber from "bignumber.js"; import axios from "axios"; @@ -377,7 +378,7 @@ describe("BlockchainClient", () => { { txid: "txid2", vout: 1, amount: 0.2, amountSats: new BigNumber(0.2) }, ]; const mockBitcoindListUnspent = jest - .spyOn(bitcoind, "bitcoindListUnspent") + .spyOn(wallet, "bitcoindListUnspent") .mockResolvedValue(mockUnspent); // Create a new instance of BlockchainClient with ClientType.PRIVATE @@ -408,7 +409,7 @@ describe("BlockchainClient", () => { // Mock the error from bitcoindListUnspent const mockError = new Error("Address not found"); const mockBitcoindListUnspent = jest - .spyOn(bitcoind, "bitcoindListUnspent") + .spyOn(wallet, "bitcoindListUnspent") .mockRejectedValue(mockError); const mockIsWalletAddressNotFoundError = jest @@ -446,7 +447,7 @@ describe("BlockchainClient", () => { .spyOn(bitcoind, "isWalletAddressNotFoundError") .mockReturnValue(false); const mockBitcoindListUnspent = jest - .spyOn(bitcoind, "bitcoindListUnspent") + .spyOn(wallet, "bitcoindListUnspent") .mockRejectedValue(mockError); // Create a new instance of BlockchainClient with ClientType.PRIVATE @@ -559,7 +560,7 @@ describe("BlockchainClient", () => { balance: 500, }; const mockBitcoindGetAddressStatus = jest.spyOn( - bitcoind, + wallet, "bitcoindGetAddressStatus", ); mockBitcoindGetAddressStatus.mockResolvedValue(mockResponse); @@ -587,7 +588,7 @@ describe("BlockchainClient", () => { // Mock the error from the API const mockError = new Error("Failed to fetch address status"); const mockBitcoindGetAddressStatus = jest.spyOn( - bitcoind, + wallet, "bitcoindGetAddressStatus", ); mockBitcoindGetAddressStatus.mockRejectedValue(mockError); From 6ca33af001e2a3c7d0b3c74f62457f1a3bd5ca48 Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 15:45:38 -0500 Subject: [PATCH 10/15] add tests for new client methods --- packages/caravan-clients/src/client.test.ts | 95 +++++++- packages/caravan-clients/src/client.ts | 15 +- packages/caravan-clients/src/wallet.test.ts | 235 ++++++++++++++++++++ packages/caravan-clients/src/wallet.ts | 39 +++- 4 files changed, 373 insertions(+), 11 deletions(-) create mode 100644 packages/caravan-clients/src/wallet.test.ts diff --git a/packages/caravan-clients/src/client.test.ts b/packages/caravan-clients/src/client.test.ts index b3f6c71c..74917c83 100644 --- a/packages/caravan-clients/src/client.test.ts +++ b/packages/caravan-clients/src/client.test.ts @@ -1,5 +1,11 @@ import { Network, satoshisToBitcoins } from "@caravan/bitcoin"; -import { BlockchainClient, ClientType, ClientBase, UTXO } from "./client"; +import { + BlockchainClient, + ClientType, + ClientBase, + UTXO, + BlockchainClientError, +} from "./client"; import * as bitcoind from "./bitcoind"; import * as wallet from "./wallet"; import BigNumber from "bignumber.js"; @@ -374,12 +380,28 @@ describe("BlockchainClient", () => { it("should fetch UTXOs using bitcoindListUnspent (PRIVATE client)", async () => { // Mock the response from bitcoindListUnspent const mockUnspent = [ - { txid: "txid1", vout: 0, amount: 0.1, amountSats: new BigNumber(0.1) }, - { txid: "txid2", vout: 1, amount: 0.2, amountSats: new BigNumber(0.2) }, + { + txid: "txid1", + amount: ".0001", + amountSats: "1000", + index: 1, + confirmed: true, + transactionHex: "hex", + time: "string", + }, + { + txid: "txid1", + amount: ".0001", + amountSats: "1000", + index: 1, + confirmed: true, + transactionHex: "hex", + time: "string", + }, ]; const mockBitcoindListUnspent = jest .spyOn(wallet, "bitcoindListUnspent") - .mockResolvedValue(mockUnspent); + .mockResolvedValue(Promise.resolve(mockUnspent)); // Create a new instance of BlockchainClient with ClientType.PRIVATE const blockchainClient = new BlockchainClient({ @@ -399,7 +421,7 @@ describe("BlockchainClient", () => { // Verify the returned result expect(result.utxos).toEqual(mockUnspent); - expect(result.balanceSats).toEqual(new BigNumber(0.3)); + expect(result.balanceSats).toEqual(new BigNumber(2000)); expect(result.addressKnown).toBe(true); expect(result.fetchedUTXOs).toBe(true); expect(result.fetchUTXOsError).toBe(""); @@ -787,4 +809,67 @@ describe("BlockchainClient", () => { } }); }); + + describe("importDescriptors", () => { + it("should throw BlockchainClientError if not a private client", () => { + const blockchainClient = new BlockchainClient({ + type: ClientType.BLOCKSTREAM, + network: Network.MAINNET, + }); + + expect(() => + blockchainClient.importDescriptors({ + receive: "receive", + change: "change", + }), + ).rejects.toThrow(BlockchainClientError); + }); + + it("calls bitcoindImportDescriptors with descriptors to import", async () => { + const mockImportDescriptors = jest.spyOn( + wallet, + "bitcoindImportDescriptors", + ); + mockImportDescriptors.mockResolvedValue({}); + const blockchainClient = new BlockchainClient({ + type: ClientType.PRIVATE, + network: Network.MAINNET, + }); + + const receive = "receive"; + const change = "change"; + await blockchainClient.importDescriptors({ receive, change }); + expect(mockImportDescriptors).toHaveBeenCalledWith({ + receive, + change, + ...blockchainClient.bitcoindParams, + }); + }); + }); + describe("getWalletInfo", () => { + it("should throw BlockchainClientError if not a private client", () => { + const blockchainClient = new BlockchainClient({ + type: ClientType.BLOCKSTREAM, + network: Network.MAINNET, + }); + + expect(() => blockchainClient.getWalletInfo()).rejects.toThrow( + BlockchainClientError, + ); + }); + + it("calls bitcoindImportDescriptors with descriptors to import", async () => { + const mockImportDescriptors = jest.spyOn(wallet, "bitcoindWalletInfo"); + mockImportDescriptors.mockResolvedValue({}); + const blockchainClient = new BlockchainClient({ + type: ClientType.PRIVATE, + network: Network.MAINNET, + }); + + await blockchainClient.getWalletInfo(); + expect(mockImportDescriptors).toHaveBeenCalledWith({ + ...blockchainClient.bitcoindParams, + }); + }); + }); }); diff --git a/packages/caravan-clients/src/client.ts b/packages/caravan-clients/src/client.ts index a9f015a4..4baa4296 100644 --- a/packages/caravan-clients/src/client.ts +++ b/packages/caravan-clients/src/client.ts @@ -19,6 +19,13 @@ import { } from "./wallet"; import BigNumber from "bignumber.js"; +export class BlockchainClientError extends Error { + constructor(message) { + super(message); + this.name = "BlockchainClientError"; + } +} + export interface UTXO { txid: string; vout: number; @@ -332,7 +339,9 @@ export class BlockchainClient extends ClientBase { change: string; }): Promise { if (this.type !== ClientType.PRIVATE) { - throw new Error("Only private clients support descriptor importing"); + throw new BlockchainClientError( + "Only private clients support descriptor importing", + ); } return await bitcoindImportDescriptors({ @@ -344,7 +353,9 @@ export class BlockchainClient extends ClientBase { public async getWalletInfo() { if (this.type !== ClientType.PRIVATE) { - throw new Error("Only private clients support wallet info"); + throw new BlockchainClientError( + "Only private clients support wallet info", + ); } return await bitcoindWalletInfo({ ...this.bitcoindParams }); diff --git a/packages/caravan-clients/src/wallet.test.ts b/packages/caravan-clients/src/wallet.test.ts new file mode 100644 index 00000000..91c84ba2 --- /dev/null +++ b/packages/caravan-clients/src/wallet.test.ts @@ -0,0 +1,235 @@ +import { + callBitcoindWallet, + bitcoindWalletInfo, + bitcoindImportDescriptors, + bitcoindGetAddressStatus, + bitcoindListUnspent, +} from "./wallet"; + +import * as bitcoind from "./bitcoind"; +import BigNumber from "bignumber.js"; + +describe("Wallet Functions", () => { + const baseUrl = "http://localhost:8332"; + const auth = { + username: "username", + password: "password", + }; + const walletName = "myWallet"; + let mockCallBitcoind; + const expectedWalletPath = `${baseUrl}/wallet/${walletName}`; + + beforeEach(() => { + mockCallBitcoind = jest.fn(); + jest.spyOn(bitcoind, "callBitcoind").mockImplementation(mockCallBitcoind); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("callBitcoindWallet", () => { + it("should add wallet path to url", () => { + callBitcoindWallet({ + baseUrl, + walletName, + auth, + method: "getwalletinfo", + }); + expect(mockCallBitcoind).toHaveBeenCalledWith( + expectedWalletPath, + auth, + "getwalletinfo", + undefined, + ); + }); + + it("should call bitcoind normally if no wallet name", () => { + callBitcoindWallet({ + baseUrl, + auth, + method: "getblockchaininfo", + }); + expect(mockCallBitcoind).toHaveBeenCalledWith( + `${baseUrl}/`, + auth, + "getblockchaininfo", + undefined, + ); + }); + }); + + describe("bitcoindWalletInfo", () => { + it("should call callBitcoindWallet with getwalletinfo", () => { + bitcoindWalletInfo({ url: baseUrl, auth, walletName }); + expect(mockCallBitcoind).toHaveBeenCalledWith( + expectedWalletPath, + auth, + "getwalletinfo", + undefined, + ); + }); + }); + + describe("bitcoindImportDescriptors", () => { + it("should call callBitcoindWallet with importdescriptors", () => { + const receive = "receive"; + const change = "change"; + const descriptorParams = [ + { desc: receive, internal: false }, + { desc: change, internal: true }, + ].map((d) => ({ + ...d, + range: [0, 1005], + timestamp: "now", + watchonly: true, + active: true, + })); + bitcoindImportDescriptors({ + url: baseUrl, + auth, + walletName, + receive, + change, + }); + expect(mockCallBitcoind).toHaveBeenCalledWith( + expectedWalletPath, + auth, + "importdescriptors", + [descriptorParams], + ); + }); + }); + + describe("bitcoindGetAddressStatus", () => { + it("should call callBitcoindWallet with getaddressinfo", async () => { + mockCallBitcoind.mockResolvedValue({ result: 1 }); + const address = "address"; + const result = await bitcoindGetAddressStatus({ + url: baseUrl, + auth, + walletName, + address, + }); + expect(mockCallBitcoind).toHaveBeenCalledWith( + expectedWalletPath, + auth, + "getreceivedbyaddress", + [address], + ); + expect(result).toEqual({ used: true }); + }); + + it("should throw an error if no result", async () => { + const consoleErrorMock = jest.spyOn(console, "error"); + consoleErrorMock.mockImplementation(() => {}); + mockCallBitcoind.mockResolvedValue({ result: undefined }); + const address = "address"; + + const resp: any = await bitcoindGetAddressStatus({ + url: baseUrl, + auth, + walletName, + address, + }); + expect(resp.message).toMatch(`Error: invalid response from ${baseUrl}`); + expect(consoleErrorMock).toHaveBeenCalled(); + }); + }); + + describe("bitcoindListUnspent", () => { + it("should return unspent utxos", async () => { + const address = "address"; + const utxos = [ + { + txid: "txid1", + vout: 0, + address, + amount: 0.1, + confirmations: 1, + spendable: true, + }, + { + txid: "txid2", + vout: 0, + address, + amount: 0.1, + confirmations: 1, + spendable: true, + }, + ]; + + // mock the first call to 'listunspent' + mockCallBitcoind.mockResolvedValueOnce({ + result: utxos, + }); + + const getTransactionResponse = { + txid: "txid", + vout: 1, + amount: 0.1, + hex: "txhex", + blocktime: 123456789, + }; + // mock the second call to 'gettransaction' + mockCallBitcoind.mockResolvedValueOnce({ + result: getTransactionResponse, + }); + + mockCallBitcoind.mockResolvedValueOnce({ + result: getTransactionResponse, + }); + + const response = await bitcoindListUnspent({ + url: baseUrl, + auth, + walletName, + address, + }); + expect(mockCallBitcoind).toHaveBeenNthCalledWith( + 1, + expectedWalletPath, + auth, + "listunspent", + { minconf: 0, maxconf: 9999999, addresses: [address] }, + ); + + expect(mockCallBitcoind).toHaveBeenNthCalledWith( + 2, + expectedWalletPath, + auth, + "gettransaction", + { txid: utxos[0].txid }, + ); + + expect(mockCallBitcoind).toHaveBeenNthCalledWith( + 3, + expectedWalletPath, + auth, + "gettransaction", + { txid: utxos[1].txid }, + ); + + expect(response).toEqual([ + { + txid: utxos[0].txid, + index: utxos[0].vout, + amount: BigNumber("0.1").toFixed(8), + amountSats: "10000000", + confirmed: true, + transactionHex: getTransactionResponse.hex, + time: getTransactionResponse.blocktime, + }, + { + txid: utxos[1].txid, + index: utxos[1].vout, + amount: BigNumber("0.1").toFixed(8), + amountSats: "10000000", + confirmed: true, + transactionHex: getTransactionResponse.hex, + time: getTransactionResponse.blocktime, + }, + ]); + }); + }); +}); diff --git a/packages/caravan-clients/src/wallet.ts b/packages/caravan-clients/src/wallet.ts index 8ac953e1..4064b773 100644 --- a/packages/caravan-clients/src/wallet.ts +++ b/packages/caravan-clients/src/wallet.ts @@ -3,6 +3,13 @@ import { isWalletAddressNotFoundError } from "./bitcoind"; import { callBitcoind } from "./bitcoind"; import BigNumber from "bignumber.js"; +export class BitcoindWalletClientError extends Error { + constructor(message) { + super(message); + this.name = "BitcoindWalletClientError"; + } +} + export interface BitcoindWalletParams { baseUrl: string; walletName?: string; @@ -36,6 +43,7 @@ export interface BaseBitcoindParams { }; walletName?: string; } + export function bitcoindWalletInfo({ url, auth, @@ -108,7 +116,9 @@ export async function bitcoindGetAddressStatus({ params: [address], }); if (typeof resp?.result === "undefined") { - throw new Error(`Error: invalid response from ${url}`); + throw new BitcoindWalletClientError( + `Error: invalid response from ${url}`, + ); } return { used: resp?.result > 0, @@ -125,6 +135,12 @@ export async function bitcoindGetAddressStatus({ } } +export interface ListUnspentResponse { + txid: string; + amount: number; + confirmations: number; + vout: number; +} /** * Fetch unspent outputs for a single or set of addresses * @param {Object} options - what is needed to communicate with the RPC @@ -139,18 +155,33 @@ export async function bitcoindListUnspent({ walletName, address, addresses, -}: BaseBitcoindParams & { address?: string; addresses?: string[] }) { +}: BaseBitcoindParams & { + address?: string; + addresses?: string[]; +}): Promise< + { + txid: string; + amount: string; + amountSats: string; + index: number; + confirmed: boolean; + transactionHex: string; + time: string; + }[] +> { try { const addressParam = addresses || [address]; - const resp = await callBitcoindWallet({ + const resp: { + result: ListUnspentResponse[]; + } = await callBitcoindWallet({ baseUrl: url, auth, walletName, method: "listunspent", - // params: [0, 9999999, addressParam], params: { minconf: 0, maxconf: 9999999, addresses: addressParam }, }); const promises: Promise[] = []; + resp.result.forEach((utxo) => { promises.push( callBitcoindWallet({ From ae767360e72130b829258a30bb1634f73729ff7f Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 15:49:08 -0500 Subject: [PATCH 11/15] cleanup --- apps/coordinator/src/actions/walletActions.js | 1 - .../src/components/ClientPicker/PrivateClientSettings.tsx | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/coordinator/src/actions/walletActions.js b/apps/coordinator/src/actions/walletActions.js index 9f961d9d..f884bb3f 100644 --- a/apps/coordinator/src/actions/walletActions.js +++ b/apps/coordinator/src/actions/walletActions.js @@ -6,7 +6,6 @@ import { import BigNumber from "bignumber.js"; import { isChange } from "../utils/slices"; import { naiveCoinSelection } from "../utils"; -import { updateBlockchainClient } from "./clientActions"; import { setBalanceError, setChangeOutput, diff --git a/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx b/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx index cbfb98bf..66faf8af 100644 --- a/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx +++ b/apps/coordinator/src/components/ClientPicker/PrivateClientSettings.tsx @@ -1,5 +1,4 @@ -import React, { EventHandler } from "react"; -import PropTypes from "prop-types"; +import React from "react"; // Components import { Grid, TextField, Button, FormHelperText, Box } from "@mui/material"; From 6c04ac497b9fcd227b821b0d5ccb8b5291a24d18 Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 15:50:54 -0500 Subject: [PATCH 12/15] add changeset --- .changeset/unlucky-beans-kiss.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/unlucky-beans-kiss.md diff --git a/.changeset/unlucky-beans-kiss.md b/.changeset/unlucky-beans-kiss.md new file mode 100644 index 00000000..17521fda --- /dev/null +++ b/.changeset/unlucky-beans-kiss.md @@ -0,0 +1,13 @@ +--- +"caravan-coordinator": major +"@caravan/clients": minor +--- + +Caravan Coordinator: +Adds descriptor import support for caravan coordinator. This is a backwards incompatible +change for instances that need to interact with bitcoind nodes older than v21 which introduced +descriptor wallets. + +@caravan/clients +- named wallet interactions +- import descriptor support From 97cdd028e1ced5ecdc880eef2119d3a6fa9f9275 Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 16:20:30 -0500 Subject: [PATCH 13/15] cleanup --- apps/coordinator/src/components/ClientPicker/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/coordinator/src/components/ClientPicker/index.jsx b/apps/coordinator/src/components/ClientPicker/index.jsx index be31fe09..5c18f9a2 100644 --- a/apps/coordinator/src/components/ClientPicker/index.jsx +++ b/apps/coordinator/src/components/ClientPicker/index.jsx @@ -50,7 +50,7 @@ const ClientPicker = ({ const [connectError, setConnectError] = useState(""); const [connectSuccess, setConnectSuccess] = useState(false); const blockchainClient = useGetClient(); - console.log("blockchainClient", blockchainClient); + const validatePassword = () => { return ""; }; From a983b2205dcceb28ffad2575eb4554dfd5891e65 Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 17:59:58 -0500 Subject: [PATCH 14/15] Update readme --- apps/coordinator/README.md | 51 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/coordinator/README.md b/apps/coordinator/README.md index 53165c8d..b78a650d 100644 --- a/apps/coordinator/README.md +++ b/apps/coordinator/README.md @@ -136,11 +136,58 @@ which can export these data can be made to work with Caravan. By default, Caravan uses a free API provided by [mempool.space](https://mempool.space) whenever it needs -information about the bitcoin blockchain or to broadcast transactions. +information about the bitcoin blockchain or to broadcast transactions. Blockstream.info is also available as a fallback +option for a public API. -You can ask Caravan to use your own private [bitcoind full +### Bitcoind client + +You can also ask Caravan to use your own private [bitcoind full node](https://bitcoin.org/en/full-node). +#### Bitcoind Wallet + +In order for Caravan to calculate wallet balances and +construct transactions from available UTXOs, when using +your own bitcoind node, you will need to have a watch-only +wallet available to import your wallet's descriptors to (available since +bitcoin v21). + +Bitcoind no longer initializes with a wallet so you will have to create +one manually: + +```shell +bitcoin-cli -named createwallet wallet_name="watcher" blank=true disable_private_keys=true load_on_startup=true +``` + +What does this do: +- `-named` means you can pass named params rather than having to do them in exactly the right order +- `createwallet` this creates our wallet (available since [v22](https://bitcoincore.org/en/doc/22.0.0/rpc/wallet/createwallet/)) +- `wallet_name`: the name of the wallet you will use to import your descriptors (multiple descriptors can be imported to the same wallet) +- `blank`: We don't need to initialize this wallet with any key information +- `disable_private_keys` this allows us to import watch-only descriptors (xpubs only, no xprivs) +- `load_on_startup` optionally set this wallet to always load when the node starts up. Wallets need to be manually loaded with `loadwallet` now so this can be handy. + +Then in Caravan you will have to use the `Import Addresses` button to have your node start +watching the addresses in your wallet. + +##### Multiple Wallets + +A node can have multiple wallets loaded at the same time. In such +cases if you don't indicate which wallet you are targeting +with wallet-specific commands then the API call will fail. + +As such, Caravan Coordinator and @caravan/clients now support an optional `walletName` configuration. If this is set in your configuration file (also available during wallet creation), then +the calls will make sure to target this wallet. Use the same value as +`wallet_name` from wallet creation above. + +#### Importing existing wallets + +IMPORTANT: if you're importing a wallet that has prior history into a node that was not +previously watching the addresses and did not have txindex enabled, you will have +to re-index your node (sync all blocks from the beginning checking for relevant history +that the node previously didn't care about) in order to see your balance reflected. + + #### Adding CORS Headers When asking Caravan to use a private bitcoind node, you may run into From a91b0f22ceee46e6648e0ac416b85e2b7368018d Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 28 Apr 2024 20:33:56 -0500 Subject: [PATCH 15/15] network connection test cleanup --- .../src/components/ClientPicker/index.jsx | 3 ++- apps/coordinator/src/hooks/descriptors.ts | 14 ++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/coordinator/src/components/ClientPicker/index.jsx b/apps/coordinator/src/components/ClientPicker/index.jsx index 5c18f9a2..8cfa1b1a 100644 --- a/apps/coordinator/src/components/ClientPicker/index.jsx +++ b/apps/coordinator/src/components/ClientPicker/index.jsx @@ -101,8 +101,9 @@ const ClientPicker = ({ try { if (blockchainClient.bitcoindParams.walletName) { await blockchainClient.getWalletInfo(); + } else { + await blockchainClient.getFeeEstimate(); } - await blockchainClient.getFeeEstimate(); if (onSuccess) { onSuccess(); } diff --git a/apps/coordinator/src/hooks/descriptors.ts b/apps/coordinator/src/hooks/descriptors.ts index 30123af9..82e0f2a3 100644 --- a/apps/coordinator/src/hooks/descriptors.ts +++ b/apps/coordinator/src/hooks/descriptors.ts @@ -6,27 +6,29 @@ import { getWalletConfig } from "../selectors/wallet"; import { getMaskedDerivation } from "@caravan/bitcoin"; export function useGetDescriptors() { - const walletConfig = useSelector(getWalletConfig); + const { quorum, extendedPublicKeys, addressType, network } = + useSelector(getWalletConfig); const [descriptors, setDescriptors] = useState({ change: "", receive: "" }); useEffect(() => { const loadAsync = async () => { const multisigConfig = { - requiredSigners: walletConfig.quorum.requiredSigners, - keyOrigins: walletConfig.extendedPublicKeys.map( + requiredSigners: quorum.requiredSigners, + keyOrigins: extendedPublicKeys.map( ({ xfp, bip32Path, xpub }: KeyOrigin) => ({ xfp, bip32Path: getMaskedDerivation({ xpub, bip32Path }), xpub, }), ), - addressType: walletConfig.addressType, - network: walletConfig.network, + addressType: addressType, + network: network, }; const { change, receive } = await encodeDescriptors(multisigConfig); setDescriptors({ change, receive }); }; + loadAsync(); - }, [walletConfig]); + }, [quorum, extendedPublicKeys, addressType, network]); return descriptors; }