@satoshai/kit 0.3.1 → 0.4.1

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/README.md CHANGED
@@ -23,7 +23,7 @@ pnpm add @satoshai/kit @stacks/transactions react react-dom
23
23
  ## Quick Start
24
24
 
25
25
  ```tsx
26
- import { StacksWalletProvider, useConnect, useWallets, useAddress, useDisconnect } from '@satoshai/kit';
26
+ import { StacksWalletProvider, useConnect, useAddress, useDisconnect } from '@satoshai/kit';
27
27
 
28
28
  function App() {
29
29
  return (
@@ -35,7 +35,6 @@ function App() {
35
35
 
36
36
  function Wallet() {
37
37
  const { connect, reset, isPending } = useConnect();
38
- const { wallets } = useWallets();
39
38
  const { address, isConnected } = useAddress();
40
39
  const { disconnect } = useDisconnect();
41
40
 
@@ -51,11 +50,7 @@ function Wallet() {
51
50
  return (
52
51
  <div>
53
52
  {isPending && <button onClick={reset}>Cancel</button>}
54
- {wallets.map(({ id, available }) => (
55
- <button key={id} onClick={() => connect(id)} disabled={!available || isPending}>
56
- {id}
57
- </button>
58
- ))}
53
+ <button onClick={() => connect()} disabled={isPending}>Connect Wallet</button>
59
54
  </div>
60
55
  );
61
56
  }
@@ -70,6 +65,7 @@ Wrap your app to provide wallet context to all hooks.
70
65
  ```tsx
71
66
  <StacksWalletProvider
72
67
  wallets={['xverse', 'leather', 'wallet-connect']} // optional — defaults to all supported
68
+ connectModal={true} // optional — defaults to true
73
69
  walletConnect={{ projectId: '...' }} // optional — enables WalletConnect
74
70
  onConnect={(provider, address) => {}} // optional
75
71
  onAddressChange={(newAddress) => {}} // optional — Xverse account switching
@@ -83,11 +79,38 @@ Wrap your app to provide wallet context to all hooks.
83
79
 
84
80
  > **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
81
 
82
+ #### `connectModal` (default: `true`)
83
+
84
+ Controls whether `connect()` with no arguments shows `@stacks/connect`'s built-in wallet selection modal.
85
+
86
+ ```tsx
87
+ // Default — modal handles wallet selection
88
+ <StacksWalletProvider>
89
+ <App /> {/* connect() opens the modal */}
90
+ </StacksWalletProvider>
91
+
92
+ // Headless — manage wallet selection yourself (wagmi-style)
93
+ <StacksWalletProvider connectModal={false}>
94
+ <App /> {/* connect('xverse') only, connect() with no args is a no-op */}
95
+ </StacksWalletProvider>
96
+ ```
97
+
98
+ When `connectModal` is enabled:
99
+ - `connect()` with no args opens the `@stacks/connect` modal
100
+ - `connect('xverse')` with an explicit provider still bypasses the modal
101
+ - The `wallets` prop controls which wallets appear in the modal
102
+ - All 6 wallets are supported in the modal
103
+ - After the user picks a wallet, the kit automatically maps it back and sets state
104
+
86
105
  ### `useConnect()`
87
106
 
88
107
  ```ts
89
108
  const { connect, reset, isPending } = useConnect();
90
109
 
110
+ // Open the @stacks/connect modal (when connectModal is enabled, the default)
111
+ await connect();
112
+
113
+ // Or connect to a specific wallet directly
91
114
  await connect('xverse');
92
115
  await connect('leather', {
93
116
  onSuccess: (address, provider) => {},
@@ -102,16 +125,20 @@ reset();
102
125
 
103
126
  ### `useWallets()`
104
127
 
105
- Returns all configured wallets with their availability status.
128
+ Returns all configured wallets with their name, icon, download link, and availability status. Metadata is sourced from `@stacks/connect`.
106
129
 
107
130
  ```ts
108
131
  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>
132
+ // [{ id: 'xverse', name: 'Xverse Wallet', icon: 'data:image/svg+xml;...', webUrl: 'https://xverse.app', available: true }, ...]
133
+
134
+ {wallets.map(({ id, name, icon, webUrl, available }) => (
135
+ <div key={id}>
136
+ <button onClick={() => connect(id)} disabled={!available}>
137
+ {icon && <img src={icon} alt={name} width={20} height={20} />}
138
+ {name}
139
+ </button>
140
+ {!available && webUrl && <a href={webUrl} target="_blank">Install</a>}
141
+ </div>
115
142
  ))}
