Kn/macos signing (#2432)

* chore: migrate from altool to notarytool

* fix litte things in build-common

* add pyenv install in build-osx

* add pyinstaller/electron/signing_logs to gitignore

* chore: migrate from altool to notarytool

* fix little things in build.common

* add pyenv install in build osx

* add pyinatsller electron signing logs to gitignore

* updated build-osx.sh

* fix entitlement

* heavily refactoring the electron app

* polish and improve

* further bugfixing and polishing

* tiny change to improve support of MacOS

* Fix dependency issues

---------

Co-authored-by: Manolis <moneymanolis@protonmail.com>
This commit is contained in:
k9ert 2024-05-01 12:31:32 +02:00 committed by GitHub
parent 7970c3d8ab
commit e9dc494d22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 932 additions and 803 deletions

1
.gitignore vendored
View file

@ -25,6 +25,7 @@ pyinstaller/electron/downloadloc.js
pyinstaller/electron/node_modules pyinstaller/electron/node_modules
pyinstaller/electron/package-lock.json pyinstaller/electron/package-lock.json
pyinstaller/electron/dist pyinstaller/electron/dist
pyinstaller/electron/signing_logs
pyinstaller/electron/fonts pyinstaller/electron/fonts
pyinstaller/electron/typography.css pyinstaller/electron/typography.css
pyinstaller/electron/output.css pyinstaller/electron/output.css

View file

@ -1,4 +1,9 @@
{ {
"python.testing.unittestEnabled": false, "python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true "python.testing.pytestEnabled": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
} }

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/> <true/>
<key>com.apple.security.device.camera</key> <key>com.apple.security.device.camera</key>

View file

@ -1,23 +1,20 @@
<html> <html>
<link rel="stylesheet" type="text/css" href="./styles.css"> <link rel="stylesheet" type="text/css" href="./output.css" />
<body style="overflow: auto; height: 100%;"> <body style="overflow: auto; height: 100%">
<div class="card" style="width:90%"> <div class="card" style="width: 90%">
This window shows you the last 700 lines Logs of the specterApp. This also includes This window shows you the last 700 lines Logs of the specterApp. This also includes the Logs of specterd which are
the Logs of specterd which are marked as such. It might give you hints on why specter is not coming up properly. marked as such. It might give you hints on why specter is not coming up properly. The best approach is to scroll to the
The best approach is to scroll to the bottom and then search upwards for errors. bottom and then search upwards for errors. You can find the logfile in [yourHomedirectory]/.specter/specterApp.log.
You can find the logfile in [yourHomedirectory]/.specter/specterApp.log. <pre><code id="specterapp-logs"></code></pre>
<pre><code id="specterapp-logs"></code></pre> <br />
<br> </div>
</div>
<script> <script>
const fs = require('fs') const fs = require('fs')
const path = require('path') const path = require('path')
const helpers = require('./helpers') const helpers = require('./src/helpers')
helpers.getSpecterAppLogs( (lines) => { helpers.getSpecterAppLogs((lines) => {
document.getElementById('specterapp-logs').innerText = lines document.getElementById('specterapp-logs').innerText = lines
}) })
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,125 +0,0 @@
const fs = require('fs')
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const readLastLines = require('read-last-lines');
const downloadloc = require('./downloadloc');
const appName = downloadloc.appName()
const appNameLower = appName.toLowerCase()
const isDev = process.env.NODE_ENV === "development"
const unresolvedDevFolder = process.env.SPECTER_DATA_FOLDER || "~/.specter_dev"
const devFolder = unresolvedDevFolder.replace(/^~/, os.homedir());
const prodFolder = path.resolve(os.homedir(), `.${appNameLower}`)
const isMac = process.platform === 'darwin'
let appSettingsPath
let specterdDirPath
let specterAppLogPath
let versionData
// Use different version-data.jsons
// Should look like this:
// {
// "version": "v2.0.0-pre32",
// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785"
// }
if (isDev) {
let versionDataPath = `${devFolder}/version-data.json`
try {
versionData = require(versionDataPath)
}
catch {
}
}
else {
try {
versionData = require('./version-data.json')
}
catch (e) {
console.log('Could not find default version data configurations: '+e)
versionData = {
"version": "",
"sha256": ""
}
}
}
if (isDev) {
appSettingsPath = `${devFolder}/app_settings.json`
specterdDirPath = `${devFolder}/specterd-binaries`
specterAppLogPath = `${devFolder}/specterApp.log`
}
else {
appSettingsPath = `${prodFolder}/app_settings.json`
specterdDirPath = `${prodFolder}/specterd-binaries`
specterAppLogPath = `${prodFolder}/specterApp.log`
}
function getFileHash(filename, callback) {
let shasum = crypto.createHash('sha256')
// Updating shasum with file content
, s = fs.ReadStream(filename)
s.on('data', function(data) {
shasum.update(data)
})
// making digest
s.on('end', function() {
var hash = shasum.digest('hex')
callback(hash)
})
}
function getAppSettings() {
let defaultSettings = {
mode: 'specterd',
specterURL: 'http://localhost:25441',
basicAuth: false,
basicAuthUser: '',
basicAuthPass: '',
tor: false,
proxyURL: "socks5://127.0.0.1:9050",
specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown',
specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256 : 'unknown',
specterdCLIArgs: "",
versionInitialized: false
}
try {
if (!fs.existsSync(appSettingsPath)){
fs.mkdirSync(path.resolve(appSettingsPath, '..'), { recursive: true });
}
fs.writeFileSync(appSettingsPath, JSON.stringify(defaultSettings), { flag: 'wx' });
} catch {
// settings file already exists
}
// Make sure to add missing settings in case the format changed or new settings were added
let appSettings = require(appSettingsPath)
for (let key of Object.keys(defaultSettings)) {
if (!appSettings.hasOwnProperty(key)) {
appSettings[key] = defaultSettings[key]
}
}
return appSettings
}
function getSpecterAppLogs(callback) {
readLastLines.read(specterAppLogPath, 700)
.then(callback);
}
module.exports = {
getFileHash: getFileHash,
appSettingsPath: appSettingsPath,
getAppSettings: getAppSettings,
specterdDirPath: specterdDirPath,
getSpecterAppLogs: getSpecterAppLogs,
specterAppLogPath: specterAppLogPath,
versionData,
isDev: isDev,
devFolder,
isMac, isMac
}

View file

@ -1,30 +1,18 @@
// Modules to control application life and create native browser window // Modules to control application life and create native browser window
const { app, nativeTheme, nativeImage, BrowserWindow, Menu, Tray, screen, shell, dialog, ipcMain } = require('electron')
const path = require('path')
const fs = require('fs') const fs = require('fs')
const { URL } = require('node:url')
const request = require('request')
const https = require('https')
const extract = require('extract-zip')
const defaultMenu = require('electron-default-menu')
const ProgressBar = require('electron-progressbar')
const { spawn, exec } = require('child_process') const { spawn, exec } = require('child_process')
const { const { app, nativeTheme, nativeImage, BrowserWindow, Menu, Tray, screen, shell, dialog, ipcMain } = require('electron')
getFileHash, const defaultMenu = require('electron-default-menu')
getAppSettings, const contextMenu = require('electron-context-menu')
appSettingsPath,
specterdDirPath, const { appSettingsPath, specterdDirPath, appSettings, platformName, appNameLower } = require('./src/config.js')
specterAppLogPath, const { logger } = require('./src/logging.js')
versionData,
isDev,
devFolder,
isMac,
} = require('./helpers')
const downloadloc = require('./downloadloc') const downloadloc = require('./downloadloc')
const { downloadSpecterd } = require('./src/download.js')
const getDownloadLocation = downloadloc.getDownloadLocation const getDownloadLocation = downloadloc.getDownloadLocation
const appName = downloadloc.appName() const { startSpecterd, quitSpecterd } = require('./src/specterd.js')
const appNameLower = appName.toLowerCase() const { getFileHash, getAppSettings, versionData, isDev, devFolder, isMac } = require('./src/helpers.js')
const { showError, updatingLoaderMsg, initMainWindow, loadUrl, initTray } = require('./src/uiHelpers.js')
// Quit again if there is no version-data in dev // Quit again if there is no version-data in dev
if (isDev && versionData === undefined) { if (isDev && versionData === undefined) {
@ -39,23 +27,6 @@ ipcMain.handle('showMessageBoxSync', (e, message, buttons) => {
dialog.showMessageBoxSync(mainWindow, { message, buttons }) dialog.showMessageBoxSync(mainWindow, { message, buttons })
}) })
// Logging
const { transports, format, createLogger } = require('winston')
const combinedLog = new transports.File({ filename: specterAppLogPath })
const winstonOptions = {
exitOnError: false,
format: format.combine(
format.timestamp(),
format.json(),
format.printf((info) => {
return `${info.timestamp} [${info.level}] : ${info.message}`
})
),
transports: [new transports.Console({ json: false }), combinedLog],
exceptionHandlers: [combinedLog],
}
const logger = createLogger(winstonOptions)
if (isDev) { if (isDev) {
logger.info('Running the Electron app in dev mode.') logger.info('Running the Electron app in dev mode.')
} }
@ -75,11 +46,10 @@ if (isMac && isDev) {
app.dock.setIcon(dockIcon) app.dock.setIcon(dockIcon)
} }
let appSettings = getAppSettings()
let dimensions = { width: 1500, height: 1000 } let dimensions = { width: 1500, height: 1000 }
// Modify the context menu // Modify the context menu
const contextMenu = require('electron-context-menu')
contextMenu({ contextMenu({
menu: (actions) => [ menu: (actions) => [
{ {
@ -101,136 +71,16 @@ contextMenu({
], ],
}) })
// The standard quit item cannot be replaced / modified and it is not triggering the
// before-quit event on MacOS if a child window is open
const dockMenuWithforceQuit = Menu.buildFromTemplate([
{
label: 'Force Quit during download',
click: () => {
// If the progress bar exists, close it
if (progressBar) {
progressBar.close()
}
// Quit the app
app.quit()
},
},
])
// Download function with progress bar
let progressBar
const download = (uri, filename, callback) => {
// HEAD request first
request.head(uri, (err, res, body) => {
if (res.statusCode != 404) {
let receivedBytes = 0
const totalBytes = res.headers['content-length']
logger.info(`Total size to download: ${totalBytes}`)
progressBar = new ProgressBar({
indeterminate: false,
abortOnError: true,
text: 'Downloading the Specter binary from GitHub',
detail:
'This can take several minutes depending on your Internet connection. Specter will start once the download is finished.',
maxValue: totalBytes,
browserWindow: {
parent: mainWindow,
},
style: {
detail: {
'margin-bottom': '12px',
},
bar: {
'background-color': '#fff',
},
value: {
'background-color': '#000',
},
},
})
// Add Force Quit item during download for MacOS dock
if (isMac) {
app.dock.setMenu(dockMenuWithforceQuit)
}
progressBar.on('completed', () => {
progressBar.close()
// Remove the Force Quit dock item again for Mac
if (isMac) {
const updatedDockMenu = Menu.buildFromTemplate(
dockMenuWithforceQuit.items.filter((item) => item.label !== 'Force Quit during download')
)
app.dock.setMenu(updatedDockMenu)
}
})
progressBar.on('aborted', () => {
logger.info('Download was aborted before it could finish.')
})
// Loggin the download progress
let lastLogTime = 0
const logInterval = 5000 // log every 5 seconds
progressBar.on('progress', () => {
const currentTime = Date.now()
if (currentTime - lastLogTime >= logInterval) {
lastLogTime = currentTime
logger.info(`Download status: ${((receivedBytes / totalBytes) * 100).toFixed(0)}%`)
}
})
// GET request
request(uri)
.on('data', (chunk) => {
receivedBytes += chunk.length
if (progressBar) {
progressBar.value = receivedBytes
}
})
.pipe(fs.createWriteStream(filename))
.on('close', callback)
}
// If the download link was not found, call callback (updatingLoaderMsg with error feedback)
else {
logger.info(`Error while trying to download specterd: ${err}`)
callback(true)
}
})
}
let specterdProcess let specterdProcess
let automaticWalletImport = false let automaticWalletImport = false
let mainWindow let mainWindow
let prefWindow let prefWindow
let tray
let trayMenu
// Flag the app was quitted // Flag the app was quitted
let quitted = false let quitted = false
let webPreferences = {
worldSafeExecuteJavaScript: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
}
app.commandLine.appendSwitch('ignore-certificate-errors') app.commandLine.appendSwitch('ignore-certificate-errors')
let platformName = ''
switch (process.platform) {
case 'darwin':
platformName = 'osx'
break
case 'win32':
platformName = 'win64'
break
case 'linux':
platformName = 'x86_64-linux-gnu'
break
default:
throw `Unknown platformName ${platformName}`
}
logger.info('Using version ' + appSettings.specterdVersion) logger.info('Using version ' + appSettings.specterdVersion)
logger.info('Using platformName ' + platformName) logger.info('Using platformName ' + platformName)
@ -261,85 +111,35 @@ app.on('login', function (event, webContents, request, authInfo, callback) {
trySavedAuth = false trySavedAuth = false
}) })
function createWindow(specterURL) {
if (!mainWindow) {
initMainWindow()
}
// Create the browser window.
if (appSettings.tor) {
mainWindow.webContents.session.setProxy({ proxyRules: appSettings.proxyURL })
}
mainWindow.loadURL(specterURL + '?mode=remote')
}
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
app.whenReady().then(() => { app.whenReady().then(() => {
// Create the tray icon // Create the tray icon
logger.info('Framework ready! Starting tray icon ...') logger.info('Framework ready! Starting tray icon ...')
if (isMac) { initTray(openPreferences, quitSpecterd)
const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png'
const createTrayIcon = (trayIconPath) => {
let trayIcon = nativeImage.createFromPath(app.getAppPath() + trayIconPath)
// Resize
trayIcon = trayIcon.resize({ width: 22, height: 22 })
return trayIcon
}
const trayIcon = createTrayIcon(trayIconPath)
tray = new Tray(trayIcon)
// Change the tray icon if appearance is changed in Mac settings
const updateTrayIcon = () => {
logger.info('Updating tray icon ...')
const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png'
const newTrayIcon = createTrayIcon(trayIconPath)
tray.setImage(newTrayIcon)
}
nativeTheme.on('updated', updateTrayIcon)
} else {
const trayIcon = nativeImage.createFromPath(app.getAppPath() + '/assets/menu_icon.png')
tray = new Tray(trayIcon)
}
trayMenu = [
{ label: 'Launching Specter ...', enabled: false },
{
label: 'Show Specter',
click() {
mainWindow.show()
},
},
{
label: 'Settings',
click() {
openPreferences()
},
},
{
label: 'Quit',
click() {
quitSpecterd()
app.quit()
},
},
]
tray.setToolTip('Specter')
tray.setContextMenu(Menu.buildFromTemplate(trayMenu))
dimensions = screen.getPrimaryDisplay().size dimensions = screen.getPrimaryDisplay().size
// create a new `splash`-Window // create a new `splash`-Window
logger.info('Framework Ready! Initializing Main-Window, populating Menu ...') logger.info('Framework Ready! Initializing Main-Window, populating Menu ...')
initMainWindow() mainWindow = initMainWindow(dimensions)
mainWindow.on('close', function (event) {
if (platformName == 'win64') {
quitSpecterd()
app.quit()
} else {
event.preventDefault()
mainWindow.hide()
}
})
setMainMenu() setMainMenu()
mainWindow.loadURL(`file://${__dirname}/splash.html`) loadUrl(`file://${__dirname}/splash.html`)
if (!fs.existsSync(specterdDirPath)) { if (!fs.existsSync(specterdDirPath)) {
logger.info('Creating specterd-binaries folder') logger.info('Creating specterd-binaries folder:' + specterdDirPath)
fs.mkdirSync(specterdDirPath, { recursive: true }) fs.mkdirSync(specterdDirPath, { recursive: true })
} }
@ -377,294 +177,6 @@ app.whenReady().then(() => {
} }
}) })
function initMainWindow() {
// In production we use the icons from the build folder
// Note: On MacOS setting an icon here as no effect
const iconPath = isDev ? path.join(__dirname, 'assets-dev/app_icon.png') : ''
mainWindow = new BrowserWindow({
width: parseInt(dimensions.width * 0.8),
minWidth: 1120,
height: parseInt(dimensions.height * 0.8),
icon: iconPath,
webPreferences,
})
// Ensures that any links with target="_blank" or window.open() will be opened in the user's default browser instead of within the app
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
mainWindow.on('close', function (event) {
if (platformName == 'win64') {
quitSpecterd()
app.quit()
} else {
event.preventDefault()
mainWindow.hide()
}
})
mainWindow.webContents.on('did-fail-load', function () {
mainWindow.loadURL(`file://${__dirname}/splash.html`)
updatingLoaderMsg(
`Failed to load: ${appSettings.specterURL}<br>Please make sure the URL is entered correctly in the settings and try again...`
)
})
}
function downloadSpecterd(specterdPath) {
updatingLoaderMsg(`Starting download`)
updateSpecterdStatus(`Downloading the ${appName} binary...`)
// Some logging
logger.info('Using version ' + appSettings.specterdVersion)
logger.info('Using platformName ' + platformName)
download_location = getDownloadLocation(appSettings.specterdVersion, platformName)
logger.info('Downloading from ' + download_location)
download(download_location, specterdPath + '.zip', function (errored) {
if (errored == true) {
updatingLoaderMsg(
`Downloading the ${appNameLower} binary from GitHub failed, could not reach the server or the file wasn't found.`
)
updateSpecterdStatus(`Downloading ${appNameLower}d failed...`)
return
}
updatingLoaderMsg('Download completed. Unpacking files...')
logger.info('Extracting ' + specterdPath)
extract(specterdPath + '.zip', { dir: specterdPath + '-dir' }).then(function () {
let extraPath = ''
switch (process.platform) {
case 'darwin':
extraPath = appNameLower + 'd'
break
case 'win32':
extraPath = appNameLower + 'd.exe'
break
case 'linux':
extraPath = appNameLower + 'd'
}
var oldPath = specterdPath + `-dir/${extraPath}`
var newPath = specterdPath + (platformName == 'win64' ? '.exe' : '')
fs.renameSync(oldPath, newPath)
fs.unlinkSync(specterdPath + '.zip')
fs.rmdirSync(specterdPath + '-dir', { recursive: true })
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
if (appSettings.specterdHash.toLowerCase() === specterdHash || appSettings.specterdHash == '') {
startSpecterd(specterdPath)
} else {
updatingLoaderMsg('Specterd version could not be validated.')
logger.error(`hash of downloaded file: ${specterdHash}`)
logger.error(`Expected hash: ${appSettings.specterdHash}`)
updateSpecterdStatus('Failed to launch specterd...')
}
})
})
})
}
function updateSpecterdStatus(status) {
trayMenu[0] = { label: status, enabled: false }
tray.setContextMenu(Menu.buildFromTemplate(trayMenu))
}
function updatingLoaderMsg(msg, showSpinner = false) {
if (mainWindow) {
let code = `
var launchText = document.getElementById('launch-text');
if (launchText) {
launchText.innerHTML = '${msg}';
}
var spinnerElement = document.getElementById('spinner');
if (spinnerElement) {
if (${showSpinner} === true) {
spinnerElement.classList.remove('hidden')
}
else {
spinnerElement.classList.add('hidden')
}
}
`
mainWindow.webContents.executeJavaScript(code)
}
logger.info('Updated LoaderMsg: ' + msg)
}
function checkSpecterd(logs, specterdStarted) {
// There doesn't seem to be another more straightforward way to check whether specterd is running: https://github.com/nodejs/help/issues/1191
// Setting a timeout to avoid waiting for specterd endlessly
const timeout = 180000 // 3 minutes
const now = Date.now()
const timeElapsed = now - specterdStarted
if (timeElapsed > timeout) {
return 'timeout'
}
if (logs.toString().includes('Serving Flask app')) {
return 'running'
} else {
return 'not running'
}
}
let specterIsRunning = false
function startSpecterd(specterdPath) {
if (platformName == 'win64') {
specterdPath += '.exe'
}
let appSettings = getAppSettings()
let hwiBridgeMode = appSettings.mode == 'hwibridge'
updatingLoaderMsg('Launching Specter ...', (showSpinner = true))
updateSpecterdStatus('Launching Specter ...')
let specterdArgs = ['server']
specterdArgs.push('--no-filelog')
if (hwiBridgeMode) specterdArgs.push('--hwibridge')
if (appSettings.specterdCLIArgs != '') {
// User has inputed cli arguments in the UI
let specterdExtraArgs = appSettings.specterdCLIArgs.split(' ')
specterdExtraArgs.forEach((arg) => {
// Ensures that whitespaces are not used as cli arguments
if (arg != '') {
specterdArgs.push(arg)
}
})
}
// locale fix (copying from nodejs-env + adding locales)
const options = {
env: { ...process.env },
}
options.env['LC_ALL'] = 'en_US.utf-8'
options.env['LANG'] = 'en_US.utf-8'
options.env['SPECTER_LOGFORMAT'] = 'SPECTERD: %(levelname)s in %(module)s: %(message)s'
specterdProcess = spawn(specterdPath, specterdArgs, options)
const specterdStarted = Date.now()
// We are checking for both, stdout and stderr, to be on the save side.
specterdProcess.stdout.on('data', (data) => {
logger.info('stdout-' + data.toString())
let serverdStatus = checkSpecterd(data, specterdStarted)
// We don't want to check the logs forever, just until specterd is up and running
if (!specterIsRunning) {
if (serverdStatus === 'running') {
logger.info(`Specter server seems to run ...`)
updateSpecterdStatus('Specter is running')
specterIsRunning = true
if (mainWindow) {
if (automaticWalletImport === true) {
logger.info('Performing automatic wallet import ...')
updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true))
setTimeout(() => {
importWallet(walletDataFromUrl)
}, 3000)
} else {
logger.info('Normal startup of Specter.')
createWindow(appSettings.specterURL)
}
}
} else if (serverdStatus === 'timeout') {
showError('Specter does not seem to start. Check the logs in the menu for more details.')
updateSpecterdStatus('Specter does not start')
logger.error('Startup timeout for specterd exceeded')
} else {
updatingLoaderMsg('Still waiting for Specter to start ...')
updateSpecterdStatus('Specter is starting')
}
}
})
specterdProcess.stderr.on('data', (data) => {
logger.info('stderr-' + data.toString())
let serverdStatus = checkSpecterd(data, specterdStarted)
if (!specterIsRunning) {
if (serverdStatus === 'running') {
logger.info(`Specter server seems to run ...`)
updateSpecterdStatus('Specter is running')
specterIsRunning = true
if (mainWindow) {
logger.info('... creating Electron window for it.')
createWindow(appSettings.specterURL)
}
} else if (serverdStatus === 'timeout') {
showError('Specter does not seem to start. Check the logs in the menu for more details.')
updateSpecterdStatus('Specter does not start')
logger.error('Startup timeout for specterd exceeded')
} else {
updatingLoaderMsg('Still waiting for Specter to start ...')
updateSpecterdStatus('Specter is starting')
}
}
})
specterdProcess.on('exit', (code) => {
logger.error(`specterd exited with code ${code}`)
showError(`Specter exited with exit code ${code}. Check the logs in the menu for more details.`)
})
specterdProcess.on('error', (err) => {
logger.error(`Error starting Specter server: ${err}`)
showError(`Specter failed to start, due to ${err.message}. Check the logs in the menu for more details.`)
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL)
})
// since these are streams, you can pipe them elsewhere
specterdProcess.on('close', (code) => {
updateSpecterdStatus('Specter stopped...')
logger.info(`child process exited with code ${code}`)
})
}
let walletDataFromUrl
// Checking whether the app was opened via a Specter URL and determine whether to perform a specific startup action
app.on('open-url', (_, url) => {
logger.info('The app was opened via URL, checking the URL to decide whether to do any automatic actions ...')
// Parse the URL to extract the query parameters
const specterUrl = new URL(url)
const searchParams = specterUrl.searchParams
// Get the query parameter values
const action = searchParams.get('action')
const data = searchParams.get('data')
if (action === 'importWallet' && data !== '') {
logger.info('Automatic wallet import identified in the URL, setting automaticWalletImport to true.')
automaticWalletImport = true
walletDataFromUrl = data
// Directly import if the app and specterd is already running
if (specterIsRunning) {
logger.info('Performing automatic wallet import ...')
mainWindow.loadURL(`file://${__dirname}/splash.html`)
updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true))
setTimeout(() => {
importWallet(walletDataFromUrl)
}, 3000)
}
}
})
// Automatically import the wallet json string, bring user to the final import wallet screen.
// Only proceed with the import if the importFromWalletSoftwareBtn can be found.
// If it is not, users are redirected by specterd to the configure connection screen.
function importWallet(walletData) {
mainWindow.loadURL(appSettings.specterURL + '/wallets/new_wallet/')
if (mainWindow) {
let code = `
const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn')
if (importFromWalletSoftwareBtn) {
importFromWalletSoftwareBtn.click()
const walletDataTextArea = document.getElementById('txt')
if (walletDataTextArea) {
walletDataTextArea.value = \`${walletData}\`
}
const continueBtn = document.getElementById('continue-with-wallet-import-btn');
continueBtn.click()
}
`
mainWindow.webContents.executeJavaScript(code)
}
}
app.on('window-all-closed', function () { app.on('window-all-closed', function () {
if (platformName == 'win64') { if (platformName == 'win64') {
quitSpecterd() quitSpecterd()
@ -715,21 +227,6 @@ ipcMain.on('request-mainprocess-action', (event, arg) => {
} }
}) })
function quitSpecterd() {
if (specterdProcess) {
try {
if (platformName == 'win64') {
exec('taskkill /F /T /PID ' + specterdProcess.pid)
exec('taskkill /IM specterd.exe ')
process.kill(-specterdProcess.pid)
}
specterdProcess.kill('SIGINT')
} catch (e) {
logger.info('Specterd quit warning: ' + e)
}
}
}
function setMainMenu() { function setMainMenu() {
const menu = defaultMenu(app, shell) const menu = defaultMenu(app, shell)
@ -808,21 +305,17 @@ function openErrorLog() {
createNewWindow('error_logs.html', width, height).show() createNewWindow('error_logs.html', width, height).show()
} }
function showError(error) {
updatingLoaderMsg('Specter encountered an error:<br>' + error.toString())
}
process.on('unhandledRejection', (error) => { process.on('unhandledRejection', (error) => {
showError(error) showError(error)
logger.error(error.toString(), error.name) logger.error(error.stack)
}) })
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
showError(error) showError(error)
// I would love to rethrow the error here as this would create a stacktrace in the logs // I would love to rethrow the error here as this would create a stacktrace in the logs
// but this will terminate the whole process even though i've set // but this will terminate the whole process even though i've set
// exitOnError: false in the wistonOptions above. // exitOnError: false in the winstonOptions above.
// Unacceptable for the folks which can't use a commandline, clicking an icon // Unacceptable for the folks which can't use a commandline, clicking an icon
//throw(error) //throw(error)
logger.error(error.toString()) logger.error(error.stack)
}) })

