diff --git a/.changeset/wicked-bananas-check.md b/.changeset/wicked-bananas-check.md
new file mode 100644
index 00000000..60d37d28
--- /dev/null
+++ b/.changeset/wicked-bananas-check.md
@@ -0,0 +1,5 @@
+---
+'@wallet-standard/react-core': major
+---
+
+Replaced the feature context providers with two hooks: `useConnect()` and `useDisconnect()`
diff --git a/packages/react/core/README.md b/packages/react/core/README.md
index b447e8a0..7a04962c 100644
--- a/packages/react/core/README.md
+++ b/packages/react/core/README.md
@@ -7,3 +7,45 @@ This package provides React hooks for using Wallet Standard wallets, accounts, a
### `useWallets()`
Vends an array of `UiWallet` objects; one for every registered Wallet Standard `Wallet`.
+
+### `useConnect(uiWallet)`
+
+Given a `UiWallet`, this hook returns a function you can call to ask the wallet to authorize the current domain to use new accounts. Calling the returned method will return a promise for the array of account authorized by the wallet.
+
+```tsx
+function ConnectButton({ onAccountsConnected, wallet }) {
+ const [isConnecting, connect] = useConnect(wallet);
+ return (
+
+ );
+}
+```
+
+Be aware that calls to `connect` operate on a single promise that is global to the entire app. If you call `connect` multiple times or from multiple places in your app, the exact same promise will be vended to all callers. This reflects the fact that a wallet can only field one authorization request at a time.
+
+### `useDisconnect(uiWallet)`
+
+Given a `UiWallet`, this hook returns a function you can call to ask the wallet to deauthorize accounts authorized for the current domain, or at least to remove them from the wallet's `accounts` array for the time being, depending on the wallet's implementation of `standard:disconnect`. Call the returned method will return a promise that resolves once the disconnection is complete.
+
+```tsx
+function DisconnectButton({ onDisconnected, wallet }) {
+ const [isDisconnecting, disconnect] = useDisconnect(wallet);
+ return (
+
+ );
+}
+```
diff --git a/packages/react/core/src/WalletStandardProvider.tsx b/packages/react/core/src/WalletStandardProvider.tsx
index e0c5eaf3..22a0d5eb 100644
--- a/packages/react/core/src/WalletStandardProvider.tsx
+++ b/packages/react/core/src/WalletStandardProvider.tsx
@@ -1,12 +1,5 @@
import type { FC, ReactNode } from 'react';
import React from 'react';
-import {
- ConnectProvider,
- DisconnectProvider,
- SignAndSendTransactionProvider,
- SignMessageProvider,
- SignTransactionProvider,
-} from './features/index.js';
import { WalletAccountProvider } from './WalletAccountProvider.js';
import { WalletProvider } from './WalletProvider.js';
@@ -17,20 +10,10 @@ export interface WalletStandardProviderProps {
}
/** TODO: docs */
-export const WalletStandardProvider: FC = ({ children, onError }) => {
+export const WalletStandardProvider: FC = ({ children }) => {
return (
-
-
-
-
-
- {children}
-
-
-
-
-
+ {children}
);
};
diff --git a/packages/react/core/src/__tests__/useConnect-test.ts b/packages/react/core/src/__tests__/useConnect-test.ts
new file mode 100644
index 00000000..0b279e7e
--- /dev/null
+++ b/packages/react/core/src/__tests__/useConnect-test.ts
@@ -0,0 +1,130 @@
+import type { Wallet, WalletVersion } from '@wallet-standard/base';
+import { StandardConnect } from '@wallet-standard/features';
+import type { UiWallet } from '@wallet-standard/ui';
+import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
+
+import { useConnect } from '../features/useConnect.js';
+import { renderHook } from '../test-renderer.js';
+
+jest.mock('@wallet-standard/ui-registry');
+
+describe('useConnect', () => {
+ let mockConnect: jest.Mock;
+ let mockUiWallet: UiWallet;
+ beforeEach(() => {
+ mockConnect = jest.fn().mockResolvedValue({ accounts: [] });
+ const mockWallet: Wallet = {
+ accounts: [],
+ chains: [],
+ features: {
+ [StandardConnect]: {
+ connect: mockConnect,
+ },
+ },
+ icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
+ name: 'Mock Wallet',
+ version: '1.0.0' as WalletVersion,
+ };
+ jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue(mockWallet);
+ // Suppresses console output when an `ErrorBoundary` is hit.
+ // See https://stackoverflow.com/a/72632884/802047
+ jest.spyOn(console, 'error').mockImplementation();
+ jest.spyOn(console, 'warn').mockImplementation();
+ });
+ describe('the `isConnecting` property', () => {
+ it('is `false` before calling `connect()`', () => {
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [isConnecting, _] = result.current;
+ expect(isConnecting).toBe(false);
+ });
+ it('is `false` after the connection resolves', async () => {
+ expect.assertions(1);
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [_A, connect] = result.current;
+ await connect();
+ const [isConnecting, _B] = result.current;
+ expect(isConnecting).toBe(false);
+ });
+ it('is `true` after calling `connect()`', () => {
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [_, connect] = result.current;
+ connect();
+ const [isConnecting] = result.current;
+ expect(isConnecting).toBe(true);
+ });
+ it('is `true` on hooks that did not trigger the connect', () => {
+ const { result: resultA } = renderHook(() => useConnect(mockUiWallet));
+ const { result: resultB } = renderHook(() => useConnect(mockUiWallet));
+ if (resultA.__type === 'error' || !resultA.current) {
+ throw resultA.current;
+ }
+ if (resultB.__type === 'error' || !resultB.current) {
+ throw resultB.current;
+ }
+ const [_, connectA] = resultA.current;
+ connectA();
+ const [isConnectingB] = resultB.current;
+ expect(isConnectingB).toBe(true);
+ });
+ });
+ describe('the `connect` property', () => {
+ it("calls the wallet's connect implementation when called ", () => {
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, connect] = result.current;
+ connect();
+ expect(mockConnect).toHaveBeenCalled();
+ }
+ });
+ it("calls the wallet's connect implementation once despite multiple calls", () => {
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, connect] = result.current;
+ connect();
+ connect();
+ expect(mockConnect).toHaveBeenCalledTimes(1);
+ }
+ });
+ it("calls the wallet's connect implementation once despite calls from different hooks", () => {
+ const { result: resultA } = renderHook(() => useConnect(mockUiWallet));
+ const { result: resultB } = renderHook(() => useConnect(mockUiWallet));
+ if (resultA.__type === 'error' || !resultA.current) {
+ throw resultA.current;
+ } else if (resultB.__type === 'error' || !resultB.current) {
+ throw resultB.current;
+ } else {
+ const [_A, connectA] = resultA.current;
+ const [_B, connectB] = resultB.current;
+ connectA();
+ connectB();
+ expect(mockConnect).toHaveBeenCalledTimes(1);
+ }
+ });
+ it("calls the wallet's connect implementation anew after the first connection resolves", async () => {
+ expect.assertions(1);
+ const { result } = renderHook(() => useConnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, connect] = result.current;
+ connect();
+ await jest.runAllTimersAsync();
+ connect();
+ expect(mockConnect).toHaveBeenCalledTimes(2);
+ }
+ });
+ });
+});
diff --git a/packages/react/core/src/__tests__/useDisconnect-test.ts b/packages/react/core/src/__tests__/useDisconnect-test.ts
new file mode 100644
index 00000000..fddb072d
--- /dev/null
+++ b/packages/react/core/src/__tests__/useDisconnect-test.ts
@@ -0,0 +1,130 @@
+import type { Wallet, WalletVersion } from '@wallet-standard/base';
+import { StandardDisconnect } from '@wallet-standard/features';
+import type { UiWallet } from '@wallet-standard/ui';
+import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
+
+import { useDisconnect } from '../features/useDisconnect.js';
+import { renderHook } from '../test-renderer.js';
+
+jest.mock('@wallet-standard/ui-registry');
+
+describe('useDisconnect', () => {
+ let mockDisconnect: jest.Mock;
+ let mockUiWallet: UiWallet;
+ beforeEach(() => {
+ mockDisconnect = jest.fn().mockResolvedValue({ accounts: [] });
+ const mockWallet: Wallet = {
+ accounts: [],
+ chains: [],
+ features: {
+ [StandardDisconnect]: {
+ disconnect: mockDisconnect,
+ },
+ },
+ icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=',
+ name: 'Mock Wallet',
+ version: '1.0.0' as WalletVersion,
+ };
+ jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue(mockWallet);
+ // Suppresses console output when an `ErrorBoundary` is hit.
+ // See https://stackoverflow.com/a/72632884/802047
+ jest.spyOn(console, 'error').mockImplementation();
+ jest.spyOn(console, 'warn').mockImplementation();
+ });
+ describe('the `isDisconnecting` property', () => {
+ it('is `false` before calling `disconnect()`', () => {
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [isDisconnecting, _] = result.current;
+ expect(isDisconnecting).toBe(false);
+ });
+ it('is `false` after the disconnection resolves', async () => {
+ expect.assertions(1);
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [_A, disconnectA] = result.current;
+ await disconnectA();
+ const [isDisconnecting, _B] = result.current;
+ expect(isDisconnecting).toBe(false);
+ });
+ it('is `true` after calling `disconnect()`', () => {
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ }
+ const [_, disconnect] = result.current;
+ disconnect();
+ const [isDisconnecting] = result.current;
+ expect(isDisconnecting).toBe(true);
+ });
+ it('is `true` on hooks that did not trigger the disconnect', () => {
+ const { result: resultA } = renderHook(() => useDisconnect(mockUiWallet));
+ const { result: resultB } = renderHook(() => useDisconnect(mockUiWallet));
+ if (resultA.__type === 'error' || !resultA.current) {
+ throw resultA.current;
+ }
+ if (resultB.__type === 'error' || !resultB.current) {
+ throw resultB.current;
+ }
+ const [_, disconnectA] = resultA.current;
+ disconnectA();
+ const [isDisconnectingB] = resultB.current;
+ expect(isDisconnectingB).toBe(true);
+ });
+ });
+ describe('the `disconnect` property', () => {
+ it("calls the wallet's disconnect implementation when called ", () => {
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, disconnect] = result.current;
+ disconnect();
+ expect(mockDisconnect).toHaveBeenCalled();
+ }
+ });
+ it("calls the wallet's disconnect implementation once despite multiple calls", () => {
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, disconnect] = result.current;
+ disconnect();
+ disconnect();
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
+ }
+ });
+ it("calls the wallet's disconnect implementation once despite calls from different hooks", () => {
+ const { result: resultA } = renderHook(() => useDisconnect(mockUiWallet));
+ const { result: resultB } = renderHook(() => useDisconnect(mockUiWallet));
+ if (resultA.__type === 'error' || !resultA.current) {
+ throw resultA.current;
+ } else if (resultB.__type === 'error' || !resultB.current) {
+ throw resultB.current;
+ } else {
+ const [_A, disconnectA] = resultA.current;
+ const [_B, disconnectB] = resultB.current;
+ disconnectA();
+ disconnectB();
+ expect(mockDisconnect).toHaveBeenCalledTimes(1);
+ }
+ });
+ it("calls the wallet's disconnect implementation anew after the first disconnection resolves", async () => {
+ expect.assertions(1);
+ const { result } = renderHook(() => useDisconnect(mockUiWallet));
+ if (result.__type === 'error' || !result.current) {
+ throw result.current;
+ } else {
+ const [_, disconnect] = result.current;
+ disconnect();
+ await jest.runAllTimersAsync();
+ disconnect();
+ expect(mockDisconnect).toHaveBeenCalledTimes(2);
+ }
+ });
+ });
+});
diff --git a/packages/react/core/src/__tests__/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT-test.ts b/packages/react/core/src/__tests__/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT-test.ts
index db27c4fb..84e8732e 100644
--- a/packages/react/core/src/__tests__/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT-test.ts
+++ b/packages/react/core/src/__tests__/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT-test.ts
@@ -21,8 +21,6 @@ describe('useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT', () => {
on: mockOn,
register: mockRegister,
});
- // Suppresses console output when an `ErrorBoundary` is hit.
- // See https://stackoverflow.com/a/72632884/802047
});
it('returns a list of registered wallets', () => {
const expectedWallets = [] as readonly Wallet[];
diff --git a/packages/react/core/src/features/connect/ConnectProvider.tsx b/packages/react/core/src/features/connect/ConnectProvider.tsx
deleted file mode 100644
index a11d9da3..00000000
--- a/packages/react/core/src/features/connect/ConnectProvider.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { Wallet } from '@wallet-standard/base';
-import type { ConnectFeature, ConnectMethod } from '@wallet-standard/features';
-import type { FC, ReactNode } from 'react';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { useWallet } from '../../useWallet.js';
-import { ConnectContext } from './useConnect.js';
-
-/** TODO: docs */
-export interface ConnectProviderProps {
- children: NonNullable;
- onError?: (error: Error) => void;
-}
-
-/** TODO: docs */
-export function hasConnectFeature(features: Wallet['features']): features is ConnectFeature {
- return 'standard:connect' in features;
-}
-
-/** TODO: docs */
-export const ConnectProvider: FC = ({ children, onError }) => {
- const { features } = useWallet();
-
- // Handle errors, logging them by default.
- const handleError = useCallback(
- (error: Error) => {
- (onError || console.error)(error);
- return error;
- },
- [onError]
- );
-
- // Connect to the wallet.
- const [waiting, setWaiting] = useState(false);
- const promise = useRef>();
- const connect = useMemo(
- () =>
- hasConnectFeature(features)
- ? async (input) => {
- // FIXME: if called first with silent=true, promise.current will be set but waiting will be false
-
- // If already waiting, wait for that promise to resolve.
- if (promise.current) {
- try {
- await promise.current;
- } catch (error: any) {
- // Error will already have been handled below.
- }
- }
-
- const { silent } = input || {};
- if (!silent) {
- setWaiting(true);
- }
- try {
- promise.current = features['standard:connect'].connect(input);
- return await promise.current;
- } catch (error: any) {
- throw handleError(error);
- } finally {
- if (!silent) {
- setWaiting(false);
- }
- promise.current = undefined;
- }
- }
- : undefined,
- [features, promise, handleError]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/react/core/src/features/connect/index.ts b/packages/react/core/src/features/connect/index.ts
deleted file mode 100644
index 8148b972..00000000
--- a/packages/react/core/src/features/connect/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './ConnectProvider.js';
-export * from './useConnect.js';
diff --git a/packages/react/core/src/features/connect/useConnect.ts b/packages/react/core/src/features/connect/useConnect.ts
deleted file mode 100644
index 0809ceb5..00000000
--- a/packages/react/core/src/features/connect/useConnect.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { ConnectMethod } from '@wallet-standard/features';
-import { createContext, useContext } from 'react';
-import { createDefaultContext } from '../../context.js';
-
-/** TODO: docs */
-export interface ConnectContextState {
- waiting: boolean;
- connect: ConnectMethod | undefined;
-}
-
-const DEFAULT_CONNECT_STATE: Readonly = {
- waiting: false,
- connect: undefined,
-} as const;
-
-const DEFAULT_CONNECT_CONTEXT = createDefaultContext('Connect', DEFAULT_CONNECT_STATE);
-
-/** TODO: docs */
-export const ConnectContext = createContext(DEFAULT_CONNECT_CONTEXT);
-
-/** TODO: docs */
-export function useConnect(): ConnectContextState {
- return useContext(ConnectContext);
-}
diff --git a/packages/react/core/src/features/disconnect/DisconnectProvider.tsx b/packages/react/core/src/features/disconnect/DisconnectProvider.tsx
deleted file mode 100644
index d5ea9c0c..00000000
--- a/packages/react/core/src/features/disconnect/DisconnectProvider.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { Wallet } from '@wallet-standard/base';
-import type { DisconnectFeature, DisconnectMethod } from '@wallet-standard/features';
-import type { FC, ReactNode } from 'react';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { useWallet } from '../../useWallet.js';
-import { DisconnectContext } from './useDisconnect.js';
-
-/** TODO: docs */
-export interface DisconnectProviderProps {
- children: NonNullable;
- onError?: (error: Error) => void;
-}
-
-/** TODO: docs */
-export function hasDisconnectFeature(features: Wallet['features']): features is DisconnectFeature {
- return 'standard:disconnect' in features;
-}
-
-/** TODO: docs */
-export const DisconnectProvider: FC = ({ children, onError }) => {
- const { features } = useWallet();
-
- // Handle errors, logging them by default.
- const handleError = useCallback(
- (error: Error) => {
- (onError || console.error)(error);
- return error;
- },
- [onError]
- );
-
- // Disconnect from the wallet.
- const [waiting, setWaiting] = useState(false);
- const promise = useRef>();
- const disconnect = useMemo(
- () =>
- hasDisconnectFeature(features)
- ? async () => {
- // If already waiting, wait for that promise to resolve.
- if (promise.current) {
- try {
- await promise.current;
- } catch (error: any) {
- // Error will already have been handled below.
- }
- }
-
- setWaiting(true);
- try {
- promise.current = features['standard:disconnect'].disconnect();
- return await promise.current;
- } catch (error: any) {
- throw handleError(error);
- } finally {
- setWaiting(false);
- promise.current = undefined;
- }
- }
- : undefined,
- [features, promise, handleError]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/react/core/src/features/disconnect/index.ts b/packages/react/core/src/features/disconnect/index.ts
deleted file mode 100644
index 3b52d541..00000000
--- a/packages/react/core/src/features/disconnect/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './DisconnectProvider.js';
-export * from './useDisconnect.js';
diff --git a/packages/react/core/src/features/disconnect/useDisconnect.ts b/packages/react/core/src/features/disconnect/useDisconnect.ts
deleted file mode 100644
index e5f9bec5..00000000
--- a/packages/react/core/src/features/disconnect/useDisconnect.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { DisconnectMethod } from '@wallet-standard/features';
-import { createContext, useContext } from 'react';
-import { createDefaultContext } from '../../context.js';
-
-/** TODO: docs */
-export interface DisconnectContextState {
- waiting: boolean;
- disconnect: DisconnectMethod | undefined;
-}
-
-const DEFAULT_DISCONNECT_STATE: Readonly = {
- waiting: false,
- disconnect: undefined,
-} as const;
-
-const DEFAULT_DISCONNECT_CONTEXT = createDefaultContext('Disconnect', DEFAULT_DISCONNECT_STATE);
-
-/** TODO: docs */
-export const DisconnectContext = createContext(DEFAULT_DISCONNECT_CONTEXT);
-
-/** TODO: docs */
-export function useDisconnect(): DisconnectContextState {
- return useContext(DisconnectContext);
-}
diff --git a/packages/react/core/src/features/events.ts b/packages/react/core/src/features/events.ts
new file mode 100644
index 00000000..4b33dbec
--- /dev/null
+++ b/packages/react/core/src/features/events.ts
@@ -0,0 +1,7 @@
+import type { Wallet, WalletWithFeatures } from '@wallet-standard/base';
+import type { StandardEventsFeature } from '@wallet-standard/features';
+import { StandardEvents } from '@wallet-standard/features';
+
+export function walletHasStandardEventsFeature(wallet: Wallet): wallet is WalletWithFeatures {
+ return StandardEvents in wallet.features;
+}
diff --git a/packages/react/core/src/features/index.ts b/packages/react/core/src/features/index.ts
index 42c05024..69b7b52b 100644
--- a/packages/react/core/src/features/index.ts
+++ b/packages/react/core/src/features/index.ts
@@ -1,5 +1,2 @@
-export * from './connect/index.js';
-export * from './disconnect/index.js';
-export * from './signAndSendTransaction/index.js';
-export * from './signMessage/index.js';
-export * from './signTransaction/index.js';
+export * from './useConnect.js';
+export * from './useDisconnect.js';
diff --git a/packages/react/core/src/features/signAndSendTransaction/SignAndSendTransactionProvider.tsx b/packages/react/core/src/features/signAndSendTransaction/SignAndSendTransactionProvider.tsx
deleted file mode 100644
index 5db0fc47..00000000
--- a/packages/react/core/src/features/signAndSendTransaction/SignAndSendTransactionProvider.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-import type { Wallet } from '@wallet-standard/base';
-import type {
- SignAndSendTransactionFeature,
- SignAndSendTransactionMethod,
-} from '@wallet-standard/experimental-features';
-import type { FC, ReactNode } from 'react';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { useWallet } from '../../useWallet.js';
-import { SignAndSendTransactionContext } from './useSignAndSendTransaction.js';
-
-/** TODO: docs */
-export interface SignAndSendTransactionProviderProps {
- children: NonNullable;
- onError?: (error: Error) => void;
-}
-
-/** TODO: docs */
-export function hasSignAndSendTransactionFeature(
- features: Wallet['features']
-): features is SignAndSendTransactionFeature {
- return 'experimental:signAndSendTransaction' in features;
-}
-
-/** TODO: docs */
-export const SignAndSendTransactionProvider: FC = ({ children, onError }) => {
- const { features } = useWallet();
-
- // Handle errors, logging them by default.
- const handleError = useCallback(
- (error: Error) => {
- (onError || console.error)(error);
- return error;
- },
- [onError]
- );
-
- // Sign transactions with the wallet.
- const [waiting, setWaiting] = useState(false);
- const promise = useRef>();
- const signAndSendTransaction = useMemo(
- () =>
- hasSignAndSendTransactionFeature(features)
- ? async (...inputs) => {
- // If already waiting, wait for that promise to resolve.
- if (promise.current) {
- try {
- await promise.current;
- } catch (error: any) {
- // Error will already have been handled below.
- }
- }
-
- setWaiting(true);
- try {
- promise.current = features['experimental:signAndSendTransaction'].signAndSendTransaction(
- ...inputs
- );
- return await promise.current;
- } catch (error: any) {
- throw handleError(error);
- } finally {
- setWaiting(false);
- promise.current = undefined;
- }
- }
- : undefined,
- [features, promise, handleError]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/react/core/src/features/signAndSendTransaction/index.ts b/packages/react/core/src/features/signAndSendTransaction/index.ts
deleted file mode 100644
index f1ad145a..00000000
--- a/packages/react/core/src/features/signAndSendTransaction/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './SignAndSendTransactionProvider.js';
-export * from './useSignAndSendTransaction.js';
diff --git a/packages/react/core/src/features/signAndSendTransaction/useSignAndSendTransaction.ts b/packages/react/core/src/features/signAndSendTransaction/useSignAndSendTransaction.ts
deleted file mode 100644
index 26510940..00000000
--- a/packages/react/core/src/features/signAndSendTransaction/useSignAndSendTransaction.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { SignAndSendTransactionMethod } from '@wallet-standard/experimental-features';
-import { createContext, useContext } from 'react';
-import { createDefaultContext } from '../../context.js';
-
-/** TODO: docs */
-export interface SignAndSendTransactionContextState {
- waiting: boolean;
- signAndSendTransaction: SignAndSendTransactionMethod | undefined;
-}
-
-const DEFAULT_SIGN_AND_SEND_TRANSACTION_STATE: Readonly = {
- waiting: false,
- signAndSendTransaction: undefined,
-} as const;
-
-const DEFAULT_SIGN_AND_SEND_TRANSACTION_CONTEXT = createDefaultContext(
- 'SignAndSendTransaction',
- DEFAULT_SIGN_AND_SEND_TRANSACTION_STATE
-);
-
-/** TODO: docs */
-export const SignAndSendTransactionContext = createContext(DEFAULT_SIGN_AND_SEND_TRANSACTION_CONTEXT);
-
-/** TODO: docs */
-export function useSignAndSendTransaction(): SignAndSendTransactionContextState {
- return useContext(SignAndSendTransactionContext);
-}
diff --git a/packages/react/core/src/features/signMessage/SignMessageProvider.tsx b/packages/react/core/src/features/signMessage/SignMessageProvider.tsx
deleted file mode 100644
index acae7b53..00000000
--- a/packages/react/core/src/features/signMessage/SignMessageProvider.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { Wallet } from '@wallet-standard/base';
-import type { SignMessageFeature, SignMessageMethod } from '@wallet-standard/experimental-features';
-import type { FC, ReactNode } from 'react';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { useWallet } from '../../useWallet.js';
-import { SignMessageContext } from './useSignMessage.js';
-
-/** TODO: docs */
-export interface SignMessageProviderProps {
- children: NonNullable;
- onError?: (error: Error) => void;
-}
-
-/** TODO: docs */
-export function hasSignMessageFeature(features: Wallet['features']): features is SignMessageFeature {
- return 'experimental:signMessage' in features;
-}
-
-/** TODO: docs */
-export const SignMessageProvider: FC = ({ children, onError }) => {
- const { features } = useWallet();
-
- // Handle errors, logging them by default.
- const handleError = useCallback(
- (error: Error) => {
- (onError || console.error)(error);
- return error;
- },
- [onError]
- );
-
- // Sign messages with the wallet.
- const [waiting, setWaiting] = useState(false);
- const promise = useRef>();
- const signMessage = useMemo(
- () =>
- hasSignMessageFeature(features)
- ? async (...inputs) => {
- // If already waiting, wait for that promise to resolve.
- if (promise.current) {
- try {
- await promise.current;
- } catch (error: any) {
- // Error will already have been handled below.
- }
- }
-
- setWaiting(true);
- try {
- promise.current = features['experimental:signMessage'].signMessage(...inputs);
- return await promise.current;
- } catch (error: any) {
- throw handleError(error);
- } finally {
- setWaiting(false);
- promise.current = undefined;
- }
- }
- : undefined,
- [features, promise, handleError]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/react/core/src/features/signMessage/index.ts b/packages/react/core/src/features/signMessage/index.ts
deleted file mode 100644
index 7996c450..00000000
--- a/packages/react/core/src/features/signMessage/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './SignMessageProvider.js';
-export * from './useSignMessage.js';
diff --git a/packages/react/core/src/features/signMessage/useSignMessage.ts b/packages/react/core/src/features/signMessage/useSignMessage.ts
deleted file mode 100644
index 9a900781..00000000
--- a/packages/react/core/src/features/signMessage/useSignMessage.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { SignMessageMethod } from '@wallet-standard/experimental-features';
-import { createContext, useContext } from 'react';
-import { createDefaultContext } from '../../context.js';
-
-/** TODO: docs */
-export interface SignMessageContextState {
- waiting: boolean;
- signMessage: SignMessageMethod | undefined;
-}
-
-const DEFAULT_SIGN_MESSAGE_STATE: Readonly = {
- waiting: false,
- signMessage: undefined,
-} as const;
-
-const DEFAULT_SIGN_MESSAGE_CONTEXT = createDefaultContext('SignMessage', DEFAULT_SIGN_MESSAGE_STATE);
-
-/** TODO: docs */
-export const SignMessageContext = createContext(DEFAULT_SIGN_MESSAGE_CONTEXT);
-
-/** TODO: docs */
-export function useSignMessage(): SignMessageContextState {
- return useContext(SignMessageContext);
-}
diff --git a/packages/react/core/src/features/signTransaction/SignTransactionProvider.tsx b/packages/react/core/src/features/signTransaction/SignTransactionProvider.tsx
deleted file mode 100644
index 1d115119..00000000
--- a/packages/react/core/src/features/signTransaction/SignTransactionProvider.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { Wallet } from '@wallet-standard/base';
-import type { SignTransactionFeature, SignTransactionMethod } from '@wallet-standard/experimental-features';
-import type { FC, ReactNode } from 'react';
-import React, { useCallback, useMemo, useRef, useState } from 'react';
-import { useWallet } from '../../useWallet.js';
-import { SignTransactionContext } from './useSignTransaction.js';
-
-/** TODO: docs */
-export interface SignTransactionProviderProps {
- children: NonNullable;
- onError?: (error: Error) => void;
-}
-
-/** TODO: docs */
-export function hasSignTransactionFeature(features: Wallet['features']): features is SignTransactionFeature {
- return 'experimental:signTransaction' in features;
-}
-
-/** TODO: docs */
-export const SignTransactionProvider: FC = ({ children, onError }) => {
- const { features } = useWallet();
-
- // Handle errors, logging them by default.
- const handleError = useCallback(
- (error: Error) => {
- (onError || console.error)(error);
- return error;
- },
- [onError]
- );
-
- // Sign transactions with the wallet.
- const [waiting, setWaiting] = useState(false);
- const promise = useRef>();
- const signTransaction = useMemo(
- () =>
- hasSignTransactionFeature(features)
- ? async (...inputs) => {
- // If already waiting, wait for that promise to resolve.
- if (promise.current) {
- try {
- await promise.current;
- } catch (error: any) {
- // Error will already have been handled below.
- }
- }
-
- setWaiting(true);
- try {
- promise.current = features['experimental:signTransaction'].signTransaction(...inputs);
- return await promise.current;
- } catch (error: any) {
- throw handleError(error);
- } finally {
- setWaiting(false);
- promise.current = undefined;
- }
- }
- : undefined,
- [features, promise, handleError]
- );
-
- return (
-
- {children}
-
- );
-};
diff --git a/packages/react/core/src/features/signTransaction/index.ts b/packages/react/core/src/features/signTransaction/index.ts
deleted file mode 100644
index afab8faa..00000000
--- a/packages/react/core/src/features/signTransaction/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './SignTransactionProvider.js';
-export * from './useSignTransaction.js';
diff --git a/packages/react/core/src/features/signTransaction/useSignTransaction.ts b/packages/react/core/src/features/signTransaction/useSignTransaction.ts
deleted file mode 100644
index cb5d49c9..00000000
--- a/packages/react/core/src/features/signTransaction/useSignTransaction.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { SignTransactionMethod } from '@wallet-standard/experimental-features';
-import { createContext, useContext } from 'react';
-import { createDefaultContext } from '../../context.js';
-
-/** TODO: docs */
-export interface SignTransactionContextState {
- waiting: boolean;
- signTransaction: SignTransactionMethod | undefined;
-}
-
-const DEFAULT_SIGN_TRANSACTION_STATE: Readonly = {
- waiting: false,
- signTransaction: undefined,
-} as const;
-
-const DEFAULT_SIGN_TRANSACTION_CONTEXT = createDefaultContext('SignTransaction', DEFAULT_SIGN_TRANSACTION_STATE);
-
-/** TODO: docs */
-export const SignTransactionContext = createContext(DEFAULT_SIGN_TRANSACTION_CONTEXT);
-
-/** TODO: docs */
-export function useSignTransaction(): SignTransactionContextState {
- return useContext(SignTransactionContext);
-}
diff --git a/packages/react/core/src/features/useConnect.ts b/packages/react/core/src/features/useConnect.ts
new file mode 100644
index 00000000..19add354
--- /dev/null
+++ b/packages/react/core/src/features/useConnect.ts
@@ -0,0 +1,66 @@
+import type { StandardConnectFeature, StandardConnectInput, StandardConnectMethod } from '@wallet-standard/features';
+import { StandardConnect } from '@wallet-standard/features';
+import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui';
+import { getWalletFeature } from '@wallet-standard/ui';
+import {
+ getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
+ getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
+} from '@wallet-standard/ui-registry';
+import { useCallback } from 'react';
+
+import { useWeakRef } from '../useWeakRef.js';
+
+type TupleSplit = N extends T['length']
+ ? [T, []]
+ : [T extends [...infer U, ...infer R] ? [...U, ...TupleSplit[0]] : never, TupleSplit[1]];
+
+type SkipFirst = T extends [] ? [] : TupleSplit[1];
+
+/**
+ * Returns a function you can call to ask the wallet to authorize the current domain to use new
+ * accounts.
+ */
+export function useConnect(
+ uiWallet: TWallet
+): [
+ isConnecting: boolean,
+ connect: (
+ input?: Omit[0]>, 'silent'>,
+ ...rest: SkipFirst, 1>
+ ) => Promise,
+] {
+ const connectFeature = getWalletFeature(
+ uiWallet,
+ StandardConnect
+ ) as StandardConnectFeature[typeof StandardConnect];
+ const wallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWallet);
+ const connectionPromise = useWeakRef>(wallet);
+ const connect = useCallback(
+ async function connect(
+ input?: Omit[0]>, 'silent'>,
+ ...rest: SkipFirst, 1>
+ ) {
+ if (connectionPromise.current) {
+ return connectionPromise.current;
+ }
+ const newConnectionPromise = connectFeature
+ .connect(input, ...rest)
+ .then(({ accounts }) => {
+ return accounts.map(
+ getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.bind(
+ null,
+ wallet
+ )
+ ) as readonly UiWalletAccount[];
+ })
+ .finally(() => {
+ connectionPromise.current = undefined;
+ });
+ connectionPromise.current = newConnectionPromise;
+ return await newConnectionPromise;
+ },
+ [connectFeature, connectionPromise, wallet]
+ );
+ const isConnecting = !!connectionPromise.current;
+ return [isConnecting, connect];
+}
diff --git a/packages/react/core/src/features/useDisconnect.ts b/packages/react/core/src/features/useDisconnect.ts
new file mode 100644
index 00000000..7e1e736f
--- /dev/null
+++ b/packages/react/core/src/features/useDisconnect.ts
@@ -0,0 +1,39 @@
+import type { StandardDisconnectFeature, StandardDisconnectMethod } from '@wallet-standard/features';
+import { StandardDisconnect } from '@wallet-standard/features';
+import type { UiWallet } from '@wallet-standard/ui';
+import { getWalletFeature } from '@wallet-standard/ui';
+import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
+import { useCallback } from 'react';
+
+import { useWeakRef } from '../useWeakRef.js';
+
+/**
+ * Returns a function you can call to ask the wallet to deauthorize accounts authorized for the
+ * current domain, or at least to remove them from the wallet's `accounts` array for the time being,
+ * depending on the wallet's implementation of `standard:disconnect`.
+ */
+export function useDisconnect(
+ uiWallet: TWallet
+): [isDisconnecting: boolean, disconnect: (...inputs: Parameters) => Promise] {
+ const disconnectFeature = getWalletFeature(
+ uiWallet,
+ StandardDisconnect
+ ) as StandardDisconnectFeature[typeof StandardDisconnect];
+ const wallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWallet);
+ const disconnectionPromise = useWeakRef | undefined>(wallet);
+ const disconnect = useCallback(
+ async function disconnect(...inputs: Parameters) {
+ if (disconnectionPromise.current) {
+ return disconnectionPromise.current;
+ }
+ const newDisconnectionPromise = disconnectFeature.disconnect(...inputs).finally(() => {
+ disconnectionPromise.current = undefined;
+ });
+ disconnectionPromise.current = newDisconnectionPromise;
+ return await newDisconnectionPromise;
+ },
+ [disconnectFeature, disconnectionPromise]
+ );
+ const isDisconnecting = !!disconnectionPromise.current;
+ return [isDisconnecting, disconnect];
+}
diff --git a/packages/react/core/src/features/version.ts b/packages/react/core/src/features/version.ts
new file mode 100644
index 00000000..42aaf466
--- /dev/null
+++ b/packages/react/core/src/features/version.ts
@@ -0,0 +1,3 @@
+import type { WalletVersion } from '@wallet-standard/base';
+
+export const FEATURE_HOOKS_SUPPORTED_WALLET_VERSION = '1.0.0' as WalletVersion;
diff --git a/packages/react/core/src/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT.ts b/packages/react/core/src/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT.ts
index 3b7cfcd6..4b265cae 100644
--- a/packages/react/core/src/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT.ts
+++ b/packages/react/core/src/useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT.ts
@@ -1,9 +1,9 @@
import { getWallets } from '@wallet-standard/app';
-import type { Wallet, WalletWithFeatures } from '@wallet-standard/base';
-import { StandardEvents, type StandardEventsFeature } from '@wallet-standard/features';
+import type { Wallet } from '@wallet-standard/base';
+import { StandardEvents } from '@wallet-standard/features';
import { useCallback, useRef, useSyncExternalStore } from 'react';
-import { hasEventsFeature } from './WalletProvider.js';
+import { walletHasStandardEventsFeature } from './features/events.js';
import { useStable } from './useStable.js';
const NO_WALLETS: readonly Wallet[] = [];
@@ -12,10 +12,6 @@ function getServerSnapshot(): readonly Wallet[] {
return NO_WALLETS;
}
-function walletHasStandardEventsFeature(wallet: Wallet): wallet is WalletWithFeatures {
- return hasEventsFeature(wallet.features);
-}
-
/** TODO: docs */
export function useWallets_INTERNAL_ONLY_NOT_FOR_EXPORT(): readonly Wallet[] {
const { get, on } = useStable(getWallets);
diff --git a/packages/ui/features/src/__tests__/getWalletFeature-test.ts b/packages/ui/features/src/__tests__/getWalletFeature-test.ts
index 1393e1d3..21e6555b 100644
--- a/packages/ui/features/src/__tests__/getWalletFeature-test.ts
+++ b/packages/ui/features/src/__tests__/getWalletFeature-test.ts
@@ -1,5 +1,8 @@
import type { Wallet, WalletVersion } from '@wallet-standard/base';
-import { WalletStandardError } from '@wallet-standard/errors';
+import {
+ WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED,
+ WalletStandardError,
+} from '@wallet-standard/errors';
import type { UiWalletHandle } from '@wallet-standard/ui-core';
import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry';
@@ -36,7 +39,14 @@ describe('getWalletFeature', () => {
it('throws if the handle provided does not support the feature requested', () => {
expect(() => {
getWalletFeature(mockWalletHandle, 'feature:b');
- }).toThrow(WalletStandardError);
+ }).toThrow(
+ new WalletStandardError(WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED, {
+ featureName: 'feature:b',
+ supportedChains: ['solana:mainnet'],
+ supportedFeatures: ['feature:a'],
+ walletName: 'Mock Wallet',
+ })
+ );
});
it('returns the feature of the underlying wallet', () => {
jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue(mockWallet);