Skip to content
91 changes: 91 additions & 0 deletions packages/connect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,97 @@ try {
}
```

### Common Error Codes

When using `isJsonRpcError` to check errors, you can reference these common error codes:

| Code | Name | Description |
|------|------|-------------|
| `-32700` | `ParseError` | Invalid JSON received by server |
| `-32600` | `InvalidRequest` | Invalid Request object |
| `-32601` | `MethodNotFound` | Method not found/available |
| `-32602` | `InvalidParams` | Invalid method parameters |
| `-32603` | `InternalError` | Internal JSON-RPC error |
| `-32000` | `UserRejection` | User rejected the request |
| `-32001` | `MethodAddressMismatch` | Address mismatch for requested method |
| `-32002` | `MethodAccessDenied` | Access denied for requested method |
| `-32003` | `NetworkError` | Network-related error (e.g., node unavailable) |
| `-32004` | `TimeoutError` | Request timed out |
| `-32005` | `ProviderNotFound` | Wallet provider not found |
| `-31000` | `UnknownError` | Unknown external error |
| `-31001` | `UserCanceled` | User canceled the request |

## Troubleshooting

### Common Issues

#### "No wallet found" or provider not detected

**Symptoms**: `isStacksWalletInstalled()` returns `false`, or connection fails.

**Solutions**:
1. Ensure a Stacks-compatible wallet (Leather, Xverse, etc.) is installed
2. Refresh the page after installing the wallet extension
3. Check if the wallet is enabled for your domain
4. Try using `forceWalletSelect: true` to prompt wallet selection:
```ts
await connect({ forceWalletSelect: true });
```

#### User rejected request (Error code -32000)

**Symptoms**: `JsonRpcError` with code `-32000`.

**Solutions**:
1. This is expected behavior when the user clicks "Reject" or closes the wallet popup
2. Handle this gracefully in your UI:
```ts
try {
await request('stx_transferStx', params);
} catch (error) {
if (isJsonRpcError(error) && error.code === -32000) {
console.log('User rejected - show friendly message');
}
}
```

#### Transaction fails with "Invalid params"

**Symptoms**: `JsonRpcError` with code `-32602`.

**Solutions**:
1. Verify all required parameters are provided
2. Check that amounts are strings (not numbers) for STX transfers
3. Ensure addresses are valid Stacks addresses (starting with `SP` or `ST`)
4. For contract calls, verify function arguments match the contract's expected types

#### Connection works in development but fails in production

**Symptoms**: Wallet connects locally but not on deployed site.

**Solutions**:
1. Ensure your site uses HTTPS (required by most wallets)
2. Check if the wallet has granted permission for your production domain
3. Verify there are no Content Security Policy (CSP) issues blocking the wallet

### Debugging Tips

1. **Enable console logging** to see wallet communication:
```ts
// Check what provider is being used
console.log('Provider:', getStacksProvider());
```

2. **Verify connection state**:
```ts
console.log('Connected:', isConnected());
console.log('Storage:', getLocalStorage());
```

3. **Use `requestRaw` for debugging** to bypass compatibility layers and see raw wallet responses.

4. **Check wallet-specific documentation** - Leather and Xverse have different method support (see Support table below).

## Compatibility

The `request` method by default adds a layer of auto-compatibility for different wallet providers.
Expand Down
59 changes: 59 additions & 0 deletions packages/connect/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { JsonRpcResponseError } from './methods';

/**
* Represents a JSON-RPC 2.0 error with code, message, and optional data.
*
* @see {@link JsonRpcErrorCode} for standard and custom error codes
*
* @example
* ```ts
* const error = new JsonRpcError('User rejected request', JsonRpcErrorCode.UserRejection);
* console.log(error.toString()); // "JsonRpcError (-32000): User rejected request"
* ```
*/
export class JsonRpcError extends Error {
constructor(
public message: string,
Expand All @@ -17,15 +28,47 @@ export class JsonRpcError extends Error {
this.cause = cause;
}

/**
* Creates a JsonRpcError from a JSON-RPC response error object.
*
* @param error - The error object from a JSON-RPC response
* @returns A new JsonRpcError instance
*/
static fromResponse(error: JsonRpcResponseError) {
return new JsonRpcError(error.message, error.code, error.data);
}

/**
* Returns a human-readable string representation of the error.
*
* @returns Formatted error string including name, code, message, and optional data
*/
toString() {
return `${this.name} (${this.code}): ${this.message}${this.data ? `: ${JSON.stringify(this.data)}` : ''}`;
}
}

/**
* Type guard to check if an error is a JsonRpcError.
*
* @param error - The error to check
* @returns `true` if the error is a JsonRpcError instance
*
* @example
* ```ts
* try {
* await request('stx_transferStx', params);
* } catch (error) {
* if (isJsonRpcError(error)) {
* console.log('JSON-RPC Error:', error.code, error.message);
* }
* }
* ```
*/
export function isJsonRpcError(error: unknown): error is JsonRpcError {
return error instanceof JsonRpcError;
}

/**
* Numeric error codes for JSON-RPC errors, used for `.code` in {@link JsonRpcError}.
* Implementation-defined wallet errors range from `-32099` to `-32000`.
Expand Down Expand Up @@ -59,6 +102,21 @@ export enum JsonRpcErrorCode {
/** Access denied for the requested method (implementation-defined wallet error) */
MethodAccessDenied = -32_002,

/** Network-related error, e.g., node unavailable (implementation-defined wallet error) */
NetworkError = -32_003,

/** Request timed out (implementation-defined wallet error) */
TimeoutError = -32_004,

/** Wallet provider not found or not installed (implementation-defined wallet error) */
ProviderNotFound = -32_005,

/** Method is not supported by this wallet (implementation-defined wallet error) */
UnsupportedMethod = -32_006,

/** Invalid or unsupported network configuration (implementation-defined wallet error) */
InvalidNetwork = -32_007,

// CUSTOM ERRORS (Custom range, not inside the JSON-RPC error code range)
/**
* Unknown external error.
Expand All @@ -72,3 +130,4 @@ export enum JsonRpcErrorCode {
*/
UserCanceled = -31_001,
}

72 changes: 66 additions & 6 deletions packages/connect/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,42 @@ import {
} from '@stacks/transactions-v6';
import { ConnectNetwork } from './types';

/** @deprecated This will default to the legacy provider. The behavior may be undefined with competing wallets. */
/**
* Gets the current Stacks wallet provider instance.
*
* @deprecated This will default to the legacy provider. The behavior may be undefined with competing wallets.
* Consider using the `request` method instead for better wallet compatibility.
*
* @returns The current Stacks provider instance, or undefined if no provider is available.
* @see {@link isStacksWalletInstalled} to check if a wallet is installed
*
* @example
* ```ts
* const provider = getStacksProvider();
* if (provider) {
* // Provider is available
* }
* ```
*/
export function getStacksProvider() {
const provider = getProviderFromId(getSelectedProviderId());
return provider || (window as any).StacksProvider || (window as any).BlockstackProvider;
}

/**
* Checks if a Stacks-compatible wallet extension is installed and available.
*
* @returns `true` if a Stacks wallet provider is detected, `false` otherwise.
*
* @example
* ```ts
* if (isStacksWalletInstalled()) {
* console.log('Wallet is available!');
* } else {
* console.log('Please install a Stacks wallet');
* }
* ```
*/
export function isStacksWalletInstalled() {
return !!getStacksProvider();
}
Expand All @@ -37,11 +67,23 @@ export function legacyNetworkFromConnectNetwork(network?: ConnectNetwork): Legac
: new LegacyStacksTestnet({ url: network.client.baseUrl });
}

function isInstance<T>(object: any, clazz: { new (...args: any[]): T }): object is T {
function isInstance<T>(object: any, clazz: { new(...args: any[]): T }): object is T {
return object instanceof clazz || object?.constructor?.name?.toLowerCase() === clazz.name;
}

/** @internal */
/**
* Converts a ConnectNetwork to its string representation.
*
* @internal
* @param network - The network configuration to convert. Can be a string, legacy network object, or new network format.
* @returns A string identifying the network: 'mainnet', 'testnet', 'devnet', or a custom URL.
*
* @example
* ```ts
* connectNetworkToString('testnet'); // Returns 'testnet'
* connectNetworkToString(undefined); // Returns 'mainnet'
* ```
*/
export function connectNetworkToString(network: ConnectNetwork): string {
// not perfect, but good enough to identify the legacy network in most cases
if (!network) return 'mainnet';
Expand All @@ -62,8 +104,16 @@ export function connectNetworkToString(network: ConnectNetwork): string {
}

/**
* @internal
* This may be moved to Stacks.js in the future.
* Converts a legacy Clarity value format to the modern format.
*
* @internal - This may be moved to Stacks.js in the future.
* @param cv - The Clarity value to convert. Can be either legacy or modern format.
* @returns The converted Clarity value in modern format.
* @throws {Error} If an unknown Clarity type is encountered.
*
* @remarks
* This function handles conversion from the v6 transaction format to the current format.
* If the value is already in modern format (string type), it's returned as-is.
*/
export function legacyCVToCV(cv: LegacyClarityValue | ClarityValue): ClarityValue {
if (typeof cv.type === 'string') return cv;
Expand Down Expand Up @@ -107,7 +157,17 @@ export function legacyCVToCV(cv: LegacyClarityValue | ClarityValue): ClarityValu
}
}

/** @internal */
/**
* Removes callback functions that cannot be serialized from an options object.
*
* @internal
* @param obj - The object to sanitize, typically wallet request options.
* @returns A new object with `onFinish` and `onCancel` callbacks removed.
*
* @remarks
* This is used when preparing request parameters to be sent to wallet providers,
* as callback functions cannot be serialized for cross-context communication.
*/
export function removeUnserializableKeys<O>(
obj: O | { onFinish?: Function; onCancel?: Function }
): O {
Expand Down
Loading