From 53e8e3b836c3127efc24c62358e5af22d5157980 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Mon, 22 Sep 2025 11:04:35 -0400 Subject: [PATCH 1/2] add logging for keyring issues --- src/components/connect.tsx | 30 ++++++- src/components/main.tsx | 166 ++++++++++++++++++++++++++++++++++--- src/sdk/sdkSession.ts | 50 ++++++++++- 3 files changed, 227 insertions(+), 19 deletions(-) diff --git a/src/components/connect.tsx b/src/components/connect.tsx index 2eb6a49..a481eaf 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -106,18 +106,40 @@ class Connect extends React.Component { } getKeyringDataFromURL = () => { + console.log(`[KEYRING] Checking URL for keyring parameters...`); + const urlParams = new URLSearchParams(window.location.search); const keyringName = urlParams.get("keyring"); + console.log(`[KEYRING] Keyring name from URL: ${keyringName}`); + if (keyringName) { - const keyringData = localStorage.getKeyringItem(keyringName); - const { deviceID, password } = keyringData; + console.log(`[KEYRING] Found keyring parameter, looking up stored data...`); - if (deviceID && password) { - return { deviceID, password }; + try { + const keyringData = localStorage.getKeyringItem(keyringName); + console.log(`[KEYRING] Retrieved keyring data:`, { + hasDeviceID: !!keyringData.deviceID, + hasPassword: !!keyringData.password, + lastLogin: keyringData.lastLogin + }); + + const { deviceID, password } = keyringData; + + if (deviceID && password) { + console.log(`[KEYRING] ✅ Valid keyring credentials found for: ${keyringName}`); + return { deviceID, password }; + } else { + console.warn(`[KEYRING] ⚠️ Incomplete keyring data - deviceID: ${!!deviceID}, password: ${!!password}`); + } + } catch (err) { + console.error(`[KEYRING] ❌ Error retrieving keyring data:`, err); } + } else { + console.log(`[KEYRING] No keyring parameter in URL`); } + console.log(`[KEYRING] No valid keyring data found, returning null`); return null; }; diff --git a/src/components/main.tsx b/src/components/main.tsx index b9603d3..1659e2b 100644 --- a/src/components/main.tsx +++ b/src/components/main.tsx @@ -59,6 +59,7 @@ type MainState = { walletIsExternal: boolean; keyringName: string; openedByKeyring: boolean; + keyringConnectionFailed: boolean; hwCheck: string; }; @@ -95,6 +96,8 @@ class Main extends React.Component { keyringName, // Was the app opened with a keyring in the url parameters openedByKeyring: !!keyringName, + // Track if keyring connection failed to prevent returning invalid data + keyringConnectionFailed: false, // Validation check on Lattice hardware. Should draw a separate component hwCheck: null, }; @@ -126,9 +129,9 @@ class Main extends React.Component { window.addEventListener("resize", this.updateWidth); if (this.isMobile()) this.setState({ collapsed: true }); - // Metamask connects through a keyring and in these cases we need + // Consuming libraries connect through a keyring and in these cases we need // to utilize window.postMessage once we connect. - // We can extend this pattern to other apps in the future. + // This pattern can be extended to other apps in the future. const params = new URLSearchParams(window.location.search); const keyringName = this.state.keyringName; const hwCheck = params.get("hwCheck"); @@ -229,37 +232,109 @@ class Main extends React.Component { } returnKeyringData() { - if (!this.state.openedByKeyring) return; + console.log(`[KEYRING_RETURN] Starting returnKeyringData process`); + console.log(`[KEYRING_RETURN] State check - openedByKeyring: ${this.state.openedByKeyring}, connectionFailed: ${this.state.keyringConnectionFailed}`); + + if (!this.state.openedByKeyring) { + console.warn(`[KEYRING_RETURN] ⚠️ Not opened by keyring, aborting return process`); + return; + } + + // CRITICAL: Do not return data if the connection failed + if (this.state.keyringConnectionFailed) { + console.error(`[KEYRING_RETURN] 🚫 BLOCKED: Connection failed - refusing to return data to consuming library`); + console.error(`[KEYRING_RETURN] This prevents consuming library from receiving invalid/stale connection data`); + return; + } + + console.log(`[KEYRING_RETURN] Saving login data for keyring: ${this.state.keyringName}`); + // Save the login for later - localStorage.setKeyringItem(this.state.keyringName, { + const keyringData = { deviceID: this.state.deviceID, password: this.state.password, lastLogin: new Date().getTime(), - }); + }; + + try { + localStorage.setKeyringItem(this.state.keyringName, keyringData); + console.log(`[KEYRING_RETURN] Successfully saved keyring data`); + } catch (err) { + console.error(`[KEYRING_RETURN] Failed to save keyring data:`, err); + } + // Send the data back to the opener const data = { deviceID: this.state.deviceID, password: this.state.password, endpoint: constants.BASE_SIGNING_URL, }; + + console.log(`[KEYRING_RETURN] Preparing response data with endpoint: ${data.endpoint}`); + // Check if there is a custom endpoint configured const settings = localStorage.getSettings(); if (settings.customEndpoint && settings.customEndpoint !== "") { + console.log(`[KEYRING_RETURN] Using custom endpoint: ${settings.customEndpoint}`); data.endpoint = settings.customEndpoint; } + + console.log(`[KEYRING_RETURN] Final response data prepared:`, { + deviceID: data.deviceID, + endpoint: data.endpoint, + hasPassword: !!data.password + }); + this.handleLogout(); + + // Check the window opener situation + console.log(`[KEYRING_RETURN] Window opener check - exists: ${!!window.opener}`); + if (window.opener) { // If there is a `window.opener` we can just post back - window.opener.postMessage(JSON.stringify(data), "*"); - window.close(); + console.log(`[KEYRING_RETURN] Using window.opener.postMessage method`); + + try { + const messageData = JSON.stringify(data); + console.log(`[KEYRING_RETURN] Sending postMessage with data length: ${messageData.length} characters`); + + window.opener.postMessage(messageData, "*"); + console.log(`[KEYRING_RETURN] ✅ PostMessage sent successfully, closing window...`); + + window.close(); + } catch (err) { + console.error(`[KEYRING_RETURN] ❌ Failed to send postMessage:`, err); + console.error(`[KEYRING_RETURN] Error details:`, { + message: err.message, + stack: err.stack, + dataSize: JSON.stringify(data).length + }); + } } else { // Otherwise we need a workaround to let the originator // know we have logged in. We will put the login data // into the URL and the requesting app will fetch that. // Note that the requesting extension is now responsible for // closing this web page. - const enc = Buffer.from(JSON.stringify(data)).toString("base64"); - window.location.href = `${window.location.href}&${LOGIN_PARAM}=${enc}`; + console.log(`[KEYRING_RETURN] No window.opener, using URL-based workaround`); + + try { + const enc = Buffer.from(JSON.stringify(data)).toString("base64"); + const newUrl = `${window.location.href}&${LOGIN_PARAM}=${enc}`; + + console.log(`[KEYRING_RETURN] Encoded data length: ${enc.length} characters`); + console.log(`[KEYRING_RETURN] Redirecting to URL with login parameter...`); + + window.location.href = newUrl; + console.log(`[KEYRING_RETURN] ✅ URL redirect initiated`); + } catch (err) { + console.error(`[KEYRING_RETURN] ❌ Failed to encode data or redirect:`, err); + console.error(`[KEYRING_RETURN] Error details:`, { + message: err.message, + stack: err.stack, + currentUrl: window.location.href + }); + } } } //------------------------------------------ @@ -325,8 +400,18 @@ class Main extends React.Component { // as we cannot connect. connectSession(data = this.state, showLoading = true) { const { deviceID, password } = data; + const sessionStartTime = Date.now(); + + console.log(`[KEYRING_CONNECTION] Starting connectSession for device: ${deviceID}`); + console.log(`[KEYRING_CONNECTION] Keyring details:`, { + keyringName: this.state.keyringName, + openedByKeyring: this.state.openedByKeyring, + showLoading + }); + // Sanity check -- this should never get hit if (!deviceID || !password) { + console.error(`[KEYRING_CONNECTION] Missing credentials - deviceID: ${!!deviceID}, password: ${!!password}`); //@ts-expect-error return this.setError({ msg: "You must provide a deviceID and password. Please refresh and log in again. ", @@ -334,35 +419,85 @@ class Main extends React.Component { } else { this.setError(); } + + console.log(`[KEYRING_CONNECTION] Credentials validated, proceeding with connection...`); + // Connect to the device this.connect(deviceID, password, () => { + console.log(`[KEYRING_CONNECTION] Local session setup completed, initiating SDK connection...`); + // Create a new session with the deviceID and password provided. if (showLoading === true) { this.wait("Looking for your Lattice", this.cancelConnect); } + this.context.session .connect(deviceID, password) .then((isPaired) => { + const sessionDuration = Date.now() - sessionStartTime; + console.log(`[KEYRING_CONNECTION] ✅ SDK connection successful after ${sessionDuration}ms`); + console.log(`[KEYRING_CONNECTION] Device paired status: ${isPaired}`); + this.unwait(); + // If the request was before we got our callback, exit here - if (!this.context.session || this.state.deviceID !== deviceID) return; + if (!this.context.session || this.state.deviceID !== deviceID) { + console.warn(`[KEYRING_CONNECTION] Session state mismatch detected, aborting. Session exists: ${!!this.context.session}, DeviceID match: ${this.state.deviceID === deviceID}`); + return; + } + + console.log(`[KEYRING_CONNECTION] Processing successful connection...`); + // We connected! // 1. Save these credentials to localStorage if this is NOT a keyring if (!this.state.openedByKeyring) { + console.log(`[KEYRING_CONNECTION] Saving login credentials to localStorage (non-keyring)`); localStorage.setLogin({ deviceID, password }); + } else { + console.log(`[KEYRING_CONNECTION] Skipping localStorage save (keyring mode)`); } + // 2. Clear errors and alerts this.setError(); + // 3. Proceed based on state if (isPaired && this.state.openedByKeyring) { + console.log(`[KEYRING_CONNECTION] Device paired and in keyring mode, returning data to consuming library...`); return this.returnKeyringData(); + } else { + console.log(`[KEYRING_CONNECTION] Connection completed. Paired: ${isPaired}, Keyring mode: ${this.state.openedByKeyring}`); } }) .catch((err) => { - localStorage.removeKeyringItem(this.state.keyringName); + const sessionDuration = Date.now() - sessionStartTime; + console.error(`[KEYRING_CONNECTION] ❌ SDK connection failed after ${sessionDuration}ms`); + console.error(`[KEYRING_CONNECTION] Error details:`, { + message: err.message, + stack: err.stack, + keyringName: this.state.keyringName, + openedByKeyring: this.state.openedByKeyring, + deviceID + }); + + // Mark keyring connection as failed if this was opened by a keyring + if (this.state.openedByKeyring) { + console.error(`[KEYRING_CONNECTION] 🚫 Marking keyring connection as failed - no data will be returned to consuming library`); + this.setState({ keyringConnectionFailed: true }); + } + + if (this.state.keyringName) { + console.log(`[KEYRING_CONNECTION] Cleaning up keyring data for: ${this.state.keyringName}`); + localStorage.removeKeyringItem(this.state.keyringName); + } + this.setError({ msg: err.message, cb: () => { + console.log(`[KEYRING_CONNECTION] Retrying connection...`); + // Reset the failure flag on retry + if (this.state.openedByKeyring) { + this.setState({ keyringConnectionFailed: false }); + } this.connectSession(data); }, }); @@ -499,6 +634,14 @@ class Main extends React.Component { } }) .catch((err) => { + console.error(`[KEYRING_PAIRING] ❌ Pairing failed:`, err); + + // Mark keyring connection as failed if this was opened by a keyring + if (this.state.openedByKeyring) { + console.error(`[KEYRING_PAIRING] 🚫 Marking keyring connection as failed due to pairing failure`); + this.setState({ keyringConnectionFailed: true }); + } + // If there was an error here, the user probably entered the wrong secret sendErrorNotification({ message: "Failed to pair", @@ -506,6 +649,7 @@ class Main extends React.Component { "You may have mistyped the pairing code, or this device is already paired and you entered the incorrect password. Please try again.", duration: null, // Don't auto-dismiss }); + this.setState({ deviceID: null, password: null }); this.context.session.disconnect(); localStorage.removeLogin(); diff --git a/src/sdk/sdkSession.ts b/src/sdk/sdkSession.ts index 32e390c..4006fd9 100644 --- a/src/sdk/sdkSession.ts +++ b/src/sdk/sdkSession.ts @@ -224,22 +224,34 @@ class SDKSession { } _tryConnect(deviceID, pw, cb, _triedLocal = false) { + const connectionMethod = _triedLocal === false ? 'LAN' : 'cloud'; + const startTime = Date.now(); + + console.log(`[LATTICE_CONNECTION] Starting connection attempt to device ${deviceID} via ${connectionMethod}`); + console.log(`[LATTICE_CONNECTION] App name: ${this.name}`); + let baseUrl = this.baseUrl; let tmpTimeout = constants.SHORT_TIMEOUT; // Artificially short timeout just for connecting if (_triedLocal === false) { baseUrl = `http://lattice-${deviceID}.local:8080`; tmpTimeout = 5000; // Shorten the timeout even more since we should discover quickly if device is on LAN } + + console.log(`[LATTICE_CONNECTION] Using baseUrl: ${baseUrl}, timeout: ${tmpTimeout}ms`); + // Derive a keypair from the deviceID and password // This key doesn't hold any coins and only allows this app to make // requests to a particular device. Nevertheless, the user should // enter a reasonably strong password to prevent unwanted requests // from nefarious actors. const key = this._genPrivKey(deviceID, pw, this.name); + console.log(`[LATTICE_CONNECTION] Generated private key for device authentication`); + // If no client exists in this session, create a new one and // attach it. let client; try { + console.log(`[LATTICE_CONNECTION] Creating new Client instance...`); client = new Client({ name: this.name, privKey: key, @@ -247,31 +259,61 @@ class SDKSession { timeout: tmpTimeout, // Artificially short timeout for simply locating the Lattice skipRetryOnWrongWallet: false, }); + console.log(`[LATTICE_CONNECTION] Client created successfully`); } catch (err) { - return cb ? cb(err.toString()) : err.toString(); + const errorMsg = err.toString(); + console.error(`[LATTICE_CONNECTION] Failed to create Client: ${errorMsg}`); + console.error(`[LATTICE_CONNECTION] Client creation error details:`, err); + return cb ? cb(errorMsg) : errorMsg; } + console.log(`[LATTICE_CONNECTION] Initiating client.connect() to device ${deviceID}...`); + const connectStartTime = Date.now(); + return client .connect(deviceID) .then((isPaired) => { + const connectDuration = Date.now() - connectStartTime; + const totalDuration = Date.now() - startTime; + + console.log(`[LATTICE_CONNECTION] ✅ Connection successful via ${connectionMethod}`); + console.log(`[LATTICE_CONNECTION] Connect duration: ${connectDuration}ms, total duration: ${totalDuration}ms`); + console.log(`[LATTICE_CONNECTION] Device paired status: ${isPaired}`); + // Update the timeout to a longer one for future async requests client.timeout = constants.ASYNC_SDK_TIMEOUT; + console.log(`[LATTICE_CONNECTION] Updated client timeout to ${constants.ASYNC_SDK_TIMEOUT}ms for future requests`); + this.client = client; // Setup a new storage session if these are new credentials. // (This call will be bypassed if the credentials are already saved // in localStorage because getBtcWalletData is also called in the constructor) this.deviceID = deviceID; this.getBtcWalletData(); + + console.log(`[LATTICE_CONNECTION] Connection setup completed successfully`); return cb ? cb(null, isPaired) : isPaired; }) .catch((err) => { + const connectDuration = Date.now() - connectStartTime; + const totalDuration = Date.now() - startTime; + + console.error(`[LATTICE_CONNECTION] ❌ Connection failed via ${connectionMethod} after ${connectDuration}ms`); + console.error(`[LATTICE_CONNECTION] Error details:`, err); + if (err) { if (_triedLocal === false) { - console.warn( - "Failed to connect to Lattice over LAN. Falling back to cloud routing." - ); + console.warn(`[LATTICE_CONNECTION] Failed to connect to Lattice over LAN after ${totalDuration}ms. Falling back to cloud routing.`); return this._tryConnect(deviceID, pw, cb, true); } else { + console.error(`[LATTICE_CONNECTION] Cloud connection also failed. Total time elapsed: ${totalDuration}ms`); + console.error(`[LATTICE_CONNECTION] Final error details:`, { + message: err.message || err, + stack: err.stack, + deviceID, + baseUrl, + timeout: tmpTimeout + }); throw err; } } else if (_triedLocal === false) { From 58dcc6ab1cd5225d861da8c8e530e00e3d3c6bd3 Mon Sep 17 00:00:00 2001 From: netbonus <151201453+netbonus@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:08:07 -0400 Subject: [PATCH 2/2] improve log --- src/components/main.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/main.tsx b/src/components/main.tsx index 1659e2b..5bd469c 100644 --- a/src/components/main.tsx +++ b/src/components/main.tsx @@ -442,7 +442,12 @@ class Main extends React.Component { // If the request was before we got our callback, exit here if (!this.context.session || this.state.deviceID !== deviceID) { - console.warn(`[KEYRING_CONNECTION] Session state mismatch detected, aborting. Session exists: ${!!this.context.session}, DeviceID match: ${this.state.deviceID === deviceID}`); + console.warn(`[KEYRING_CONNECTION] Session state mismatch detected, aborting:`, { + sessionExists: !!this.context.session, + deviceIdMatch: this.state.deviceID === deviceID, + currentDeviceId: this.state.deviceID, + callbackDeviceId: deviceID + }); return; }