Skip to content
Open
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: 30 additions & 3 deletions core/provider-dapp/src/DappSyncProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,44 @@ import {

export class DappSyncProvider extends AbstractProvider<DappRpcTypes> {
private client: SpliceWalletJSONRPCDAppAPI
private unsubscribeEvents?: () => void

constructor(transport?: RpcTransport) {
super()
this.client = new SpliceWalletJSONRPCDAppAPI(
transport ?? new WindowTransport(window)
)
const resolvedTransport = transport ?? new WindowTransport(window)
this.client = new SpliceWalletJSONRPCDAppAPI(resolvedTransport)

// Fork-only: see docs/fork-only-dapp-sync-event-channel.md.
// The DappAsyncProvider (HTTP/SSE flow) wires wallet push events into
// `AbstractProvider.emit` via the EventSource listeners in its
// constructor. The Sync flow (postMessage) had no equivalent until
// `RpcTransport.onEvent` was added, so dApps using @canton-network/
// dapp-sdk against a CIP-103 browser extension never received
// `txChanged`/`accountsChanged`/`statusChanged`/`connected` events —
// including the `txChanged: executed` event needed to render a
// submitted transaction in the UI. Forward those events here so
// `DappClient.onTxChanged(...)` listeners fire as documented.
if (resolvedTransport.onEvent) {
this.unsubscribeEvents = resolvedTransport.onEvent(
(event, payload) => {
this.emit(event, payload)
}
)
}
}

public async request<M extends keyof DappRpcTypes>(
args: RequestArgs<DappRpcTypes, M>
): Promise<DappRpcTypes[M]['result']> {
return await this.client.request<M>(args)
}

/**
* Release the transport-level event subscription. Idempotent.
* Fork-only.
*/
teardown(): void {
this.unsubscribeEvents?.()
delete this.unsubscribeEvents
}
}
59 changes: 59 additions & 0 deletions core/rpc-transport/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,26 @@ export const jsonRpcResponse = (
}
}

/**
* Handler invoked when the transport receives a wallet push event.
* `event` is the CIP-103 event name (e.g. 'txChanged'); `payload` is the
* event payload as described in the OpenRPC schema for that event.
*
* Fork-only: see docs/fork-only-dapp-sync-event-channel.md.
*/
export type TransportEventHandler = (event: string, payload: unknown) => void

export interface RpcTransport {
submit: (payload: RequestPayload) => Promise<ResponsePayload>
/**
* Optional push-event subscription. Transports that surface wallet events
* out-of-band (e.g. WindowTransport's SPLICE_WALLET_EVENT frames; HTTP/SSE
* transports' EventSource stream) implement this; callers should treat the
* absence of `onEvent` as "no push channel — poll the event methods".
*
* Returns an unsubscribe function. Fork-only.
*/
onEvent?: (handler: TransportEventHandler) => () => void
}

export type WindowTransportOptions = {
Expand All @@ -49,6 +67,12 @@ export type WindowTransportOptions = {
}

export class WindowTransport implements RpcTransport {
// Lazily-installed listener that fans SPLICE_WALLET_EVENT frames out to
// every onEvent subscriber. We attach at most one window listener per
// transport instance to avoid leaking N listeners across N subscribers.
private eventListeners: Set<TransportEventHandler> = new Set()
private eventDispatcherInstalled = false

constructor(
private win: Window,
private options: WindowTransportOptions = {}
Expand Down Expand Up @@ -92,6 +116,41 @@ export class WindowTransport implements RpcTransport {
}
this.win.postMessage(message, '*')
}

/**
* Subscribe to wallet push events delivered as `SPLICE_WALLET_EVENT`
* postMessage frames. Returns an unsubscribe function.
*
* Fork-only: see docs/fork-only-dapp-sync-event-channel.md.
*/
onEvent = (handler: TransportEventHandler): (() => void) => {
this.eventListeners.add(handler)
this.installEventDispatcher()
return () => {
this.eventListeners.delete(handler)
}
}

private installEventDispatcher() {
if (this.eventDispatcherInstalled) return
this.eventDispatcherInstalled = true

// Single shared listener. Each onEvent subscription just adds to the
// Set; we never add/remove the underlying DOM listener after this.
const dispatch = (event: MessageEvent) => {
if (!isSpliceMessageEvent(event)) return
const data = event.data
if (data.type !== WalletEvent.SPLICE_WALLET_EVENT) return
if (this.options.target && data.target !== this.options.target) {
return
}
for (const handler of this.eventListeners) {
handler(data.event, data.payload)
}
}

window.addEventListener('message', dispatch)
}
}

export class HttpTransport implements RpcTransport {
Expand Down
23 changes: 23 additions & 0 deletions core/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export enum WalletEvent {
// JSON-RPC related events
SPLICE_WALLET_REQUEST = 'SPLICE_WALLET_REQUEST',
SPLICE_WALLET_RESPONSE = 'SPLICE_WALLET_RESPONSE',
// Wallet push-event channel (fork-only: see docs/fork-only-dapp-sync-event-channel.md).
// CIP-103 OpenRPC defines `accountsChanged`, `statusChanged`, `connected`, and
// `txChanged` as methods (pull). The Sync API docs describe them as `Events` but
// do not specify a postMessage envelope for pushing them. The Send webext (and
// any CIP-103-compliant extension that wants parity with the SSE-based HTTP
// `DappAsyncProvider` push path) needs a wire shape here; until upstream agrees
// on the envelope this is a fork-only addition.
SPLICE_WALLET_EVENT = 'SPLICE_WALLET_EVENT',
// Browser extension related events
SPLICE_WALLET_EXT_READY = 'SPLICE_WALLET_EXT_READY', // A request from the dApp to the browser extension to see if its loaded
SPLICE_WALLET_EXT_ACK = 'SPLICE_WALLET_EXT_ACK', // A response from the extension back to the dapp to acknowledge readiness
Expand Down Expand Up @@ -99,6 +107,21 @@ export const SpliceMessage = z.discriminatedUnion('type', [
type: z.literal(WalletEvent.SPLICE_WALLET_RESPONSE),
response: JsonRpcResponse,
}),
// fork-only: see docs/fork-only-dapp-sync-event-channel.md
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_EVENT),
event: z
.string()
.describe(
"Name of the dApp-API event being pushed (e.g. 'txChanged', 'accountsChanged', 'statusChanged', 'connected'). Mirrors the CIP-103 OpenRPC `methods[].name` for the event in question."
),
payload: z
.unknown()
.describe(
'Event payload. Shape is the CIP-103 result schema for the corresponding method (e.g. TxChangedEvent for `txChanged`).'
),
target: SpliceTarget.optional(),
}),
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_EXT_READY),
target: SpliceTarget.optional(),
Expand Down
Loading
Loading