Skip to content
Draft
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
7 changes: 6 additions & 1 deletion api-specs/openrpc-user-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,13 +797,18 @@
"type": "object",
"additionalProperties": false,
"properties": {
"origin": {
"title": "origin",
"type": "string",
"description": "origin of the dApp establishing a session"
},
"networkId": {
"title": "networkId",
"type": "string",
"description": "Network Id"
}
},
"required": ["networkId"]
"required": ["networkId", "origin"]
}
}
],
Expand Down
12 changes: 11 additions & 1 deletion core/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export enum WalletEvent {
// Auth events
SPLICE_WALLET_IDP_AUTH_SUCCESS = 'SPLICE_WALLET_IDP_AUTH_SUCCESS',
SPLICE_WALLET_LOGOUT = 'SPLICE_WALLET_LOGOUT',
// Other
SPLICE_WALLET_BROADCAST_ORIGIN = 'SPLICE_WALLET_BROADCAST_ORIGIN', // Used by the dApp (parent) to broadcast its origin down to the wallet (child) for cross-origin communication
SPLICE_WALLET_BROADCAST_ORIGIN_ACK = 'SPLICE_WALLET_BROADCAST_ORIGIN_ACK', // Used by the wallet (child) to acknowledge receipt of the dApp's origin for cross-origin communication
}

export type SpliceMessageEvent = MessageEvent<SpliceMessage>
Expand Down Expand Up @@ -122,14 +125,21 @@ export const SpliceMessage = z.discriminatedUnion('type', [
}),
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_EXT_OPEN),
url: z.string().url(),
url: z.url(),
target: SpliceTarget.optional(),
}),
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS),
token: z.string(),
sessionId: z.string(),
}),
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_BROADCAST_ORIGIN),
origin: z.url(),
}),
z.object({
type: z.literal(WalletEvent.SPLICE_WALLET_BROADCAST_ORIGIN_ACK),
}),
])
export type SpliceMessage = z.infer<typeof SpliceMessage>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Kysely } from 'kysely'
import { DB } from '../schema.js'

export async function up(db: Kysely<DB>): Promise<void> {
console.log('Adding origin column to networks table')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Session currently doesn't have a primary key. We should consider making the id the pk, as well as a unique constraint around origin, user_id. Then there's also the question of the access_token where i'm not sure if it's a good idea to make this unique, although it might very well be valid.

await db.schema.alterTable('sessions').addColumn('origin', 'text').execute()
}