View file

@ -1,26 +1,26 @@
{ {
"name": "Specter", "name": "Specter",
"version": "v0.0.0", "version": "v2.0.3-pre1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "Specter", "name": "Specter",
"version": "v0.0.0", "version": "v2.0.3-pre1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"electron-context-menu": "^2.3.0", "electron-context-menu": "^2.5.2",
"electron-default-menu": "^1.0.2", "electron-default-menu": "^1.0.2",
"electron-progressbar": "^2.0.1", "electron-progressbar": "^2.1.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"read-last-lines": "^1.8.0", "read-last-lines": "^1.8.0",
"request": "^2.88.2", "request": "^2.88.2",
"winston": "^3.3.3" "winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"electron": "^22.1.0", "electron": "^22.3.12",
"electron-builder": "^23.3.1" "electron-builder": "^23.6.0"
} }
}, },
"node_modules/@dabh/diagnostics": { "node_modules/@dabh/diagnostics": {
@ -1421,9 +1421,9 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "22.1.0", "version": "22.3.27",
"resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.27.tgz",
"integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", "integrity": "sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
@ -1502,13 +1502,16 @@
} }
}, },
"node_modules/electron-context-menu": { "node_modules/electron-context-menu": {
"version": "2.3.0", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.3.0.tgz", "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz",
"integrity": "sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw==", "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==",
"dependencies": { "dependencies": {
"cli-truncate": "^2.0.0", "cli-truncate": "^2.1.0",
"electron-dl": "^3.0.0", "electron-dl": "^3.1.0",
"electron-is-dev": "^1.0.1" "electron-is-dev": "^1.2.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/electron-default-menu": { "node_modules/electron-default-menu": {
@ -1517,13 +1520,19 @@
"integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ==" "integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ=="
}, },
"node_modules/electron-dl": { "node_modules/electron-dl": {
"version": "3.0.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.0.2.tgz", "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.2.tgz",
"integrity": "sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==", "integrity": "sha512-i104cl+u8yJ0lhpRAtUWfeGuWuL1PL6TBiw2gLf0MMIBjfgE485Ags2mcySx4uWU9P9uj/vsD3jd7X+w1lzZxw==",
"dependencies": { "dependencies": {
"ext-name": "^5.0.0", "ext-name": "^5.0.0",
"pupa": "^2.0.1", "pupa": "^2.0.1",
"unused-filename": "^2.1.0" "unused-filename": "^2.1.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/electron-is-dev": { "node_modules/electron-is-dev": {
@ -1581,9 +1590,9 @@
"dev": true "dev": true
}, },
"node_modules/electron-progressbar": { "node_modules/electron-progressbar": {
"version": "2.0.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.2.1.tgz",
"integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", "integrity": "sha512-LQ9bxM3Tf5PG/1QngY8ywvht7IKvQ8tEIra8uh3RkLASqN/GYvr4r0uU9qz38r0Zn72UcouYzumKDfLwoI/rsw==",
"dependencies": { "dependencies": {
"extend": "^3.0.1" "extend": "^3.0.1"
} }
@ -2216,7 +2225,7 @@
"node_modules/is-plain-obj": { "node_modules/is-plain-obj": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -2514,7 +2523,7 @@
"node_modules/modify-filename": { "node_modules/modify-filename": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=", "integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -2669,9 +2678,9 @@
} }
}, },
"node_modules/pupa": { "node_modules/pupa": {
"version": "2.0.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
"integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
"dependencies": { "dependencies": {
"escape-goat": "^2.0.0" "escape-goat": "^2.0.0"
}, },
@ -2968,7 +2977,7 @@
"node_modules/sort-keys": { "node_modules/sort-keys": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==",
"dependencies": { "dependencies": {
"is-plain-obj": "^1.0.0" "is-plain-obj": "^1.0.0"
}, },
@ -2979,7 +2988,7 @@
"node_modules/sort-keys-length": { "node_modules/sort-keys-length": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==",
"dependencies": { "dependencies": {
"sort-keys": "^1.0.0" "sort-keys": "^1.0.0"
}, },
@ -4592,9 +4601,9 @@
} }
}, },
"electron": { "electron": {
"version": "22.1.0", "version": "22.3.27",
"resolved": "https://registry.npmjs.org/electron/-/electron-22.1.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-22.3.27.tgz",
"integrity": "sha512-wz5s4N6V7zyKm4YQmXJImFoxO1Doai32ShYm0FzOLPBMwLMdQBV+REY+j1opRx0KJ9xJEIdjYgcA8OSw6vx3pA==", "integrity": "sha512-7Rht21vHqj4ZFRnKuZdFqZFsvMBCmDqmjetiMqPtF+TmTBiGne1mnstVXOA/SRGhN2Qy5gY5bznJKpiqogjM8A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
@ -4652,13 +4661,13 @@
} }
}, },
"electron-context-menu": { "electron-context-menu": {
"version": "2.3.0", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.3.0.tgz", "resolved": "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-2.5.2.tgz",
"integrity": "sha512-XYsYkNY+jvX4C5o09qMuZoKL6e9frnQzBFehZSIiKp6zK0u3XYowJYDyK3vDKKZxYsOIGiE/Gbx40jERC03Ctw==", "integrity": "sha512-1cEQR6fA9ktFsRBc+eXPwvrOgAPytUD7rUV4iBAA5zTrLAPKokJ23xeMjcK2fjrDPrlFRBxcLz0KP+GUhMrSCQ==",
"requires": { "requires": {
"cli-truncate": "^2.0.0", "cli-truncate": "^2.1.0",
"electron-dl": "^3.0.0", "electron-dl": "^3.1.0",
"electron-is-dev": "^1.0.1" "electron-is-dev": "^1.2.0"
} }
}, },
"electron-default-menu": { "electron-default-menu": {
@ -4667,9 +4676,9 @@
"integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ==" "integrity": "sha512-YAL/UNR3kPG58wOOlmDpTG3i6+bzwhHx6NllIOaLuVrU7uYifeYGGdk5IH2Hap4wVEx2YTA8cqQ2PGSplYwDWQ=="
}, },
"electron-dl": { "electron-dl": {
"version": "3.0.2", "version": "3.5.2",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.0.2.tgz", "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-3.5.2.tgz",
"integrity": "sha512-pRgE9Jbhoo5z6Vk3qi+vIrfpMDlCp2oB1UeR96SMnsfz073jj0AZGQwp69EdIcEvlUlwBSGyJK8Jt6OB6JLn+g==", "integrity": "sha512-i104cl+u8yJ0lhpRAtUWfeGuWuL1PL6TBiw2gLf0MMIBjfgE485Ags2mcySx4uWU9P9uj/vsD3jd7X+w1lzZxw==",
"requires": { "requires": {
"ext-name": "^5.0.0", "ext-name": "^5.0.0",
"pupa": "^2.0.1", "pupa": "^2.0.1",
@ -4722,9 +4731,9 @@
} }
}, },
"electron-progressbar": { "electron-progressbar": {
"version": "2.0.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.0.1.tgz", "resolved": "https://registry.npmjs.org/electron-progressbar/-/electron-progressbar-2.2.1.tgz",
"integrity": "sha512-+N60GX2q+KH5OvZXxwtjMTZB/1AyxriFd95vOnR3sOfNpvz+30LMsM0a9SnEivZE6N8Djy7F3z4TY8pLs8aopw==", "integrity": "sha512-LQ9bxM3Tf5PG/1QngY8ywvht7IKvQ8tEIra8uh3RkLASqN/GYvr4r0uU9qz38r0Zn72UcouYzumKDfLwoI/rsw==",
"requires": { "requires": {
"extend": "^3.0.1" "extend": "^3.0.1"
} }
@ -5220,7 +5229,7 @@
"is-plain-obj": { "is-plain-obj": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="
}, },
"is-stream": { "is-stream": {
"version": "2.0.1", "version": "2.0.1",
@ -5445,7 +5454,7 @@
"modify-filename": { "modify-filename": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" "integrity": "sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog=="
}, },
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
@ -5570,9 +5579,9 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
}, },
"pupa": { "pupa": {
"version": "2.0.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz",
"integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==",
"requires": { "requires": {
"escape-goat": "^2.0.0" "escape-goat": "^2.0.0"
} }
@ -5809,7 +5818,7 @@
"sort-keys": { "sort-keys": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==",
"requires": { "requires": {
"is-plain-obj": "^1.0.0" "is-plain-obj": "^1.0.0"
} }
@ -5817,7 +5826,7 @@
"sort-keys-length": { "sort-keys-length": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==",
"requires": { "requires": {
"sort-keys": "^1.0.0" "sort-keys": "^1.0.0"
} }

View file

@ -20,8 +20,8 @@
"author": "Specter", "author": "Specter",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"electron": "^22.1.0", "electron": "^22.3.12",
"electron-builder": "^23.3.1" "electron-builder": "^23.6.0"
}, },
"build": { "build": {
"productName": "Specter", "productName": "Specter",
@ -30,9 +30,9 @@
"category": "public.app-category.utilities", "category": "public.app-category.utilities",
"identity": "Kim Neunert (FWV59JHV83)", "identity": "Kim Neunert (FWV59JHV83)",
"entitlements": "./build/entitlements.mac.plist", "entitlements": "./build/entitlements.mac.plist",
"entitlementsInherit": "./build/entitlements.mac.plist",
"hardenedRuntime": true, "hardenedRuntime": true,
"icon": "./icons/icon.icns" "icon": "./icons/icon.icns",
"provisioningProfile": "/Users/kim/Specter_Desktop_vanilla.provisionprofile"
}, },
"win": { "win": {
"icon": "./icons/icon.ico" "icon": "./icons/icon.ico"
@ -45,12 +45,12 @@
} }
}, },
"dependencies": { "dependencies": {
"electron-context-menu": "^2.3.0", "electron-context-menu": "^2.5.2",
"electron-default-menu": "^1.0.2", "electron-default-menu": "^1.0.2",
"electron-progressbar": "^2.0.1", "electron-progressbar": "^2.1.0",
"extract-zip": "^2.0.1", "extract-zip": "^2.0.1",
"read-last-lines": "^1.8.0", "read-last-lines": "^1.8.0",
"request": "^2.88.2", "request": "^2.88.2",
"winston": "^3.3.3" "winston": "^3.3.3"
} }
} }

View file

@ -1,11 +1,19 @@
// All of the Node.js APIs are available in the preload process. // All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension. // It has the same sandbox as a Chrome extension.
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('api', {
send: (channel, data) => ipcRenderer.send(channel, data),
receive: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
})
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
// No idea why this is here
const replaceText = (selector, text) => { const replaceText = (selector, text) => {
const element = document.getElementById(selector) const element = document.getElementById(selector)
if (element) element.innerText = text if (element) element.innerText = text
} }
for (const type of ['chrome', 'node', 'electron']) { for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type]) replaceText(`${type}-version`, process.versions[type])
} }

View file

@ -4,3 +4,25 @@
// `nodeIntegration` is turned off. Use `preload.js` to // `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering // selectively enable features needed in the rendering
// process. // process.
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
const updateSpinner = (show) => {
const spinnerElement = document.getElementById('spinner');
if (spinnerElement) {
spinnerElement.classList.toggle('hidden', !show);
}
};
window.api.receive('update-loader-message', (data) => {
const launchTextElement = document.getElementById('launch-text');
if (launchTextElement) {
launchTextElement.textContent = data.msg;
updateSpinner(data.showSpinner);
}
});
})

