Skip to content

Commit 059b504

Browse files
satoshai-devclaude
andauthored
feat: add typed error classes for wallet hooks (#44) (#54)
Add wagmi-style typed errors: BaseError, WalletNotConnectedError, WalletNotFoundError, UnsupportedMethodError, WalletRequestError. All hooks now throw typed errors with shortMessage, details, walk(), and name-based discrimination. Developers can use instanceof or error.name checks for specific error handling. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e34212d commit 059b504

File tree

12 files changed

+206
-37
lines changed

12 files changed

+206
-37
lines changed

.changeset/typed-errors.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@satoshai/kit": minor
3+
---
4+
5+
Add typed error classes (UnsupportedMethodError, WalletNotConnectedError, WalletNotFoundError, WalletRequestError) following wagmi's error pattern. All hooks now throw typed errors that can be checked via `instanceof` or `error.name`.

packages/kit/src/errors.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
export type BaseErrorType = BaseError & { name: 'StacksKitError' }
2+
3+
export class BaseError extends Error {
4+
override name = 'StacksKitError'
5+
6+
shortMessage: string
7+
8+
constructor(shortMessage: string, options?: { cause?: Error; details?: string }) {
9+
const message = [
10+
shortMessage,
11+
options?.details && `Details: ${options.details}`,
12+
].filter(Boolean).join('\n\n')
13+
14+
super(message, options?.cause ? { cause: options.cause } : undefined)
15+
this.shortMessage = shortMessage
16+
}
17+
18+
walk(fn?: (err: unknown) => boolean): unknown {
19+
return walk(this, fn)
20+
}
21+
}
22+
23+
function walk(err: unknown, fn?: (err: unknown) => boolean): unknown {
24+
if (fn?.(err)) return err
25+
if (err && typeof err === 'object' && 'cause' in err) {
26+
return walk((err as { cause: unknown }).cause, fn)
27+
}
28+
return err
29+
}
30+
31+
export type WalletNotConnectedErrorType = WalletNotConnectedError & {
32+
name: 'WalletNotConnectedError'
33+
}
34+
35+
export class WalletNotConnectedError extends BaseError {
36+
override name = 'WalletNotConnectedError'
37+
38+
constructor() {
39+
super('Wallet is not connected')
40+
}
41+
}
42+
43+
export type WalletNotFoundErrorType = WalletNotFoundError & {
44+
name: 'WalletNotFoundError'
45+
}
46+
47+
export class WalletNotFoundError extends BaseError {
48+
override name = 'WalletNotFoundError'
49+
50+
wallet: string
51+
52+
constructor({ wallet }: { wallet: string }) {
53+
super(`${wallet} wallet not found`, {
54+
details: 'The wallet extension may not be installed.',
55+
})
56+
this.wallet = wallet
57+
}
58+
}
59+
60+
export type UnsupportedMethodErrorType = UnsupportedMethodError & {
61+
name: 'UnsupportedMethodError'
62+
}
63+
64+
export class UnsupportedMethodError extends BaseError {
65+
override name = 'UnsupportedMethodError'
66+
67+
method: string
68+
wallet: string
69+
70+
constructor({ method, wallet }: { method: string; wallet: string }) {
71+
super(`${method} is not supported by ${wallet} wallet`)
72+
this.method = method
73+
this.wallet = wallet
74+
}
75+
}
76+
77+
export type WalletRequestErrorType = WalletRequestError & {
78+
name: 'WalletRequestError'
79+
}
80+
81+
export class WalletRequestError extends BaseError {
82+
override name = 'WalletRequestError'
83+
84+
method: string
85+
wallet: string
86+
87+
constructor({ method, wallet, cause }: { method: string; wallet: string; cause: Error }) {
88+
super(`${wallet} wallet request failed`, {
89+
cause,
90+
details: cause.message,
91+
})
92+
this.method = method
93+
this.wallet = wallet
94+
}
95+
}

packages/kit/src/hooks/use-sign-message.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import { request } from '@stacks/connect';
44
import { useCallback, useMemo, useState } from 'react';
55

6+
import {
7+
BaseError,
8+
WalletNotConnectedError,
9+
WalletNotFoundError,
10+
WalletRequestError,
11+
} from '../errors';
612
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
713
import { useAddress } from './use-address';
814

@@ -28,13 +34,13 @@ export interface SignMessageOptions {
2834
export const useSignMessage = () => {
2935
const { isConnected, provider } = useAddress();
3036
const [data, setData] = useState<SignMessageData | undefined>(undefined);
31-
const [error, setError] = useState<Error | null>(null);
37+
const [error, setError] = useState<BaseError | null>(null);
3238
const [status, setStatus] = useState<MutationStatus>('idle');
3339

3440
const signMessageAsync = useCallback(
3541
async (variables: SignMessageVariables): Promise<SignMessageData> => {
3642
if (!isConnected) {
37-
throw new Error('Wallet is not connected');
43+
throw new WalletNotConnectedError();
3844
}
3945

4046
setStatus('pending');
@@ -46,7 +52,7 @@ export const useSignMessage = () => {
4652

4753
if (provider === 'okx') {
4854
if (!window.okxwallet) {
49-
throw new Error('OKX wallet not found');
55+
throw new WalletNotFoundError({ wallet: 'OKX' });
5056
}
5157

5258
result = await window.okxwallet.stacks.signMessage({
@@ -65,8 +71,13 @@ export const useSignMessage = () => {
6571
setStatus('success');
6672
return result;
6773
} catch (err) {
68-
const error =
69-
err instanceof Error ? err : new Error(String(err));
74+
const error = err instanceof BaseError
75+
? err
76+
: new WalletRequestError({
77+
method: 'stx_signMessage',
78+
wallet: provider ?? 'unknown',
79+
cause: err instanceof Error ? err : new Error(String(err)),
80+
});
7081
setError(error);
7182
setStatus('error');
7283
throw error;

packages/kit/src/hooks/use-sign-structured-message.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ import { request } from '@stacks/connect';
44
import type { ClarityValue, TupleCV } from '@stacks/transactions';
55
import { useCallback, useMemo, useState } from 'react';
66

7+
import {
8+
BaseError,
9+
WalletNotConnectedError,
10+
UnsupportedMethodError,
11+
WalletRequestError,
12+
} from '../errors';
713
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
814
import { useAddress } from './use-address';
915

@@ -31,21 +37,22 @@ export const useSignStructuredMessage = () => {
3137
const [data, setData] = useState<SignStructuredMessageData | undefined>(
3238
undefined
3339
);
34-
const [error, setError] = useState<Error | null>(null);
40+
const [error, setError] = useState<BaseError | null>(null);
3541
const [status, setStatus] = useState<MutationStatus>('idle');
3642

3743
const signStructuredMessageAsync = useCallback(
3844
async (
3945
variables: SignStructuredMessageVariables
4046
): Promise<SignStructuredMessageData> => {
4147
if (!isConnected) {
42-
throw new Error('Wallet is not connected');
48+
throw new WalletNotConnectedError();
4349
}
4450

4551
if (provider === 'okx') {
46-
throw new Error(
47-
'Structured message signing is not supported by OKX wallet'
48-
);
52+
throw new UnsupportedMethodError({
53+
method: 'stx_signStructuredMessage',
54+
wallet: 'OKX',
55+
});
4956
}
5057

5158
setStatus('pending');
@@ -62,8 +69,13 @@ export const useSignStructuredMessage = () => {
6269
setStatus('success');
6370
return result;
6471
} catch (err) {
65-
const error =
66-
err instanceof Error ? err : new Error(String(err));
72+
const error = err instanceof BaseError
73+
? err
74+
: new WalletRequestError({
75+
method: 'stx_signStructuredMessage',
76+
wallet: provider ?? 'unknown',
77+
cause: err instanceof Error ? err : new Error(String(err)),
78+
});
6779
setError(error);
6880
setStatus('error');
6981
throw error;

packages/kit/src/hooks/use-sign-transaction.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import { request } from '@stacks/connect';
44
import { useCallback, useMemo, useState } from 'react';
55

6+
import {
7+
BaseError,
8+
WalletNotConnectedError,
9+
UnsupportedMethodError,
10+
WalletRequestError,
11+
} from '../errors';
612
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
713
import { useAddress } from './use-address';
814

@@ -28,21 +34,22 @@ export interface SignTransactionOptions {
2834
export const useSignTransaction = () => {
2935
const { isConnected, provider } = useAddress();
3036
const [data, setData] = useState<SignTransactionData | undefined>(undefined);
31-
const [error, setError] = useState<Error | null>(null);
37+
const [error, setError] = useState<BaseError | null>(null);
3238
const [status, setStatus] = useState<MutationStatus>('idle');
3339

3440
const signTransactionAsync = useCallback(
3541
async (
3642
variables: SignTransactionVariables
3743
): Promise<SignTransactionData> => {
3844
if (!isConnected) {
39-
throw new Error('Wallet is not connected');
45+
throw new WalletNotConnectedError();
4046
}
4147

4248
if (provider === 'okx') {
43-
throw new Error(
44-
'Transaction signing is not supported by OKX wallet'
45-
);
49+
throw new UnsupportedMethodError({
50+
method: 'stx_signTransaction',
51+
wallet: 'OKX',
52+
});
4653
}
4754

4855
setStatus('pending');
@@ -61,8 +68,13 @@ export const useSignTransaction = () => {
6168
setStatus('success');
6269
return result;
6370
} catch (err) {
64-
const error =
65-
err instanceof Error ? err : new Error(String(err));
71+
const error = err instanceof BaseError
72+
? err
73+
: new WalletRequestError({
74+
method: 'stx_signTransaction',
75+
wallet: provider ?? 'unknown',
76+
cause: err instanceof Error ? err : new Error(String(err)),
77+
});
6678
setError(error);
6779
setStatus('error');
6880
throw error;

packages/kit/src/hooks/use-transfer-stx.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
import { request } from '@stacks/connect';
44
import { useCallback, useMemo, useState } from 'react';
55

6+
import {
7+
BaseError,
8+
WalletNotConnectedError,
9+
WalletNotFoundError,
10+
WalletRequestError,
11+
} from '../errors';
612
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
713
import { useAddress } from './use-address';
814
import { getNetworkFromAddress } from '../utils/get-network-from-address';
@@ -24,13 +30,13 @@ export interface TransferSTXOptions {
2430
export const useTransferSTX = () => {
2531
const { isConnected, address, provider } = useAddress();
2632
const [data, setData] = useState<string | undefined>(undefined);
27-
const [error, setError] = useState<Error | null>(null);
33+
const [error, setError] = useState<BaseError | null>(null);
2834
const [status, setStatus] = useState<MutationStatus>('idle');
2935

3036
const transferSTXAsync = useCallback(
3137
async (variables: TransferSTXVariables): Promise<string> => {
3238
if (!isConnected || !address) {
33-
throw new Error('Wallet is not connected');
39+
throw new WalletNotConnectedError();
3440
}
3541

3642
setStatus('pending');
@@ -40,7 +46,7 @@ export const useTransferSTX = () => {
4046
try {
4147
if (provider === 'okx') {
4248
if (!window.okxwallet) {
43-
throw new Error('OKX wallet not found');
49+
throw new WalletNotFoundError({ wallet: 'OKX' });
4450
}
4551

4652
const response =
@@ -81,8 +87,13 @@ export const useTransferSTX = () => {
8187
setStatus('success');
8288
return response.txid;
8389
} catch (err) {
84-
const error =
85-
err instanceof Error ? err : new Error(String(err));
90+
const error = err instanceof BaseError
91+
? err
92+
: new WalletRequestError({
93+
method: 'stx_transferStx',
94+
wallet: provider ?? 'unknown',
95+
cause: err instanceof Error ? err : new Error(String(err)),
96+
});
8697
setError(error);
8798
setStatus('error');
8899
throw error;

packages/kit/src/hooks/use-write-contract/use-write-contract.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import type { ClarityValue } from '@stacks/transactions';
55
import { PostConditionMode } from '@stacks/transactions';
66
import { useCallback, useMemo, useState } from 'react';
77

8+
import {
9+
BaseError,
10+
WalletNotConnectedError,
11+
WalletNotFoundError,
12+
WalletRequestError,
13+
} from '../../errors';
814
import type { MutationStatus } from '../../provider/stacks-wallet-provider.types';
915
import { useAddress } from '../use-address';
1016
import { getNetworkFromAddress } from '../../utils/get-network-from-address';
@@ -65,13 +71,13 @@ export const useWriteContract = () => {
6571
const { isConnected, address, provider } = useAddress();
6672

6773
const [data, setData] = useState<string | undefined>(undefined);
68-
const [error, setError] = useState<Error | null>(null);
74+
const [error, setError] = useState<BaseError | null>(null);
6975
const [status, setStatus] = useState<MutationStatus>('idle');
7076

7177
const writeContractAsync = useCallback(
7278
async (variables: WriteContractVariablesInternal): Promise<string> => {
7379
if (!isConnected || !address) {
74-
throw new Error('Wallet is not connected');
80+
throw new WalletNotConnectedError();
7581
}
7682

7783
setStatus('pending');
@@ -83,7 +89,7 @@ export const useWriteContract = () => {
8389
try {
8490
if (provider === 'okx') {
8591
if (!window.okxwallet) {
86-
throw new Error('OKX wallet not found');
92+
throw new WalletNotFoundError({ wallet: 'OKX' });
8793
}
8894

8995
const response =
@@ -127,8 +133,13 @@ export const useWriteContract = () => {
127133
setStatus('success');
128134
return response.txid;
129135
} catch (err) {
130-
const error =
131-
err instanceof Error ? err : new Error(String(err));
136+
const error = err instanceof BaseError
137+
? err
138+
: new WalletRequestError({
139+
method: 'stx_callContract',
140+
wallet: provider ?? 'unknown',
141+
cause: err instanceof Error ? err : new Error(String(err)),
142+
});
132143
setError(error);
133144
setStatus('error');
134145
throw error;

packages/kit/src/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
// Errors
2+
export {
3+
BaseError,
4+
type BaseErrorType,
5+
WalletNotConnectedError,
6+
type WalletNotConnectedErrorType,
7+
WalletNotFoundError,
8+
type WalletNotFoundErrorType,
9+
UnsupportedMethodError,
10+
type UnsupportedMethodErrorType,
11+
WalletRequestError,
12+
type WalletRequestErrorType,
13+
} from './errors';
14+
115
// Provider
216
export { StacksWalletProvider } from './provider/stacks-wallet-provider';
317

0 commit comments

Comments
 (0)