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 @@
1
+ {"version":3,"sources":["../src/store/index.ts","../src/createConfig.ts","../src/utils/psbt.ts"],"names":[],"mappings":"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","sourcesContent":["import { createStore as createZustandStore } from 'zustand/vanilla';\nimport { persist, subscribeWithSelector } from 'zustand/middleware';\nimport type { PersistOptions } from 'zustand/middleware';\nimport type { BitcoinConnector } from '../types/connector';\nimport type { WalletAccount } from '../types/account';\nimport type { BtcWalletState, ConnectionStatus } from '../types/state';\nimport type { BitcoinNetwork } from '../types/network';\nimport type { Config } from '../types/config';\n\n/**\n * Store actions\n */\nexport type StoreActions = {\n /** Connect to a wallet */\n connect: (\n connector: BitcoinConnector,\n network?: BitcoinNetwork\n ) => Promise<WalletAccount>;\n\n /** Disconnect from current wallet */\n disconnect: () => Promise<void>;\n\n /** Reconnect to previously connected wallet */\n reconnect: (connectors: BitcoinConnector[]) => Promise<void>;\n\n /** Set account */\n setAccount: (account: WalletAccount | null) => void;\n\n /** Set connection status */\n setStatus: (status: ConnectionStatus) => void;\n\n /** Set error */\n setError: (error: Error | null) => void;\n\n /** Reset store to initial state */\n reset: () => void;\n};\n\nexport type Store = BtcWalletState & StoreActions;\n\n/** Persisted state shape - only these fields are saved to storage */\ntype PersistedState = {\n connectorId: string | null;\n network: string;\n};\n\nconst initialState: BtcWalletState = {\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n network: 'mainnet',\n error: null,\n};\n\n/**\n * Create the btc-wallet store\n */\nexport function createStore(config: Config) {\n // Track cleanup functions for event listeners to prevent memory leaks\n let unsubscribeAccounts: (() => void) | null = null;\n let unsubscribeNetwork: (() => void) | null = null;\n\n /**\n * Cleanup all event listeners\n */\n const cleanupListeners = () => {\n if (unsubscribeAccounts) {\n unsubscribeAccounts();\n unsubscribeAccounts = null;\n }\n if (unsubscribeNetwork) {\n unsubscribeNetwork();\n unsubscribeNetwork = null;\n }\n };\n\n /**\n * Setup event listeners for a connector\n * This is shared between connect and reconnect to avoid code duplication\n */\n const setupConnectorListeners = (\n connector: BitcoinConnector,\n get: () => Store,\n set: (partial: Partial<Store>) => void\n ) => {\n // Clean up any existing listeners first\n cleanupListeners();\n\n // Subscribe to account changes\n unsubscribeAccounts = connector.onAccountsChanged((accounts) => {\n const currentState = get();\n\n // Only handle if this connector is still the active one\n if (currentState.connector?.id !== connector.id) {\n return;\n }\n\n if (accounts.length === 0) {\n // Wallet disconnected from extension side\n // Use catch to handle any errors silently\n get().disconnect().catch(() => {\n // Disconnect failed, but we should still clean up state\n set({\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n error: null,\n });\n });\n } else if (accounts[0]) {\n set({ account: accounts[0] });\n }\n });\n\n // Subscribe to network changes\n unsubscribeNetwork = connector.onNetworkChanged((network) => {\n const currentState = get();\n\n // Only handle if this connector is still the active one\n if (currentState.connector?.id !== connector.id) {\n return;\n }\n\n set({ network });\n });\n };\n\n // Define persist options with proper typing\n const persistOptions: PersistOptions<Store, PersistedState> = {\n name: config.storageKey,\n storage: config.storage\n ? {\n getItem: (name) => {\n const value = config.storage?.getItem(name);\n if (!value) return null;\n try {\n return JSON.parse(value) as { state: PersistedState };\n } catch {\n return null;\n }\n },\n setItem: (name, value) => {\n config.storage?.setItem(name, JSON.stringify(value));\n },\n removeItem: (name) => {\n config.storage?.removeItem(name);\n },\n }\n : undefined,\n // Only persist connectorId and network\n partialize: (state): PersistedState => ({\n connectorId: state.connectorId,\n network: state.network,\n }),\n };\n\n const store = createZustandStore<Store>()(\n subscribeWithSelector(\n persist(\n (set, get) => ({\n // Initial state\n ...initialState,\n\n // Actions\n connect: async (\n connector: BitcoinConnector,\n networkParam?: BitcoinNetwork\n ) => {\n const { status, network: storeNetwork } = get();\n const network = networkParam ?? storeNetwork;\n\n // Prevent duplicate connection attempts\n if (status === 'connecting') {\n throw new Error('Connection already in progress');\n }\n\n // Clean up any existing connection first\n cleanupListeners();\n\n set({ status: 'connecting', error: null, network });\n\n try {\n // Check if wallet is ready\n if (!connector.ready) {\n throw new Error(`${connector.name} wallet is not available`);\n }\n\n // Connect to wallet with network\n const account = await connector.connect(network);\n\n set({\n status: 'connected',\n account,\n connector,\n connectorId: connector.id,\n error: null,\n });\n\n // Setup event listeners\n setupConnectorListeners(connector, get, set);\n\n return account;\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n set({\n status: 'disconnected',\n error: err,\n });\n throw error;\n }\n },\n\n disconnect: async () => {\n const { connector } = get();\n\n // Clean up listeners first\n cleanupListeners();\n\n try {\n if (connector) {\n await connector.disconnect();\n }\n } catch {\n // Ignore disconnect errors (wallet might already be locked)\n } finally {\n set({\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n error: null,\n });\n }\n },\n\n reconnect: async (connectors: BitcoinConnector[]) => {\n const { connectorId, status, network } = get();\n\n // Only reconnect if we have a saved connector and not already connected\n if (!connectorId || status === 'connected') {\n return;\n }\n\n // Find the connector\n const connector = connectors.find((c) => c.id === connectorId);\n if (!connector?.ready) {\n // Clear saved connector if not available\n set({ connectorId: null });\n return;\n }\n\n set({ status: 'reconnecting' });\n\n try {\n const account = await connector.connect(network);\n\n set({\n status: 'connected',\n account,\n connector,\n error: null,\n });\n\n // Setup event listeners (reusing shared function)\n setupConnectorListeners(connector, get, set);\n } catch {\n // Reconnect failed silently, clear state\n set({\n status: 'disconnected',\n connectorId: null,\n });\n }\n },\n\n setAccount: (account) => set({ account }),\n\n setStatus: (status) => set({ status }),\n\n setError: (error) => set({ error }),\n\n reset: () => {\n cleanupListeners();\n set(initialState);\n },\n }),\n persistOptions\n )\n )\n );\n\n return store;\n}\n\nexport type BtcWalletStore = ReturnType<typeof createStore>;\n","import type { BtcWalletConfig, Config } from './types/config';\nimport { createStore, type BtcWalletStore } from './store';\n\n/**\n * Resolved config with store instance\n */\nexport type ResolvedConfig = Config & {\n store: BtcWalletStore;\n};\n\n/**\n * Create otx-btc-wallet configuration\n *\n * @example\n * ```ts\n * import { createConfig } from 'otx-btc-wallet-core';\n * import { UnisatConnector } from 'otx-btc-wallet-connectors/unisat';\n *\n * const config = createConfig({\n * connectors: [new UnisatConnector()],\n * autoConnect: true,\n * });\n * ```\n */\nexport function createConfig(options: BtcWalletConfig): ResolvedConfig {\n // Validate connectors\n if (!options.connectors || options.connectors.length === 0) {\n throw new Error('At least one connector is required');\n }\n\n // Check for duplicate connector IDs\n const connectorIds = new Set<string>();\n for (const connector of options.connectors) {\n if (connectorIds.has(connector.id)) {\n throw new Error(`Duplicate connector ID: ${connector.id}`);\n }\n connectorIds.add(connector.id);\n }\n\n // Resolve defaults\n const config: Config = {\n connectors: options.connectors,\n autoConnect: options.autoConnect ?? true,\n storage: getStorage(options.storage),\n storageKey: options.storageKey ?? 'optimex-btc-wallet.store',\n };\n\n // Create store\n const store = createStore(config);\n\n // Auto-connect if enabled\n if (config.autoConnect) {\n // Use setTimeout to ensure this runs after store is fully initialized\n setTimeout(() => {\n void store.getState().reconnect(config.connectors);\n }, 0);\n }\n\n return {\n ...config,\n store,\n };\n}\n\n/**\n * Get storage implementation\n */\nfunction getStorage(storage?: Storage): Storage | null {\n // If explicitly provided, use it\n if (storage !== undefined) {\n return storage;\n }\n\n // Default to localStorage if available\n if (typeof window !== 'undefined' && window.localStorage) {\n return window.localStorage;\n }\n\n return null;\n}\n","/**\n * PSBT (Partially Signed Bitcoin Transaction) utilities\n */\n\n/**\n * Convert PSBT hex to base64\n */\nexport function psbtHexToBase64(hex: string): string {\n // Remove any whitespace\n const cleanHex = hex.replace(/\\s/g, '');\n\n // Validate hex string\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\n throw new Error('Invalid hex string');\n }\n\n if (cleanHex.length % 2 !== 0) {\n throw new Error('Hex string must have even length');\n }\n\n // Convert hex to bytes then to base64\n const bytes = new Uint8Array(cleanHex.length / 2);\n for (let i = 0; i < cleanHex.length; i += 2) {\n bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);\n }\n\n // Use Buffer in Node.js or btoa in browser\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(bytes).toString('base64');\n }\n\n // Browser fallback\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary);\n}\n\n/**\n * Convert PSBT base64 to hex\n */\nexport function psbtBase64ToHex(base64: string): string {\n // Decode base64 to bytes\n let bytes: Uint8Array;\n\n if (typeof Buffer !== 'undefined') {\n bytes = Buffer.from(base64, 'base64');\n } else {\n // Browser fallback\n const binary = atob(base64);\n bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n }\n\n // Convert bytes to hex\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Validate PSBT hex format\n * PSBT magic bytes: 0x70736274ff (psbt\\xff)\n */\nexport function isValidPsbtHex(hex: string): boolean {\n const cleanHex = hex.replace(/\\s/g, '').toLowerCase();\n\n // Check minimum length (magic bytes = 5 bytes = 10 hex chars)\n if (cleanHex.length < 10) {\n return false;\n }\n\n // Check magic bytes\n return cleanHex.startsWith('70736274ff');\n}\n\n/**\n * Validate PSBT base64 format\n */\nexport function isValidPsbtBase64(base64: string): boolean {\n try {\n const hex = psbtBase64ToHex(base64);\n return isValidPsbtHex(hex);\n } catch {\n return false;\n }\n}\n\n/**\n * Extract basic info from PSBT hex (without full parsing)\n * This is a lightweight check - for full parsing use a library like bitcoinjs-lib\n */\nexport function getPsbtInfo(psbtHex: string): {\n isValid: boolean;\n version: number | null;\n inputCount: number | null;\n outputCount: number | null;\n} {\n if (!isValidPsbtHex(psbtHex)) {\n return {\n isValid: false,\n version: null,\n inputCount: null,\n outputCount: null,\n };\n }\n\n // PSBT structure is complex, return basic validation only\n // For full parsing, use bitcoinjs-lib or similar\n return {\n isValid: true,\n version: null, // Would need full parsing\n inputCount: null, // Would need full parsing\n outputCount: null, // Would need full parsing\n };\n}\n\n/**\n * Combine multiple signed PSBTs into one\n * Note: This is a basic implementation. For complex cases, use bitcoinjs-lib.\n *\n * @param psbts - Array of PSBT hex strings to combine\n * @returns Combined PSBT hex\n */\nexport function combinePsbts(psbts: string[]): string {\n if (psbts.length === 0) {\n throw new Error('No PSBTs provided');\n }\n\n if (psbts.length === 1) {\n return psbts[0]!;\n }\n\n // For proper PSBT combination, you need bitcoinjs-lib\n // This is just a placeholder that returns the first PSBT\n // In production, use: bitcoin.Psbt.combine([psbt1, psbt2, ...])\n console.warn(\n 'combinePsbts: For proper PSBT combination, use bitcoinjs-lib. ' +\n 'This function returns the first PSBT as a fallback.'\n );\n\n return psbts[0]!;\n}\n\n/**\n * Sighash types for Bitcoin transactions\n */\nexport const SighashType = {\n ALL: 0x01,\n NONE: 0x02,\n SINGLE: 0x03,\n ANYONECANPAY: 0x80,\n ALL_ANYONECANPAY: 0x81,\n NONE_ANYONECANPAY: 0x82,\n SINGLE_ANYONECANPAY: 0x83,\n} as const;\n\nexport type SighashType = (typeof SighashType)[keyof typeof SighashType];\n\n/**\n * Get human-readable name for sighash type\n */\nexport function getSighashTypeName(sighash: number): string {\n switch (sighash) {\n case SighashType.ALL:\n return 'SIGHASH_ALL';\n case SighashType.NONE:\n return 'SIGHASH_NONE';\n case SighashType.SINGLE:\n return 'SIGHASH_SINGLE';\n case SighashType.ANYONECANPAY:\n return 'SIGHASH_ANYONECANPAY';\n case SighashType.ALL_ANYONECANPAY:\n return 'SIGHASH_ALL|ANYONECANPAY';\n case SighashType.NONE_ANYONECANPAY:\n return 'SIGHASH_NONE|ANYONECANPAY';\n case SighashType.SINGLE_ANYONECANPAY:\n return 'SIGHASH_SINGLE|ANYONECANPAY';\n default:\n return `UNKNOWN(${sighash})`;\n }\n}\n"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,331 @@
1
+ import { createStore as createStore$1 } from 'zustand/vanilla';
2
+ import { subscribeWithSelector, persist } from 'zustand/middleware';
3
+
4
+ // src/store/index.ts
5
+ var initialState = {
6
+ status: "disconnected",
7
+ account: null,
8
+ connector: null,
9
+ connectorId: null,
10
+ network: "mainnet",
11
+ error: null
12
+ };
13
+ function createStore(config) {
14
+ let unsubscribeAccounts = null;
15
+ let unsubscribeNetwork = null;
16
+ const cleanupListeners = () => {
17
+ if (unsubscribeAccounts) {
18
+ unsubscribeAccounts();
19
+ unsubscribeAccounts = null;
20
+ }
21
+ if (unsubscribeNetwork) {
22
+ unsubscribeNetwork();
23
+ unsubscribeNetwork = null;
24
+ }
25
+ };
26
+ const setupConnectorListeners = (connector, get, set) => {
27
+ cleanupListeners();
28
+ unsubscribeAccounts = connector.onAccountsChanged((accounts) => {
29
+ const currentState = get();
30
+ if (currentState.connector?.id !== connector.id) {
31
+ return;
32
+ }
33
+ if (accounts.length === 0) {
34
+ get().disconnect().catch(() => {
35
+ set({
36
+ status: "disconnected",
37
+ account: null,
38
+ connector: null,
39
+ connectorId: null,
40
+ error: null
41
+ });
42
+ });
43
+ } else if (accounts[0]) {
44
+ set({ account: accounts[0] });
45
+ }
46
+ });
47
+ unsubscribeNetwork = connector.onNetworkChanged((network) => {
48
+ const currentState = get();
49
+ if (currentState.connector?.id !== connector.id) {
50
+ return;
51
+ }
52
+ set({ network });
53
+ });
54
+ };
55
+ const persistOptions = {
56
+ name: config.storageKey,
57
+ storage: config.storage ? {
58
+ getItem: (name) => {
59
+ const value = config.storage?.getItem(name);
60
+ if (!value)
61
+ return null;
62
+ try {
63
+ return JSON.parse(value);
64
+ } catch {
65
+ return null;
66
+ }
67
+ },
68
+ setItem: (name, value) => {
69
+ config.storage?.setItem(name, JSON.stringify(value));
70
+ },
71
+ removeItem: (name) => {
72
+ config.storage?.removeItem(name);
73
+ }
74
+ } : void 0,
75
+ // Only persist connectorId and network
76
+ partialize: (state) => ({
77
+ connectorId: state.connectorId,
78
+ network: state.network
79
+ })
80
+ };
81
+ const store = createStore$1()(
82
+ subscribeWithSelector(
83
+ persist(
84
+ (set, get) => ({
85
+ // Initial state
86
+ ...initialState,
87
+ // Actions
88
+ connect: async (connector, networkParam) => {
89
+ const { status, network: storeNetwork } = get();
90
+ const network = networkParam ?? storeNetwork;
91
+ if (status === "connecting") {
92
+ throw new Error("Connection already in progress");
93
+ }
94
+ cleanupListeners();
95
+ set({ status: "connecting", error: null, network });
96
+ try {
97
+ if (!connector.ready) {
98
+ throw new Error(`${connector.name} wallet is not available`);
99
+ }
100
+ const account = await connector.connect(network);
101
+ set({
102
+ status: "connected",
103
+ account,
104
+ connector,
105
+ connectorId: connector.id,
106
+ error: null
107
+ });
108
+ setupConnectorListeners(connector, get, set);
109
+ return account;
110
+ } catch (error) {
111
+ const err = error instanceof Error ? error : new Error(String(error));
112
+ set({
113
+ status: "disconnected",
114
+ error: err
115
+ });
116
+ throw error;
117
+ }
118
+ },
119
+ disconnect: async () => {
120
+ const { connector } = get();
121
+ cleanupListeners();
122
+ try {
123
+ if (connector) {
124
+ await connector.disconnect();
125
+ }
126
+ } catch {
127
+ } finally {
128
+ set({
129
+ status: "disconnected",
130
+ account: null,
131
+ connector: null,
132
+ connectorId: null,
133
+ error: null
134
+ });
135
+ }
136
+ },
137
+ reconnect: async (connectors) => {
138
+ const { connectorId, status, network } = get();
139
+ if (!connectorId || status === "connected") {
140
+ return;
141
+ }
142
+ const connector = connectors.find((c) => c.id === connectorId);
143
+ if (!connector?.ready) {
144
+ set({ connectorId: null });
145
+ return;
146
+ }
147
+ set({ status: "reconnecting" });
148
+ try {
149
+ const account = await connector.connect(network);
150
+ set({
151
+ status: "connected",
152
+ account,
153
+ connector,
154
+ error: null
155
+ });
156
+ setupConnectorListeners(connector, get, set);
157
+ } catch {
158
+ set({
159
+ status: "disconnected",
160
+ connectorId: null
161
+ });
162
+ }
163
+ },
164
+ setAccount: (account) => set({ account }),
165
+ setStatus: (status) => set({ status }),
166
+ setError: (error) => set({ error }),
167
+ reset: () => {
168
+ cleanupListeners();
169
+ set(initialState);
170
+ }
171
+ }),
172
+ persistOptions
173
+ )
174
+ )
175
+ );
176
+ return store;
177
+ }
178
+
179
+ // src/createConfig.ts
180
+ function createConfig(options) {
181
+ if (!options.connectors || options.connectors.length === 0) {
182
+ throw new Error("At least one connector is required");
183
+ }
184
+ const connectorIds = /* @__PURE__ */ new Set();
185
+ for (const connector of options.connectors) {
186
+ if (connectorIds.has(connector.id)) {
187
+ throw new Error(`Duplicate connector ID: ${connector.id}`);
188
+ }
189
+ connectorIds.add(connector.id);
190
+ }
191
+ const config = {
192
+ connectors: options.connectors,
193
+ autoConnect: options.autoConnect ?? true,
194
+ storage: getStorage(options.storage),
195
+ storageKey: options.storageKey ?? "optimex-btc-wallet.store"
196
+ };
197
+ const store = createStore(config);
198
+ if (config.autoConnect) {
199
+ setTimeout(() => {
200
+ void store.getState().reconnect(config.connectors);
201
+ }, 0);
202
+ }
203
+ return {
204
+ ...config,
205
+ store
206
+ };
207
+ }
208
+ function getStorage(storage) {
209
+ if (storage !== void 0) {
210
+ return storage;
211
+ }
212
+ if (typeof window !== "undefined" && window.localStorage) {
213
+ return window.localStorage;
214
+ }
215
+ return null;
216
+ }
217
+
218
+ // src/utils/psbt.ts
219
+ function psbtHexToBase64(hex) {
220
+ const cleanHex = hex.replace(/\s/g, "");
221
+ if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {
222
+ throw new Error("Invalid hex string");
223
+ }
224
+ if (cleanHex.length % 2 !== 0) {
225
+ throw new Error("Hex string must have even length");
226
+ }
227
+ const bytes = new Uint8Array(cleanHex.length / 2);
228
+ for (let i = 0; i < cleanHex.length; i += 2) {
229
+ bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
230
+ }
231
+ if (typeof Buffer !== "undefined") {
232
+ return Buffer.from(bytes).toString("base64");
233
+ }
234
+ let binary = "";
235
+ for (let i = 0; i < bytes.length; i++) {
236
+ binary += String.fromCharCode(bytes[i]);
237
+ }
238
+ return btoa(binary);
239
+ }
240
+ function psbtBase64ToHex(base64) {
241
+ let bytes;
242
+ if (typeof Buffer !== "undefined") {
243
+ bytes = Buffer.from(base64, "base64");
244
+ } else {
245
+ const binary = atob(base64);
246
+ bytes = new Uint8Array(binary.length);
247
+ for (let i = 0; i < binary.length; i++) {
248
+ bytes[i] = binary.charCodeAt(i);
249
+ }
250
+ }
251
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
252
+ }
253
+ function isValidPsbtHex(hex) {
254
+ const cleanHex = hex.replace(/\s/g, "").toLowerCase();
255
+ if (cleanHex.length < 10) {
256
+ return false;
257
+ }
258
+ return cleanHex.startsWith("70736274ff");
259
+ }
260
+ function isValidPsbtBase64(base64) {
261
+ try {
262
+ const hex = psbtBase64ToHex(base64);
263
+ return isValidPsbtHex(hex);
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
268
+ function getPsbtInfo(psbtHex) {
269
+ if (!isValidPsbtHex(psbtHex)) {
270
+ return {
271
+ isValid: false,
272
+ version: null,
273
+ inputCount: null,
274
+ outputCount: null
275
+ };
276
+ }
277
+ return {
278
+ isValid: true,
279
+ version: null,
280
+ // Would need full parsing
281
+ inputCount: null,
282
+ // Would need full parsing
283
+ outputCount: null
284
+ // Would need full parsing
285
+ };
286
+ }
287
+ function combinePsbts(psbts) {
288
+ if (psbts.length === 0) {
289
+ throw new Error("No PSBTs provided");
290
+ }
291
+ if (psbts.length === 1) {
292
+ return psbts[0];
293
+ }
294
+ console.warn(
295
+ "combinePsbts: For proper PSBT combination, use bitcoinjs-lib. This function returns the first PSBT as a fallback."
296
+ );
297
+ return psbts[0];
298
+ }
299
+ var SighashType = {
300
+ ALL: 1,
301
+ NONE: 2,
302
+ SINGLE: 3,
303
+ ANYONECANPAY: 128,
304
+ ALL_ANYONECANPAY: 129,
305
+ NONE_ANYONECANPAY: 130,
306
+ SINGLE_ANYONECANPAY: 131
307
+ };
308
+ function getSighashTypeName(sighash) {
309
+ switch (sighash) {
310
+ case SighashType.ALL:
311
+ return "SIGHASH_ALL";
312
+ case SighashType.NONE:
313
+ return "SIGHASH_NONE";
314
+ case SighashType.SINGLE:
315
+ return "SIGHASH_SINGLE";
316
+ case SighashType.ANYONECANPAY:
317
+ return "SIGHASH_ANYONECANPAY";
318
+ case SighashType.ALL_ANYONECANPAY:
319
+ return "SIGHASH_ALL|ANYONECANPAY";
320
+ case SighashType.NONE_ANYONECANPAY:
321
+ return "SIGHASH_NONE|ANYONECANPAY";
322
+ case SighashType.SINGLE_ANYONECANPAY:
323
+ return "SIGHASH_SINGLE|ANYONECANPAY";
324
+ default:
325
+ return `UNKNOWN(${sighash})`;
326
+ }
327
+ }
328
+
329
+ export { SighashType, combinePsbts, createConfig, createStore, getPsbtInfo, getSighashTypeName, isValidPsbtBase64, isValidPsbtHex, psbtBase64ToHex, psbtHexToBase64 };
330
+ //# sourceMappingURL=out.js.map
331
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/index.ts","../src/createConfig.ts","../src/utils/psbt.ts"],"names":[],"mappings":"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","sourcesContent":["import { createStore as createZustandStore } from 'zustand/vanilla';\nimport { persist, subscribeWithSelector } from 'zustand/middleware';\nimport type { PersistOptions } from 'zustand/middleware';\nimport type { BitcoinConnector } from '../types/connector';\nimport type { WalletAccount } from '../types/account';\nimport type { BtcWalletState, ConnectionStatus } from '../types/state';\nimport type { BitcoinNetwork } from '../types/network';\nimport type { Config } from '../types/config';\n\n/**\n * Store actions\n */\nexport type StoreActions = {\n /** Connect to a wallet */\n connect: (\n connector: BitcoinConnector,\n network?: BitcoinNetwork\n ) => Promise<WalletAccount>;\n\n /** Disconnect from current wallet */\n disconnect: () => Promise<void>;\n\n /** Reconnect to previously connected wallet */\n reconnect: (connectors: BitcoinConnector[]) => Promise<void>;\n\n /** Set account */\n setAccount: (account: WalletAccount | null) => void;\n\n /** Set connection status */\n setStatus: (status: ConnectionStatus) => void;\n\n /** Set error */\n setError: (error: Error | null) => void;\n\n /** Reset store to initial state */\n reset: () => void;\n};\n\nexport type Store = BtcWalletState & StoreActions;\n\n/** Persisted state shape - only these fields are saved to storage */\ntype PersistedState = {\n connectorId: string | null;\n network: string;\n};\n\nconst initialState: BtcWalletState = {\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n network: 'mainnet',\n error: null,\n};\n\n/**\n * Create the btc-wallet store\n */\nexport function createStore(config: Config) {\n // Track cleanup functions for event listeners to prevent memory leaks\n let unsubscribeAccounts: (() => void) | null = null;\n let unsubscribeNetwork: (() => void) | null = null;\n\n /**\n * Cleanup all event listeners\n */\n const cleanupListeners = () => {\n if (unsubscribeAccounts) {\n unsubscribeAccounts();\n unsubscribeAccounts = null;\n }\n if (unsubscribeNetwork) {\n unsubscribeNetwork();\n unsubscribeNetwork = null;\n }\n };\n\n /**\n * Setup event listeners for a connector\n * This is shared between connect and reconnect to avoid code duplication\n */\n const setupConnectorListeners = (\n connector: BitcoinConnector,\n get: () => Store,\n set: (partial: Partial<Store>) => void\n ) => {\n // Clean up any existing listeners first\n cleanupListeners();\n\n // Subscribe to account changes\n unsubscribeAccounts = connector.onAccountsChanged((accounts) => {\n const currentState = get();\n\n // Only handle if this connector is still the active one\n if (currentState.connector?.id !== connector.id) {\n return;\n }\n\n if (accounts.length === 0) {\n // Wallet disconnected from extension side\n // Use catch to handle any errors silently\n get().disconnect().catch(() => {\n // Disconnect failed, but we should still clean up state\n set({\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n error: null,\n });\n });\n } else if (accounts[0]) {\n set({ account: accounts[0] });\n }\n });\n\n // Subscribe to network changes\n unsubscribeNetwork = connector.onNetworkChanged((network) => {\n const currentState = get();\n\n // Only handle if this connector is still the active one\n if (currentState.connector?.id !== connector.id) {\n return;\n }\n\n set({ network });\n });\n };\n\n // Define persist options with proper typing\n const persistOptions: PersistOptions<Store, PersistedState> = {\n name: config.storageKey,\n storage: config.storage\n ? {\n getItem: (name) => {\n const value = config.storage?.getItem(name);\n if (!value) return null;\n try {\n return JSON.parse(value) as { state: PersistedState };\n } catch {\n return null;\n }\n },\n setItem: (name, value) => {\n config.storage?.setItem(name, JSON.stringify(value));\n },\n removeItem: (name) => {\n config.storage?.removeItem(name);\n },\n }\n : undefined,\n // Only persist connectorId and network\n partialize: (state): PersistedState => ({\n connectorId: state.connectorId,\n network: state.network,\n }),\n };\n\n const store = createZustandStore<Store>()(\n subscribeWithSelector(\n persist(\n (set, get) => ({\n // Initial state\n ...initialState,\n\n // Actions\n connect: async (\n connector: BitcoinConnector,\n networkParam?: BitcoinNetwork\n ) => {\n const { status, network: storeNetwork } = get();\n const network = networkParam ?? storeNetwork;\n\n // Prevent duplicate connection attempts\n if (status === 'connecting') {\n throw new Error('Connection already in progress');\n }\n\n // Clean up any existing connection first\n cleanupListeners();\n\n set({ status: 'connecting', error: null, network });\n\n try {\n // Check if wallet is ready\n if (!connector.ready) {\n throw new Error(`${connector.name} wallet is not available`);\n }\n\n // Connect to wallet with network\n const account = await connector.connect(network);\n\n set({\n status: 'connected',\n account,\n connector,\n connectorId: connector.id,\n error: null,\n });\n\n // Setup event listeners\n setupConnectorListeners(connector, get, set);\n\n return account;\n } catch (error) {\n const err = error instanceof Error ? error : new Error(String(error));\n set({\n status: 'disconnected',\n error: err,\n });\n throw error;\n }\n },\n\n disconnect: async () => {\n const { connector } = get();\n\n // Clean up listeners first\n cleanupListeners();\n\n try {\n if (connector) {\n await connector.disconnect();\n }\n } catch {\n // Ignore disconnect errors (wallet might already be locked)\n } finally {\n set({\n status: 'disconnected',\n account: null,\n connector: null,\n connectorId: null,\n error: null,\n });\n }\n },\n\n reconnect: async (connectors: BitcoinConnector[]) => {\n const { connectorId, status, network } = get();\n\n // Only reconnect if we have a saved connector and not already connected\n if (!connectorId || status === 'connected') {\n return;\n }\n\n // Find the connector\n const connector = connectors.find((c) => c.id === connectorId);\n if (!connector?.ready) {\n // Clear saved connector if not available\n set({ connectorId: null });\n return;\n }\n\n set({ status: 'reconnecting' });\n\n try {\n const account = await connector.connect(network);\n\n set({\n status: 'connected',\n account,\n connector,\n error: null,\n });\n\n // Setup event listeners (reusing shared function)\n setupConnectorListeners(connector, get, set);\n } catch {\n // Reconnect failed silently, clear state\n set({\n status: 'disconnected',\n connectorId: null,\n });\n }\n },\n\n setAccount: (account) => set({ account }),\n\n setStatus: (status) => set({ status }),\n\n setError: (error) => set({ error }),\n\n reset: () => {\n cleanupListeners();\n set(initialState);\n },\n }),\n persistOptions\n )\n )\n );\n\n return store;\n}\n\nexport type BtcWalletStore = ReturnType<typeof createStore>;\n","import type { BtcWalletConfig, Config } from './types/config';\nimport { createStore, type BtcWalletStore } from './store';\n\n/**\n * Resolved config with store instance\n */\nexport type ResolvedConfig = Config & {\n store: BtcWalletStore;\n};\n\n/**\n * Create otx-btc-wallet configuration\n *\n * @example\n * ```ts\n * import { createConfig } from 'otx-btc-wallet-core';\n * import { UnisatConnector } from 'otx-btc-wallet-connectors/unisat';\n *\n * const config = createConfig({\n * connectors: [new UnisatConnector()],\n * autoConnect: true,\n * });\n * ```\n */\nexport function createConfig(options: BtcWalletConfig): ResolvedConfig {\n // Validate connectors\n if (!options.connectors || options.connectors.length === 0) {\n throw new Error('At least one connector is required');\n }\n\n // Check for duplicate connector IDs\n const connectorIds = new Set<string>();\n for (const connector of options.connectors) {\n if (connectorIds.has(connector.id)) {\n throw new Error(`Duplicate connector ID: ${connector.id}`);\n }\n connectorIds.add(connector.id);\n }\n\n // Resolve defaults\n const config: Config = {\n connectors: options.connectors,\n autoConnect: options.autoConnect ?? true,\n storage: getStorage(options.storage),\n storageKey: options.storageKey ?? 'optimex-btc-wallet.store',\n };\n\n // Create store\n const store = createStore(config);\n\n // Auto-connect if enabled\n if (config.autoConnect) {\n // Use setTimeout to ensure this runs after store is fully initialized\n setTimeout(() => {\n void store.getState().reconnect(config.connectors);\n }, 0);\n }\n\n return {\n ...config,\n store,\n };\n}\n\n/**\n * Get storage implementation\n */\nfunction getStorage(storage?: Storage): Storage | null {\n // If explicitly provided, use it\n if (storage !== undefined) {\n return storage;\n }\n\n // Default to localStorage if available\n if (typeof window !== 'undefined' && window.localStorage) {\n return window.localStorage;\n }\n\n return null;\n}\n","/**\n * PSBT (Partially Signed Bitcoin Transaction) utilities\n */\n\n/**\n * Convert PSBT hex to base64\n */\nexport function psbtHexToBase64(hex: string): string {\n // Remove any whitespace\n const cleanHex = hex.replace(/\\s/g, '');\n\n // Validate hex string\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\n throw new Error('Invalid hex string');\n }\n\n if (cleanHex.length % 2 !== 0) {\n throw new Error('Hex string must have even length');\n }\n\n // Convert hex to bytes then to base64\n const bytes = new Uint8Array(cleanHex.length / 2);\n for (let i = 0; i < cleanHex.length; i += 2) {\n bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);\n }\n\n // Use Buffer in Node.js or btoa in browser\n if (typeof Buffer !== 'undefined') {\n return Buffer.from(bytes).toString('base64');\n }\n\n // Browser fallback\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return btoa(binary);\n}\n\n/**\n * Convert PSBT base64 to hex\n */\nexport function psbtBase64ToHex(base64: string): string {\n // Decode base64 to bytes\n let bytes: Uint8Array;\n\n if (typeof Buffer !== 'undefined') {\n bytes = Buffer.from(base64, 'base64');\n } else {\n // Browser fallback\n const binary = atob(base64);\n bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n }\n\n // Convert bytes to hex\n return Array.from(bytes)\n .map((b) => b.toString(16).padStart(2, '0'))\n .join('');\n}\n\n/**\n * Validate PSBT hex format\n * PSBT magic bytes: 0x70736274ff (psbt\\xff)\n */\nexport function isValidPsbtHex(hex: string): boolean {\n const cleanHex = hex.replace(/\\s/g, '').toLowerCase();\n\n // Check minimum length (magic bytes = 5 bytes = 10 hex chars)\n if (cleanHex.length < 10) {\n return false;\n }\n\n // Check magic bytes\n return cleanHex.startsWith('70736274ff');\n}\n\n/**\n * Validate PSBT base64 format\n */\nexport function isValidPsbtBase64(base64: string): boolean {\n try {\n const hex = psbtBase64ToHex(base64);\n return isValidPsbtHex(hex);\n } catch {\n return false;\n }\n}\n\n/**\n * Extract basic info from PSBT hex (without full parsing)\n * This is a lightweight check - for full parsing use a library like bitcoinjs-lib\n */\nexport function getPsbtInfo(psbtHex: string): {\n isValid: boolean;\n version: number | null;\n inputCount: number | null;\n outputCount: number | null;\n} {\n if (!isValidPsbtHex(psbtHex)) {\n return {\n isValid: false,\n version: null,\n inputCount: null,\n outputCount: null,\n };\n }\n\n // PSBT structure is complex, return basic validation only\n // For full parsing, use bitcoinjs-lib or similar\n return {\n isValid: true,\n version: null, // Would need full parsing\n inputCount: null, // Would need full parsing\n outputCount: null, // Would need full parsing\n };\n}\n\n/**\n * Combine multiple signed PSBTs into one\n * Note: This is a basic implementation. For complex cases, use bitcoinjs-lib.\n *\n * @param psbts - Array of PSBT hex strings to combine\n * @returns Combined PSBT hex\n */\nexport function combinePsbts(psbts: string[]): string {\n if (psbts.length === 0) {\n throw new Error('No PSBTs provided');\n }\n\n if (psbts.length === 1) {\n return psbts[0]!;\n }\n\n // For proper PSBT combination, you need bitcoinjs-lib\n // This is just a placeholder that returns the first PSBT\n // In production, use: bitcoin.Psbt.combine([psbt1, psbt2, ...])\n console.warn(\n 'combinePsbts: For proper PSBT combination, use bitcoinjs-lib. ' +\n 'This function returns the first PSBT as a fallback.'\n );\n\n return psbts[0]!;\n}\n\n/**\n * Sighash types for Bitcoin transactions\n */\nexport const SighashType = {\n ALL: 0x01,\n NONE: 0x02,\n SINGLE: 0x03,\n ANYONECANPAY: 0x80,\n ALL_ANYONECANPAY: 0x81,\n NONE_ANYONECANPAY: 0x82,\n SINGLE_ANYONECANPAY: 0x83,\n} as const;\n\nexport type SighashType = (typeof SighashType)[keyof typeof SighashType];\n\n/**\n * Get human-readable name for sighash type\n */\nexport function getSighashTypeName(sighash: number): string {\n switch (sighash) {\n case SighashType.ALL:\n return 'SIGHASH_ALL';\n case SighashType.NONE:\n return 'SIGHASH_NONE';\n case SighashType.SINGLE:\n return 'SIGHASH_SINGLE';\n case SighashType.ANYONECANPAY:\n return 'SIGHASH_ANYONECANPAY';\n case SighashType.ALL_ANYONECANPAY:\n return 'SIGHASH_ALL|ANYONECANPAY';\n case SighashType.NONE_ANYONECANPAY:\n return 'SIGHASH_NONE|ANYONECANPAY';\n case SighashType.SINGLE_ANYONECANPAY:\n return 'SIGHASH_SINGLE|ANYONECANPAY';\n default:\n return `UNKNOWN(${sighash})`;\n }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "otx-btc-wallet-core",
3
+ "version": "0.1.0",
4
+ "description": "Core state management and types 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
+ "zustand": "^4.4.7"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.3.2",
26
+ "tsup": "^8.0.1"
27
+ },
28
+ "peerDependencies": {},
29
+ "keywords": [
30
+ "bitcoin",
31
+ "wallet",
32
+ "btc",
33
+ "web3"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsup",
37
+ "dev": "tsup --watch",
38
+ "clean": "rm -rf dist",
39
+ "typecheck": "tsc --noEmit"
40
+ }
41
+ }
@@ -0,0 +1,80 @@
1
+ import type { BtcWalletConfig, Config } from './types/config';
2
+ import { createStore, type BtcWalletStore } from './store';
3
+
4
+ /**
5
+ * Resolved config with store instance
6
+ */
7
+ export type ResolvedConfig = Config & {
8
+ store: BtcWalletStore;
9
+ };
10
+
11
+ /**
12
+ * Create otx-btc-wallet configuration
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { createConfig } from 'otx-btc-wallet-core';
17
+ * import { UnisatConnector } from 'otx-btc-wallet-connectors/unisat';
18
+ *
19
+ * const config = createConfig({
20
+ * connectors: [new UnisatConnector()],
21
+ * autoConnect: true,
22
+ * });
23
+ * ```
24
+ */
25
+ export function createConfig(options: BtcWalletConfig): ResolvedConfig {
26
+ // Validate connectors
27
+ if (!options.connectors || options.connectors.length === 0) {
28
+ throw new Error('At least one connector is required');
29
+ }
30
+
31
+ // Check for duplicate connector IDs
32
+ const connectorIds = new Set<string>();
33
+ for (const connector of options.connectors) {
34
+ if (connectorIds.has(connector.id)) {
35
+ throw new Error(`Duplicate connector ID: ${connector.id}`);
36
+ }
37
+ connectorIds.add(connector.id);
38
+ }
39
+
40
+ // Resolve defaults
41
+ const config: Config = {
42
+ connectors: options.connectors,
43
+ autoConnect: options.autoConnect ?? true,
44
+ storage: getStorage(options.storage),
45
+ storageKey: options.storageKey ?? 'optimex-btc-wallet.store',
46
+ };
47
+
48
+ // Create store
49
+ const store = createStore(config);
50
+
51
+ // Auto-connect if enabled
52
+ if (config.autoConnect) {
53
+ // Use setTimeout to ensure this runs after store is fully initialized
54
+ setTimeout(() => {
55
+ void store.getState().reconnect(config.connectors);
56
+ }, 0);
57
+ }
58
+
59
+ return {
60
+ ...config,
61
+ store,
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Get storage implementation
67
+ */
68
+ function getStorage(storage?: Storage): Storage | null {
69
+ // If explicitly provided, use it
70
+ if (storage !== undefined) {
71
+ return storage;
72
+ }
73
+
74
+ // Default to localStorage if available
75
+ if (typeof window !== 'undefined' && window.localStorage) {
76
+ return window.localStorage;
77
+ }
78
+
79
+ return null;
80
+ }
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ // otx-btc-wallet-core - Core state management and types
2
+
3
+ // Types
4
+ export type {
5
+ BitcoinConnector,
6
+ WalletAccount,
7
+ AddressType,
8
+ MultiAddressAccount,
9
+ BitcoinNetwork,
10
+ SignPsbtOptions,
11
+ SignInput,
12
+ BtcWalletConfig,
13
+ Config,
14
+ BtcWalletState,
15
+ ConnectionStatus,
16
+ } from './types';
17
+
18
+
19
+ // Config
20
+ export { createConfig, type ResolvedConfig } from './createConfig';
21
+
22
+ // Store
23
+ export {
24
+ createStore,
25
+ type BtcWalletStore,
26
+ type Store,
27
+ type StoreActions,
28
+ } from './store';
29
+
30
+ // PSBT Utilities
31
+ export {
32
+ psbtHexToBase64,
33
+ psbtBase64ToHex,
34
+ isValidPsbtHex,
35
+ isValidPsbtBase64,
36
+ getPsbtInfo,
37
+ combinePsbts,
38
+ SighashType,
39
+ getSighashTypeName,
40
+ } from './utils/psbt';