@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 +1 -1
- package/README.md +214 -0
- package/dist/index.cjs +79 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -3
- package/dist/index.d.ts +14 -3
- package/dist/index.js +80 -19
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
package/LICENSE
CHANGED
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
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
322
|
-
|
|
359
|
+
if (typedProvider !== "okx") {
|
|
360
|
+
connect.getSelectedProvider()?.disconnect?.();
|
|
361
|
+
connect.clearSelectedProviderId();
|
|
362
|
+
}
|
|
323
363
|
options?.onError?.(error);
|
|
324
364
|
} finally {
|
|
325
|
-
|
|
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
|
-
|
|
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
|