From 6d4bb5e36703b63dc694f9fef9925af191919f9f Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:56:29 +0000 Subject: [PATCH 01/13] feat: update @stacks/connect to 8.2.5 and leverage WalletConnect.initializeProvider Pin @stacks/connect to 8.2.5 (from ^8.2.2) picking up bundle size improvements, provider regression fixes, and the new initializeProvider API. Pre-initialize WalletConnect only when the user connects with it or when restoring a wallet-connect session from localStorage. Closes #1 Co-Authored-By: Claude Opus 4.6 --- packages/kit/package.json | 2 +- .../src/provider/stacks-wallet-provider.tsx | 39 ++++++++++++++----- pnpm-lock.yaml | 13 +++---- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/packages/kit/package.json b/packages/kit/package.json index 1fd9fba..aa7d19c 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -32,7 +32,7 @@ "clean": "rm -rf dist" }, "dependencies": { - "@stacks/connect": "^8.2.2", + "@stacks/connect": "8.2.5", "bns-v2-sdk": "^2.1.0" }, "peerDependencies": { diff --git a/packages/kit/src/provider/stacks-wallet-provider.tsx b/packages/kit/src/provider/stacks-wallet-provider.tsx index 74182fe..901df6a 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.tsx +++ b/packages/kit/src/provider/stacks-wallet-provider.tsx @@ -5,6 +5,7 @@ import { setSelectedProviderId, request, getSelectedProvider, + WalletConnect, } from '@stacks/connect'; import { createContext, @@ -68,6 +69,19 @@ export const StacksWalletProvider = ({ return; } + if ( + persisted.provider === 'wallet-connect' && + walletConnect?.projectId + ) { + await WalletConnect.initializeProvider( + buildWalletConnectConfig( + walletConnect.projectId, + walletConnect.metadata, + walletConnect.chains + ) + ); + } + setAddress(persisted.address); setProvider(persisted.provider); setSelectedProviderId( @@ -81,7 +95,7 @@ export const StacksWalletProvider = ({ }; void loadPersistedWallet(); - }, []); + }, [walletConnect]); const connect = useCallback( async (providerId: SupportedStacksWallet, options?: ConnectOptions) => { @@ -134,15 +148,22 @@ export const StacksWalletProvider = ({ STACKS_TO_STACKS_CONNECT_PROVIDERS[typedProvider] ); - const data = walletConnect + const wcConfig = + typedProvider === 'wallet-connect' && walletConnect + ? buildWalletConnectConfig( + walletConnect.projectId, + walletConnect.metadata, + walletConnect.chains + ) + : undefined; + + if (wcConfig) { + await WalletConnect.initializeProvider(wcConfig); + } + + const data = wcConfig ? await request( - { - walletConnect: buildWalletConnectConfig( - walletConnect.projectId, - walletConnect.metadata, - walletConnect.chains - ), - }, + { walletConnect: wcConfig }, 'getAddresses', {} ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 107b58c..642be5a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: packages/kit: dependencies: '@stacks/connect': - specifier: ^8.2.2 + specifier: 8.2.5 version: 8.2.5(@types/react@19.2.14)(lit@3.3.0)(react@19.2.4)(typescript@5.9.3)(zod@3.22.4) bns-v2-sdk: specifier: ^2.1.0 @@ -3874,7 +3874,6 @@ snapshots: '@noble/curves@1.9.7': dependencies: '@noble/hashes': 1.8.0 - optional: true '@noble/hashes@1.1.5': {} @@ -4336,7 +4335,7 @@ snapshots: '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.9.1 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 @@ -6179,11 +6178,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.3)(zod@3.22.4) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 @@ -6194,11 +6193,11 @@ snapshots: dependencies: '@adraffy/ens-normalize': 1.11.1 '@noble/ciphers': 1.3.0 - '@noble/curves': 1.9.2 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.0.8(typescript@5.9.3)(zod@3.22.4) + abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: typescript: 5.9.3 From 5718978fb9c33e91064a47184d781f2931c215bd Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 08:56:46 +0000 Subject: [PATCH 02/13] chore: add changeset for @stacks/connect update Co-Authored-By: Claude Opus 4.6 --- .changeset/update-stacks-connect.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/update-stacks-connect.md diff --git a/.changeset/update-stacks-connect.md b/.changeset/update-stacks-connect.md new file mode 100644 index 0000000..8df4ccb --- /dev/null +++ b/.changeset/update-stacks-connect.md @@ -0,0 +1,5 @@ +--- +"@satoshai/kit": patch +--- + +Update @stacks/connect to 8.2.5 (pinned) and leverage WalletConnect.initializeProvider for faster wallet-connect connections and session restores From b7c3c821e5a48d51dbaad503f4447a0e8405ed65 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 09:02:36 +0000 Subject: [PATCH 03/13] fix: use stable primitive dep for session restore effect Depend on walletConnect?.projectId instead of the walletConnect object to prevent the effect from re-running on every parent render when the prop is passed inline. Co-Authored-By: Claude Opus 4.6 --- packages/kit/src/provider/stacks-wallet-provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/provider/stacks-wallet-provider.tsx b/packages/kit/src/provider/stacks-wallet-provider.tsx index 901df6a..d33c6f2 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.tsx +++ b/packages/kit/src/provider/stacks-wallet-provider.tsx @@ -95,7 +95,7 @@ export const StacksWalletProvider = ({ }; void loadPersistedWallet(); - }, [walletConnect]); + }, [walletConnect?.projectId]); const connect = useCallback( async (providerId: SupportedStacksWallet, options?: ConnectOptions) => { From c43036b96bf121002c471eb8898f7214ca6e1231 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:44:19 +0000 Subject: [PATCH 04/13] feat(example): disable buttons for unavailable wallets Disable wallet buttons when the extension is not installed, and disable wallet-connect when no WalletConnect config is provided. Co-Authored-By: Claude Opus 4.6 --- examples/vite-react/src/app.tsx | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/examples/vite-react/src/app.tsx b/examples/vite-react/src/app.tsx index 23472d1..a1c2f60 100644 --- a/examples/vite-react/src/app.tsx +++ b/examples/vite-react/src/app.tsx @@ -4,9 +4,18 @@ import { useConnect, useDisconnect, useBnsName, + getStacksWallets, SUPPORTED_STACKS_WALLETS, + type SupportedStacksWallet, } from '@satoshai/kit'; +const hasWalletConnectConfig = false; + +const isWalletAvailable = (wallet: SupportedStacksWallet) => { + if (wallet === 'wallet-connect') return hasWalletConnectConfig; + return getStacksWallets().installed.includes(wallet); +}; + export const App = () => { return ( @@ -48,15 +57,18 @@ const Wallet = () => {

