The window host owns the prompt and the secret (app/mac/Biometric.swift): LAPolicy.deviceOwnerAuthenticationWithBiometrics
with "Use password" as the fallback button (never the device password), the wallet's password sealed to a Secure
Enclave key made with .biometryCurrentSet (the Keychain refuses biometric access controls under the ad hoc signature,
-34018, measured) in <data>/wallet/biometric.json; a fingerprint change invalidates it. The engine owns the gate
(igneum-common/src/biometric.rs): a nonce per action, read by the host with its token (the HOST line on stdout,
X-Igneum-Host on host-only calls), confirmed after the prompt, taken once within 30 s and bound to the exact quote;
/api/send refuses without it while enrolled; /api/reveal with a nonce reads the unlocked key in memory; the password
never goes through the page (enrolment parks it under a one-time token the host takes). Idle lock after 5 minutes
without window activity (setting, default on). A password change or a wallet removal deletes the sealed file.
Reason lines in our voice ("Unlock your wallet", "Send 1.5 IGN to 0x7E5F…5Bdf", "Show your recovery words"); the page
shows its own ember line after every prompt. Windows: app/windows/biometric.h (UserConsentVerifier through
IUserConsentVerifierInterop, DPAPI), wired into wallet-host.cpp and BUILD-WALLET-APP.bat, not yet compiled on a PC.
Hosts gain a @main entry so Biometric.swift compiles alongside; build-wallet-dmg.sh links LocalAuthentication.
Balance card: the coin at 56 px, "0" (or the balance) as soon as the node answers, "reading the chain, N of M blocks"
under it while the history scans. Version: v0.1.2 in the brand band, "Igneum Wallet 0.1.2 · up to date" in Settings.
Unit tests: the gate (7, igneum-common), the wallet's 17 still green. README: the flows, the threat model, what was
verified on this Mac (enrol and unlock through the real prompt) and what was not.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
486 lines
22 KiB
C++
486 lines
22 KiB
C++
// Igneum Wallet for Windows: the window around the wallet engine. A copy of host.cpp (the miner's window) with the
|
|
// names and the tray menu changed: "Lock" instead of "Pause". Built on the PC by BUILD-WALLET-APP.bat (MSVC, the
|
|
// WebView2 SDK from NuGet, static loader). It starts igneum-wallet.exe --wrapper next to it, reads "URL ..." /
|
|
// "STATE {...}" / "EXIT" from its stdout, shows the URL in a WebView2 control, keeps a tray icon with the state, and on
|
|
// quit writes "quit" to the engine's stdin and waits for EXIT. Closing the window hides it to the tray. 4 October 2026.
|
|
// Windows Hello (biometric.h, the page's window.chrome.webview.postMessage bridge, the HOST line): 5 October 2026.
|
|
//
|
|
// Untested on a real PC at the time of writing (written on a Mac): BUILD-WALLET-APP.bat is the first run.
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#ifndef UNICODE
|
|
#define UNICODE
|
|
#endif
|
|
#ifndef _UNICODE
|
|
#define _UNICODE
|
|
#endif
|
|
#include <windows.h>
|
|
#include <shellapi.h>
|
|
#include <wrl.h>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <thread>
|
|
#include <mutex>
|
|
#include "WebView2.h"
|
|
#include "wallet-version.h"
|
|
#include "biometric.h"
|
|
|
|
using namespace Microsoft::WRL;
|
|
|
|
#define WM_ENGINE_LINE (WM_APP + 1)
|
|
#define WM_TRAY (WM_APP + 2)
|
|
#define WM_BIO_REPLY (WM_APP + 3)
|
|
#define ID_TRAY_OPEN 1001
|
|
#define ID_TRAY_PAUSE 1002
|
|
#define ID_TRAY_QUIT 1003
|
|
#define ID_QUIT_TIMER 7
|
|
#define IDI_APP 1
|
|
|
|
static HWND g_hwnd = nullptr;
|
|
static HANDLE g_engine = nullptr, g_engineIn = nullptr, g_engineOut = nullptr;
|
|
static ComPtr<ICoreWebView2Controller> g_controller;
|
|
static ComPtr<ICoreWebView2> g_webview;
|
|
static std::wstring g_url, g_status = L"starting the engine";
|
|
static std::string g_hostLine; // the engine's HOST {...} line, kept until the window and the URL exist
|
|
static bool g_paused = false, g_quitting = false, g_exited = false, g_webviewOk = false, g_hintShown = false;
|
|
static NOTIFYICONDATAW g_nid = {};
|
|
static std::wstring g_trayTitle = L"Igneum Wallet";
|
|
static ULONGLONG g_quitStarted = 0;
|
|
|
|
static std::wstring widen(const std::string& s) {
|
|
if (s.empty()) return L"";
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), nullptr, 0);
|
|
std::wstring w(n, 0);
|
|
MultiByteToWideChar(CP_UTF8, 0, s.data(), (int)s.size(), &w[0], n);
|
|
return w;
|
|
}
|
|
|
|
static std::wstring exeDir() {
|
|
wchar_t buf[MAX_PATH];
|
|
GetModuleFileNameW(nullptr, buf, MAX_PATH);
|
|
std::wstring p(buf);
|
|
size_t i = p.find_last_of(L"\\/");
|
|
return i == std::wstring::npos ? L"." : p.substr(0, i);
|
|
}
|
|
|
|
static void sendEngine(const char* line) {
|
|
if (!g_engineIn) return;
|
|
DWORD w = 0;
|
|
WriteFile(g_engineIn, line, (DWORD)strlen(line), &w, nullptr);
|
|
WriteFile(g_engineIn, "\n", 1, &w, nullptr);
|
|
}
|
|
|
|
static void setTray(const std::wstring& tip) {
|
|
g_trayTitle = tip;
|
|
wcsncpy_s(g_nid.szTip, tip.c_str(), _TRUNCATE);
|
|
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
|
}
|
|
|
|
static void repaintStatus() {
|
|
InvalidateRect(g_hwnd, nullptr, TRUE);
|
|
}
|
|
|
|
// A tiny JSON number/string/bool reader for the STATE line (flat object, known keys).
|
|
static std::string jsonField(const std::string& j, const char* key) {
|
|
std::string k = std::string("\"") + key + "\":";
|
|
size_t i = j.find(k);
|
|
if (i == std::string::npos) return "";
|
|
i += k.size();
|
|
while (i < j.size() && j[i] == ' ') i++;
|
|
if (i < j.size() && j[i] == '"') {
|
|
size_t e = j.find('"', i + 1);
|
|
return e == std::string::npos ? "" : j.substr(i + 1, e - i - 1);
|
|
}
|
|
size_t e = i;
|
|
while (e < j.size() && j[e] != ',' && j[e] != '}') e++;
|
|
return j.substr(i, e - i);
|
|
}
|
|
|
|
static void applyState(const std::string& j) {
|
|
std::string mining = jsonField(j, "mining"), node = jsonField(j, "node"), phase = jsonField(j, "phase");
|
|
g_paused = jsonField(j, "paused") == "true";
|
|
double hash = atof(jsonField(j, "hash_total").c_str());
|
|
std::string blocks = jsonField(j, "blocks"), accepted = jsonField(j, "accepted_total");
|
|
wchar_t tip[128];
|
|
if (jsonField(j, "quitting") == "true") swprintf_s(tip, L"Igneum Wallet: stopping");
|
|
else if (phase != "dashboard") swprintf_s(tip, L"Igneum Wallet: set up");
|
|
else if (mining == "mining") swprintf_s(tip, L"Igneum Wallet: %.1f MH/s, %S blocks found, node %S", hash, accepted.c_str(), node.c_str());
|
|
else if (mining == "paused") swprintf_s(tip, L"Igneum Wallet: paused, node %S", node.c_str());
|
|
else swprintf_s(tip, L"Igneum Wallet: %S, node %S (%S blocks)", mining.c_str(), node.c_str(), blocks.c_str());
|
|
setTray(tip);
|
|
}
|
|
|
|
// The engine asks for an elevated step (the NVIDIA power cap): this process has a UI context, so the UAC prompt shows.
|
|
// Runs cmd /c <line> as administrator, waits, and answers on the engine's stdin.
|
|
static void runElevated(std::wstring line) {
|
|
std::thread([line] {
|
|
std::wstring params = L"/c " + line;
|
|
wchar_t sysdir[MAX_PATH];
|
|
GetSystemDirectoryW(sysdir, MAX_PATH);
|
|
std::wstring cmdExe = std::wstring(sysdir) + L"\\cmd.exe"; // the absolute path, never a bare name (R4.3.3)
|
|
SHELLEXECUTEINFOW sei = { sizeof(sei) };
|
|
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
|
|
sei.lpVerb = L"runas";
|
|
sei.lpFile = cmdExe.c_str();
|
|
sei.lpParameters = params.c_str();
|
|
sei.nShow = SW_HIDE;
|
|
if (!ShellExecuteExW(&sei) || !sei.hProcess) {
|
|
DWORD err = GetLastError();
|
|
sendEngine(err == ERROR_CANCELLED ? "elevated fail: the administrator prompt was cancelled" : "elevated fail: could not start the elevated step");
|
|
return;
|
|
}
|
|
WaitForSingleObject(sei.hProcess, 120000);
|
|
DWORD code = 1;
|
|
GetExitCodeProcess(sei.hProcess, &code);
|
|
CloseHandle(sei.hProcess);
|
|
if (code == 0) sendEngine("elevated ok");
|
|
else { char buf[64]; sprintf_s(buf, "elevated fail: exit code %lu", code); sendEngine(buf); }
|
|
}).detach();
|
|
}
|
|
|
|
static void openInBrowser(const std::wstring& url) {
|
|
ShellExecuteW(nullptr, L"open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
|
|
}
|
|
|
|
static void navigate() {
|
|
if (g_webview && !g_url.empty()) g_webview->Navigate((g_url + L"?host=windows").c_str());
|
|
}
|
|
|
|
static void initWebView() {
|
|
std::wstring data = L"";
|
|
wchar_t* local = nullptr;
|
|
size_t len = 0;
|
|
if (_wdupenv_s(&local, &len, L"LOCALAPPDATA") == 0 && local) { data = std::wstring(local) + L"\\igneum\\webview2"; free(local); }
|
|
HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(nullptr, data.empty() ? nullptr : data.c_str(), nullptr,
|
|
Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>([](HRESULT result, ICoreWebView2Environment* env) -> HRESULT {
|
|
if (FAILED(result) || !env) {
|
|
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
return S_OK;
|
|
}
|
|
env->CreateCoreWebView2Controller(g_hwnd, Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>([](HRESULT result, ICoreWebView2Controller* controller) -> HRESULT {
|
|
if (FAILED(result) || !controller) {
|
|
g_status = L"The app window could not start WebView2. The dashboard opens in your browser.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
return S_OK;
|
|
}
|
|
g_controller = controller;
|
|
g_controller->get_CoreWebView2(&g_webview);
|
|
g_webviewOk = true;
|
|
ComPtr<ICoreWebView2Settings> settings;
|
|
if (g_webview && SUCCEEDED(g_webview->get_Settings(&settings)) && settings) {
|
|
settings->put_AreDefaultContextMenusEnabled(FALSE);
|
|
settings->put_IsStatusBarEnabled(FALSE);
|
|
settings->put_AreDevToolsEnabled(FALSE);
|
|
}
|
|
// links off 127.0.0.1 open in the default browser
|
|
g_webview->add_NewWindowRequested(Callback<ICoreWebView2NewWindowRequestedEventHandler>([](ICoreWebView2*, ICoreWebView2NewWindowRequestedEventArgs* args) -> HRESULT {
|
|
LPWSTR uri = nullptr;
|
|
if (SUCCEEDED(args->get_Uri(&uri)) && uri) { openInBrowser(uri); CoTaskMemFree(uri); }
|
|
args->put_Handled(TRUE);
|
|
return S_OK;
|
|
}).Get(), nullptr);
|
|
g_webview->add_NavigationStarting(Callback<ICoreWebView2NavigationStartingEventHandler>([](ICoreWebView2*, ICoreWebView2NavigationStartingEventArgs* args) -> HRESULT {
|
|
LPWSTR uri = nullptr;
|
|
if (SUCCEEDED(args->get_Uri(&uri)) && uri) {
|
|
std::wstring u(uri);
|
|
CoTaskMemFree(uri);
|
|
if (u.rfind(L"http", 0) == 0 && u.find(L"127.0.0.1") == std::wstring::npos) { args->put_Cancel(TRUE); openInBrowser(u); }
|
|
}
|
|
return S_OK;
|
|
}).Get(), nullptr);
|
|
// the page's Windows Hello bridge (biometric.h): a JSON string in, a JSON object back on the UI thread
|
|
g_webview->add_WebMessageReceived(Callback<ICoreWebView2WebMessageReceivedEventHandler>([](ICoreWebView2*, ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT {
|
|
LPWSTR src = nullptr;
|
|
if (FAILED(args->get_Source(&src)) || !src) return S_OK;
|
|
std::wstring origin(src);
|
|
CoTaskMemFree(src);
|
|
if (origin.rfind(L"http://127.0.0.1:", 0) != 0) return S_OK; // only the engine's own page
|
|
LPWSTR text = nullptr;
|
|
if (SUCCEEDED(args->TryGetWebMessageAsString(&text)) && text) {
|
|
std::string msg = igbio::narrow8(text);
|
|
CoTaskMemFree(text);
|
|
igbio::handle(msg, [](const std::string& json) { PostMessageW(g_hwnd, WM_BIO_REPLY, 0, (LPARAM) new std::string(json)); });
|
|
}
|
|
return S_OK;
|
|
}).Get(), nullptr);
|
|
RECT rc; GetClientRect(g_hwnd, &rc);
|
|
g_controller->put_Bounds(rc);
|
|
COREWEBVIEW2_COLOR dark = { 255, 12, 12, 14 };
|
|
ComPtr<ICoreWebView2Controller2> c2;
|
|
if (SUCCEEDED(g_controller.As(&c2)) && c2) c2->put_DefaultBackgroundColor(dark);
|
|
g_status = L"";
|
|
repaintStatus();
|
|
navigate();
|
|
return S_OK;
|
|
}).Get());
|
|
return S_OK;
|
|
}).Get());
|
|
if (FAILED(hr)) {
|
|
g_status = L"The WebView2 runtime is not installed. The dashboard opens in your browser; install the runtime from microsoft.com for the app window.";
|
|
repaintStatus();
|
|
if (!g_url.empty()) { openInBrowser(g_url); g_hintShown = true; }
|
|
}
|
|
}
|
|
|
|
static bool startEngine() {
|
|
SECURITY_ATTRIBUTES sa = { sizeof(sa), nullptr, TRUE };
|
|
HANDLE outR, outW, inR, inW;
|
|
if (!CreatePipe(&outR, &outW, &sa, 0) || !CreatePipe(&inR, &inW, &sa, 0)) return false;
|
|
SetHandleInformation(outR, HANDLE_FLAG_INHERIT, 0);
|
|
SetHandleInformation(inW, HANDLE_FLAG_INHERIT, 0);
|
|
STARTUPINFOW si = { sizeof(si) };
|
|
si.dwFlags = STARTF_USESTDHANDLES;
|
|
si.hStdOutput = outW;
|
|
si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
|
|
si.hStdInput = inR;
|
|
PROCESS_INFORMATION pi = {};
|
|
std::wstring exe = exeDir() + L"\\igneum-wallet.exe";
|
|
std::wstring cmd = L"\"" + exe + L"\" --wrapper";
|
|
std::vector<wchar_t> buf(cmd.begin(), cmd.end());
|
|
buf.push_back(0);
|
|
BOOL ok = CreateProcessW(exe.c_str(), buf.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, exeDir().c_str(), &si, &pi);
|
|
CloseHandle(outW);
|
|
CloseHandle(inR);
|
|
if (!ok) { CloseHandle(outR); CloseHandle(inW); return false; }
|
|
CloseHandle(pi.hThread);
|
|
g_engine = pi.hProcess;
|
|
g_engineIn = inW;
|
|
g_engineOut = outR;
|
|
std::thread([] {
|
|
std::string acc;
|
|
char chunk[4096];
|
|
DWORD n;
|
|
while (ReadFile(g_engineOut, chunk, sizeof(chunk), &n, nullptr) && n > 0) {
|
|
acc.append(chunk, n);
|
|
size_t nl;
|
|
while ((nl = acc.find('\n')) != std::string::npos) {
|
|
std::string line = acc.substr(0, nl);
|
|
acc.erase(0, nl + 1);
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
PostMessageW(g_hwnd, WM_ENGINE_LINE, 0, (LPARAM) new std::string(line));
|
|
}
|
|
}
|
|
PostMessageW(g_hwnd, WM_ENGINE_LINE, 1, 0);
|
|
}).detach();
|
|
return true;
|
|
}
|
|
|
|
static void showTrayMenu() {
|
|
HMENU m = CreatePopupMenu();
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_OPEN, L"Open Igneum Wallet");
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_PAUSE, g_paused ? L"Lock" : L"Lock");
|
|
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
|
AppendMenuW(m, MF_STRING | MF_GRAYED, 0, g_trayTitle.c_str());
|
|
AppendMenuW(m, MF_SEPARATOR, 0, nullptr);
|
|
AppendMenuW(m, MF_STRING, ID_TRAY_QUIT, L"Quit Igneum Wallet");
|
|
POINT pt; GetCursorPos(&pt);
|
|
SetForegroundWindow(g_hwnd);
|
|
TrackPopupMenu(m, TPM_RIGHTBUTTON | TPM_BOTTOMALIGN, pt.x, pt.y, 0, g_hwnd, nullptr);
|
|
DestroyMenu(m);
|
|
}
|
|
|
|
static void beginQuit() {
|
|
if (g_quitting) return;
|
|
g_quitting = true;
|
|
g_quitStarted = GetTickCount64();
|
|
g_status = L"Stopping: the miner first, then the node";
|
|
setTray(L"Igneum Wallet: stopping");
|
|
if (g_webview) g_webview->ExecuteScript(L"document.getElementById('pill-text').textContent='stopping'", nullptr);
|
|
if (g_engine && !g_exited) {
|
|
sendEngine("quit");
|
|
SetTimer(g_hwnd, ID_QUIT_TIMER, 500, nullptr);
|
|
} else {
|
|
DestroyWindow(g_hwnd);
|
|
}
|
|
}
|
|
|
|
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
|
switch (msg) {
|
|
case WM_SIZE:
|
|
if (g_controller) { RECT rc; GetClientRect(hwnd, &rc); g_controller->put_Bounds(rc); }
|
|
return 0;
|
|
case WM_ENGINE_LINE: {
|
|
if (wp == 1) {
|
|
g_exited = true;
|
|
if (g_quitting) { DestroyWindow(hwnd); return 0; }
|
|
g_status = L"The engine stopped. Close this window and open Igneum Wallet again.";
|
|
setTray(L"Igneum Wallet: stopped");
|
|
repaintStatus();
|
|
return 0;
|
|
}
|
|
std::string* line = (std::string*)lp;
|
|
if (line->rfind("URL ", 0) == 0) {
|
|
g_url = widen(line->substr(4));
|
|
if (g_webviewOk) navigate();
|
|
else if (!g_webview && g_status.find(L"WebView2") != std::wstring::npos && !g_hintShown) { openInBrowser(g_url); g_hintShown = true; }
|
|
} else if (line->rfind("HOST ", 0) == 0) {
|
|
// the host token and the sealed file's path; the page never sees this line
|
|
g_hostLine = line->substr(5);
|
|
igbio::configure(g_hostLine, g_url, L"Igneum Wallet", L"Turn on Windows Hello for your wallet", hwnd);
|
|
} else if (line->rfind("STATE ", 0) == 0) {
|
|
applyState(line->substr(6));
|
|
} else if (line->rfind("ELEVATE ", 0) == 0) {
|
|
runElevated(widen(line->substr(8)));
|
|
} else if (line->rfind("FATAL ", 0) == 0) {
|
|
g_status = widen(line->substr(6));
|
|
repaintStatus();
|
|
MessageBoxW(hwnd, g_status.c_str(), L"Igneum Wallet", MB_OK | MB_ICONERROR);
|
|
} else if (*line == "EXIT") {
|
|
g_exited = true;
|
|
if (g_quitting) DestroyWindow(hwnd);
|
|
}
|
|
delete line;
|
|
return 0;
|
|
}
|
|
case WM_BIO_REPLY: {
|
|
std::string* json = (std::string*)lp;
|
|
if (g_webview) g_webview->PostWebMessageAsJson(igbio::widen8(*json).c_str());
|
|
delete json;
|
|
return 0;
|
|
}
|
|
case WM_TIMER:
|
|
if (wp == ID_QUIT_TIMER) {
|
|
DWORD code = 0;
|
|
bool gone = !g_engine || (GetExitCodeProcess(g_engine, &code) && code != STILL_ACTIVE);
|
|
if (gone || g_exited || GetTickCount64() - g_quitStarted > 45000) {
|
|
if (!gone && g_engine) TerminateProcess(g_engine, 1);
|
|
KillTimer(hwnd, ID_QUIT_TIMER);
|
|
DestroyWindow(hwnd);
|
|
}
|
|
}
|
|
return 0;
|
|
case WM_TRAY:
|
|
if (lp == WM_LBUTTONUP || lp == WM_LBUTTONDBLCLK) { ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); }
|
|
else if (lp == WM_RBUTTONUP || lp == WM_CONTEXTMENU) showTrayMenu();
|
|
return 0;
|
|
case WM_COMMAND:
|
|
switch (LOWORD(wp)) {
|
|
case ID_TRAY_OPEN: ShowWindow(hwnd, SW_SHOW); SetForegroundWindow(hwnd); return 0;
|
|
case ID_TRAY_PAUSE: sendEngine(g_paused ? "lock" : "lock"); return 0;
|
|
case ID_TRAY_QUIT: beginQuit(); return 0;
|
|
}
|
|
return 0;
|
|
case WM_CLOSE:
|
|
// hide to the tray; the miner keeps running
|
|
ShowWindow(hwnd, SW_HIDE);
|
|
if (!g_hintShown) {
|
|
g_nid.uFlags |= NIF_INFO;
|
|
wcscpy_s(g_nid.szInfoTitle, L"Igneum Wallet keeps mining");
|
|
wcscpy_s(g_nid.szInfo, L"The window is in the tray. Right-click the icon to pause or quit.");
|
|
g_nid.dwInfoFlags = NIIF_INFO;
|
|
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
|
|
g_nid.uFlags &= ~NIF_INFO;
|
|
g_hintShown = true;
|
|
}
|
|
return 0;
|
|
case WM_PAINT: {
|
|
PAINTSTRUCT ps;
|
|
HDC dc = BeginPaint(hwnd, &ps);
|
|
RECT rc; GetClientRect(hwnd, &rc);
|
|
HBRUSH bg = CreateSolidBrush(RGB(12, 12, 14));
|
|
FillRect(dc, &rc, bg);
|
|
DeleteObject(bg);
|
|
if (!g_status.empty()) {
|
|
SetBkMode(dc, TRANSPARENT);
|
|
SetTextColor(dc, RGB(154, 154, 158));
|
|
HFONT f = CreateFontW(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0, DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
|
|
HFONT old = (HFONT)SelectObject(dc, f);
|
|
RECT tr = rc; tr.left += 40; tr.right -= 40;
|
|
DrawTextW(dc, g_status.c_str(), -1, &tr, DT_CENTER | DT_VCENTER | DT_WORDBREAK | DT_NOPREFIX | (g_webviewOk ? DT_BOTTOM : DT_VCENTER));
|
|
SelectObject(dc, old);
|
|
DeleteObject(f);
|
|
}
|
|
EndPaint(hwnd, &ps);
|
|
return 0;
|
|
}
|
|
case WM_DESTROY:
|
|
Shell_NotifyIconW(NIM_DELETE, &g_nid);
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
// --version and --help print one line and exit, for the CI smoke run and support scripts. A windows-subsystem exe has
|
|
// no console of its own: the text goes to the inherited stdout when there is one (a pipe or a file), else to the
|
|
// parent's console.
|
|
static bool handleCliFlags() {
|
|
int argc = 0;
|
|
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
|
if (!argv) return false;
|
|
std::wstring text;
|
|
for (int i = 1; i < argc; i++) {
|
|
std::wstring a = argv[i];
|
|
if (a == L"--version" || a == L"-V") text = L"Igneum Wallet " IGNEUM_HOST_VERSION_STR L" (window host)\r\n";
|
|
else if (a == L"--help" || a == L"-h" || a == L"/?")
|
|
text = L"Igneum Wallet " IGNEUM_HOST_VERSION_STR L" (window host)\r\n"
|
|
L"Usage: \"Igneum Wallet.exe\" [--version | --help]\r\n"
|
|
L"Without flags it starts igneum-wallet.exe --wrapper next to it and shows the dashboard in a WebView2 window.\r\n";
|
|
}
|
|
LocalFree(argv);
|
|
if (text.empty()) return false;
|
|
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
if (out == nullptr || out == INVALID_HANDLE_VALUE) {
|
|
if (AttachConsole(ATTACH_PARENT_PROCESS)) out = GetStdHandle(STD_OUTPUT_HANDLE);
|
|
}
|
|
if (out && out != INVALID_HANDLE_VALUE) {
|
|
int n = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), nullptr, 0, nullptr, nullptr);
|
|
std::string utf8(n, 0);
|
|
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), (int)text.size(), &utf8[0], n, nullptr, nullptr);
|
|
DWORD written = 0;
|
|
WriteFile(out, utf8.data(), (DWORD)utf8.size(), &written, nullptr);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) {
|
|
if (handleCliFlags()) return 0;
|
|
HANDLE once = CreateMutexW(nullptr, TRUE, L"Local\\IgneumWalletWindow");
|
|
if (GetLastError() == ERROR_ALREADY_EXISTS) {
|
|
HWND other = FindWindowW(L"IgneumWalletWindow", nullptr);
|
|
if (other) { ShowWindow(other, SW_SHOW); SetForegroundWindow(other); }
|
|
return 0;
|
|
}
|
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
|
WNDCLASSW wc = {};
|
|
wc.lpfnWndProc = WndProc;
|
|
wc.hInstance = hInst;
|
|
wc.lpszClassName = L"IgneumWalletWindow";
|
|
wc.hIcon = LoadIconW(hInst, MAKEINTRESOURCEW(IDI_APP));
|
|
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
|
wc.hbrBackground = CreateSolidBrush(RGB(12, 12, 14));
|
|
RegisterClassW(&wc);
|
|
int w = 1120, h = 820;
|
|
int sx = (GetSystemMetrics(SM_CXSCREEN) - w) / 2, sy = (GetSystemMetrics(SM_CYSCREEN) - h) / 2;
|
|
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"Igneum Wallet", WS_OVERLAPPEDWINDOW, sx, sy, w, h, nullptr, nullptr, hInst, nullptr);
|
|
ShowWindow(g_hwnd, SW_SHOW);
|
|
|
|
g_nid.cbSize = sizeof(g_nid);
|
|
g_nid.hWnd = g_hwnd;
|
|
g_nid.uID = 1;
|
|
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
|
g_nid.uCallbackMessage = WM_TRAY;
|
|
g_nid.hIcon = (HICON)LoadImageW(hInst, MAKEINTRESOURCEW(IDI_APP), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR);
|
|
wcscpy_s(g_nid.szTip, L"Igneum Wallet: starting");
|
|
Shell_NotifyIconW(NIM_ADD, &g_nid);
|
|
|
|
if (!startEngine()) {
|
|
g_status = L"igneum-wallet.exe is missing next to this program. Run the installer again.";
|
|
repaintStatus();
|
|
MessageBoxW(g_hwnd, g_status.c_str(), L"Igneum Wallet", MB_OK | MB_ICONERROR);
|
|
}
|
|
initWebView();
|
|
|
|
MSG msg;
|
|
while (GetMessageW(&msg, nullptr, 0, 0)) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
if (g_engine) { CloseHandle(g_engine); }
|
|
CloseHandle(once);
|
|
CoUninitialize();
|
|
return 0;
|
|
}
|