@satoshai/kit 0.1.1 → 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Satoshai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -6,6 +6,7 @@ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired
6
6
 
7
7
  - **`StacksWalletProvider`** — React context provider for wallet state
8
8
  - **`useConnect` / `useDisconnect`** — Connect and disconnect wallets
9
+ - **`useWallets`** — Configured wallets with availability status
9
10
  - **`useAddress`** — Access connected wallet address and status
10
11
  - **`useSignMessage`** — Sign arbitrary messages
11
12
  - **`useWriteContract`** — Call smart contracts with post-conditions
@@ -22,7 +23,7 @@ pnpm add @satoshai/kit @stacks/transactions react react-dom
22
23
  ## Quick Start
23
24
 
24
25
  ```tsx
25
- import { StacksWalletProvider, useConnect, useAddress, useDisconnect } from '@satoshai/kit';
26
+ import { StacksWalletProvider, useConnect, useWallets, useAddress, useDisconnect } from '@satoshai/kit';
26
27
 
27
28
  function App() {
28
29
  return (
@@ -33,7 +34,8 @@ function App() {
33
34
  }
34
35
 
35
36
  function Wallet() {
36
- const { connect, connectors } = useConnect();
37
+ const { connect, reset, isPending } = useConnect();
38
+ const { wallets } = useWallets();
37
39
  const { address, isConnected } = useAddress();
38
40
  const { disconnect } = useDisconnect();
39
41
 
@@ -48,9 +50,10 @@ function Wallet() {
48
50
 
49
51
  return (
50
52
  <div>
51
- {connectors.map((wallet) => (
52
- <button key={wallet} onClick={() => connect(wallet)}>
53
- {wallet}
53
+ {isPending && <button onClick={reset}>Cancel</button>}
54
+ {wallets.map(({ id, available }) => (
55
+ <button key={id} onClick={() => connect(id)} disabled={!available || isPending}>
56
+ {id}
54
57
  </button>
55
58
  ))}
56
59
  </div>
@@ -66,28 +69,54 @@ Wrap your app to provide wallet context to all hooks.
66
69
 
67
70
  ```tsx
68
71
  <StacksWalletProvider
69
- walletConnect={{ projectId: '...' }} // optional — enables WalletConnect
70
- onConnect={(provider, address) => {}} // optional
71
- onAddressChange={(newAddress) => {}} // optional — Xverse account switching
72
- onDisconnect={() => {}} // optional
72
+ wallets={['xverse', 'leather', 'wallet-connect']} // optional — defaults to all supported
73
+ walletConnect={{ projectId: '...' }} // optional — enables WalletConnect
74
+ onConnect={(provider, address) => {}} // optional
75
+ onAddressChange={(newAddress) => {}} // optional — Xverse account switching
76
+ onDisconnect={() => {}} // optional
73
77
  >
74
78
  {children}
75
79
  </StacksWalletProvider>
76
80
  ```
77
81
 
82
+ > If `wallets` includes `'wallet-connect'`, you must provide `walletConnect.projectId` or the provider will throw at mount.
83
+
84
+ > **Important:** Define `wallets` and `walletConnect` outside of your component (or memoize them) so they remain referentially stable across renders. These values are treated as static configuration.
85
+
78
86
  ### `useConnect()`
79
87
 
80
88
  ```ts
81
- const { connect, connectors, isPending } = useConnect();
89
+ const { connect, reset, isPending } = useConnect();
82
90
 
83
- // connectors = ['xverse', 'leather', 'okx', 'asigna', 'fordefi', 'wallet-connect']
84
91
  await connect('xverse');
85
92
  await connect('leather', {
86
93
  onSuccess: (address, provider) => {},
87
94
  onError: (error) => {},
88
95
  });
96
+
97
+ // Reset stuck connecting state (e.g. when a wallet modal is dismissed)
98
+ reset();
99
+ ```
100
+
101
+ > **Note:** Some wallets (e.g. OKX) never reject the connection promise when the user closes the popup. Use `reset()` to clear the pending state in those cases.
102
+
103
+ ### `useWallets()`
104
+
105
+ Returns all configured wallets with their availability status.
106
+
107
+ ```ts
108
+ const { wallets } = useWallets();
109
+ // [{ id: 'xverse', available: true }, { id: 'leather', available: false }, ...]
110
+
111
+ {wallets.map(({ id, available }) => (
112
+ <button key={id} onClick={() => connect(id)} disabled={!available}>
113
+ {id}
114
+ </button>
115
+ ))}
89
116
  ```
90
117
 
118
+ A wallet is `available` when its browser extension is installed. For `wallet-connect`, it's `available` when a `walletConnect.projectId` is provided to the provider.
119
+
91
120
  ### `useDisconnect()`
92
121
 
93
122
  ```ts
