diff --git a/packages/wallet-sdk/src/CoinbaseWalletProvider.test.ts b/packages/wallet-sdk/src/CoinbaseWalletProvider.test.ts index 89bd3015d4..417dc55c99 100644 --- a/packages/wallet-sdk/src/CoinbaseWalletProvider.test.ts +++ b/packages/wallet-sdk/src/CoinbaseWalletProvider.test.ts @@ -6,7 +6,6 @@ describe('EIP1193Provider', () => { beforeEach(() => { provider = new CoinbaseWalletProvider({ - scwUrl: 'http://fooUrl.com', appName: 'TestApp', appChainIds: [], smartWalletOnly: false, @@ -27,7 +26,6 @@ describe('EIP1193Provider', () => { describe('default chain id', () => { it('uses the first chain id when appChainIds is not empty', () => { const provider = new CoinbaseWalletProvider({ - scwUrl: 'http://fooUrl.com', appName: 'TestApp', appChainIds: [8453, 84532], smartWalletOnly: false, @@ -37,7 +35,6 @@ describe('EIP1193Provider', () => { it('fallback to 1 when appChainIds is empty', () => { const provider = new CoinbaseWalletProvider({ - scwUrl: 'http://fooUrl.com', appName: 'TestApp', appChainIds: [], smartWalletOnly: false, diff --git a/packages/wallet-sdk/src/CoinbaseWalletProvider.ts b/packages/wallet-sdk/src/CoinbaseWalletProvider.ts index d08c0afb12..04a2b08cbb 100644 --- a/packages/wallet-sdk/src/CoinbaseWalletProvider.ts +++ b/packages/wallet-sdk/src/CoinbaseWalletProvider.ts @@ -19,7 +19,6 @@ import { AccountsUpdate, ChainUpdate } from './sign/UpdateListenerInterface'; import { SubscriptionRequestHandler } from './subscription/SubscriptionRequestHandler'; interface ConstructorOptions { - scwUrl?: string; appName: string; appLogoUrl?: string | null; appChainIds: number[]; @@ -36,6 +35,10 @@ export class CoinbaseWalletProvider extends EventEmitter implements ProviderInte return this.chain.id; } + public get isCoinbaseWallet() { + return true; + } + constructor(options: Readonly) { super(); diff --git a/packages/wallet-sdk/src/core/communicator/CrossDomainCommunicator.ts b/packages/wallet-sdk/src/core/communicator/CrossDomainCommunicator.ts index 0d4676704d..22aa542275 100644 --- a/packages/wallet-sdk/src/core/communicator/CrossDomainCommunicator.ts +++ b/packages/wallet-sdk/src/core/communicator/CrossDomainCommunicator.ts @@ -9,6 +9,10 @@ export abstract class CrossDomainCommunicator { return this._connected; } + protected set connected(value: boolean) { + this._connected = value; + } + protected abstract onConnect(): Promise; protected abstract onDisconnect(): void; protected abstract onEvent(event: MessageEvent): void; @@ -19,12 +23,13 @@ export abstract class CrossDomainCommunicator { } disconnect(): void { - this._connected = false; + this.connected = false; this.onDisconnect(); } protected peerWindow: Window | null = null; - protected postMessage(message: Message, options?: { bypassTargetOriginCheck: boolean }) { + + postMessage(message: Message, options?: { bypassTargetOriginCheck: boolean }) { let targetOrigin = this.url?.origin; if (targetOrigin === undefined) { if (options?.bypassTargetOriginCheck) { @@ -33,6 +38,11 @@ export abstract class CrossDomainCommunicator { throw standardErrors.rpc.internal('Communicator: No target origin'); } } - this.peerWindow?.postMessage(message, targetOrigin); + + if (!this.peerWindow) { + throw standardErrors.rpc.internal('Communicator: No peer window found'); + } + + this.peerWindow.postMessage(message, targetOrigin); } } diff --git a/packages/wallet-sdk/src/sign/SignRequestHandler.ts b/packages/wallet-sdk/src/sign/SignRequestHandler.ts index b8fe14d0eb..23d457a9fe 100644 --- a/packages/wallet-sdk/src/sign/SignRequestHandler.ts +++ b/packages/wallet-sdk/src/sign/SignRequestHandler.ts @@ -1,18 +1,13 @@ -import { SCWSigner } from './scw/SCWSigner'; -import { ConnectionType } from './scw/transport/ConfigMessage'; import { PopUpCommunicator } from './scw/transport/PopUpCommunicator'; -import { Signer, SignerUpdateListener } from './SignerInterface'; +import { SignerConfigurator } from './SignerConfigurator'; import { SignRequestHandlerListener } from './UpdateListenerInterface'; -import { WLSigner } from './walletlink/WLSigner'; import { CB_KEYS_URL } from ':core/constants'; import { standardErrorCodes, standardErrors } from ':core/error'; -import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage'; import { AddressString } from ':core/type'; import { RequestArguments } from ':core/type/ProviderInterface'; import { RequestHandler } from ':core/type/RequestHandlerInterface'; interface SignRequestHandlerOptions { - scwUrl?: string; appName: string; appLogoUrl?: string | null; appChainIds: number[]; @@ -20,113 +15,35 @@ interface SignRequestHandlerOptions { updateListener: SignRequestHandlerListener; } -const SIGNER_TYPE_KEY = 'SignerType'; - export class SignRequestHandler implements RequestHandler { - private appName: string; - private appLogoUrl: string | null; - private appChainIds: number[]; - private smartWalletOnly: boolean; - - private connectionType: string | null; - private connectionTypeSelectionResolver: ((value: unknown) => void) | undefined; - private signer: Signer | undefined; - - // should be encapsulated under ConnectorConfigurator - private signerTypeStorage = new ScopedLocalStorage('CBWSDK', 'SignRequestHandler'); private popupCommunicator: PopUpCommunicator; private updateListener: SignRequestHandlerListener; + private signerConfigurator: SignerConfigurator; constructor(options: Readonly) { this.popupCommunicator = new PopUpCommunicator({ - url: options.scwUrl || CB_KEYS_URL, + url: CB_KEYS_URL, }); this.updateListener = options.updateListener; - - // getWalletLinkUrl is called by the PopUpCommunicator when - // it receives message.type === 'wlQRCodeUrl' from the cb-wallet-scw popup - // its injected because we don't want to instantiate WalletLinkSigner until we have to - this.getWalletLinkUrl = this.getWalletLinkUrl.bind(this); - this.popupCommunicator.setWLQRCodeUrlCallback(this.getWalletLinkUrl); - - this.appName = options.appName; - this.appLogoUrl = options.appLogoUrl ?? null; - this.appChainIds = options.appChainIds; - - const persistedConnectionType = this.signerTypeStorage.getItem(SIGNER_TYPE_KEY); - this.connectionType = persistedConnectionType; - this.smartWalletOnly = options.smartWalletOnly; - - if (persistedConnectionType) { - this.initSigner(); - } - - this.setConnectionType = this.setConnectionType.bind(this); - this.initWalletLinkSigner = this.initWalletLinkSigner.bind(this); - } - - private readonly updateRelay: SignerUpdateListener = { - onAccountsUpdate: (signer, ...rest) => { - if (this.signer && signer !== this.signer) return; // ignore events from inactive signers - this.updateListener.onAccountsUpdate(...rest); - }, - onChainUpdate: (signer, ...rest) => { - if (this.signer && signer !== this.signer) return; // ignore events from inactive signers - if (signer instanceof WLSigner) { - this.connectionTypeSelectionResolver?.('walletlink'); - } - this.updateListener.onChainUpdate(...rest); - }, - }; - - private initSigner = () => { - if (this.connectionType === 'scw') { - this.initScwSigner(); - } else if (this.connectionType === 'walletlink') { - this.initWalletLinkSigner(); - } - }; - - private initScwSigner() { - if (this.signer instanceof SCWSigner) return; - this.signer = new SCWSigner({ - appName: this.appName, - appLogoUrl: this.appLogoUrl, - appChainIds: this.appChainIds, - puc: this.popupCommunicator, - updateListener: this.updateRelay, + this.signerConfigurator = new SignerConfigurator({ + ...options, + popupCommunicator: this.popupCommunicator, }); } - private initWalletLinkSigner() { - if (this.signer instanceof WLSigner) return; - - this.signer = new WLSigner({ - appName: this.appName, - appLogoUrl: this.appLogoUrl, - updateListener: this.updateRelay, - }); - } - - async onDisconnect() { - this.connectionType = null; - this.signerTypeStorage.removeItem(SIGNER_TYPE_KEY); - await this.signer?.disconnect(); - } - async handleRequest(request: RequestArguments, accounts: AddressString[]) { try { if (request.method === 'eth_requestAccounts') { return await this.eth_requestAccounts(accounts); } - if (!this.signer || accounts.length <= 0) { + if (!this.signerConfigurator.signer || accounts.length <= 0) { throw standardErrors.provider.unauthorized( "Must call 'eth_requestAccounts' before other methods" ); } - return await this.signer.request(request); + return await this.signerConfigurator.signer.request(request); } catch (err) { if ((err as { code?: unknown })?.code === standardErrorCodes.provider.unauthorized) { this.updateListener.onResetConnection(); @@ -135,27 +52,26 @@ export class SignRequestHandler implements RequestHandler { } } - async eth_requestAccounts(accounts: AddressString[]): Promise { + private async eth_requestAccounts(accounts: AddressString[]): Promise { if (accounts.length > 0) { this.updateListener.onConnect(); return Promise.resolve(accounts); } - if (!this.connectionType) { + if (!this.signerConfigurator.signerType) { // WL: this promise hangs until the QR code is scanned // SCW: this promise hangs until the user signs in with passkey - const connectionType = await this.completeConnectionTypeSelection(); - this.setConnectionType(connectionType as ConnectionType); + await this.signerConfigurator.completeSignerTypeSelection(); } try { // in the case of walletlink, this doesn't do anything since signer is initialized // when the wallet link QR code url is requested - this.initSigner(); + this.signerConfigurator.initSigner(); - const ethAddresses = await this.signer?.handshake(); + const ethAddresses = await this.signerConfigurator.signer?.handshake(); if (Array.isArray(ethAddresses)) { - if (this.connectionType === 'walletlink') { + if (this.signerConfigurator.signerType === 'walletlink') { this.popupCommunicator.walletLinkQrScanned(); } this.updateListener.onConnect(); @@ -164,47 +80,14 @@ export class SignRequestHandler implements RequestHandler { return Promise.reject(standardErrors.rpc.internal('Failed to get accounts')); } catch (err) { - if (this.connectionType === 'walletlink') { + if (this.signerConfigurator.signerType === 'walletlink') { this.popupCommunicator.disconnect(); - this.onDisconnect(); + await this.onDisconnect(); } throw err; } } - private getWalletLinkUrl() { - this.initWalletLinkSigner(); - if (!(this.signer instanceof WLSigner)) { - throw standardErrors.rpc.internal( - 'Signer not initialized or Signer.getWalletLinkUrl not defined' - ); - } - return this.signer.getQRCodeUrl(); - } - - private async completeConnectionTypeSelection() { - await this.popupCommunicator.connect(); - - return new Promise((resolve) => { - this.connectionTypeSelectionResolver = resolve.bind(this); - - this.popupCommunicator - .selectConnectionType({ - smartWalletOnly: this.smartWalletOnly, - }) - .then((connectionType) => { - resolve(connectionType); - }); - }); - } - - // storage methods - private setConnectionType(connectionType: string) { - if (this.connectionType === connectionType) return; - this.connectionType = connectionType; - this.signerTypeStorage.setItem(SIGNER_TYPE_KEY, this.connectionType); - } - canHandleRequest(request: RequestArguments): boolean { const methodsThatRequireSigning = [ 'eth_requestAccounts', @@ -226,4 +109,8 @@ export class SignRequestHandler implements RequestHandler { return methodsThatRequireSigning.includes(request.method); } + + async onDisconnect() { + await this.signerConfigurator.onDisconnect(); + } } diff --git a/packages/wallet-sdk/src/sign/SignerConfigurator.ts b/packages/wallet-sdk/src/sign/SignerConfigurator.ts new file mode 100644 index 0000000000..4c14298b3b --- /dev/null +++ b/packages/wallet-sdk/src/sign/SignerConfigurator.ts @@ -0,0 +1,146 @@ +import { SCWSigner } from './scw/SCWSigner'; +import { PopUpCommunicator } from './scw/transport/PopUpCommunicator'; +import { Signer, SignerUpdateListener } from './SignerInterface'; +import { SignRequestHandlerListener } from './UpdateListenerInterface'; +import { WLSigner } from './walletlink/WLSigner'; +import { standardErrors } from ':core/error'; +import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage'; + +const SIGNER_TYPE_KEY = 'SignerType'; + +interface SignerConfiguratorOptions { + appName: string; + appLogoUrl?: string | null; + appChainIds: number[]; + smartWalletOnly: boolean; + updateListener: SignRequestHandlerListener; + popupCommunicator: PopUpCommunicator; +} + +export class SignerConfigurator { + private appName: string; + private appLogoUrl: string | null; + private appChainIds: number[]; + private smartWalletOnly: boolean; + + private popupCommunicator: PopUpCommunicator; + private updateListener: SignRequestHandlerListener; + + private signerTypeStorage = new ScopedLocalStorage('CBWSDK', 'SignerConfigurator'); + private signerTypeSelectionResolver: ((signerType: string) => void) | undefined; + + signerType: string | null; + signer: Signer | undefined; + + constructor(options: Readonly) { + this.popupCommunicator = options.popupCommunicator; + this.updateListener = options.updateListener; + + const persistedSignerType = this.signerTypeStorage.getItem(SIGNER_TYPE_KEY); + this.signerType = persistedSignerType; + if (persistedSignerType) { + this.initSigner(); + } + + this.appName = options.appName; + this.appLogoUrl = options.appLogoUrl ?? null; + this.appChainIds = options.appChainIds; + this.smartWalletOnly = options.smartWalletOnly; + + // getWalletLinkQRCodeUrl is called by the PopUpCommunicator when + // it receives message.type === 'wlQRCodeUrl' from the cb-wallet-scw popup + // its injected because we don't want to instantiate WalletLinkSigner until we have to + this.getWalletLinkQRCodeUrl = this.getWalletLinkQRCodeUrl.bind(this); + this.popupCommunicator.setGetWalletLinkQRCodeUrlCallback(this.getWalletLinkQRCodeUrl); + + this.setSignerType = this.setSignerType.bind(this); + this.initWalletLinkSigner = this.initWalletLinkSigner.bind(this); + } + + private readonly updateRelay: SignerUpdateListener = { + onAccountsUpdate: (signer, ...rest) => { + if (this.signer && signer !== this.signer) return; // ignore events from inactive signers + this.updateListener.onAccountsUpdate(...rest); + }, + onChainUpdate: (signer, ...rest) => { + if (this.signer && signer !== this.signer) return; // ignore events from inactive signers + if (signer instanceof WLSigner) { + this.signerTypeSelectionResolver?.('walletlink'); + } + this.updateListener.onChainUpdate(...rest); + }, + }; + + initSigner = () => { + if (this.signerType === 'scw') { + this.initScwSigner(); + } else if (this.signerType === 'walletlink') { + this.initWalletLinkSigner(); + } + }; + + private initScwSigner() { + if (this.signer instanceof SCWSigner) return; + + this.signer = new SCWSigner({ + appName: this.appName, + appLogoUrl: this.appLogoUrl, + appChainIds: this.appChainIds, + puc: this.popupCommunicator, + updateListener: this.updateRelay, + }); + } + + private initWalletLinkSigner() { + if (this.signer instanceof WLSigner) return; + + this.signer = new WLSigner({ + appName: this.appName, + appLogoUrl: this.appLogoUrl, + updateListener: this.updateRelay, + }); + } + + async onDisconnect() { + this.signerType = null; + this.signerTypeStorage.removeItem(SIGNER_TYPE_KEY); + await this.signer?.disconnect(); + this.signer = undefined; + } + + getWalletLinkQRCodeUrl() { + this.initWalletLinkSigner(); + if (!(this.signer instanceof WLSigner)) { + throw standardErrors.rpc.internal( + 'Signer not initialized or Signer.getWalletLinkUrl not defined' + ); + } + return this.signer.getQRCodeUrl(); + } + + async completeSignerTypeSelection() { + await this.popupCommunicator.connect(); + + return new Promise((resolve) => { + this.signerTypeSelectionResolver = (signerType: string) => { + this.setSignerType(signerType); + resolve(signerType); + }; + + this.popupCommunicator + .selectSignerType({ + smartWalletOnly: this.smartWalletOnly, + }) + .then((signerType) => { + this.signerTypeSelectionResolver?.(signerType); + }); + }); + } + + // storage methods + private setSignerType(signerType: string) { + if (this.signerType === signerType) return; + this.signerType = signerType; + this.signerTypeStorage.setItem(SIGNER_TYPE_KEY, this.signerType); + } +} diff --git a/packages/wallet-sdk/src/sign/scw/transport/ConfigMessage.ts b/packages/wallet-sdk/src/sign/scw/transport/ConfigMessage.ts index 1fc420146f..b2c608ac46 100644 --- a/packages/wallet-sdk/src/sign/scw/transport/ConfigMessage.ts +++ b/packages/wallet-sdk/src/sign/scw/transport/ConfigMessage.ts @@ -23,7 +23,7 @@ export enum HostConfigEventType { PopupUnload = 'popupUnload', } -export type ConnectionType = 'scw' | 'walletlink' | 'extension'; +export type SignerType = 'scw' | 'walletlink'; export function isConfigMessage(msg: Message): msg is ConfigMessage { return msg.type === 'config' && 'event' in msg; diff --git a/packages/wallet-sdk/src/sign/scw/transport/PopUpCommunicator.ts b/packages/wallet-sdk/src/sign/scw/transport/PopUpCommunicator.ts index 5f5da3f7d3..68ed2fd9aa 100644 --- a/packages/wallet-sdk/src/sign/scw/transport/PopUpCommunicator.ts +++ b/packages/wallet-sdk/src/sign/scw/transport/PopUpCommunicator.ts @@ -1,12 +1,7 @@ import { UUID } from 'crypto'; -import { - ClientConfigEventType, - ConfigMessage, - ConnectionType, - HostConfigEventType, - isConfigMessage, -} from './ConfigMessage'; +import { ClientConfigEventType, isConfigMessage, SignerType } from './ConfigMessage'; +import { PopUpConfigurator } from './PopUpConfigurator'; import { CrossDomainCommunicator } from ':core/communicator/CrossDomainCommunicator'; import { Message } from ':core/communicator/Message'; import { standardErrors } from ':core/error'; @@ -23,58 +18,34 @@ type Fulfillment = { export class PopUpCommunicator extends CrossDomainCommunicator { private requestMap = new Map(); - // TODO: let's revisit this when we migrate all this to ConnectionConfigurator. - private wlQRCodeUrlCallback?: () => string; + private popUpConfigurator: PopUpConfigurator; constructor({ url }: { url: string }) { super(); this.url = new URL(url); - } - - // should be set before calling .connect() - setWLQRCodeUrlCallback(callback: () => string) { - this.wlQRCodeUrlCallback = callback; + this.popUpConfigurator = new PopUpConfigurator({ communicator: this }); } protected onConnect(): Promise { - return new Promise((resolve, reject) => { - this.resolvePopupReady = resolve; + return new Promise((resolve) => { + this.popUpConfigurator.resolvePopupConnection = () => { + this.connected = true; + resolve(); + }; this.openFixedSizePopUpWindow(); - - if (!this.peerWindow) { - reject(standardErrors.rpc.internal('No pop up window opened')); - } }); } - private respondToWlQRCodeUrlRequest() { - if (!this.wlQRCodeUrlCallback) { - throw standardErrors.rpc.internal( - 'PopUpCommunicator.wlQRCodeUrlCallback not set! make sure .setWLQRCodeUrlCallback is called first' - ); - } - const wlQRCodeUrl = this.wlQRCodeUrlCallback(); - const configMessage: ConfigMessage = { - type: 'config', - id: crypto.randomUUID(), - event: { - type: ClientConfigEventType.WalletLinkUrl, - value: wlQRCodeUrl, - }, - }; - this.postMessage(configMessage); - } - protected onEvent(event: MessageEvent) { if (event.origin !== this.url?.origin) return; const message = event.data; if (isConfigMessage(message)) { - this.handleConfigMessage(message); + this.popUpConfigurator.handleConfigMessage(message); return; } - if (!this._connected) return; + if (!this.connected) return; if (!('requestId' in message)) return; const requestId = message.requestId as UUID; @@ -83,92 +54,35 @@ export class PopUpCommunicator extends CrossDomainCommunicator { resolveFunction?.(message); } - // TODO: move to ConnectionConfigurator - private resolvePopupReady?: () => void; - private resolveConnectionType?: (_: ConnectionType) => void; - - private handleConfigMessage(message: ConfigMessage) { - switch (message.event.type) { - case HostConfigEventType.PopupListenerAdded: - // Handshake Step 2: After receiving POPUP_LISTENER_ADDED_MESSAGE from Dapp, - // Dapp sends DAPP_ORIGIN_MESSAGE to FE to help FE confirm the origin of the Dapp - this.postClientConfigMessage(ClientConfigEventType.DappOriginMessage); - break; - case HostConfigEventType.PopupReadyForRequest: - // Handshake Step 4: After receiving POPUP_READY_MESSAGE from Dapp, FE knows that - // Dapp is ready to receive requests, handshake is done - this._connected = true; - this.resolvePopupReady?.(); - this.resolvePopupReady = undefined; - break; - case HostConfigEventType.ConnectionTypeSelected: - if (!this._connected) return; - this.resolveConnectionType?.(message.event.value as ConnectionType); - this.resolveConnectionType = undefined; - break; - case HostConfigEventType.RequestWalletLinkUrl: - if (!this._connected) return; - if (!this.wlQRCodeUrlCallback) { - throw standardErrors.rpc.internal( - 'PopUpCommunicator.wlQRCodeUrlCallback not set! should never happen' - ); - } - this.respondToWlQRCodeUrlRequest(); - break; - case HostConfigEventType.PopupUnload: - this.disconnect(); - break; - } + protected onDisconnect() { + this.connected = false; + this.closeChildWindow(); + this.requestMap.forEach((fulfillment, uuid, map) => { + fulfillment.reject(standardErrors.provider.userRejectedRequest('Request rejected')); + map.delete(uuid); + }); + this.popUpConfigurator.onDisconnect(); } - selectConnectionType({ smartWalletOnly }: { smartWalletOnly: boolean }): Promise { - return new Promise((resolve, reject) => { - if (!this.peerWindow) { - reject( - standardErrors.rpc.internal( - 'No pop up window found. Make sure to run .connect() before .send()' - ) - ); - } - - this.resolveConnectionType = resolve; - this.postClientConfigMessage(ClientConfigEventType.SelectConnectionType, { + setGetWalletLinkQRCodeUrlCallback(callback: () => string) { + this.popUpConfigurator.getWalletLinkQRCodeUrlCallback = callback; + } + + selectSignerType({ smartWalletOnly }: { smartWalletOnly: boolean }): Promise { + return new Promise((resolve) => { + this.popUpConfigurator.resolveSignerTypeSelection = resolve; + this.popUpConfigurator.postClientConfigMessage(ClientConfigEventType.SelectConnectionType, { smartWalletOnly, }); }); } walletLinkQrScanned() { - this.postClientConfigMessage(ClientConfigEventType.WalletLinkQrScanned); - } - - private postClientConfigMessage(type: ClientConfigEventType, options?: any) { - if (options && type !== ClientConfigEventType.SelectConnectionType) { - throw standardErrors.rpc.internal('ClientConfigEvent does not accept options'); - } - - const configMessage: ConfigMessage = { - type: 'config', - id: crypto.randomUUID(), - event: { - type, - value: options, - }, - }; - this.postMessage(configMessage); + this.popUpConfigurator.postClientConfigMessage(ClientConfigEventType.WalletLinkQrScanned); } - // Send message that expect to receive response request(message: Message): Promise { return new Promise((resolve, reject) => { - if (!this.peerWindow) { - reject( - standardErrors.rpc.internal( - 'No pop up window found. Make sure to run .connect() before .send()' - ) - ); - } - this.postMessage(message); const fulfillment: Fulfillment = { @@ -180,14 +94,7 @@ export class PopUpCommunicator extends CrossDomainCommunicator { }); } - protected onDisconnect() { - this._connected = false; - this.closeChildWindow(); - this.requestMap.forEach((fulfillment, uuid, map) => { - fulfillment.reject(standardErrors.provider.userRejectedRequest('Request rejected')); - map.delete(uuid); - }); - } + // Window Management private openFixedSizePopUpWindow() { const left = (window.innerWidth - POPUP_WIDTH) / 2 + window.screenX; @@ -202,14 +109,17 @@ export class PopUpCommunicator extends CrossDomainCommunicator { const popupUrl = new URL(this.url); popupUrl.search = urlParams.toString(); - const popupWindow = window.open( + this.peerWindow = window.open( popupUrl, 'SCW Child Window', `width=${POPUP_WIDTH}, height=${POPUP_HEIGHT}, left=${left}, top=${top}` ); - this.peerWindow = popupWindow; - popupWindow?.focus(); + this.peerWindow?.focus(); + + if (!this.peerWindow) { + throw standardErrors.rpc.internal('Pop up window failed to open'); + } } private closeChildWindow() { diff --git a/packages/wallet-sdk/src/sign/scw/transport/PopUpConfigurator.ts b/packages/wallet-sdk/src/sign/scw/transport/PopUpConfigurator.ts new file mode 100644 index 0000000000..d6eedda5ef --- /dev/null +++ b/packages/wallet-sdk/src/sign/scw/transport/PopUpConfigurator.ts @@ -0,0 +1,88 @@ +import { + ClientConfigEventType, + ConfigMessage, + HostConfigEventType, + SignerType, +} from './ConfigMessage'; +import { PopUpCommunicator } from './PopUpCommunicator'; +import { standardErrors } from ':core/error'; + +export class PopUpConfigurator { + private communicator: PopUpCommunicator; + + getWalletLinkQRCodeUrlCallback?: () => string; + resolvePopupConnection?: () => void; + resolveSignerTypeSelection?: (_: SignerType) => void; + + constructor({ communicator }: { communicator: PopUpCommunicator }) { + this.communicator = communicator; + } + + handleConfigMessage(message: ConfigMessage) { + switch (message.event.type) { + case HostConfigEventType.PopupListenerAdded: + // Handshake Step 2: After receiving POPUP_LISTENER_ADDED_MESSAGE from Dapp, + // Dapp sends DAPP_ORIGIN_MESSAGE to FE to help FE confirm the origin of the Dapp + this.postClientConfigMessage(ClientConfigEventType.DappOriginMessage); + break; + case HostConfigEventType.PopupReadyForRequest: + // Handshake Step 4: After receiving POPUP_READY_MESSAGE from Dapp, FE knows that + // Dapp is ready to receive requests, handshake is done + this.resolvePopupConnection?.(); + this.resolvePopupConnection = undefined; + break; + case HostConfigEventType.ConnectionTypeSelected: + if (!this.communicator.connected) return; + this.resolveSignerTypeSelection?.(message.event.value as SignerType); + this.resolveSignerTypeSelection = undefined; + break; + case HostConfigEventType.RequestWalletLinkUrl: + if (!this.communicator.connected) return; + if (!this.getWalletLinkQRCodeUrlCallback) { + throw standardErrors.rpc.internal('getWalletLinkQRCodeUrlCallback not set'); + } + this.respondToWlQRCodeUrlRequest(); + break; + case HostConfigEventType.PopupUnload: + this.communicator.disconnect(); + break; + } + } + + postClientConfigMessage(type: ClientConfigEventType, options?: any) { + if (options && type !== ClientConfigEventType.SelectConnectionType) { + throw standardErrors.rpc.internal('ClientConfigEvent does not accept options'); + } + + const configMessage: ConfigMessage = { + type: 'config', + id: crypto.randomUUID(), + event: { + type, + value: options, + }, + }; + this.communicator.postMessage(configMessage); + } + + onDisconnect() { + this.resolvePopupConnection = undefined; + this.resolveSignerTypeSelection = undefined; + } + + private respondToWlQRCodeUrlRequest() { + if (!this.getWalletLinkQRCodeUrlCallback) { + throw standardErrors.rpc.internal('PopUpCommunicator.getWalletLinkQRCodeUrlCallback not set'); + } + const walletLinkQRCodeUrl = this.getWalletLinkQRCodeUrlCallback(); + const configMessage: ConfigMessage = { + type: 'config', + id: crypto.randomUUID(), + event: { + type: ClientConfigEventType.WalletLinkUrl, + value: walletLinkQRCodeUrl, + }, + }; + this.communicator.postMessage(configMessage); + } +} diff --git a/packages/wallet-sdk/src/sign/walletlink/relay/WLRelayAdapter.ts b/packages/wallet-sdk/src/sign/walletlink/relay/WLRelayAdapter.ts index 5361978eb2..bf6d5792bc 100644 --- a/packages/wallet-sdk/src/sign/walletlink/relay/WLRelayAdapter.ts +++ b/packages/wallet-sdk/src/sign/walletlink/relay/WLRelayAdapter.ts @@ -230,6 +230,7 @@ export class WLRelayAdapter { public async close() { const relay = this.initializeRelay(); relay.resetAndReload(); + this._storage.clear(); } public async request(args: RequestArguments): Promise {