View file

@ -6,4 +6,5 @@
<svg id="spinner" class="text-white rounded-full bg-dark-700 w-4 h-4 animate-spin hidden" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg> <svg id="spinner" class="text-white rounded-full bg-dark-700 w-4 h-4 animate-spin hidden" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><!--Generated by IJSVG (https://github.com/iconjar/IJSVG)--><path d="M52.8359,43.6067l1.30463e-08,-1.88843e-08c-0.312506,0.452349 -0.932543,0.565714 -1.38489,0.253208c-0.0990138,-0.0684038 -0.184804,-0.154194 -0.253208,-0.253208l-6.9888,-9.9742l-5.91546e-08,-8.31834e-08c-0.328807,-0.46237 -0.220533,-1.10375 0.241837,-1.43255c0.168889,-0.120103 0.370155,-0.186318 0.577363,-0.189948h4.9835l3.54852e-07,0.000503677c-0.00686545,-9.94223 -8.0722,-17.9964 -18.0144,-17.9896c-4.13508,0.00285541 -8.14312,1.42919 -11.3504,4.03924l4.60883e-08,-3.91242e-08c-0.842285,0.715013 -2.10472,0.611839 -2.81974,-0.230446c-0.715013,-0.842285 -0.611839,-2.10472 0.230446,-2.81974l3.46098e-07,-2.83814e-07c9.39634,-7.70537 23.26,-6.33457 30.9654,3.06177c3.22304,3.93035 4.98589,8.85536 4.98903,13.9382h4.9935l2.44822e-09,4.33431e-11c0.567328,0.0100438 1.0191,0.478096 1.00905,1.04542c-0.0036664,0.207098 -0.0698445,0.408252 -0.189853,0.577075Zm-40.0129,-23.1717l6.9888,9.9742l-2.71216e-09,-3.81325e-09c0.32881,0.4623 0.220594,1.10362 -0.241706,1.43243c-0.168809,0.120065 -0.369975,0.186285 -0.577094,0.189969h-4.983l2.60913e-07,0.000383721c0.0068661,9.94206 8.07207,17.9961 18.0141,17.9893c4.1354,-0.00285595 8.14372,-1.42943 11.3511,-4.03989l-7.47965e-08,6.34738e-08c0.8424,-0.714877 2.10482,-0.6115 2.8197,0.2309c0.714877,0.8424 0.6115,2.10482 -0.2309,2.8197l4.04125e-07,-3.31403e-07c-9.39617,7.70531 -23.2597,6.33461 -30.965,-3.06156c-3.22311,-3.93039 -4.98597,-8.85549 -4.98903,-13.9384h-4.9947l5.45126e-08,9.68797e-10c-0.567328,-0.0100825 -1.01906,-0.478166 -1.00898,-1.04549c0.00367938,-0.207033 0.0698308,-0.408123 0.189782,-0.576906l6.9888,-9.9742l-3.21808e-08,4.66031e-08c0.312407,-0.452417 0.93242,-0.565917 1.38484,-0.25351c0.0990287,0.0683822 0.184838,0.154154 0.253263,0.253153Z" fill="currentColor" fill-rule="evenodd"></path></svg>
<p id="launch-text" class="text-lg">Launching Specter Desktop...</p> <p id="launch-text" class="text-lg">Launching Specter Desktop...</p>
</div> </div>
<script src="renderer.js"></script>
</body> </body>

View file

@ -0,0 +1,109 @@
const os = require('os')
const path = require('path')
const fs = require('fs')
const downloadloc = require('../downloadloc');
let platformName = ''
switch (process.platform) {
case 'darwin':
platformName = 'osx'
break
case 'win32':
platformName = 'win64'
break
case 'linux':
platformName = 'x86_64-linux-gnu'
break
default:
throw `Unknown platformName ${platformName}`
}
const appName = downloadloc.appName()
const appNameLower = appName.toLowerCase()
const isDev = process.env.NODE_ENV === "development"
const unresolvedDevFolder = process.env.SPECTER_DATA_FOLDER || "~/.specter_dev"
const devFolder = unresolvedDevFolder.replace(/^~/, os.homedir());
const prodFolder = path.resolve(os.homedir(), `.${appNameLower}`)
let appSettingsPath
let appSettings
let specterdDirPath
let specterAppLogPath
let versionDataPath
let versionData
function getAppSettings() {
let defaultSettings = {
mode: 'specterd',
specterURL: 'http://localhost:25441',
basicAuth: false,
basicAuthUser: '',
basicAuthPass: '',
tor: false,
proxyURL: "socks5://127.0.0.1:9050",
specterdVersion: (versionData && versionData.version !== undefined) ? versionData.version : 'unknown',
specterdHash: (versionData && versionData.sha256 !== undefined) ? versionData.sha256 : 'unknown',
specterdCLIArgs: "",
versionInitialized: false
}
try {
if (!fs.existsSync(appSettingsPath)){
fs.mkdirSync(path.resolve(appSettingsPath, '..'), { recursive: true });
}
fs.writeFileSync(appSettingsPath, JSON.stringify(defaultSettings), { flag: 'wx' });
} catch (error) {
if (error.toString().startsWith("Error: EEXIST: file already exists,")) {
//ignore
} else {
throw error
}
}
// Make sure to add missing settings in case the format changed or new settings were added
let appSettings = require(appSettingsPath)
for (let key of Object.keys(defaultSettings)) {
if (!appSettings.hasOwnProperty(key)) {
appSettings[key] = defaultSettings[key]
}
}
return appSettings
}
if (isDev) {
versionDataPath = `${devFolder}/version-data.json`
} else {
versionDataPath = `../version-data.json`
}
versionData = require(versionDataPath)
if (isDev) {
appSettingsPath = `${devFolder}/app_settings.json`
specterdDirPath = `${devFolder}/specterd-binaries`
specterAppLogPath = `${devFolder}/specterApp.log`
}
else {
appSettingsPath = `${prodFolder}/app_settings.json`
specterdDirPath = `${prodFolder}/specterd-binaries`
specterAppLogPath = `${prodFolder}/specterApp.log`
}
appSettings = getAppSettings()
module.exports = {
platformName,
appSettings,
appName,
appNameLower,
appSettingsPath,
specterdDirPath,
specterAppLogPath,
versionDataPath,
versionData,
isDev: isDev,
devFolder,
}

View file

@ -0,0 +1,145 @@
const request = require('request')
const fs = require('fs')
const { app, Menu } = require('electron')
const extract = require('extract-zip')
const { getDownloadLocation } = require('../downloadloc.js')
const { appName, appSettings, platformName, appNameLower, versionData, versionDataPath } = require('./config.js')
const { isMac, getFileHash } = require('./helpers.js')
const { logger } = require('./logging.js')
const ProgressBar = require('electron-progressbar')
const { updateSpecterdStatus, updatingLoaderMsg, createProgressBar } = require('./uiHelpers.js')
const { startSpecterd } = require('./specterd.js')
// The standard quit item cannot be replaced / modified and it is not triggering the
// before-quit event on MacOS if a child window is open
const dockMenuWithforceQuit = Menu.buildFromTemplate([
{
label: 'Force Quit during download',
click: () => {
// If the progress bar exists, close it
if (progressBar) {
progressBar.close()
}
// Quit the app
app.quit()
},
},
])
function downloadSpecterd(specterdPath) {
updatingLoaderMsg(`Starting download`)
updateSpecterdStatus(`Downloading the ${appName} binary...`)
// Some logging
logger.info('Using version ' + appSettings.specterdVersion)
logger.info('Using platformName ' + platformName)
download_location = getDownloadLocation(appSettings.specterdVersion, platformName)
logger.info('Downloading from ' + download_location)
download(download_location, specterdPath + '.zip', function (errored) {
if (errored == true) {
updatingLoaderMsg(
`Downloading the ${appNameLower} binary from GitHub failed, could not reach the server or the file wasn't found.`
)
updateSpecterdStatus(`Downloading ${appNameLower}d failed...`)
return
}
updatingLoaderMsg('Download completed. Unpacking files...')
logger.info('Extracting ' + specterdPath)
extract(specterdPath + '.zip', { dir: specterdPath + '-dir' }).then(function () {
let extraPath = ''
switch (process.platform) {
case 'darwin':
extraPath = appNameLower + 'd'
break
case 'win32':
extraPath = appNameLower + 'd.exe'
break
case 'linux':
extraPath = appNameLower + 'd'
}
var oldPath = specterdPath + `-dir/${extraPath}`
var newPath = specterdPath + (platformName == 'win64' ? '.exe' : '')
fs.renameSync(oldPath, newPath)
fs.unlinkSync(specterdPath + '.zip')
fs.rmdirSync(specterdPath + '-dir', { recursive: true })
getFileHash(specterdPath + (platformName == 'win64' ? '.exe' : ''), function (specterdHash) {
if (appSettings.specterdHash.toLowerCase() === specterdHash || appSettings.specterdHash == '') {
startSpecterd(specterdPath)
} else {
updatingLoaderMsg('Specterd version could not be validated.')
logger.error(`hash of downloaded file: ${specterdHash}`)
logger.error(`Expected hash: ${appSettings.specterdHash} from ${versionDataPath}`)
updateSpecterdStatus('Failed to launch specterd...')
}
})
})
})
}
// Download function with progress bar
const download = (uri, filename, callback) => {
// HEAD request first
request.head(uri, (err, res, body) => {
if (res.statusCode != 404) {
let receivedBytes = 0
const totalBytes = res.headers['content-length']
logger.info(`Total size to download: ${totalBytes}`)
const progressBar = createProgressBar(totalBytes)
// Add Force Quit item during download for MacOS dock
if (isMac) {
app.dock.setMenu(dockMenuWithforceQuit)
}
progressBar.on('completed', () => {
progressBar.close()
// Remove the Force Quit dock item again for Mac
if (isMac) {
const updatedDockMenu = Menu.buildFromTemplate(
dockMenuWithforceQuit.items.filter((item) => item.label !== 'Force Quit during download')
)
app.dock.setMenu(updatedDockMenu)
}
})
progressBar.on('aborted', () => {
logger.warn('Download was aborted before it could finish.')
})
// Loggin the download progress
let lastLogTime = 0
const logInterval = 5000 // log every 5 seconds
progressBar.on('progress', () => {
const currentTime = Date.now()
if (currentTime - lastLogTime >= logInterval) {
lastLogTime = currentTime
logger.info(`Download status: ${((receivedBytes / totalBytes) * 100).toFixed(0)}%`)
}
})
// GET request
request(uri)
.on('data', (chunk) => {
receivedBytes += chunk.length
if (progressBar) {
progressBar.value = receivedBytes
}
})
.pipe(fs.createWriteStream(filename))
.on('close', callback)
}
// If the download link was not found, call callback (updatingLoaderMsg with error feedback)
else {
logger.error(`Error while trying to download specterd: ${err}`)
try {
callback(true)
} catch (error) {
logger.error(error)
throw error
}
}
})
}
module.exports = {
downloadSpecterd: downloadSpecterd,
download: download,
}

View file

@ -0,0 +1,43 @@
const { Menu } = require('electron')
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const readLastLines = require('read-last-lines')
const isMac = process.platform === 'darwin'
const { versionData, appSettingsPath, specterAppLogPath } = require('./config.js')
const { logger } = require('./logging.js')
// Use different version-data.jsons
// Should look like this:
// {
// "version": "v2.0.0-pre32",
// "sha256": "aa049abf3e75199bad26fbded08ee5911ad48e325b42c43ec195136bd0736785"
// }
function getFileHash(filename, callback) {
let shasum = crypto.createHash('sha256'),
// Updating shasum with file content
s = fs.ReadStream(filename)
s.on('data', function (data) {
shasum.update(data)
})
// making digest
s.on('end', function () {
var hash = shasum.digest('hex')
callback(hash)
})
}
function getSpecterAppLogs(callback) {
readLastLines.read(specterAppLogPath, 700).then(callback)
}
module.exports = {
getFileHash,
getSpecterAppLogs,
versionData,
isMac,
isMac,
}

View file

@ -0,0 +1,22 @@
const { specterAppLogPath } = require('./config.js')
// Logging
const { transports, format, createLogger } = require('winston')
const combinedLog = new transports.File({ filename: specterAppLogPath })
const winstonOptions = {
exitOnError: false,
format: format.combine(
format.timestamp(),
format.json(),
format.printf((info) => {
return `${info.timestamp} [${info.level}] : ${info.message}`
})
),
transports: [new transports.Console({ json: false }), combinedLog],
exceptionHandlers: [combinedLog],
}
const logger = createLogger(winstonOptions)
module.exports = {
logger: logger,
}

View file

@ -0,0 +1,189 @@
const { app, BrowserWindow } = require('electron')
const { appSettings, platformName } = require('./config')
const {
showError,
updateSpecterdStatus,
updatingLoaderMsg,
createWindow,
loadUrl,
executeJavaScript,
} = require('./uiHelpers')
const { logger } = require('./logging')
const { spawn } = require('child_process')
const { URL } = require('url')
function checkSpecterd(logs, specterdStarted) {
// There doesn't seem to be another more straightforward way to check whether specterd is running: https://github.com/nodejs/help/issues/1191
// Setting a timeout to avoid waiting for specterd endlessly
const timeout = 180000 // 3 minutes
const now = Date.now()
const timeElapsed = now - specterdStarted
if (timeElapsed > timeout) {
return 'timeout'
}
if (logs.toString().includes('Serving Flask app')) {
return 'running'
} else {
return 'not running'
}
}
let specterIsRunning = false
function startSpecterd(specterdPath, automaticWalletImport = false) {
if (platformName == 'win64') {
specterdPath += '.exe'
}
let hwiBridgeMode = appSettings.mode == 'hwibridge'
updatingLoaderMsg('Launching Specter ...', (showSpinner = 'true'))
updateSpecterdStatus('Launching Specter ...')
let specterdArgs = ['server']
specterdArgs.push('--no-filelog')
if (hwiBridgeMode) specterdArgs.push('--hwibridge')
if (appSettings.specterdCLIArgs != '') {
// User has inputed cli arguments in the UI
let specterdExtraArgs = appSettings.specterdCLIArgs.split(' ')
specterdExtraArgs.forEach((arg) => {
// Ensures that whitespaces are not used as cli arguments
if (arg != '') {
specterdArgs.push(arg)
}
})
}
// locale fix (copying from nodejs-env + adding locales)
const options = {
env: { ...process.env },
}
options.env['LC_ALL'] = 'en_US.utf-8'
options.env['LANG'] = 'en_US.utf-8'
options.env['SPECTER_LOGFORMAT'] = 'SPECTERD: %(levelname)s in %(module)s: %(message)s'
logger.info(`Starting specter: ${specterdPath} ${specterdArgs.join(' ')}`)
specterdProcess = spawn(specterdPath, specterdArgs, options)
const specterdStarted = Date.now()
// We are checking for both, stdout and stderr, to be on the save side.
specterdProcess.stdout.on('data', (data) => {
actOnNewLogLine(data.toString(), 'stdout')
})
specterdProcess.stderr.on('data', (data) => {
actOnNewLogLine(data.toString(), 'stderr')
})
const actOnNewLogLine = (logLine, origin) => {
logger.info(`${origin}: ${logLine.replace(/(\r\n|\n|\r)/gm, '')}`)
const serverdStatus = checkSpecterd(logLine, specterdStarted)
if (!specterIsRunning) {
if (serverdStatus === 'running') {
logger.info(`Specter server seems to run ...`)
updateSpecterdStatus('Specter is running')
specterIsRunning = true
if (automaticWalletImport === true) {
logger.info('Performing automatic wallet import ...')
updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true))
setTimeout(() => {
importWallet(walletDataFromUrl)
}, 3000)
} else {
logger.info('Normal startup of Specter.')
createWindow(appSettings.specterURL)
}
} else if (serverdStatus === 'timeout') {
showError('Specter does not seem to start. Check the logs in the menu for more details.')
updateSpecterdStatus('Specter does not start')
logger.error('Startup timeout for specterd exceeded')
} else {
updatingLoaderMsg('Still waiting for Specter to start ...')
updateSpecterdStatus('Specter is starting')
}
}
}
specterdProcess.on('exit', (code) => {
if (code !== 0) {
logger.error(`specterd exited with code ${code}`)
showError(`Specter exited with exit code ${code}. Check the logs in the menu for more details.`)
}
})
specterdProcess.on('error', (err) => {
logger.error(`Error starting Specter server: ${err}`)
showError(`Specter failed to start, due to ${err.message}. Check the logs in the menu for more details.`)
})
app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow(appSettings.specterURL)
})
// since these are streams, you can pipe them elsewhere
specterdProcess.on('close', (code) => {
updateSpecterdStatus('Specter stopped...')
logger.info(`child process exited with code ${code}`)
})
}
function quitSpecterd() {
if (specterdProcess) {
try {
if (platformName == 'win64') {
exec('taskkill /F /T /PID ' + specterdProcess.pid)
exec('taskkill /IM specterd.exe ')
process.kill(-specterdProcess.pid)
}
specterdProcess.kill('SIGINT')
} catch (e) {
logger.info('Specterd quit warning: ' + e)
}
}
}
let walletDataFromUrl
// Checking whether the app was opened via a Specter URL and determine whether to perform a specific startup action
app.on('open-url', (_, url) => {
logger.info('The app was opened via URL, checking the URL to decide whether to do any automatic actions ...')
// Parse the URL to extract the query parameters
const specterUrl = new URL(url)
const searchParams = specterUrl.searchParams
// Get the query parameter values
const action = searchParams.get('action')
const data = searchParams.get('data')
if (action === 'importWallet' && data !== '') {
logger.info('Automatic wallet import identified in the URL, setting automaticWalletImport to true.')
automaticWalletImport = true
walletDataFromUrl = data
// Directly import if the app and specterd is already running
if (specterIsRunning) {
logger.info('Performing automatic wallet import ...')
loadURL(`file://${__dirname}/splash.html`)
updatingLoaderMsg('Launching wallet importer. This will only work with a node connection.', (showSpinner = true))
setTimeout(() => {
importWallet(walletDataFromUrl)
}, 3000)
}
}
})
// Automatically import the wallet json string, bring user to the final import wallet screen.
// Only proceed with the import if the importFromWalletSoftwareBtn can be found.
// If it is not, users are redirected by specterd to the configure connection screen.
function importWallet(walletData) {
loadUrl(appSettings.specterURL + '/wallets/new_wallet/')
let code = `
const importFromWalletSoftwareBtn = document.getElementById('import-from-wallet-software-btn')
if (importFromWalletSoftwareBtn) {
importFromWalletSoftwareBtn.click()
const walletDataTextArea = document.getElementById('txt')
if (walletDataTextArea) {
walletDataTextArea.value = \`${walletData}\`
}
const continueBtn = document.getElementById('continue-with-wallet-import-btn');
continueBtn.click()
}
`
executeJavaScript(code)
}
module.exports = {
startSpecterd,
quitSpecterd,
}