package/dist/index.cjs CHANGED
@@ -162,15 +162,18 @@ var useXverse = ({
162
162
  }, [provider]);
163
163
  react.useEffect(() => {
164
164
  if (provider !== "xverse" || !address || !isProviderReady) return;
165
+ let cancelled = false;
165
166
  let removeListener;
166
167
  const setupXverse = async () => {
167
168
  try {
168
169
  const productInfo = await getXverseProductInfo();
170
+ if (cancelled) return;
169
171
  if (!shouldSupportAccountChange(productInfo?.version)) return;
170
172
  const response = await connect.getSelectedProvider()?.request(
171
173
  "wallet_connect",
172
174
  null
173
175
  );
176
+ if (cancelled) return;
174
177
  extractAndValidateStacksAddress(
175
178
  response?.result?.addresses,
176
179
  address,
@@ -194,6 +197,7 @@ var useXverse = ({
194
197
  };
195
198
  void setupXverse();
196
199
  return () => {
200
+ cancelled = true;
197
201
  if (!removeListener) return;
198
202
  try {
199
203
  removeListener();
@@ -224,6 +228,7 @@ var StacksWalletContext = react.createContext(
224
228
  );
225
229
  var StacksWalletProvider = ({
226
230
  children,
231
+ wallets,
227
232
  walletConnect,
228
233
  onConnect,
229
234
  onAddressChange,
@@ -232,6 +237,16 @@ var StacksWalletProvider = ({
232
237
  const [address, setAddress] = react.useState();
233
238
  const [provider, setProvider] = react.useState();
234
239
  const [isConnecting, setIsConnecting] = react.useState(false);
240
+ const connectGenRef = react.useRef(0);
241
+ const wcInitRef = react.useRef(null);
242
+ const walletsKey = wallets?.join(",");
243
+ react.useEffect(() => {
244
+ if (wallets?.includes("wallet-connect") && !walletConnect?.projectId) {
245
+ throw new Error(
246
+ 'StacksWalletProvider: "wallet-connect" is listed in wallets but no walletConnect.projectId was provided.'
247
+ );
248
+ }
249
+ }, [walletsKey, walletConnect?.projectId]);
235
250
  react.useEffect(() => {
236
251
  const loadPersistedWallet = async () => {
237
252
  const persisted = getLocalStorageWallet();
@@ -244,6 +259,18 @@ var StacksWalletProvider = ({
244
259
  setProvider(data.provider);
245
260
  return;
246
261
  }
262
+ if (persisted.provider === "wallet-connect" && walletConnect?.projectId) {
263
+ const initPromise = connect.WalletConnect.initializeProvider(
264
+ buildWalletConnectConfig(
265
+ walletConnect.projectId,
266
+ walletConnect.metadata,
267
+ walletConnect.chains
268
+ )
269
+ );
270
+ wcInitRef.current = initPromise;
271
+ await initPromise;
272
+ wcInitRef.current = null;
273
+ }
247
274
  setAddress(persisted.address);
248
275
  setProvider(persisted.provider);
249
276
  connect.setSelectedProviderId(
@@ -256,7 +283,7 @@ var StacksWalletProvider = ({
256
283
  }
257
284
  };
258
285
  void loadPersistedWallet();
259
- }, []);
286
+ }, [walletConnect?.projectId]);
260
287
  const connect$1 = react.useCallback(
261
288
  async (providerId, options) => {
262
289
  const typedProvider = SUPPORTED_STACKS_WALLETS.find(
@@ -286,10 +313,12 @@ var StacksWalletProvider = ({
286
313
  options?.onError?.(error);
287
314
  return;
288
315
  }
316
+ const gen = ++connectGenRef.current;
289
317
  setIsConnecting(true);
290
318
  try {
291
319
  if (typedProvider === "okx") {
292
320
  const data2 = await getOKXStacksAddress();
321
+ if (connectGenRef.current !== gen) return;
293
322
  setAddress(data2.address);
294
323
  setProvider(data2.provider);
295
324
  options?.onSuccess?.(data2.address, data2.provider);
@@ -298,17 +327,25 @@ var StacksWalletProvider = ({
298
327
  connect.setSelectedProviderId(
299
328
  STACKS_TO_STACKS_CONNECT_PROVIDERS[typedProvider]
300
329
  );
301
- const data = walletConnect ? await connect.request(
302
- {
303
- walletConnect: buildWalletConnectConfig(
304
- walletConnect.projectId,
305
- walletConnect.metadata,
306
- walletConnect.chains
307
- )
308
- },
330
+ const wcConfig = typedProvider === "wallet-connect" && walletConnect ? buildWalletConnectConfig(
331
+ walletConnect.projectId,
332
+ walletConnect.metadata,
333
+ walletConnect.chains
334
+ ) : void 0;
335
+ if (wcConfig) {
336
+ if (wcInitRef.current) await wcInitRef.current;
337
+ const initPromise = connect.WalletConnect.initializeProvider(wcConfig);
338
+ wcInitRef.current = initPromise;
339
+ await initPromise;
340
+ wcInitRef.current = null;
341
+ }
342
+ if (connectGenRef.current !== gen) return;
343
+ const data = wcConfig ? await connect.request(
344
+ { walletConnect: wcConfig },
309
345
  "getAddresses",
310
346
  {}
311
347
  ) : await connect.request("getAddresses");
348
+ if (connectGenRef.current !== gen) return;
312
349
  const extractedAddress = extractStacksAddress(
313
350
  typedProvider,
314
351
  data.addresses
@@ -317,16 +354,26 @@ var StacksWalletProvider = ({
317
354
  setProvider(typedProvider);
318
355
  options?.onSuccess?.(extractedAddress, typedProvider);
319
356
  } catch (error) {
357
+ if (connectGenRef.current !== gen) return;
320
358
  console.error("Failed to connect wallet:", error);
321
- connect.getSelectedProvider()?.disconnect?.();
322
- connect.clearSelectedProviderId();
359
+ if (typedProvider !== "okx") {
360
+ connect.getSelectedProvider()?.disconnect?.();
361
+ connect.clearSelectedProviderId();
362
+ }
323
363
  options?.onError?.(error);
324
364
  } finally {
325
- setIsConnecting(false);
365
+ if (connectGenRef.current === gen) {
366
+ setIsConnecting(false);
367
+ }
326
368
  }
327
369
  },
328
370
  [walletConnect]
329
371
  );
372
+ const reset = react.useCallback(() => {
373
+ connectGenRef.current++;
374
+ setIsConnecting(false);
375
+ connect.clearSelectedProviderId();
376
+ }, []);
330
377
  const disconnect = react.useCallback(
331
378
  (callback) => {
332
379
  localStorage.removeItem(LOCAL_STORAGE_STACKS);
@@ -359,6 +406,14 @@ var StacksWalletProvider = ({
359
406
  },
360
407
  connect: connect$1
361
408
  });
409
+ const walletInfos = react.useMemo(() => {
410
+ const { installed } = getStacksWallets();
411
+ const configured = wallets ?? [...SUPPORTED_STACKS_WALLETS];
412
+ return configured.map((w) => ({
413
+ id: w,
414
+ available: w === "wallet-connect" ? !!walletConnect?.projectId : installed.includes(w)
415
+ }));
416
+ }, [walletsKey, walletConnect?.projectId]);
362
417
  const value = react.useMemo(() => {
363
418
  const walletState = isConnecting ? { status: "connecting", address: void 0, provider: void 0 } : address && provider ? { status: "connected", address, provider } : {
364
419
  status: "disconnected",
@@ -368,9 +423,11 @@ var StacksWalletProvider = ({
368
423
  return {
369
424
  ...walletState,
370
425
  connect: connect$1,
371
- disconnect
426
+ disconnect,
427
+ reset,
428
+ wallets: walletInfos
372
429
  };
373
- }, [address, provider, isConnecting, connect$1, disconnect]);
430
+ }, [address, provider, isConnecting, connect$1, disconnect, reset, walletInfos]);
374
431
  return /* @__PURE__ */ jsxRuntime.jsx(StacksWalletContext.Provider, { value, children });
375
432
  };
376
433
  var useStacksWalletContext = () => {
@@ -404,37 +461,108 @@ var useAddress = () => {
404
461
  }, [address, status, provider]);
405
462
  };
406
463
  var useConnect = () => {
407
- const { connect, status } = useStacksWalletContext();
464
+ const {
465
+ connect: contextConnect,
466
+ reset: contextReset,
467
+ status: walletStatus
468
+ } = useStacksWalletContext();
469
+ const [error, setError] = react.useState(null);
470
+ const [mutationStatus, setMutationStatus] = react.useState("idle");
471
+ const connect = react.useCallback(
472
+ async (providerId, options) => {
473
+ setError(null);
474
+ setMutationStatus("pending");
475
+ let settled = false;
476
+ try {
477
+ await contextConnect(providerId, {
478
+ onSuccess: (address, provider) => {
479
+ settled = true;
480
+ setMutationStatus("success");
481
+ options?.onSuccess?.(address, provider);
482
+ },
483
+ onError: (err) => {
484
+ settled = true;
485
+ setError(err);
486
+ setMutationStatus("error");
487
+ options?.onError?.(err);
488
+ }
489
+ });
490
+ } finally {
491
+ if (!settled) {
492
+ setMutationStatus("idle");
493
+ }
494
+ }
495
+ },
496
+ [contextConnect]
497
+ );
498
+ const reset = react.useCallback(() => {
499
+ setError(null);
500
+ setMutationStatus("idle");
501
+ contextReset();
502
+ }, [contextReset]);
408
503
  const value = react.useMemo(
409
504
  () => ({
410
505
  connect,
411
- connectors: SUPPORTED_STACKS_WALLETS,
412
- isPending: status === "connecting"
506
+ reset,
507
+ error,
508
+ isError: mutationStatus === "error",
509
+ isIdle: mutationStatus === "idle",
510
+ isPending: mutationStatus === "pending" || walletStatus === "connecting",
511
+ isSuccess: mutationStatus === "success",
512
+ status: mutationStatus
413
513
  }),
414
- [connect, status]
514
+ [connect, reset, error, mutationStatus, walletStatus]
415
515
  );
416
516
  return value;
417
517
  };
418
518
  var useDisconnect = () => {
419
- const { disconnect } = useStacksWalletContext();
420
- return react.useMemo(
519
+ const { disconnect: contextDisconnect } = useStacksWalletContext();
520
+ const [error, setError] = react.useState(null);
521
+ const [mutationStatus, setMutationStatus] = react.useState("idle");
522
+ const disconnect = react.useCallback(
523
+ (callback) => {
524
+ setError(null);
525
+ try {
526
+ contextDisconnect(callback);
527
+ setMutationStatus("success");
528
+ } catch (err) {
529
+ const normalizedError = err instanceof Error ? err : new Error(String(err));
530
+ setError(normalizedError);
531
+ setMutationStatus("error");
532
+ }
533
+ },
534
+ [contextDisconnect]
535
+ );
536
+ const reset = react.useCallback(() => {
537
+ setError(null);
538
+ setMutationStatus("idle");
539
+ }, []);
540
+ const value = react.useMemo(
421
541
  () => ({
422
- disconnect
542
+ disconnect,
543
+ reset,
544
+ error,
545
+ isError: mutationStatus === "error",
546
+ isIdle: mutationStatus === "idle",
547
+ isPending: mutationStatus === "pending",
548
+ isSuccess: mutationStatus === "success",
549
+ status: mutationStatus
423
550
  }),
424
- [disconnect]
551
+ [disconnect, reset, error, mutationStatus]
425
552
  );
553
+ return value;
426
554
  };
427
555
  var useSignMessage = () => {
428
556
  const { isConnected, provider } = useAddress();
429
557
  const [data, setData] = react.useState(void 0);
430
558
  const [error, setError] = react.useState(null);
431
- const [isPending, setIsPending] = react.useState(false);
559
+ const [status, setStatus] = react.useState("idle");
432
560
  const signMessageAsync = react.useCallback(
433
561
  async (variables) => {
434
562
  if (!isConnected) {
435
563
  throw new Error("Wallet is not connected");
436
564
  }
437
- setIsPending(true);
565
+ setStatus("pending");
438
566
  setError(null);
439
567
  setData(void 0);
440
568
  try {
@@ -455,12 +583,12 @@ var useSignMessage = () => {
455
583
  });
456
584
  }
457
585
  setData(result);
458
- setIsPending(false);
586
+ setStatus("success");
459
587
  return result;
460
588
  } catch (err) {
461
589
  const error2 = err instanceof Error ? err : new Error(String(err));
462
590
  setError(error2);
463
- setIsPending(false);
591
+ setStatus("error");
464
592
  throw error2;
465
593
  }
466
594
  },
@@ -478,13 +606,26 @@ var useSignMessage = () => {
478
606
  },
479
607
  [signMessageAsync]
480
608
  );
481
- return {
482
- signMessage,
483
- signMessageAsync,
484
- data,
485
- error,
486
- isPending
487
- };
609
+ const reset = react.useCallback(() => {
610
+ setData(void 0);
611
+ setError(null);
612
+ setStatus("idle");
613
+ }, []);
614
+ return react.useMemo(
615
+ () => ({
616
+ signMessage,
617
+ signMessageAsync,
618
+ reset,
619
+ data,
620
+ error,
621
+ isError: status === "error",
622
+ isIdle: status === "idle",
623
+ isPending: status === "pending",
624
+ isSuccess: status === "success",
625
+ status
626
+ }),
627
+ [signMessage, signMessageAsync, reset, data, error, status]
628
+ );
488
629
  };
489
630
 
490
631
  // src/utils/get-network-from-address.ts
@@ -505,13 +646,13 @@ var useWriteContract = () => {
505
646
  const { isConnected, address, provider } = useAddress();
506
647
  const [data, setData] = react.useState(void 0);
507
648
  const [error, setError] = react.useState(null);
508
- const [isPending, setIsPending] = react.useState(false);
649
+ const [status, setStatus] = react.useState("idle");
509
650
  const writeContractAsync = react.useCallback(
510
651
  async (variables) => {
511
652
  if (!isConnected || !address) {
512
653
  throw new Error("Wallet is not connected");
513
654
  }
514
- setIsPending(true);
655
+ setStatus("pending");
515
656
  setError(null);
516
657
  setData(void 0);
517
658
  try {
@@ -533,7 +674,7 @@ var useWriteContract = () => {
533
674
  anchorMode: 3
534
675
  });
535
676
  setData(response2.txHash);
536
- setIsPending(false);
677
+ setStatus("success");
537
678
  return response2.txHash;
538
679
  }
539
680
  const response = await connect.request("stx_callContract", {
@@ -549,12 +690,12 @@ var useWriteContract = () => {
549
690
  throw new Error("No transaction ID returned");
550
691
  }
551
692
  setData(response.txid);
552
- setIsPending(false);
693
+ setStatus("success");
553
694
  return response.txid;
554
695
  } catch (err) {
555
696
  const error2 = err instanceof Error ? err : new Error(String(err));
556
697
  setError(error2);
557
- setIsPending(false);
698
+ setStatus("error");
558
699
  throw error2;
559
700
  }
560
701
  },
@@ -572,13 +713,26 @@ var useWriteContract = () => {
572
713
  },
573
714
  [writeContractAsync]
574
715
  );
575
- return {
576
- writeContract,
577
- writeContractAsync,
578
- data,
579
- error,
580
- isPending
581
- };
716
+ const reset = react.useCallback(() => {
717
+ setData(void 0);
718
+ setError(null);
719
+ setStatus("idle");
720
+ }, []);
721
+ return react.useMemo(
722
+ () => ({
723
+ writeContract,
724
+ writeContractAsync,
725
+ reset,
726
+ data,
727
+ error,
728
+ isError: status === "error",
729
+ isIdle: status === "idle",
730
+ isPending: status === "pending",
731
+ isSuccess: status === "success",
732
+ status
733
+ }),
734
+ [writeContract, writeContractAsync, reset, data, error, status]
735
+ );
582
736
  };
583
737
  var useBnsName = (address) => {
584
738
  const [bnsName, setBnsName] = react.useState(null);
@@ -606,6 +760,10 @@ var useBnsName = (address) => {
606
760
  }, [address]);
607
761
  return { bnsName, isLoading };
608
762
  };
763
+ var useWallets = () => {
764
+ const { wallets } = useStacksWalletContext();
765
+ return react.useMemo(() => ({ wallets }), [wallets]);
766
+ };
609
767
 
610
768
  exports.SUPPORTED_STACKS_WALLETS = SUPPORTED_STACKS_WALLETS;
611
769
  exports.StacksWalletProvider = StacksWalletProvider;
@@ -617,6 +775,7 @@ exports.useBnsName = useBnsName;
617
775
  exports.useConnect = useConnect;
618
776
  exports.useDisconnect = useDisconnect;
619
777
  exports.useSignMessage = useSignMessage;
778
+ exports.useWallets = useWallets;
620
779
  exports.useWriteContract = useWriteContract;
621
780
  //# sourceMappingURL=index.cjs.map
622
781
  //# sourceMappingURL=index.cjs.map