Adapterless CIP-0103 integration: the two generic paths
PartyLayer connects dApps to Canton wallets. You do not need a PartyLayer-specific adapter package to be one of those wallets. There are exactly two generic paths, and every CIP-0103 wallet fits one of them. Neither puts any wallet-specific code in the PartyLayer codebase.
This guide is written for a wallet team we have never met. Read it once and you should know which path is yours, what to implement, what to put in the registry, and that you need nothing from us.
The two paths, and how to tell which is yours
- Path A, announce. The wallet lives in the page: a browser extension that announces itself over
canton:announceProvider. PartyLayer discovers it and drives it directly. No adapter object at all. - Path B, discovery adapter. The wallet is a remote service or opens a popup, so it is not in the page to announce. The wallet ships its own adapter, an object satisfying the official
ProviderAdaptershape, and the dApp hands that object to PartyLayer, which wraps it. This is the path for gateways, hosted wallets, and out-of-process desktop and mobile apps.
Decision guide: if the wallet lives in the page, Path A; if it is a remote service or opens a popup, Path B.
Which path each wallet shape takes
- Browser extension (Path A): it runs in the page, so it announces and PartyLayer drives it with no adapter object.
- Remote or gateway service (Path B): it is out of the page, so it ships an official adapter and the dApp supplies it. Its
detect()returnstruebecause a gateway is always reachable. - Mobile wallet (deep link) (Path A or B): a deep link is how the wallet is opened, not a third path. If the wallet presents an in-page surface, Path A; if it is reached as a remote or gateway, Path B.
- Desktop app (usually Path B): typically reached as a local gateway or service, so Path B. Path A only if it injects into the page.
A deep link is an installation and launch detail, not an integration path. A mobile wallet still integrates through Path A or Path B like any other wallet; the deep link is simply how its adapter brings the wallet to the foreground.
A checklist a wallet team can work through
- Decide your shape from the list above. That fixes your path.
- Implement the CIP-0103 request methods you support. The baseline is
connect,signMessage, andprepareExecute; the rest are additive. The exact request and result shape of every method is on the CIP-0103 provider reference. - Path A: announce over
canton:announceProvider. Path B: ship a small package that exports an object satisfying the officialProviderAdaptershape. - Path B only: handle the remote concerns in their own section, namely popup policy, session survival, event streams, and origin validation.
- Add a registry entry, then open it as described in Submitting your registry entry. It is optional on Path A and expected on Path B, and it is metadata only, no code.
- Verify your adapter with the conformance runner, as described in Verifying your wallet. Connecting through a live dApp built on
@partylayer/sdkis the manual second path. - There is no step seven. Nothing is required from PartyLayer.
Path A: announce (in-page wallets)
Discovery follows the same pattern as EIP-6963 in Ethereum, adapted to Canton.
- The dApp dispatches a
canton:requestProviderevent onwindow. - Each installed wallet replies with a
canton:announceProviderevent carrying its metadata. - PartyLayer collects the replies, deduplicates by stable id, and registers one adapter per wallet under the provider id
browser:ext:<id>.
The announce payload is:
target is the channel the bridge talks to. When omitted it defaults to id, so an announce with no explicit target still routes to the announcing wallet's own channel, never a shared or last-one-wins slot. Because every call is scoped to that channel, a pick in the wallet list can only ever reach the wallet that announced it. The implementation is GenericAnnounceAdapter in packages/sdk/src/announce-adapter.ts.
A wallet that PartyLayer already ships a first-party adapter for (for example Console) is mapped to that adapter by id. Every other announcing CIP-0103 wallet is driven by the generic announce adapter, with no code on our side.
What a Path A wallet implements
Announce over canton:announceProvider, and implement these CIP-0103 request methods:
connect: establish the session and return the connected party.signMessage: sign an arbitrary message.prepareExecute: prepare and submit a transaction (this is what a transfer maps to).
With just these, the wallet exposes three capabilities through PartyLayer: connect, signMessage, and submitTransaction. That is a complete connect-and-transact surface, adapterless.
Each of the following is feature-detected. Implement it and the matching capability turns on; leave it out and the baseline is unaffected.
ledgerApi: proxy Canton Ledger API reads and writes through the wallet. Adds theledgerApicapability.statusplusgetPrimaryAccount: used for silent session restore on reload. Adds therestorecapability.txChangedevent: lets the dApp observe transaction status transitions. Adds theeventscapability.
Capability mapping reference
How each PartyLayer capability maps to the CIP-0103 method or methods it calls:
connectcallsconnect(plusgetPrimaryAccountandstatus). Baseline.signMessagecallssignMessage. Baseline.submitTransactioncallsprepareExecute. Baseline.ledgerApicallsledgerApi. Optional.restorecallsstatusandgetPrimaryAccount. Optional.eventsusestxChanged. Optional.
Optional registry entry
A Path A wallet works with no registry presence at all. A small entry is additive: it adds the wallet's name and icon to the picker and can opt the wallet into optional capabilities declaratively, still with no code.
adapter.transport: "announce"routes the entry through the generic announce path.cip0103.native: trueis the canonical marker that the wallet speaks CIP-0103.capabilitiesand anyadapter.configflags enable the optional surface above.
Path B: discovery adapter (remote and popup wallets)
This is the path for a wallet that is not in the page to announce: a gateway, a hosted wallet, a popup, a desktop or mobile app reached out of process. It carries equal weight with Path A and is the right path for a large class of wallets.
What the wallet ships
The wallet ships a small package that exports an object satisfying the official ProviderAdapter shape from @canton-network/core-wallet-discovery (current release 1.8.0). There is no PartyLayer-specific package. Any standards-compliant Canton adapter inherits this path, because PartyLayer matches the shape structurally rather than importing any @canton-network package. The generic host is GenericDiscoveryAdapter in packages/sdk/src/discovery-adapter.ts; it delegates every call to the provider your adapter returns.
The ProviderAdapter members
Taken from the official interface, with what each member is for:
providerId: stable id for the wallet. Aligns with the registry entry'sid.name,icon: display in the wallet picker.type: one ofbrowser,desktop,mobile,remote.getInfo(): WalletInfo: wallet metadata for the picker, including capabilities and the popup-policy flag described below.detect(): Promise<boolean>: whether the wallet is currently available. A gateway always returnstrue; an extension probes for itself.provider(): Provider<DappRpcTypes>: returns the provider that carries the RPC. A remote adapter may return a provider that bridges the remote API to the dApp API surface. The caller invokesprovider.request({ method: 'connect' })and, later,disconnect.teardown(): void: clean up adapter-specific resources, for example closing popup windows. Called after disconnect; it does not itself call disconnect on the provider.restore?(): Promise<Provider<DappRpcTypes> | null>: optional. Attempt to reinstate a previous session, returning a ready-to-use provider ornull. See session survival for how PartyLayer's bridge treats this.
The provider shape, which is the crux
Provider has exactly four members:
request(args): the one you write. It dispatches an RPC call to the wallet.on(event, listener),emit(event, ...args),removeListener(event, listener): event handling.
The official provider package ships an AbstractProvider base class that implements the three event methods, so an implementer writes only request. A minimal remote adapter is therefore short:
The four imports resolve against the published @canton-network packages (core-splice-provider, core-wallet-discovery, core-wallet-dapp-rpc-client, and core-types); DappRpcTypes is the RpcTypes request-and-result map re-exported under that name. Copy the block into a .ts file and it typechecks as is.
Everything inside request, and how the adapter reaches its gateway, opens and validates its popup, and persists a session, is the wallet's own business. PartyLayer only ever calls request(args).
How the dApp wires it
The dApp passes your adapter instance in the SDK config. The SDK detects the official shape and wraps it through the generic discovery bridge automatically:
There are two supply forms. An instance with a host baked in at construction is used as is. A factory form, create(host), lets the SDK build the adapter with the host resolved from the registry entry's adapter.networkHosts for the active network, which is how a single registry entry serves devnet, testnet, and mainnet.
The registry entry
For a discovery-adapter wallet the registry entry is expected, because it tells the dApp which package to load and which host to use per network. Walley is the live example in the stable registry (shown here as an example, not as the subject of this guide):
adapter.typenames the wallet's own published package, not a PartyLayer package.adapter.transport: "discovery-adapter"routes the entry through the generic host.adapter.networkHostssupplies the per-network host for the factory form.
Walley's registry description states, in as many words, that there is no PartyLayer-specific adapter package: it is bridged through its own @k2flabs/walley-dapp-sdk adapter. That package (published at 1.1.0) depends on the official discovery and provider packages, @canton-network/core-wallet-discovery and @canton-network/core-splice-provider, and nothing from PartyLayer.
Remote and gateway wallets: the recurring questions
Path B wallets share a set of concerns that in-page wallets do not. They are answered factually here so a wallet team does not have to ask.
Session survival
A page reload tears down the provider. The official ProviderAdapter.restore member exists for this: a wallet reinstates a previous session, for example from localStorage, and returns a ready-to-use provider or null.
PartyLayer's generic discovery bridge calls your restore on reload. The SDK revives its persisted session record, validates it against the configured network, then asks the official adapter to restore; when restore returns a live provider the bridge adopts it, so the first request after the reload uses the restored session and succeeds. So put your restoration logic in restore, as the official interface intends: read your session from your own storage and return the live provider.
One constraint: restore runs on the reload path, outside any user gesture, so keep it gesture-free, a storage read rather than a popup. If a wallet genuinely needs a fresh interaction to reconnect, it falls back to a fresh connect.
A wallet whose official adapter has no restore revives as-is: the app shows it as connected, but the first real request throws until the user reconnects. Implementing restore is what closes that gap.
Popup policy
A wallet declares reuseGlobalWalletPopup on its WalletInfo. When set, the wallet picker keeps its global popup open after the user picks, so the wallet can reuse it for asynchronous navigations. The documented case for this is an HTTP wallet gateway; it is not used for synchronous dApp-API wallets even when type is remote.
The practical constraint is the browser's user-gesture requirement: a popup opens only from the synchronous call stack of a user action. PartyLayer's connect path is built to reach the wallet's provider() and the popup with no awaits in front of it, so the popup survives the gesture. Your adapter must not insert an await before it opens the popup, or the browser will block it.
Event streams
Remote wallets often deliver status over a stream. If the server does not attach event ids, a client cannot resume with Last-Event-ID after a dropped connection, because there is no cursor to resume from. The correct behavior is to re-read state after a reconnect rather than assume the stream continued uninterrupted. PartyLayer's discovery bridge does not depend on a wallet emitting events at all; it never reports the events capability for a discovery-adapter wallet, and it re-probes state rather than trusting continuity.
Origin validation
A popup that returns its result to the opener by postMessage must validate both the event origin and the source window before trusting the message. Validate origin against the exact expected wallet origin, and confirm source is the popup window the adapter opened. This belongs in the wallet's adapter, because that is where the popup is opened and where the expected origin and window reference are known. Getting it wrong is not cosmetic: any window that can post to the opener, including an unrelated page or a malicious frame, could otherwise supply a forged result and the opener would accept it as the wallet's answer.
Verifying your wallet
Before you ship, check your work against the published conformance runner, @partylayer/conformance-runner, a CLI that validates an adapter against the CIP-0103 surface: it loads your adapter, runs the suite, writes a JSON report, prints a summary, and exits non-zero on any failure, so it drops straight into CI.
run takes --adapter (an npm package name or a path to your built adapter) and an optional --network (default devnet), and writes conformance-report.json. Point it at your Path B discovery adapter, or at any adapter package you build.
The manual second path, and the only one for an adapterless Path A wallet, is to connect through a live dApp built on @partylayer/sdk (or the prebuilt ConnectButton) and exercise connect, sign, and submit by hand. The runner is faster and repeatable; the manual path is the real-world confirmation.
Submitting your registry entry
You add a registry entry five times over in this guide; here is how you actually get it in. The registry is a signed JSON file the SDK fetches from https://registry.partylayer.xyz, one file per channel: registry/v1/beta/registry.json and registry/v1/stable/registry.json in this repository. Your wallet is one entry in the wallets array.
New wallets land in beta first and are promoted to stable after a soak. Beta is what a dApp opts into for early testing; stable is the default channel every dApp sees.
To get listed:
- Build your entry against the schema in the registry onboarding guide, which is the authoritative field list. The snippets in this guide show only the transport and
cip0103fields; the full entry also requiressupportedNetworksand thecapabilitiesbooleans. - Open a pull request adding it to
registry/v1/beta/registry.json. The gate (pnpm gate:registry) validates your entry against the schema on the pull request, so you get immediate feedback. You do not need our signing keys: a maintainer signs the channel after review. - A maintainer reviews the entry (schema, truthful capabilities, and the
cip0103evidence), signs the beta registry, and it publishes to the CDN athttps://registry.partylayer.xyz, where the SDK picks it up by channel. After the beta soak we promote it to stable.
If you would rather not open a pull request, open a GitHub issue with your entry and a maintainer will add it. The registry operations guide documents the signing, promotion, and CDN mechanics on our side; you do not run those steps.
CIP-0103 method coverage
The bridge speaks the standard CIP-0103 surface, and it is identical on both paths. A wallet implements CIP-0103 once; whether PartyLayer reaches it by announce or by discovery adapter does not change the method set.
- Requests:
connect,disconnect,isConnected,status,getActiveNetwork,listAccounts,getPrimaryAccount,signMessage,prepareExecute,ledgerApi. - Events:
statusChanged,accountsChanged,txChanged,connected.
A wallet does not need all of these. The baseline three, connect, signMessage, and prepareExecute, are enough to be usable; the rest are additive. The CIP-0103 provider reference gives the exact request and result shape of every method and event.
A note on the five wallets with a PartyLayer-specific adapter
The stable registry today has eight wallets: two on announce (Console, Send), one on the discovery adapter (Walley), and five with no declared transport that still ship a PartyLayer-specific adapter package (5N Loop, Cantor8, Bron, Nightly, WalletConnect).
Read plainly, that list could suggest that writing a PartyLayer-specific package is the expected route. It is not, and reading it that way has already cost an external contributor real work. Those five predate the generic paths and exist for historical reasons. New wallets should not follow that pattern: they should use Path A or Path B and ship no PartyLayer-specific code.
A wallet already shipping a PartyLayer-specific adapter can move to a generic path. In practice that means announcing over canton:announceProvider (Path A) or shipping an official ProviderAdapter and switching its registry entry to adapter.transport: "discovery-adapter" with the package under adapter.type (Path B), after which the PartyLayer-specific package is no longer needed.
Scope and limits
The generic bridge normalizes the connection handshake and the call surface: one API mapped to each wallet's CIP-0103 methods, on either path, with no per-wallet code.
What neither path does is change how a wallet marshals commands internally. If a wallet's own prepare or submit path diverges from the spec, for example decoding a TextMap choice context as a record, that is a wallet-side fix and is independent of the bridge. The bridge delivers the correct, spec-shaped payload to the wallet either way.
Neither path invents capabilities a wallet does not have. Capabilities are feature-detected and reported truthfully, so a dApp checks session.capabilitiesSnapshot before relying on an optional one. The discovery bridge in particular never reports events, because popup and remote wallets expose the event surface but do not emit.
How Ethereum settled the same shape
The same two-path shape is where Ethereum's ecosystem landed, which is worth one paragraph as context. In-page wallets are discovered through EIP-6963 and driven by a single injected connector, the direct analogue of Path A. Remote wallets go through one shared protocol rather than a package per wallet, the analogue of Path B. RainbowKit additionally ships a generic fallback entry so that a remote wallet absent from its curated list still works, which is the idea proposed below.
Choosing a path
If the wallet lives in the page, Path A: announce, implement the baseline CIP-0103 methods, and optionally add a registry entry. If the wallet is a remote service or opens a popup, Path B: ship an official ProviderAdapter, let the dApp supply it, and add a registry entry with transport: "discovery-adapter".
And once more, because it is the point of this document: a wallet that already ships an adapter for the wider Canton ecosystem is done. Nothing PartyLayer-specific is required, only a registry entry.
Proposal, not shipped: a generic fallback picker entry
Following RainbowKit's generic fallback, PartyLayer could show a single generic entry in the wallet picker for any wallet that supplies an official ProviderAdapter but is absent from our registry. A user with such a wallet could then connect without waiting for a registry entry to land.
What it would take: a picker entry that accepts an app-supplied official adapter with no matching registry id, resolves its host from the adapter rather than from networkHosts, and labels the entry generically. It is not implemented today, and this document does not claim otherwise. A discovery-adapter wallet is surfaced today through its registry entry, as above.