@satoshai/kit 0.1.0 → 0.2.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Joao Faustino
3
+ Copyright (c) 2026 Satoshai
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # @satoshai/kit
2
+
3
+ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired hook API for connecting wallets, signing messages, and calling contracts on the Stacks blockchain.
4
+
5
+ ## Features
6
+
7
+ - **`StacksWalletProvider`** — React context provider for wallet state
8
+ - **`useConnect` / `useDisconnect`** — Connect and disconnect wallets
9
+ - **`useWallets`** — Configured wallets with availability status
10
+ - **`useAddress`** — Access connected wallet address and status
11
+ - **`useSignMessage`** — Sign arbitrary messages
12
+ - **`useWriteContract`** — Call smart contracts with post-conditions
13
+ - **`useBnsName`** — Resolve BNS v2 names
14
+ - **6 wallets supported** — Xverse, Leather, OKX, Asigna, Fordefi, WalletConnect
15
+ - **Next.js App Router compatible** — `"use client"` directives included
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pnpm add @satoshai/kit @stacks/transactions react react-dom
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ```tsx
26
+ import { StacksWalletProvider, useConnect, useWallets, useAddress, useDisconnect } from '@satoshai/kit';
27
+
28
+ function App() {
29
+ return (
30
+ <StacksWalletProvider>
31
+ <Wallet />
32
+ </StacksWalletProvider>
33
+ );
34
+ }
35
+
36
+ function Wallet() {
37
+ const { connect, reset, isPending } = useConnect();
38
+ const { wallets } = useWallets();
39
+ const { address, isConnected } = useAddress();
40
+ const { disconnect } = useDisconnect();
41
+
42
+ if (isConnected) {
43
+ return (
44
+ <div>
45
+ <p>Connected: {address}</p>
46
+ <button onClick={() => disconnect()}>Disconnect</button>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <div>
53
+ {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
+ ))}
59
+ </div>
60
+ );
61
+ }
62
+ ```
63
+
64
+ ## API
65
+
66
+ ### `<StacksWalletProvider>`
67
+
68
+ Wrap your app to provide wallet context to all hooks.
69
+
70
+ ```tsx
71
+ <StacksWalletProvider
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
77
+ >
78
+ {children}
79
+ </StacksWalletProvider>
80
+ ```
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
+
86
+ ### `useConnect()`
87
+
88
+ ```ts
89
+ const { connect, reset, isPending } = useConnect();
90
+
91
+ await connect('xverse');
92
+ await connect('leather', {
93
+ onSuccess: (address, provider) => {},
94
+ onError: (error) => {},
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
+ ))}
116
+ ```
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
+
120
+ ### `useDisconnect()`
121
+
122
+ ```ts
123
+ const { disconnect } = useDisconnect();
124
+
125
+ disconnect();
126
+ disconnect(() => { /* callback after disconnect */ });
127
+ ```
128
+
129
+ ### `useAddress()`
130
+
131
+ ```ts
132
+ const { address, isConnected, isConnecting, isDisconnected, provider } = useAddress();
133
+
134
+ if (isConnected) {
135
+ console.log(address); // 'SP...' or 'ST...'
136
+ console.log(provider); // 'xverse' | 'leather' | ...
137
+ }
138
+ ```
139
+
140
+ ### `useSignMessage()`
141
+
142
+ ```ts
143
+ const { signMessage, signMessageAsync, data, error, isPending } = useSignMessage();
144
+
145
+ // Callback style
146
+ signMessage({ message: 'Hello Stacks' }, {
147
+ onSuccess: ({ publicKey, signature }) => {},
148
+ onError: (error) => {},
149
+ });
150
+
151
+ // Async style
152
+ const { publicKey, signature } = await signMessageAsync({ message: 'Hello Stacks' });
153
+ ```
154
+
155
+ ### `useWriteContract()`
156
+
157
+ ```ts
158
+ import { Pc, PostConditionMode } from '@stacks/transactions';
159
+
160
+ const { writeContract, writeContractAsync, data, error, isPending } = useWriteContract();
161
+
162
+ writeContract({
163
+ address: 'SP...',
164
+ contract: 'my-contract',
165
+ functionName: 'my-function',
166
+ args: [uintCV(100)],
167
+ pc: {
168
+ postConditions: [Pc.principal('SP...').willSendLte(100n).ustx()],
169
+ mode: PostConditionMode.Deny,
170
+ },
171
+ }, {
172
+ onSuccess: (txHash) => {},
173
+ onError: (error) => {},
174
+ });
175
+ ```
176
+
177
+ ### `useBnsName()`
178
+
179
+ ```ts
180
+ const { bnsName, isLoading } = useBnsName(address);
181
+ // bnsName = 'satoshi.btc' | null
182
+ ```
183
+
184
+ ### Utilities
185
+
186
+ ```ts
187
+ import { getNetworkFromAddress, getStacksWallets, getLocalStorageWallet } from '@satoshai/kit';
188
+
189
+ getNetworkFromAddress('SP...'); // 'mainnet'
190
+ getNetworkFromAddress('ST...'); // 'testnet'
191
+
192
+ const { supported, installed } = getStacksWallets();
193
+ ```
194
+
195
+ ## Supported Wallets
196
+
197
+ | Wallet | ID |
198
+ |---|---|
199
+ | Xverse | `xverse` |
200
+ | Leather | `leather` |
201
+ | OKX | `okx` |
202
+ | Asigna | `asigna` |
203
+ | Fordefi | `fordefi` |
204
+ | WalletConnect | `wallet-connect` |
205
+
206
+ ## Peer Dependencies
207
+
208
+ - `react` ^18 or ^19
209
+ - `react-dom` ^18 or ^19
210
+ - `@stacks/transactions` >=7.0.0
211
+
212
+ ## License
213
+
214
+ MIT
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,14 +461,14 @@ var useAddress = () => {
404
461
  }, [address, status, provider]);
405
462
  };
406
463
  var useConnect = () => {
407
- const { connect, status } = useStacksWalletContext();
464
+ const { connect, reset, status } = useStacksWalletContext();
408
465
  const value = react.useMemo(
409
466
  () => ({
410
467
  connect,
411
- connectors: SUPPORTED_STACKS_WALLETS,
468
+ reset,
412
469
  isPending: status === "connecting"
413
470
  }),
414
- [connect, status]
471
+ [connect, reset, status]
415
472
  );
416
473
  return value;
417
474
  };
@@ -606,6 +663,10 @@ var useBnsName = (address) => {
606
663
  }, [address]);
607
664
  return { bnsName, isLoading };
608
665
  };
666
+ var useWallets = () => {
667
+ const { wallets } = useStacksWalletContext();
668
+ return react.useMemo(() => ({ wallets }), [wallets]);
669
+ };
609
670
 
610
671
  exports.SUPPORTED_STACKS_WALLETS = SUPPORTED_STACKS_WALLETS;
611
672
  exports.StacksWalletProvider = StacksWalletProvider;
@@ -617,6 +678,7 @@ exports.useBnsName = useBnsName;
617
678
  exports.useConnect = useConnect;
618
679
  exports.useDisconnect = useDisconnect;
619
680
  exports.useSignMessage = useSignMessage;
681
+ exports.useWallets = useWallets;
620
682
  exports.useWriteContract = useWriteContract;
621
683
  //# sourceMappingURL=index.cjs.map
622
684
  //# sourceMappingURL=index.cjs.map