Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/typed-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@satoshai/kit": minor
---

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`.
95 changes: 95 additions & 0 deletions packages/kit/src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export type BaseErrorType = BaseError & { name: 'StacksKitError' }

export class BaseError extends Error {
override name = 'StacksKitError'

shortMessage: string

constructor(shortMessage: string, options?: { cause?: Error; details?: string }) {
const message = [
shortMessage,
options?.details && `Details: ${options.details}`,
].filter(Boolean).join('\n\n')

super(message, options?.cause ? { cause: options.cause } : undefined)
this.shortMessage = shortMessage
}

walk(fn?: (err: unknown) => boolean): unknown {
return walk(this, fn)
}
}

function walk(err: unknown, fn?: (err: unknown) => boolean): unknown {
if (fn?.(err)) return err
if (err && typeof err === 'object' && 'cause' in err) {
return walk((err as { cause: unknown }).cause, fn)
}
return err
}

export type WalletNotConnectedErrorType = WalletNotConnectedError & {
name: 'WalletNotConnectedError'
}

export class WalletNotConnectedError extends BaseError {
override name = 'WalletNotConnectedError'

constructor() {
super('Wallet is not connected')
}
}

export type WalletNotFoundErrorType = WalletNotFoundError & {
name: 'WalletNotFoundError'
}

export class WalletNotFoundError extends BaseError {
override name = 'WalletNotFoundError'

wallet: string

constructor({ wallet }: { wallet: string }) {
super(`${wallet} wallet not found`, {
details: 'The wallet extension may not be installed.',
})
this.wallet = wallet
}
}

export type UnsupportedMethodErrorType = UnsupportedMethodError & {
name: 'UnsupportedMethodError'
}

export class UnsupportedMethodError extends BaseError {
override name = 'UnsupportedMethodError'

method: string
wallet: string

constructor({ method, wallet }: { method: string; wallet: string }) {
super(`${method} is not supported by ${wallet} wallet`)
this.method = method
this.wallet = wallet
}
}

export type WalletRequestErrorType = WalletRequestError & {
name: 'WalletRequestError'
}

export class WalletRequestError extends BaseError {
override name = 'WalletRequestError'

method: string
wallet: string

constructor({ method, wallet, cause }: { method: string; wallet: string; cause: Error }) {
super(`${wallet} wallet request failed`, {
cause,
details: cause.message,
})
this.method = method
this.wallet = wallet
}
}
21 changes: 16 additions & 5 deletions packages/kit/src/hooks/use-sign-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import { request } from '@stacks/connect';
import { useCallback, useMemo, useState } from 'react';

import {
BaseError,
WalletNotConnectedError,
WalletNotFoundError,
WalletRequestError,
} from '../errors';
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
import { useAddress } from './use-address';

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

const signMessageAsync = useCallback(
async (variables: SignMessageVariables): Promise<SignMessageData> => {
if (!isConnected) {
throw new Error('Wallet is not connected');
throw new WalletNotConnectedError();
}

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

if (provider === 'okx') {
if (!window.okxwallet) {
throw new Error('OKX wallet not found');
throw new WalletNotFoundError({ wallet: 'OKX' });
}

result = await window.okxwallet.stacks.signMessage({
Expand All @@ -65,8 +71,13 @@ export const useSignMessage = () => {
setStatus('success');
return result;
} catch (err) {
const error =
err instanceof Error ? err : new Error(String(err));
const error = err instanceof BaseError
? err
: new WalletRequestError({
method: 'stx_signMessage',
wallet: provider ?? 'unknown',
cause: err instanceof Error ? err : new Error(String(err)),
});
setError(error);
setStatus('error');
throw error;
Expand Down
26 changes: 19 additions & 7 deletions packages/kit/src/hooks/use-sign-structured-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { request } from '@stacks/connect';
import type { ClarityValue, TupleCV } from '@stacks/transactions';
import { useCallback, useMemo, useState } from 'react';

import {
BaseError,
WalletNotConnectedError,
UnsupportedMethodError,
WalletRequestError,
} from '../errors';
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
import { useAddress } from './use-address';

Expand Down Expand Up @@ -31,21 +37,22 @@ export const useSignStructuredMessage = () => {
const [data, setData] = useState<SignStructuredMessageData | undefined>(
undefined
);
const [error, setError] = useState<Error | null>(null);
const [error, setError] = useState<BaseError | null>(null);
const [status, setStatus] = useState<MutationStatus>('idle');

const signStructuredMessageAsync = useCallback(
async (
variables: SignStructuredMessageVariables
): Promise<SignStructuredMessageData> => {
if (!isConnected) {
throw new Error('Wallet is not connected');
throw new WalletNotConnectedError();
}

if (provider === 'okx') {
throw new Error(
'Structured message signing is not supported by OKX wallet'
);
throw new UnsupportedMethodError({
method: 'stx_signStructuredMessage',
wallet: 'OKX',
});
}

setStatus('pending');
Expand All @@ -62,8 +69,13 @@ export const useSignStructuredMessage = () => {
setStatus('success');
return result;
} catch (err) {
const error =
err instanceof Error ? err : new Error(String(err));
const error = err instanceof BaseError
? err
: new WalletRequestError({
method: 'stx_signStructuredMessage',
wallet: provider ?? 'unknown',
cause: err instanceof Error ? err : new Error(String(err)),
});
setError(error);
setStatus('error');
throw error;
Expand Down
26 changes: 19 additions & 7 deletions packages/kit/src/hooks/use-sign-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import { request } from '@stacks/connect';
import { useCallback, useMemo, useState } from 'react';

import {
BaseError,
WalletNotConnectedError,
UnsupportedMethodError,
WalletRequestError,
} from '../errors';
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
import { useAddress } from './use-address';

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

const signTransactionAsync = useCallback(
async (
variables: SignTransactionVariables
): Promise<SignTransactionData> => {
if (!isConnected) {
throw new Error('Wallet is not connected');
throw new WalletNotConnectedError();
}

if (provider === 'okx') {
throw new Error(
'Transaction signing is not supported by OKX wallet'
);
throw new UnsupportedMethodError({
method: 'stx_signTransaction',
wallet: 'OKX',
});
}

setStatus('pending');
Expand All @@ -61,8 +68,13 @@ export const useSignTransaction = () => {
setStatus('success');
return result;
} catch (err) {
const error =
err instanceof Error ? err : new Error(String(err));
const error = err instanceof BaseError
? err
: new WalletRequestError({
method: 'stx_signTransaction',
wallet: provider ?? 'unknown',
cause: err instanceof Error ? err : new Error(String(err)),
});
setError(error);
setStatus('error');
throw error;
Expand Down
21 changes: 16 additions & 5 deletions packages/kit/src/hooks/use-transfer-stx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
import { request } from '@stacks/connect';
import { useCallback, useMemo, useState } from 'react';

import {
BaseError,
WalletNotConnectedError,
WalletNotFoundError,
WalletRequestError,
} from '../errors';
import type { MutationStatus } from '../provider/stacks-wallet-provider.types';
import { useAddress } from './use-address';
import { getNetworkFromAddress } from '../utils/get-network-from-address';
Expand All @@ -24,13 +30,13 @@ export interface TransferSTXOptions {
export const useTransferSTX = () => {
const { isConnected, address, provider } = useAddress();
const [data, setData] = useState<string | undefined>(undefined);
const [error, setError] = useState<Error | null>(null);
const [error, setError] = useState<BaseError | null>(null);
const [status, setStatus] = useState<MutationStatus>('idle');

const transferSTXAsync = useCallback(
async (variables: TransferSTXVariables): Promise<string> => {
if (!isConnected || !address) {
throw new Error('Wallet is not connected');
throw new WalletNotConnectedError();
}

setStatus('pending');
Expand All @@ -40,7 +46,7 @@ export const useTransferSTX = () => {
try {
if (provider === 'okx') {
if (!window.okxwallet) {
throw new Error('OKX wallet not found');
throw new WalletNotFoundError({ wallet: 'OKX' });
}

const response =
Expand Down Expand Up @@ -81,8 +87,13 @@ export const useTransferSTX = () => {
setStatus('success');
return response.txid;
} catch (err) {
const error =
err instanceof Error ? err : new Error(String(err));
const error = err instanceof BaseError
? err
: new WalletRequestError({
method: 'stx_transferStx',
wallet: provider ?? 'unknown',
cause: err instanceof Error ? err : new Error(String(err)),
});
setError(error);
setStatus('error');
throw error;
Expand Down
21 changes: 16 additions & 5 deletions packages/kit/src/hooks/use-write-contract/use-write-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import type { ClarityValue } from '@stacks/transactions';
import { PostConditionMode } from '@stacks/transactions';
import { useCallback, useMemo, useState } from 'react';

import {
BaseError,
WalletNotConnectedError,
WalletNotFoundError,
WalletRequestError,
} from '../../errors';
import type { MutationStatus } from '../../provider/stacks-wallet-provider.types';
import { useAddress } from '../use-address';
import { getNetworkFromAddress } from '../../utils/get-network-from-address';
Expand Down Expand Up @@ -65,13 +71,13 @@ export const useWriteContract = () => {
const { isConnected, address, provider } = useAddress();

const [data, setData] = useState<string | undefined>(undefined);
const [error, setError] = useState<Error | null>(null);
const [error, setError] = useState<BaseError | null>(null);
const [status, setStatus] = useState<MutationStatus>('idle');

const writeContractAsync = useCallback(
async (variables: WriteContractVariablesInternal): Promise<string> => {
if (!isConnected || !address) {
throw new Error('Wallet is not connected');
throw new WalletNotConnectedError();
}

setStatus('pending');
Expand All @@ -83,7 +89,7 @@ export const useWriteContract = () => {
try {
if (provider === 'okx') {
if (!window.okxwallet) {
throw new Error('OKX wallet not found');
throw new WalletNotFoundError({ wallet: 'OKX' });
}

const response =
Expand Down Expand Up @@ -127,8 +133,13 @@ export const useWriteContract = () => {
setStatus('success');
return response.txid;
} catch (err) {
const error =
err instanceof Error ? err : new Error(String(err));
const error = err instanceof BaseError
? err
: new WalletRequestError({
method: 'stx_callContract',
wallet: provider ?? 'unknown',
cause: err instanceof Error ? err : new Error(String(err)),
});
setError(error);
setStatus('error');
throw error;
Expand Down
14 changes: 14 additions & 0 deletions packages/kit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Errors
export {
BaseError,
type BaseErrorType,
WalletNotConnectedError,
type WalletNotConnectedErrorType,
WalletNotFoundError,
type WalletNotFoundErrorType,
UnsupportedMethodError,
type UnsupportedMethodErrorType,
WalletRequestError,
type WalletRequestErrorType,
} from './errors';

// Provider
export { StacksWalletProvider } from './provider/stacks-wallet-provider';

Expand Down
Loading