Skip to content

Commit

Permalink
Merge pull request #76 from caravan-bitcoin/watch-wallets
Browse files Browse the repository at this point in the history
Update to client interactions to support wallet api with bitcoind
  • Loading branch information
bucko13 authored May 1, 2024
2 parents 22173ec + d973241 commit 6189631
Show file tree
Hide file tree
Showing 29 changed files with 1,018 additions and 259 deletions.
13 changes: 13 additions & 0 deletions .changeset/unlucky-beans-kiss.md
Original file line number Diff line number Diff line change
@@ -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
51 changes: 49 additions & 2 deletions apps/coordinator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions apps/coordinator/src/actions/braidActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 {
Expand All @@ -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),
]);
});
Expand Down
89 changes: 70 additions & 19 deletions apps/coordinator/src/actions/clientActions.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -12,36 +13,86 @@ export const SET_CLIENT_PASSWORD_ERROR = "SET_CLIENT_PASSWORD_ERROR";

export const SET_BLOCKCHAIN_CLIENT = "SET_BLOCKCHAIN_CLIENT";

export const getBlockchainClientFromStore = () => {
return async (
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
// 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: ClientSettings,
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: ClientSettings): 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<any>,
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());
};
};

export const setBlockchainClient = () => {
return (
dispatch: Dispatch<any>,
getState: () => { settings: any; client: ClientSettings },
) => {
const { network } = getState().settings;
const { client } = getState();

const blockchainClient = new BlockchainClient({
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;
};
};
5 changes: 2 additions & 3 deletions apps/coordinator/src/actions/walletActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
import BigNumber from "bignumber.js";
import { isChange } from "../utils/slices";
import { naiveCoinSelection } from "../utils";
import { getBlockchainClientFromStore } from "./clientActions";
import {
setBalanceError,
setChangeOutput,
Expand Down Expand Up @@ -180,12 +179,12 @@ export function updateTxSlices(
deposits: { nextNode: nextDepositSlice, nodes: depositSlices },
change: { nodes: changeSlices },
},
client: { blockchainClient },
} = 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 client.fetchAddressUTXOs(address);
const utxos = await blockchainClient.fetchAddressUtxos(address);
return {
addressUsed: true,
change: isChange(bip32Path),
Expand Down
126 changes: 0 additions & 126 deletions apps/coordinator/src/components/ClientPicker/PrivateClientSettings.jsx

This file was deleted.

Loading

0 comments on commit 6189631

Please sign in to comment.