Skip to content

Commit

Permalink
fix: readme updates and fixes (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks authored Mar 6, 2025
1 parent fb0b4bf commit 1323dc0
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 36 deletions.
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

0 comments on commit 1323dc0

Please sign in to comment.