116
143
  ```
117
144
 
@@ -194,14 +221,16 @@ const { supported, installed } = getStacksWallets();
194
221
 
195
222
  ## Supported Wallets
196
223
 
224
+ All 6 wallets work with both headless (`connect('xverse')`) and modal (`connect()`) modes.
225
+
197
226
  | Wallet | ID |
198
227
  |---|---|
199
228
  | Xverse | `xverse` |
200
229
  | Leather | `leather` |
201
- | OKX | `okx` |
202
230
  | Asigna | `asigna` |
203
231
  | Fordefi | `fordefi` |
204
232
  | WalletConnect | `wallet-connect` |
233
+ | OKX | `okx` |
205
234
 
206
235
  ## Peer Dependencies
207
236
 
package/dist/index.cjs CHANGED
@@ -10,10 +10,17 @@ var bnsV2Sdk = require('bns-v2-sdk');
10
10
  var STACKS_TO_STACKS_CONNECT_PROVIDERS = {
11
11
  xverse: "XverseProviders.BitcoinProvider",
12
12
  leather: "LeatherProvider",
13
+ okx: "OkxStacksProvider",
13
14
  asigna: "AsignaProvider",
14
15
  fordefi: "FordefiProviders.UtxoProvider",
15
16
  "wallet-connect": "WalletConnectProvider"
16
17
  };
18
+ var STACKS_CONNECT_TO_STACKS_PROVIDERS = Object.fromEntries(
19
+ Object.entries(STACKS_TO_STACKS_CONNECT_PROVIDERS).map(([kit, connect]) => [
20
+ connect,
21
+ kit
22
+ ])
23
+ );
17
24
 
18
25
  // src/constants/storage-keys.ts
19
26
  var LOCAL_STORAGE_STACKS = "@satoshai/kit";
@@ -22,10 +29,10 @@ var LOCAL_STORAGE_STACKS = "@satoshai/kit";
22
29
  var SUPPORTED_STACKS_WALLETS = [
23
30
  "xverse",
24
31
  "leather",
25
- "okx",
26
32
  "asigna",
27
33
  "fordefi",
28
- "wallet-connect"
34
+ "wallet-connect",
35
+ "okx"
29
36
  ];
30
37
 
31
38
  // src/utils/get-stacks-wallets.ts
@@ -37,7 +44,7 @@ var getStacksWallets = () => {
37
44
  return { supported, installed };
38
45
  };
39
46
  var checkIfStacksProviderIsInstalled = (wallet) => {
40
- if (typeof window === "undefined") return true;
47
+ if (typeof window === "undefined") return wallet === "wallet-connect";
41
48
  switch (wallet) {
42
49
  case "xverse":
43
50
  return !!window.XverseProviders;
@@ -92,6 +99,38 @@ var getOKXStacksAddress = async () => {
92
99
  provider: "okx"
93
100
  };
94
101
  };
102
+ var OKX_PROVIDER_META = {
103
+ id: "OkxStacksProvider",
104
+ name: "OKX Wallet",
105
+ icon: "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiICAgICB4bWxuczp4b2RtPSJodHRwOi8vd3d3LmNvcmVsLmNvbS9jb3JlbGRyYXcvb2RtLzIwMDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjUwMCAyNTAwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyNTAwIDI1MDA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KICAgIC5zdDB7ZmlsbC1ydWxlOmV2ZW5vZGQ7Y2xpcC1ydWxlOmV2ZW5vZGQ7fQogICAgLnN0MXtmaWxsOiNGRkZGRkY7fQo8L3N0eWxlPgo8ZyBpZD0iTGF5ZXJfeDAwMjBfMSI+CiAgICA8ZyBpZD0iXzIxODczODEzMjM4NTYiPgogICAgICAgIDxyZWN0IHk9IjAiIGNsYXNzPSJzdDAiIHdpZHRoPSIyNTAwIiBoZWlnaHQ9IjI1MDAiPjwvcmVjdD4KICAgICAgICA8Zz4KICAgICAgICAgICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTE0NjMsMTAxNWgtNDA0Yy0xNywwLTMxLDE0LTMxLDMxdjQwNGMwLDE3LDE0LDMxLDMxLDMxaDQwNGMxNywwLDMxLTE0LDMxLTMxdi00MDQgICAgIEMxNDk0LDEwMjksMTQ4MCwxMDE1LDE0NjMsMTAxNXoiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTk5Niw1NDlINTkyYy0xNywwLTMxLDE0LTMxLDMxdjQwNGMwLDE3LDE0LDMxLDMxLDMxaDQwNGMxNywwLDMxLTE0LDMxLTMxVjU4MEMxMDI3LDU2MywxMDEzLDU0OSw5OTYsNTQ5eiI+PC9wYXRoPgogICAgICAgICAgICA8cGF0aCBjbGFzcz0ic3QxIiBkPSJNMTkzMCw1NDloLTQwNGMtMTcsMC0zMSwxNC0zMSwzMXY0MDRjMCwxNywxNCwzMSwzMSwzMWg0MDRjMTcsMCwzMS0xNCwzMS0zMVY1ODAgICAgIEMxOTYxLDU2MywxOTQ3LDU0OSwxOTMwLDU0OXoiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTk5NiwxNDgySDU5MmMtMTcsMC0zMSwxNC0zMSwzMXY0MDRjMCwxNywxNCwzMSwzMSwzMWg0MDRjMTcsMCwzMS0xNCwzMS0zMXYtNDA0ICAgICBDMTAyNywxNDk2LDEwMTMsMTQ4Miw5OTYsMTQ4MnoiPjwvcGF0aD4KICAgICAgICAgICAgPHBhdGggY2xhc3M9InN0MSIgZD0iTTE5MzAsMTQ4MmgtNDA0Yy0xNywwLTMxLDE0LTMxLDMxdjQwNGMwLDE3LDE0LDMxLDMxLDMxaDQwNGMxNywwLDMxLTE0LDMxLTMxdi00MDQgICAgIEMxOTYxLDE0OTYsMTk0NywxNDgyLDE5MzAsMTQ4MnoiPjwvcGF0aD4KICAgICAgICA8L2c+CiAgICA8L2c+CjwvZz4KPC9zdmc+",
106
+ webUrl: "https://www.okx.com/"
107
+ };
108
+ var registerOkxProvider = () => {
109
+ if (typeof window === "undefined") return;
110
+ if (!window.OkxStacksProvider) {
111
+ window.OkxStacksProvider = {
112
+ request: async (method) => {
113
+ if (method === "getAddresses") {
114
+ const data = await getOKXStacksAddress();
115
+ return {
116
+ result: {
117
+ addresses: [
118
+ { address: data.address, symbol: "STX" }
119
+ ]
120
+ }
121
+ };
122
+ }
123
+ throw new Error(
124
+ `OKX adapter: unsupported method "${method}". Use connect('okx') for direct OKX SDK access.`
125
+ );
126
+ }
127
+ };
128
+ }
129
+ };
130
+ var unregisterOkxProvider = () => {
131
+ if (typeof window === "undefined") return;
132
+ delete window.OkxStacksProvider;
133
+ };
95
134
  var extractStacksAddress = (typedProvider, addresses) => {
96
135
  if (!addresses.length) {
97
136
  throw new Error(`No addresses provided for ${typedProvider} wallet`);
@@ -223,12 +262,21 @@ var getLocalStorageWallet = () => {
223
262
  return null;
224
263
  }
225
264
  };
265
+ var PROVIDER_META_BY_KIT_ID = Object.fromEntries(
266
+ [...connect.DEFAULT_PROVIDERS, connect.WALLET_CONNECT_PROVIDER, OKX_PROVIDER_META].map(
267
+ (p) => [
268
+ STACKS_CONNECT_TO_STACKS_PROVIDERS[p.id],
269
+ { name: p.name, icon: p.icon ?? "", webUrl: p.webUrl ?? "" }
270
+ ]
271
+ )
272
+ );
226
273
  var StacksWalletContext = react.createContext(
227
274
  void 0
228
275
  );
229
276
  var StacksWalletProvider = ({
230
277
  children,
231
278
  wallets,
279
+ connectModal = true,
232
280
  walletConnect,
233
281
  onConnect,
234
282
  onAddressChange,
@@ -238,15 +286,23 @@ var StacksWalletProvider = ({
238
286
  const [provider, setProvider] = react.useState();
239
287
  const [isConnecting, setIsConnecting] = react.useState(false);
240
288
  const connectGenRef = react.useRef(0);
289
+ const wasConnectedRef = react.useRef(false);
241
290
  const wcInitRef = react.useRef(null);
291
+ const walletConnectRef = react.useRef(walletConnect);
292
+ walletConnectRef.current = walletConnect;
242
293
  const walletsKey = wallets?.join(",");
294
+ if (wallets?.includes("wallet-connect") && !walletConnect?.projectId) {
295
+ throw new Error(
296
+ 'StacksWalletProvider: "wallet-connect" is listed in wallets but no walletConnect.projectId was provided.'
297
+ );
298
+ }
243
299
  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
- );
300
+ const okxConfigured = !wallets || wallets.includes("okx");
301
+ if (connectModal && okxConfigured) {
302
+ registerOkxProvider();
303
+ return () => unregisterOkxProvider();
248
304
  }
249
- }, [walletsKey, walletConnect?.projectId]);
305
+ }, [connectModal, walletsKey]);
250
306
  react.useEffect(() => {
251
307
  const loadPersistedWallet = async () => {
252
308
  const persisted = getLocalStorageWallet();
@@ -286,6 +342,65 @@ var StacksWalletProvider = ({
286
342
  }, [walletConnect?.projectId]);
287
343
  const connect$1 = react.useCallback(
288
344
  async (providerId, options) => {
345
+ if (connectModal && !providerId) {
346
+ const gen2 = ++connectGenRef.current;
347
+ setIsConnecting(true);
348
+ try {
349
+ connect.clearSelectedProviderId();
350
+ const requestOptions = {
351
+ forceWalletSelect: true,
352
+ // OKX at the end so it appears last among installed wallets
353
+ defaultProviders: [
354
+ ...connect.DEFAULT_PROVIDERS,
355
+ OKX_PROVIDER_META
356
+ ]
357
+ };
358
+ if (wallets) {
359
+ requestOptions.approvedProviderIds = wallets.map(
360
+ (w) => STACKS_TO_STACKS_CONNECT_PROVIDERS[w]
361
+ );
362
+ }
363
+ const wc2 = walletConnectRef.current;
364
+ if (wc2?.projectId) {
365
+ requestOptions.walletConnect = buildWalletConnectConfig(
366
+ wc2.projectId,
367
+ wc2.metadata,
368
+ wc2.chains
369
+ );
370
+ }
371
+ const data = await connect.request(
372
+ requestOptions,
373
+ "getAddresses",
374
+ {}
375
+ );
376
+ if (connectGenRef.current !== gen2) return;
377
+ const selectedId = connect.getSelectedProviderId();
378
+ const resolvedProvider = selectedId ? STACKS_CONNECT_TO_STACKS_PROVIDERS[selectedId] : void 0;
379
+ if (!resolvedProvider) {
380
+ throw new Error(
381
+ `Unknown provider returned from @stacks/connect modal: ${selectedId ?? "none"}`
382
+ );
383
+ }
384
+ const extractedAddress = extractStacksAddress(
385
+ resolvedProvider,
386
+ data.addresses
387
+ );
388
+ setAddress(extractedAddress);
389
+ setProvider(resolvedProvider);
390
+ options?.onSuccess?.(extractedAddress, resolvedProvider);
391
+ } catch (error) {
392
+ if (connectGenRef.current !== gen2) return;
393
+ console.error("Failed to connect wallet:", error);
394
+ connect.getSelectedProvider()?.disconnect?.();
395
+ connect.clearSelectedProviderId();
396
+ options?.onError?.(error);
397
+ } finally {
398
+ if (connectGenRef.current === gen2) {
399
+ setIsConnecting(false);
400
+ }
401
+ }
402
+ return;
403
+ }
289
404
  const typedProvider = SUPPORTED_STACKS_WALLETS.find(
290
405
  (wallet) => wallet === providerId
291
406
  );
@@ -305,7 +420,8 @@ var StacksWalletProvider = ({
305
420
  options?.onError?.(error);
306
421
  return;
307
422
  }
308
- if (typedProvider === "wallet-connect" && !walletConnect?.projectId) {
423
+ const wc = walletConnectRef.current;
424
+ if (typedProvider === "wallet-connect" && !wc?.projectId) {
309
425
  const error = new Error(
310
426
  "WalletConnect requires a project ID. Please provide walletConnect.projectId to the StacksWalletProvider."
311
427
  );
@@ -327,10 +443,10 @@ var StacksWalletProvider = ({
327
443
  connect.setSelectedProviderId(
328
444
  STACKS_TO_STACKS_CONNECT_PROVIDERS[typedProvider]
329
445
  );
330
- const wcConfig = typedProvider === "wallet-connect" && walletConnect ? buildWalletConnectConfig(
331
- walletConnect.projectId,
332
- walletConnect.metadata,
333
- walletConnect.chains
446
+ const wcConfig = typedProvider === "wallet-connect" && wc ? buildWalletConnectConfig(
447
+ wc.projectId,
448
+ wc.metadata,
449
+ wc.chains
334
450
  ) : void 0;
335
451
  if (wcConfig) {
336
452
  if (wcInitRef.current) await wcInitRef.current;
@@ -367,7 +483,7 @@ var StacksWalletProvider = ({
367
483
  }
368
484
  }
369
485
  },
370
- [walletConnect]
486
+ [connectModal, walletsKey]
371
487
  );
372
488
  const reset = react.useCallback(() => {
373
489
  connectGenRef.current++;
@@ -394,24 +510,35 @@ var StacksWalletProvider = ({
394
510
  );
395
511
  }, [address, provider]);
396
512
  react.useEffect(() => {
397
- if (!address || !provider || !onConnect) return;
398
- onConnect(provider, address);
513
+ const isConnected = !!address && !!provider;
514
+ if (isConnected && !wasConnectedRef.current) {
515
+ onConnect?.(provider, address);
516
+ }
517
+ wasConnectedRef.current = isConnected;
399
518
  }, [address, provider, onConnect]);
400
- useXverse({
401
- address,
402
- provider,
403
- onAddressChange: (newAddress) => {
519
+ const handleAddressChange = react.useCallback(
520
+ (newAddress) => {
404
521
  setAddress(newAddress);
405
522
  onAddressChange?.(newAddress);
406
523
  },
524
+ [onAddressChange]
525
+ );
526
+ useXverse({
527
+ address,
528
+ provider,
529
+ onAddressChange: handleAddressChange,
407
530
  connect: connect$1
408
531
  });
409
532
  const { installed } = getStacksWallets();
410
533
  const configured = wallets ?? [...SUPPORTED_STACKS_WALLETS];
411
534
  const walletInfos = configured.map((w) => ({
412
535
  id: w,
536
+ name: PROVIDER_META_BY_KIT_ID[w]?.name ?? w,
537
+ icon: PROVIDER_META_BY_KIT_ID[w]?.icon ?? "",
538
+ webUrl: PROVIDER_META_BY_KIT_ID[w]?.webUrl ?? "",
413
539
  available: w === "wallet-connect" ? !!walletConnect?.projectId : installed.includes(w)
414
540
  }));
541
+ const walletInfosKey = walletInfos.map((w) => `${w.id}:${w.available}`).join(",");
415
542
  const value = react.useMemo(() => {
416
543
  const walletState = isConnecting ? { status: "connecting", address: void 0, provider: void 0 } : address && provider ? { status: "connected", address, provider } : {
417
544
  status: "disconnected",
@@ -425,7 +552,7 @@ var StacksWalletProvider = ({
425
552
  reset,
426
553
  wallets: walletInfos
427
554
  };
428
- }, [address, provider, isConnecting, connect$1, disconnect, reset, walletInfos]);
555
+ }, [address, provider, isConnecting, connect$1, disconnect, reset, walletInfosKey]);
429
556
  return /* @__PURE__ */ jsxRuntime.jsx(StacksWalletContext.Provider, { value, children });
430
557
  };
431
558
  var useStacksWalletContext = () => {
@@ -741,20 +868,28 @@ var useBnsName = (address) => {
741
868
  setIsLoading(false);
742
869
  return;
743
870
  }
871
+ let cancelled = false;
744
872
  const fetchBnsName = async () => {
745
873
  setIsLoading(true);
746
874
  try {
747
875
  const network = getNetworkFromAddress(address);
748
876
  const result = await bnsV2Sdk.getPrimaryName({ address, network });
877
+ if (cancelled) return;
749
878
  const fullName = result ? `${result.name}.${result.namespace}` : null;
750
879
  setBnsName(fullName);
751
880
  } catch {
881
+ if (cancelled) return;
752
882
  setBnsName(null);
753
883
  } finally {
754
- setIsLoading(false);
884
+ if (!cancelled) {
885
+ setIsLoading(false);
886
+ }
755
887
  }
756
888
  };
757
889
  void fetchBnsName();
890
+ return () => {
891
+ cancelled = true;
892
+ };
758
893
  }, [address]);
759
894
  return { bnsName, isLoading };
760
895
  };