export async function down(db: Kysely<DB>): Promise<void> {
await db.schema.alterTable('sessions').dropColumn('origin').execute()
}
2 changes: 2 additions & 0 deletions core/wallet-store-sql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ interface MessageRawTable {

interface SessionTable {
id: string
origin: string
network: string
accessToken: string
userId: UserId
Expand Down Expand Up @@ -241,6 +242,7 @@ export const toSession = (table: SessionTable): Session => {
return {
id: table.id,
network: table.network,
origin: table.origin,
accessToken: table.accessToken,
userId: table.userId,
}
Expand Down
14 changes: 13 additions & 1 deletion core/wallet-store-sql/src/store-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,16 +383,28 @@

async setSession(session: Session): Promise<void> {
const userId = this.assertConnected()

if (!session.origin) {
throw new Error('Session origin is required')

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > removeWallet should cascade-delete userPartyRights

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:632:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should allow duplicate commandIds and update by transaction id

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:580:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > addWallet should upsert when same party exists on different network

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:547:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should have separate primary wallets per network

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:468:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should allow same party ID across different networks

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:382:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should set and get session

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:292:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should persist wallet rights and update rights-only changes

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:265:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should set and get primary wallet

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:238:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should filter wallets

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:179:25

Check failure on line 388 in core/wallet-store-sql/src/store-sql.ts

View workflow job for this annotation

GitHub Actions / test-unit

[node] src/store-sql.test.ts > StoreSql > should add and retrieve wallets

Error: Session origin is required ❯ StoreSql.setSession src/store-sql.ts:388:19 ❯ src/store-sql.test.ts:114:25
}

await this.db.transaction().execute(async (trx) => {
const deleted = await trx
.deleteFrom('sessions')
.where('userId', '=', userId)
.where((eb) =>
eb.and([
eb('userId', '=', userId),
eb('origin', '=', session.origin),
])
)
.execute()
this.logger.debug(deleted, 'Deleted old session')

const inserted = await trx
.insertInto('sessions')
.values({ ...session, userId })
.execute()

this.logger.debug(inserted, 'Inserted new session')
})
}
Expand Down
17 changes: 15 additions & 2 deletions core/wallet-store/src/Store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export interface Wallet {

export interface Session {
id: string
origin: string
network: string
accessToken: string
userId?: string
Expand Down Expand Up @@ -148,9 +149,21 @@ export interface Store {
): Promise<void>

// Session methods
getSession(): Promise<Session | undefined>
/**
* getSession is keyed by the accessToken, which is unique per session. It retrieves the session associated with the provided accessToken.
* @param accessToken The access token associated with the session to retrieve.
* @returns A Promise that resolves to the Session object if found, or undefined if no session exists for the given accessToken.
*/
getSession(accessToken: string): Promise<Session | undefined>

setSession(session: Session): Promise<void>
removeSession(): Promise<void>

/**
* removeSession is keyed by the accessToken, which is unique per session. It removes the session associated with the provided accessToken.
* @param accessToken The access token associated with the session to remove.
* @returns A Promise that resolves when the session has been removed.
*/
removeSession(accessToken: string): Promise<void>

// IDP methods
getIdp(idpId: string): Promise<Idp>
Expand Down
42 changes: 41 additions & 1 deletion core/wallet-ui-components/src/windows/popup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import {
isSpliceMessageEvent,
SpliceMessage,
WalletEvent,
} from '@canton-network/core-types'

interface PopupOptions {
title?: string
target?: string
Expand Down Expand Up @@ -57,7 +63,41 @@ class PopupInstance {
urlOrComponent instanceof URL
) {
const win = PopupInstance.getInstance()
win.location.href = urlOrComponent.toString()
const url = urlOrComponent.toString()
const childOrigin = new URL(url).origin
win.location.href = url

const message: SpliceMessage = {
type: WalletEvent.SPLICE_WALLET_BROADCAST_ORIGIN,
origin: window.location.origin,
}

// due to the asynchronicity when sending the postMessage immediately after redirecting,
// there is a chance that the child window has not yet loaded,
// and does not an event listener established yet. Therefore,
// we repeatedly poll until the child window sends back an acknowledgment message.
const handleMessage = (event: MessageEvent) => {
if (!isSpliceMessageEvent(event)) return
if (
event.data.type !==
WalletEvent.SPLICE_WALLET_BROADCAST_ORIGIN_ACK
)
return
if (childOrigin !== event.origin) return

console.log('Received acknowledgment from child window:', event)

clearInterval(originPoller)
window.removeEventListener('message', handleMessage)
}

window.addEventListener('message', handleMessage)

const originPoller = setInterval(() => {
console.log('posting message!', { message, childOrigin })
win.postMessage(message, childOrigin)
}, 500)

win.focus()
return win
} else {
Expand Down
13 changes: 7 additions & 6 deletions core/wallet-user-rpc-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,12 @@ export type MessageId = string
*/
export type Signature = string
export type SignedBy = string
/**
*
* The origin (dApp URL) that initiated this transaction request.
*
*/
export type Origin = string
/**
*
* Authentication method configured for this network
Expand Down Expand Up @@ -350,12 +356,6 @@ export type Status = string
*
*/
export type Message = string
/**
*
* The origin (dApp URL) that initiated this transaction request.
*
*/
export type Origin = string
/**
*
* The timestamp when the API key was created.
Expand Down Expand Up @@ -526,6 +526,7 @@ export interface ExecuteParams {
signedBy: SignedBy
}
export interface AddSessionParams {
origin: Origin
networkId: NetworkId
}
export interface GetTransactionParams {
Expand Down
7 changes: 6 additions & 1 deletion core/wallet-user-rpc-client/src/openrpc.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,13 +797,18 @@
"type": "object",
"additionalProperties": false,
"properties": {
"origin": {
"title": "origin",
"type": "string",
"description": "origin of the dApp establishing a session"
},
"networkId": {
"title": "networkId",
"type": "string",
"description": "Network Id"
}
},
"required": ["networkId"]
"required": ["networkId", "origin"]
}
}
],
Expand Down
1 change: 1 addition & 0 deletions wallet-gateway/remote/src/dapp-api/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const auth: AuthContext = {

const session: Session = {
id: 'session-1',
origin: 'dapp-1',
network: 'network1',
accessToken: 'session-token',
}
Expand Down
10 changes: 5 additions & 5 deletions wallet-gateway/remote/src/dapp-api/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const dappController = (

return buildController({
connect: async () => {
if (!context || !(await store.getSession())) {
if (!context || !(await store.getSession(context.accessToken))) {
return {
isConnected: false,
isNetworkConnected: false,
Expand Down Expand Up @@ -128,7 +128,7 @@ export const dappController = (
return null
} else {
const notifier = notificationService.getNotifier(context.userId)
await store.removeSession()
await store.removeSession(context.accessToken)
notifier.emit('statusChanged', {
provider: {
id: kernelInfo.id,
Expand All @@ -148,7 +148,7 @@ export const dappController = (
return null
},
isConnected: async () => {
if (!context || !(await store.getSession())) {
if (!context || !(await store.getSession(context.accessToken))) {
return {
isConnected: false,
isNetworkConnected: false,
Expand Down Expand Up @@ -403,7 +403,7 @@ export const dappController = (
url: dappUrl,
userUrl: `${userUrl}/login/`,
}
if (!context || !(await store.getSession())) {
if (!context || !(await store.getSession(context.accessToken))) {
return {
provider: provider,
connection: {
Expand All @@ -415,7 +415,7 @@ export const dappController = (
}
}

const session = await store.getSession()
const session = await store.getSession(context.accessToken)
const network = await store.getCurrentNetwork()
const ledgerClient = new LedgerClient({
baseUrl: new URL(network.ledgerApi.baseUrl),
Expand Down
2 changes: 1 addition & 1 deletion wallet-gateway/remote/src/dapp-api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const dapp = (
}

const newStore = store.withAuthContext(context)
const session = await newStore.getSession()
const session = await newStore.getSession(context.accessToken)
const sessionId = session?.id

if (!sessionId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ describe('WalletSyncService - multi-network features', () => {
const setSession = async (networkId: string) => {
await store.setSession({
id: `sess-${networkId}`,
origin: 'dapp-1',
network: networkId,
accessToken: 'token',
})
Expand Down
1 change: 1 addition & 0 deletions wallet-gateway/remote/src/middleware/apiKeyAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function apiKeyAuth(
// automatically initiate a session for the API key user
await authStore.setSession({
id: v4(),
origin: req.ip || 'unknown', // use the requestor's IP address as the origin for the session
network: matchingKey.networkId,
accessToken: 'unused',
})
Expand Down
4 changes: 3 additions & 1 deletion wallet-gateway/remote/src/middleware/sessionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export function sessionHandler(
next()
} else {
logger.debug('Checking for active session for ' + context?.userId)
const session = await store.withAuthContext(context).getSession()
const session = await store
.withAuthContext(context)
.getSession(context?.accessToken || '')
if (!session) {
logger.debug('No active session found for ' + context?.userId)
res.status(401).json({ error: 'No active session found' })
Expand Down
Loading
Loading