forked from cheng/wallet
442 lines
17 KiB
C++
442 lines
17 KiB
C++
#include "stdafx.h"
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// frame
|
|
// ----------------------------------------------------------------------------
|
|
|
|
void Frame::RestorePositionFromConfig(const wxSize& bestSize) {
|
|
// SetPath() understands ".." but you should probably never use it.
|
|
singletonApp->pConfig->SetPath(_T("/MainFrame")); wxPoint scr{ wxSystemSettings::GetMetric(wxSYS_SCREEN_X), wxSystemSettings::GetMetric(wxSYS_SCREEN_Y) };
|
|
// restore frame position and size
|
|
int x = singletonApp->pConfig->ReadLong(_T("x"), scr.x / 4);
|
|
int y = singletonApp->pConfig->ReadLong(_T("y"), scr.y / 4);
|
|
int w = singletonApp->pConfig->ReadLong(_T("w"), scr.x / 2);
|
|
int h = singletonApp->pConfig->ReadLong(_T("h"), scr.y / 2);
|
|
w = std::min(std::max(std::max(w, scr.x / 5), bestSize.GetWidth()), 8 * scr.x / 9);
|
|
h = std::min(std::max(std::max(h, scr.y / 9), bestSize.GetHeight()), 4 * scr.y / 5);
|
|
x = std::max(scr.x / 12, std::min(x, scr.x - w - scr.x / 12));
|
|
y = std::max(scr.y / 10, std::min(y, scr.y - h - scr.y / 10));
|
|
this->Move(x, y);
|
|
this->Maximize(singletonApp->pConfig->ReadBool(_T("Maximized"), false));
|
|
this->SetSize(w, h);
|
|
singletonApp->pConfig->SetPath(_T("/"));
|
|
if (singletonApp->m_display || m_pLogWindow != nullptr) {
|
|
m_pLogWindow->GetFrame()->SetSize(w, h);
|
|
if (singletonApp->m_display_in_front) {
|
|
m_pLogWindow->GetFrame()->Move(std::min(x + 46, scr.x - w), std::min(y + 46, scr.y - h));
|
|
}
|
|
else {
|
|
m_pLogWindow->GetFrame()->Move(std::max(x - 32, 0), std::max(y - 32, 0));
|
|
}
|
|
m_pLogWindow->GetFrame()->SetTitle(sz_unit_test_log);
|
|
m_pLogWindow->Show(true);
|
|
}
|
|
else {
|
|
m_pLogNull = std::make_unique<wxLogNull>();
|
|
}
|
|
}
|
|
|
|
void Frame::StorePositionToConfig() {
|
|
if (singletonApp->pConfig) {
|
|
singletonApp->pConfig->SetPath(_T("/MainFrame"));
|
|
if (this->IsMaximized()) {
|
|
singletonApp->pConfig->Write(_T("Maximized"), true);
|
|
}
|
|
else {
|
|
// save the frame position
|
|
int x, y, w, h;
|
|
this->GetSize(&w, &h);
|
|
this->GetPosition(&x, &y);
|
|
singletonApp->pConfig->Write(_T("x"), (long)x);
|
|
singletonApp->pConfig->Write(_T("y"), (long)y);
|
|
singletonApp->pConfig->Write(_T("w"), (long)w);
|
|
singletonApp->pConfig->Write(_T("h"), (long)h);
|
|
singletonApp->pConfig->Write(_T("Maximized"), false);
|
|
}
|
|
singletonApp->pConfig->SetPath(_T("/"));
|
|
}
|
|
}
|
|
|
|
// main frame ctor
|
|
Frame::Frame(wxString wxs)
|
|
: wxFrame(nullptr, myID_MAINFRAME, wxs, wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE, wxs),
|
|
m_panel(),
|
|
m_LastUsedSqlite()
|
|
{
|
|
try {
|
|
assert(singletonFrame == nullptr);
|
|
singletonFrame = this;
|
|
SetIcon(wxICON(AAArho));
|
|
sqlite3_init();
|
|
if (sodium_init() == -1) {
|
|
szError = "Fatal Error: Encryption library did not init.";
|
|
errorCode = 6;
|
|
// Cannot log the error, because logging not set up yet, so logging itself causes an exception
|
|
throw FatalException(szError.c_str());
|
|
}
|
|
wxIdleEvent::SetMode(wxIDLE_PROCESS_SPECIFIED);
|
|
if (singletonApp->m_display || singletonApp->m_log_focus_events) {
|
|
m_pLogNull.reset(nullptr);
|
|
m_pLogWindow = new wxLogWindow(this, wxs, false, false);
|
|
wxLog::EnableLogging(true);
|
|
wxLog::SetActiveTarget(m_pLogWindow);
|
|
m_pLogWindow->GetFrame()->SetName(sz_unit_test_log);
|
|
m_pLogWindow->GetFrame()->SetIcon(wxICON(AAArho));
|
|
if (singletonApp->m_unit_test) {
|
|
wxLogMessage(_T("Command line specified %s unit test with%s exit on completion of unit test."),
|
|
singletonApp->m_complete_unit_test?_T("complete"): singletonApp->m_quick_unit_test?_T("quick"):_T(""),
|
|
singletonApp->m_display ? _T("out") : _T(""));
|
|
wxLogMessage(_T("If an error occurs during unit test, the program will return a non zero "
|
|
"error number on exit."));
|
|
wxLogMessage(_T(""));
|
|
}
|
|
}else {
|
|
wxLog::EnableLogging(false);
|
|
wxLog::SetActiveTarget(nullptr);
|
|
m_pLogNull.reset(new wxLogNull());
|
|
}
|
|
if (singletonApp->m_unit_test) singletonApp->Bind(
|
|
wxEVT_IDLE,
|
|
&UnitTest
|
|
);
|
|
if (singletonApp->m_log_focus_events) {
|
|
wxLogMessage(_T("Logging focus events"));
|
|
wxLogMessage(_T(""));
|
|
}
|
|
if (singletonApp->m_params.empty()) {
|
|
wxLogMessage(_T("No wallet specified. Attempting to open last used wallet"));
|
|
}else {
|
|
wxString subcommands( _T(""));
|
|
for (auto& str : singletonApp->m_params) {
|
|
subcommands += str + _T(" ");
|
|
}
|
|
wxLogMessage(_T("command argument%s %s"), singletonApp->m_params.size()==1?"":"s", subcommands);
|
|
wxLogMessage(_T("attempting to open %s"), singletonApp->m_params[0]);
|
|
}
|
|
SetIcon(wxICON(AAArho)); //Does not appear to do anything. Maybe it does something in Unix.
|
|
//wxICON is a namestring on windows, and a symbol on Unix
|
|
Bind(wxEVT_CLOSE_WINDOW, &Frame::OnClose, this);
|
|
Bind(wxEVT_CLOSE_WINDOW, &Frame::OnClose, this);
|
|
wxMenu* menuFile = new wxMenu;
|
|
|
|
menuFile->Append(wxID_NEW, menu_strings[0].tail[0][0], menu_strings[0].tail[0][1]);
|
|
menuFile->Bind(wxEVT_MENU, &Frame::OnSaveNew, this, wxID_NEW);
|
|
|
|
menuFile->Append(wxID_REFRESH, menu_strings[0].tail[1][0], menu_strings[0].tail[1][1]);
|
|
menuFile->Bind(wxEVT_MENU, &Frame::RecreateWalletFromExistingSecret, this, wxID_REFRESH);
|
|
|
|
menuFile->Append(wxID_OPEN, menu_strings[0].tail[2][0], menu_strings[0].tail[2][1]);
|
|
menuFile->Bind(wxEVT_MENU, &Frame::OnFileOpen, this, wxID_OPEN);
|
|
|
|
menuFile->Append(wxID_DELETE, menu_strings[0].tail[3][0], menu_strings[0].tail[3][1] + m_LastUsedSqlite.GetFullPath());
|
|
menuFile->Bind(wxEVT_MENU, &Frame::OnDelete, this, wxID_DELETE);
|
|
|
|
menuFile->AppendSeparator();
|
|
|
|
menuFile->Append(myID_DELETECONFIG, menu_strings[0].tail[4][0], menu_strings[0].tail[4][1] + m_LastUsedSqlite.GetFullPath());
|
|
menuFile->Bind(wxEVT_MENU, &Frame::OnDeleteConfiguration, this, myID_DELETECONFIG);
|
|
|
|
wxMenu* menuHelp = new wxMenu;
|
|
menuHelp->Append(wxID_ABOUT);
|
|
menuHelp->Bind(wxEVT_MENU, &Frame::OnAbout, this, wxID_ABOUT);
|
|
menuHelp->Append(myID_ADD_SUBWINDOW, _T("insert new window"), _T("insert"));
|
|
menuHelp->Bind(wxEVT_MENU, &Frame::OnAddSubwindow, this, myID_ADD_SUBWINDOW);
|
|
menu_OnDeleteSubwindow.Append(menuHelp,this, _T("delete first subwindow"), _T("delete"));
|
|
menuHelp->Append(myID_DELETE_LAST_SUBWINDOW, _T("delete last subwindow"), _T("delete"));
|
|
menuHelp->Bind(wxEVT_MENU, &Frame::OnDeleteLastSubwindow, this, myID_DELETE_LAST_SUBWINDOW);
|
|
// menu_OnFirstUse.Append(menuHelp, this, _T("New wallet"), _T("add new wallet"));
|
|
wxMenuBar* menuBar = new wxMenuBar;
|
|
menuBar->Append(menuFile, menu_strings[0].head);
|
|
menuBar->Append(new wxMenu, menu_strings[1].head);
|
|
menuBar->Append(menuHelp, menu_strings[2].head);
|
|
SetMenuBar(menuBar);
|
|
CreateStatusBar();
|
|
// child controls
|
|
m_LastUsedSqlite.Assign(singletonApp->pConfig->Read(_T("/Wallet/LastUsed"), _T("")));
|
|
wxString debug_data{ m_LastUsedSqlite.GetFullPath() };
|
|
if (!m_LastUsedSqlite.IsOk() || !m_LastUsedSqlite.HasName() || !m_LastUsedSqlite.HasExt()) {
|
|
m_panel = new welcome_to_rhocoin(this); //Owner is "this", via the base class wxFrame. m_panel is a
|
|
// non owning pointer in the derived class that duplicates the owning pointer in the base class.
|
|
}
|
|
else {
|
|
display_wallet* panel = new display_wallet(this, m_LastUsedSqlite);
|
|
m_panel = panel;
|
|
}
|
|
this->RestorePositionFromConfig(ClientToWindowSize(m_panel->GetBestSize()));
|
|
SetClientSize(GetClientSize());
|
|
}
|
|
catch (const std::exception& e) {
|
|
// cannot throw when no window is available. Construction of the base frame has to be completed, come what may.
|
|
// if an exception propagated from the constructor of the derived frame, it would destruct the base frame
|
|
// and chaos would ensue as a windowing program attempts to handle an error with no main window.
|
|
// so exceptions in the constructor of the main frame have to be caught and not rethrown.
|
|
queue_error_message(e.what());
|
|
}
|
|
}
|
|
void Frame::OnExit(wxCommandEvent& event) {
|
|
Close(true);
|
|
}
|
|
|
|
void Frame::OnClose(wxCloseEvent& event) {
|
|
// This event gives you the opportunity to clean up anything that needs explicit cleanup, albeit if you have done your work right nothing should need explicit cleanup,
|
|
// and to object to the closing in a "file not saved" type situation.
|
|
// https://docs.wxwidgets.org/trunk/classwx_close_event.html
|
|
if (sqlite3_shutdown())wxMessageBox(_T(R"|(Sqlite3 shutdown error)|"), _T("Error"), wxICON_ERROR);
|
|
Destroy(); //Default handler will destroy the window. This is our handler for the user calling close, replacing the default handler.
|
|
}
|
|
|
|
void Frame::OnAbout(wxCommandEvent& event)
|
|
{
|
|
wxMessageBox("This is a wxWidgets' Config sample",
|
|
"About sample code", wxOK | wxICON_INFORMATION);
|
|
}
|
|
|
|
void Frame::OnDeleteConfiguration(wxCommandEvent&)
|
|
{
|
|
std::unique_ptr<wxConfigBase>pConfig{ wxConfigBase::Set(nullptr) };
|
|
if (pConfig)
|
|
{
|
|
if (pConfig->DeleteAll())
|
|
{
|
|
wxLogMessage(_T("Config file/registry key successfully deleted."));
|
|
wxConfigBase::DontCreateOnDemand();
|
|
pConfig.release();
|
|
}
|
|
else
|
|
{
|
|
wxLogError(_T("Deleting config file/registry key failed."));
|
|
}
|
|
}
|
|
else {
|
|
wxLogError(_T("No config to delete!"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
void Frame::OnAddSubwindow(wxCommandEvent& WXUNUSED(event)) {
|
|
wxBoxSizer* sizer(static_cast<wxBoxSizer*>(m_panel->GetSizer()));
|
|
sizer->Prepend(new wxStaticText(m_panel, myID_TESTWINDOW, _T("test")));
|
|
sizer->Layout();
|
|
}
|
|
|
|
void Frame::OnDeleteSubwindow(wxCommandEvent& WXUNUSED(event)) {
|
|
auto sizer(m_panel->GetSizer());
|
|
int item_number(0);
|
|
if (sizer->GetItemCount()) {
|
|
wxSizerItem* t1 = sizer->GetItem(item_number);
|
|
if (t1) {
|
|
auto t2 = t1->GetWindow();
|
|
if (t2) {
|
|
sizer->Detach(t2);
|
|
t2->Destroy();
|
|
sizer->Layout();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Frame::OnDeleteLastSubwindow(wxCommandEvent& WXUNUSED(event)) {
|
|
auto sizer(m_panel->GetSizer());
|
|
int item_number(sizer->GetItemCount()-1);
|
|
if (item_number >= 0) {
|
|
wxSizerItem* t1 = sizer->GetItem(item_number);
|
|
if (t1) {
|
|
auto t2 = t1->GetWindow();
|
|
if (t2) {
|
|
sizer->Detach(t2);
|
|
t2->Destroy();
|
|
sizer->Layout();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
using ro::bin2hex, ro::to_base64_string;
|
|
|
|
void Frame::NewWalletNewSecret(wxCommandEvent&) {
|
|
wxMessageBox(_T("new wallet new secret event"), _T(""));
|
|
/* If LastUsed is the empty string, check if default filename exists. If it exists, set Last Used to default file name
|
|
If LastUsed is not the empty string, or no longer the empty string, attempt to open indicated file. If open fails, or reading the opened file produces bad results, abort with exception
|
|
If LastUsed is still the empty string, attempt to create default filename. If creation fails, abort with exception. If it succeeds, set LastUsed to default filename.
|
|
The exception in unit test should simply generate an error message, but if run during initialization, should bring up the more complex UI for constructing or selecting your wallet file.*/
|
|
ILogMessage("\tWallet file");
|
|
/*
|
|
bool fWalletNameOk{ false };
|
|
wxStandardPaths& StandardPaths(wxStandardPaths::Get());
|
|
StandardPaths.UseAppInfo(3);
|
|
std::unique_ptr<ISqlite3> db;
|
|
if (fWalletNameOk) {
|
|
if (!m_LastUsedSqlite.FileExists())
|
|
throw MyException((std::string("Expected wallet:\n") + LastUsedSqlite.GetFullPath() + "\nfile not found").c_str());
|
|
db.reset(Sqlite3_open(m_LastUsedSqlite.GetFullPath().ToUTF8()));
|
|
sql_read_from_misc read_from_misc(db);
|
|
if (!read_from_misc(1) || read_from_misc.value<int64_t>() != WALLET_FILE_IDENTIFIER)
|
|
throw MyException(sz_unrecognizable_wallet_file_format);
|
|
if (!read_from_misc(2) || read_from_misc.value<int64_t>() != WALLET_FILE_SCHEMA_VERSION_0_0)
|
|
throw MyException(sz_unrecognized_wallet_schema);
|
|
if (!read_from_misc(4))
|
|
throw MyException(sz_mastersecret_missing);
|
|
ristretto255::CMasterSecret MasterSecret(*read_from_misc.value<ristretto255::scalar>());
|
|
ro::sql read_keys(db, R"|(SELECT * FROM "Keys" LIMIT 5;)|");
|
|
sql_read_name read_name(db);
|
|
// db.reset(nullptr);// Force error of premature destruction of Isqlite3
|
|
while (read_keys.step() == Icompiled_sql::ROW) {
|
|
auto pubkey = read_keys.column<ristretto255::point>(0);
|
|
auto id = read_keys.column<int>(1);
|
|
auto use = read_keys.column<int>(2);
|
|
if (use != 1)throw MyException(sz_unknown_secret_key_algorithm);
|
|
if (!read_name(id)) throw MyException(sz_no_corresponding_entry);
|
|
const char* name = read_name.name();
|
|
if (MasterSecret(name).timesBase() != *pubkey)throw MyException(sz_name_does_not_correspond);
|
|
wxLogMessage(_T("\t\t\"%s\" has expected public key 0x%s"), name, (wxString)bin2hex(*pubkey));
|
|
}
|
|
}
|
|
else {
|
|
// At this point in the code the filename LastUsedSqlite is a bad filename, normally the empty string, and the default wallet file does not exist in the default location.
|
|
// Construct default wallet and filename*//*
|
|
wxFileName path{ StandardPaths.GetUserLocalDataDir() };
|
|
try {
|
|
// Disk operations to create wallet, which may throw.
|
|
// This try/catch block exists to catch disk io issues.
|
|
if (!path.DirExists())path.Mkdir();
|
|
if (!DefaultSqlite.DirExists())DefaultSqlite.Mkdir();
|
|
db.reset(Sqlite3_create(DefaultSqlite.GetFullPath().ToUTF8()));
|
|
db->exec(R"|(
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA synchronous = 1;
|
|
BEGIN TRANSACTION;
|
|
CREATE TABLE "Keys"(
|
|
"pubkey" BLOB NOT NULL UNIQUE PRIMARY KEY,
|
|
"id" integer NOT NULL,
|
|
"use" INTEGER NOT NULL);
|
|
|
|
CREATE TABLE "Names"(
|
|
"name" TEXT NOT NULL UNIQUE
|
|
);
|
|
|
|
CREATE TABLE "Misc"(
|
|
"index" INTEGER NOT NULL UNIQUE PRIMARY KEY,
|
|
"m" BLOB
|
|
);
|
|
COMMIT;)|");
|
|
LastUsedSqlite = DefaultSqlite;
|
|
singletonApp->pConfig->Write(_T("LastUsed"), DefaultSqlite.GetFullPath());
|
|
wxLogMessage("\t\tConstructing default wallet %s", DefaultSqlite.GetFullPath());
|
|
// We now have a working wallet file with no valid data. Attempting to create a strong random secret, a name, and public and private keys for that name.
|
|
|
|
wxLogMessage("\t\tGenerating random 128 bit wallet secret");
|
|
auto text_secret{ DeriveTextSecret(ristretto255::scalar::random(), 1) };
|
|
ro::msec start_time{ ro::msec_since_epoch() };
|
|
ristretto255::CMasterSecret MasterSecret(DeriveStrongSecret(&text_secret[0]));
|
|
decltype(start_time) end_time{ ro::msec_since_epoch() };
|
|
wxLogMessage("\t\tStrong secret derivation took %d milliseconds", (end_time - start_time).count());
|
|
sql_update_to_misc update_to_misc(db);
|
|
update_to_misc(1, WALLET_FILE_IDENTIFIER);
|
|
update_to_misc(2, WALLET_FILE_SCHEMA_VERSION_0_0);
|
|
update_to_misc(3, &text_secret[0]);
|
|
update_to_misc(4, MasterSecret);
|
|
sql_insert_name insert_name(db);
|
|
const char* const cpsz("Unit Tester");
|
|
insert_name(cpsz, MasterSecret(cpsz).timesBase());
|
|
}
|
|
catch (const MyException& e) {
|
|
ILogError(R"|(Failed to create or failed to properly initialize wallet)|");
|
|
errorCode = 20;
|
|
szError = e.what();
|
|
ILogError(szError.c_str());
|
|
}
|
|
} // End of wallet creation branch*/
|
|
|
|
}
|
|
|
|
|
|
void Frame::OnSaveNew(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
wxFileDialog dialog(this,
|
|
sz_new_wallet_new_secret,
|
|
wxStandardPaths::Get().GetUserLocalDataDir(),
|
|
sz_default_wallet_name,
|
|
sz_wallet_files_title,
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
|
|
|
dialog.SetFilterIndex(1);
|
|
|
|
if (dialog.ShowModal() == wxID_OK)
|
|
{
|
|
wxLogMessage("%s, filter %d",
|
|
dialog.GetPath(), dialog.GetFilterIndex());
|
|
}
|
|
auto wallet{ dialog.GetDirectory() + "/" + dialog.GetFilename() };
|
|
wxLogMessage("new wallet created: %s", wallet);
|
|
wxConfigBase::Get()->Write("Wallet", wallet);
|
|
}
|
|
|
|
void Frame::OnFileOpen(wxCommandEvent&) {
|
|
wxString directory{ _T("") };
|
|
wxString file{ _T("") };
|
|
if (m_LastUsedSqlite.IsOk()) {
|
|
directory = m_LastUsedSqlite.GetPath();
|
|
file = m_LastUsedSqlite.GetFullName();
|
|
}
|
|
|
|
wxFileDialog
|
|
dialog(this, sz_open_wallet_file, directory, file,
|
|
sz_wallet_files_title, wxFD_OPEN | wxFD_FILE_MUST_EXIST | wxFD_CHANGE_DIR);
|
|
if (dialog.ShowModal() == wxID_CANCEL)
|
|
return; // the user changed idea...
|
|
wxLogMessage("Opening %s", dialog.GetPath());
|
|
wxFileName walletfile(dialog.GetPath());
|
|
display_wallet* panel = new display_wallet(this, walletfile);
|
|
if (m_panel)m_panel->Destroy(); //Destroy somehow manages to execute
|
|
// the correct derived destructor regardless of what kind of object it is.
|
|
m_panel = panel;
|
|
m_panel->Show();
|
|
}
|
|
|
|
void Frame::RecreateWalletFromExistingSecret(wxCommandEvent&) {
|
|
wxMessageBox(_T("new wallet existing secret event"), _T(""));
|
|
auto standardpaths = wxStandardPaths::Get();
|
|
wxFileDialog dialog(this,
|
|
sz_new_wallet_existing_secret,
|
|
wxStandardPaths::Get().GetAppDocumentsDir(),
|
|
sz_default_wallet_name,
|
|
sz_wallet_files_title,
|
|
wxFD_SAVE | wxFD_OVERWRITE_PROMPT);
|
|
|
|
dialog.SetFilterIndex(1);
|
|
|
|
if (dialog.ShowModal() == wxID_OK)
|
|
{
|
|
wxLogMessage("%s, filter %d",
|
|
dialog.GetPath(), dialog.GetFilterIndex());
|
|
}
|
|
}
|
|
|
|
void Frame::OnDelete(wxCommandEvent& WXUNUSED(event))
|
|
{
|
|
singletonApp->pConfig->SetPath(_T("/Wallet"));
|
|
wxFileName LastUsedSqlite(singletonApp->pConfig->Read(_T("LastUsed"), _T("")));
|
|
singletonApp->pConfig->DeleteEntry(_T("LastUsed"));
|
|
if (LastUsedSqlite.IsOk() && LastUsedSqlite.FileExists()) {
|
|
if (wxRemoveFile(LastUsedSqlite.GetFullPath()))wxLogMessage(_T("Deleting % s"), LastUsedSqlite.GetFullPath());
|
|
}
|
|
}
|
|
|
|
void Frame::OnMenuOpen(wxMenuEvent& evt) {
|
|
auto pMenu(evt.GetMenu());
|
|
if (pMenu) {
|
|
auto label(pMenu->GetTitle());
|
|
wxLogMessage(_T("Open menu \"%s\""), label);
|
|
}
|
|
}
|
|
|
|
Frame::~Frame(){
|
|
assert(singletonFrame == this);
|
|
singletonFrame = nullptr;
|
|
if (errorCode)return;
|
|
wxConfigBase *pConfig = wxConfigBase::Get();
|
|
if (pConfig == nullptr)return;
|
|
StorePositionToConfig();
|
|
pConfig->Write(_T("/Wallet/LastUsed"), m_LastUsedSqlite.GetFullPath());
|
|
} |