View file

@ -0,0 +1,177 @@
const path = require('path')
const { Menu, BrowserWindow, nativeTheme, app, Tray, nativeImage } = require('electron')
const { logger } = require('./logging')
const { appSettings, isDev, platformName } = require('./config')
const ProgressBar = require('electron-progressbar')
const { isMac } = require('./helpers')
// Initialized with initMainWindow
let mainWindow
// Initialized with InitTray
let tray
let trayMenu
const loadUrl = (url) => {
mainWindow.loadURL(url)
}
const executeJavaScript = (code) => {
mainWindow.webContents.executeJavaScript(code)
}
function showError(error) {
updatingLoaderMsg('Specter encountered an error:' + error.toString())
}
function updatingLoaderMsg(msg, showSpinner = false) {
if (mainWindow) {
// see preload.js where this is setup
mainWindow.webContents.send('update-loader-message', {
msg,
showSpinner,
})
} else {
logger.error('mainWindow not initialized in updatingLoaderMsg')
}
logger.info('Updated LoaderMsg: ' + msg)
}
function updateSpecterdStatus(status) {
trayMenu[0] = { label: status, enabled: false }
tray.setContextMenu(Menu.buildFromTemplate(trayMenu))
}
function createWindow(specterURL) {
if (!mainWindow) {
initMainWindow()
}
// Create the browser window.
if (appSettings.tor) {
mainWindow.webContents.session.setProxy({ proxyRules: appSettings.proxyURL })
}
mainWindow.loadURL(specterURL + '?mode=remote')
}
let webPreferences = {
worldSafeExecuteJavaScript: true,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
}
function initMainWindow(dimensions) {
// In production we use the icons from the build folder
// Note: On MacOS setting an icon here as no effect
const iconPath = isDev ? path.join(__dirname, 'assets-dev/app_icon.png') : ''
mainWindow = new BrowserWindow({
width: parseInt(dimensions.width * 0.8),
minWidth: 1120,
height: parseInt(dimensions.height * 0.8),
icon: iconPath,
webPreferences,
})
// Ensures that any links with target="_blank" or window.open() will be opened in the user's default browser instead of within the app
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url)
return { action: 'deny' }
})
mainWindow.webContents.on('did-fail-load', function () {
mainWindow.loadURL(`file://${__dirname}/splash.html`)
updatingLoaderMsg(
`Failed to load: ${appSettings.specterURL}<br>Please make sure the URL is entered correctly in the settings and try again...</b>`
)
})
return mainWindow
}
const initTray = (openPreferences, quitSpecterd) => {
if (isMac) {
const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png'
const createTrayIcon = (trayIconPath) => {
let trayIcon = nativeImage.createFromPath(app.getAppPath() + trayIconPath)
// Resize
trayIcon = trayIcon.resize({ width: 22, height: 22 })
return trayIcon
}
const trayIcon = createTrayIcon(trayIconPath)
tray = new Tray(trayIcon)
// Change the tray icon if appearance is changed in Mac settings
const updateTrayIcon = () => {
logger.info('Updating tray icon ...')
const trayIconPath = nativeTheme.shouldUseDarkColors ? '/assets/menu_icon_dark.png' : '/assets/menu_icon_light.png'
const newTrayIcon = createTrayIcon(trayIconPath)
tray.setImage(newTrayIcon)
}
nativeTheme.on('updated', updateTrayIcon)
} else {
const trayIcon = nativeImage.createFromPath(app.getAppPath() + '/assets/menu_icon.png')
tray = new Tray(trayIcon)
}
trayMenu = [
{ label: 'Launching Specter ...', enabled: false },
{
label: 'Show Specter',
click() {
mainWindow.show()
},
},
{
label: 'Settings',
click() {
openPreferences()
},
},
{
label: 'Quit',
click() {
quitSpecterd()
app.quit()
},
},
]
tray.setToolTip('Specter')
tray.setContextMenu(Menu.buildFromTemplate(trayMenu))
}
const createProgressBar = (totalBytes) => {
progressBar = new ProgressBar({
indeterminate: false,
abortOnError: true,
text: 'Downloading the Specter binary from GitHub',
detail:
'This can take several minutes depending on your Internet connection. Specter will start once the download is finished.',
maxValue: totalBytes,
browserWindow: {
parent: mainWindow,
},
style: {
detail: {
'margin-bottom': '12px',
},
bar: {
'background-color': '#fff',
},
value: {
'background-color': '#000',
},
},
})
return progressBar
}
module.exports = {
loadUrl,
executeJavaScript,
showError,
updatingLoaderMsg,
updateSpecterdStatus,
createWindow,
initMainWindow,
mainWindow,
initTray,
createProgressBar,
}

