The Unified Wallet SDK for Canton Network
Connect your dApp to multiple Canton wallets with a single integration.
Website | Quick Start | CIP-0103 | Documentation | Examples | API Reference | Contributing
PartyLayer is a production-grade SDK that enables decentralized applications (dApps) on the Canton Network to connect with multiple wallet providers through a unified interface. It abstracts away the complexity of integrating with different wallets, giving you a single API for every Canton wallet.
- CIP-0103 Compliant: Implements the Canton dApp Standard for wallet-dApp communication
- Single Integration: Connect to all Canton wallets with one SDK
- Type-Safe: Full TypeScript support with strict mode
- React Ready: First-class React hooks and components
- Secure: Origin-bound sessions with encrypted storage
- Extensible: Easy to add custom wallet adapters
- Production Ready: Battle-tested with comprehensive error handling
| Wallet | Type | Transport | Auto-registered | Status |
|---|---|---|---|---|
| Console Wallet | Extension + Mobile | Injected / QR / Deep Link | ✅ Yes | Ready |
| 5N Loop | Mobile / Web | QR Code / Popup | ✅ Yes | Ready |
| Cantor8 (C8) | Browser Extension | Deep Link | ✅ Yes | Ready |
| Nightly | Multichain Wallet | Injected | ✅ Yes | Ready |
| Bron | Enterprise | OAuth2 / API | ⚙️ Requires config | Ready |
Note: Bron is an enterprise wallet that requires OAuth configuration. See Using Bron for setup instructions.
Native CIP-0103 wallets are also auto-discovered at runtime via
window.canton.*— no configuration needed.
Get started in under 3 minutes:
npm install @partylayer/sdk @partylayer/reactimport { PartyLayerKit, ConnectButton } from '@partylayer/react';
function App() {
return (
<PartyLayerKit network="devnet" appName="My dApp">
<ConnectButton />
<YourApp />
</PartyLayerKit>
);
}That's it! PartyLayerKit automatically:
- Creates and manages the SDK client
- Registers all built-in wallet adapters (Console, Loop, Cantor8, Nightly)
- Discovers native CIP-0103 wallets (
window.canton.*) - Provides light/dark/auto theme support
import { createPartyLayer } from '@partylayer/sdk';
const client = createPartyLayer({
network: 'devnet',
app: { name: 'My dApp' },
});
// List available wallets
const wallets = await client.listWallets();
// Connect to a wallet
const session = await client.connect({ walletId: 'console' });
console.log('Connected:', session.partyId);
// Sign a message
const signed = await client.signMessage({ message: 'Hello, Canton!' });# npm
npm install @partylayer/sdk
# yarn
yarn add @partylayer/sdk
# pnpm
pnpm add @partylayer/sdknpm install @partylayer/sdk @partylayer/reactPartyLayer is written in TypeScript and includes type definitions out of the box. No additional @types packages needed.
Minimum Requirements:
- Node.js 18+
- TypeScript 5.0+ (if using TypeScript)
- React 18+ (if using React integration)
import { createPartyLayer } from '@partylayer/sdk';
// Initialize
const client = createPartyLayer({
registryUrl: 'https://registry.partylayer.xyz',
network: 'devnet',
app: { name: 'My dApp' },
});
// Connect
const session = await client.connect({ walletId: 'console' });
// Sign message
const { signature } = await client.signMessage({
message: 'Authorize login',
});
// Sign transaction
const { signedTx } = await client.signTransaction({
tx: myTransaction,
});
// Submit transaction
const { txHash } = await client.submitTransaction({
signedTx,
});
// Listen to events
client.on('session:connected', (event) => {
console.log('Connected:', event.session);
});
client.on('session:disconnected', () => {
console.log('Disconnected');
});
// Disconnect
await client.disconnect();The simplest way to add wallet connectivity to a React app:
import { PartyLayerKit, ConnectButton } from '@partylayer/react';
function App() {
return (
<PartyLayerKit
network="devnet"
appName="My dApp"
theme="auto" // 'light' | 'dark' | 'auto' | custom theme object
>
<ConnectButton />
<MyDApp />
</PartyLayerKit>
);
}ConnectButton renders a polished button that handles the entire connect flow: wallet selection modal, connection, success/error states, and a connected dropdown with disconnect.
For custom UIs, use the hooks directly:
import {
PartyLayerKit,
useSession,
useWallets,
useConnect,
useDisconnect,
useSignMessage,
WalletModal,
} from '@partylayer/react';
function App() {
return (
<PartyLayerKit network="devnet" appName="My dApp">
<MyDApp />
</PartyLayerKit>
);
}
function WalletStatus() {
const session = useSession();
const { wallets, isLoading } = useWallets();
const { connect, isConnecting } = useConnect();
const { disconnect } = useDisconnect();
const { signMessage } = useSignMessage();
if (session) {
return (
<div>
<p>Connected: {session.partyId}</p>
<button onClick={disconnect}>Disconnect</button>
<button onClick={() => signMessage({ message: 'Hello!' })}>
Sign Message
</button>
</div>
);
}
return (
<div>
{wallets.map((wallet) => (
<button
key={String(wallet.walletId)}
onClick={() => connect({ walletId: wallet.walletId })}
disabled={isConnecting}
>
Connect {wallet.name}
</button>
))}
</div>
);
}// app/providers.tsx
'use client';
import { PartyLayerKit, ConnectButton } from '@partylayer/react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<PartyLayerKit
network="devnet"
appName="My Next.js App"
registryUrl={process.env.NEXT_PUBLIC_REGISTRY_URL}
theme="auto"
>
{children}
</PartyLayerKit>
);
}Monorepo note: If using workspace packages, add
@partylayer/*packages totranspilePackagesand webpackresolve.aliasinnext.config.js. See the demo app for a complete example.
Bron requires OAuth2 configuration and is not auto-registered. Add it manually:
import { createPartyLayer, BronAdapter, getBuiltinAdapters } from '@partylayer/sdk';
const client = createPartyLayer({
network: 'devnet',
app: { name: 'My dApp' },
adapters: [
...getBuiltinAdapters(), // Console, Loop, Cantor8, Nightly
new BronAdapter({
auth: {
clientId: 'your-client-id',
redirectUri: 'https://your-app.com/auth/callback',
authorizationUrl: 'https://auth.bron.example/authorize',
tokenUrl: 'https://auth.bron.example/token',
},
api: {
baseUrl: 'https://api.bron.example',
getAccessToken: async () => getStoredAccessToken(),
},
}),
],
});import {
WalletNotFoundError,
WalletNotInstalledError,
UserRejectedError,
SessionExpiredError,
TimeoutError,
} from '@partylayer/sdk';
try {
await client.connect({ walletId: 'console' });
} catch (error) {
if (error instanceof WalletNotFoundError) {
console.error('Wallet not found in registry');
} else if (error instanceof WalletNotInstalledError) {
console.error('Please install the wallet extension');
} else if (error instanceof UserRejectedError) {
console.error('User rejected the connection');
} else if (error instanceof SessionExpiredError) {
console.error('Session expired, please reconnect');
} else if (error instanceof TimeoutError) {
console.error('Connection timed out');
} else {
console.error('Unexpected error:', error);
}
}graph TD
subgraph dApp[dApp Application]
A[Your App]
B["@partylayer/react"]
end
subgraph SDK[PartyLayer SDK]
C["@partylayer/sdk"]
D[Session Manager]
E[Event System]
F[Storage]
end
subgraph Adapters[Wallet Adapters]
G[Console]
H[Loop]
I[Cantor8]
J[Bron]
O[Nightly]
end
subgraph Wallets[Canton Wallets]
K[Console Wallet]
L[5N Loop]
M[Cantor8 Wallet]
N[Bron Wallet]
P[Nightly Wallet]
end
subgraph Native[CIP-0103 Native]
Q[window.canton.*]
end
A --> B
B --> C
C --> D
C --> E
C --> F
C --> G
C --> H
C --> I
C --> J
C --> O
G --> K
H --> L
I --> M
J --> N
O --> P
B -.->|auto-discovery| Q
| Package | Description |
|---|---|
@partylayer/core |
Core types, errors, transport abstractions, and CIP-0103 type definitions |
@partylayer/sdk |
Main SDK with session management and event system |
@partylayer/provider |
CIP-0103 native Provider, bridge, wallet discovery, and error model |
@partylayer/react |
React hooks, provider, and wallet modal |
@partylayer/registry-client |
Registry fetching, caching, and validation |
@partylayer/conformance-runner |
CLI for adapter and CIP-0103 conformance testing |
@partylayer/adapter-console |
Console Wallet adapter |
@partylayer/adapter-loop |
5N Loop adapter |
@partylayer/adapter-cantor8 |
Cantor8 adapter |
@partylayer/adapter-bron |
Bron adapter |
@partylayer/adapter-nightly |
Nightly multichain wallet adapter |
Creates a new PartyLayer client instance.
const client = createPartyLayer(config: PartyLayerConfig);| Option | Type | Required | Default | Description |
|---|---|---|---|---|
network |
'devnet' | 'testnet' | 'mainnet' |
Yes | - | Target network |
app.name |
string |
Yes | - | Your application name |
registryUrl |
string |
No | https://registry.partylayer.xyz/v1/wallets.json |
URL of the wallet registry |
app.origin |
string |
No | window.location.origin |
Application origin |
channel |
'stable' | 'beta' |
No | 'stable' |
Registry channel |
storage |
StorageAdapter |
No | localStorage |
Custom storage adapter |
adapters |
WalletAdapter[] |
No | All built-in adapters | Custom wallet adapters |
| Method | Description |
|---|---|
listWallets(filter?) |
List available wallets (registry + registered adapters) |
connect(options?) |
Connect to a wallet |
disconnect() |
Disconnect from current wallet |
getActiveSession() |
Get the current active session |
signMessage(params) |
Sign an arbitrary message |
signTransaction(params) |
Sign a transaction |
submitTransaction(params) |
Submit a signed transaction |
registerAdapter(adapter) |
Register a custom wallet adapter at runtime |
asProvider() |
Get a CIP-0103 compliant Provider bridge |
on(event, handler) |
Subscribe to events |
off(event, handler) |
Unsubscribe from events |
destroy() |
Clean up client resources |
| Hook | Description |
|---|---|
usePartyLayer() |
Access the SDK client instance |
useSession() |
Get the current session |
useWallets() |
Get available wallets (registry + native CIP-0103) |
useConnect() |
Connect hook with loading state |
useDisconnect() |
Disconnect hook with loading state |
useSignMessage() |
Sign message hook |
useSignTransaction() |
Sign transaction hook |
useSubmitTransaction() |
Submit transaction hook |
useRegistryStatus() |
Get registry status |
useWalletIcons() |
Access wallet icon overrides from PartyLayerKit |
useTheme() |
Access current theme (light/dark/auto) |
| Event | Description |
|---|---|
session:connected |
Wallet connected successfully |
session:disconnected |
Wallet disconnected |
session:expired |
Session has expired |
tx:status |
Transaction status update |
registry:status |
Registry status change |
error |
Error occurred |
PartyLayer implements several security measures:
Sessions are bound to the domain that created them, preventing cross-site session hijacking.
Session data is encrypted using Web Crypto API (AES-GCM) before storing in localStorage.
All wallet operations require explicit user approval:
- Connection requests display app name and permissions
- Signing operations show the payload to be signed
- Transaction submissions display transaction details
The wallet registry supports cryptographic signatures to prevent tampering.
PartyLayer implements CIP-0103, the Canton dApp Standard for wallet-dApp communication. The @partylayer/provider package provides a spec-compliant Provider interface.
PartyLayer offers two paths to CIP-0103 compliance:
Native Provider (PartyLayerProvider) — wraps any CIP-0103-compliant wallet provider (e.g. window.canton.*). This is the recommended path for new integrations.
import { PartyLayerProvider, discoverInjectedProviders } from '@partylayer/provider';
// Discover injected CIP-0103 wallets
const wallets = discoverInjectedProviders();
// Create a Provider backed by a native wallet
const provider = new PartyLayerProvider({ wallet: wallets[0].provider });Legacy Bridge (createProviderBridge) — wraps an existing PartyLayerClient instance as a CIP-0103 Provider. Use this for backward compatibility with the adapter-based SDK.
import { createPartyLayer } from '@partylayer/sdk';
const client = createPartyLayer({ network: 'devnet', app: { name: 'My dApp' } });
const provider = client.asProvider();All 10 mandatory methods are implemented on both paths:
| Method | Description |
|---|---|
connect |
Establish wallet connection |
disconnect |
Close wallet connection |
isConnected |
Check connection status |
status |
Get full provider status (connection, identity, network, session) |
getActiveNetwork |
Get active network as a CAIP-2 identifier |
listAccounts |
List available accounts |
getPrimaryAccount |
Get the primary account |
signMessage |
Sign an arbitrary message |
prepareExecute |
Prepare and submit a transaction |
ledgerApi |
Proxy Ledger API requests (native path only) |
import { PartyLayerProvider } from '@partylayer/provider';
const provider = new PartyLayerProvider({ wallet: nativeWallet });
// Connect
const result = await provider.request({ method: 'connect' });
console.log(result.isConnected); // true
// Listen for status changes
provider.on('statusChanged', (status) => {
console.log('Connection:', status.connection.isConnected);
console.log('Network:', status.network?.networkId);
});
// Listen for transaction lifecycle
provider.on('txChanged', (event) => {
switch (event.status) {
case 'pending': console.log('Tx pending:', event.commandId); break;
case 'executed': console.log('Tx executed:', event.payload.updateId); break;
case 'failed': console.log('Tx failed:', event.commandId); break;
}
});
// Sign a message
const signature = await provider.request({
method: 'signMessage',
params: { message: 'Hello, Canton!' },
});
// Prepare and execute a transaction
await provider.request({
method: 'prepareExecute',
params: { /* transaction payload */ },
});The native PartyLayerProvider supports both synchronous and asynchronous wallet flows:
- Sync wallets return results immediately from
connect()andprepareExecute(). - Async wallets (e.g. mobile wallets with QR codes) return a
userUrlfromconnect(). The Provider handles the async flow automatically: it opens the URL, waits for theconnectedevent, and resolves the Promise when the wallet confirms.
Note: The legacy bridge (
client.asProvider()) only supports synchronous wallets. For async wallet flows, use the nativePartyLayerProviderdirectly.
The CIP-0103 Provider uses ProviderRpcError with standard numeric error codes from EIP-1193 and EIP-1474:
EIP-1193 (Provider) Codes:
| Code | Name | Description |
|---|---|---|
| 4001 | User Rejected | User rejected the request |
| 4100 | Unauthorized | Not authorized for this operation |
| 4200 | Unsupported Method | Method not supported by this provider |
| 4900 | Disconnected | Provider is disconnected |
| 4901 | Chain Disconnected | Provider is disconnected from the requested chain |
EIP-1474 (JSON-RPC) Codes:
| Code | Name | Description |
|---|---|---|
| -32700 | Parse Error | Invalid JSON |
| -32600 | Invalid Request | Request object is invalid |
| -32601 | Method Not Found | Method does not exist |
| -32602 | Invalid Params | Invalid method parameters |
| -32603 | Internal Error | Internal provider error |
| -32000 | Invalid Input | Missing or invalid parameter |
| -32001 | Resource Not Found | Requested resource not found |
| -32002 | Resource Unavailable | Requested resource is not available |
| -32003 | Transaction Rejected | Transaction was rejected |
| -32004 | Method Not Supported | Method is not implemented |
| -32005 | Limit Exceeded | Rate or resource limit exceeded |
import { ProviderRpcError, RPC_ERRORS } from '@partylayer/provider';
try {
await provider.request({ method: 'signMessage', params: { message: 'test' } });
} catch (err) {
if (err instanceof ProviderRpcError) {
switch (err.code) {
case RPC_ERRORS.USER_REJECTED:
console.log('User declined');
break;
case RPC_ERRORS.DISCONNECTED:
console.log('Not connected');
break;
default:
console.log(`Error ${err.code}: ${err.message}`);
}
}
}The @partylayer/conformance-runner package validates CIP-0103 compliance for any Provider implementation:
import { runCIP0103ConformanceTests, formatCIP0103Report } from '@partylayer/conformance-runner';
const report = await runCIP0103ConformanceTests(provider);
console.log(formatCIP0103Report(report));
// ═══ CIP-0103 Conformance Report ═══
// Total: 18 Passed: 18 Failed: 0- Node.js 18+
- pnpm 9+
# Clone the repository
git clone https://github.com/PartyLayer/PartyLayer.git
cd PartyLayer
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test# Start demo app
pnpm dev
# Run registry server
pnpm --filter registry-server dev
# Type check
pnpm typecheck
# Lint
pnpm lint
# Clean build artifacts
pnpm cleanwallet-sdk/
├── packages/
│ ├── core/ # Core types, errors, and CIP-0103 type definitions
│ ├── sdk/ # Main SDK implementation
│ ├── provider/ # CIP-0103 native Provider and bridge
│ ├── react/ # React integration
│ ├── registry-client/ # Registry client
│ ├── conformance-runner/ # Adapter & CIP-0103 conformance testing CLI
│ └── adapters/ # Wallet adapters
│ ├── console/
│ ├── loop/
│ ├── cantor8/
│ ├── bron/
│ └── nightly/
├── apps/
│ ├── demo/ # Next.js demo application
│ └── registry-server/ # Registry server
├── examples/
│ └── test-dapp/ # Vite + React example
└── docs/ # Documentation
We welcome contributions! Please see our Contributing Guide for details.
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
pnpm test - Commit with conventional commits:
git commit -m "feat: add new feature" - Push and create a Pull Request
Want to add support for a new wallet? See the Wallet Provider Guide.
- Multi-party support (multiple parties per session)
- Transaction batching
- Offline transaction preparation
- CIP-0103 (Canton dApp Standard) compliance
- Native CIP-0103 wallet discovery (
window.canton.*) - Enhanced telemetry (opt-in, privacy-safe)
- PartyLayerKit zero-config React wrapper
- ConnectButton with wallet modal
- Light/dark/auto theme support
- Nightly multichain wallet support
Which wallets are supported?
5 wallets built-in: Console Wallet, 5N Loop, Cantor8, Nightly, and Bron. Console, Loop, Cantor8, and Nightly are auto-registered. Bron requires OAuth configuration. Additionally, any CIP-0103 compliant wallet injected at window.canton.* is auto-discovered at runtime.
What is PartyLayerKit vs PartyLayerProvider?
PartyLayerKit is a zero-config wrapper that creates the SDK client, registers adapters, discovers native wallets, and sets up theming — all automatically. PartyLayerProvider is the lower-level context provider for advanced use cases where you need full control over client configuration. For most apps, use PartyLayerKit.
Do I need to install wallet adapters separately?
No! All wallet adapters (except Bron) are bundled with @partylayer/sdk. Just install the SDK and you're ready to go.
How does session persistence work?
Sessions are encrypted and stored in localStorage. On page reload, the SDK attempts to restore existing sessions. Wallets that support session restoration (like Console and Nightly) will automatically reconnect.
Is the SDK compatible with Next.js?
Yes! Use PartyLayerKit with the 'use client' directive. For monorepo setups, add @partylayer/* packages to transpilePackages in next.config.js.
How do I handle connection errors?
PartyLayer exports typed error classes. Use try-catch with instanceof checks to handle specific error types like WalletNotInstalledError or UserRejectedError. The WalletModal component handles these automatically with dedicated error views.
- Quick Start Guide
- API Reference
- Architecture
- CIP-0103 Compliance - Provider model, methods, error codes
- CIP-0103 Alignment (detailed) - For Foundation reviewers and wallet providers
- Error Handling
- Wallet Provider Guide
- Security Checklist - For wallet providers
- Production Security Guide - For dApp developers
- GitHub Issues - Bug reports and feature requests
- Discussions - Questions and community
MIT License - see LICENSE for details.
- Canton Network - The privacy-enabled blockchain network
- Digital Asset - Daml and Canton development
Built with ❤️ for the Canton Network ecosystem