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,320 @@
1
+ import * as zustand_vanilla from 'zustand/vanilla';
2
+ import { PersistOptions } from 'zustand/middleware';
3
+
4
+ /**
5
+ * Bitcoin address types supported by wallets
6
+ */
7
+ type AddressType = 'legacy' | 'nested-segwit' | 'segwit' | 'taproot';
8
+ /**
9
+ * Wallet account information returned from connection
10
+ */
11
+ type WalletAccount = {
12
+ /** Bitcoin address */
13
+ address: string;
14
+ /** Hex-encoded public key */
15
+ publicKey: string;
16
+ /** Address format type */
17
+ type: AddressType;
18
+ };
19
+ /**
20
+ * Multi-address account for wallets supporting both payment and ordinals addresses
21
+ */
22
+ type MultiAddressAccount = {
23
+ /** Payment address for BTC transfers (typically segwit) */
24
+ payment: WalletAccount;
25
+ /** Ordinals address for Ordinals/Runes (typically taproot) */
26
+ ordinals: WalletAccount;
27
+ };
28
+
29
+ /**
30
+ * Supported Bitcoin networks
31
+ */
32
+ type BitcoinNetwork = 'mainnet' | 'testnet' | 'testnet4' | 'signet';
33
+
34
+ /**
35
+ * Options for signing a PSBT
36
+ */
37
+ type SignPsbtOptions = {
38
+ /** Auto-finalize inputs after signing (default: true) */
39
+ autoFinalize?: boolean;
40
+ /** Specific inputs to sign */
41
+ toSignInputs?: SignInput[];
42
+ /** Broadcast transaction after signing (where supported) */
43
+ broadcast?: boolean;
44
+ };
45
+ /**
46
+ * Configuration for signing a specific input
47
+ */
48
+ type SignInput = {
49
+ /** Input index to sign */
50
+ index: number;
51
+ /** Address to use for signing */
52
+ address?: string;
53
+ /** Public key to use for signing */
54
+ publicKey?: string;
55
+ /** Allowed sighash types */
56
+ sighashTypes?: number[];
57
+ /** Disable tweak signer for taproot key-path spends */
58
+ disableTweakSigner?: boolean;
59
+ };
60
+
61
+ /**
62
+ * Interface that all wallet connectors must implement
63
+ */
64
+ type NetworkType = 'mainnet' | 'testnet' | 'testnet4' | 'signet';
65
+ interface BitcoinConnector {
66
+ /** Unique identifier for the connector (e.g., 'unisat') */
67
+ readonly id: string;
68
+ /** Display name for the wallet (e.g., 'Unisat Wallet') */
69
+ readonly name: string;
70
+ /** Base64 encoded icon or URL to wallet icon */
71
+ readonly icon: string;
72
+ /** Whether the wallet extension is detected and ready */
73
+ ready: boolean;
74
+ /**
75
+ * Connect to the wallet
76
+ * @returns The connected wallet account
77
+ * @throws ConnectionError if connection fails
78
+ * @throws UserRejectedError if user rejects connection
79
+ */
80
+ connect(network?: NetworkType): Promise<WalletAccount>;
81
+ /**
82
+ * Disconnect from the wallet
83
+ */
84
+ disconnect(): Promise<void>;
85
+ /**
86
+ * Get all accounts from the wallet
87
+ * @returns Array of wallet accounts
88
+ */
89
+ getAccounts(): Promise<WalletAccount[]>;
90
+ /**
91
+ * Sign an arbitrary message
92
+ * @param message - Message to sign
93
+ * @returns Signature string
94
+ * @throws SigningError if signing fails
95
+ * @throws UserRejectedError if user rejects signing
96
+ */
97
+ signMessage(message: string): Promise<string>;
98
+ /**
99
+ * Sign a PSBT (Partially Signed Bitcoin Transaction)
100
+ * @param psbtHex - PSBT in hex format
101
+ * @param options - Signing options
102
+ * @returns Signed PSBT in hex format
103
+ * @throws SigningError if signing fails
104
+ * @throws UserRejectedError if user rejects signing
105
+ */
106
+ signPsbt(psbtHex: string, options?: SignPsbtOptions): Promise<string>;
107
+ /**
108
+ * Sign multiple PSBTs (optional, not all wallets support this)
109
+ * @param psbtHexs - Array of PSBTs in hex format
110
+ * @param options - Signing options
111
+ * @returns Array of signed PSBTs in hex format
112
+ */
113
+ signPsbts?(psbtHexs: string[], options?: SignPsbtOptions): Promise<string[]>;
114
+ /**
115
+ * Send a Bitcoin transaction
116
+ * @param to - Recipient address
117
+ * @param satoshis - Amount in satoshis
118
+ * @returns Transaction ID
119
+ * @throws SigningError if transaction fails
120
+ * @throws UserRejectedError if user rejects transaction
121
+ */
122
+ sendTransaction(to: string, satoshis: number): Promise<string>;
123
+ /**
124
+ * Get current network from the wallet
125
+ * @returns Current network
126
+ */
127
+ getNetwork(): Promise<BitcoinNetwork>;
128
+ /**
129
+ * Switch to a different network (optional, not all wallets support this)
130
+ * @param network - Network to switch to
131
+ * @throws NetworkError if switch fails
132
+ */
133
+ switchNetwork?(network: BitcoinNetwork): Promise<void>;
134
+ /**
135
+ * Subscribe to account changes
136
+ * @param callback - Called when accounts change
137
+ * @returns Unsubscribe function
138
+ */
139
+ onAccountsChanged(callback: (accounts: WalletAccount[]) => void): () => void;
140
+ /**
141
+ * Subscribe to network changes
142
+ * @param callback - Called when network changes
143
+ * @returns Unsubscribe function
144
+ */
145
+ onNetworkChanged(callback: (network: BitcoinNetwork) => void): () => void;
146
+ }
147
+
148
+ /**
149
+ * Configuration for otx-btc-wallet
150
+ */
151
+ type BtcWalletConfig = {
152
+ /** Array of wallet connectors to support */
153
+ connectors: BitcoinConnector[];
154
+ /** Whether to auto-connect on page load (default: true) */
155
+ autoConnect?: boolean;
156
+ /** Storage implementation for persistence (default: localStorage) */
157
+ storage?: Storage;
158
+ /** Key to use for storage (default: 'optimex-btc-wallet.store') */
159
+ storageKey?: string;
160
+ };
161
+ /**
162
+ * Internal config with resolved defaults
163
+ */
164
+ type Config = {
165
+ connectors: BitcoinConnector[];
166
+ autoConnect: boolean;
167
+ storage: Storage | null;
168
+ storageKey: string;
169
+ };
170
+
171
+ /**
172
+ * Connection status states
173
+ */
174
+ type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
175
+ /**
176
+ * Store state shape
177
+ */
178
+ type BtcWalletState = {
179
+ /** Current connection status */
180
+ status: ConnectionStatus;
181
+ /** Connected account (null if disconnected) */
182
+ account: WalletAccount | null;
183
+ /** Active connector (null if disconnected) */
184
+ connector: BitcoinConnector | null;
185
+ /** Connected connector ID (for persistence) */
186
+ connectorId: string | null;
187
+ /** Current network */
188
+ network: BitcoinNetwork;
189
+ /** Last error (null if no error) */
190
+ error: Error | null;
191
+ };
192
+
193
+ /**
194
+ * Store actions
195
+ */
196
+ type StoreActions = {
197
+ /** Connect to a wallet */
198
+ connect: (connector: BitcoinConnector, network?: BitcoinNetwork) => Promise<WalletAccount>;
199
+ /** Disconnect from current wallet */
200
+ disconnect: () => Promise<void>;
201
+ /** Reconnect to previously connected wallet */
202
+ reconnect: (connectors: BitcoinConnector[]) => Promise<void>;
203
+ /** Set account */
204
+ setAccount: (account: WalletAccount | null) => void;
205
+ /** Set connection status */
206
+ setStatus: (status: ConnectionStatus) => void;
207
+ /** Set error */
208
+ setError: (error: Error | null) => void;
209
+ /** Reset store to initial state */
210
+ reset: () => void;
211
+ };
212
+ type Store = BtcWalletState & StoreActions;
213
+ /** Persisted state shape - only these fields are saved to storage */
214
+ type PersistedState = {
215
+ connectorId: string | null;
216
+ network: string;
217
+ };
218
+ /**
219
+ * Create the btc-wallet store
220
+ */
221
+ declare function createStore(config: Config): Omit<Omit<zustand_vanilla.StoreApi<Store>, "subscribe"> & {
222
+ subscribe: {
223
+ (listener: (selectedState: Store, previousSelectedState: Store) => void): () => void;
224
+ <U>(selector: (state: Store) => U, listener: (selectedState: U, previousSelectedState: U) => void, options?: {
225
+ equalityFn?: (a: U, b: U) => boolean;
226
+ fireImmediately?: boolean;
227
+ } | undefined): () => void;
228
+ };
229
+ }, "persist"> & {
230
+ persist: {
231
+ setOptions: (options: Partial<PersistOptions<Store, PersistedState>>) => void;
232
+ clearStorage: () => void;
233
+ rehydrate: () => void | Promise<void>;
234
+ hasHydrated: () => boolean;
235
+ onHydrate: (fn: (state: Store) => void) => () => void;
236
+ onFinishHydration: (fn: (state: Store) => void) => () => void;
237
+ getOptions: () => Partial<PersistOptions<Store, PersistedState>>;
238
+ };
239
+ };
240
+ type BtcWalletStore = ReturnType<typeof createStore>;
241
+
242
+ /**
243
+ * Resolved config with store instance
244
+ */
245
+ type ResolvedConfig = Config & {
246
+ store: BtcWalletStore;
247
+ };
248
+ /**
249
+ * Create otx-btc-wallet configuration
250
+ *
251
+ * @example
252
+ * ```ts
253
+ * import { createConfig } from 'otx-btc-wallet-core';
254
+ * import { UnisatConnector } from 'otx-btc-wallet-connectors/unisat';
255
+ *
256
+ * const config = createConfig({
257
+ * connectors: [new UnisatConnector()],
258
+ * autoConnect: true,
259
+ * });
260
+ * ```
261
+ */
262
+ declare function createConfig(options: BtcWalletConfig): ResolvedConfig;
263
+
264
+ /**
265
+ * PSBT (Partially Signed Bitcoin Transaction) utilities
266
+ */
267
+ /**
268
+ * Convert PSBT hex to base64
269
+ */
270
+ declare function psbtHexToBase64(hex: string): string;
271
+ /**
272
+ * Convert PSBT base64 to hex
273
+ */
274
+ declare function psbtBase64ToHex(base64: string): string;
275
+ /**
276
+ * Validate PSBT hex format
277
+ * PSBT magic bytes: 0x70736274ff (psbt\xff)
278
+ */
279
+ declare function isValidPsbtHex(hex: string): boolean;
280
+ /**
281
+ * Validate PSBT base64 format
282
+ */
283
+ declare function isValidPsbtBase64(base64: string): boolean;
284
+ /**
285
+ * Extract basic info from PSBT hex (without full parsing)
286
+ * This is a lightweight check - for full parsing use a library like bitcoinjs-lib
287
+ */
288
+ declare function getPsbtInfo(psbtHex: string): {
289
+ isValid: boolean;
290
+ version: number | null;
291
+ inputCount: number | null;
292
+ outputCount: number | null;
293
+ };
294
+ /**
295
+ * Combine multiple signed PSBTs into one
296
+ * Note: This is a basic implementation. For complex cases, use bitcoinjs-lib.
297
+ *
298
+ * @param psbts - Array of PSBT hex strings to combine
299
+ * @returns Combined PSBT hex
300
+ */
301
+ declare function combinePsbts(psbts: string[]): string;
302
+ /**
303
+ * Sighash types for Bitcoin transactions
304
+ */
305
+ declare const SighashType: {
306
+ readonly ALL: 1;
307
+ readonly NONE: 2;
308
+ readonly SINGLE: 3;
309
+ readonly ANYONECANPAY: 128;
310
+ readonly ALL_ANYONECANPAY: 129;
311
+ readonly NONE_ANYONECANPAY: 130;
312
+ readonly SINGLE_ANYONECANPAY: 131;
313
+ };
314
+ type SighashType = (typeof SighashType)[keyof typeof SighashType];
315
+ /**
316
+ * Get human-readable name for sighash type
317
+ */
318
+ declare function getSighashTypeName(sighash: number): string;
319
+
320
+ export { type AddressType, type BitcoinConnector, type BitcoinNetwork, type BtcWalletConfig, type BtcWalletState, type BtcWalletStore, type Config, type ConnectionStatus, type MultiAddressAccount, type ResolvedConfig, SighashType, type SignInput, type SignPsbtOptions, type Store, type StoreActions, type WalletAccount, combinePsbts, createConfig, createStore, getPsbtInfo, getSighashTypeName, isValidPsbtBase64, isValidPsbtHex, psbtBase64ToHex, psbtHexToBase64 };
package/dist/index.js ADDED
@@ -0,0 +1,342 @@
1
+ 'use strict';
2
+
3
+ var vanilla = require('zustand/vanilla');
4
+ var middleware = require('zustand/middleware');
5
+
6
+ // src/store/index.ts
7
+ var initialState = {
8
+ status: "disconnected",
9
+ account: null,
10
+ connector: null,
11
+ connectorId: null,
12
+ network: "mainnet",
13
+ error: null
14
+ };
15
+ function createStore(config) {
16
+ let unsubscribeAccounts = null;
17
+ let unsubscribeNetwork = null;
18
+ const cleanupListeners = () => {
19
+ if (unsubscribeAccounts) {
20
+ unsubscribeAccounts();
21
+ unsubscribeAccounts = null;
22
+ }
23
+ if (unsubscribeNetwork) {
24
+ unsubscribeNetwork();
25
+ unsubscribeNetwork = null;
26
+ }
27
+ };
28
+ const setupConnectorListeners = (connector, get, set) => {
29
+ cleanupListeners();
30
+ unsubscribeAccounts = connector.onAccountsChanged((accounts) => {
31
+ const currentState = get();
32
+ if (currentState.connector?.id !== connector.id) {
33
+ return;
34
+ }
35
+ if (accounts.length === 0) {
36
+ get().disconnect().catch(() => {
37
+ set({
38
+ status: "disconnected",
39
+ account: null,
40
+ connector: null,
41
+ connectorId: null,
42
+ error: null
43
+ });
44
+ });
45
+ } else if (accounts[0]) {
46
+ set({ account: accounts[0] });
47
+ }
48
+ });
49
+ unsubscribeNetwork = connector.onNetworkChanged((network) => {
50
+ const currentState = get();
51
+ if (currentState.connector?.id !== connector.id) {
52
+ return;
53
+ }
54
+ set({ network });
55
+ });
56
+ };
57
+ const persistOptions = {
58
+ name: config.storageKey,
59
+ storage: config.storage ? {
60
+ getItem: (name) => {
61
+ const value = config.storage?.getItem(name);
62
+ if (!value)
63
+ return null;
64
+ try {
65
+ return JSON.parse(value);
66
+ } catch {
67
+ return null;
68
+ }
69
+ },
70
+ setItem: (name, value) => {
71
+ config.storage?.setItem(name, JSON.stringify(value));
72
+ },
73
+ removeItem: (name) => {
74
+ config.storage?.removeItem(name);
75
+ }
76
+ } : void 0,
77
+ // Only persist connectorId and network
78
+ partialize: (state) => ({
79
+ connectorId: state.connectorId,
80
+ network: state.network
81
+ })
82
+ };
83
+ const store = vanilla.createStore()(
84
+ middleware.subscribeWithSelector(
85
+ middleware.persist(
86
+ (set, get) => ({
87
+ // Initial state
88
+ ...initialState,
89
+ // Actions
90
+ connect: async (connector, networkParam) => {
91
+ const { status, network: storeNetwork } = get();
92
+ const network = networkParam ?? storeNetwork;
93
+ if (status === "connecting") {
94
+ throw new Error("Connection already in progress");
95
+ }
96
+ cleanupListeners();
97
+ set({ status: "connecting", error: null, network });
98
+ try {
99
+ if (!connector.ready) {
100
+ throw new Error(`${connector.name} wallet is not available`);
101
+ }
102
+ const account = await connector.connect(network);
103
+ set({
104
+ status: "connected",
105
+ account,
106
+ connector,
107
+ connectorId: connector.id,
108
+ error: null
109
+ });
110
+ setupConnectorListeners(connector, get, set);
111
+ return account;
112
+ } catch (error) {
113
+ const err = error instanceof Error ? error : new Error(String(error));
114
+ set({
115
+ status: "disconnected",
116
+ error: err
117
+ });
118
+ throw error;
119
+ }
120
+ },
121
+ disconnect: async () => {
122
+ const { connector } = get();
123
+ cleanupListeners();
124
+ try {
125
+ if (connector) {
126
+ await connector.disconnect();
127
+ }
128
+ } catch {
129
+ } finally {
130
+ set({
131
+ status: "disconnected",
132
+ account: null,
133
+ connector: null,
134
+ connectorId: null,
135
+ error: null
136
+ });
137
+ }
138
+ },
139
+ reconnect: async (connectors) => {
140
+ const { connectorId, status, network } = get();
141
+ if (!connectorId || status === "connected") {
142
+ return;
143
+ }
144
+ const connector = connectors.find((c) => c.id === connectorId);
145
+ if (!connector?.ready) {
146
+ set({ connectorId: null });
147
+ return;
148
+ }
149
+ set({ status: "reconnecting" });
150
+ try {
151
+ const account = await connector.connect(network);
152
+ set({
153
+ status: "connected",
154
+ account,
155
+ connector,
156
+ error: null
157
+ });
158
+ setupConnectorListeners(connector, get, set);
159
+ } catch {
160
+ set({
161
+ status: "disconnected",
162
+ connectorId: null
163
+ });
164
+ }
165
+ },
166
+ setAccount: (account) => set({ account }),
167
+ setStatus: (status) => set({ status }),
168
+ setError: (error) => set({ error }),
169
+ reset: () => {
170
+ cleanupListeners();
171
+ set(initialState);
172
+ }
173
+ }),
174
+ persistOptions
175
+ )
176
+ )
177
+ );
178
+ return store;
179
+ }
180
+
181
+ // src/createConfig.ts
182
+ function createConfig(options) {
183
+ if (!options.connectors || options.connectors.length === 0) {
184
+ throw new Error("At least one connector is required");
185
+ }
186
+ const connectorIds = /* @__PURE__ */ new Set();
187
+ for (const connector of options.connectors) {
188
+ if (connectorIds.has(connector.id)) {
189
+ throw new Error(`Duplicate connector ID: ${connector.id}`);
190
+ }
191
+ connectorIds.add(connector.id);
192
+ }
193
+ const config = {
194
+ connectors: options.connectors,
195
+ autoConnect: options.autoConnect ?? true,
196
+ storage: getStorage(options.storage),
197
+ storageKey: options.storageKey ?? "optimex-btc-wallet.store"
198
+ };
199
+ const store = createStore(config);
200
+ if (config.autoConnect) {
201
+ setTimeout(() => {
202
+ void store.getState().reconnect(config.connectors);
203
+ }, 0);
204
+ }
205
+ return {
206
+ ...config,
207
+ store
208
+ };
209
+ }
210
+ function getStorage(storage) {
211
+ if (storage !== void 0) {
212
+ return storage;
213
+ }
214
+ if (typeof window !== "undefined" && window.localStorage) {
215
+ return window.localStorage;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ // src/utils/psbt.ts
221
+ function psbtHexToBase64(hex) {
222
+ const cleanHex = hex.replace(/\s/g, "");
223
+ if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {
224
+ throw new Error("Invalid hex string");
225
+ }
226
+ if (cleanHex.length % 2 !== 0) {
227
+ throw new Error("Hex string must have even length");
228
+ }
229
+ const bytes = new Uint8Array(cleanHex.length / 2);
230
+ for (let i = 0; i < cleanHex.length; i += 2) {
231
+ bytes[i / 2] = parseInt(cleanHex.substring(i, i + 2), 16);
232
+ }
233
+ if (typeof Buffer !== "undefined") {
234
+ return Buffer.from(bytes).toString("base64");
235
+ }
236
+ let binary = "";
237
+ for (let i = 0; i < bytes.length; i++) {
238
+ binary += String.fromCharCode(bytes[i]);
239
+ }
240
+ return btoa(binary);
241
+ }
242
+ function psbtBase64ToHex(base64) {
243
+ let bytes;
244
+ if (typeof Buffer !== "undefined") {
245
+ bytes = Buffer.from(base64, "base64");
246
+ } else {
247
+ const binary = atob(base64);
248
+ bytes = new Uint8Array(binary.length);
249
+ for (let i = 0; i < binary.length; i++) {
250
+ bytes[i] = binary.charCodeAt(i);
251
+ }
252
+ }
253
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
254
+ }
255
+ function isValidPsbtHex(hex) {
256
+ const cleanHex = hex.replace(/\s/g, "").toLowerCase();
257
+ if (cleanHex.length < 10) {
258
+ return false;
259
+ }
260
+ return cleanHex.startsWith("70736274ff");
261
+ }
262
+ function isValidPsbtBase64(base64) {
263
+ try {
264
+ const hex = psbtBase64ToHex(base64);
265
+ return isValidPsbtHex(hex);
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ function getPsbtInfo(psbtHex) {
271
+ if (!isValidPsbtHex(psbtHex)) {
272
+ return {
273
+ isValid: false,
274
+ version: null,
275
+ inputCount: null,
276
+ outputCount: null
277
+ };
278
+ }
279
+ return {
280
+ isValid: true,
281
+ version: null,
282
+ // Would need full parsing
283
+ inputCount: null,
284
+ // Would need full parsing
285
+ outputCount: null
286
+ // Would need full parsing
287
+ };
288
+ }
289
+ function combinePsbts(psbts) {
290
+ if (psbts.length === 0) {
291
+ throw new Error("No PSBTs provided");
292
+ }
293
+ if (psbts.length === 1) {
294
+ return psbts[0];
295
+ }
296
+ console.warn(
297
+ "combinePsbts: For proper PSBT combination, use bitcoinjs-lib. This function returns the first PSBT as a fallback."
298
+ );
299
+ return psbts[0];
300
+ }
301
+ var SighashType = {
302
+ ALL: 1,
303
+ NONE: 2,
304
+ SINGLE: 3,
305
+ ANYONECANPAY: 128,
306
+ ALL_ANYONECANPAY: 129,
307
+ NONE_ANYONECANPAY: 130,
308
+ SINGLE_ANYONECANPAY: 131
309
+ };
310
+ function getSighashTypeName(sighash) {
311
+ switch (sighash) {
312
+ case SighashType.ALL:
313
+ return "SIGHASH_ALL";
314
+ case SighashType.NONE:
315
+ return "SIGHASH_NONE";
316
+ case SighashType.SINGLE:
317
+ return "SIGHASH_SINGLE";
318
+ case SighashType.ANYONECANPAY:
319
+ return "SIGHASH_ANYONECANPAY";
320
+ case SighashType.ALL_ANYONECANPAY:
321
+ return "SIGHASH_ALL|ANYONECANPAY";
322
+ case SighashType.NONE_ANYONECANPAY:
323
+ return "SIGHASH_NONE|ANYONECANPAY";
324
+ case SighashType.SINGLE_ANYONECANPAY:
325
+ return "SIGHASH_SINGLE|ANYONECANPAY";
326
+ default:
327
+ return `UNKNOWN(${sighash})`;
328
+ }
329
+ }
330
+
331
+ exports.SighashType = SighashType;
332
+ exports.combinePsbts = combinePsbts;
333
+ exports.createConfig = createConfig;
334
+ exports.createStore = createStore;
335
+ exports.getPsbtInfo = getPsbtInfo;
336
+ exports.getSighashTypeName = getSighashTypeName;
337
+ exports.isValidPsbtBase64 = isValidPsbtBase64;
338
+ exports.isValidPsbtHex = isValidPsbtHex;
339
+ exports.psbtBase64ToHex = psbtBase64ToHex;
340
+ exports.psbtHexToBase64 = psbtHexToBase64;
341
+ //# sourceMappingURL=out.js.map
342
+ //# sourceMappingURL=index.js.map