Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: readme updates and fixes #436

Merged
merged 6 commits into from
Mar 6, 2025
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/serious-planets-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stacks/connect': patch
---

Add `approvedProviderIds` options to filter allowed wallets
5 changes: 5 additions & 0 deletions .changeset/twelve-eels-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@stacks/connect': patch
---

Export error types
23 changes: 6 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,23 @@
<img src="/.github/img/banner.svg" alt="Stacks Connect">
</div>

Connect is a JavaScript library for building web applications connected to [Stacks](https://stacks.co).
## ⚡️ Building a Stacks-enabled web app?

<div align="center">
<code><a href="./packages/connect">@stacks/connect</a></code> •
<code><a href="./packages/connect-react">@stacks/connect-react</a></code> •
<code><a href="./packages/connect-ui">@stacks/connect-ui</a></code>
</div>

> See methods and migration notes in the [`@stacks/connect` documentation](./packages/connect).
Head over to the [`@stacks/connect` README](https://github.com/hirosystems/connect/tree/main/packages/connect).

---

## ⚡️ Installation

Use your favorite package manager to install `@stacks/connect` in your project.
Follow the **Getting Started** section of the [`@stacks/connect` README](https://github.com/hirosystems/connect/tree/main/packages/connect).

> Or use one of our starter-templates to bootstrap a fresh project already including connect using the [command-line](https://github.com/hirosystems/stacks.js-starters) locally via `npm create stacks`
## Development Notes

## 📦 Packages
### Packages

This repository includes three packages:

- [`@stacks/connect`](./packages/connect): The one-stop-shop tool for letting web-apps interact with Stacks web wallets.
- [`@stacks/connect-ui`](./packages/connect-ui): A web-component UI for displaying an intro modal in Stacks web-apps during authentication _(used in the background by `@stacks/connect`)_.
- ~~[`@stacks/connect-react`](./packages/connect-react): A wrapper library for making `@stacks/connect` use in React even easier~~

## 🛠️ Wallet Implementation Guide
### Wallet Implementation Guide

Wallets implement a "Provider" interface.
The latest spec uses a simple JS Object exposing a `.request(method: string, params?: object)` method.
Expand Down Expand Up @@ -86,7 +75,7 @@ window.wbip_providers.push({
});
```

### JSON RPC 2.0
#### JSON RPC 2.0

Wallets may add their own unstandardized methods.
However, the minimum recommended methods are:
Expand Down
122 changes: 115 additions & 7 deletions packages/connect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,19 @@ npm install @stacks/connect@latest

- `request` follows the pattern `request(method: string, params: object)`, see [Usage](#usage) for more details
- `request` is an async function, so replace the `onFinish` and `onCancel` callbacks with `.then().catch()` or `try & await`
- e.g., `showConnect()`, `authenticate()` → `connect()`
- e.g., `useConnect().doContractCall({})` → `request("stx_callContract", {})`
- e.g., `openContractDeploy()` → `request("stx_deployContract", {})`

3. Switch from `showConnect` or`authenticate` to `connect()` methods
1. Switch from `showConnect` or`authenticate` to `connect()` methods

- `connect()` is an alias for `request({forceWalletSelect: true}, 'getAddresses')`
- `connect()` by default caches the user's address in local storage

4. Switch from `UserSession.isSignedIn()` to `isConnected()`
5. Switch from `UserSession.signUserOut()` to `disconnect()`
6. Remove code referencing deprecated methods (`AppConfig`, `UserSession`, etc.)
7. Remove the `@stacks/connect-react` package.
2. Switch from `UserSession.isSignedIn()` to `isConnected()`
3. Switch from `UserSession.signUserOut()` to `disconnect()`
4. Remove code referencing deprecated methods (`AppConfig`, `UserSession`, etc.)
5. Remove the `@stacks/connect-react` package.
- You may need to manually reload a component to see local storage updates.
- No custom hooks are needed to use Stacks Connect anymore.
- We are working on a new `@stacks/react` package that will make usage even easier in the future (e.g. tracking transaction status, reloading components when a connection is established, updating the page when the network changes, and more).
Expand All @@ -76,7 +79,7 @@ npm install @stacks/connect@latest
Previously, the `UserSession` class was used to access the user's addresses and data, which abstracted away the underlying implementation details.
Now, the `request` method is used to directly interact with the wallet, giving developers more explicit control and clarity over what's happening under the hood.
This manual approach makes the wallet interaction more transparent and customizable.
Developer can manually manage the currently connected user's address in e.g. local storage, jotai, etc. or use the `connect()` method to cache the address in local storage.
Developer can manually manage the currently connected user's address in e.g. local storage, jotai, etc. or use the `connect()`/`request()` method to cache the address in local storage.

> [!IMPORTANT]
> For security reasons, the `8.x.x` release only returns the current network's address (where previously both mainnet and testnet addresses were returned).
Expand Down Expand Up @@ -333,6 +336,59 @@ const response = await request('stx_signStructuredMessage', {
// }
```

## Error Handling

The `request` method returns a Promise, allowing you to handle errors using standard Promise-based error handling patterns. You can use either `try/catch` with `async/await` or the `.catch()` method with Promise chains.

### Using try/catch with async/await

```ts
import { request } from '@stacks/connect';

try {
const response = await request('stx_transferStx', {
amount: '1000',
recipient: 'SP2MF04VAGYHGAZWGTEDW5VYCPDWWSY08Z1QFNDSN',
});
// SUCCESS
console.log('Transaction successful:', response.txid);
} catch (error) {
// ERROR
console.error('Wallet returned an error:', error);
}
```

## Compatibility

The `request` method by default adds a layer of auto-compatibility for different wallet providers.
This is meant to unify the interface where wallet providers may not implement methods and results the same way.

| Method | | Notes |
| --------------------------- | --- | ---------------------------------------------------------------------------------------------------- |
| `getAddresses` | 🔵 | <sub>Maps to `wallet_connect` for Xverse-like wallets</sub> |
| `sendTransfer` | 🔵 | <sub>Converts `amount` to number for Xverse, string for Leather</sub> |
| `signPsbt` | 🟡 | <sub>Transforms PSBT format for Leather (base64 to hex) with lossy restructure of `signInputs`</sub> |
| `stx_getAddresses` | 🔵 | <sub>Maps to `wallet_connect` for Xverse-like wallets</sub> |
| `stx_getAccounts` | 🟢 | |
| `stx_getNetworks` | 🟢 | |
| `stx_transferStx` | 🟢 | |
| `stx_transferSip10Ft` | 🟢 | |
| `stx_transferSip9Nft` | 🟢 | |
| `stx_callContract` | 🔵 | <sub>Transforms Clarity values to hex-encoded format for compatibility</sub> |
| `stx_deployContract` | 🔵 | <sub>Transforms Clarity values to hex-encoded format for compatibility</sub> |
| `stx_signTransaction` | 🔵 | <sub>Transforms Clarity values to hex-encoded format for compatibility</sub> |
| `stx_signMessage` | 🔵 | <sub>Transforms Clarity values to hex-encoded format for compatibility</sub> |
| `stx_signStructuredMessage` | 🔵 | <sub>Transforms Clarity values to hex-encoded format for compatibility</sub> |
| `stx_updateProfile` | 🟢 | |
| `stx_accountChange` (event) | 🟢 | |
| `stx_networkChange` (event) | 🟢 | |

- 🟢 No overrides needed for any wallet
- 🔵 Has compatibility overrides that maintain functionality
- 🟡 Has breaking overrides that may lose some information

> To disable this behavior, you can set the `enableOverrides` option to `false` or use the `requestRaw` method detailed below.

## Advanced Usage

### `request`
Expand All @@ -346,10 +402,14 @@ import { request } from '@stacks/connect';
const response = await request(
{
provider?: StacksProvider; // Custom provider to use for the request
defaultProviders?: WbipProvider[]; // Default wallets to display in modal

forceWalletSelect?: boolean; // Force user to select a wallet (default: false)
persistWalletSelect?: boolean; // Persist selected wallet (default: true)
enableOverrides?: boolean; // Enable provider compatibility (default: true)
enableLocalStorage?: boolean; // Store address in local storage (default: true)

defaultProviders?: WbipProvider[]; // Default wallets to display in modal
approvedProviderIds?: string[]; // List of approved provider IDs to show in modal
},
'method',
params
Expand All @@ -363,6 +423,20 @@ const response = await request('method', params);
> For example, it handles converting numeric types between string and number formats as needed by different wallets, and remaps certain method names to match wallet-specific implementations.
> This ensures consistent behavior across different wallet providers without requiring manual adjustments.

> The `approvedProviderIds` option allows you to filter which wallet providers are shown in the connect modal.
> This is useful when you want to limit the available wallet options to specific providers.
> For example, you might only want to support Leather wallet:
>
> ```ts
> connect({ approvedProviderIds: ['LeatherProvider'] });
> ```
>
> Or multiple specific wallets:
>
> ```ts
> connect({ approvedProviderIds: ['LeatherProvider', 'xverse'] });
> ```

### `requestRaw`

The `requestRaw` method provides direct access to wallet providers without the additional features of `request`:
Expand All @@ -376,6 +450,40 @@ const response = await requestRaw(provider, 'method', params);
> Note: `requestRaw` bypasses the UI wallet selector, automatic provider compatibility fixes, and other features that come with `request`.
> Use this when you need more manual control over the wallet interaction process.

## Support

Here's a list of methods and events that are supported by popular wallets:

| Method | Leather | Xverse-like |
| --------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------ |
| `getAddresses` | 🟡 <sub>No support for experimental purposes</sub> | 🟡 <sub>Use `wallet_connect` instead</sub> |
| `sendTransfer` | 🟡 <sub>Expects `amount` as string</sub> | 🟡 <sub>Expects `amount` as number</sub> |
| `signPsbt` | 🟡 <sub>Uses signing index array only</sub> | 🟡 <sub>Uses `signInputs` record instead of array</sub> |
| `stx_getAddresses` | 🟢 | 🔴 |
| `stx_getAccounts` | 🔴 | 🟢 |
| `stx_getNetworks` | 🔴 | 🔴 |
| `stx_transferStx` | 🟢 | 🟢 |
| `stx_transferSip10Ft` | 🟢 | 🔴 |
| `stx_transferSip9Nft` | 🟢 | 🔴 |
| `stx_callContract` | 🟡 <sub>Hex-encoded Clarity values only</sub> | 🟡 <sub>Hex-encoded Clarity values only, no support for `postConditions`</sub> |
| `stx_deployContract` | 🟡 <sub>Hex-encoded Clarity values only</sub> | 🟡 <sub>Hex-encoded Clarity values only, no support for `postConditions`</sub> |
| `stx_signTransaction` | 🟡 <sub>Hex-encoded Clarity values only</sub> | 🟡 <sub>Hex-encoded Clarity values only</sub> |
| `stx_signMessage` | 🟡 <sub>Hex-encoded Clarity values only</sub> | 🟡 <sub>Hex-encoded Clarity values only</sub> |
| `stx_signStructuredMessage` | 🟡 <sub>Hex-encoded Clarity values only</sub> | 🟡 <sub>Hex-encoded Clarity values only</sub> |
| `stx_updateProfile` | 🔴 | 🔴 |

| Event | Leather | Xverse |
| --------------------- | ------- | ------ |
| `accountChange` | 🔴 | 🟢 |
| `accountDisconnected` | 🔴 | 🟢 |
| `networkChange` | 🔴 | 🟢 |
| `stx_accountChange` | 🔴 | 🔴 |
| `stx_networkChange` | 🔴 | 🔴 |

- 🔴 No support (yet)
- 🟡 Partial support
- 🟢 Supported

---

<div align="center"><br>
Expand Down
1 change: 1 addition & 0 deletions packages/connect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './auth';
export * from './providers';
export * from './types';
export * from './ui';
export * from './errors';

// Manual exports to avoid exporting internals (e.g. `LEGACY_XYZ`)
export { getDefaultPsbtRequestOptions, makePsbtToken, openPsbtRequestPopup } from './bitcoin';
Expand Down
31 changes: 23 additions & 8 deletions packages/connect/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export interface ConnectRequestOptions {
*/
provider?: StacksProvider;

/**
* The default wallets to display in the modal.
* Defaults to some known popular wallets.
*/
defaultProviders?: WbipProvider[];

/**
* Forces the user to select a wallet.
* Defaults to `false`.
Expand All @@ -62,6 +56,18 @@ export interface ConnectRequestOptions {
*/
enableLocalStorage?: boolean;

/**
* The default wallets to display in the modal.
* Defaults to some known popular wallets.
*/
defaultProviders?: WbipProvider[];

/**
* A list of provider IDs that are approved to be shown in the Stacks Connect modal.
* If not provided, all default and installed providers will be shown.
*/
approvedProviderIds?: string[];

// todo: maybe add callbacks, if set use them instead of throwing errors
}

Expand Down Expand Up @@ -196,8 +202,11 @@ export async function request<M extends keyof Methods>(

return new Promise((resolve, reject) => {
const element = document.createElement('connect-modal');
element.defaultProviders = opts.defaultProviders;
element.installedProviders = getInstalledProviders(opts.defaultProviders);
element.defaultProviders = filterProviders(opts.approvedProviderIds, opts.defaultProviders);
element.installedProviders = filterProviders(
opts.approvedProviderIds,
getInstalledProviders(opts.defaultProviders)
);

const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
Expand Down Expand Up @@ -254,6 +263,12 @@ function requestArgs<M extends keyof Methods>(
return { options: args[0], method: args[1] as M, params: args[2] };
}

/** @internal */
function filterProviders(approvedProviderIds: string[], providers: WbipProvider[]): WbipProvider[] {
if (!approvedProviderIds) return providers;
return providers.filter(p => approvedProviderIds.includes(p.id));
}

/**
* Initiate a wallet connection and request addresses.
* Alias for `request` to `getAddresses` with `forceWalletSelect: true`.
Expand Down
77 changes: 77 additions & 0 deletions packages/connect/src/stories/Connect.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { DEFAULT_PROVIDERS } from '../providers';
import { ConnectPage } from './ConnectPage';
import { WebBTCProvider } from '@stacks/connect-ui';
import { connect } from '../request';

// Define window types
declare global {
Expand Down Expand Up @@ -59,3 +61,78 @@ export const WithMockedWallet: Story = {
};
},
};

// 4. With approved providers only
export const WithApprovedProvidersOnly: Story = {
render: () => {
const handleConnect = () => {
return connect({
approvedProviderIds: ['LeatherProvider'],
});
};

return (
<ConnectPage customConnectFunction={handleConnect}>
<p>This demo only allows Leather wallet. Click connect() to test.</p>
</ConnectPage>
);
},
};

// 5. With multiple approved providers
export const WithMultipleApprovedProviders: Story = {
render: () => {
const handleConnect = () => {
return connect({
approvedProviderIds: ['LeatherProvider', 'FordefiProviders.UtxoProvider'],
});
};

return (
<ConnectPage customConnectFunction={handleConnect}>
<p>This demo allows only Leather and Fordefi wallets. Click connect() to test.</p>
</ConnectPage>
);
},
};

// 6. With approved providers and custom default providers
export const WithApprovedAndCustomProviders: Story = {
render: () => {
const handleConnect = () => {
// Create a custom set of providers
const customProviders = [
{
id: 'custom-wallet-1',
name: 'Custom Wallet 1',
icon: 'https://via.placeholder.com/48',
},
{
id: 'leather',
name: 'Custom Leather',
icon: 'https://www.leather.io/favicon.ico',
},
{
id: 'custom-wallet-2',
name: 'Custom Wallet 2',
icon: 'https://via.placeholder.com/48',
},
];

return connect({
approvedProviderIds: ['leather', 'xverse'],
defaultProviders: customProviders,
forceWalletSelect: true,
});
};

return (
<ConnectPage customConnectFunction={handleConnect}>
<p>
This demo combines approvedProviderIds with custom defaultProviders. Only "Custom Leather"
should appear.
</p>
</ConnectPage>
);
},
};
Loading
Loading