Connect a Wallet

{isPending &&

Connecting...

}
- {SUPPORTED_STACKS_WALLETS.map((wallet) => ( - - ))} + {SUPPORTED_STACKS_WALLETS.map((wallet) => { + const available = isWalletAvailable(wallet); + return ( + + ); + })}
); From abe7ce5fd76bb82cb8925c955fe5485196fbec4e Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:46:32 +0000 Subject: [PATCH 05/13] feat: add useAvailableWallets hook Expose available wallets from the provider context, accounting for both extension installation and WalletConnect configuration. Consumers no longer need to manually check wallet availability. Co-Authored-By: Claude Opus 4.6 --- examples/vite-react/src/app.tsx | 13 +++---------- packages/kit/src/hooks/use-available-wallets.ts | 11 +++++++++++ packages/kit/src/index.ts | 1 + .../kit/src/provider/stacks-wallet-provider.tsx | 14 ++++++++++++-- .../src/provider/stacks-wallet-provider.types.ts | 1 + 5 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 packages/kit/src/hooks/use-available-wallets.ts diff --git a/examples/vite-react/src/app.tsx b/examples/vite-react/src/app.tsx index a1c2f60..ab4f34b 100644 --- a/examples/vite-react/src/app.tsx +++ b/examples/vite-react/src/app.tsx @@ -4,18 +4,10 @@ import { useConnect, useDisconnect, useBnsName, - getStacksWallets, + useAvailableWallets, SUPPORTED_STACKS_WALLETS, - type SupportedStacksWallet, } from '@satoshai/kit'; -const hasWalletConnectConfig = false; - -const isWalletAvailable = (wallet: SupportedStacksWallet) => { - if (wallet === 'wallet-connect') return hasWalletConnectConfig; - return getStacksWallets().installed.includes(wallet); -}; - export const App = () => { return ( @@ -32,6 +24,7 @@ const Wallet = () => { const { address, isConnected } = useAddress(); const { disconnect } = useDisconnect(); const { bnsName, isLoading: isBnsLoading } = useBnsName(address); + const { availableWallets } = useAvailableWallets(); if (isConnected) { return ( @@ -58,7 +51,7 @@ const Wallet = () => { {isPending &&

Connecting...

}
{SUPPORTED_STACKS_WALLETS.map((wallet) => { - const available = isWalletAvailable(wallet); + const available = availableWallets.includes(wallet); return ( +
+ )}
{SUPPORTED_STACKS_WALLETS.map((wallet) => { const available = availableWallets.includes(wallet); diff --git a/packages/kit/src/hooks/use-connect.ts b/packages/kit/src/hooks/use-connect.ts index b70ca5b..f4fa95a 100644 --- a/packages/kit/src/hooks/use-connect.ts +++ b/packages/kit/src/hooks/use-connect.ts @@ -6,15 +6,16 @@ import { SUPPORTED_STACKS_WALLETS } from "../constants/wallets"; import { useStacksWalletContext } from "../provider/stacks-wallet-provider"; export const useConnect = () => { - const { connect, status } = useStacksWalletContext(); + const { connect, reset, status } = useStacksWalletContext(); const value = useMemo( () => ({ connect, + reset, connectors: SUPPORTED_STACKS_WALLETS, isPending: status === "connecting", }), - [connect, status] + [connect, reset, status] ); return value; diff --git a/packages/kit/src/provider/stacks-wallet-provider.tsx b/packages/kit/src/provider/stacks-wallet-provider.tsx index 4bb7e18..41dc8a8 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.tsx +++ b/packages/kit/src/provider/stacks-wallet-provider.tsx @@ -199,6 +199,11 @@ export const StacksWalletProvider = ({ [walletConnect] ); + const reset = useCallback(() => { + setIsConnecting(false); + clearSelectedProviderId(); + }, []); + const disconnect = useCallback( (callback?: () => void) => { localStorage.removeItem(LOCAL_STORAGE_STACKS); @@ -262,9 +267,10 @@ export const StacksWalletProvider = ({ ...walletState, connect, disconnect, + reset, availableWallets, }; - }, [address, provider, isConnecting, connect, disconnect, availableWallets]); + }, [address, provider, isConnecting, connect, disconnect, reset, availableWallets]); return ( diff --git a/packages/kit/src/provider/stacks-wallet-provider.types.ts b/packages/kit/src/provider/stacks-wallet-provider.types.ts index 6c74dba..3014ead 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.types.ts +++ b/packages/kit/src/provider/stacks-wallet-provider.types.ts @@ -50,5 +50,6 @@ export type WalletContextValue = WalletState & { options?: ConnectOptions ) => Promise; disconnect: (callback?: () => void) => void; + reset: () => void; availableWallets: SupportedStacksWallet[]; }; From 01c9a3ce4b095d0f46aa2f075efc0b2a13456aa9 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:33:52 +0000 Subject: [PATCH 09/13] chore: update changeset to minor with full scope Co-Authored-By: Claude Opus 4.6 --- .changeset/update-stacks-connect.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.changeset/update-stacks-connect.md b/.changeset/update-stacks-connect.md index 8df4ccb..e9927f9 100644 --- a/.changeset/update-stacks-connect.md +++ b/.changeset/update-stacks-connect.md @@ -1,5 +1,11 @@ --- -"@satoshai/kit": patch +"@satoshai/kit": minor --- -Update @stacks/connect to 8.2.5 (pinned) and leverage WalletConnect.initializeProvider for faster wallet-connect connections and session restores +Update @stacks/connect to 8.2.5 (pinned) and leverage WalletConnect.initializeProvider for faster wallet-connect connections and session restores. + +New features: +- `useAvailableWallets` hook — returns wallets that are installed and configured +- `wallets` prop on `StacksWalletProvider` — configure which wallets to support +- `reset` on `useConnect` — clear stuck connecting state when wallet modals are dismissed +- Runtime guard: throws if `wallet-connect` is in `wallets` without a `walletConnect.projectId` From 5eb4d32031a543c09e63b082ff30b28e530f1a08 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:38:11 +0000 Subject: [PATCH 10/13] feat: replace useAvailableWallets with useWallets returning WalletInfo[] Return all configured wallets with an `available` flag instead of only available ones. Consumers can now show all wallets and disable unavailable ones. Also removes `connectors` from useConnect in favor of useWallets. Co-Authored-By: Claude Opus 4.6 --- .changeset/update-stacks-connect.md | 7 ++++- examples/vite-react/src/app.tsx | 26 ++++++++----------- .../kit/src/hooks/use-available-wallets.ts | 11 -------- packages/kit/src/hooks/use-connect.ts | 2 -- packages/kit/src/hooks/use-wallets.ts | 11 ++++++++ packages/kit/src/index.ts | 3 ++- .../src/provider/stacks-wallet-provider.tsx | 21 ++++++++------- .../provider/stacks-wallet-provider.types.ts | 7 ++++- 8 files changed, 48 insertions(+), 40 deletions(-) delete mode 100644 packages/kit/src/hooks/use-available-wallets.ts create mode 100644 packages/kit/src/hooks/use-wallets.ts diff --git a/.changeset/update-stacks-connect.md b/.changeset/update-stacks-connect.md index e9927f9..52aeef6 100644 --- a/.changeset/update-stacks-connect.md +++ b/.changeset/update-stacks-connect.md @@ -5,7 +5,12 @@ Update @stacks/connect to 8.2.5 (pinned) and leverage WalletConnect.initializeProvider for faster wallet-connect connections and session restores. New features: -- `useAvailableWallets` hook — returns wallets that are installed and configured +- `useWallets` hook — returns configured wallets with `available` flag (installed + configured check) - `wallets` prop on `StacksWalletProvider` — configure which wallets to support - `reset` on `useConnect` — clear stuck connecting state when wallet modals are dismissed +- `WalletInfo` type exported for consumers - Runtime guard: throws if `wallet-connect` is in `wallets` without a `walletConnect.projectId` + +Breaking: +- Removed `useAvailableWallets` (replaced by `useWallets`) +- Removed `connectors` from `useConnect` (replaced by `useWallets`) diff --git a/examples/vite-react/src/app.tsx b/examples/vite-react/src/app.tsx index dac74de..8d980ba 100644 --- a/examples/vite-react/src/app.tsx +++ b/examples/vite-react/src/app.tsx @@ -4,8 +4,7 @@ import { useConnect, useDisconnect, useBnsName, - useAvailableWallets, - SUPPORTED_STACKS_WALLETS, + useWallets, } from '@satoshai/kit'; const wcProjectId = import.meta.env.VITE_WALLETCONNECT_PROJECT_ID as string | undefined; @@ -28,7 +27,7 @@ const Wallet = () => { const { address, isConnected } = useAddress(); const { disconnect } = useDisconnect(); const { bnsName, isLoading: isBnsLoading } = useBnsName(address); - const { availableWallets } = useAvailableWallets(); + const { wallets } = useWallets(); if (isConnected) { return ( @@ -59,18 +58,15 @@ const Wallet = () => {
)}
- {SUPPORTED_STACKS_WALLETS.map((wallet) => { - const available = availableWallets.includes(wallet); - return ( - - ); - })} + {wallets.map(({ id, available }) => ( + + ))}
); diff --git a/packages/kit/src/hooks/use-available-wallets.ts b/packages/kit/src/hooks/use-available-wallets.ts deleted file mode 100644 index bdc643a..0000000 --- a/packages/kit/src/hooks/use-available-wallets.ts +++ /dev/null @@ -1,11 +0,0 @@ -'use client'; - -import { useMemo } from 'react'; - -import { useStacksWalletContext } from '../provider/stacks-wallet-provider'; - -export const useAvailableWallets = () => { - const { availableWallets } = useStacksWalletContext(); - - return useMemo(() => ({ availableWallets }), [availableWallets]); -}; diff --git a/packages/kit/src/hooks/use-connect.ts b/packages/kit/src/hooks/use-connect.ts index f4fa95a..19b34c4 100644 --- a/packages/kit/src/hooks/use-connect.ts +++ b/packages/kit/src/hooks/use-connect.ts @@ -2,7 +2,6 @@ import { useMemo } from "react"; -import { SUPPORTED_STACKS_WALLETS } from "../constants/wallets"; import { useStacksWalletContext } from "../provider/stacks-wallet-provider"; export const useConnect = () => { @@ -12,7 +11,6 @@ export const useConnect = () => { () => ({ connect, reset, - connectors: SUPPORTED_STACKS_WALLETS, isPending: status === "connecting", }), [connect, reset, status] diff --git a/packages/kit/src/hooks/use-wallets.ts b/packages/kit/src/hooks/use-wallets.ts new file mode 100644 index 0000000..87e5d2f --- /dev/null +++ b/packages/kit/src/hooks/use-wallets.ts @@ -0,0 +1,11 @@ +'use client'; + +import { useMemo } from 'react'; + +import { useStacksWalletContext } from '../provider/stacks-wallet-provider'; + +export const useWallets = () => { + const { wallets } = useStacksWalletContext(); + + return useMemo(() => ({ wallets }), [wallets]); +}; diff --git a/packages/kit/src/index.ts b/packages/kit/src/index.ts index 591ee78..0483d14 100644 --- a/packages/kit/src/index.ts +++ b/packages/kit/src/index.ts @@ -18,13 +18,14 @@ export { type PostConditionConfig, } from './hooks/use-write-contract/use-write-contract'; export { useBnsName } from './hooks/use-bns-name'; -export { useAvailableWallets } from './hooks/use-available-wallets'; +export { useWallets } from './hooks/use-wallets'; // Types export type { WalletState, WalletContextValue, WalletConnectMetadata, + WalletInfo, StacksChain, ConnectOptions, } from './provider/stacks-wallet-provider.types'; diff --git a/packages/kit/src/provider/stacks-wallet-provider.tsx b/packages/kit/src/provider/stacks-wallet-provider.tsx index 41dc8a8..4226224 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.tsx +++ b/packages/kit/src/provider/stacks-wallet-provider.tsx @@ -242,14 +242,17 @@ export const StacksWalletProvider = ({ connect, }); - const availableWallets = useMemo(() => { + const walletInfos = useMemo(() => { const { installed } = getStacksWallets(); - const configured = wallets ?? SUPPORTED_STACKS_WALLETS; - - return [...configured].filter((w) => { - if (w === 'wallet-connect') return !!walletConnect?.projectId; - return installed.includes(w); - }); + const configured = wallets ?? [...SUPPORTED_STACKS_WALLETS]; + + return configured.map((w) => ({ + id: w, + available: + w === 'wallet-connect' + ? !!walletConnect?.projectId + : installed.includes(w), + })); }, [wallets, walletConnect?.projectId]); const value = useMemo((): WalletContextValue => { @@ -268,9 +271,9 @@ export const StacksWalletProvider = ({ connect, disconnect, reset, - availableWallets, + wallets: walletInfos, }; - }, [address, provider, isConnecting, connect, disconnect, reset, availableWallets]); + }, [address, provider, isConnecting, connect, disconnect, reset, walletInfos]); return ( diff --git a/packages/kit/src/provider/stacks-wallet-provider.types.ts b/packages/kit/src/provider/stacks-wallet-provider.types.ts index 3014ead..efb13ae 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.types.ts +++ b/packages/kit/src/provider/stacks-wallet-provider.types.ts @@ -44,6 +44,11 @@ export type WalletState = provider: SupportedStacksWallet; }; +export interface WalletInfo { + id: SupportedStacksWallet; + available: boolean; +} + export type WalletContextValue = WalletState & { connect: ( providerId: SupportedStacksWallet, @@ -51,5 +56,5 @@ export type WalletContextValue = WalletState & { ) => Promise; disconnect: (callback?: () => void) => void; reset: () => void; - availableWallets: SupportedStacksWallet[]; + wallets: WalletInfo[]; }; From ea545bad0e36c8d978269e0f691e51763b2eb850 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:31:34 +0000 Subject: [PATCH 11/13] docs: update READMEs with new wallet API Update Quick Start and API docs to reflect useWallets, wallets prop, reset, and removal of connectors from useConnect. Co-Authored-By: Claude Opus 4.6 --- README.md | 13 +++++++----- packages/kit/README.md | 47 ++++++++++++++++++++++++++++++++---------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7d6eb4d..2974214 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired - **`StacksWalletProvider`** — React context provider for wallet state - **`useConnect` / `useDisconnect`** — Connect and disconnect wallets +- **`useWallets`** — Configured wallets with availability status - **`useAddress`** — Access connected wallet address and status - **`useSignMessage`** — Sign arbitrary messages - **`useWriteContract`** — Call smart contracts with post-conditions @@ -25,7 +26,7 @@ pnpm add @satoshai/kit @stacks/transactions react react-dom ## Quick Start ```tsx -import { StacksWalletProvider, useConnect, useAddress, useDisconnect } from '@satoshai/kit'; +import { StacksWalletProvider, useConnect, useWallets, useAddress, useDisconnect } from '@satoshai/kit'; function App() { return ( @@ -36,7 +37,8 @@ function App() { } function Wallet() { - const { connect, connectors } = useConnect(); + const { connect, reset, isPending } = useConnect(); + const { wallets } = useWallets(); const { address, isConnected } = useAddress(); const { disconnect } = useDisconnect(); @@ -51,9 +53,10 @@ function Wallet() { return (
- {connectors.map((wallet) => ( - } + {wallets.map(({ id, available }) => ( + ))}
diff --git a/packages/kit/README.md b/packages/kit/README.md index 27a3337..00abb9e 100644 --- a/packages/kit/README.md +++ b/packages/kit/README.md @@ -6,6 +6,7 @@ Typesafe Stacks wallet & contract interaction library for React. Wagmi-inspired - **`StacksWalletProvider`** — React context provider for wallet state - **`useConnect` / `useDisconnect`** — Connect and disconnect wallets +- **`useWallets`** — Configured wallets with availability status - **`useAddress`** — Access connected wallet address and status - **`useSignMessage`** — Sign arbitrary messages - **`useWriteContract`** — Call smart contracts with post-conditions @@ -22,7 +23,7 @@ pnpm add @satoshai/kit @stacks/transactions react react-dom ## Quick Start ```tsx -import { StacksWalletProvider, useConnect, useAddress, useDisconnect } from '@satoshai/kit'; +import { StacksWalletProvider, useConnect, useWallets, useAddress, useDisconnect } from '@satoshai/kit'; function App() { return ( @@ -33,7 +34,8 @@ function App() { } function Wallet() { - const { connect, connectors } = useConnect(); + const { connect, reset, isPending } = useConnect(); + const { wallets } = useWallets(); const { address, isConnected } = useAddress(); const { disconnect } = useDisconnect(); @@ -48,9 +50,10 @@ function Wallet() { return (
- {connectors.map((wallet) => ( - } + {wallets.map(({ id, available }) => ( + ))}
@@ -66,28 +69,50 @@ Wrap your app to provide wallet context to all hooks. ```tsx {}} // optional - onAddressChange={(newAddress) => {}} // optional — Xverse account switching - onDisconnect={() => {}} // optional + wallets={['xverse', 'leather', 'wallet-connect']} // optional — defaults to all supported + walletConnect={{ projectId: '...' }} // optional — enables WalletConnect + onConnect={(provider, address) => {}} // optional + onAddressChange={(newAddress) => {}} // optional — Xverse account switching + onDisconnect={() => {}} // optional > {children} ``` +> If `wallets` includes `'wallet-connect'`, you must provide `walletConnect.projectId` or the provider will throw at mount. + ### `useConnect()` ```ts -const { connect, connectors, isPending } = useConnect(); +const { connect, reset, isPending } = useConnect(); -// connectors = ['xverse', 'leather', 'okx', 'asigna', 'fordefi', 'wallet-connect'] await connect('xverse'); await connect('leather', { onSuccess: (address, provider) => {}, onError: (error) => {}, }); + +// Reset stuck connecting state (e.g. when a wallet modal is dismissed) +reset(); +``` + +### `useWallets()` + +Returns all configured wallets with their availability status. + +```ts +const { wallets } = useWallets(); +// [{ id: 'xverse', available: true }, { id: 'leather', available: false }, ...] + +{wallets.map(({ id, available }) => ( + +))} ``` +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. + ### `useDisconnect()` ```ts From fd1bd14c1e8e307f61920ef49ae49f96da5da3a2 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:20:20 +0000 Subject: [PATCH 12/13] fix: stale connect after reset, WC double-init race, and strict mode guard - Move runtime guard from render body to useEffect (React Strict Mode safe) - Add generation counter ref so reset() invalidates in-flight connect() promises - Guard concurrent WalletConnect.initializeProvider calls with wcInitRef Co-Authored-By: Claude Opus 4.6 --- .changeset/update-stacks-connect.md | 5 ++ .../src/provider/stacks-wallet-provider.tsx | 48 +++++++++++++++---- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/.changeset/update-stacks-connect.md b/.changeset/update-stacks-connect.md index 52aeef6..732d68f 100644 --- a/.changeset/update-stacks-connect.md +++ b/.changeset/update-stacks-connect.md @@ -11,6 +11,11 @@ New features: - `WalletInfo` type exported for consumers - Runtime guard: throws if `wallet-connect` is in `wallets` without a `walletConnect.projectId` +Fixes: +- Moved runtime guard from render body to `useEffect` (React Strict Mode safe) +- `reset()` now invalidates in-flight `connect()` promises via generation counter (prevents stale state) +- Guard against concurrent `WalletConnect.initializeProvider` calls between session restore and connect + Breaking: - Removed `useAvailableWallets` (replaced by `useWallets`) - Removed `connectors` from `useConnect` (replaced by `useWallets`) diff --git a/packages/kit/src/provider/stacks-wallet-provider.tsx b/packages/kit/src/provider/stacks-wallet-provider.tsx index 4226224..be02870 100644 --- a/packages/kit/src/provider/stacks-wallet-provider.tsx +++ b/packages/kit/src/provider/stacks-wallet-provider.tsx @@ -12,6 +12,7 @@ import { useContext, useCallback, useEffect, + useRef, useState, useMemo, } from 'react'; @@ -51,18 +52,27 @@ export const StacksWalletProvider = ({ onAddressChange, onDisconnect, }: StacksWalletProviderProps) => { - if (wallets?.includes('wallet-connect') && !walletConnect?.projectId) { - throw new Error( - 'StacksWalletProvider: "wallet-connect" is listed in wallets but no walletConnect.projectId was provided.' - ); - } - const [address, setAddress] = useState(); const [provider, setProvider] = useState< SupportedStacksWallet | undefined >(); const [isConnecting, setIsConnecting] = useState(false); + // Generation counter — incremented by reset() to invalidate in-flight connect promises + const connectGenRef = useRef(0); + + // Guard against concurrent WalletConnect.initializeProvider calls + const wcInitRef = useRef | null>(null); + + // Fix #1: runtime guard in useEffect instead of render body + useEffect(() => { + if (wallets?.includes('wallet-connect') && !walletConnect?.projectId) { + throw new Error( + 'StacksWalletProvider: "wallet-connect" is listed in wallets but no walletConnect.projectId was provided.' + ); + } + }, [wallets, walletConnect?.projectId]); + useEffect(() => { const loadPersistedWallet = async () => { const persisted = getLocalStorageWallet(); @@ -83,13 +93,16 @@ export const StacksWalletProvider = ({ persisted.provider === 'wallet-connect' && walletConnect?.projectId ) { - await WalletConnect.initializeProvider( + const initPromise = WalletConnect.initializeProvider( buildWalletConnectConfig( walletConnect.projectId, walletConnect.metadata, walletConnect.chains ) ); + wcInitRef.current = initPromise; + await initPromise; + wcInitRef.current = null; } setAddress(persisted.address); @@ -143,11 +156,14 @@ export const StacksWalletProvider = ({ return; } + // Capture generation so we can detect if reset() was called during await + const gen = ++connectGenRef.current; setIsConnecting(true); try { if (typedProvider === 'okx') { const data = await getOKXStacksAddress(); + if (connectGenRef.current !== gen) return; setAddress(data.address); setProvider(data.provider); options?.onSuccess?.(data.address, data.provider); @@ -168,9 +184,17 @@ export const StacksWalletProvider = ({ : undefined; if (wcConfig) { - await WalletConnect.initializeProvider(wcConfig); + // Wait for any in-flight init, then start ours + if (wcInitRef.current) await wcInitRef.current; + const initPromise = + WalletConnect.initializeProvider(wcConfig); + wcInitRef.current = initPromise; + await initPromise; + wcInitRef.current = null; } + if (connectGenRef.current !== gen) return; + const data = wcConfig ? await request( { walletConnect: wcConfig }, @@ -179,6 +203,8 @@ export const StacksWalletProvider = ({ ) : await request('getAddresses'); + if (connectGenRef.current !== gen) return; + const extractedAddress = extractStacksAddress( typedProvider, data.addresses @@ -188,18 +214,22 @@ export const StacksWalletProvider = ({ setProvider(typedProvider); options?.onSuccess?.(extractedAddress, typedProvider); } catch (error) { + if (connectGenRef.current !== gen) return; console.error('Failed to connect wallet:', error); getSelectedProvider()?.disconnect?.(); clearSelectedProviderId(); options?.onError?.(error as Error); } finally { - setIsConnecting(false); + if (connectGenRef.current === gen) { + setIsConnecting(false); + } } }, [walletConnect] ); const reset = useCallback(() => { + connectGenRef.current++; setIsConnecting(false); clearSelectedProviderId(); }, []); From b0d818bdbef572fbc22408a81d2d0c127ff1ff86 Mon Sep 17 00:00:00 2001 From: satoshai-dev <262845409+satoshai-dev@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:24:18 +0000 Subject: [PATCH 13/13] docs: add note about OKX popup close not rejecting Co-Authored-By: Claude Opus 4.6 --- packages/kit/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/kit/README.md b/packages/kit/README.md index 00abb9e..77d7ba5 100644 --- a/packages/kit/README.md +++ b/packages/kit/README.md @@ -96,6 +96,8 @@ await connect('leather', { reset(); ``` +> **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. + ### `useWallets()` Returns all configured wallets with their availability status.