From 1d077e7bd2acca9f31ec9783afd29809695019cb Mon Sep 17 00:00:00 2001 From: buck Date: Sun, 14 Jan 2024 21:43:35 -0600 Subject: [PATCH] feat(clients): use new blockchain client via redux --- src/actions/braidActions.js | 13 +++-- src/actions/clientActions.js | 8 --- src/actions/clientActions.ts | 50 +++++++++++++++++++ src/actions/walletActions.js | 8 ++- src/components/ClientPicker/index.jsx | 26 +++++++--- src/components/ScriptExplorer/OutputsForm.jsx | 9 ++-- src/components/ScriptExplorer/ScriptEntry.jsx | 13 +++-- src/components/ScriptExplorer/Transaction.jsx | 13 +++-- src/components/Wallet/WalletDeposit.jsx | 10 ++-- src/components/Wallet/WalletGenerator.jsx | 27 +++++----- src/reducers/clientReducer.js | 5 +- 11 files changed, 118 insertions(+), 64 deletions(-) delete mode 100644 src/actions/clientActions.js create mode 100644 src/actions/clientActions.ts diff --git a/src/actions/braidActions.js b/src/actions/braidActions.js index ffd42646..d249e625 100644 --- a/src/actions/braidActions.js +++ b/src/actions/braidActions.js @@ -2,8 +2,8 @@ import { updateDepositSliceAction, updateChangeSliceAction, } from "./walletActions"; -import { fetchAddressUTXOs, getAddressStatus } from "../clients/blockchain"; import { setErrorNotification } from "./errorNotificationActions"; +import { getBlockchainClientFromStore } from "./clientActions"; export const UPDATE_BRAID_SLICE = "UPDATE_BRAID_SLICE"; @@ -15,10 +15,9 @@ export const UPDATE_BRAID_SLICE = "UPDATE_BRAID_SLICE"; * @param {array} slices - array of slices from one or more braids */ export const fetchSliceData = async (slices) => { - return async (dispatch, getState) => { - const { network } = getState().settings; - const { client } = getState(); - + return async (dispatch) => { + const blockchainClient = await dispatch(getBlockchainClientFromStore()); + if (!blockchainClient) return; try { // Create a list of the async calls for updating the slice data. // This lets us run these requests in parallel with a Promise.all @@ -27,8 +26,8 @@ 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([ - fetchAddressUTXOs(address, network, client), - getAddressStatus(address, network, client), + blockchainClient.fetchAddressUTXOs(address), + blockchainClient.getAddressStatus(address), ]); }); diff --git a/src/actions/clientActions.js b/src/actions/clientActions.js deleted file mode 100644 index 0c9945e5..00000000 --- a/src/actions/clientActions.js +++ /dev/null @@ -1,8 +0,0 @@ -export const SET_CLIENT_TYPE = "SET_CLIENT_TYPE"; -export const SET_CLIENT_URL = "SET_CLIENT_URL"; -export const SET_CLIENT_USERNAME = "SET_CLIENT_USERNAME"; -export const SET_CLIENT_PASSWORD = "SET_CLIENT_PASSWORD"; - -export const SET_CLIENT_URL_ERROR = "SET_CLIENT_URL_ERROR"; -export const SET_CLIENT_USERNAME_ERROR = "SET_CLIENT_USERNAME_ERROR"; -export const SET_CLIENT_PASSWORD_ERROR = "SET_CLIENT_PASSWORD_ERROR"; diff --git a/src/actions/clientActions.ts b/src/actions/clientActions.ts new file mode 100644 index 00000000..669a1e10 --- /dev/null +++ b/src/actions/clientActions.ts @@ -0,0 +1,50 @@ +import { Dispatch } from "react"; +import { BlockchainClient, ClientType } from "../clients/client"; + +export const SET_CLIENT_TYPE = "SET_CLIENT_TYPE"; +export const SET_CLIENT_URL = "SET_CLIENT_URL"; +export const SET_CLIENT_USERNAME = "SET_CLIENT_USERNAME"; +export const SET_CLIENT_PASSWORD = "SET_CLIENT_PASSWORD"; + +export const SET_CLIENT_URL_ERROR = "SET_CLIENT_URL_ERROR"; +export const SET_CLIENT_USERNAME_ERROR = "SET_CLIENT_USERNAME_ERROR"; +export const SET_CLIENT_PASSWORD_ERROR = "SET_CLIENT_PASSWORD_ERROR"; + +export const SET_BLOCKCHAIN_CLIENT = "SET_BLOCKCHAIN_CLIENT"; + +// TODO: use this to add more flexibility to client support +// For example, this defaults to blockstream for public client +// but can also support mempool.space as an option +export const getBlockchainClientFromStore = async () => { + return async ( + dispatch: Dispatch, + getState: () => { settings: any; client: any } + ) => { + const { network } = getState().settings; + const { client } = getState(); + if (!client) return; + let clientType: ClientType; + + switch (client.type) { + case "public": + clientType = ClientType.BLOCKSTREAM; + break; + case "private": + clientType = ClientType.PRIVATE; + break; + default: + // this allows us to support other clients in the future + // like mempool.space + clientType = client.type; + } + + const blockchainClient = new BlockchainClient({ + client, + type: clientType, + network, + throttled: true, + }); + dispatch({ type: SET_BLOCKCHAIN_CLIENT, value: blockchainClient }); + return blockchainClient; + }; +}; diff --git a/src/actions/walletActions.js b/src/actions/walletActions.js index 9560cb39..e0079c21 100644 --- a/src/actions/walletActions.js +++ b/src/actions/walletActions.js @@ -4,7 +4,6 @@ import { } from "unchained-bitcoin"; import BigNumber from "bignumber.js"; -import { fetchAddressUTXOs } from "../clients/blockchain"; import { isChange } from "../utils/slices"; import { naiveCoinSelection } from "../utils"; import { @@ -16,6 +15,7 @@ import { } from "./transactionActions"; import { setErrorNotification } from "./errorNotificationActions"; import { getSpendableSlices } from "../selectors/wallet"; +import { getBlockchainClientFromStore } from "./clientActions"; export const UPDATE_DEPOSIT_SLICE = "UPDATE_DEPOSIT_SLICE"; export const UPDATE_CHANGE_SLICE = "UPDATE_CHANGE_SLICE"; @@ -173,8 +173,6 @@ export function updateTxSlices( // eslint-disable-next-line consistent-return return async (dispatch, getState) => { const { - settings: { network }, - client, spend: { transaction: { changeAddress, inputs, txid }, }, @@ -183,11 +181,11 @@ export function updateTxSlices( change: { nodes: changeSlices }, }, } = getState(); - + const client = await dispatch(getBlockchainClientFromStore()); // 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 fetchAddressUTXOs(address, network, client); + const utxos = await client.fetchAddressUtxos(address); return { addressUsed: true, change: isChange(bip32Path), diff --git a/src/components/ClientPicker/index.jsx b/src/components/ClientPicker/index.jsx index 81dba697..4e784f22 100644 --- a/src/components/ClientPicker/index.jsx +++ b/src/components/ClientPicker/index.jsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import PropTypes from "prop-types"; -import { connect } from "react-redux"; +import { connect, useDispatch } from "react-redux"; import { Grid, Card, @@ -12,7 +12,6 @@ import { RadioGroup, FormHelperText, } from "@mui/material"; -import { fetchFeeEstimate } from "../../clients/blockchain"; // Components @@ -26,6 +25,7 @@ import { SET_CLIENT_URL_ERROR, SET_CLIENT_USERNAME_ERROR, SET_CLIENT_PASSWORD_ERROR, + getBlockchainClientFromStore, } from "../../actions/clientActions"; import PrivateClientSettings from "./PrivateClientSettings"; @@ -49,6 +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 validatePassword = () => { return ""; @@ -64,41 +66,49 @@ const ClientPicker = ({ return ""; }; - const handleTypeChange = (event) => { + 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 = (event) => { + const handleUrlChange = async (event) => { const url = event.target.value; const error = validateUrl(url); if (!urlEdited && !error) setUrlEdited(true); setUrl(url); setUrlError(error); + await updateBlockchainClient(); }; - const handleUsernameChange = (event) => { + const handleUsernameChange = async (event) => { const username = event.target.value; const error = validateUsername(username); setUsername(username); setUsernameError(error); + await updateBlockchainClient(); }; - const handlePasswordChange = (event) => { + const handlePasswordChange = async (event) => { const password = event.target.value; const error = validatePassword(password); setPassword(password); setPasswordError(error); + await updateBlockchainClient(); }; const testConnection = async () => { setConnectError(""); setConnectSuccess(false); try { - await fetchFeeEstimate(network, client); + await blockchainClient.getFeeEstimate(); if (onSuccess) { onSuccess(); } @@ -139,7 +149,7 @@ const ClientPicker = ({ {client.type === "public" && ( {"'Public' uses the "} - mempool.space + {blockchainClient?.type} {" API. Switch to private to use a "} bitcoind {" node."} diff --git a/src/components/ScriptExplorer/OutputsForm.jsx b/src/components/ScriptExplorer/OutputsForm.jsx index 9cfd7c9d..ddc06dd8 100644 --- a/src/components/ScriptExplorer/OutputsForm.jsx +++ b/src/components/ScriptExplorer/OutputsForm.jsx @@ -25,10 +25,10 @@ import { finalizeOutputs as finalizeOutputsAction, resetOutputs as resetOutputsAction, } from "../../actions/transactionActions"; -import { fetchFeeEstimate } from "../../clients/blockchain"; import { MIN_SATS_PER_BYTE_FEE } from "../Wallet/constants"; import OutputEntry from "./OutputEntry"; import styles from "./styles.module.scss"; +import { getBlockchainClientFromStore } from "../../actions/clientActions"; class OutputsForm extends React.Component { static unitLabel(label, options) { @@ -181,11 +181,12 @@ class OutputsForm extends React.Component { }; getFeeEstimate = async () => { - const { client, network, setFeeRate } = this.props; + const { getBlockchainClient, setFeeRate } = this.props; + const client = await getBlockchainClient(); let feeEstimate; let feeRateFetchError = ""; try { - feeEstimate = await fetchFeeEstimate(network, client); + feeEstimate = await client.getFeeEstimate(); } catch (e) { feeRateFetchError = "There was an error fetching the fee rate."; } finally { @@ -485,6 +486,7 @@ OutputsForm.propTypes = { setOutputAmount: PropTypes.func.isRequired, signatureImporters: PropTypes.shape({}).isRequired, updatesComplete: PropTypes.bool, + getBlockchainClient: PropTypes.func.isRequired, }; OutputsForm.defaultProps = { @@ -511,6 +513,7 @@ const mapDispatchToProps = { setFee: setFeeAction, finalizeOutputs: finalizeOutputsAction, resetOutputs: resetOutputsAction, + getBlockchainClient: getBlockchainClientFromStore, }; export default connect(mapStateToProps, mapDispatchToProps)(OutputsForm); diff --git a/src/components/ScriptExplorer/ScriptEntry.jsx b/src/components/ScriptExplorer/ScriptEntry.jsx index c7c6e424..569e405b 100644 --- a/src/components/ScriptExplorer/ScriptEntry.jsx +++ b/src/components/ScriptExplorer/ScriptEntry.jsx @@ -21,7 +21,6 @@ import { TextField, FormHelperText, } from "@mui/material"; -import { fetchAddressUTXOs } from "../../clients/blockchain"; // Components import MultisigDetails from "../MultisigDetails"; @@ -48,6 +47,7 @@ import { chooseConfirmOwnership as chooseConfirmOwnershipAction, setOwnershipMultisig as setOwnershipMultisigAction, } from "../../actions/ownershipActions"; +import { getBlockchainClientFromStore } from "../../actions/clientActions"; class ScriptEntry extends React.Component { constructor(props) { @@ -249,12 +249,9 @@ class ScriptEntry extends React.Component { }; fetchUTXOs = async (multisig) => { - const { network, client } = this.props; - const addressData = await fetchAddressUTXOs( - multisig.address, - network, - client - ); + const { getBlockchainClient } = this.props; + const client = await getBlockchainClient(); + const addressData = await client.fetchAddressUtxos(multisig.address); return addressData; }; @@ -415,6 +412,7 @@ ScriptEntry.propTypes = { setTotalSigners: PropTypes.func.isRequired, importLegacyPSBT: PropTypes.func.isRequired, setUnsignedPSBT: PropTypes.func.isRequired, + getBlockchainClient: PropTypes.func.isRequired, }; function mapStateToProps(state) { @@ -443,6 +441,7 @@ const mapDispatchToProps = { setFee: setFeeAction, setUnsignedPSBT: setUnsignedPSBTAction, finalizeOutputs: finalizeOutputsAction, + getBlockchainClient: getBlockchainClientFromStore, }; export default connect(mapStateToProps, mapDispatchToProps)(ScriptEntry); diff --git a/src/components/ScriptExplorer/Transaction.jsx b/src/components/ScriptExplorer/Transaction.jsx index 770dd091..b9a0e127 100644 --- a/src/components/ScriptExplorer/Transaction.jsx +++ b/src/components/ScriptExplorer/Transaction.jsx @@ -16,10 +16,10 @@ import { CardContent, } from "@mui/material"; import { OpenInNew } from "@mui/icons-material"; -import { broadcastTransaction } from "../../clients/blockchain"; import Copyable from "../Copyable"; import { externalLink } from "utils/ExternalLink"; import { setTXID } from "../../actions/transactionActions"; +import { getBlockchainClientFromStore } from "../../actions/clientActions"; class Transaction extends React.Component { constructor(props) { @@ -44,17 +44,14 @@ class Transaction extends React.Component { }; handleBroadcast = async () => { - const { client, network, setTxid } = this.props; + const { getBlockchainClient, setTxid } = this.props; + const client = await getBlockchainClient(); const signedTransaction = this.buildSignedTransaction(); let error = ""; let txid = ""; this.setState({ broadcasting: true }); try { - txid = await broadcastTransaction( - signedTransaction.toHex(), - network, - client - ); + txid = await client.broadcastTransaction(signedTransaction.toHex()); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -127,6 +124,7 @@ Transaction.propTypes = { outputs: PropTypes.arrayOf(PropTypes.shape({})).isRequired, setTxid: PropTypes.func.isRequired, signatureImporters: PropTypes.shape({}).isRequired, + getBlockchainClient: PropTypes.func.isRequired, }; function mapStateToProps(state) { @@ -142,6 +140,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { setTxid: setTXID, + getBlockchainClient: getBlockchainClientFromStore, }; export default connect(mapStateToProps, mapDispatchToProps)(Transaction); diff --git a/src/components/Wallet/WalletDeposit.jsx b/src/components/Wallet/WalletDeposit.jsx index 67e4df12..b11a695e 100644 --- a/src/components/Wallet/WalletDeposit.jsx +++ b/src/components/Wallet/WalletDeposit.jsx @@ -15,11 +15,11 @@ import { } from "@mui/material"; import { enqueueSnackbar } from "notistack"; -import { fetchAddressUTXOs } from "../../clients/blockchain"; import { updateDepositSliceAction, resetWalletView as resetWalletViewAction, } from "../../actions/walletActions"; +import { getBlockchainClientFromStore } from "../../actions/clientActions"; import { getDepositableSlices } from "../../selectors/wallet"; import { slicePropTypes } from "../../proptypes"; @@ -27,7 +27,6 @@ import { slicePropTypes } from "../../proptypes"; import Copyable from "../Copyable"; import BitcoinIcon from "../BitcoinIcon"; import SlicesTable from "../Slices/SlicesTable"; - let depositTimer; class WalletDeposit extends React.Component { @@ -58,7 +57,7 @@ class WalletDeposit extends React.Component { }; getDepositAddress = () => { - const { network, client, updateDepositSlice, depositableSlices } = + const { getBlockchainClient, updateDepositSlice, depositableSlices } = this.props; const { depositIndex } = this.state; @@ -73,7 +72,8 @@ class WalletDeposit extends React.Component { let updates; try { const { address, slice } = this.state; - updates = await fetchAddressUTXOs(address, network, client); + const client = await getBlockchainClient(); + updates = await client.fetchAddressUtxos(address); if (updates && updates.utxos && updates.utxos.length) { clearInterval(depositTimer); updateDepositSlice({ ...updates, bip32Path: slice.bip32Path }); @@ -200,6 +200,7 @@ WalletDeposit.propTypes = { .isRequired, network: PropTypes.string.isRequired, updateDepositSlice: PropTypes.func.isRequired, + getBlockchainClient: PropTypes.func.isRequired, }; function mapStateToProps(state) { @@ -213,6 +214,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { updateDepositSlice: updateDepositSliceAction, resetWalletView: resetWalletViewAction, + getBlockchainClient: getBlockchainClientFromStore, }; export default connect(mapStateToProps, mapDispatchToProps)(WalletDeposit); diff --git a/src/components/Wallet/WalletGenerator.jsx b/src/components/Wallet/WalletGenerator.jsx index d0e38160..43289728 100644 --- a/src/components/Wallet/WalletGenerator.jsx +++ b/src/components/Wallet/WalletGenerator.jsx @@ -20,11 +20,6 @@ import { Box, } from "@mui/material"; import AccountCircleIcon from "@mui/icons-material/AccountCircle"; -import { - fetchAddressUTXOs, - getAddressStatus, - fetchFeeEstimate, -} from "../../clients/blockchain"; import ClientPicker from "../ClientPicker"; import ConfirmWallet from "./ConfirmWallet"; import RegisterWallet from "./RegisterWallet"; @@ -39,11 +34,11 @@ import { initialLoadComplete as initialLoadCompleteAction, updateWalletPolicyRegistrationsAction, } from "../../actions/walletActions"; -import { fetchSliceData as fetchSliceDataAction } from "../../actions/braidActions"; import { setExtendedPublicKeyImporterVisible } from "../../actions/extendedPublicKeyImporterActions"; import { setIsWallet as setIsWalletAction } from "../../actions/transactionActions"; import { wrappedActions } from "../../actions/utils"; import { + getBlockchainClientFromStore, SET_CLIENT_PASSWORD, SET_CLIENT_PASSWORD_ERROR, } from "../../actions/clientActions"; @@ -77,7 +72,6 @@ class WalletGenerator extends React.Component { const prevPassword = prevProps.client.password; const { setPasswordError, - network, client, common: { nodesLoaded }, setIsWallet, @@ -95,7 +89,7 @@ class WalletGenerator extends React.Component { if (prevPassword !== client.password && client.password.length) { // test the connection using the set password // but only if the password field hasn't been changed for 500ms - this.debouncedTestConnection({ network, client, setPasswordError }); + this.debouncedTestConnection({ setPasswordError }); } // make sure the spend view knows it's a wallet view once nodes @@ -169,14 +163,16 @@ class WalletGenerator extends React.Component { }; fetchUTXOs = async (isChange, multisig, attemptToKeepGenerating) => { - const { network, client } = this.props; + const { getBlockchainClient } = this.props; let addressStatus; - let updates = await fetchAddressUTXOs(multisig.address, network, client); + + const client = await getBlockchainClient(); + let updates = await client.fetchAddressUtxos(multisig.address); // only check for address status if there weren't any errors // fetching the utxos for the address if (updates && !updates.fetchUTXOsError.length) - addressStatus = await getAddressStatus(multisig.address, network, client); + addressStatus = await client.getAddressStatus(multisig.address); if (addressStatus) { updates = { ...updates, addressUsed: addressStatus.used }; @@ -259,9 +255,11 @@ class WalletGenerator extends React.Component { ).length; }; - testConnection = async ({ network, client, setPasswordError }, cb) => { + testConnection = async ({ setPasswordError }, cb) => { try { - await fetchFeeEstimate(network, client); + const { getBlockchainClient } = this.props; + const client = await getBlockchainClient(); + await client.getFeeEstimate(); setPasswordError(""); this.setState({ connectSuccess: true }, () => { // if testConnection was passed a callback @@ -523,6 +521,7 @@ WalletGenerator.propTypes = { totalSigners: PropTypes.number.isRequired, updateChangeSlice: PropTypes.func.isRequired, updateDepositSlice: PropTypes.func.isRequired, + getBlockchainClient: PropTypes.func.isRequired, }; function mapStateToProps(state) { @@ -539,11 +538,11 @@ const mapDispatchToProps = { freeze: setFrozen, updateDepositSlice: updateDepositSliceAction, updateChangeSlice: updateChangeSliceAction, - fetchSliceData: fetchSliceDataAction, setImportersVisible: setExtendedPublicKeyImporterVisible, setIsWallet: setIsWalletAction, resetWallet: resetWalletAction, resetNodesFetchErrors: resetNodesFetchErrorsAction, + getBlockchainClient: getBlockchainClientFromStore, ...wrappedActions({ setPassword: SET_CLIENT_PASSWORD, setPasswordError: SET_CLIENT_PASSWORD_ERROR, diff --git a/src/reducers/clientReducer.js b/src/reducers/clientReducer.js index e1cdb6bc..097c383c 100644 --- a/src/reducers/clientReducer.js +++ b/src/reducers/clientReducer.js @@ -7,6 +7,7 @@ import { SET_CLIENT_URL_ERROR, SET_CLIENT_USERNAME_ERROR, SET_CLIENT_PASSWORD_ERROR, + SET_BLOCKCHAIN_CLIENT, } from "../actions/clientActions"; const initialState = { @@ -18,6 +19,7 @@ const initialState = { usernameError: "", passwordError: "", status: "unknown", + blockchainClient: null, }; export default (state = initialState, action) => { @@ -36,7 +38,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_BLOCKCHAIN_CLIENT: + return updateState(state, { blockchainClient: action.value }); default: return state; }