View file

@ -77,7 +77,7 @@ dependencies = {file = ["requirements.in"]}
test = [ test = [
"black==22.3.0", "black==22.3.0",
"pre-commit==2.13.0", "pre-commit==2.13.0",
"pip-tools==6.12.2", "pip-tools==6.13",
"pytest==7.1.2", "pytest==7.1.2",
"PySocks==1.7.1", "PySocks==1.7.1",
"pytest-cov==2.10.1", "pytest-cov==2.10.1",

View file

@ -816,9 +816,9 @@ urllib3==1.26.14 \
--hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \ --hash=sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72 \
--hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1 --hash=sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1
# via requests # via requests
werkzeug==2.2.3 \ werkzeug==2.2.2 \
--hash=sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe \ --hash=sha256:7ea2d48322cc7c0f8b3a215ed73eabd7b5d75d0b50e31ab006286ccff9e00b8f \
--hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612 --hash=sha256:f979ab81f58d7318e064e99c4506445d60135ac5cd2e177a2de0089bfd4c9bd5
# via # via
# flask # flask
# flask-login # flask-login
@ -832,6 +832,5 @@ wtforms==3.0.1 \
# via flask-wtf # via flask-wtf
# WARNING: The following packages were not pinned, but pip requires them to be # WARNING: The following packages were not pinned, but pip requires them to be
# pinned when the requirements file includes hashes and the requirement is not # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
# satisfied by a package already installed. Consider using the --allow-unsafe flag.
# setuptools # setuptools

View file

@ -85,7 +85,7 @@ function calc_pytestinit_nodeimpl_version {
if cat ../pyproject.toml | grep -q "${node_impl}d-version" ; then if cat ../pyproject.toml | grep -q "${node_impl}d-version" ; then
# in this case, we use the expected version from the test also as the tag to be checked out # in this case, we use the expected version from the test also as the tag to be checked out
# i admit that this is REALLY ugly. Happy for any recommendations to do that more easy # i admit that this is REALLY ugly. Happy for any recommendations to do that more easy
PINNED=$(cat ../pyproject.toml | grep "addopts = " | grep -oP "${node_impl}d-version \K\S+" | cut -d'"' -f1) PINNED=$(cat ../pyproject.toml | grep "addopts = " | ${grep} -oP "${node_impl}d-version \K\S+" | cut -d'"' -f1)
if [ "$node_impl" = "elements" ]; then if [ "$node_impl" = "elements" ]; then
# in the case of elements, the tags have a "elements-" prefix # in the case of elements, the tags have a "elements-" prefix
@ -182,7 +182,14 @@ function check_compile_prerequisites {
function check_binary_prerequisites { function check_binary_prerequisites {
if [ $(uname) = "Darwin" ]; then if [ $(uname) = "Darwin" ]; then
echo " --> No binary prerequisites checking for MacOS, GOOD LUCK!" if brew list --versions grep &>/dev/null; then
grep=ggrep
else
echo "install grep via brew install grep"
exit 1
fi
echo " --> No FURTHER binary prerequisites checking for MacOS, GOOD LUCK!"
else else
REQUIRED_PKGS="wget" REQUIRED_PKGS="wget"
for REQUIRED_PKG in $REQUIRED_PKGS; do for REQUIRED_PKG in $REQUIRED_PKGS; do
@ -194,6 +201,7 @@ function check_binary_prerequisites {
apt-get --yes install $REQUIRED_PKG apt-get --yes install $REQUIRED_PKG
fi fi
done done
grep=grep
fi fi
} }
@ -212,7 +220,7 @@ function sub_compile {
if [ $(uname) = "Darwin" ]; then if [ $(uname) = "Darwin" ]; then
find tests/${node_impl}/src -maxdepth 1 -type f -perm +111 -exec ls -ld {} \; find tests/${node_impl}/src -maxdepth 1 -type f -perm +111 -exec ls -ld {} \;
else else
find tests/${node_impl}/src -maxdepth 1 -type f -executable -exec ls -ld {} \; find tests/${node_impl}/src -maxdepth 1 -type f -executable -exec ls -ld {} \;
fi fi
END=$(date +%s.%N) END=$(date +%s.%N)
DIFF=$(echo "$END - $START" | bc) DIFF=$(echo "$END - $START" | bc)
@ -251,6 +259,7 @@ function sub_binary {
rm -rf ./"$node_impl" rm -rf ./"$node_impl"
fi fi
fi fi
rm "$node_impl"
ln -s ./"$node_impl"-${version} "$node_impl" ln -s ./"$node_impl"-${version} "$node_impl"
echo " --> Listing binaries" echo " --> Listing binaries"
if [ $(uname) = "Darwin" ]; then if [ $(uname) = "Darwin" ]; then

View file

@ -11,7 +11,7 @@ function create_virtualenv_for_pyinstaller {
echo " But first Delete it ..." echo " But first Delete it ..."
rm -rf .buildenv rm -rf .buildenv
fi fi
virtualenv --python=python3 .buildenv virtualenv --python=python3.10 .buildenv
source .buildenv/bin/activate source .buildenv/bin/activate
pip3 install -e ".[test]" pip3 install -e ".[test]"
} }
@ -102,6 +102,13 @@ function make_hash_if_necessary {
} }
function building_electron_app { function building_electron_app {
# https://www.electron.build/
# Prerequisites:
# * A developer Certificate (in the System keychain)
# * private and public key in the login-keychain
# * The cert needs to be referenced in pyinstaller/electron/package.json -> build.mac.identity
platform="-- --${1}" # either linux or win (maxOS is empty) platform="-- --${1}" # either linux or win (maxOS is empty)
cd pyinstaller/electron cd pyinstaller/electron
echo " --> building electron-app" echo " --> building electron-app"
@ -113,8 +120,18 @@ function building_electron_app {
} }
function macos_code_sign { function macos_code_sign {
# prerequisites for this:
# in short:
# * make sure you have a proper app-specific password on https://appleid.apple.com/account/manage
# * collect some information via scrun altool --list-providers -u "<yourAppleID>"
# * create profile via xcrun notarytool store-credentials --apple-id "<YourAppleID>" --password "app-specific-pw" --team-id "seeFromAbove"
# * Call the profile: SpecterProfile
# For details see:
# * https://www.youtube.com/watch?v=2xJcMzoi0EI
# * https://blog.dgunia.de/2022/09/01/switching-from-altool-to-notarytool/
# * https://scriptingosx.com/2021/07/notarize-a-command-line-tool-with-notarytool/
# This creates a ZIP archive from the app package (using the ditto command). # This creates a ZIP archive from the app package (using the ditto command).
# This ZIP archive is then used to upload the app to the Apple notarization service. # This ZIP archive is then used to upload the app to the Apple notarization service via xcrun notarytool (formerly xcrun altool)
# After the app has been uploaded to the Apple servers and notarized, the ZIP archive is not used again. # After the app has been uploaded to the Apple servers and notarized, the ZIP archive is not used again.
# The function uses the xcrun stapler command to attach the notarization result to the app, and then exits. # The function uses the xcrun stapler command to attach the notarization result to the app, and then exits.
@ -124,46 +141,27 @@ function macos_code_sign {
cd pyinstaller/electron cd pyinstaller/electron
echo ' --> Attempting to code sign...' echo ' --> Attempting to code sign...'
echo ' executing: ditto -c -k --keepParent "dist/mac/${specterimg_filename}.app" dist/${specterimg_filename}.zip' echo ' executing: ditto -c -k --keepParent "dist/mac/${specterimg_filename}.app" dist/${specterimg_filename}.zip'
ditto -c -k --keepParent "dist/mac/${specterimg_filename}.app" dist/${specterimg_filename}.zip
ditto -c -k --keepParent "dist/${dist_mac_folder_name}/${specterimg_filename}.app" dist/${specterimg_filename}.zip
# upload # upload
echo ' uploading ... ' echo ' uploading ... '
echo ' executing: xcrun altool --notarize-app -t osx -f dist/${specterimg_filename}.zip --primary-bundle-id "solutions.specter.desktop" -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json'
output_json=$(xcrun altool --notarize-app -t osx -f dist/${specterimg_filename}.zip --primary-bundle-id "solutions.specter.desktop" -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json) output_json=$(xcrun notarytool submit dist/${specterimg_filename}.zip --apple-id "kneunert@gmail.com" --keychain-profile "SpecterProfile" --output-format json --wait )
echo "JSON-Output:"
echo "Request ID: "
# parsing the requestuuid which we'll need to track progress # parsing the requestuuid which we'll need to track progress
requestuuid=$(echo $output_json | jq -r '."notarization-upload".RequestUUID') requestuuid=$(echo $output_json | jq -r '.id')
mkdir -p signing_logs status=$(echo $output_json | jq -r '.status')
i=1 if [ "$status" = "Invalid" ]; then
while [ $i -le 6 ] ; do mkdir -p signing_logs
echo " check result in minute $i ..." echo "issues with notarisation"
sign_result_json=$(xcrun altool --notarization-info $requestuuid -u "${mail}" --password "@keychain:AC_PASSWORD" --output-format json) xcrun notarytool log ${requestuuid} --keychain-profile SpecterProfile | tee ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log
timestamp=$(date +"%Y%m%d-%H%M")
# If it's not json-parseable
if ! echo "$sign_result_json" | jq .; then
echo $sign_result_json > ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log
echo "ERROR: track-json not parseable."
echo "$sign_result_json"
exit 1
fi
# if it's no longer in progress
status=$(echo "$sign_result_json" | jq -e -r '.["notarization-info"].Status')
if [ "$status" != "in progress" ]; then
echo " Finished code sign with status $status"
echo $sign_result_json | jq . > ./signing_logs/${app_name}_${timestamp}_${requestuuid}.log
break
fi
i=$(( $i + 1 ))
sleep 60
done
if [ "$status" != "success" ]; then
echo "ERROR: status $status"
echo $(echo $sign_result_json | jq .)
echo
exit 1 exit 1
fi fi
# The stapler somehow "staples" the result of the notarisation in to your app # The stapler somehow "staples" the result of the notarisation in to your app
# see e.g. https://stackoverflow.com/questions/58817903/how-to-download-notarized-files-from-apple # see e.g. https://stackoverflow.com/questions/58817903/how-to-download-notarized-files-from-apple
xcrun stapler staple "dist/mac/${specterimg_filename}.app" xcrun stapler staple "dist/${dist_mac_folder_name}/${specterimg_filename}.app"
cd ../.. cd ../..
} }

View file

@ -9,15 +9,37 @@ cd ..
# Overriding this function # Overriding this function
function create_virtualenv_for_pyinstaller { function create_virtualenv_for_pyinstaller {
if [ -d .buildenv ]; then # This currently assumes to be run with: Python 3.10.11
echo " --> Deleting .buildenv"
rm -rf .buildenv
fi
# This currently assumes to be run with: Python 3.10.4
# Important: pyinstaller needs a Python binary with shared library files # Important: pyinstaller needs a Python binary with shared library files
# With pyenv, for example, you get this like so: env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.4 # With pyenv, for example, you get this like so: env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.10.4
virtualenv .buildenv # Use pyenv if available
source .buildenv/bin/activate if command -v pyenv >/dev/null 2>&1; then
### This is usually in .zshrc, putting it in .bashrc didn't work ###
###
export PYENV_ROOT="$HOME/.pyenv"
command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
### this needs the pyenv-virtualenv plugin. If you don't have it:
### git clone https://github.com/pyenv/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
eval "$(pyenv virtualenv-init -)"
### ------------------------------------------------------------ ###
PYTHON_VERSION=3.10.11
export PYENV_VERSION=$PYTHON_VERSION
echo "pyenv is available. Setting PYENV_VERSION to 3.10.4, using pyenv-virtualenv to create the buildenv..."
# echo " --> Deleting .buildenv"
# pyenv uninstall -f .buildenv
# rm -rf "$HOME/.pyenv/versions/$PYTHON_VERSION/envs/.buildenv"
# pyenv virtualenv 3.10.4 .buildenv
pyenv activate .buildenv
else
echo "pyenv is not available. Using system Python version."
if [ -d .buildenv ]; then
echo " --> Deleting .buildenv"
rm -rf .buildenv
fi
virtualenv .buildenv
source .buildenv/bin/activate
fi
pip3 install -e ".[test]" pip3 install -e ".[test]"
} }
@ -41,17 +63,10 @@ END_COMMENT
### Prerequisites ### Prerequisites
# brew install gmp # to prevent module 'embit.util' has no attribute 'ctypes_secp256k1' # brew install gmp # to prevent module 'embit.util' has no attribute 'ctypes_secp256k1'
# brew install jq
# npm install --global create-dmg # npm install --global create-dmg
### Trouble shooting ### Trouble shooting
# Currently, only MacOS Catalina is supported to build the dmg-file
# Therefore we expect xcode 12.1 (according to google)
# After installation of xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# otherwise you get xcrun: error: unable to find utility "altool", not a developer tool or in PATH
# catalina might have a a too old version of bash. You need at least 4.0 or so
# 3.2 is too low definitely
# brew install bash
# If you have the common issue "errSecInternalComponent" while signing the code: # If you have the common issue "errSecInternalComponent" while signing the code:
# https://medium.com/@ceyhunkeklik/how-to-fix-ios-application-code-signing-error-4818bd331327 # https://medium.com/@ceyhunkeklik/how-to-fix-ios-application-code-signing-error-4818bd331327
@ -67,6 +82,15 @@ We have:
* upload will upload all the binary artifacts to the github-release-page. This includes the creation of the hash-files * upload will upload all the binary artifacts to the github-release-page. This includes the creation of the hash-files
and the gnupg signing and the gnupg signing
### Trouble shooting (Legacy)
# Currently, only MacOS Catalina is supported to build the dmg-file
# Therefore we expect xcode 12.1 (according to google)
# After installation of xcode: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
# otherwise you get xcrun: error: unable to find utility "altool", not a developer tool or in PATH
# catalina might have a a too old version of bash. You need at least 4.0 or so
# 3.2 is too low definitely
# brew install bash
# Example-call: # Example-call:
./utils/build-osx.sh --debug --version v1.10.0-pre23 --appleid "Kim Neunert (FWV59JHV83)" --mail "kim@specter.solutions" make-hash specterd electron sign upload ./utils/build-osx.sh --debug --version v1.10.0-pre23 --appleid "Kim Neunert (FWV59JHV83)" --mail "kim@specter.solutions" make-hash specterd electron sign upload
EOF EOF
@ -158,9 +182,6 @@ fi
if [[ "$build_electron" = "True" ]]; then if [[ "$build_electron" = "True" ]]; then
prepare_npm prepare_npm
make_hash_if_necessary make_hash_if_necessary
npm i npm i
if [[ "${appleid}" == '' ]] if [[ "${appleid}" == '' ]]
then then
@ -174,6 +195,10 @@ if [[ "$build_electron" = "True" ]]; then
fi fi
if [[ "$build_sign" = "True" ]]; then if [[ "$build_sign" = "True" ]]; then
dist_mac_folder_name=mac
if [ "$(uname -m)" = "arm64" ]; then
dist_mac_folder_name=${dist_mac_folder_name}-arm64
fi
if [[ "$appleid" != '' ]] if [[ "$appleid" != '' ]]
then then
macos_code_sign macos_code_sign
@ -183,7 +208,7 @@ if [[ "$build_sign" = "True" ]]; then
mkdir -p release mkdir -p release
rm -rf release/* rm -rf release/*
create-dmg pyinstaller/electron/dist/mac/${specterimg_filename}.app --identity="Developer ID Application: ${appleid}" dist create-dmg pyinstaller/electron/dist/${dist_mac_folder_name}/${specterimg_filename}.app --identity="Developer ID Application: ${appleid}" dist
# create-dmg doesn't create the prepending "v" to the version # create-dmg doesn't create the prepending "v" to the version
node_comp_version=$(python3 -c "print('$version'[1:])") node_comp_version=$(python3 -c "print('$version'[1:])")
mv "dist/${specterimg_filename} ${node_comp_version}.dmg" release/${specterimg_filename}-${version}.dmg mv "dist/${specterimg_filename} ${node_comp_version}.dmg" release/${specterimg_filename}-${version}.dmg
@ -211,12 +236,12 @@ if [ "$app_name" == "specter" ]; then
echo "sha256sum * > SHA256SUMS-macos" echo "sha256sum * > SHA256SUMS-macos"
echo "python3 ../../utils/github.py upload SHA256SUMS-macos" echo "python3 ../../utils/github.py upload SHA256SUMS-macos"
echo "gpg --detach-sign --armor SHA256SUMS-macos" echo "gpg --detach-sign --armor SHA256SUMS-macos"
echo "python3 ../../utils/github.py upload SHA256SUMS-macos.asc" echo "python3 ../utils/github.py upload SHA256SUMS-macos.asc"
if [[ "$upload" = "True" ]]; then if [[ "$upload" = "True" ]]; then
echo " --> This build got triggered for version $version" echo " --> This build got triggered for version $version"
. ../../specter_gh_upload.sh . ../../specter_gh_upload.sh # A simple file looks like: export GH_BIN_UPLOAD_PW=...(GH token)
export CI_COMMIT_TAG=$version export CI_COMMIT_TAG=$version
if [[ -z "$CI_PROJECT_ROOT_NAMESPACE" ]]; then if [[ -z "$CI_PROJECT_ROOT_NAMESPACE" ]]; then
export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance export CI_PROJECT_ROOT_NAMESPACE=cryptoadvance