@satoshai/kit 0.3.0 → 0.4.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/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,
@@ -239,14 +287,21 @@ var StacksWalletProvider = ({
239
287
  const [isConnecting, setIsConnecting] = react.useState(false);
240
288
  const connectGenRef = react.useRef(0);
241
289
  const wcInitRef = react.useRef(null);
290
+ const walletConnectRef = react.useRef(walletConnect);
291
+ walletConnectRef.current = walletConnect;
242
292
  const walletsKey = wallets?.join(",");
293
+ if (wallets?.includes("wallet-connect") && !walletConnect?.projectId) {
294
+ throw new Error(
295
+ 'StacksWalletProvider: "wallet-connect" is listed in wallets but no walletConnect.projectId was provided.'
296
+ );
297
+ }
243
298
  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
- );
299
+ const okxConfigured = !wallets || wallets.includes("okx");
300
+ if (connectModal && okxConfigured) {
301
+ registerOkxProvider();
302
+ return () => unregisterOkxProvider();
248
303
  }
249
- }, [walletsKey, walletConnect?.projectId]);
304
+ }, [connectModal, walletsKey]);
250
305
  react.useEffect(() => {
251
306
  const loadPersistedWallet = async () => {
252
307
  const persisted = getLocalStorageWallet();
@@ -286,6 +341,65 @@ var StacksWalletProvider = ({
286
341
  }, [walletConnect?.projectId]);
287
342
  const connect$1 = react.useCallback(
288
343
  async (providerId, options) => {
344
+ if (connectModal && !providerId) {
345
+ const gen2 = ++connectGenRef.current;
346
+ setIsConnecting(true);
347
+ try {
348
+ connect.clearSelectedProviderId();
349
+ const requestOptions = {
350
+ forceWalletSelect: true,
351
+ // OKX at the end so it appears last among installed wallets
352
+ defaultProviders: [
353
+ ...connect.DEFAULT_PROVIDERS,
354
+ OKX_PROVIDER_META
355
+ ]
356
+ };
357
+ if (wallets) {
358
+ requestOptions.approvedProviderIds = wallets.map(
359
+ (w) => STACKS_TO_STACKS_CONNECT_PROVIDERS[w]
360
+ );
361
+ }
362
+ const wc2 = walletConnectRef.current;
363
+ if (wc2?.projectId) {
364
+ requestOptions.walletConnect = buildWalletConnectConfig(
365
+ wc2.projectId,
366
+ wc2.metadata,
367
+ wc2.chains
368
+ );
369
+ }
370
+ const data = await connect.request(
371
+ requestOptions,
372
+ "getAddresses",
373
+ {}
374
+ );
375
+ if (connectGenRef.current !== gen2) return;
376
+ const selectedId = connect.getSelectedProviderId();
377
+ const resolvedProvider = selectedId ? STACKS_CONNECT_TO_STACKS_PROVIDERS[selectedId] : void 0;
378
+ if (!resolvedProvider) {
379
+ throw new Error(
380
+ `Unknown provider returned from @stacks/connect modal: ${selectedId ?? "none"}`
381
+ );
382
+ }
383
+ const extractedAddress = extractStacksAddress(
384
+ resolvedProvider,
385
+ data.addresses
386
+ );
387
+ setAddress(extractedAddress);
388
+ setProvider(resolvedProvider);
389
+ options?.onSuccess?.(extractedAddress, resolvedProvider);
390
+ } catch (error) {
391
+ if (connectGenRef.current !== gen2) return;
392
+ console.error("Failed to connect wallet:", error);
393
+ connect.getSelectedProvider()?.disconnect?.();
394
+ connect.clearSelectedProviderId();
395
+ options?.onError?.(error);
396
+ } finally {
397
+ if (connectGenRef.current === gen2) {
398
+ setIsConnecting(false);
399
+ }
400
+ }
401
+ return;
402
+ }
289
403
  const typedProvider = SUPPORTED_STACKS_WALLETS.find(
290
404
  (wallet) => wallet === providerId
291
405
  );
@@ -305,7 +419,8 @@ var StacksWalletProvider = ({
305
419
  options?.onError?.(error);
306
420
  return;
307
421
  }
308
- if (typedProvider === "wallet-connect" && !walletConnect?.projectId) {
422
+ const wc = walletConnectRef.current;
423
+ if (typedProvider === "wallet-connect" && !wc?.projectId) {
309
424
  const error = new Error(
310
425
  "WalletConnect requires a project ID. Please provide walletConnect.projectId to the StacksWalletProvider."
311
426
  );
@@ -327,10 +442,10 @@ var StacksWalletProvider = ({
327
442
  connect.setSelectedProviderId(
328
443
  STACKS_TO_STACKS_CONNECT_PROVIDERS[typedProvider]
329
444
  );
330
- const wcConfig = typedProvider === "wallet-connect" && walletConnect ? buildWalletConnectConfig(
331
- walletConnect.projectId,
332
- walletConnect.metadata,
333
- walletConnect.chains
445
+ const wcConfig = typedProvider === "wallet-connect" && wc ? buildWalletConnectConfig(
446
+ wc.projectId,
447
+ wc.metadata,
448
+ wc.chains
334
449
  ) : void 0;
335
450
  if (wcConfig) {
336
451
  if (wcInitRef.current) await wcInitRef.current;
@@ -367,7 +482,7 @@ var StacksWalletProvider = ({
367
482
  }
368
483
  }
369
484
  },
370
- [walletConnect]
485
+ [connectModal, walletsKey]
371
486
  );
372
487
  const reset = react.useCallback(() => {
373
488
  connectGenRef.current++;
@@ -397,23 +512,29 @@ var StacksWalletProvider = ({
397
512
  if (!address || !provider || !onConnect) return;
398
513
  onConnect(provider, address);
399
514
  }, [address, provider, onConnect]);
400
- useXverse({
401
- address,
402
- provider,
403
- onAddressChange: (newAddress) => {
515
+ const handleAddressChange = react.useCallback(
516
+ (newAddress) => {
404
517
  setAddress(newAddress);
405
518
  onAddressChange?.(newAddress);
406
519
  },
520
+ [onAddressChange]
521
+ );
522
+ useXverse({
523
+ address,
524
+ provider,
525
+ onAddressChange: handleAddressChange,
407
526
  connect: connect$1
408
527
  });
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]);
528
+ const { installed } = getStacksWallets();
529
+ const configured = wallets ?? [...SUPPORTED_STACKS_WALLETS];
530
+ const walletInfos = configured.map((w) => ({
531
+ id: w,
532
+ name: PROVIDER_META_BY_KIT_ID[w]?.name ?? w,
533
+ icon: PROVIDER_META_BY_KIT_ID[w]?.icon ?? "",
534
+ webUrl: PROVIDER_META_BY_KIT_ID[w]?.webUrl ?? "",
535
+ available: w === "wallet-connect" ? !!walletConnect?.projectId : installed.includes(w)
536
+ }));
537
+ const walletInfosKey = walletInfos.map((w) => `${w.id}:${w.available}`).join(",");
417
538
  const value = react.useMemo(() => {
418
539
  const walletState = isConnecting ? { status: "connecting", address: void 0, provider: void 0 } : address && provider ? { status: "connected", address, provider } : {
419
540
  status: "disconnected",
@@ -427,7 +548,7 @@ var StacksWalletProvider = ({
427
548
  reset,
428
549
  wallets: walletInfos
429
550
  };
430
- }, [address, provider, isConnecting, connect$1, disconnect, reset, walletInfos]);
551
+ }, [address, provider, isConnecting, connect$1, disconnect, reset, walletInfosKey]);
431
552
  return /* @__PURE__ */ jsxRuntime.jsx(StacksWalletContext.Provider, { value, children });
432
553
  };
433
554
  var useStacksWalletContext = () => {