diff --git a/src/browser/BrowserFunc.ts b/src/browser/BrowserFunc.ts index ef86609..c772457 100644 --- a/src/browser/BrowserFunc.ts +++ b/src/browser/BrowserFunc.ts @@ -9,6 +9,7 @@ import { Counters, DashboardData, MorePromotion, PromotionalItem } from '../inte import { QuizData } from '../interface/QuizData' import { AppUserData } from '../interface/AppUserData' import { EarnablePoints } from '../interface/Points' +import { logError } from '../util/Logger' export default class BrowserFunc { @@ -148,7 +149,7 @@ export default class BrowserFunc { // Force a navigation retry once before failing hard await this.goHome(target) - await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(() => {}) + await target.waitForLoadState('domcontentloaded', { timeout: TIMEOUTS.VERY_LONG }).catch(logError('BROWSER-FUNC', 'Dashboard recovery load failed', this.bot.isMobile)) await this.bot.utils.wait(this.bot.isMobile ? TIMEOUTS.LONG : TIMEOUTS.MEDIUM) scriptContent = await this.extractDashboardScript(target) diff --git a/src/browser/BrowserUtil.ts b/src/browser/BrowserUtil.ts index 7944b19..48ed8d8 100644 --- a/src/browser/BrowserUtil.ts +++ b/src/browser/BrowserUtil.ts @@ -1,6 +1,7 @@ import { Page } from 'rebrowser-playwright' import { load } from 'cheerio' import { MicrosoftRewardsBot } from '../index' +import { logError } from '../util/Logger' type DismissButton = { selector: string; label: string; isXPath?: boolean } @@ -80,7 +81,7 @@ export default class BrowserUtil { const visible = await loc.first().isVisible({ timeout: 200 }).catch(() => false) if (!visible) return false - await loc.first().click({ timeout: 500 }).catch(() => {}) + await loc.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', `Failed to click ${btn.label}`, this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', `Dismissed: ${btn.label}`) return true } catch { @@ -97,14 +98,14 @@ export default class BrowserUtil { const rejectBtn = overlay.locator(reject) if (await rejectBtn.first().isVisible().catch(() => false)) { - await rejectBtn.first().click({ timeout: 500 }).catch(() => {}) + await rejectBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay reject click failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Reject') return 1 } const acceptBtn = overlay.locator(accept) if (await acceptBtn.first().isVisible().catch(() => false)) { - await acceptBtn.first().click({ timeout: 500 }).catch(() => {}) + await acceptBtn.first().click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Overlay accept click failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Overlay Accept') return 1 } @@ -124,12 +125,12 @@ export default class BrowserUtil { const closeBtn = dialog.locator(closeButtons).first() if (await closeBtn.isVisible({ timeout: 200 }).catch(() => false)) { - await closeBtn.click({ timeout: 500 }).catch(() => {}) + await closeBtn.click({ timeout: 500 }).catch(logError('BROWSER-UTIL', 'Streak dialog close failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Button') return 1 } - await page.keyboard.press('Escape').catch(() => {}) + await page.keyboard.press('Escape').catch(logError('BROWSER-UTIL', 'Streak dialog Escape failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Streak Protection Dialog Escape') return 1 } catch { @@ -153,7 +154,7 @@ export default class BrowserUtil { // Click the Next button const nextBtn = page.locator(nextButton).first() if (await nextBtn.isVisible({ timeout: 500 }).catch(() => false)) { - await nextBtn.click({ timeout: 1000 }).catch(() => {}) + await nextBtn.click({ timeout: 1000 }).catch(logError('BROWSER-UTIL', 'Terms update next button click failed', this.bot.isMobile)) this.bot.log(this.bot.isMobile, 'DISMISS-ALL-MESSAGES', 'Dismissed: Terms Update Dialog (Next)') // Wait a bit for navigation await page.waitForTimeout(1000) diff --git a/src/functions/Login.ts b/src/functions/Login.ts index 624b813..de0ed42 100644 --- a/src/functions/Login.ts +++ b/src/functions/Login.ts @@ -9,6 +9,7 @@ import { MicrosoftRewardsBot } from '../index' import { OAuth } from '../interface/OAuth' import { Retry } from '../util/Retry' import { LoginState, LoginStateDetector } from '../util/LoginStateDetector' +import { logError } from '../util/Logger' // ------------------------------- // Constants / Tunables @@ -243,7 +244,7 @@ export class Login { const homeUrl = 'https://rewards.bing.com/' try { await page.goto(homeUrl) - await page.waitForLoadState('domcontentloaded').catch(()=>{}) + await page.waitForLoadState('domcontentloaded').catch(logError('LOGIN', 'DOMContentLoaded timeout', this.bot.isMobile)) await this.bot.browser.utils.reloadBadPage(page) await this.bot.utils.wait(250) @@ -384,7 +385,10 @@ export class Login { this.bot.log(this.bot.isMobile, 'LOGIN', 'Email prefilled') } const next = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null) - if (next) { await next.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') } + if (next) { + await next.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Email submit click failed: ${e}`, 'warn')) + this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted email') + } } private async inputPasswordOr2FA(page: Page, password: string) { @@ -394,7 +398,10 @@ export class Login { // Some flows require switching to password first const switchBtn = await page.waitForSelector('#idA_PWD_SwitchToPassword', { timeout: 1500 }).catch(()=>null) - if (switchBtn) { await switchBtn.click().catch(()=>{}); await this.bot.utils.wait(1000) } + if (switchBtn) { + await switchBtn.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Switch to password failed: ${e}`, 'warn')) + await this.bot.utils.wait(1000) + } // Early TOTP check - if totpSecret is configured, check for TOTP challenge before password if (this.currentTotpSecret) { @@ -429,7 +436,10 @@ export class Login { await page.fill(SELECTORS.passwordInput, '') await page.fill(SELECTORS.passwordInput, password) const submit = await page.waitForSelector(SELECTORS.submitBtn, { timeout: 2000 }).catch(()=>null) - if (submit) { await submit.click().catch(()=>{}); this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') } + if (submit) { + await submit.click().catch(e => this.bot.log(this.bot.isMobile, 'LOGIN', `Password submit failed: ${e}`, 'warn')) + this.bot.log(this.bot.isMobile, 'LOGIN', 'Password submitted') + } } // --------------- 2FA Handling --------------- @@ -462,10 +472,10 @@ export class Login { const resend = await page.waitForSelector('button[aria-describedby="pushNotificationsTitle errorDescription"]', { timeout: 1500 }).catch(()=>null) if (!resend) break await this.bot.utils.wait(60000) - await resend.click().catch(()=>{}) + await resend.click().catch(logError('LOGIN', 'Resend click failed', this.bot.isMobile)) } } - await page.click('button[aria-describedby="confirmSendTitle"]').catch(()=>{}) + await page.click('button[aria-describedby="confirmSendTitle"]').catch(logError('LOGIN', 'Confirm send click failed', this.bot.isMobile)) await this.bot.utils.wait(1500) try { const el = await page.waitForSelector('#displaySign, div[data-testid="displaySign"]>span', { timeout: 2000 }) @@ -484,7 +494,7 @@ export class Login { } catch { this.bot.log(this.bot.isMobile, 'LOGIN', 'Authenticator code expired – refreshing') const retryBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 3000 }).catch(()=>null) - if (retryBtn) await retryBtn.click().catch(()=>{}) + if (retryBtn) await retryBtn.click().catch(logError('LOGIN-AUTH', 'Refresh button click failed', this.bot.isMobile)) const refreshed = await this.fetchAuthenticatorNumber(page) if (!refreshed) { this.bot.log(this.bot.isMobile, 'LOGIN', 'Could not refresh authenticator code', 'warn'); return } numberToPress = refreshed @@ -584,9 +594,9 @@ export class Login { // Use unified selector system const submit = await this.findFirstVisibleLocator(page, Login.TOTP_SELECTORS.submit) if (submit) { - await submit.click().catch(()=>{}) + await submit.click().catch(logError('LOGIN-TOTP', 'Auto-submit click failed', this.bot.isMobile)) } else { - await page.keyboard.press('Enter').catch(()=>{}) + await page.keyboard.press('Enter').catch(logError('LOGIN-TOTP', 'Auto-submit Enter failed', this.bot.isMobile)) } this.bot.log(this.bot.isMobile, 'LOGIN', 'Submitted TOTP automatically') } catch (error) { @@ -747,7 +757,7 @@ export class Login { for (const sel of selectors) { const loc = page.locator(sel).first() if (await loc.isVisible().catch(() => false)) { - await loc.click().catch(()=>{}) + await loc.click().catch(logError('LOGIN', `Click failed for selector: ${sel}`, this.bot.isMobile)) return true } } @@ -1017,7 +1027,7 @@ export class Login { const text = (await skipBtn.textContent() || '').trim() // Check if it's actually a skip button (could be other secondary buttons) if (/skip|later|not now|non merci|pas maintenant/i.test(text)) { - await skipBtn.click().catch(()=>{}) + await skipBtn.click().catch(logError('LOGIN-PASSKEY', 'Skip button click failed', this.bot.isMobile)) did = true this.logPasskeyOnce('data-testid secondaryButton') } @@ -1028,7 +1038,11 @@ export class Login { const biometric = await page.waitForSelector(SELECTORS.biometricVideo, { timeout: 500 }).catch(()=>null) if (biometric) { const btn = await page.$(SELECTORS.passkeySecondary) - if (btn) { await btn.click().catch(()=>{}); did = true; this.logPasskeyOnce('video heuristic') } + if (btn) { + await btn.click().catch(logError('LOGIN-PASSKEY', 'Video heuristic click failed', this.bot.isMobile)) + did = true + this.logPasskeyOnce('video heuristic') + } } } @@ -1039,11 +1053,17 @@ export class Login { const primBtn = await page.waitForSelector(SELECTORS.passkeyPrimary, { timeout: 500 }).catch(()=>null) const title = (titleEl ? (await titleEl.textContent()) : '')?.trim() || '' const looksLike = /sign in faster|passkey|fingerprint|face|pin|empreinte|visage|windows hello|hello/i.test(title) - if (looksLike && secBtn) { await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('title heuristic '+title) } + if (looksLike && secBtn) { + await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Title heuristic click failed', this.bot.isMobile)) + did = true + this.logPasskeyOnce('title heuristic '+title) + } else if (!did && secBtn && primBtn) { const text = (await secBtn.textContent()||'').trim() if (/skip for now|not now|later|passer|plus tard/i.test(text)) { - await secBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('secondary button text') + await secBtn.click().catch(logError('LOGIN-PASSKEY', 'Secondary button text click failed', this.bot.isMobile)) + did = true + this.logPasskeyOnce('secondary button text') } } } @@ -1051,7 +1071,11 @@ export class Login { // Priority 4: XPath fallback (includes Windows Hello specific patterns) if (!did) { const textBtn = await page.locator('xpath=//button[contains(normalize-space(.),"Skip for now") or contains(normalize-space(.),"Not now") or contains(normalize-space(.),"Passer") or contains(normalize-space(.),"No thanks")]').first() - if (await textBtn.isVisible().catch(()=>false)) { await textBtn.click().catch(()=>{}); did = true; this.logPasskeyOnce('xpath fallback') } + if (await textBtn.isVisible().catch(()=>false)) { + await textBtn.click().catch(logError('LOGIN-PASSKEY', 'XPath fallback click failed', this.bot.isMobile)) + did = true + this.logPasskeyOnce('xpath fallback') + } } // Priority 4.5: Windows Hello specific detection @@ -1070,7 +1094,7 @@ export class Login { for (const pattern of skipPatterns) { const btn = await page.locator(pattern).first() if (await btn.isVisible().catch(() => false)) { - await btn.click().catch(() => {}) + await btn.click().catch(logError('LOGIN-PASSKEY', 'Windows Hello skip failed', this.bot.isMobile)) did = true this.logPasskeyOnce('Windows Hello skip') break @@ -1082,14 +1106,22 @@ export class Login { // Priority 5: Close button fallback if (!did) { const close = await page.$('#close-button') - if (close) { await close.click().catch(()=>{}); did = true; this.logPasskeyOnce('close button') } + if (close) { + await close.click().catch(logError('LOGIN-PASSKEY', 'Close button fallback failed', this.bot.isMobile)) + did = true + this.logPasskeyOnce('close button') + } } // KMSI prompt const kmsi = await page.waitForSelector(SELECTORS.kmsiVideo, { timeout: 400 }).catch(()=>null) if (kmsi) { const yes = await page.$(SELECTORS.passkeyPrimary) - if (yes) { await yes.click().catch(()=>{}); did = true; this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') } + if (yes) { + await yes.click().catch(logError('LOGIN-KMSI', 'KMSI accept click failed', this.bot.isMobile)) + did = true + this.bot.log(this.bot.isMobile,'LOGIN-KMSI','Accepted KMSI prompt') + } } if (!did && context === 'main') { @@ -1140,9 +1172,9 @@ export class Login { this.bot.compromisedModeActive = true this.bot.compromisedReason = 'sign-in-blocked' this.startCompromisedInterval() - await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(()=>{}) + await this.bot.engageGlobalStandby('sign-in-blocked', email).catch(logError('LOGIN-SECURITY', 'Global standby engagement failed', this.bot.isMobile)) // Open security docs for immediate guidance (best-effort) - await this.openDocsTab(page, docsUrl).catch(()=>{}) + await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-SECURITY', 'Failed to open docs tab', this.bot.isMobile)) return true } catch { return false } } @@ -1250,8 +1282,8 @@ export class Login { this.bot.compromisedModeActive = true this.bot.compromisedReason = 'recovery-mismatch' this.startCompromisedInterval() - await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(()=>{}) - await this.openDocsTab(page, docsUrl).catch(()=>{}) + await this.bot.engageGlobalStandby('recovery-mismatch', email).catch(logError('LOGIN-RECOVERY', 'Global standby failed', this.bot.isMobile)) + await this.openDocsTab(page, docsUrl).catch(logError('LOGIN-RECOVERY', 'Failed to open docs tab', this.bot.isMobile)) } else { const mode = observedPrefix.length === 1 ? 'lenient' : 'strict' this.bot.log(this.bot.isMobile,'LOGIN-RECOVERY',`Recovery OK (${mode}): ${extracted} matches ${matchRef.prefix2}**@${matchRef.domain}`) @@ -1263,7 +1295,7 @@ export class Login { try { const link = await page.locator('xpath=//span[@role="button" and (contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"use your password") or contains(translate(normalize-space(.),"ABCDEFGHIJKLMNOPQRSTUVWXYZ","abcdefghijklmnopqrstuvwxyz"),"utilisez votre mot de passe"))]').first() if (await link.isVisible().catch(()=>false)) { - await link.click().catch(()=>{}) + await link.click().catch(logError('LOGIN', 'Use password link click failed', this.bot.isMobile)) await this.bot.utils.wait(800) this.bot.log(this.bot.isMobile,'LOGIN','Clicked "Use your password" link') } @@ -1335,6 +1367,6 @@ export class Login { body.isFidoSupported = false route.continue({ postData: JSON.stringify(body) }) } catch { route.continue() } - }).catch(()=>{}) + }).catch(logError('LOGIN-FIDO', 'Route interception setup failed', this.bot.isMobile)) } } diff --git a/src/functions/Workers.ts b/src/functions/Workers.ts index dce02e9..14a6874 100644 --- a/src/functions/Workers.ts +++ b/src/functions/Workers.ts @@ -6,6 +6,7 @@ import { MicrosoftRewardsBot } from '../index' import JobState from '../util/JobState' import { Retry } from '../util/Retry' import { AdaptiveThrottler } from '../util/AdaptiveThrottler' +import { logError } from '../util/Logger' export class Workers { public bot: MicrosoftRewardsBot @@ -204,7 +205,7 @@ export class Workers { } private async prepareActivityPage(page: Page, selector: string, throttle: AdaptiveThrottler): Promise { - await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {}) + await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(logError('WORKERS', 'Network idle wait failed', this.bot.isMobile)) await this.bot.browser.utils.humanizePage(page) await this.applyThrottle(throttle, 1200, 2600) } diff --git a/src/index.ts b/src/index.ts index 743ab26..54bb08b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -120,10 +120,16 @@ export class MicrosoftRewardsBot { // Run comprehensive startup validation const validator = new StartupValidator() - await validator.validate(this.config, this.accounts) + try { + await validator.validate(this.config, this.accounts) + } catch (error) { + // Critical validation errors prevent startup + const errorMsg = error instanceof Error ? error.message : String(error) + log('main', 'VALIDATION', `Fatal validation error: ${errorMsg}`, 'error') + throw error // Re-throw to stop execution + } - // Always continue - validation is informative, not blocking - // This allows users to proceed even with warnings or minor issues + // Validation passed - continue with initialization // Initialize job state if (this.config.jobState?.enabled !== false) { diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 14f8609..8367e58 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -7,6 +7,18 @@ import { DISCORD } from '../constants' const WEBHOOK_USERNAME = 'MS Rewards - Live Logs' +/** + * Safe error logger for catch blocks + * Use in .catch() to log errors without breaking flow + * @example await action().catch(logError('ACTION', 'Failed to do something')) + */ +export function logError(title: string, message: string, isMobile: boolean | 'main' = 'main') { + return (error: unknown) => { + const errMsg = error instanceof Error ? error.message : String(error) + log(isMobile, title, `${message}: ${errMsg}`, 'warn') + } +} + type WebhookBuffer = { lines: string[] sending: boolean diff --git a/src/util/StartupValidator.ts b/src/util/StartupValidator.ts index 8f379ca..d77842e 100644 --- a/src/util/StartupValidator.ts +++ b/src/util/StartupValidator.ts @@ -11,6 +11,7 @@ interface ValidationError { message: string fix?: string docsLink?: string + blocking?: boolean // If true, prevents bot startup } export class StartupValidator { @@ -19,8 +20,8 @@ export class StartupValidator { /** * Run all validation checks before starting the bot. - * Always returns true - validation is informative, not blocking. - * Displays errors and warnings but lets execution continue. + * Throws ValidationError if critical (blocking) errors are found. + * Displays errors and warnings to help users fix configuration issues. */ async validate(config: Config, accounts: Account[]): Promise { log('main', 'STARTUP', 'Running configuration validation...') @@ -40,8 +41,15 @@ export class StartupValidator { // Display results (await to respect the delay) await this.displayResults() - // Always return true - validation is informative only - // Users can proceed even with errors (they might be false positives) + // Check for blocking errors + const blockingErrors = this.errors.filter(e => e.blocking === true) + if (blockingErrors.length > 0) { + const errorMsg = `Validation failed with ${blockingErrors.length} critical error(s). Fix configuration before proceeding.` + log('main', 'VALIDATION', errorMsg, 'error') + throw new Error(errorMsg) + } + + // Non-blocking errors and warnings allow execution to continue return true } @@ -51,7 +59,8 @@ export class StartupValidator { 'accounts', 'No accounts found in accounts.json', 'Add at least one account to src/accounts.json or src/accounts.jsonc', - 'docs/accounts.md' + 'docs/accounts.md', + true // blocking: no accounts = nothing to run ) return } @@ -64,13 +73,17 @@ export class StartupValidator { this.addError( 'accounts', `${prefix}: Missing or invalid email address`, - 'Add a valid email address in the "email" field' + 'Add a valid email address in the "email" field', + undefined, + true // blocking: email is required ) } else if (!/@/.test(account.email)) { this.addError( 'accounts', `${prefix}: Email format is invalid`, - 'Email must contain @ symbol (e.g., user@example.com)' + 'Email must contain @ symbol (e.g., user@example.com)', + undefined, + true // blocking: invalid email = cannot login ) } @@ -79,7 +92,9 @@ export class StartupValidator { this.addError( 'accounts', `${prefix}: Missing or invalid password`, - 'Add your Microsoft account password in the "password" field' + 'Add your Microsoft account password in the "password" field', + undefined, + true // blocking: password is required ) } else if (account.password.length < 4) { this.addWarning( @@ -218,7 +233,9 @@ export class StartupValidator { this.addError( 'config', 'Global timeout is set to 0', - 'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs' + 'Set a reasonable timeout value (e.g., "30s", "60s") to prevent infinite hangs', + undefined, + true // blocking: 0 timeout = infinite hangs guaranteed ) } @@ -368,13 +385,16 @@ export class StartupValidator { 'network', 'Webhook enabled but URL is missing', 'Add webhook URL or set webhook.enabled=false', - 'docs/config.md' + 'docs/config.md', + true // blocking: enabled but no URL = will crash ) } else if (!config.webhook.url.startsWith('http')) { this.addError( 'network', `Invalid webhook URL: ${config.webhook.url}`, - 'Webhook URL must start with http:// or https://' + 'Webhook URL must start with http:// or https://', + undefined, + true // blocking: invalid URL = will crash ) } } @@ -385,7 +405,9 @@ export class StartupValidator { this.addError( 'network', 'Conclusion webhook enabled but URL is missing', - 'Add conclusion webhook URL or disable it' + 'Add conclusion webhook URL or disable it', + undefined, + true // blocking: enabled but no URL = will crash ) } } @@ -397,7 +419,8 @@ export class StartupValidator { 'network', 'NTFY enabled but URL is missing', 'Add NTFY server URL or set ntfy.enabled=false', - 'docs/ntfy.md' + 'docs/ntfy.md', + true // blocking: enabled but no URL = will crash ) } if (!config.ntfy.topic || config.ntfy.topic.trim() === '') { @@ -405,7 +428,8 @@ export class StartupValidator { 'network', 'NTFY enabled but topic is missing', 'Add NTFY topic name', - 'docs/ntfy.md' + 'docs/ntfy.md', + true // blocking: enabled but no topic = will crash ) } } @@ -611,12 +635,12 @@ export class StartupValidator { } } - private addError(category: string, message: string, fix?: string, docsLink?: string): void { - this.errors.push({ severity: 'error', category, message, fix, docsLink }) + private addError(category: string, message: string, fix?: string, docsLink?: string, blocking = false): void { + this.errors.push({ severity: 'error', category, message, fix, docsLink, blocking }) } private addWarning(category: string, message: string, fix?: string, docsLink?: string): void { - this.warnings.push({ severity: 'warning', category, message, fix, docsLink }) + this.warnings.push({ severity: 'warning', category, message, fix, docsLink, blocking: false }) } private async displayResults(): Promise {