otx-btc-wallet-core 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.
@@ -0,0 +1,296 @@
1
+ import { createStore as createZustandStore } from 'zustand/vanilla';
2
+ import { persist, subscribeWithSelector } from 'zustand/middleware';
3
+ import type { PersistOptions } from 'zustand/middleware';
4
+ import type { BitcoinConnector } from '../types/connector';
5
+ import type { WalletAccount } from '../types/account';
6
+ import type { BtcWalletState, ConnectionStatus } from '../types/state';
7
+ import type { BitcoinNetwork } from '../types/network';
8
+ import type { Config } from '../types/config';
9
+
10
+ /**
11
+ * Store actions
12
+ */
13
+ export type StoreActions = {
14
+ /** Connect to a wallet */
15
+ connect: (
16
+ connector: BitcoinConnector,
17
+ network?: BitcoinNetwork
18
+ ) => Promise<WalletAccount>;
19
+
20
+ /** Disconnect from current wallet */
21
+ disconnect: () => Promise<void>;
22
+
23
+ /** Reconnect to previously connected wallet */
24
+ reconnect: (connectors: BitcoinConnector[]) => Promise<void>;
25
+
26
+ /** Set account */
27
+ setAccount: (account: WalletAccount | null) => void;
28
+
29
+ /** Set connection status */
30
+ setStatus: (status: ConnectionStatus) => void;
31
+
32
+ /** Set error */
33
+ setError: (error: Error | null) => void;
34
+
35
+ /** Reset store to initial state */
36
+ reset: () => void;
37
+ };
38
+
39
+ export type Store = BtcWalletState & StoreActions;
40
+
41
+ /** Persisted state shape - only these fields are saved to storage */
42
+ type PersistedState = {
43
+ connectorId: string | null;
44
+ network: string;
45
+ };
46
+
47
+ const initialState: BtcWalletState = {
48
+ status: 'disconnected',
49
+ account: null,
50
+ connector: null,
51
+ connectorId: null,
52
+ network: 'mainnet',
53
+ error: null,
54
+ };
55
+
56
+ /**
57
+ * Create the btc-wallet store
58
+ */
59
+ export function createStore(config: Config) {
60
+ // Track cleanup functions for event listeners to prevent memory leaks
61
+ let unsubscribeAccounts: (() => void) | null = null;
62
+ let unsubscribeNetwork: (() => void) | null = null;
63
+
64
+ /**
65
+ * Cleanup all event listeners
66
+ */
67
+ const cleanupListeners = () => {
68
+ if (unsubscribeAccounts) {
69
+ unsubscribeAccounts();
70
+ unsubscribeAccounts = null;
71
+ }
72
+ if (unsubscribeNetwork) {
73
+ unsubscribeNetwork();
74
+ unsubscribeNetwork = null;
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Setup event listeners for a connector
80
+ * This is shared between connect and reconnect to avoid code duplication
81
+ */
82
+ const setupConnectorListeners = (
83
+ connector: BitcoinConnector,
84
+ get: () => Store,
85
+ set: (partial: Partial<Store>) => void
86
+ ) => {
87
+ // Clean up any existing listeners first
88
+ cleanupListeners();
89
+
90
+ // Subscribe to account changes
91
+ unsubscribeAccounts = connector.onAccountsChanged((accounts) => {
92
+ const currentState = get();
93
+
94
+ // Only handle if this connector is still the active one
95
+ if (currentState.connector?.id !== connector.id) {
96
+ return;
97
+ }
98
+
99
+ if (accounts.length === 0) {
100
+ // Wallet disconnected from extension side
101
+ // Use catch to handle any errors silently
102
+ get().disconnect().catch(() => {
103
+ // Disconnect failed, but we should still clean up state
104
+ set({
105
+ status: 'disconnected',
106
+ account: null,
107
+ connector: null,
108
+ connectorId: null,
109
+ error: null,
110
+ });
111
+ });
112
+ } else if (accounts[0]) {
113
+ set({ account: accounts[0] });
114
+ }
115
+ });
116
+
117
+ // Subscribe to network changes
118
+ unsubscribeNetwork = connector.onNetworkChanged((network) => {
119
+ const currentState = get();
120
+
121
+ // Only handle if this connector is still the active one
122
+ if (currentState.connector?.id !== connector.id) {
123
+ return;
124
+ }
125
+
126
+ set({ network });
127
+ });
128
+ };
129
+
130
+ // Define persist options with proper typing
131
+ const persistOptions: PersistOptions<Store, PersistedState> = {
132
+ name: config.storageKey,
133
+ storage: config.storage
134
+ ? {
135
+ getItem: (name) => {
136
+ const value = config.storage?.getItem(name);
137
+ if (!value) return null;
138
+ try {
139
+ return JSON.parse(value) as { state: PersistedState };
140
+ } catch {
141
+ return null;
142
+ }
143
+ },
144
+ setItem: (name, value) => {
145
+ config.storage?.setItem(name, JSON.stringify(value));
146
+ },
147
+ removeItem: (name) => {
148
+ config.storage?.removeItem(name);
149
+ },
150
+ }
151
+ : undefined,
152
+ // Only persist connectorId and network
153
+ partialize: (state): PersistedState => ({
154
+ connectorId: state.connectorId,
155
+ network: state.network,
156
+ }),
157
+ };
158
+
159
+ const store = createZustandStore<Store>()(
160
+ subscribeWithSelector(
161
+ persist(
162
+ (set, get) => ({
163
+ // Initial state
164
+ ...initialState,
165
+
166
+ // Actions
167
+ connect: async (
168
+ connector: BitcoinConnector,
169
+ networkParam?: BitcoinNetwork
170
+ ) => {
171
+ const { status, network: storeNetwork } = get();
172
+ const network = networkParam ?? storeNetwork;
173
+
174
+ // Prevent duplicate connection attempts
175
+ if (status === 'connecting') {
176
+ throw new Error('Connection already in progress');
177
+ }
178
+
179
+ // Clean up any existing connection first
180
+ cleanupListeners();
181
+
182
+ set({ status: 'connecting', error: null, network });
183
+
184
+ try {
185
+ // Check if wallet is ready
186
+ if (!connector.ready) {
187
+ throw new Error(`${connector.name} wallet is not available`);
188
+ }
189
+
190
+ // Connect to wallet with network
191
+ const account = await connector.connect(network);
192
+
193
+ set({
194
+ status: 'connected',
195
+ account,
196
+ connector,
197
+ connectorId: connector.id,
198
+ error: null,
199
+ });
200
+
201
+ // Setup event listeners
202
+ setupConnectorListeners(connector, get, set);
203
+
204
+ return account;
205
+ } catch (error) {
206
+ const err = error instanceof Error ? error : new Error(String(error));
207
+ set({
208
+ status: 'disconnected',
209
+ error: err,
210
+ });
211
+ throw error;
212
+ }
213
+ },
214
+
215
+ disconnect: async () => {
216
+ const { connector } = get();
217
+
218
+ // Clean up listeners first
219
+ cleanupListeners();
220
+
221
+ try {
222
+ if (connector) {
223
+ await connector.disconnect();
224
+ }
225
+ } catch {
226
+ // Ignore disconnect errors (wallet might already be locked)
227
+ } finally {
228
+ set({
229
+ status: 'disconnected',
230
+ account: null,
231
+ connector: null,
232
+ connectorId: null,
233
+ error: null,
234
+ });
235
+ }
236
+ },
237
+
238
+ reconnect: async (connectors: BitcoinConnector[]) => {
239
+ const { connectorId, status, network } = get();
240
+
241
+ // Only reconnect if we have a saved connector and not already connected
242
+ if (!connectorId || status === 'connected') {
243
+ return;
244
+ }
245
+
246
+ // Find the connector
247
+ const connector = connectors.find((c) => c.id === connectorId);
248
+ if (!connector?.ready) {
249
+ // Clear saved connector if not available
250
+ set({ connectorId: null });
251
+ return;
252
+ }
253
+
254
+ set({ status: 'reconnecting' });
255
+
256
+ try {
257
+ const account = await connector.connect(network);
258
+
259
+ set({
260
+ status: 'connected',
261
+ account,
262
+ connector,
263
+ error: null,
264
+ });
265
+
266
+ // Setup event listeners (reusing shared function)
267
+ setupConnectorListeners(connector, get, set);
268
+ } catch {
269
+ // Reconnect failed silently, clear state
270
+ set({
271
+ status: 'disconnected',
272
+ connectorId: null,
273
+ });
274
+ }
275
+ },
276
+
277
+ setAccount: (account) => set({ account }),
278
+
279
+ setStatus: (status) => set({ status }),
280
+
281
+ setError: (error) => set({ error }),
282
+
283
+ reset: () => {
284
+ cleanupListeners();
285
+ set(initialState);
286
+ },
287
+ }),
288
+ persistOptions
289
+ )
290
+ )
291
+ );
292
+
293
+ return store;
294
+ }
295
+
296
+ export type BtcWalletStore = ReturnType<typeof createStore>;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Bitcoin address types supported by wallets
3
+ */
4
+ export type AddressType = 'legacy' | 'nested-segwit' | 'segwit' | 'taproot';
5
+
6
+ /**
7
+ * Wallet account information returned from connection
8
+ */
9
+ export type WalletAccount = {
10
+ /** Bitcoin address */
11
+ address: string;
12
+ /** Hex-encoded public key */
13
+ publicKey: string;
14
+ /** Address format type */
15
+ type: AddressType;
16
+ };
17
+
18
+ /**
19
+ * Multi-address account for wallets supporting both payment and ordinals addresses
20
+ */
21
+ export type MultiAddressAccount = {
22
+ /** Payment address for BTC transfers (typically segwit) */
23
+ payment: WalletAccount;
24
+ /** Ordinals address for Ordinals/Runes (typically taproot) */
25
+ ordinals: WalletAccount;
26
+ };
@@ -0,0 +1,28 @@
1
+ import type { BitcoinConnector } from './connector';
2
+
3
+ /**
4
+ * Configuration for otx-btc-wallet
5
+ */
6
+ export type BtcWalletConfig = {
7
+ /** Array of wallet connectors to support */
8
+ connectors: BitcoinConnector[];
9
+
10
+ /** Whether to auto-connect on page load (default: true) */
11
+ autoConnect?: boolean;
12
+
13
+ /** Storage implementation for persistence (default: localStorage) */
14
+ storage?: Storage;
15
+
16
+ /** Key to use for storage (default: 'optimex-btc-wallet.store') */
17
+ storageKey?: string;
18
+ };
19
+
20
+ /**
21
+ * Internal config with resolved defaults
22
+ */
23
+ export type Config = {
24
+ connectors: BitcoinConnector[];
25
+ autoConnect: boolean;
26
+ storage: Storage | null;
27
+ storageKey: string;
28
+ };
@@ -0,0 +1,109 @@
1
+ import type { WalletAccount } from './account';
2
+ import type { BitcoinNetwork } from './network';
3
+ import type { SignPsbtOptions } from './psbt';
4
+
5
+ /**
6
+ * Interface that all wallet connectors must implement
7
+ */
8
+ export type NetworkType = 'mainnet' | 'testnet' | 'testnet4' | 'signet';
9
+
10
+ export interface BitcoinConnector {
11
+ /** Unique identifier for the connector (e.g., 'unisat') */
12
+ readonly id: string;
13
+
14
+ /** Display name for the wallet (e.g., 'Unisat Wallet') */
15
+ readonly name: string;
16
+
17
+ /** Base64 encoded icon or URL to wallet icon */
18
+ readonly icon: string;
19
+
20
+ /** Whether the wallet extension is detected and ready */
21
+ ready: boolean;
22
+
23
+ /**
24
+ * Connect to the wallet
25
+ * @returns The connected wallet account
26
+ * @throws ConnectionError if connection fails
27
+ * @throws UserRejectedError if user rejects connection
28
+ */
29
+ connect(network?: NetworkType): Promise<WalletAccount>;
30
+
31
+ /**
32
+ * Disconnect from the wallet
33
+ */
34
+ disconnect(): Promise<void>;
35
+
36
+ /**
37
+ * Get all accounts from the wallet
38
+ * @returns Array of wallet accounts
39
+ */
40
+ getAccounts(): Promise<WalletAccount[]>;
41
+
42
+ /**
43
+ * Sign an arbitrary message
44
+ * @param message - Message to sign
45
+ * @returns Signature string
46
+ * @throws SigningError if signing fails
47
+ * @throws UserRejectedError if user rejects signing
48
+ */
49
+ signMessage(message: string): Promise<string>;
50
+
51
+ /**
52
+ * Sign a PSBT (Partially Signed Bitcoin Transaction)
53
+ * @param psbtHex - PSBT in hex format
54
+ * @param options - Signing options
55
+ * @returns Signed PSBT in hex format
56
+ * @throws SigningError if signing fails
57
+ * @throws UserRejectedError if user rejects signing
58
+ */
59
+ signPsbt(psbtHex: string, options?: SignPsbtOptions): Promise<string>;
60
+
61
+ /**
62
+ * Sign multiple PSBTs (optional, not all wallets support this)
63
+ * @param psbtHexs - Array of PSBTs in hex format
64
+ * @param options - Signing options
65
+ * @returns Array of signed PSBTs in hex format
66
+ */
67
+ signPsbts?(psbtHexs: string[], options?: SignPsbtOptions): Promise<string[]>;
68
+
69
+ /**
70
+ * Send a Bitcoin transaction
71
+ * @param to - Recipient address
72
+ * @param satoshis - Amount in satoshis
73
+ * @returns Transaction ID
74
+ * @throws SigningError if transaction fails
75
+ * @throws UserRejectedError if user rejects transaction
76
+ */
77
+ sendTransaction(to: string, satoshis: number): Promise<string>;
78
+
79
+ /**
80
+ * Get current network from the wallet
81
+ * @returns Current network
82
+ */
83
+ getNetwork(): Promise<BitcoinNetwork>;
84
+
85
+ /**
86
+ * Switch to a different network (optional, not all wallets support this)
87
+ * @param network - Network to switch to
88
+ * @throws NetworkError if switch fails
89
+ */
90
+ switchNetwork?(network: BitcoinNetwork): Promise<void>;
91
+
92
+ /**
93
+ * Subscribe to account changes
94
+ * @param callback - Called when accounts change
95
+ * @returns Unsubscribe function
96
+ */
97
+ onAccountsChanged(
98
+ callback: (accounts: WalletAccount[]) => void
99
+ ): () => void;
100
+
101
+ /**
102
+ * Subscribe to network changes
103
+ * @param callback - Called when network changes
104
+ * @returns Unsubscribe function
105
+ */
106
+ onNetworkChanged(
107
+ callback: (network: BitcoinNetwork) => void
108
+ ): () => void;
109
+ }
@@ -0,0 +1,12 @@
1
+ // otx-btc-wallet-core types
2
+
3
+ export type { BitcoinConnector, NetworkType } from './connector';
4
+ export type {
5
+ WalletAccount,
6
+ AddressType,
7
+ MultiAddressAccount,
8
+ } from './account';
9
+ export type { BitcoinNetwork } from './network';
10
+ export type { SignPsbtOptions, SignInput } from './psbt';
11
+ export type { BtcWalletConfig, Config } from './config';
12
+ export type { BtcWalletState, ConnectionStatus } from './state';
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Supported Bitcoin networks
3
+ */
4
+ export type BitcoinNetwork = 'mainnet' | 'testnet' | 'testnet4' | 'signet';
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Options for signing a PSBT
3
+ */
4
+ export type SignPsbtOptions = {
5
+ /** Auto-finalize inputs after signing (default: true) */
6
+ autoFinalize?: boolean;
7
+ /** Specific inputs to sign */
8
+ toSignInputs?: SignInput[];
9
+ /** Broadcast transaction after signing (where supported) */
10
+ broadcast?: boolean;
11
+ };
12
+
13
+ /**
14
+ * Configuration for signing a specific input
15
+ */
16
+ export type SignInput = {
17
+ /** Input index to sign */
18
+ index: number;
19
+ /** Address to use for signing */
20
+ address?: string;
21
+ /** Public key to use for signing */
22
+ publicKey?: string;
23
+ /** Allowed sighash types */
24
+ sighashTypes?: number[];
25
+ /** Disable tweak signer for taproot key-path spends */
26
+ disableTweakSigner?: boolean;
27
+ };
@@ -0,0 +1,35 @@
1
+ import type { BitcoinConnector } from './connector';
2
+ import type { WalletAccount } from './account';
3
+ import type { BitcoinNetwork } from './network';
4
+
5
+ /**
6
+ * Connection status states
7
+ */
8
+ export type ConnectionStatus =
9
+ | 'disconnected'
10
+ | 'connecting'
11
+ | 'connected'
12
+ | 'reconnecting';
13
+
14
+ /**
15
+ * Store state shape
16
+ */
17
+ export type BtcWalletState = {
18
+ /** Current connection status */
19
+ status: ConnectionStatus;
20
+
21
+ /** Connected account (null if disconnected) */
22
+ account: WalletAccount | null;
23
+
24
+ /** Active connector (null if disconnected) */
25
+ connector: BitcoinConnector | null;
26
+
27
+ /** Connected connector ID (for persistence) */
28
+ connectorId: string | null;
29
+
30
+ /** Current network */
31
+ network: BitcoinNetwork;
32
+
33
+ /** Last error (null if no error) */
34
+ error: Error | null;
35
+ };
@@ -0,0 +1 @@
1
+ export * from './psbt';