|
6 | 6 |
|
7 | 7 | Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired hook API for connecting wallets, signing messages, and calling contracts on the Stacks blockchain. |
8 | 8 |
|
9 | | -See the full documentation in [`packages/kit/README.md`](./packages/kit/README.md). |
| 9 | +## Features |
| 10 | + |
| 11 | +- **`StacksWalletProvider`** — React context provider for wallet state |
| 12 | +- **`useConnect` / `useDisconnect`** — Connect and disconnect wallets |
| 13 | +- **`useWallets`** — Configured wallets with availability status |
| 14 | +- **`useAddress`** — Access connected wallet address and status |
| 15 | +- **`useSignMessage`** — Sign arbitrary messages |
| 16 | +- **`useSignStructuredMessage`** — Sign SIP-018 structured data |
| 17 | +- **`useSignTransaction`** — Sign serialized transactions (sponsored tx flows) |
| 18 | +- **`useWriteContract`** — Call smart contracts with post-conditions |
| 19 | +- **`useTransferSTX`** — Native STX transfers |
| 20 | +- **`useBnsName`** — Resolve BNS v2 names |
| 21 | +- **6 wallets supported** — Xverse, Leather, OKX, Asigna, Fordefi, WalletConnect |
| 22 | +- **Next.js App Router compatible** — `"use client"` directives included |
| 23 | + |
| 24 | +## Install |
| 25 | + |
| 26 | +```bash |
| 27 | +pnpm add @satoshai/kit @stacks/transactions react react-dom |
| 28 | +``` |
| 29 | + |
| 30 | +## Quick Start |
| 31 | + |
| 32 | +```tsx |
| 33 | +import { StacksWalletProvider, useConnect, useAddress, useDisconnect } from '@satoshai/kit'; |
| 34 | + |
| 35 | +function App() { |
| 36 | + return ( |
| 37 | + <StacksWalletProvider> |
| 38 | + <Wallet /> |
| 39 | + </StacksWalletProvider> |
| 40 | + ); |
| 41 | +} |
| 42 | + |
| 43 | +function Wallet() { |
| 44 | + const { connect, reset, isPending } = useConnect(); |
| 45 | + const { address, isConnected } = useAddress(); |
| 46 | + const { disconnect } = useDisconnect(); |
| 47 | + |
| 48 | + if (isConnected) { |
| 49 | + return ( |
| 50 | + <div> |
| 51 | + <p>Connected: {address}</p> |
| 52 | + <button onClick={() => disconnect()}>Disconnect</button> |
| 53 | + </div> |
| 54 | + ); |
| 55 | + } |
| 56 | + |
| 57 | + return ( |
| 58 | + <div> |
| 59 | + {isPending && <button onClick={reset}>Cancel</button>} |
| 60 | + <button onClick={() => connect()} disabled={isPending}>Connect Wallet</button> |
| 61 | + </div> |
| 62 | + ); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +## API |
| 67 | + |
| 68 | +### `<StacksWalletProvider>` |
| 69 | + |
| 70 | +Wrap your app to provide wallet context to all hooks. |
| 71 | + |
| 72 | +```tsx |
| 73 | +<StacksWalletProvider |
| 74 | + wallets={['xverse', 'leather', 'wallet-connect']} // optional — defaults to all supported |
| 75 | + connectModal={true} // optional — defaults to true |
| 76 | + walletConnect={{ projectId: '...' }} // optional — enables WalletConnect |
| 77 | + onConnect={(provider, address) => {}} // optional |
| 78 | + onAddressChange={(newAddress) => {}} // optional — Xverse account switching |
| 79 | + onDisconnect={() => {}} // optional |
| 80 | +> |
| 81 | + {children} |
| 82 | +</StacksWalletProvider> |
| 83 | +``` |
| 84 | + |
| 85 | +> If `wallets` includes `'wallet-connect'`, you must provide `walletConnect.projectId` or the provider will throw at mount. |
| 86 | +
|
| 87 | +> **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. |
| 88 | +
|
| 89 | +#### `connectModal` (default: `true`) |
| 90 | + |
| 91 | +Controls whether `connect()` with no arguments shows `@stacks/connect`'s built-in wallet selection modal. |
| 92 | + |
| 93 | +```tsx |
| 94 | +// Default — modal handles wallet selection |
| 95 | +<StacksWalletProvider> |
| 96 | + <App /> {/* connect() opens the modal */} |
| 97 | +</StacksWalletProvider> |
| 98 | + |
| 99 | +// Headless — manage wallet selection yourself (wagmi-style) |
| 100 | +<StacksWalletProvider connectModal={false}> |
| 101 | + <App /> {/* connect('xverse') only, connect() with no args is a no-op */} |
| 102 | +</StacksWalletProvider> |
| 103 | +``` |
| 104 | + |
| 105 | +When `connectModal` is enabled: |
| 106 | +- `connect()` with no args opens the `@stacks/connect` modal |
| 107 | +- `connect('xverse')` with an explicit provider still bypasses the modal |
| 108 | +- The `wallets` prop controls which wallets appear in the modal |
| 109 | +- All 6 wallets are supported in the modal |
| 110 | +- After the user picks a wallet, the kit automatically maps it back and sets state |
| 111 | + |
| 112 | +### `useConnect()` |
| 113 | + |
| 114 | +```ts |
| 115 | +const { connect, reset, isPending } = useConnect(); |
| 116 | + |
| 117 | +// Open the @stacks/connect modal (when connectModal is enabled, the default) |
| 118 | +await connect(); |
| 119 | + |
| 120 | +// Or connect to a specific wallet directly |
| 121 | +await connect('xverse'); |
| 122 | +await connect('leather', { |
| 123 | + onSuccess: (address, provider) => {}, |
| 124 | + onError: (error) => {}, |
| 125 | +}); |
| 126 | + |
| 127 | +// Reset stuck connecting state (e.g. when a wallet modal is dismissed) |
| 128 | +reset(); |
| 129 | +``` |
| 130 | + |
| 131 | +> **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. |
| 132 | +
|
| 133 | +### `useWallets()` |
| 134 | + |
| 135 | +Returns all configured wallets with their name, icon, download link, and availability status. Metadata is sourced from `@stacks/connect`. |
| 136 | + |
| 137 | +```ts |
| 138 | +const { wallets } = useWallets(); |
| 139 | +// [{ id: 'xverse', name: 'Xverse Wallet', icon: 'data:image/svg+xml;...', webUrl: 'https://xverse.app', available: true }, ...] |
| 140 | + |
| 141 | +{wallets.map(({ id, name, icon, webUrl, available }) => ( |
| 142 | + <div key={id}> |
| 143 | + <button onClick={() => connect(id)} disabled={!available}> |
| 144 | + {icon && <img src={icon} alt={name} width={20} height={20} />} |
| 145 | + {name} |
| 146 | + </button> |
| 147 | + {!available && webUrl && <a href={webUrl} target="_blank">Install</a>} |
| 148 | + </div> |
| 149 | +))} |
| 150 | +``` |
| 151 | + |
| 152 | +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. |
| 153 | + |
| 154 | +### `useDisconnect()` |
| 155 | + |
| 156 | +```ts |
| 157 | +const { disconnect } = useDisconnect(); |
| 158 | + |
| 159 | +disconnect(); |
| 160 | +disconnect(() => { /* callback after disconnect */ }); |
| 161 | +``` |
| 162 | + |
| 163 | +### `useAddress()` |
| 164 | + |
| 165 | +```ts |
| 166 | +const { address, isConnected, isConnecting, isDisconnected, provider } = useAddress(); |
| 167 | + |
| 168 | +if (isConnected) { |
| 169 | + console.log(address); // 'SP...' or 'ST...' |
| 170 | + console.log(provider); // 'xverse' | 'leather' | ... |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +### `useSignMessage()` |
| 175 | + |
| 176 | +```ts |
| 177 | +const { signMessage, signMessageAsync, data, error, isPending } = useSignMessage(); |
| 178 | + |
| 179 | +// Callback style |
| 180 | +signMessage({ message: 'Hello Stacks' }, { |
| 181 | + onSuccess: ({ publicKey, signature }) => {}, |
| 182 | + onError: (error) => {}, |
| 183 | +}); |
| 184 | + |
| 185 | +// Async style |
| 186 | +const { publicKey, signature } = await signMessageAsync({ message: 'Hello Stacks' }); |
| 187 | +``` |
| 188 | + |
| 189 | +### `useSignStructuredMessage()` |
| 190 | + |
| 191 | +Sign SIP-018 structured data for typed, verifiable off-chain messages. |
| 192 | + |
| 193 | +> **Note:** OKX wallet does not support structured message signing and will throw an error. |
| 194 | +
|
| 195 | +```ts |
| 196 | +import { tupleCV, stringAsciiCV, uintCV } from '@stacks/transactions'; |
| 197 | + |
| 198 | +const { signStructuredMessage, signStructuredMessageAsync, data, error, isPending } = useSignStructuredMessage(); |
| 199 | + |
| 200 | +// Callback style |
| 201 | +signStructuredMessage({ |
| 202 | + domain: tupleCV({ |
| 203 | + name: stringAsciiCV('MyApp'), |
| 204 | + version: stringAsciiCV('1.0'), |
| 205 | + 'chain-id': uintCV(1), |
| 206 | + }), |
| 207 | + message: tupleCV({ |
| 208 | + action: stringAsciiCV('authorize'), |
| 209 | + amount: uintCV(1000), |
| 210 | + }), |
| 211 | +}, { |
| 212 | + onSuccess: ({ publicKey, signature }) => {}, |
| 213 | + onError: (error) => {}, |
| 214 | +}); |
| 215 | + |
| 216 | +// Async style |
| 217 | +const { publicKey, signature } = await signStructuredMessageAsync({ |
| 218 | + domain: tupleCV({ ... }), |
| 219 | + message: tupleCV({ ... }), |
| 220 | +}); |
| 221 | +``` |
| 222 | + |
| 223 | +### `useTransferSTX()` |
| 224 | + |
| 225 | +```ts |
| 226 | +const { transferSTX, transferSTXAsync, data, error, isPending } = useTransferSTX(); |
| 227 | + |
| 228 | +// Callback style |
| 229 | +transferSTX({ |
| 230 | + recipient: 'SP2...', |
| 231 | + amount: 1000000n, // in microSTX |
| 232 | + memo: 'optional memo', |
| 233 | +}, { |
| 234 | + onSuccess: (txid) => {}, |
| 235 | + onError: (error) => {}, |
| 236 | +}); |
| 237 | + |
| 238 | +// Async style |
| 239 | +const txid = await transferSTXAsync({ |
| 240 | + recipient: 'SP2...', |
| 241 | + amount: 1000000n, |
| 242 | +}); |
| 243 | +``` |
| 244 | + |
| 245 | +### `useWriteContract()` |
| 246 | + |
| 247 | +```ts |
| 248 | +import { Pc, PostConditionMode } from '@stacks/transactions'; |
| 249 | + |
| 250 | +const { writeContract, writeContractAsync, data, error, isPending } = useWriteContract(); |
| 251 | + |
| 252 | +writeContract({ |
| 253 | + address: 'SP...', |
| 254 | + contract: 'my-contract', |
| 255 | + functionName: 'my-function', |
| 256 | + args: [uintCV(100)], |
| 257 | + pc: { |
| 258 | + postConditions: [Pc.principal('SP...').willSendLte(100n).ustx()], |
| 259 | + mode: PostConditionMode.Deny, |
| 260 | + }, |
| 261 | +}, { |
| 262 | + onSuccess: (txHash) => {}, |
| 263 | + onError: (error) => {}, |
| 264 | +}); |
| 265 | +``` |
| 266 | + |
| 267 | +### `useSignTransaction()` |
| 268 | + |
| 269 | +Sign a serialized transaction without automatically broadcasting it. Useful for sponsored transaction flows where a separate service pays the fee. |
| 270 | + |
| 271 | +> **Note:** OKX wallet does not support raw transaction signing and will throw an error. |
| 272 | +
|
| 273 | +```ts |
| 274 | +const { signTransaction, signTransactionAsync, data, error, isPending } = useSignTransaction(); |
| 275 | + |
| 276 | +// Callback style |
| 277 | +signTransaction({ transaction: '0x0100...', broadcast: false }, { |
| 278 | + onSuccess: ({ transaction, txid }) => {}, |
| 279 | + onError: (error) => {}, |
| 280 | +}); |
| 281 | + |
| 282 | +// Async style |
| 283 | +const { transaction, txid } = await signTransactionAsync({ |
| 284 | + transaction: '0x0100...', |
| 285 | + broadcast: false, |
| 286 | +}); |
| 287 | +``` |
| 288 | + |
| 289 | +### `useBnsName()` |
| 290 | + |
| 291 | +```ts |
| 292 | +const { bnsName, isLoading } = useBnsName(address); |
| 293 | +// bnsName = 'satoshi.btc' | null |
| 294 | +``` |
| 295 | + |
| 296 | +### Utilities |
| 297 | + |
| 298 | +```ts |
| 299 | +import { getNetworkFromAddress, getStacksWallets, getLocalStorageWallet } from '@satoshai/kit'; |
| 300 | + |
| 301 | +getNetworkFromAddress('SP...'); // 'mainnet' |
| 302 | +getNetworkFromAddress('ST...'); // 'testnet' |
| 303 | + |
| 304 | +const { supported, installed } = getStacksWallets(); |
| 305 | +``` |
| 306 | + |
| 307 | +## Supported Wallets |
| 308 | + |
| 309 | +All 6 wallets work with both headless (`connect('xverse')`) and modal (`connect()`) modes. |
| 310 | + |
| 311 | +| Wallet | ID | |
| 312 | +|---|---| |
| 313 | +| Xverse | `xverse` | |
| 314 | +| Leather | `leather` | |
| 315 | +| Asigna | `asigna` | |
| 316 | +| Fordefi | `fordefi` | |
| 317 | +| WalletConnect | `wallet-connect` | |
| 318 | +| OKX | `okx` | |
| 319 | + |
| 320 | +## Peer Dependencies |
| 321 | + |
| 322 | +- `react` ^18 or ^19 |
| 323 | +- `react-dom` ^18 or ^19 |
| 324 | +- `@stacks/transactions` >=7.0.0 |
10 | 325 |
|
11 | 326 | ## License |
12 | 327 |
|
|
0 commit comments