diff --git a/wallet-gateway/remote/src/config/Config.ts b/wallet-gateway/remote/src/config/Config.ts index 8ea383689..fb121a38f 100644 --- a/wallet-gateway/remote/src/config/Config.ts +++ b/wallet-gateway/remote/src/config/Config.ts @@ -13,15 +13,24 @@ export const kernelInfoSchema = z.object({ z.literal('mobile'), z.literal('remote'), ]), - url: z.string().url(), - userUrl: z.string().url(), +}) + +export const serverConfigSchema = z.object({ + host: z.string(), + port: z.number(), + tls: z.boolean(), + dappPath: z.string().default('/api/v0/dapp'), + userPath: z.string().default('/api/v0/user'), + allowedOrigins: z.union([z.literal('*'), z.array(z.string())]).default('*'), }) export const configSchema = z.object({ kernel: kernelInfoSchema, + server: serverConfigSchema, store: storeConfigSchema, signingStore: signingStoreConfigSchema, }) export type KernelInfo = z.infer +export type ServerConfig = z.infer export type Config = z.infer diff --git a/wallet-gateway/remote/src/config/ConfigUtils.ts b/wallet-gateway/remote/src/config/ConfigUtils.ts index 857a02443..9e97b6ba2 100644 --- a/wallet-gateway/remote/src/config/ConfigUtils.ts +++ b/wallet-gateway/remote/src/config/ConfigUtils.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { readFileSync, existsSync } from 'fs' -import { Config, configSchema } from './Config.js' +import { Config, configSchema, ServerConfig } from './Config.js' export class ConfigUtils { static loadConfigFile(filePath: string): Config { @@ -104,3 +104,14 @@ function validateNetworkAuthMethods( } } } + +export const deriveKernelUrls = ( + serverConfig: ServerConfig +): { dappUrl: string; userUrl: string } => { + const protocol = serverConfig.tls ? 'https' : 'http' + const dappUrl = `${protocol}://${serverConfig.host}:${serverConfig.port}${serverConfig.dappPath}` + // userUrl is the base URL for the web frontend (no path) + const userUrl = `${protocol}://${serverConfig.host}:${serverConfig.port}` + + return { dappUrl, userUrl } +} diff --git a/wallet-gateway/remote/src/dapp-api/controller.ts b/wallet-gateway/remote/src/dapp-api/controller.ts index d1cb99aad..488d081d6 100644 --- a/wallet-gateway/remote/src/dapp-api/controller.ts +++ b/wallet-gateway/remote/src/dapp-api/controller.ts @@ -25,6 +25,8 @@ import { networkStatus } from '../utils.js' export const dappController = ( kernelInfo: KernelInfoConfig, + dappUrl: string, + userUrl: string, store: Store, notificationService: NotificationService, _logger: Logger, @@ -41,7 +43,7 @@ export const dappController = ( isConnected: false, isNetworkConnected: false, networkReason: 'Unauthenticated', - userUrl: 'http://localhost:3030/login/', // TODO: pull user URL from config + userUrl: `${userUrl}/login/`, }, } } @@ -61,7 +63,7 @@ export const dappController = ( isConnected: true, isNetworkConnected: status.isConnected, networkReason: status.reason ? status.reason : 'OK', - userUrl: 'http://localhost:3030/login/', // TODO: pull user URL from config + userUrl: `${userUrl}/login/`, }, } }, @@ -76,7 +78,7 @@ export const dappController = ( isConnected: false, isNetworkConnected: false, networkReason: 'Unauthenticated', - userUrl: 'http://localhost:3030/login/', // TODO: pull user URL from config + userUrl: `${userUrl}/login/`, } as StatusEvent) } @@ -164,8 +166,7 @@ export const dappController = ( }) return { - // TODO: pull user base URL / port from config - userUrl: `http://localhost:3030/approve/index.html?commandId=${commandId}`, + userUrl: `${userUrl}/approve/index.html?commandId=${commandId}`, } }, prepareReturn: async (params: PrepareReturnParams) => { diff --git a/wallet-gateway/remote/src/dapp-api/server.test.ts b/wallet-gateway/remote/src/dapp-api/server.test.ts index dde196bf2..bd8ecf24a 100644 --- a/wallet-gateway/remote/src/dapp-api/server.test.ts +++ b/wallet-gateway/remote/src/dapp-api/server.test.ts @@ -9,7 +9,7 @@ import express from 'express' import { dapp } from './server.js' import { StoreInternal } from '@canton-network/core-wallet-store-inmemory' import { AuthService } from '@canton-network/core-wallet-auth' -import { ConfigUtils } from '../config/ConfigUtils.js' +import { ConfigUtils, deriveKernelUrls } from '../config/ConfigUtils.js' import { Notifier } from '../notification/NotificationService.js' import { pino } from 'pino' import { sink } from 'pino-test' @@ -41,6 +41,7 @@ test('call connect rpc', async () => { app.use(cors()) app.use(express.json()) const server = createServer(app) + const { dappUrl, userUrl } = deriveKernelUrls(config.server) const response = await request( dapp( '/api/v0/dapp', @@ -48,6 +49,9 @@ test('call connect rpc', async () => { pino(sink()), server, config.kernel, + dappUrl, + userUrl, + config.server, notificationService, authService, store @@ -67,8 +71,6 @@ test('call connect rpc', async () => { kernel: { id: 'remote-da', clientType: 'remote', - url: 'http://localhost:3030/api/v0/dapp', - userUrl: 'http://localhost:3030', }, isConnected: false, isNetworkConnected: false, diff --git a/wallet-gateway/remote/src/dapp-api/server.ts b/wallet-gateway/remote/src/dapp-api/server.ts index f4fe8a104..740d8b4f0 100644 --- a/wallet-gateway/remote/src/dapp-api/server.ts +++ b/wallet-gateway/remote/src/dapp-api/server.ts @@ -15,7 +15,7 @@ import { NotificationService, Notifier, } from '../notification/NotificationService.js' -import { KernelInfo } from '../config/Config.js' +import { KernelInfo, ServerConfig } from '../config/Config.js' export const dapp = ( route: string, @@ -23,15 +23,24 @@ export const dapp = ( logger: Logger, server: Server, kernelInfo: KernelInfo, + dappUrl: string, + userUrl: string, + serverConfig: ServerConfig, notificationService: NotificationService, authService: AuthService, store: Store & AuthAware ) => { - app.use(cors()) // TODO: read allowedOrigins from config + app.use( + cors({ + origin: serverConfig.allowedOrigins, + }) + ) app.use(route, (req, res, next) => jsonRpcHandler({ controller: dappController( kernelInfo, + dappUrl, + userUrl, store.withAuthContext(req.authContext), notificationService, logger, @@ -43,7 +52,7 @@ export const dapp = ( const io = new SocketIoServer(server, { cors: { - origin: '*', // TODO: read allowedOrigins from config + origin: serverConfig.allowedOrigins, methods: ['GET', 'POST'], }, }) diff --git a/wallet-gateway/remote/src/example-config.ts b/wallet-gateway/remote/src/example-config.ts index 9d0c3168f..0815576e9 100644 --- a/wallet-gateway/remote/src/example-config.ts +++ b/wallet-gateway/remote/src/example-config.ts @@ -7,8 +7,14 @@ export default { kernel: { id: 'remote-da', clientType: 'remote', - url: 'http://localhost:3030/api/v0/dapp', - userUrl: 'http://localhost:3030', + }, + server: { + host: 'localhost', + port: 3030, + tls: false, + dappPath: '/api/v0/dapp', + userPath: '/api/v0/user', + allowedOrigins: ['http://localhost:8080'], }, signingStore: { connection: { diff --git a/wallet-gateway/remote/src/init.ts b/wallet-gateway/remote/src/init.ts index b1b8403aa..c91bfb2d4 100644 --- a/wallet-gateway/remote/src/init.ts +++ b/wallet-gateway/remote/src/init.ts @@ -30,6 +30,7 @@ import { CliOptions } from './index.js' import { jwtAuth } from './middleware/jwtAuth.js' import { rpcRateLimit } from './middleware/rateLimit.js' import { Config } from './config/Config.js' +import { deriveKernelUrls } from './config/ConfigUtils.js' import { existsSync, readFileSync } from 'fs' import path from 'path' @@ -133,12 +134,17 @@ async function initializeSigningDatabase( } export async function initialize(opts: CliOptions, logger: Logger) { - const port = opts.port ? Number(opts.port) : 3030 + const config = ConfigUtils.loadConfigFile(opts.config) + + // Use CLI port override or config port + const port = opts.port ? Number(opts.port) : config.server.port + const host = config.server.host + const protocol = config.server.tls ? 'https' : 'http' const app = express() - const server = app.listen(port, () => { + const server = app.listen(port, host, () => { logger.info( - `Remote Wallet Gateway starting on http://localhost:${port}` + `Remote Wallet Gateway starting on ${protocol}://${host}:${port}` ) }) @@ -153,8 +159,6 @@ export async function initialize(opts: CliOptions, logger: Logger) { const notificationService = new NotificationService(logger) - const config = ConfigUtils.loadConfigFile(opts.config) - const store = await initializeDatabase(config, logger) const signingStore = await initializeSigningDatabase(config, logger) const authService = jwtAuthService(store, logger) @@ -192,13 +196,25 @@ export async function initialize(opts: CliOptions, logger: Logger) { app.use('/api/*splat', rpcRateLimit) app.use('/api/*splat', jwtAuth(authService, logger)) + // Override config port with CLI parameter port if provided, then derive URLs + const serverConfigWithOverride = { + ...config.server, + port, // Use the actual port we're listening on + } + const { dappUrl, userUrl } = deriveKernelUrls(serverConfigWithOverride) + + const kernelInfo = config.kernel + // register dapp API handlers dapp( - '/api/v0/dapp', + config.server.dappPath, app, logger, server, - config.kernel, + kernelInfo, + dappUrl, + userUrl, + config.server, notificationService, authService, store @@ -206,17 +222,18 @@ export async function initialize(opts: CliOptions, logger: Logger) { // register user API handlers user( - '/api/v0/user', + config.server.userPath, app, logger, - config.kernel, + kernelInfo, + userUrl, notificationService, drivers, store ) // register web handler - web(app, server) + web(app, server, config.server.userPath) isReady = true logger.info('Wallet Gateway initialization complete') diff --git a/wallet-gateway/remote/src/user-api/controller.ts b/wallet-gateway/remote/src/user-api/controller.ts index 48dd90c5c..1f3cf0732 100644 --- a/wallet-gateway/remote/src/user-api/controller.ts +++ b/wallet-gateway/remote/src/user-api/controller.ts @@ -58,6 +58,7 @@ type AvailableSigningDrivers = Partial< export const userController = ( kernelInfo: KernelInfo, + userUrl: string, store: Store, notificationService: NotificationService, authContext: AuthContext | undefined, @@ -603,7 +604,7 @@ export const userController = ( isConnected: false, isNetworkConnected: false, networkReason: 'Unauthenticated', - userUrl: 'http://localhost:3030/login/', // TODO: pull user URL from config + userUrl: `${userUrl}/login/`, } as StatusEvent) return null }, diff --git a/wallet-gateway/remote/src/user-api/server.test.ts b/wallet-gateway/remote/src/user-api/server.test.ts index 22c5ec230..f78dcd36f 100644 --- a/wallet-gateway/remote/src/user-api/server.test.ts +++ b/wallet-gateway/remote/src/user-api/server.test.ts @@ -9,7 +9,7 @@ import request from 'supertest' import { user } from './server.js' import { StoreInternal } from '@canton-network/core-wallet-store-inmemory' import { Network } from '@canton-network/core-wallet-store' -import { ConfigUtils } from '../config/ConfigUtils.js' +import { ConfigUtils, deriveKernelUrls } from '../config/ConfigUtils.js' import { Notifier } from '../notification/NotificationService.js' import { pino } from 'pino' import { sink } from 'pino-test' @@ -33,12 +33,14 @@ test('call listNetworks rpc', async () => { app.use(cors()) app.use(express.json()) + const { userUrl } = deriveKernelUrls(config.server) const response = await request( user( '/api/v0/user', app, pino(sink()), config.kernel, + userUrl, notificationService, drivers, store diff --git a/wallet-gateway/remote/src/user-api/server.ts b/wallet-gateway/remote/src/user-api/server.ts index 1abf17cf7..0bb1b9b6d 100644 --- a/wallet-gateway/remote/src/user-api/server.ts +++ b/wallet-gateway/remote/src/user-api/server.ts @@ -20,6 +20,7 @@ export const user = ( app: express.Express, logger: Logger, kernelInfo: KernelInfo, + userUrl: string, notificationService: NotificationService, drivers: Partial>, store: Store & AuthAware @@ -28,6 +29,7 @@ export const user = ( jsonRpcHandler({ controller: userController( kernelInfo, + userUrl, store.withAuthContext(req.authContext), notificationService, req.authContext, diff --git a/wallet-gateway/remote/src/web/frontend/approve/index.ts b/wallet-gateway/remote/src/web/frontend/approve/index.ts index 92038e1af..c64479fad 100644 --- a/wallet-gateway/remote/src/web/frontend/approve/index.ts +++ b/wallet-gateway/remote/src/web/frontend/approve/index.ts @@ -168,7 +168,9 @@ export class ApproveUi extends LitElement { } private async updateState() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) userClient .request('getTransaction', { commandId: this.commandId }) .then((result) => { @@ -201,7 +203,9 @@ export class ApproveUi extends LitElement { preparedTransaction: this.tx, } - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const { signature, signedBy } = await userClient.request( 'sign', signRequest diff --git a/wallet-gateway/remote/src/web/frontend/index.ts b/wallet-gateway/remote/src/web/frontend/index.ts index 51b0dd7b9..8608ac463 100644 --- a/wallet-gateway/remote/src/web/frontend/index.ts +++ b/wallet-gateway/remote/src/web/frontend/index.ts @@ -21,7 +21,9 @@ export class UserApp extends LitElement { private async handleLogout() { localStorage.clear() - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('removeSession') if ( @@ -106,7 +108,7 @@ export const authenticate = async ( accessToken: string, networkId: string ): Promise => { - const authenticatedUserClient = createUserClient(accessToken) + const authenticatedUserClient = await createUserClient(accessToken) await authenticatedUserClient.request('addSession', { networkId, }) diff --git a/wallet-gateway/remote/src/web/frontend/login/login.ts b/wallet-gateway/remote/src/web/frontend/login/login.ts index 0da956178..e5b4025d1 100644 --- a/wallet-gateway/remote/src/web/frontend/login/login.ts +++ b/wallet-gateway/remote/src/web/frontend/login/login.ts @@ -177,13 +177,17 @@ export class LoginUI extends LitElement { } private async loadNetworks() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const response = await userClient.request('listNetworks') return response.networks } private async loadIdps() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const response = await userClient.request('listIdps') return response.idps } @@ -289,7 +293,7 @@ export class LoginUI extends LitElement { stateManager.accessToken.set(access_token) - const authenticatedUserClient = createUserClient(access_token) + const authenticatedUserClient = await createUserClient(access_token) await authenticatedUserClient.request('addSession', { networkId: stateManager.networkId.get() || '', diff --git a/wallet-gateway/remote/src/web/frontend/rpc-client.ts b/wallet-gateway/remote/src/web/frontend/rpc-client.ts index f74e85da1..a0072bc53 100644 --- a/wallet-gateway/remote/src/web/frontend/rpc-client.ts +++ b/wallet-gateway/remote/src/web/frontend/rpc-client.ts @@ -4,11 +4,28 @@ import { HttpTransport } from '@canton-network/core-types' import UserApiClient from '@canton-network/core-wallet-user-rpc-client' -export const createUserClient = (token?: string) => { - return new UserApiClient( - new HttpTransport( - new URL(`${window.location.origin}/api/v0/user`), - token - ) - ) +let userPathPromise: Promise | null = null + +const getUserPath = async (): Promise => { + if (!userPathPromise) { + userPathPromise = fetch('/.well-known/wallet-gateway-config') + .then((response) => response.json()) + .then((config) => config.userPath || '/api/v0/user') + .catch((error) => { + console.warn( + 'Failed to fetch userPath from config, using default', + error + ) + return '/api/v0/user' // Fallback to default + }) + } + return userPathPromise +} + +export const createUserClient = async ( + token?: string +): Promise => { + const userPath = await getUserPath() + const url = new URL(`${window.location.origin}${userPath}`) + return new UserApiClient(new HttpTransport(url, token)) } diff --git a/wallet-gateway/remote/src/web/frontend/settings/index.ts b/wallet-gateway/remote/src/web/frontend/settings/index.ts index 2059a8bdd..b87c88428 100644 --- a/wallet-gateway/remote/src/web/frontend/settings/index.ts +++ b/wallet-gateway/remote/src/web/frontend/settings/index.ts @@ -19,6 +19,7 @@ import { Idp, Auth as ApiAuth, } from '@canton-network/core-wallet-user-rpc-client' +import UserApiClient from '@canton-network/core-wallet-user-rpc-client' import '../index' import '/index.css' @@ -43,28 +44,36 @@ export class UserUiSettings extends LitElement { @state() accessor networks: Network[] = [] @state() accessor sessions: Session[] = [] @state() accessor idps: Idp[] = [] + @state() accessor client: UserApiClient | null = null - connectedCallback(): void { + async connectedCallback(): Promise { super.connectedCallback() + this.client = await createUserClient(stateManager.accessToken.get()) this.listNetworks() this.listSessions() this.listIdps() } private async listNetworks() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const response = await userClient.request('listNetworks') this.networks = response.networks } private async listSessions() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const response = await userClient.request('listSessions') this.sessions = response.sessions } private async listIdps() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) const response = await userClient.request('listIdps') this.idps = response.idps } @@ -108,7 +117,9 @@ export class UserUiSettings extends LitElement { } try { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('addNetwork', { network }) await this.listNetworks() } catch (e) { @@ -122,7 +133,9 @@ export class UserUiSettings extends LitElement { const params: RemoveNetworkParams = { networkName: e.network.id, } - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('removeNetwork', params) await this.listNetworks() } catch (e) { @@ -133,7 +146,9 @@ export class UserUiSettings extends LitElement { private handleIdpSubmit = async (ev: IdpAddEvent) => { console.log(ev) try { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('addIdp', { idp: ev.idp }) await this.listIdps() } catch (e) { @@ -144,7 +159,9 @@ export class UserUiSettings extends LitElement { private handleIdpDelete = async (ev: IdpCardDeleteEvent) => { console.log(ev) try { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('removeIdp', { identityProviderId: ev.idp.id, }) @@ -155,7 +172,10 @@ export class UserUiSettings extends LitElement { } protected render() { - const client = createUserClient(stateManager.accessToken.get()) + if (!this.client) { + return html`` + } + const client = this.client return html` diff --git a/wallet-gateway/remote/src/web/frontend/transactions/index.ts b/wallet-gateway/remote/src/web/frontend/transactions/index.ts index c02683f69..026a0372d 100644 --- a/wallet-gateway/remote/src/web/frontend/transactions/index.ts +++ b/wallet-gateway/remote/src/web/frontend/transactions/index.ts @@ -185,7 +185,9 @@ export class UserUiTransactions extends LitElement { } private async updateTransactions() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) userClient.request('listTransactions').then((result) => { this.transactions = result.transactions || [] for (const tx of this.transactions) { diff --git a/wallet-gateway/remote/src/web/frontend/wallets/index.ts b/wallet-gateway/remote/src/web/frontend/wallets/index.ts index 3e024f205..d943d3abb 100644 --- a/wallet-gateway/remote/src/web/frontend/wallets/index.ts +++ b/wallet-gateway/remote/src/web/frontend/wallets/index.ts @@ -339,21 +339,27 @@ export class UserUiWallets extends LitElement { } private async updateNetworks() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) userClient.request('listNetworks').then(({ networks }) => { this.networks = networks.map((network) => network.id) }) } private async updateWallets() { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) userClient.request('listWallets', []).then((wallets) => { this.wallets = wallets || [] }) } private async _setPrimary(wallet: Wallet) { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('setPrimaryWallet', { partyId: wallet.partyId, }) @@ -381,7 +387,9 @@ export class UserUiWallets extends LitElement { signingProviderId, } - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('createWallet', body) } catch (e) { handleErrorToast(e) @@ -398,7 +406,9 @@ export class UserUiWallets extends LitElement { private async _allocateParty(wallet: Wallet) { this.loading = true try { - const userClient = createUserClient(stateManager.accessToken.get()) + const userClient = await createUserClient( + stateManager.accessToken.get() + ) await userClient.request('createWallet', { primary: wallet.primary, partyHint: wallet.hint, diff --git a/wallet-gateway/remote/src/web/server.ts b/wallet-gateway/remote/src/web/server.ts index 7fb7f6bbb..86adaa3b6 100644 --- a/wallet-gateway/remote/src/web/server.ts +++ b/wallet-gateway/remote/src/web/server.ts @@ -7,7 +7,11 @@ import path, { dirname } from 'path' import { fileURLToPath } from 'url' import ViteExpress from 'vite-express' -export const web = (app: express.Express, server: Server) => { +export const web = (app: express.Express, server: Server, userPath: string) => { + // Expose userPath via well-known configuration endpoint + app.get('/.well-known/wallet-gateway-config', (_req, res) => { + res.json({ userPath }) + }) if (process.env.NODE_ENV === 'development') { // Enable live reloading and Vite dev server for frontend in development ViteExpress.bind(app, server) diff --git a/wallet-gateway/test/config.json b/wallet-gateway/test/config.json index 4dcfab25c..711d873ec 100644 --- a/wallet-gateway/test/config.json +++ b/wallet-gateway/test/config.json @@ -1,9 +1,15 @@ { "kernel": { "id": "remote-da", - "clientType": "remote", - "url": "http://localhost:3030/api/v0/dapp", - "userUrl": "http://localhost:3030" + "clientType": "remote" + }, + "server": { + "host": "localhost", + "port": 3030, + "tls": false, + "dappPath": "/api/v0/dapp", + "userPath": "/api/v0/user", + "allowedOrigins": ["http://localhost:8080"] }, "store": { "connection": {