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
33 changes: 33 additions & 0 deletions canton-connect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,39 @@ and a wallet-side disconnect look the same
here. `useLedger().isReady` covers both, and `useParty().party` is `undefined` for the duration:
gate session content on the party, and use `isLocked` only to explain why it went away.

### Connecting through a Wallet Gateway

```tsx
import { RemoteAdapter } from '@canton-network/dapp-sdk'

const config = {
appName: 'My dApp',
additionalAdapters: [
new RemoteAdapter({ name: 'Gateway', rpcUrl: 'http://localhost:3030/api/v0/dapp' }),
],
}
```

> [!NOTE]
> The dapp-sdk wallet picker also lets a user paste any gateway URL and connect to it without the
> dApp listing it. That session does not survive a reload; one with a gateway listed here does.

Details in [architecture.md](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-connect/architecture.md#remote-gateway).

### Connecting through WalletConnect

```tsx
const config = {
appName: 'My dApp',
networkId: 'canton:devnet',
walletConnectProjectId: 'YOUR_REOWN_PROJECT_ID',
}
```

`networkId` is the Canton network the dApp targets, as a CAIP-2 chain id.

Details in [architecture.md](https://github.com/BootNodeDev/canton-dappbooster/blob/main/canton-connect/architecture.md#walletconnect).

## Reference

Every hook and every config field is documented in JSDoc, which your editor surfaces at the call
Expand Down
48 changes: 46 additions & 2 deletions canton-connect/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,52 @@ init actor passes `defaultAdapters: []`, dropping the SDK's bundled `localhost:3
`networkId` (default `'canton:local'`) is both the WalletConnect `chainId` and the fallback
`Party.networkId` for a wallet that reports none.

### Remote gateway

Configured like any adapter: `additionalAdapters: [new RemoteAdapter({ name, rpcUrl })]`, with
`RemoteAdapter` imported from `dapp-sdk`. Connect, restore, disconnect and execute reach a gateway
with no change to this package, and the popup-close guard behaves as it does on an extension. The
flow itself (login page, status push, review page) is the SDK's:
https://docs.canton.network/sdks-tools/sdks/dapp-sdk/wallet-providers/remote-wallet.md.

What differs on a gateway:

- Restore after a reload works only for a gateway registered through `additionalAdapters` at init:
`RemoteAdapter.restore()` matches the stored discovery URL against a registered `rpcUrl`, so a URL
the user typed into the SDK picker has nothing to match next time.
- No lock: a gateway has none, so `useWalletStatus().isLocked` never turns true.
- `ledgerApi` must name the route as a template with the values in `path`; a route with the value
written in is refused. See [Ledger reads](#ledger-reads).

### WalletConnect

`walletConnectProjectId` is what makes `buildAdditionalAdapters` build the SDK's
`WalletConnectAdapter` (Adapters above). Execute, disconnect and the popup-close guard behave as on
an extension. Pairing and requests are the SDK's:
https://docs.canton.network/sdks-tools/sdks/dapp-sdk/wallet-providers/walletconnect.md.

What differs over WalletConnect:

- Restore after a reload is silent: the sign client persists the session.
- No lock reaches the dApp: `useWalletStatus().isLocked` never turns true, and a request sent to a
locked wallet waits.
- `networkId` is the CAIP-2 chain id of the network the dApp targets: a wallet on another network
cannot pair, and the default `canton:local` is a local participant, so a dApp on devnet or mainnet
sets it.

### Ledger reads

`ledgerApi` names its route the way Canton's JSON API OpenAPI does: templated, with the variable
parts in `path`, never written into the string. It is the shape the SDK's own client sends, and the
only one a gateway accepts.

```text
resource: '/v2/users/{user-id}/rights', path: { 'user-id': userId } // the SDK's shape
resource: `/v2/users/${userId}/rights` // a gateway refuses this
```

`query` holds query parameters, `body` the request body; neither goes into `resource`.

### The party type

A party under the hosting participant's namespace is local, any other is external. A dApp cares
Expand All @@ -131,8 +177,6 @@ announce, detect and connect path. `createAutoPicker` answers the picker headles

## Deferred

- Remote / Wallet Gateway (OIDC) path: a configurable `RemoteAdapter` through `additionalAdapters`
and `CantonConnectConfig` (#2).
- Themed in-page picker (#50): its PR (#63) was closed unmerged, so the SDK popup is still the only
picker; a new attempt starts from the `walletPicker` seam.

Expand Down
3 changes: 2 additions & 1 deletion canton-connect/coming-from-wagmi.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lovely

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The hook names follow wagmi, so a developer arriving from it knows which one to

| wagmi | canton-connect | why |
|---|---|---|
| `connectors: [injected(), walletConnect({ projectId })]` | `walletConnectProjectId`, `additionalAdapters: [new RemoteAdapter(...)]` for a gateway | Extensions are discovered automatically; there is no connector to list for them. `networkId` is the CAIP-2 chain the wallet must serve. |
| none | `useConnect().cancelConnect` | Abandons a connect in flight, rejecting it with `ConnectCancelledError`; wagmi has no cancel. |
| `useAccount().address` | `useParty().party.partyId` | A Canton identity is a party. |
| `useAccount().addresses`, `.connector`, `.chain` | none | Not exposed yet. |
Expand All @@ -14,5 +15,5 @@ The hook names follow wagmi, so a developer arriving from it knows which one to
| `useWriteContract` then `useWaitForTransactionReceipt` | `useExecute().execute`, resolving after execution | The wallet submits and waits; one call covers both. |
| none | `useExecute().lastTx` | The wallet pushes `pending`, `signed`, `executed`, `failed` as it goes; wagmi has no hook returning a stream. |
| `useSignMessage().data`, a hex string | `useSignMessage().signature` | The name says the type. |
| `usePublicClient()`, a typed client | `useLedger().ledgerApi`, untyped, gated by `isReady` | The participant's JSON API, passed through the wallet's session. |
| `useReadContract`, `usePublicClient().request` | `useLedger().ledgerApi`, untyped, gated by `isReady` | The participant's JSON API; the route is templated, with values in `path`, never written into the string. |
| `mutate`, `mutateAsync`, `status`, `variables`, `data` | none; `isPending`, `error`, `reset` carry over | No TanStack Query underneath. |
6 changes: 5 additions & 1 deletion canton-connect/src/hooks/useLedger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export interface UseLedgerResult {
*
* @example
* const { ledgerApi } = useLedger()
* await ledgerApi({ requestMethod: 'get', resource: '/v2/state/ledger-end' })
* await ledgerApi({
* requestMethod: 'get',
* resource: '/v2/users/{user-id}/rights',
* path: { 'user-id': 'alice' },
* })
*
* @category Hooks
*/
Expand Down
17 changes: 16 additions & 1 deletion canton-connect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,22 @@ export interface Party {
* and that surface is yours. The app fields stay inert until `walletConnectProjectId` (Reown) is set.
*
* @example
* const config: CantonConnectConfig = { appName: 'Vesting', networkId: 'canton:devnet' }
* const config: CantonConnectConfig = {
* appName: 'Vesting',
* networkId: 'canton:devnet',
* walletConnectProjectId: 'REOWN_PROJECT_ID',
* }
*
* @example
* import type { CantonConnectConfig } from '#src/types'
* import { RemoteAdapter } from '@canton-network/dapp-sdk'
*
* const config: CantonConnectConfig = {
* appName: 'Vesting',
* additionalAdapters: [
* new RemoteAdapter({ name: 'Gateway', rpcUrl: 'https://gateway.example.com/dapp' }),
* ],
* }
*
* @category Configuration
*/
Expand Down