From 71488182f8f592f1a58102b56a213d0f6a9ca2af Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Wed, 6 Aug 2025 17:43:49 +0200 Subject: [PATCH 1/6] Move Remote dApp API to SDK Signed-off-by: Marc Juchli --- .../web/frontend/callback/login-callback.ts | 6 +- .../src/SpliceProviderWindow.ts | 52 +-- core/types/src/index.ts | 83 +++++ rpc-generator-config-dapp.json | 2 +- sdk/package.json | 4 +- sdk/src/connect/index.ts | 20 +- sdk/src/connect/listNetworks.ts | 0 sdk/src/dapp-api/controller.ts | 124 +++++++ sdk/src/dapp-api/rpc-gen/README.md | 3 + sdk/src/dapp-api/rpc-gen/index.ts | 42 +++ sdk/src/dapp-api/rpc-gen/typings.ts | 348 ++++++++++++++++++ sdk/src/dapp-api/server.ts | 120 ++++++ sdk/src/index.ts | 1 + yarn.lock | 2 + 14 files changed, 754 insertions(+), 53 deletions(-) delete mode 100644 sdk/src/connect/listNetworks.ts create mode 100644 sdk/src/dapp-api/controller.ts create mode 100644 sdk/src/dapp-api/rpc-gen/README.md create mode 100644 sdk/src/dapp-api/rpc-gen/index.ts create mode 100644 sdk/src/dapp-api/rpc-gen/typings.ts create mode 100644 sdk/src/dapp-api/server.ts diff --git a/clients/remote/src/web/frontend/callback/login-callback.ts b/clients/remote/src/web/frontend/callback/login-callback.ts index c989e68dd..255db08fa 100644 --- a/clients/remote/src/web/frontend/callback/login-callback.ts +++ b/clients/remote/src/web/frontend/callback/login-callback.ts @@ -1,4 +1,4 @@ -import { WalletEvent } from 'core-types' +import { WalletEvent, WindowTransport } from 'core-types' import { LitElement, html } from 'lit' import { customElement } from 'lit/decorators.js' import { userClient } from '../rpc-client' @@ -56,13 +56,15 @@ export class LoginCallback extends LitElement { stateManager.accessToken.set(tokenResponse.access_token) - await userClient.transport.submit({ + const session = await userClient.transport.submit({ method: 'addSession', params: { chainId: stateManager.chainId.get(), }, }) + new WindowTransport(window.opener).submitUserResponse(session) + window.location.replace('/') } } diff --git a/core/splice-provider/src/SpliceProviderWindow.ts b/core/splice-provider/src/SpliceProviderWindow.ts index 698524408..1eee6c70e 100644 --- a/core/splice-provider/src/SpliceProviderWindow.ts +++ b/core/splice-provider/src/SpliceProviderWindow.ts @@ -1,50 +1,16 @@ -import { - RequestPayload, - SpliceMessage, - SpliceMessageEvent, - WalletEvent, -} from 'core-types' +import { RequestPayload, WindowTransport } from 'core-types' import { SpliceProviderBase } from './SpliceProvider.js' export class SpliceProviderWindow extends SpliceProviderBase { - public async request({ method, params }: RequestPayload): Promise { - return await this.jsonRpcRequest(method, params) - } - - async jsonRpcRequest( - method: string, - params?: RequestPayload['params'] - ): Promise { - const message: SpliceMessage = { - type: WalletEvent.SPLICE_WALLET_REQUEST, - request: { - jsonrpc: '2.0', - id: Date.now(), - method, - params, - }, - } - - return new Promise((resolve, reject) => { - window.postMessage(message, '*') + private transport: WindowTransport - const listener = (event: SpliceMessageEvent) => { - if ( - event.source !== window || - event.data.type !== WalletEvent.SPLICE_WALLET_RESPONSE - ) - return - - window.removeEventListener('message', listener) - - if ('error' in event.data.response) { - reject(event.data.response.error) - } else { - resolve(event.data.response.result as T) - } - } + constructor() { + super() + this.transport = new WindowTransport(window) + } - window.addEventListener('message', listener) - }) + public async request({ method, params }: RequestPayload): Promise { + console.log('SpliceProviderWindow request:', method, params) + return (await this.transport.submit({ method, params })).result as T } } diff --git a/core/types/src/index.ts b/core/types/src/index.ts index 6ef44b4fb..db0014f9a 100644 --- a/core/types/src/index.ts +++ b/core/types/src/index.ts @@ -48,6 +48,28 @@ export type JsonRpcRequest = z.infer export const JsonRpcResponse = z.intersection(JsonRpcMeta, ResponsePayload) export type JsonRpcResponse = z.infer +export const jsonRpcRequest = ( + id: string | number | null, + payload: RequestPayload +): JsonRpcRequest => { + return { + jsonrpc: '2.0', + id, // id should be set based on the request context + ...payload, + } +} + +export const jsonRpcResponse = ( + id: string | number | null, + payload: ResponsePayload +): JsonRpcResponse => { + return { + jsonrpc: '2.0', + id, // id should be set based on the request context + ...payload, + } +} + /** * Window / message events */ @@ -61,6 +83,8 @@ export enum WalletEvent { SPLICE_WALLET_EXT_OPEN = 'SPLICE_WALLET_EXT_OPEN', // A request from the dApp to the browser extension to open the wallet UI // Auth events SPLICE_WALLET_IDP_AUTH_SUCCESS = 'SPLICE_WALLET_IDP_AUTH_SUCCESS', + // User Interactions + SPLICE_WALLET_USER_RESPONSE = 'SPLICE_WALLET_USER_RESPONSE', } export interface SpliceMessageEvent extends MessageEvent { @@ -86,6 +110,10 @@ export const SpliceMessage = z.discriminatedUnion('type', [ type: z.literal(WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS), token: z.string(), }), + z.object({ + type: z.literal(WalletEvent.SPLICE_WALLET_USER_RESPONSE), + response: JsonRpcResponse, + }), ]) export type SpliceMessage = z.infer @@ -123,4 +151,59 @@ export type DiscoverResult = z.infer export interface RpcTransport { submit: (payload: RequestPayload) => Promise + submitRequest: (payload: RequestPayload) => Promise + submitResponse: (payload: ResponsePayload) => void + submitUserResponse: (payload: ResponsePayload) => void +} + +export class WindowTransport implements RpcTransport { + constructor(private win: Window) { + this.win = win + } + + // Default is SPLICE_WALLET_REQUEST + submit = async (payload: RequestPayload) => this.submitRequest(payload) + + submitRequest = async (payload: RequestPayload) => { + const message: SpliceMessage = { + request: jsonRpcRequest('', payload), + type: WalletEvent.SPLICE_WALLET_REQUEST, + } + this.win.postMessage(message, '*') + return new Promise((resolve, reject) => { + const listener = (event: MessageEvent) => { + if ( + !isSpliceMessageEvent(event) || + event.data.type !== WalletEvent.SPLICE_WALLET_RESPONSE + ) { + return + } + + window.removeEventListener('message', listener) + if ('error' in event.data.response) { + reject(event.data.response.error) + } else { + resolve(event.data.response) + } + } + + window.addEventListener('message', listener) + }) + } + + submitResponse = (payload: ResponsePayload) => { + const message: SpliceMessage = { + response: jsonRpcResponse('', payload), + type: WalletEvent.SPLICE_WALLET_RESPONSE, + } + this.win.postMessage(message, '*') + } + + submitUserResponse = (payload: ResponsePayload) => { + const message: SpliceMessage = { + response: jsonRpcResponse('', payload), + type: WalletEvent.SPLICE_WALLET_USER_RESPONSE, + } + this.win.postMessage(message, '*') + } } diff --git a/rpc-generator-config-dapp.json b/rpc-generator-config-dapp.json index fc5afa1d2..943c45cc1 100644 --- a/rpc-generator-config-dapp.json +++ b/rpc-generator-config-dapp.json @@ -14,7 +14,7 @@ "name": "wallet-dapp-rpc-server", "language": "typescript", "openRPCPath": null, - "outPath": "./clients/remote/src/dapp-api/rpc-gen", + "outPath": "./sdk/src/dapp-api/rpc-gen", "customComponent": "./core/rpc-generator/dist/components/controller.js", "customType": "controller" }, diff --git a/sdk/package.json b/sdk/package.json index 8bc77f399..0738be749 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -14,10 +14,12 @@ "author": "Marc Juchli ", "license": "Apache-2.0", "dependencies": { + "@metamask/rpc-errors": "^7.0.2", "core-splice-provider": "workspace:^", "core-types": "workspace:^", "core-wallet-dapp-rpc-client": "workspace:^", - "core-wallet-ui-components": "workspace:^" + "core-wallet-ui-components": "workspace:^", + "core-wallet-user-rpc-client": "workspace:^" }, "devDependencies": { "typescript": "^5.8.3" diff --git a/sdk/src/connect/index.ts b/sdk/src/connect/index.ts index 926174220..54b920db8 100644 --- a/sdk/src/connect/index.ts +++ b/sdk/src/connect/index.ts @@ -7,15 +7,22 @@ import { import * as dappAPI from 'core-wallet-dapp-rpc-client' import { SDK } from '../enums.js' import { DiscoverResult, SpliceMessage, WalletEvent } from 'core-types' +import { DappServer } from '../dapp-api/server.js' export * from 'core-splice-provider' -const injectProvider = ({ walletType, url, sessionToken }: DiscoverResult) => { +let dappServer: DappServer | undefined = undefined + +const injectProvider = ({ walletType }: DiscoverResult) => { if (walletType === 'remote') { - return injectSpliceProvider( - ProviderType.HTTP, - new URL(url), - sessionToken - ) + // return injectSpliceProvider( + // ProviderType.HTTP, + // new URL(url), + // sessionToken + // ) + dappServer = new DappServer() + dappServer.run() + + return injectSpliceProvider(ProviderType.WINDOW) } else { return injectSpliceProvider(ProviderType.WINDOW) } @@ -77,6 +84,7 @@ export type ConnectError = { export async function connect(): Promise { return discover() .then(async (result) => { + dappServer?.stop() const provider = injectProvider(result) // Listen for connected eved from the provider diff --git a/sdk/src/connect/listNetworks.ts b/sdk/src/connect/listNetworks.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts new file mode 100644 index 000000000..95ea41072 --- /dev/null +++ b/sdk/src/dapp-api/controller.ts @@ -0,0 +1,124 @@ +import { popupHref } from 'core-wallet-ui-components' +import buildController from './rpc-gen' +import { + StatusResult, + ConnectResult, + PrepareReturnResult, + PrepareExecuteResult, + LedgerApiResult, + RequestAccountsResult, +} from './rpc-gen/typings' +import { isSpliceMessage, SuccessResponse, WalletEvent } from 'core-types' +import * as userApi from 'core-wallet-user-rpc-client' + +enum WK_URL { + LOGIN = 'http://localhost:3002/login/', +} + +const popupInteraction = async ( + url: WK_URL, + callback: (data: SuccessResponse) => T +): Promise => { + const win = await popupHref(new URL(url)) + console.log('popup window opened', win) + let response = false + return new Promise((resolve, reject) => { + const listener = (event: MessageEvent) => { + console.log('user has done stuff in the UI', event.data) + + // TODO: check that is of type some user api result? + if ( + isSpliceMessage(event.data) && + event.data.type === WalletEvent.SPLICE_WALLET_USER_RESPONSE + ) { + window.removeEventListener('message', listener) + + if ('error' in event.data.response) { + console.error( + 'Error in user response:', + event.data.response.error + ) + reject(event.data.response.error) + } else { + console.log( + 'User response received:', + event.data.response.result + ) + response = true + resolve(callback(event.data.response)) + } + } + } + window.addEventListener('message', listener) + + const interval = setInterval(() => { + if (!win || win.closed) { + console.log('Wallet discovery window closed by user') + clearInterval(interval) + window.removeEventListener('message', listener) + if (response === false) { + reject('User closed the wallet window prior to interaction') + } + } else { + console.log('Waiting for user to interact with the UI') + } + }, 1000) + }) +} + +export const dappController = () => + buildController({ + status: function (): Promise { + return Promise.resolve({ + kernel: { + id: 'kernel-id', // TODO: get from userApi + clientType: 'remote', + url: 'url', // TODO: get from userApi + }, + isConnected: false, + chainId: 'chain-id', // TODO: get from userApi + }) + }, + connect: async function (): Promise { + return popupInteraction(WK_URL.LOGIN, (data: SuccessResponse) => { + const addSessionResult = data.result as userApi.AddSessionResult + console.log( + 'User has logged in, received result:', + addSessionResult + ) + return { + kernel: { + id: 'kernel-id', // TODO: get from userApi + clientType: 'remote', + url: 'url', // TODO: get from userApi + }, + isConnected: true, + chainId: addSessionResult.network.chainId, + userUrl: 'user-url', // TODO: get from userApi + sessionToken: addSessionResult.accessToken, + } + }) + }, + darsAvailable: async () => ({ dars: ['default-dar'] }), + prepareReturn: function (): Promise { + throw new Error('Function not implemented.') + }, + prepareExecute: function (): Promise { + throw new Error('Function not implemented.') + }, + ledgerApi: function (): Promise { + throw new Error('Function not implemented.') + }, + onConnected: async () => { + throw new Error('Only for events.') + }, + onAccountsChanged: async () => { + throw new Error('Only for events.') + }, + requestAccounts: function (): Promise { + return Promise.resolve([]) + }, + onTxChanged: async () => { + throw new Error('Only for events.') + }, + }) diff --git a/sdk/src/dapp-api/rpc-gen/README.md b/sdk/src/dapp-api/rpc-gen/README.md new file mode 100644 index 000000000..288cdd18f --- /dev/null +++ b/sdk/src/dapp-api/rpc-gen/README.md @@ -0,0 +1,3 @@ +All files in this directory have been auto-generated by `core/rpc-generator`! Do not edit them directly! + +If you'd like to modify the template format, edit the templates in `core/rpc-generator`. diff --git a/sdk/src/dapp-api/rpc-gen/index.ts b/sdk/src/dapp-api/rpc-gen/index.ts new file mode 100644 index 000000000..e8a1cd004 --- /dev/null +++ b/sdk/src/dapp-api/rpc-gen/index.ts @@ -0,0 +1,42 @@ +// Code generated by rpc-generator DO NOT EDIT!! + +import { Status } from './typings.js' +import { Connect } from './typings.js' +import { DarsAvailable } from './typings.js' +import { PrepareReturn } from './typings.js' +import { PrepareExecute } from './typings.js' +import { LedgerApi } from './typings.js' +import { OnConnected } from './typings.js' +import { OnAccountsChanged } from './typings.js' +import { RequestAccounts } from './typings.js' +import { OnTxChanged } from './typings.js' + +export type Methods = { + status: Status + connect: Connect + darsAvailable: DarsAvailable + prepareReturn: PrepareReturn + prepareExecute: PrepareExecute + ledgerApi: LedgerApi + onConnected: OnConnected + onAccountsChanged: OnAccountsChanged + requestAccounts: RequestAccounts + onTxChanged: OnTxChanged +} + +function buildController(methods: Methods) { + return { + status: methods.status, + connect: methods.connect, + darsAvailable: methods.darsAvailable, + prepareReturn: methods.prepareReturn, + prepareExecute: methods.prepareExecute, + ledgerApi: methods.ledgerApi, + onConnected: methods.onConnected, + onAccountsChanged: methods.onAccountsChanged, + requestAccounts: methods.requestAccounts, + onTxChanged: methods.onTxChanged, + } +} + +export default buildController diff --git a/sdk/src/dapp-api/rpc-gen/typings.ts b/sdk/src/dapp-api/rpc-gen/typings.ts new file mode 100644 index 000000000..059cbb848 --- /dev/null +++ b/sdk/src/dapp-api/rpc-gen/typings.ts @@ -0,0 +1,348 @@ +// Code generated by rpc-generator DO NOT EDIT!! +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * + * Structure representing JS commands for transaction execution + * + */ +export interface JsCommands { + [key: string]: any +} +export type RequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' +export type Resource = string +export type Body = string +/** + * + * The unique identifier of the wallet kernel. + * + */ +export type Id = string +/** + * + * The type of client that implements the wallet kernel. + * + */ +export type ClientType = 'browser' | 'desktop' | 'mobile' | 'remote' +/** + * + * The URL of the wallet kernel. + * + */ +export type Url = string +/** + * + * Represents a wallet kernel. + * + */ +export interface KernelInfo { + id: Id + clientType: ClientType + url?: Url + [k: string]: any +} +/** + * + * Whether or not a connection to a network is esablished. + * + */ +export type IsConnected = boolean +/** + * + * The network ID the wallet corresponds to. + * + */ +export type ChainId = string +/** + * + * A URL that points to a user interface. + * + */ +export type UserUrl = string +export type Dar = string +export type Dars = Dar[] +/** + * + * The prepared transaction data. + * + */ +export type PreparedTransaction = string +/** + * + * The hash of the prepared transaction. + * + */ +export type PreparedTransactionHash = string +/** + * + * Structure representing the result of a prepareReturn call + * + */ +export interface JsPrepareSubmissionResponse { + preparedTransaction?: PreparedTransaction + preparedTransactionHash?: PreparedTransactionHash + [k: string]: any +} +export type Response = string +/** + * + * JWT authentication token (if applicable). + * + */ +export type SessionToken = string +/** + * + * Set as primary wallet for dApp usage. + * + */ +export type Primary = boolean +/** + * + * The party ID corresponding to the wallet. + * + */ +export type PartyId = string +/** + * + * The party hint and name of the wallet. + * + */ +export type Hint = string +/** + * + * The public key of the party. + * + */ +export type PublicKey = string +/** + * + * The namespace of the party. + * + */ +export type Namespace = string +/** + * + * The signing provider ID the wallet corresponds to. + * + */ +export type SigningProviderId = string +/** + * + * Structure representing a wallet + * + */ +export interface Wallet { + primary: Primary + partyId: PartyId + hint: Hint + publicKey: PublicKey + namespace: Namespace + chainId: ChainId + signingProviderId: SigningProviderId + [k: string]: any +} +/** + * + * The status of the transaction. + * + */ +export type StatusPending = 'pending' +/** + * + * The unique identifier of the command associated with the transaction. + * + */ +export type CommandId = string +/** + * + * Event emitted when a transaction is pending. + * + */ +export interface TxChangedPendingEvent { + status: StatusPending + commandId: CommandId +} +/** + * + * The status of the transaction. + * + */ +export type StatusSigned = 'signed' +/** + * + * The signature of the transaction. + * + */ +export type Signature = string +/** + * + * The identifier of the provider that signed the transaction. + * + */ +export type SignedBy = string +/** + * + * The party that signed the transaction. + * + */ +export type Party = string +/** + * + * Payload for the TxChangedSignedEvent. + * + */ +export interface TxChangedSignedPayload { + signature: Signature + signedBy: SignedBy + party: Party +} +/** + * + * Event emitted when a transaction has been signed. + * + */ +export interface TxChangedSignedEvent { + status: StatusSigned + commandId: CommandId + payload: TxChangedSignedPayload +} +/** + * + * The status of the transaction. + * + */ +export type StatusExecuted = 'executed' +/** + * + * The update ID corresponding to the transaction. + * + */ +export type UpdateId = string +export type CompletionOffset = number +/** + * + * Payload for the TxChangedExecutedEvent. + * + */ +export interface TxChangedExecutedPayload { + updateId: UpdateId + completionOffset: CompletionOffset +} +/** + * + * Event emitted when a transaction is executed against the participant. + * + */ +export interface TxChangedExecutedEvent { + status: StatusExecuted + commandId: CommandId + payload: TxChangedExecutedPayload +} +/** + * + * The status of the transaction. + * + */ +export type StatusFailed = 'failed' +/** + * + * Event emitted when a transaction has failed. + * + */ +export interface TxChangedFailedEvent { + status: StatusFailed + commandId: CommandId +} +export interface PrepareReturnParams { + commands: JsCommands + [k: string]: any +} +export interface PrepareExecuteParams { + commands: JsCommands + [k: string]: any +} +export interface LedgerApiParams { + requestMethod: RequestMethod + resource: Resource + body?: Body + [k: string]: any +} +export interface StatusResult { + kernel: KernelInfo + isConnected: IsConnected + chainId?: ChainId + [k: string]: any +} +export interface ConnectResult { + kernel: KernelInfo + isConnected: IsConnected + chainId?: ChainId + userUrl: UserUrl + [k: string]: any +} +export interface DarsAvailableResult { + dars: Dars + [k: string]: any +} +export type PrepareReturnResult = any +export interface PrepareExecuteResult { + userUrl: UserUrl + [k: string]: any +} +/** + * + * Ledger Api configuration options + * + */ +export interface LedgerApiResult { + response: Response + [k: string]: any +} +export interface OnConnectedEvent { + kernel: KernelInfo + chainId: ChainId + sessionToken?: SessionToken + [k: string]: any +} +/** + * + * Event emitted when the user's accounts change. + * + */ +export type AccountsChangedEvent = Wallet[] +/** + * + * An array of accounts that the user has authorized the dapp to access.. + * + */ +export type RequestAccountsResult = Wallet[] +/** + * + * Event emitted when a transaction changes. + * + */ +export type TxChangedEvent = + | TxChangedPendingEvent + | TxChangedSignedEvent + | TxChangedExecutedEvent + | TxChangedFailedEvent +/** + * + * Generated! Represents an alias to any of the provided schemas + * + */ + +export type Status = () => Promise +export type Connect = () => Promise +export type DarsAvailable = () => Promise +export type PrepareReturn = ( + params: PrepareReturnParams +) => Promise +export type PrepareExecute = ( + params: PrepareExecuteParams +) => Promise +export type LedgerApi = (params: LedgerApiParams) => Promise +export type OnConnected = () => Promise +export type OnAccountsChanged = () => Promise +export type RequestAccounts = () => Promise +export type OnTxChanged = () => Promise diff --git a/sdk/src/dapp-api/server.ts b/sdk/src/dapp-api/server.ts new file mode 100644 index 000000000..18dfc75c1 --- /dev/null +++ b/sdk/src/dapp-api/server.ts @@ -0,0 +1,120 @@ +import { + isSpliceMessage, + SpliceMessage, + SpliceMessageEvent, + WalletEvent, + jsonRpcResponse, + JsonRpcResponse, + WindowTransport, +} from 'core-types' +import { Methods } from './rpc-gen' +import { dappController } from './controller' +import { rpcErrors } from '@metamask/rpc-errors' + +export class DappServer { + controller: Methods + + constructor() { + this.controller = dappController() + } + + static sendResponse(response: SpliceMessage) { + console.log('Sending response:', response) + window.postMessage(response, '*') + } + + // Main RPC handler for incoming JSON-RPC requests + async handleRpcRequest(message: unknown): Promise { + return new Promise((resolve, reject) => { + if ( + isSpliceMessage(message) && + message.type === WalletEvent.SPLICE_WALLET_REQUEST + ) { + console.log('Processing JSON-RPC request:', message.request) + const { request } = message + + const id = request.id || null + const method = request.method as keyof Methods + + const methodFn = this.controller[method] + + if (!methodFn) { + resolve( + jsonRpcResponse(id, { + error: rpcErrors.methodNotFound({ + message: `Method ${method} not found`, + }), + }) + ) + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + methodFn(request.params as any) + .then((result) => resolve(jsonRpcResponse(id, { result }))) + .catch((error) => + reject( + jsonRpcResponse(id, { + error: rpcErrors.internal({ + message: + error instanceof Error + ? error.message + : String(error), + }), + }) + ) + ) + } else { + reject() + } + }) + } + + // Handle incoming RPC requests from the dapp, + // proxy them to the controller, and send the response back to the dapp + run() { + console.log('DappServer is running and listening for messages') + window.addEventListener( + 'message', + async (event: SpliceMessageEvent) => { + const { data: message, success } = SpliceMessage.safeParse( + event.data + ) + + if (!success) { + // not a valid SpliceMessage, ignore + return + } + + const transport = new WindowTransport(window) + + // Forward JSON RPC requests to the background script + if (message.type === WalletEvent.SPLICE_WALLET_REQUEST) { + console.log('Received request:', message) + this.handleRpcRequest(message) + .then(transport.submitResponse) + .catch((error: unknown) => { + const e = JsonRpcResponse.safeParse(error) + if (e.success) { + transport.submitResponse(e.data) + } else { + console.error( + 'No response generated for the request', + error + ) + transport.submitResponse({ + error: rpcErrors.internal({ + message: 'Internal error', + data: 'No response generated for the request', + }), + }) + } + }) + } + } + ) + } + + stop() { + window.removeEventListener('message', this.run) + } +} diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 1cde74b10..570f06d39 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -1,2 +1,3 @@ export * from './connect/index' +export * from './dapp-api/server' export * as dappAPI from 'core-wallet-dapp-rpc-client' diff --git a/yarn.lock b/yarn.lock index 0594112e0..838d42566 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12282,10 +12282,12 @@ __metadata: version: 0.0.0-use.local resolution: "splice-wallet-sdk@workspace:sdk" dependencies: + "@metamask/rpc-errors": "npm:^7.0.2" core-splice-provider: "workspace:^" core-types: "workspace:^" core-wallet-dapp-rpc-client: "workspace:^" core-wallet-ui-components: "workspace:^" + core-wallet-user-rpc-client: "workspace:^" typescript: "npm:^5.8.3" languageName: unknown linkType: soft From 323ba0aee7fa7b1638cd75313e1c831ce1c93c22 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Thu, 7 Aug 2025 01:32:10 +0200 Subject: [PATCH 2/6] Getting user ui from user api endpoint Signed-off-by: Marc Juchli --- api-specs/openrpc-user-api.json | 54 +++++++++++++- clients/remote/src/config/Config.ts | 3 +- clients/remote/src/user-api/controller.ts | 6 ++ clients/remote/src/user-api/rpc-gen/index.ts | 3 + .../remote/src/user-api/rpc-gen/typings.ts | 49 +++++++++++++ clients/test/config.json | 3 +- .../src/SpliceProviderWindow.ts | 4 +- core/types/src/index.ts | 10 +-- .../src/components/Discovery.ts | 2 +- core/wallet-user-rpc-client/package.json | 6 +- core/wallet-user-rpc-client/src/index.ts | 58 +++++++++++++++ core/wallet-user-rpc-client/src/openrpc.json | 54 +++++++++++++- sdk/src/connect/index.ts | 71 +++++-------------- sdk/src/dapp-api/controller.ts | 65 +++++++++-------- sdk/src/dapp-api/server.ts | 52 +++++++++++--- 15 files changed, 330 insertions(+), 110 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index 876c2db5d..a754381bb 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -6,6 +6,24 @@ "description": "An OpenRPC specification for the user to interact with the wallet kernel." }, "methods": [ + { + "name": "info", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "InfoResult", + "type": "object", + "properties": { + "kernel": { + "$ref": "#/components/schemas/KernelInfo" + } + }, + "required": ["kernel"] + } + }, + "description": "Returns the current chainId if connected." + }, { "name": "addNetwork", "params": [ @@ -382,6 +400,35 @@ "type": "null", "description": "Represents a null value, used in responses where no data is returned." }, + "KernelInfo": { + "title": "KernelInfo", + "type": "object", + "description": "Represents a wallet kernel.", + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the wallet kernel." + }, + "clientType": { + "title": "clientType", + "type": "string", + "enum": ["browser", "desktop", "mobile", "remote"], + "description": "The type of client that implements the wallet kernel." + }, + "rpcUrl": { + "title": "rpcUrl", + "type": "string", + "description": "The RPC URL of the wallet kernel." + }, + "uiUrl": { + "title": "uiUrl", + "type": "string", + "description": "The UI URL of the wallet kernel." + } + }, + "required": ["id", "clientType"] + }, "Network": { "title": "Network", "type": "object", @@ -551,6 +598,11 @@ "network": { "$ref": "#/components/schemas/Network" }, + "userId": { + "title": "userId", + "type": "string", + "description": "The user ID associated with the session." + }, "accessToken": { "title": "accessToken", "type": "string", @@ -562,7 +614,7 @@ "enum": ["connected", "disconnected"] } }, - "required": ["network", "status", "accessToken"], + "required": ["network", "status", "userId", "accessToken"], "additionalProperties": false } } diff --git a/clients/remote/src/config/Config.ts b/clients/remote/src/config/Config.ts index 25bbeada9..a6f58785d 100644 --- a/clients/remote/src/config/Config.ts +++ b/clients/remote/src/config/Config.ts @@ -9,7 +9,8 @@ export const kernelInfoSchema = z.object({ z.literal('mobile'), z.literal('remote'), ]), - url: z.string().url(), + rpcUrl: z.string().url(), + uiUrl: z.string().url(), }) export const configSchema = z.object({ diff --git a/clients/remote/src/user-api/controller.ts b/clients/remote/src/user-api/controller.ts index c7143a217..759e37d3c 100644 --- a/clients/remote/src/user-api/controller.ts +++ b/clients/remote/src/user-api/controller.ts @@ -13,6 +13,7 @@ import { AddSessionResult, ListSessionsResult, SetPrimaryWalletParams, + InfoResult, } from './rpc-gen/typings.js' import { Store, Wallet, Auth } from 'core-wallet-store' import { Logger } from 'pino' @@ -124,6 +125,9 @@ export const userController = ( ) => { const logger = _logger.child({ component: 'user-controller' }) return buildController({ + info: function (): Promise { + return Promise.resolve({ kernel: kernelInfo }) + }, addNetwork: async (network: AddNetworkParams) => { const ledgerApi = { baseUrl: network.ledgerApiUrl ?? '', @@ -312,6 +316,7 @@ export const userController = ( }) return Promise.resolve({ + userId, accessToken, status: 'connected', network: { @@ -337,6 +342,7 @@ export const userController = ( return { sessions: [ { + userId: authContext!.userId, accessToken: authContext!.accessToken, status: 'connected', network: { diff --git a/clients/remote/src/user-api/rpc-gen/index.ts b/clients/remote/src/user-api/rpc-gen/index.ts index 88dd1d8f2..af259e52f 100644 --- a/clients/remote/src/user-api/rpc-gen/index.ts +++ b/clients/remote/src/user-api/rpc-gen/index.ts @@ -1,5 +1,6 @@ // Code generated by rpc-generator DO NOT EDIT!! +import { Info } from './typings.js' import { AddNetwork } from './typings.js' import { RemoveNetwork } from './typings.js' import { CreateWallet } from './typings.js' @@ -13,6 +14,7 @@ import { AddSession } from './typings.js' import { ListSessions } from './typings.js' export type Methods = { + info: Info addNetwork: AddNetwork removeNetwork: RemoveNetwork createWallet: CreateWallet @@ -28,6 +30,7 @@ export type Methods = { function buildController(methods: Methods) { return { + info: methods.info, addNetwork: methods.addNetwork, removeNetwork: methods.removeNetwork, createWallet: methods.createWallet, diff --git a/clients/remote/src/user-api/rpc-gen/typings.ts b/clients/remote/src/user-api/rpc-gen/typings.ts index 8fdb377f2..0cfdf4fd2 100644 --- a/clients/remote/src/user-api/rpc-gen/typings.ts +++ b/clients/remote/src/user-api/rpc-gen/typings.ts @@ -124,6 +124,42 @@ export type Signature = string */ export type CommandId = string export type SignedBy = string +/** + * + * The unique identifier of the wallet kernel. + * + */ +export type Id = string +/** + * + * The type of client that implements the wallet kernel. + * + */ +export type ClientType = 'browser' | 'desktop' | 'mobile' | 'remote' +/** + * + * The RPC URL of the wallet kernel. + * + */ +export type RpcUrl = string +/** + * + * The UI URL of the wallet kernel. + * + */ +export type UiUrl = string +/** + * + * Represents a wallet kernel. + * + */ +export interface KernelInfo { + id: Id + clientType: ClientType + rpcUrl?: RpcUrl + uiUrl?: UiUrl + [k: string]: any +} /** * * The party hint and name of the wallet. @@ -160,6 +196,12 @@ export interface Wallet { export type CorrelationId = string export type TraceId = string export type Networks = Network[] +/** + * + * The user ID associated with the session. + * + */ +export type UserId = string /** * * The access token for the session. @@ -174,6 +216,7 @@ export type Status = 'connected' | 'disconnected' */ export interface Session { network: Network + userId: UserId accessToken: AccessToken status: Status } @@ -221,6 +264,10 @@ export interface AddSessionParams { chainId: ChainId [k: string]: any } +export interface InfoResult { + kernel: KernelInfo + [k: string]: any +} /** * * Represents a null value, used in responses where no data is returned. @@ -262,6 +309,7 @@ export interface ListNetworksResult { */ export interface AddSessionResult { network: Network + userId: UserId accessToken: AccessToken status: Status } @@ -275,6 +323,7 @@ export interface ListSessionsResult { * */ +export type Info = () => Promise export type AddNetwork = (params: AddNetworkParams) => Promise export type RemoveNetwork = (params: RemoveNetworkParams) => Promise export type CreateWallet = ( diff --git a/clients/test/config.json b/clients/test/config.json index febc9f3c4..5221bacc0 100644 --- a/clients/test/config.json +++ b/clients/test/config.json @@ -2,7 +2,8 @@ "kernel": { "id": "remote-da", "clientType": "remote", - "url": "http://localhost:3000/rpc" + "rpcUrl": "http://localhost:3001/rpc", + "uiUrl": "http://localhost:3002" }, "store": { "networks": [ diff --git a/core/splice-provider/src/SpliceProviderWindow.ts b/core/splice-provider/src/SpliceProviderWindow.ts index 1eee6c70e..538bbdc55 100644 --- a/core/splice-provider/src/SpliceProviderWindow.ts +++ b/core/splice-provider/src/SpliceProviderWindow.ts @@ -11,6 +11,8 @@ export class SpliceProviderWindow extends SpliceProviderBase { public async request({ method, params }: RequestPayload): Promise { console.log('SpliceProviderWindow request:', method, params) - return (await this.transport.submit({ method, params })).result as T + const response = await this.transport.submit({ method, params }) + console.log('SpliceProviderWindow response:', response) + return response.result as T } } diff --git a/core/types/src/index.ts b/core/types/src/index.ts index db0014f9a..ea649ebd7 100644 --- a/core/types/src/index.ts +++ b/core/types/src/index.ts @@ -138,12 +138,10 @@ export const DiscoverResult = z.discriminatedUnion('walletType', [ z.object({ walletType: z.literal('extension'), url: z.optional(z.never()), - sessionToken: z.optional(z.string()), }), z.object({ walletType: z.literal('remote'), url: z.string().url(), - sessionToken: z.optional(z.string()), }), ]) @@ -151,9 +149,6 @@ export type DiscoverResult = z.infer export interface RpcTransport { submit: (payload: RequestPayload) => Promise - submitRequest: (payload: RequestPayload) => Promise - submitResponse: (payload: ResponsePayload) => void - submitUserResponse: (payload: ResponsePayload) => void } export class WindowTransport implements RpcTransport { @@ -166,7 +161,7 @@ export class WindowTransport implements RpcTransport { submitRequest = async (payload: RequestPayload) => { const message: SpliceMessage = { - request: jsonRpcRequest('', payload), + request: jsonRpcRequest('', payload), // TODO: add id type: WalletEvent.SPLICE_WALLET_REQUEST, } this.win.postMessage(message, '*') @@ -179,6 +174,7 @@ export class WindowTransport implements RpcTransport { return } + console.log('Received message from wallet:', event.data) window.removeEventListener('message', listener) if ('error' in event.data.response) { reject(event.data.response.error) @@ -193,7 +189,7 @@ export class WindowTransport implements RpcTransport { submitResponse = (payload: ResponsePayload) => { const message: SpliceMessage = { - response: jsonRpcResponse('', payload), + response: jsonRpcResponse(null, payload), type: WalletEvent.SPLICE_WALLET_RESPONSE, } this.win.postMessage(message, '*') diff --git a/core/wallet-ui-components/src/components/Discovery.ts b/core/wallet-ui-components/src/components/Discovery.ts index e163c444f..3a91fa6cb 100644 --- a/core/wallet-ui-components/src/components/Discovery.ts +++ b/core/wallet-ui-components/src/components/Discovery.ts @@ -79,7 +79,7 @@ export class Discovery extends HTMLElement { } verifiedKernels(): DiscoverResult[] { - return [{ url: 'http://localhost:3000/rpc', walletType: 'remote' }] + return [{ url: 'http://localhost:3001/rpc', walletType: 'remote' }] } private renderKernelOption(kernel: DiscoverResult) { diff --git a/core/wallet-user-rpc-client/package.json b/core/wallet-user-rpc-client/package.json index 2508f7bc7..daaccba29 100644 --- a/core/wallet-user-rpc-client/package.json +++ b/core/wallet-user-rpc-client/package.json @@ -21,14 +21,14 @@ }, "devDependencies": { "@eslint/js": "9.21.0", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^29.5.12", "@types/json-schema": "7.0.3", "@types/lodash": "^4.14.149", "@types/ws": "^6.0.1", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.3", "globals": "^16.0.0", "prettier": "^3.5.2", "typedoc": "^0.27.9", diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index 31794326b..ef8d5950a 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -126,6 +126,42 @@ export type Signature = string */ export type CommandId = string export type SignedBy = string +/** + * + * The unique identifier of the wallet kernel. + * + */ +export type Id = string +/** + * + * The type of client that implements the wallet kernel. + * + */ +export type ClientType = 'browser' | 'desktop' | 'mobile' | 'remote' +/** + * + * The RPC URL of the wallet kernel. + * + */ +export type RpcUrl = string +/** + * + * The UI URL of the wallet kernel. + * + */ +export type UiUrl = string +/** + * + * Represents a wallet kernel. + * + */ +export interface KernelInfo { + id: Id + clientType: ClientType + rpcUrl?: RpcUrl + uiUrl?: UiUrl + [k: string]: any +} /** * * The party hint and name of the wallet. @@ -162,6 +198,12 @@ export interface Wallet { export type CorrelationId = string export type TraceId = string export type Networks = Network[] +/** + * + * The user ID associated with the session. + * + */ +export type UserId = string /** * * The access token for the session. @@ -176,6 +218,7 @@ export type Status = 'connected' | 'disconnected' */ export interface Session { network: Network + userId: UserId accessToken: AccessToken status: Status } @@ -223,6 +266,10 @@ export interface AddSessionParams { chainId: ChainId [k: string]: any } +export interface InfoResult { + kernel: KernelInfo + [k: string]: any +} /** * * Represents a null value, used in responses where no data is returned. @@ -264,6 +311,7 @@ export interface ListNetworksResult { */ export interface AddSessionResult { network: Network + userId: UserId accessToken: AccessToken status: Status } @@ -277,6 +325,7 @@ export interface ListSessionsResult { * */ +export type Info = () => Promise export type AddNetwork = (params: AddNetworkParams) => Promise export type RemoveNetwork = (params: RemoveNetworkParams) => Promise export type CreateWallet = ( @@ -302,6 +351,15 @@ export class SpliceWalletJSONRPCUserAPI { this.transport = transport } + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'info', + ...params: Parameters + ): ReturnType + /** * */ diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 876c2db5d..a754381bb 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -6,6 +6,24 @@ "description": "An OpenRPC specification for the user to interact with the wallet kernel." }, "methods": [ + { + "name": "info", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "InfoResult", + "type": "object", + "properties": { + "kernel": { + "$ref": "#/components/schemas/KernelInfo" + } + }, + "required": ["kernel"] + } + }, + "description": "Returns the current chainId if connected." + }, { "name": "addNetwork", "params": [ @@ -382,6 +400,35 @@ "type": "null", "description": "Represents a null value, used in responses where no data is returned." }, + "KernelInfo": { + "title": "KernelInfo", + "type": "object", + "description": "Represents a wallet kernel.", + "properties": { + "id": { + "title": "id", + "type": "string", + "description": "The unique identifier of the wallet kernel." + }, + "clientType": { + "title": "clientType", + "type": "string", + "enum": ["browser", "desktop", "mobile", "remote"], + "description": "The type of client that implements the wallet kernel." + }, + "rpcUrl": { + "title": "rpcUrl", + "type": "string", + "description": "The RPC URL of the wallet kernel." + }, + "uiUrl": { + "title": "uiUrl", + "type": "string", + "description": "The UI URL of the wallet kernel." + } + }, + "required": ["id", "clientType"] + }, "Network": { "title": "Network", "type": "object", @@ -551,6 +598,11 @@ "network": { "$ref": "#/components/schemas/Network" }, + "userId": { + "title": "userId", + "type": "string", + "description": "The user ID associated with the session." + }, "accessToken": { "title": "accessToken", "type": "string", @@ -562,7 +614,7 @@ "enum": ["connected", "disconnected"] } }, - "required": ["network", "status", "accessToken"], + "required": ["network", "status", "userId", "accessToken"], "additionalProperties": false } } diff --git a/sdk/src/connect/index.ts b/sdk/src/connect/index.ts index 54b920db8..d9380f338 100644 --- a/sdk/src/connect/index.ts +++ b/sdk/src/connect/index.ts @@ -1,27 +1,20 @@ -import { discover, popupHref } from 'core-wallet-ui-components' -import { - injectSpliceProvider, - ProviderType, - SpliceProvider, -} from 'core-splice-provider' +import { discover } from 'core-wallet-ui-components' +import { injectSpliceProvider, ProviderType } from 'core-splice-provider' import * as dappAPI from 'core-wallet-dapp-rpc-client' import { SDK } from '../enums.js' -import { DiscoverResult, SpliceMessage, WalletEvent } from 'core-types' +import { DiscoverResult } from 'core-types' import { DappServer } from '../dapp-api/server.js' export * from 'core-splice-provider' let dappServer: DappServer | undefined = undefined -const injectProvider = ({ walletType }: DiscoverResult) => { +const injectProvider = ({ walletType, url }: DiscoverResult) => { + // Stop the previous DappServer if it exists + dappServer?.stop() + if (walletType === 'remote') { - // return injectSpliceProvider( - // ProviderType.HTTP, - // new URL(url), - // sessionToken - // ) - dappServer = new DappServer() + dappServer = new DappServer(new URL(url)) dappServer.run() - return injectSpliceProvider(ProviderType.WINDOW) } else { return injectSpliceProvider(ProviderType.WINDOW) @@ -38,38 +31,6 @@ if (connection) { } } -const onConnected = (provider: SpliceProvider, result: DiscoverResult) => { - provider.on('onConnected', (event) => { - console.log('SDK: Store connection') - localStorage.setItem( - SDK.LOCAL_STORAGE_KEY_CONNECTION, - JSON.stringify({ - ...result, - sessionToken: event.sessionToken, - }) - ) - }) -} - -const openKernelUserUI = ( - walletType: DiscoverResult['walletType'], - userUrl: string -) => { - switch (walletType) { - case 'remote': - popupHref(new URL(userUrl)) - break - case 'extension': { - const msg: SpliceMessage = { - type: WalletEvent.SPLICE_WALLET_EXT_OPEN, - url: userUrl, - } - window.postMessage(msg, '*') - break - } - } -} - export enum ErrorCode { UserCancelled, Other, @@ -84,19 +45,19 @@ export type ConnectError = { export async function connect(): Promise { return discover() .then(async (result) => { - dappServer?.stop() const provider = injectProvider(result) - - // Listen for connected eved from the provider - // This will be triggered when the user connects to the wallet kernel - onConnected(provider, result) - const response = await provider.request({ method: 'connect', }) - if (!response.isConnected) - openKernelUserUI(result.walletType, response.userUrl) + console.log('SDK: Store connection') + localStorage.setItem( + SDK.LOCAL_STORAGE_KEY_CONNECTION, + JSON.stringify({ + ...result, + sessionToken: response.sessionToken, + }) + ) return response }) diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts index 95ea41072..b49336869 100644 --- a/sdk/src/dapp-api/controller.ts +++ b/sdk/src/dapp-api/controller.ts @@ -12,16 +12,16 @@ import { isSpliceMessage, SuccessResponse, WalletEvent } from 'core-types' import * as userApi from 'core-wallet-user-rpc-client' enum WK_URL { - LOGIN = 'http://localhost:3002/login/', + LOGIN = '/login/', } const popupInteraction = async ( - url: WK_URL, + url: URL, callback: (data: SuccessResponse) => T ): Promise => { - const win = await popupHref(new URL(url)) + const win = await popupHref(url) console.log('popup window opened', win) - let response = false + let eventReceived = false return new Promise((resolve, reject) => { const listener = (event: MessageEvent) => { console.log('user has done stuff in the UI', event.data) @@ -32,6 +32,7 @@ const popupInteraction = async ( event.data.type === WalletEvent.SPLICE_WALLET_USER_RESPONSE ) { window.removeEventListener('message', listener) + eventReceived = true if ('error' in event.data.response) { console.error( @@ -44,7 +45,6 @@ const popupInteraction = async ( 'User response received:', event.data.response.result ) - response = true resolve(callback(event.data.response)) } } @@ -53,21 +53,25 @@ const popupInteraction = async ( const interval = setInterval(() => { if (!win || win.closed) { - console.log('Wallet discovery window closed by user') clearInterval(interval) window.removeEventListener('message', listener) - if (response === false) { + if (eventReceived === false) { reject('User closed the wallet window prior to interaction') } - } else { - console.log('Waiting for user to interact with the UI') } }, 1000) }) } -export const dappController = () => - buildController({ +export const dappController = (rpcUrl: URL, uiUrl: URL) => { + console.log( + 'Creating dapp controller with rpcUrl:', + rpcUrl, + 'and uiUrl:', + uiUrl + ) + const url = (page: WK_URL) => new URL(page, uiUrl) + return buildController({ status: function (): Promise { return Promise.resolve({ kernel: { @@ -80,24 +84,28 @@ export const dappController = () => }) }, connect: async function (): Promise { - return popupInteraction(WK_URL.LOGIN, (data: SuccessResponse) => { - const addSessionResult = data.result as userApi.AddSessionResult - console.log( - 'User has logged in, received result:', - addSessionResult - ) - return { - kernel: { - id: 'kernel-id', // TODO: get from userApi - clientType: 'remote', - url: 'url', // TODO: get from userApi - }, - isConnected: true, - chainId: addSessionResult.network.chainId, - userUrl: 'user-url', // TODO: get from userApi - sessionToken: addSessionResult.accessToken, + return popupInteraction( + url(WK_URL.LOGIN), + (data: SuccessResponse) => { + const addSessionResult = + data.result as userApi.AddSessionResult + console.log( + 'User has logged in, received result:', + addSessionResult + ) + return { + kernel: { + id: 'kernel-id', // TODO: get from userApi + clientType: 'remote', + url: 'url', // TODO: get from userApi + }, + isConnected: true, + chainId: addSessionResult.network.chainId, + userUrl: 'user-url', // TODO: get from userApi + sessionToken: addSessionResult.accessToken, + } } - }) + ) }, darsAvailable: async () => ({ dars: ['default-dar'] }), prepareReturn: function (): Promise { @@ -122,3 +130,4 @@ export const dappController = () => throw new Error('Only for events.') }, }) +} diff --git a/sdk/src/dapp-api/server.ts b/sdk/src/dapp-api/server.ts index 18dfc75c1..15ac6e174 100644 --- a/sdk/src/dapp-api/server.ts +++ b/sdk/src/dapp-api/server.ts @@ -10,21 +10,20 @@ import { import { Methods } from './rpc-gen' import { dappController } from './controller' import { rpcErrors } from '@metamask/rpc-errors' +import * as userApi from 'core-wallet-user-rpc-client' export class DappServer { - controller: Methods + private controller: Promise - constructor() { - this.controller = dappController() - } - - static sendResponse(response: SpliceMessage) { - console.log('Sending response:', response) - window.postMessage(response, '*') + constructor(private rpcUrl: URL) { + this.controller = this.getController() } // Main RPC handler for incoming JSON-RPC requests - async handleRpcRequest(message: unknown): Promise { + async handleRpcRequest( + controller: Methods, + message: unknown + ): Promise { return new Promise((resolve, reject) => { if ( isSpliceMessage(message) && @@ -36,7 +35,7 @@ export class DappServer { const id = request.id || null const method = request.method as keyof Methods - const methodFn = this.controller[method] + const methodFn = controller[method] if (!methodFn) { resolve( @@ -69,6 +68,37 @@ export class DappServer { }) } + async getKernelInfo(): Promise { + console.log('Fetching Kernel info...') + const res = await fetch(this.rpcUrl.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: Date.now(), + method: 'info', + }), + }) + + const body = await res.json().then(JsonRpcResponse.parse) + if ('error' in body) throw new Error(body.error.message) + return body.result as userApi.InfoResult + } + + async getController(): Promise { + const kernelInfo = await this.getKernelInfo() + if (!kernelInfo.kernel.uiUrl) { + throw new Error('Kernel info does not contain uiUrl') + } + const controller = dappController( + this.rpcUrl, + new URL(kernelInfo.kernel.uiUrl) + ) + return controller + } + // Handle incoming RPC requests from the dapp, // proxy them to the controller, and send the response back to the dapp run() { @@ -90,7 +120,7 @@ export class DappServer { // Forward JSON RPC requests to the background script if (message.type === WalletEvent.SPLICE_WALLET_REQUEST) { console.log('Received request:', message) - this.handleRpcRequest(message) + this.handleRpcRequest(await this.controller, message) .then(transport.submitResponse) .catch((error: unknown) => { const e = JsonRpcResponse.safeParse(error) From 85fce096725a193baf1bc70cbbc765f4e2207499 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Thu, 7 Aug 2025 11:28:33 +0200 Subject: [PATCH 3/6] Session handling Signed-off-by: Marc Juchli --- api-specs/openrpc-user-api.json | 18 +++ clients/remote/src/user-api/controller.ts | 24 ++++ clients/remote/src/user-api/rpc-gen/index.ts | 3 + .../remote/src/user-api/rpc-gen/typings.ts | 16 +++ .../splice-provider/src/SpliceProviderHttp.ts | 45 +------ .../src/SpliceProviderWindow.ts | 2 - core/types/src/index.ts | 31 +++++ core/wallet-user-rpc-client/src/index.ts | 25 ++++ core/wallet-user-rpc-client/src/openrpc.json | 18 +++ sdk/src/dapp-api/controller.ts | 112 +++++++++++++----- sdk/src/enums.ts | 1 + 11 files changed, 227 insertions(+), 68 deletions(-) diff --git a/api-specs/openrpc-user-api.json b/api-specs/openrpc-user-api.json index a754381bb..9016414a2 100644 --- a/api-specs/openrpc-user-api.json +++ b/api-specs/openrpc-user-api.json @@ -342,6 +342,24 @@ } } }, + { + "name": "getSession", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "GetSessionResult", + "type": "object", + "properties": { + "session": { + "title": "session", + "$ref": "#/components/schemas/Session" + } + }, + "required": ["session"] + } + } + }, { "name": "addSession", "description": "Adds a network session.", diff --git a/clients/remote/src/user-api/controller.ts b/clients/remote/src/user-api/controller.ts index 759e37d3c..cf4449fd8 100644 --- a/clients/remote/src/user-api/controller.ts +++ b/clients/remote/src/user-api/controller.ts @@ -14,6 +14,7 @@ import { ListSessionsResult, SetPrimaryWalletParams, InfoResult, + GetSessionResult, } from './rpc-gen/typings.js' import { Store, Wallet, Auth } from 'core-wallet-store' import { Logger } from 'pino' @@ -357,5 +358,28 @@ export const userController = ( ], } }, + getSession: async function (): Promise { + const session = await store.getSession() + if (!session) { + throw new Error('No active session found') + } + const network = await store.getNetwork(session.network) + + return { + session: { + userId: authContext!.userId, + accessToken: authContext!.accessToken, + status: 'connected', + network: { + name: network.name, + chainId: network.chainId, + synchronizerId: network.synchronizerId, + description: network.description, + ledgerApi: network.ledgerApi, + auth: network.auth, + }, + }, + } + }, }) } diff --git a/clients/remote/src/user-api/rpc-gen/index.ts b/clients/remote/src/user-api/rpc-gen/index.ts index af259e52f..543399d00 100644 --- a/clients/remote/src/user-api/rpc-gen/index.ts +++ b/clients/remote/src/user-api/rpc-gen/index.ts @@ -10,6 +10,7 @@ import { ListWallets } from './typings.js' import { Sign } from './typings.js' import { Execute } from './typings.js' import { ListNetworks } from './typings.js' +import { GetSession } from './typings.js' import { AddSession } from './typings.js' import { ListSessions } from './typings.js' @@ -24,6 +25,7 @@ export type Methods = { sign: Sign execute: Execute listNetworks: ListNetworks + getSession: GetSession addSession: AddSession listSessions: ListSessions } @@ -40,6 +42,7 @@ function buildController(methods: Methods) { sign: methods.sign, execute: methods.execute, listNetworks: methods.listNetworks, + getSession: methods.getSession, addSession: methods.addSession, listSessions: methods.listSessions, } diff --git a/clients/remote/src/user-api/rpc-gen/typings.ts b/clients/remote/src/user-api/rpc-gen/typings.ts index 0cfdf4fd2..23a419a79 100644 --- a/clients/remote/src/user-api/rpc-gen/typings.ts +++ b/clients/remote/src/user-api/rpc-gen/typings.ts @@ -220,6 +220,17 @@ export interface Session { accessToken: AccessToken status: Status } +/** + * + * Structure representing the connected network session + * + */ +export interface Session { + network: Network + userId: UserId + accessToken: AccessToken + status: Status +} export type Sessions = Session[] export interface AddNetworkParams { network: Network @@ -302,6 +313,10 @@ export interface ListNetworksResult { networks: Networks [k: string]: any } +export interface GetSessionResult { + session: Session + [k: string]: any +} /** * * Structure representing the connected network session @@ -339,5 +354,6 @@ export type ListWallets = ( export type Sign = (params: SignParams) => Promise export type Execute = (params: ExecuteParams) => Promise export type ListNetworks = () => Promise +export type GetSession = () => Promise export type AddSession = (params: AddSessionParams) => Promise export type ListSessions = () => Promise diff --git a/core/splice-provider/src/SpliceProviderHttp.ts b/core/splice-provider/src/SpliceProviderHttp.ts index 8bb7c68f6..fd76c37af 100644 --- a/core/splice-provider/src/SpliceProviderHttp.ts +++ b/core/splice-provider/src/SpliceProviderHttp.ts @@ -1,16 +1,16 @@ import { + HttpTransport, isSpliceMessageEvent, - JsonRpcResponse, RequestPayload, WalletEvent, } from 'core-types' import { SpliceProviderBase } from './SpliceProvider' import { io, Socket } from 'socket.io-client' -import { popupHref } from 'core-wallet-ui-components' export class SpliceProviderHttp extends SpliceProviderBase { private sessionToken?: string private socket: Socket + private transport: HttpTransport private openSocket(url: URL): Socket { // Assumes the RPC URL is on /rpc, and the socket URL is the same but without the /rpc path. @@ -43,6 +43,7 @@ export class SpliceProviderHttp extends SpliceProviderBase { if (sessionToken) this.sessionToken = sessionToken this.socket = this.openSocket(url) + this.transport = new HttpTransport(url, sessionToken) // Listen for the auth success event sent from the WK UI popup to the SDK running in the parent window. window.addEventListener('message', async (event) => { @@ -66,42 +67,8 @@ export class SpliceProviderHttp extends SpliceProviderBase { } public async request({ method, params }: RequestPayload): Promise { - return await this.jsonRpcRequest(this.url, method, params) - } - - async jsonRpcRequest( - url: URL, - method: string, - params: unknown - ): Promise { - const res = await fetch(url.href, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(this.sessionToken && { - Authorization: `Bearer ${this.sessionToken}`, - }), - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - method, - params, - }), - }) - - const body = await res.json().then(JsonRpcResponse.parse) - - if ('error' in body) throw new Error(body.error.message) - - if (method === 'prepareExecute') { - const { userUrl } = body.result as { userUrl?: string } - if (!userUrl) { - throw new Error('No userUrl provided in response') - } - popupHref(userUrl) - } - - return body.result as T + const response = await this.transport.submit({ method, params }) + if ('error' in response) throw new Error(response.error.message) + return response.result as T } } diff --git a/core/splice-provider/src/SpliceProviderWindow.ts b/core/splice-provider/src/SpliceProviderWindow.ts index 538bbdc55..b70b6c893 100644 --- a/core/splice-provider/src/SpliceProviderWindow.ts +++ b/core/splice-provider/src/SpliceProviderWindow.ts @@ -10,9 +10,7 @@ export class SpliceProviderWindow extends SpliceProviderBase { } public async request({ method, params }: RequestPayload): Promise { - console.log('SpliceProviderWindow request:', method, params) const response = await this.transport.submit({ method, params }) - console.log('SpliceProviderWindow response:', response) return response.result as T } } diff --git a/core/types/src/index.ts b/core/types/src/index.ts index ea649ebd7..705464e97 100644 --- a/core/types/src/index.ts +++ b/core/types/src/index.ts @@ -203,3 +203,34 @@ export class WindowTransport implements RpcTransport { this.win.postMessage(message, '*') } } + +export class HttpTransport implements RpcTransport { + constructor( + private url: URL, + private sessionToken?: string + ) { + this.url = url + this.sessionToken = sessionToken + } + + submit = async (payload: RequestPayload) => { + const res = await fetch(this.url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(this.sessionToken && { + Authorization: `Bearer ${this.sessionToken}`, + }), + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: Date.now(), + method: payload.method, + params: payload.params, + }), + }) + + const body = await res.json().then(JsonRpcResponse.parse) + return body + } +} diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index ef8d5950a..1a36f58d1 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -222,6 +222,17 @@ export interface Session { accessToken: AccessToken status: Status } +/** + * + * Structure representing the connected network session + * + */ +export interface Session { + network: Network + userId: UserId + accessToken: AccessToken + status: Status +} export type Sessions = Session[] export interface AddNetworkParams { network: Network @@ -304,6 +315,10 @@ export interface ListNetworksResult { networks: Networks [k: string]: any } +export interface GetSessionResult { + session: Session + [k: string]: any +} /** * * Structure representing the connected network session @@ -341,6 +356,7 @@ export type ListWallets = ( export type Sign = (params: SignParams) => Promise export type Execute = (params: ExecuteParams) => Promise export type ListNetworks = () => Promise +export type GetSession = () => Promise export type AddSession = (params: AddSessionParams) => Promise export type ListSessions = () => Promise @@ -441,6 +457,15 @@ export class SpliceWalletJSONRPCUserAPI { ...params: Parameters ): ReturnType + /** + * + */ + // tslint:disable-next-line:max-line-length + public async request( + method: 'getSession', + ...params: Parameters + ): ReturnType + /** * */ diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index a754381bb..9016414a2 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -342,6 +342,24 @@ } } }, + { + "name": "getSession", + "params": [], + "result": { + "name": "result", + "schema": { + "title": "GetSessionResult", + "type": "object", + "properties": { + "session": { + "title": "session", + "$ref": "#/components/schemas/Session" + } + }, + "required": ["session"] + } + } + }, { "name": "addSession", "description": "Adds a network session.", diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts index b49336869..e2a715ab3 100644 --- a/sdk/src/dapp-api/controller.ts +++ b/sdk/src/dapp-api/controller.ts @@ -8,8 +8,15 @@ import { LedgerApiResult, RequestAccountsResult, } from './rpc-gen/typings' -import { isSpliceMessage, SuccessResponse, WalletEvent } from 'core-types' +import { + HttpTransport, + isSpliceMessage, + RequestPayload, + SuccessResponse, + WalletEvent, +} from 'core-types' import * as userApi from 'core-wallet-user-rpc-client' +import { SDK } from '../enums' enum WK_URL { LOGIN = '/login/', @@ -63,46 +70,97 @@ const popupInteraction = async ( }) } +class UserClient { + private transport: HttpTransport + + constructor(url: URL, sessionToken?: string) { + this.transport = new HttpTransport(url, sessionToken) + } + + public async request({ method, params }: RequestPayload): Promise { + const response = await this.transport.submit({ method, params }) + if ('error' in response) throw new Error(response.error.message) + return response.result as T + } +} + export const dappController = (rpcUrl: URL, uiUrl: URL) => { - console.log( - 'Creating dapp controller with rpcUrl:', - rpcUrl, - 'and uiUrl:', - uiUrl - ) const url = (page: WK_URL) => new URL(page, uiUrl) + let sessionToken: string | undefined = undefined + const session = localStorage.getItem(SDK.LOCAL_STORAGE_KEY_SESSION) + if (session) { + try { + const sessionData = JSON.parse(session) as userApi.AddSessionResult + sessionToken = sessionData.accessToken + console.log('SDK: Restored session:', sessionData) + } catch (e) { + console.error('Failed to parse stored session:', e) + } + } + let userClient = new UserClient(rpcUrl, sessionToken) + return buildController({ - status: function (): Promise { - return Promise.resolve({ - kernel: { - id: 'kernel-id', // TODO: get from userApi - clientType: 'remote', - url: 'url', // TODO: get from userApi - }, - isConnected: false, - chainId: 'chain-id', // TODO: get from userApi + status: async function (): Promise { + const info = await userClient.request({ + method: 'info', + params: [], }) + try { + const session = + await userClient.request({ + method: 'getSession', + params: [], + }) + return Promise.resolve({ + kernel: { + id: info.kernel.id, + clientType: info.kernel.clientType, + url: '', // TODO: remove + }, + isConnected: true, + chainId: session.chainId, + }) + } catch (error) { + console.error('Error fetching session:', error) + return Promise.resolve({ + kernel: { + id: info.kernel.id, + clientType: info.kernel.clientType, + url: '', // TODO: remove + }, + isConnected: false, + }) + } }, connect: async function (): Promise { return popupInteraction( url(WK_URL.LOGIN), - (data: SuccessResponse) => { - const addSessionResult = - data.result as userApi.AddSessionResult - console.log( - 'User has logged in, received result:', - addSessionResult + async (data: SuccessResponse) => { + const session = data.result as userApi.AddSessionResult + console.log('User has logged in, received result:', session) + userClient = new UserClient(rpcUrl, session.accessToken) + + // TODO: localstorage service + localStorage.setItem( + SDK.LOCAL_STORAGE_KEY_SESSION, + JSON.stringify(session) ) + + const info = await userClient.request({ + method: 'info', + params: [], + }) + return { kernel: { - id: 'kernel-id', // TODO: get from userApi - clientType: 'remote', - url: 'url', // TODO: get from userApi + id: info.kernel.id, + clientType: info.kernel.clientType, + url: '', // TODO: remove }, isConnected: true, - chainId: addSessionResult.network.chainId, + chainId: session.network.chainId, userUrl: 'user-url', // TODO: get from userApi - sessionToken: addSessionResult.accessToken, + sessionToken: session.accessToken, } } ) diff --git a/sdk/src/enums.ts b/sdk/src/enums.ts index 4d6a2dc50..c22bf89bf 100644 --- a/sdk/src/enums.ts +++ b/sdk/src/enums.ts @@ -1,3 +1,4 @@ export enum SDK { LOCAL_STORAGE_KEY_CONNECTION = 'splice_wallet_connection', + LOCAL_STORAGE_KEY_SESSION = 'splice_wallet_session', } From db48a3d497b47ddcc452b687e7ecac42a13fb709 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Thu, 7 Aug 2025 14:49:40 +0200 Subject: [PATCH 4/6] Extract local storage Signed-off-by: Marc Juchli --- .../src/SpliceProviderWindow.ts | 2 + sdk/src/connect/index.ts | 20 ++----- sdk/src/dapp-api/controller.ts | 54 ++++++------------- sdk/src/dapp-api/server.ts | 32 ++++------- sdk/src/enums.ts | 4 -- sdk/src/http-client.ts | 15 ++++++ sdk/src/storage.ts | 42 +++++++++++++++ 7 files changed, 89 insertions(+), 80 deletions(-) delete mode 100644 sdk/src/enums.ts create mode 100644 sdk/src/http-client.ts create mode 100644 sdk/src/storage.ts diff --git a/core/splice-provider/src/SpliceProviderWindow.ts b/core/splice-provider/src/SpliceProviderWindow.ts index b70b6c893..538bbdc55 100644 --- a/core/splice-provider/src/SpliceProviderWindow.ts +++ b/core/splice-provider/src/SpliceProviderWindow.ts @@ -10,7 +10,9 @@ export class SpliceProviderWindow extends SpliceProviderBase { } public async request({ method, params }: RequestPayload): Promise { + console.log('SpliceProviderWindow request:', method, params) const response = await this.transport.submit({ method, params }) + console.log('SpliceProviderWindow response:', response) return response.result as T } } diff --git a/sdk/src/connect/index.ts b/sdk/src/connect/index.ts index d9380f338..e5ef12900 100644 --- a/sdk/src/connect/index.ts +++ b/sdk/src/connect/index.ts @@ -1,10 +1,10 @@ import { discover } from 'core-wallet-ui-components' import { injectSpliceProvider, ProviderType } from 'core-splice-provider' import * as dappAPI from 'core-wallet-dapp-rpc-client' -import { SDK } from '../enums.js' import { DiscoverResult } from 'core-types' import { DappServer } from '../dapp-api/server.js' export * from 'core-splice-provider' +import * as storage from '../storage.js' let dappServer: DappServer | undefined = undefined @@ -22,14 +22,8 @@ const injectProvider = ({ walletType, url }: DiscoverResult) => { } // On page load, restore and re-register the listener if needed -const connection = localStorage.getItem(SDK.LOCAL_STORAGE_KEY_CONNECTION) -if (connection) { - try { - injectProvider(DiscoverResult.parse(JSON.parse(connection))) - } catch (e) { - console.error('Failed to parse stored wallet connection:', e) - } -} +const discovery = storage.getKernelDiscovery() +if (discovery) injectProvider(discovery) export enum ErrorCode { UserCancelled, @@ -51,13 +45,7 @@ export async function connect(): Promise { }) console.log('SDK: Store connection') - localStorage.setItem( - SDK.LOCAL_STORAGE_KEY_CONNECTION, - JSON.stringify({ - ...result, - sessionToken: response.sessionToken, - }) - ) + storage.setKernelDiscovery(result) return response }) diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts index e2a715ab3..c8b0b7211 100644 --- a/sdk/src/dapp-api/controller.ts +++ b/sdk/src/dapp-api/controller.ts @@ -8,15 +8,10 @@ import { LedgerApiResult, RequestAccountsResult, } from './rpc-gen/typings' -import { - HttpTransport, - isSpliceMessage, - RequestPayload, - SuccessResponse, - WalletEvent, -} from 'core-types' +import { isSpliceMessage, SuccessResponse, WalletEvent } from 'core-types' import * as userApi from 'core-wallet-user-rpc-client' -import { SDK } from '../enums' +import { HttpClient } from '../http-client' +import * as storage from '../storage.js' enum WK_URL { LOGIN = '/login/', @@ -70,34 +65,17 @@ const popupInteraction = async ( }) } -class UserClient { - private transport: HttpTransport - - constructor(url: URL, sessionToken?: string) { - this.transport = new HttpTransport(url, sessionToken) - } - - public async request({ method, params }: RequestPayload): Promise { - const response = await this.transport.submit({ method, params }) - if ('error' in response) throw new Error(response.error.message) - return response.result as T - } -} - export const dappController = (rpcUrl: URL, uiUrl: URL) => { const url = (page: WK_URL) => new URL(page, uiUrl) - let sessionToken: string | undefined = undefined - const session = localStorage.getItem(SDK.LOCAL_STORAGE_KEY_SESSION) - if (session) { - try { - const sessionData = JSON.parse(session) as userApi.AddSessionResult - sessionToken = sessionData.accessToken - console.log('SDK: Restored session:', sessionData) - } catch (e) { - console.error('Failed to parse stored session:', e) - } + + // Initialize the userApi client with the rpcUrl and sessionToken + const sessionToken = storage.getKernelSession()?.accessToken + if (sessionToken) { + console.log('SDK: Restored session token:', sessionToken) + } else { + console.warn('SDK: No session token found, proceeding without it') } - let userClient = new UserClient(rpcUrl, sessionToken) + let userClient = new HttpClient(rpcUrl, sessionToken) return buildController({ status: async function (): Promise { @@ -111,6 +89,7 @@ export const dappController = (rpcUrl: URL, uiUrl: URL) => { method: 'getSession', params: [], }) + return Promise.resolve({ kernel: { id: info.kernel.id, @@ -138,13 +117,10 @@ export const dappController = (rpcUrl: URL, uiUrl: URL) => { async (data: SuccessResponse) => { const session = data.result as userApi.AddSessionResult console.log('User has logged in, received result:', session) - userClient = new UserClient(rpcUrl, session.accessToken) + userClient = new HttpClient(rpcUrl, session.accessToken) - // TODO: localstorage service - localStorage.setItem( - SDK.LOCAL_STORAGE_KEY_SESSION, - JSON.stringify(session) - ) + console.log('SDK: Store connection') + storage.setKernelSession(session) const info = await userClient.request({ method: 'info', diff --git a/sdk/src/dapp-api/server.ts b/sdk/src/dapp-api/server.ts index 15ac6e174..ded399761 100644 --- a/sdk/src/dapp-api/server.ts +++ b/sdk/src/dapp-api/server.ts @@ -11,11 +11,14 @@ import { Methods } from './rpc-gen' import { dappController } from './controller' import { rpcErrors } from '@metamask/rpc-errors' import * as userApi from 'core-wallet-user-rpc-client' +import { HttpClient } from '../http-client' export class DappServer { private controller: Promise + private httpClient: HttpClient constructor(private rpcUrl: URL) { + this.httpClient = new HttpClient(rpcUrl) this.controller = this.getController() } @@ -49,7 +52,10 @@ export class DappServer { // eslint-disable-next-line @typescript-eslint/no-explicit-any methodFn(request.params as any) - .then((result) => resolve(jsonRpcResponse(id, { result }))) + .then((result) => { + console.log('RPC method response:', result) + resolve(jsonRpcResponse(id, { result })) + }) .catch((error) => reject( jsonRpcResponse(id, { @@ -68,27 +74,11 @@ export class DappServer { }) } - async getKernelInfo(): Promise { - console.log('Fetching Kernel info...') - const res = await fetch(this.rpcUrl.href, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - id: Date.now(), - method: 'info', - }), - }) - - const body = await res.json().then(JsonRpcResponse.parse) - if ('error' in body) throw new Error(body.error.message) - return body.result as userApi.InfoResult - } - async getController(): Promise { - const kernelInfo = await this.getKernelInfo() + const kernelInfo = await this.httpClient.request({ + method: 'info', + params: [], + }) if (!kernelInfo.kernel.uiUrl) { throw new Error('Kernel info does not contain uiUrl') } diff --git a/sdk/src/enums.ts b/sdk/src/enums.ts deleted file mode 100644 index c22bf89bf..000000000 --- a/sdk/src/enums.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum SDK { - LOCAL_STORAGE_KEY_CONNECTION = 'splice_wallet_connection', - LOCAL_STORAGE_KEY_SESSION = 'splice_wallet_session', -} diff --git a/sdk/src/http-client.ts b/sdk/src/http-client.ts new file mode 100644 index 000000000..e96717e71 --- /dev/null +++ b/sdk/src/http-client.ts @@ -0,0 +1,15 @@ +import { HttpTransport, RequestPayload } from 'core-types' + +export class HttpClient { + private transport: HttpTransport + + constructor(url: URL, sessionToken?: string) { + this.transport = new HttpTransport(url, sessionToken) + } + + public async request({ method, params }: RequestPayload): Promise { + const response = await this.transport.submit({ method, params }) + if ('error' in response) throw new Error(response.error.message) + return response.result as T + } +} diff --git a/sdk/src/storage.ts b/sdk/src/storage.ts new file mode 100644 index 000000000..1145d9832 --- /dev/null +++ b/sdk/src/storage.ts @@ -0,0 +1,42 @@ +import { DiscoverResult } from 'core-types' +import * as userApi from 'core-wallet-user-rpc-client' + +enum LOCAL_STORAGE { + KERNEL_DISCOVERY = 'splice_wallet_kernel_discovery', + KERNEL_SESSION = 'splice_wallet_kernel_session', +} + +export const getKernelDiscovery = (): DiscoverResult | undefined => { + const discovery = localStorage.getItem(LOCAL_STORAGE.KERNEL_DISCOVERY) + if (discovery) { + try { + return DiscoverResult.parse(JSON.parse(discovery)) + } catch (e) { + console.error('Failed to parse stored kernel discovery:', e) + } + } + return undefined +} + +export const setKernelDiscovery = (discovery: DiscoverResult): void => { + localStorage.setItem( + LOCAL_STORAGE.KERNEL_DISCOVERY, + JSON.stringify(discovery) + ) +} + +export const getKernelSession = (): userApi.AddSessionResult | undefined => { + const session = localStorage.getItem(LOCAL_STORAGE.KERNEL_SESSION) + if (session) { + try { + return JSON.parse(session) as userApi.AddSessionResult + } catch (e) { + console.error('Failed to parse stored kernel session:', e) + } + } + return undefined +} + +export const setKernelSession = (session: userApi.AddSessionResult): void => { + localStorage.setItem(LOCAL_STORAGE.KERNEL_SESSION, JSON.stringify(session)) +} From 805bf8799126ee7824d84dc242505ad642e56c12 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Fri, 8 Aug 2025 02:00:32 +0200 Subject: [PATCH 5/6] Socket is working Signed-off-by: Marc Juchli --- clients/remote/src/dapp-api/server.ts | 59 +--------- clients/remote/src/user-api/server.ts | 59 +++++++++- .../web/frontend/callback/login-callback.ts | 17 +-- .../splice-provider/src/SpliceProviderHttp.ts | 13 ++- core/types/package.json | 1 + core/types/src/index.ts | 28 +++-- core/wallet-user-rpc-client/package.json | 6 +- example/src/App.tsx | 5 +- sdk/src/connect/index.ts | 10 +- sdk/src/dapp-api/controller.ts | 66 ++++-------- sdk/src/dapp-api/server.ts | 102 +++++++++--------- yarn.lock | 1 + 12 files changed, 180 insertions(+), 187 deletions(-) diff --git a/clients/remote/src/dapp-api/server.ts b/clients/remote/src/dapp-api/server.ts index 5fa91c673..3d8772d3b 100644 --- a/clients/remote/src/dapp-api/server.ts +++ b/clients/remote/src/dapp-api/server.ts @@ -8,12 +8,7 @@ import { jwtAuth } from '../middleware/jwtAuth.js' import { AuthService, AuthAware } from 'core-wallet-auth' import { rpcRateLimit } from '../middleware/rateLimit.js' import cors from 'cors' -import { createServer } from 'http' -import { Server } from 'socket.io' -import { - NotificationService, - Notifier, -} from '../notification/NotificationService.js' +import { NotificationService } from '../notification/NotificationService.js' import { KernelInfo } from '../config/Config.js' const logger = pino({ name: 'main', level: 'debug' }) @@ -43,55 +38,5 @@ export const dapp = ( })(req, res, next) ) - const server = createServer(app) - const io = new Server(server, { - cors: { - origin: '*', - methods: ['GET', 'POST'], - }, - }) - - io.on('connection', (socket) => { - logger.info('Socket.io client connected') - - let notifier: Notifier | undefined = undefined - - const onAccountsChanged = (...event: unknown[]) => { - io.emit('accountsChanged', ...event) - } - const onConnected = (...event: unknown[]) => { - io.emit('onConnected', ...event) - } - const onTxChanged = (...event: unknown[]) => { - io.emit('txChanged', ...event) - } - - authService - .verifyToken(socket.handshake.auth.token) - .then((authContext) => { - const userId = authContext?.userId - - if (!userId) { - return - } - - notifier = notificationService.getNotifier(userId) - - notifier.on('accountsChanged', onAccountsChanged) - notifier.on('onConnected', onConnected) - notifier.on('txChanged', onTxChanged) - }) - - socket.on('disconnect', () => { - logger.info('Socket.io client disconnected') - - if (notifier) { - notifier.removeListener('accountsChanged', onAccountsChanged) - notifier.removeListener('onConnected', onConnected) - notifier.removeListener('txChanged', onTxChanged) - } - }) - }) - - return server + return app } diff --git a/clients/remote/src/user-api/server.ts b/clients/remote/src/user-api/server.ts index a43bec0bb..1afc45b5e 100644 --- a/clients/remote/src/user-api/server.ts +++ b/clients/remote/src/user-api/server.ts @@ -8,9 +8,14 @@ import { AuthService, AuthAware } from 'core-wallet-auth' import { jwtAuth } from '../middleware/jwtAuth.js' import { rpcRateLimit } from '../middleware/rateLimit.js' import cors from 'cors' -import { NotificationService } from '../notification/NotificationService.js' +import { + NotificationService, + Notifier, +} from '../notification/NotificationService.js' import { KernelInfo } from '../config/Config.js' import { SigningDriverInterface, SigningProvider } from 'core-signing-lib' +import { createServer } from 'http' +import { Server } from 'socket.io' const logger = pino({ name: 'main', level: 'debug' }) @@ -42,5 +47,55 @@ export const user = ( })(req, res, next) ) - return user + const server = createServer(user) + const io = new Server(server, { + cors: { + origin: '*', + methods: ['GET', 'POST'], + }, + }) + + io.on('connection', (socket) => { + logger.info('Socket.io client connected') + + let notifier: Notifier | undefined = undefined + + const onAccountsChanged = (...event: unknown[]) => { + io.emit('accountsChanged', ...event) + } + const onConnected = (...event: unknown[]) => { + io.emit('onConnected', ...event) + } + const onTxChanged = (...event: unknown[]) => { + io.emit('txChanged', ...event) + } + + authService + .verifyToken(socket.handshake.auth.token) + .then((authContext) => { + const userId = authContext?.userId + + if (!userId) { + return + } + + notifier = notificationService.getNotifier(userId) + + notifier.on('accountsChanged', onAccountsChanged) + notifier.on('onConnected', onConnected) + notifier.on('txChanged', onTxChanged) + }) + + socket.on('disconnect', () => { + logger.info('Socket.io client disconnected') + + if (notifier) { + notifier.removeListener('accountsChanged', onAccountsChanged) + notifier.removeListener('onConnected', onConnected) + notifier.removeListener('txChanged', onTxChanged) + } + }) + }) + + return server } diff --git a/clients/remote/src/web/frontend/callback/login-callback.ts b/clients/remote/src/web/frontend/callback/login-callback.ts index 255db08fa..5386a64ca 100644 --- a/clients/remote/src/web/frontend/callback/login-callback.ts +++ b/clients/remote/src/web/frontend/callback/login-callback.ts @@ -1,4 +1,4 @@ -import { WalletEvent, WindowTransport } from 'core-types' +import { WindowTransport } from 'core-types' import { LitElement, html } from 'lit' import { customElement } from 'lit/decorators.js' import { userClient } from '../rpc-client' @@ -44,16 +44,6 @@ export class LoginCallback extends LitElement { const tokenResponse = await res.json() if (tokenResponse.access_token) { - if (window.opener && !window.opener.closed) { - window.opener.postMessage( - { - type: WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS, - token: tokenResponse.access_token, - }, - '*' - ) - } - stateManager.accessToken.set(tokenResponse.access_token) const session = await userClient.transport.submit({ @@ -63,7 +53,10 @@ export class LoginCallback extends LitElement { }, }) - new WindowTransport(window.opener).submitUserResponse(session) + new WindowTransport(window.opener).submitUserResponse( + null, + session + ) window.location.replace('/') } diff --git a/core/splice-provider/src/SpliceProviderHttp.ts b/core/splice-provider/src/SpliceProviderHttp.ts index fd76c37af..10779e55d 100644 --- a/core/splice-provider/src/SpliceProviderHttp.ts +++ b/core/splice-provider/src/SpliceProviderHttp.ts @@ -1,8 +1,8 @@ import { - HttpTransport, isSpliceMessageEvent, RequestPayload, WalletEvent, + WindowTransport, } from 'core-types' import { SpliceProviderBase } from './SpliceProvider' import { io, Socket } from 'socket.io-client' @@ -10,7 +10,7 @@ import { io, Socket } from 'socket.io-client' export class SpliceProviderHttp extends SpliceProviderBase { private sessionToken?: string private socket: Socket - private transport: HttpTransport + private transport: WindowTransport private openSocket(url: URL): Socket { // Assumes the RPC URL is on /rpc, and the socket URL is the same but without the /rpc path. @@ -41,9 +41,10 @@ export class SpliceProviderHttp extends SpliceProviderBase { ) { super() + this.transport = new WindowTransport(window) + if (sessionToken) this.sessionToken = sessionToken this.socket = this.openSocket(url) - this.transport = new HttpTransport(url, sessionToken) // Listen for the auth success event sent from the WK UI popup to the SDK running in the parent window. window.addEventListener('message', async (event) => { @@ -53,9 +54,6 @@ export class SpliceProviderHttp extends SpliceProviderBase { event.data.type === WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS ) { this.sessionToken = event.data.token - console.log( - `SpliceProviderHttp: setting sessionToken to ${this.sessionToken}` - ) this.openSocket(this.url) // we requery the status explicitly here, as it's not guaranteed that the socket will be open & authenticated before the `onConnected` event is fired from the `addSession` RPC call. @@ -67,8 +65,9 @@ export class SpliceProviderHttp extends SpliceProviderBase { } public async request({ method, params }: RequestPayload): Promise { + console.log('SpliceProviderHTTP request:', method, params) const response = await this.transport.submit({ method, params }) - if ('error' in response) throw new Error(response.error.message) + console.log('SpliceProviderHTTP response:', response) return response.result as T } } diff --git a/core/types/package.json b/core/types/package.json index 1875fdddb..e09b3b498 100644 --- a/core/types/package.json +++ b/core/types/package.json @@ -12,6 +12,7 @@ "clean": "tsc -b --clean; rm -rf dist" }, "dependencies": { + "uuid": "^11.1.0", "zod": "^3.25.67" }, "devDependencies": { diff --git a/core/types/src/index.ts b/core/types/src/index.ts index 705464e97..af8fb2804 100644 --- a/core/types/src/index.ts +++ b/core/types/src/index.ts @@ -1,4 +1,5 @@ import { z } from 'zod' +import { v4 as uuidv4 } from 'uuid' /** * Requests / responses @@ -161,7 +162,7 @@ export class WindowTransport implements RpcTransport { submitRequest = async (payload: RequestPayload) => { const message: SpliceMessage = { - request: jsonRpcRequest('', payload), // TODO: add id + request: jsonRpcRequest(uuidv4(), payload), type: WalletEvent.SPLICE_WALLET_REQUEST, } this.win.postMessage(message, '*') @@ -169,16 +170,24 @@ export class WindowTransport implements RpcTransport { const listener = (event: MessageEvent) => { if ( !isSpliceMessageEvent(event) || - event.data.type !== WalletEvent.SPLICE_WALLET_RESPONSE + event.data.type !== WalletEvent.SPLICE_WALLET_RESPONSE || + event.data.response.id !== message.request.id ) { return } - console.log('Received message from wallet:', event.data) window.removeEventListener('message', listener) if ('error' in event.data.response) { + console.error( + 'Error in response:', + event.data.response.error + ) reject(event.data.response.error) } else { + console.log( + 'Received response for request', + event.data.response + ) resolve(event.data.response) } } @@ -187,17 +196,20 @@ export class WindowTransport implements RpcTransport { }) } - submitResponse = (payload: ResponsePayload) => { + submitResponse = (id: string | number | null, payload: ResponsePayload) => { const message: SpliceMessage = { - response: jsonRpcResponse(null, payload), + response: jsonRpcResponse(id, payload), type: WalletEvent.SPLICE_WALLET_RESPONSE, } this.win.postMessage(message, '*') } - submitUserResponse = (payload: ResponsePayload) => { + submitUserResponse = ( + id: string | number | null, + payload: ResponsePayload + ) => { const message: SpliceMessage = { - response: jsonRpcResponse('', payload), + response: jsonRpcResponse(id, payload), type: WalletEvent.SPLICE_WALLET_USER_RESPONSE, } this.win.postMessage(message, '*') @@ -224,7 +236,7 @@ export class HttpTransport implements RpcTransport { }, body: JSON.stringify({ jsonrpc: '2.0', - id: Date.now(), + id: uuidv4(), method: payload.method, params: payload.params, }), diff --git a/core/wallet-user-rpc-client/package.json b/core/wallet-user-rpc-client/package.json index daaccba29..2508f7bc7 100644 --- a/core/wallet-user-rpc-client/package.json +++ b/core/wallet-user-rpc-client/package.json @@ -21,14 +21,14 @@ }, "devDependencies": { "@eslint/js": "9.21.0", - "@typescript-eslint/eslint-plugin": "^8.25.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.3", "@types/isomorphic-fetch": "^0.0.39", "@types/jest": "^29.5.12", "@types/json-schema": "7.0.3", "@types/lodash": "^4.14.149", "@types/ws": "^6.0.1", + "@typescript-eslint/eslint-plugin": "^8.25.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.3", "globals": "^16.0.0", "prettier": "^3.5.2", "typedoc": "^0.27.9", diff --git a/example/src/App.tsx b/example/src/App.tsx index d579bf524..6ae1fbbb4 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -29,7 +29,10 @@ function App() { `Wallet Kernel: ${result.kernel.id}, status: ${result.isConnected ? 'connected' : 'disconnected'}, chain: ${result.chainId}` ) }) - .catch(() => setStatus('disconnected')) + .catch((e) => { + console.error('Error getting status:', e) + setStatus('disconnected') + }) provider .request({ diff --git a/sdk/src/connect/index.ts b/sdk/src/connect/index.ts index e5ef12900..ffe2081e4 100644 --- a/sdk/src/connect/index.ts +++ b/sdk/src/connect/index.ts @@ -13,9 +13,13 @@ const injectProvider = ({ walletType, url }: DiscoverResult) => { dappServer?.stop() if (walletType === 'remote') { - dappServer = new DappServer(new URL(url)) - dappServer.run() - return injectSpliceProvider(ProviderType.WINDOW) + const rpcUrl = new URL(url) + dappServer = new DappServer(rpcUrl) + return injectSpliceProvider( + ProviderType.HTTP, + rpcUrl, + storage.getKernelSession()?.accessToken + ) } else { return injectSpliceProvider(ProviderType.WINDOW) } diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts index c8b0b7211..d76fe316a 100644 --- a/sdk/src/dapp-api/controller.ts +++ b/sdk/src/dapp-api/controller.ts @@ -22,12 +22,9 @@ const popupInteraction = async ( callback: (data: SuccessResponse) => T ): Promise => { const win = await popupHref(url) - console.log('popup window opened', win) let eventReceived = false return new Promise((resolve, reject) => { const listener = (event: MessageEvent) => { - console.log('user has done stuff in the UI', event.data) - // TODO: check that is of type some user api result? if ( isSpliceMessage(event.data) && @@ -37,16 +34,8 @@ const popupInteraction = async ( eventReceived = true if ('error' in event.data.response) { - console.error( - 'Error in user response:', - event.data.response.error - ) reject(event.data.response.error) } else { - console.log( - 'User response received:', - event.data.response.result - ) resolve(callback(event.data.response)) } } @@ -70,9 +59,7 @@ export const dappController = (rpcUrl: URL, uiUrl: URL) => { // Initialize the userApi client with the rpcUrl and sessionToken const sessionToken = storage.getKernelSession()?.accessToken - if (sessionToken) { - console.log('SDK: Restored session token:', sessionToken) - } else { + if (!sessionToken) { console.warn('SDK: No session token found, proceeding without it') } let userClient = new HttpClient(rpcUrl, sessionToken) @@ -83,45 +70,38 @@ export const dappController = (rpcUrl: URL, uiUrl: URL) => { method: 'info', params: [], }) - try { - const session = - await userClient.request({ - method: 'getSession', - params: [], - }) + const session = await userClient.request({ + method: 'getSession', + params: [], + }) - return Promise.resolve({ - kernel: { - id: info.kernel.id, - clientType: info.kernel.clientType, - url: '', // TODO: remove - }, - isConnected: true, - chainId: session.chainId, - }) - } catch (error) { - console.error('Error fetching session:', error) - return Promise.resolve({ - kernel: { - id: info.kernel.id, - clientType: info.kernel.clientType, - url: '', // TODO: remove - }, - isConnected: false, - }) - } + return Promise.resolve({ + kernel: { + id: info.kernel.id, + clientType: info.kernel.clientType, + url: '', // TODO: remove + }, + isConnected: true, + chainId: session.session.network.chainId, + }) }, connect: async function (): Promise { return popupInteraction( url(WK_URL.LOGIN), async (data: SuccessResponse) => { const session = data.result as userApi.AddSessionResult - console.log('User has logged in, received result:', session) - userClient = new HttpClient(rpcUrl, session.accessToken) - console.log('SDK: Store connection') + // Store connection and notify the provider storage.setKernelSession(session) + window.postMessage( + { + type: WalletEvent.SPLICE_WALLET_IDP_AUTH_SUCCESS, + token: session.accessToken, + }, + '*' + ) + userClient = new HttpClient(rpcUrl, session.accessToken) const info = await userClient.request({ method: 'info', params: [], diff --git a/sdk/src/dapp-api/server.ts b/sdk/src/dapp-api/server.ts index ded399761..18938c4c6 100644 --- a/sdk/src/dapp-api/server.ts +++ b/sdk/src/dapp-api/server.ts @@ -16,17 +16,62 @@ import { HttpClient } from '../http-client' export class DappServer { private controller: Promise private httpClient: HttpClient + private listener = async (event: SpliceMessageEvent) => { + const { data: message, success } = SpliceMessage.safeParse(event.data) + + if (!success) { + // not a valid SpliceMessage, ignore + return + } + + const transport = new WindowTransport(window) + + // Forward JSON RPC requests to the background script + if (message.type === WalletEvent.SPLICE_WALLET_REQUEST) { + this.handleRpcRequest(message) + .then((response) => { + console.log( + 'Sending response for request', + message.request.id, + response + ) + transport.submitResponse( + message.request.id || null, + response + ) + }) + .catch((error: unknown) => { + const e = JsonRpcResponse.safeParse(error) + if (e.success) { + transport.submitResponse( + message.request.id || null, + e.data + ) + } else { + console.error( + 'No response generated for the request', + error + ) + transport.submitResponse(message.request.id || null, { + error: rpcErrors.internal({ + message: 'Internal error', + data: 'No response generated for the request', + }), + }) + } + }) + } + } constructor(private rpcUrl: URL) { this.httpClient = new HttpClient(rpcUrl) this.controller = this.getController() + window.addEventListener('message', this.listener) } // Main RPC handler for incoming JSON-RPC requests - async handleRpcRequest( - controller: Methods, - message: unknown - ): Promise { + async handleRpcRequest(message: unknown): Promise { + const controller = await this.controller return new Promise((resolve, reject) => { if ( isSpliceMessage(message) && @@ -53,7 +98,6 @@ export class DappServer { // eslint-disable-next-line @typescript-eslint/no-explicit-any methodFn(request.params as any) .then((result) => { - console.log('RPC method response:', result) resolve(jsonRpcResponse(id, { result })) }) .catch((error) => @@ -89,52 +133,8 @@ export class DappServer { return controller } - // Handle incoming RPC requests from the dapp, - // proxy them to the controller, and send the response back to the dapp - run() { - console.log('DappServer is running and listening for messages') - window.addEventListener( - 'message', - async (event: SpliceMessageEvent) => { - const { data: message, success } = SpliceMessage.safeParse( - event.data - ) - - if (!success) { - // not a valid SpliceMessage, ignore - return - } - - const transport = new WindowTransport(window) - - // Forward JSON RPC requests to the background script - if (message.type === WalletEvent.SPLICE_WALLET_REQUEST) { - console.log('Received request:', message) - this.handleRpcRequest(await this.controller, message) - .then(transport.submitResponse) - .catch((error: unknown) => { - const e = JsonRpcResponse.safeParse(error) - if (e.success) { - transport.submitResponse(e.data) - } else { - console.error( - 'No response generated for the request', - error - ) - transport.submitResponse({ - error: rpcErrors.internal({ - message: 'Internal error', - data: 'No response generated for the request', - }), - }) - } - }) - } - } - ) - } - stop() { - window.removeEventListener('message', this.run) + window.removeEventListener('message', this.listener) + console.log('DappServer stopped listening for messages') } } diff --git a/yarn.lock b/yarn.lock index 838d42566..f83eac16e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5568,6 +5568,7 @@ __metadata: resolution: "core-types@workspace:core/types" dependencies: typescript: "npm:^5.8.3" + uuid: "npm:^11.1.0" zod: "npm:^3.25.67" languageName: unknown linkType: soft From 661d2dd5b8388d698639c3b855dddf6698333af2 Mon Sep 17 00:00:00 2001 From: Marc Juchli Date: Mon, 11 Aug 2025 11:22:24 +0200 Subject: [PATCH 6/6] Hightlight error issue Signed-off-by: Marc Juchli --- sdk/src/dapp-api/controller.ts | 48 ++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/sdk/src/dapp-api/controller.ts b/sdk/src/dapp-api/controller.ts index d76fe316a..0e4f20798 100644 --- a/sdk/src/dapp-api/controller.ts +++ b/sdk/src/dapp-api/controller.ts @@ -66,24 +66,38 @@ export const dappController = (rpcUrl: URL, uiUrl: URL) => { return buildController({ status: async function (): Promise { - const info = await userClient.request({ - method: 'info', - params: [], - }) - const session = await userClient.request({ - method: 'getSession', - params: [], - }) + try { + const info = await userClient.request({ + method: 'info', + params: [], + }) + const session = + await userClient.request({ + method: 'getSession', + params: [], + }) - return Promise.resolve({ - kernel: { - id: info.kernel.id, - clientType: info.kernel.clientType, - url: '', // TODO: remove - }, - isConnected: true, - chainId: session.session.network.chainId, - }) + return Promise.resolve({ + kernel: { + id: info.kernel.id, + clientType: info.kernel.clientType, + url: '', // TODO: remove + }, + isConnected: true, + chainId: session.session.network.chainId, + }) + } catch (error) { + console.error(error) + return Promise.resolve({ + kernel: { + id: 'error', + clientType: 'remote', + url: 'error', + }, + isConnected: true, + chainId: 'error', + }) + } }, connect: async function (): Promise { return popupInteraction(