otx-btc-wallet-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "otx-btc-wallet-react",
3
+ "version": "0.1.0",
4
+ "description": "React hooks and components for otx-btc-wallet",
5
+ "license": "MIT",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src"
19
+ ],
20
+ "sideEffects": false,
21
+ "dependencies": {
22
+ "otx-btc-wallet-core": "0.1.0"
23
+ },
24
+ "devDependencies": {
25
+ "@types/react": "^18.2.42",
26
+ "react": "^18.2.0",
27
+ "typescript": "^5.3.2",
28
+ "tsup": "^8.0.1"
29
+ },
30
+ "peerDependencies": {
31
+ "react": ">=18.0.0"
32
+ },
33
+ "keywords": [
34
+ "bitcoin",
35
+ "wallet",
36
+ "react",
37
+ "hooks"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsup --watch",
42
+ "clean": "rm -rf dist",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }
@@ -0,0 +1,24 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ResolvedConfig } from 'otx-btc-wallet-core';
3
+
4
+ /**
5
+ * Context for otx-btc-wallet configuration
6
+ */
7
+ export const BtcWalletContext = createContext<ResolvedConfig | null>(null);
8
+
9
+ /**
10
+ * Hook to access the otx-btc-wallet config
11
+ * @throws Error if used outside of BtcWalletProvider
12
+ */
13
+ export function useConfig(): ResolvedConfig {
14
+ const config = useContext(BtcWalletContext);
15
+
16
+ if (!config) {
17
+ throw new Error(
18
+ 'useConfig must be used within a BtcWalletProvider. ' +
19
+ 'Wrap your app in <BtcWalletProvider config={config}>.'
20
+ );
21
+ }
22
+
23
+ return config;
24
+ }
@@ -0,0 +1,74 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import type {
3
+ WalletAccount,
4
+ BitcoinConnector,
5
+ ConnectionStatus,
6
+ AddressType,
7
+ } from 'otx-btc-wallet-core';
8
+ import { useConfig } from '../context';
9
+
10
+ export interface UseAccountReturn {
11
+ /** Connected address */
12
+ address: string | undefined;
13
+ /** Connected public key */
14
+ publicKey: string | undefined;
15
+ /** Address type */
16
+ addressType: AddressType | undefined;
17
+ /** Full account object */
18
+ account: WalletAccount | undefined;
19
+ /** Active connector */
20
+ connector: BitcoinConnector | undefined;
21
+ /** Connection status */
22
+ status: ConnectionStatus;
23
+ /** Is connected */
24
+ isConnected: boolean;
25
+ /** Is connecting */
26
+ isConnecting: boolean;
27
+ /** Is disconnected */
28
+ isDisconnected: boolean;
29
+ /** Is reconnecting */
30
+ isReconnecting: boolean;
31
+ }
32
+
33
+ /**
34
+ * Hook to get current account state
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * function Profile() {
39
+ * const { address, isConnected, connector } = useAccount();
40
+ *
41
+ * if (!isConnected) return <div>Not connected</div>;
42
+ *
43
+ * return (
44
+ * <div>
45
+ * <p>Address: {address}</p>
46
+ * <p>Connected via {connector?.name}</p>
47
+ * </div>
48
+ * );
49
+ * }
50
+ * ```
51
+ */
52
+ export function useAccount(): UseAccountReturn {
53
+ const config = useConfig();
54
+ const { store } = config;
55
+
56
+ const state = useSyncExternalStore(
57
+ store.subscribe,
58
+ () => store.getState(),
59
+ () => store.getState()
60
+ );
61
+
62
+ return {
63
+ address: state.account?.address,
64
+ publicKey: state.account?.publicKey,
65
+ addressType: state.account?.type,
66
+ account: state.account ?? undefined,
67
+ connector: state.connector ?? undefined,
68
+ status: state.status,
69
+ isConnected: state.status === 'connected',
70
+ isConnecting: state.status === 'connecting',
71
+ isDisconnected: state.status === 'disconnected',
72
+ isReconnecting: state.status === 'reconnecting',
73
+ };
74
+ }
@@ -0,0 +1,136 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import type {
3
+ BitcoinConnector,
4
+ BitcoinNetwork,
5
+ WalletAccount,
6
+ } from 'otx-btc-wallet-core';
7
+ import { useConfig } from '../context';
8
+
9
+ export type ConnectArgs = {
10
+ connector: BitcoinConnector;
11
+ network?: BitcoinNetwork;
12
+ };
13
+
14
+ export interface UseConnectReturn {
15
+ /** Connect to a wallet (fire-and-forget) */
16
+ connect: (args: ConnectArgs) => void;
17
+ /** Connect to a wallet (returns promise) */
18
+ connectAsync: (args: ConnectArgs) => Promise<WalletAccount>;
19
+ /** Available connectors */
20
+ connectors: BitcoinConnector[];
21
+ /** Last error */
22
+ error: Error | null;
23
+ /** Is currently connecting */
24
+ isLoading: boolean;
25
+ /** Did connection fail */
26
+ isError: boolean;
27
+ /** Did connection succeed */
28
+ isSuccess: boolean;
29
+ /** Reset state */
30
+ reset: () => void;
31
+ }
32
+
33
+ /**
34
+ * Hook to connect to a wallet
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * function ConnectButtons() {
39
+ * const { connect, connectors, isLoading, error } = useConnect();
40
+ *
41
+ * return (
42
+ * <div>
43
+ * {connectors.map((connector) => (
44
+ * <button
45
+ * key={connector.id}
46
+ * onClick={() => connect({ connector })}
47
+ * disabled={isLoading}
48
+ * >
49
+ * Connect {connector.name}
50
+ * </button>
51
+ * ))}
52
+ * {error && <p>Error: {error.message}</p>}
53
+ * </div>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export function useConnect(): UseConnectReturn {
59
+ const config = useConfig();
60
+ const { store, connectors } = config;
61
+
62
+ const [state, setState] = useState<{
63
+ isLoading: boolean;
64
+ isError: boolean;
65
+ isSuccess: boolean;
66
+ error: Error | null;
67
+ }>({
68
+ isLoading: false,
69
+ isError: false,
70
+ isSuccess: false,
71
+ error: null,
72
+ });
73
+
74
+ const connectAsync = useCallback(
75
+ async ({ connector, network }: ConnectArgs) => {
76
+ setState({
77
+ isLoading: true,
78
+ isError: false,
79
+ isSuccess: false,
80
+ error: null,
81
+ });
82
+
83
+ try {
84
+ const account = await store.getState().connect(connector, network);
85
+ setState({
86
+ isLoading: false,
87
+ isError: false,
88
+ isSuccess: true,
89
+ error: null,
90
+ });
91
+ return account;
92
+ } catch (error) {
93
+ setState({
94
+ isLoading: false,
95
+ isError: true,
96
+ isSuccess: false,
97
+ error: error as Error,
98
+ });
99
+ throw error;
100
+ }
101
+ },
102
+ [store]
103
+ );
104
+
105
+ const connect = useCallback(
106
+ (args: ConnectArgs) => {
107
+ connectAsync(args).catch(() => {
108
+ // Error is already captured in state
109
+ });
110
+ },
111
+ [connectAsync]
112
+ );
113
+
114
+ const reset = useCallback(() => {
115
+ setState({
116
+ isLoading: false,
117
+ isError: false,
118
+ isSuccess: false,
119
+ error: null,
120
+ });
121
+ }, []);
122
+
123
+ return useMemo(
124
+ () => ({
125
+ connect,
126
+ connectAsync,
127
+ connectors,
128
+ error: state.error,
129
+ isLoading: state.isLoading,
130
+ isError: state.isError,
131
+ isSuccess: state.isSuccess,
132
+ reset,
133
+ }),
134
+ [connect, connectAsync, connectors, state, reset]
135
+ );
136
+ }
@@ -0,0 +1,95 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { useConfig } from '../context';
3
+
4
+ export interface UseDisconnectReturn {
5
+ /** Disconnect from wallet (fire-and-forget) */
6
+ disconnect: () => void;
7
+ /** Disconnect from wallet (returns promise) */
8
+ disconnectAsync: () => Promise<void>;
9
+ /** Is currently disconnecting */
10
+ isLoading: boolean;
11
+ /** Did disconnect fail */
12
+ isError: boolean;
13
+ /** Did disconnect succeed */
14
+ isSuccess: boolean;
15
+ /** Last error */
16
+ error: Error | null;
17
+ }
18
+
19
+ /**
20
+ * Hook to disconnect from wallet
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * function DisconnectButton() {
25
+ * const { disconnect, isLoading } = useDisconnect();
26
+ *
27
+ * return (
28
+ * <button onClick={disconnect} disabled={isLoading}>
29
+ * Disconnect
30
+ * </button>
31
+ * );
32
+ * }
33
+ * ```
34
+ */
35
+ export function useDisconnect(): UseDisconnectReturn {
36
+ const config = useConfig();
37
+ const { store } = config;
38
+
39
+ const [state, setState] = useState<{
40
+ isLoading: boolean;
41
+ isError: boolean;
42
+ isSuccess: boolean;
43
+ error: Error | null;
44
+ }>({
45
+ isLoading: false,
46
+ isError: false,
47
+ isSuccess: false,
48
+ error: null,
49
+ });
50
+
51
+ const disconnectAsync = useCallback(async () => {
52
+ setState({
53
+ isLoading: true,
54
+ isError: false,
55
+ isSuccess: false,
56
+ error: null,
57
+ });
58
+
59
+ try {
60
+ await store.getState().disconnect();
61
+ setState({
62
+ isLoading: false,
63
+ isError: false,
64
+ isSuccess: true,
65
+ error: null,
66
+ });
67
+ } catch (error) {
68
+ setState({
69
+ isLoading: false,
70
+ isError: true,
71
+ isSuccess: false,
72
+ error: error as Error,
73
+ });
74
+ throw error;
75
+ }
76
+ }, [store]);
77
+
78
+ const disconnect = useCallback(() => {
79
+ disconnectAsync().catch(() => {
80
+ // Error is already captured in state
81
+ });
82
+ }, [disconnectAsync]);
83
+
84
+ return useMemo(
85
+ () => ({
86
+ disconnect,
87
+ disconnectAsync,
88
+ isLoading: state.isLoading,
89
+ isError: state.isError,
90
+ isSuccess: state.isSuccess,
91
+ error: state.error,
92
+ }),
93
+ [disconnect, disconnectAsync, state]
94
+ );
95
+ }
@@ -0,0 +1,135 @@
1
+ import { useCallback, useMemo, useState, useEffect } from 'react';
2
+ import type { WalletAccount } from 'otx-btc-wallet-core';
3
+ import { useConfig } from '../context';
4
+
5
+ export interface UseMultiAddressReturn {
6
+ /** All available addresses from the wallet */
7
+ addresses: WalletAccount[];
8
+ /** Payment address (for sending/receiving BTC) */
9
+ paymentAddress: WalletAccount | null;
10
+ /** Ordinals address (for NFTs/inscriptions) */
11
+ ordinalsAddress: WalletAccount | null;
12
+ /** Currently selected primary address */
13
+ primaryAddress: WalletAccount | null;
14
+ /** Set the primary address */
15
+ setPrimaryAddress: (address: WalletAccount) => void;
16
+ /** Is loading addresses */
17
+ isLoading: boolean;
18
+ /** Refresh addresses from wallet */
19
+ refresh: () => Promise<void>;
20
+ }
21
+
22
+ /**
23
+ * Hook to manage multiple addresses from a connected wallet
24
+ *
25
+ * Xverse and Leather wallets provide both payment and ordinals addresses.
26
+ * This hook helps manage and switch between them.
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * function MultiAddressDisplay() {
31
+ * const {
32
+ * paymentAddress,
33
+ * ordinalsAddress,
34
+ * primaryAddress,
35
+ * setPrimaryAddress
36
+ * } = useMultiAddress();
37
+ *
38
+ * return (
39
+ * <div>
40
+ * <h3>Payment: {paymentAddress?.address}</h3>
41
+ * <h3>Ordinals: {ordinalsAddress?.address}</h3>
42
+ * <select onChange={(e) => {
43
+ * const addr = e.target.value === 'payment' ? paymentAddress : ordinalsAddress;
44
+ * if (addr) setPrimaryAddress(addr);
45
+ * }}>
46
+ * <option value="payment">Payment</option>
47
+ * <option value="ordinals">Ordinals</option>
48
+ * </select>
49
+ * </div>
50
+ * );
51
+ * }
52
+ * ```
53
+ */
54
+ export function useMultiAddress(): UseMultiAddressReturn {
55
+ const config = useConfig();
56
+ const { store } = config;
57
+
58
+ const [addresses, setAddresses] = useState<WalletAccount[]>([]);
59
+ const [primaryAddress, setPrimaryAddressState] = useState<WalletAccount | null>(null);
60
+ const [isLoading, setIsLoading] = useState(false);
61
+
62
+ // Fetch addresses from connector
63
+ const fetchAddresses = useCallback(async () => {
64
+ const { connector } = store.getState();
65
+ if (!connector) {
66
+ setAddresses([]);
67
+ setPrimaryAddressState(null);
68
+ return;
69
+ }
70
+
71
+ setIsLoading(true);
72
+ try {
73
+ const accounts = await connector.getAccounts();
74
+ setAddresses(accounts);
75
+
76
+ // Set primary address to first one if not set
77
+ if (accounts.length > 0 && !primaryAddress) {
78
+ setPrimaryAddressState(accounts[0] ?? null);
79
+ }
80
+ } catch {
81
+ setAddresses([]);
82
+ } finally {
83
+ setIsLoading(false);
84
+ }
85
+ }, [store, primaryAddress]);
86
+
87
+ // Fetch addresses when connector changes
88
+ useEffect(() => {
89
+ const unsubscribe = store.subscribe(
90
+ (state) => state.connector,
91
+ () => {
92
+ void fetchAddresses();
93
+ }
94
+ );
95
+
96
+ // Initial fetch
97
+ void fetchAddresses();
98
+
99
+ return unsubscribe;
100
+ }, [store, fetchAddresses]);
101
+
102
+ // Derive payment and ordinals addresses
103
+ const { paymentAddress, ordinalsAddress } = useMemo(() => {
104
+ // Payment addresses are typically segwit (bc1q) or nested-segwit (3)
105
+ const payment = addresses.find(
106
+ (a) => a.type === 'segwit' || a.type === 'nested-segwit'
107
+ ) ?? addresses[0] ?? null;
108
+
109
+ // Ordinals addresses are typically taproot (bc1p)
110
+ const ordinals = addresses.find((a) => a.type === 'taproot') ?? null;
111
+
112
+ return { paymentAddress: payment, ordinalsAddress: ordinals };
113
+ }, [addresses]);
114
+
115
+ const setPrimaryAddress = useCallback((address: WalletAccount) => {
116
+ setPrimaryAddressState(address);
117
+ }, []);
118
+
119
+ const refresh = useCallback(async () => {
120
+ await fetchAddresses();
121
+ }, [fetchAddresses]);
122
+
123
+ return useMemo(
124
+ () => ({
125
+ addresses,
126
+ paymentAddress,
127
+ ordinalsAddress,
128
+ primaryAddress,
129
+ setPrimaryAddress,
130
+ isLoading,
131
+ refresh,
132
+ }),
133
+ [addresses, paymentAddress, ordinalsAddress, primaryAddress, setPrimaryAddress, isLoading, refresh]
134
+ );
135
+ }
@@ -0,0 +1,33 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import type { BitcoinNetwork } from 'otx-btc-wallet-core';
3
+ import { useConfig } from '../context';
4
+
5
+ export interface UseNetworkReturn {
6
+ /** Current network */
7
+ network: BitcoinNetwork;
8
+ }
9
+
10
+ /**
11
+ * Hook to get current network
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * function NetworkInfo() {
16
+ * const { network } = useNetwork();
17
+ *
18
+ * return <p>Network: {network}</p>;
19
+ * }
20
+ * ```
21
+ */
22
+ export function useNetwork(): UseNetworkReturn {
23
+ const config = useConfig();
24
+ const { store } = config;
25
+
26
+ const network = useSyncExternalStore(
27
+ store.subscribe,
28
+ () => store.getState().network,
29
+ () => store.getState().network
30
+ );
31
+
32
+ return { network };
33
+ }
@@ -0,0 +1,137 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { useConfig } from '../context';
3
+
4
+ export interface UseSendTransactionReturn {
5
+ /** Send transaction (fire-and-forget) */
6
+ sendTransaction: (args: { to: string; amount: number }) => void;
7
+ /** Send transaction (returns promise with txid) */
8
+ sendTransactionAsync: (args: { to: string; amount: number }) => Promise<string>;
9
+ /** Transaction ID */
10
+ data: string | undefined;
11
+ /** Last error */
12
+ error: Error | null;
13
+ /** Is sending */
14
+ isLoading: boolean;
15
+ /** Did send fail */
16
+ isError: boolean;
17
+ /** Did send succeed */
18
+ isSuccess: boolean;
19
+ /** Reset state */
20
+ reset: () => void;
21
+ }
22
+
23
+ /**
24
+ * Hook to send a Bitcoin transaction
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * function SendForm() {
29
+ * const { sendTransaction, isLoading, data, error } = useSendTransaction();
30
+ *
31
+ * const handleSend = () => {
32
+ * sendTransaction({ to: 'bc1q...', amount: 10000 }); // 10000 sats
33
+ * };
34
+ *
35
+ * return (
36
+ * <div>
37
+ * <button onClick={handleSend} disabled={isLoading}>
38
+ * Send 10,000 sats
39
+ * </button>
40
+ * {data && <p>TX: {data}</p>}
41
+ * {error && <p>Error: {error.message}</p>}
42
+ * </div>
43
+ * );
44
+ * }
45
+ * ```
46
+ */
47
+ export function useSendTransaction(): UseSendTransactionReturn {
48
+ const config = useConfig();
49
+ const { store } = config;
50
+
51
+ const [state, setState] = useState<{
52
+ data: string | undefined;
53
+ error: Error | null;
54
+ isLoading: boolean;
55
+ isError: boolean;
56
+ isSuccess: boolean;
57
+ }>({
58
+ data: undefined,
59
+ error: null,
60
+ isLoading: false,
61
+ isError: false,
62
+ isSuccess: false,
63
+ });
64
+
65
+ const sendTransactionAsync = useCallback(
66
+ async ({ to, amount }: { to: string; amount: number }) => {
67
+ const { connector } = store.getState();
68
+
69
+ if (!connector) {
70
+ throw new Error('Not connected');
71
+ }
72
+
73
+ setState({
74
+ data: undefined,
75
+ error: null,
76
+ isLoading: true,
77
+ isError: false,
78
+ isSuccess: false,
79
+ });
80
+
81
+ try {
82
+ const txid = await connector.sendTransaction(to, amount);
83
+ setState({
84
+ data: txid,
85
+ error: null,
86
+ isLoading: false,
87
+ isError: false,
88
+ isSuccess: true,
89
+ });
90
+ return txid;
91
+ } catch (error) {
92
+ setState({
93
+ data: undefined,
94
+ error: error as Error,
95
+ isLoading: false,
96
+ isError: true,
97
+ isSuccess: false,
98
+ });
99
+ throw error;
100
+ }
101
+ },
102
+ [store]
103
+ );
104
+
105
+ const sendTransaction = useCallback(
106
+ ({ to, amount }: { to: string; amount: number }) => {
107
+ sendTransactionAsync({ to, amount }).catch(() => {
108
+ // Error is already captured in state
109
+ });
110
+ },
111
+ [sendTransactionAsync]
112
+ );
113
+
114
+ const reset = useCallback(() => {
115
+ setState({
116
+ data: undefined,
117
+ error: null,
118
+ isLoading: false,
119
+ isError: false,
120
+ isSuccess: false,
121
+ });
122
+ }, []);
123
+
124
+ return useMemo(
125
+ () => ({
126
+ sendTransaction,
127
+ sendTransactionAsync,
128
+ data: state.data,
129
+ error: state.error,
130
+ isLoading: state.isLoading,
131
+ isError: state.isError,
132
+ isSuccess: state.isSuccess,
133
+ reset,
134
+ }),
135
+ [sendTransaction, sendTransactionAsync, state, reset]
136
+ );
137
+ }