2025-11-12 13:54:22 +01:00
|
|
|
/*
|
|
|
|
|
* This file is part of the Flowee project
|
|
|
|
|
* Copyright (C) 2025 Tom Zander <tom@flowee.org>
|
|
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
#include "BackupSyncModuleInfo.h"
|
|
|
|
|
#include "BackupNFCHandler.h"
|
|
|
|
|
#include "BackupAccountInfo.h"
|
|
|
|
|
#include "QMLData.h"
|
|
|
|
|
#include "WalletData.h"
|
|
|
|
|
#include <FloweePay.h>
|
|
|
|
|
#include <QQmlEngine> // for the qmlRegisterType
|
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
|
|
|
|
#include <streaming/BufferPools.h>
|
|
|
|
|
#include <streaming/MessageBuilder.h>
|
|
|
|
|
#include <SeedsBackup.h>
|
|
|
|
|
|
|
|
|
|
namespace Module {
|
|
|
|
|
enum Tags {
|
|
|
|
|
WalletSegment,
|
|
|
|
|
PrivateKey,
|
|
|
|
|
LastSavedTime, // long, epoch based timestamp
|
|
|
|
|
NeedsExport, // bool
|
|
|
|
|
// LastSaveHash,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
static BackupSyncModuleInfo *g_instance = nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BackupSyncModuleInfo *BackupSyncModuleInfo::instance()
|
|
|
|
|
{
|
|
|
|
|
assert(g_instance);
|
|
|
|
|
return g_instance;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleInfo * BackupSyncModuleInfo::build()
|
|
|
|
|
{
|
|
|
|
|
assert(g_instance == nullptr);
|
|
|
|
|
g_instance = new BackupSyncModuleInfo();
|
|
|
|
|
BackupSyncModuleInfo *info = g_instance;
|
|
|
|
|
info->setId("backupSyncModule");
|
2026-01-19 00:06:41 +01:00
|
|
|
info->setTitle(tr("Wallet Sync"));
|
|
|
|
|
info->setDescription(tr("Transaction comments cloud storage."));
|
2025-11-12 13:54:22 +01:00
|
|
|
|
|
|
|
|
// Provide a way to configure backup on a certain wallet.
|
|
|
|
|
auto *section = new ModuleSection(ModuleSection::CustomSectionType, info);
|
|
|
|
|
section->setSectionId("walletOptions");
|
|
|
|
|
if (ModuleManager::frontEnd() == ModuleManager::Mobile)
|
|
|
|
|
section->setStartQMLFile("qrc:/backup-sync/WalletOptions.qml");
|
|
|
|
|
else
|
|
|
|
|
section->setStartQMLFile("qrc:/backup-sync/WalletOptions-desktop.qml");
|
|
|
|
|
info->addSection(section);
|
|
|
|
|
|
|
|
|
|
// Starting point to initiate storing a wallet on an NFC tag
|
|
|
|
|
auto *section2 = new ModuleSection(ModuleSection::CustomSectionType, info);
|
|
|
|
|
section2->setSectionId("storeOnNfc");
|
|
|
|
|
section2->setStartQMLFile("qrc:/backup-sync/WalletToNFC.qml");
|
|
|
|
|
info->addSection(section2);
|
|
|
|
|
|
|
|
|
|
qmlRegisterType<BackupNFCHandler>("Flowee.org.pay.backup", 1, 0, "NFC");
|
|
|
|
|
qmlRegisterType<BackupAccountInfo>("Flowee.org.pay.backup", 1, 0, "BackupAccountInfo");
|
|
|
|
|
return info;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
BackupSyncModuleInfo::BackupSyncModuleInfo()
|
|
|
|
|
{
|
|
|
|
|
connect(FloweePay::instance(), &FloweePay::newNfcTagData,
|
|
|
|
|
this, &BackupSyncModuleInfo::onTagRead, Qt::QueuedConnection);
|
|
|
|
|
connect(FloweePay::instance(), &FloweePay::nfcStatusChanged,
|
|
|
|
|
this, &BackupSyncModuleInfo::nfcAvailableChanged);
|
|
|
|
|
#if 0
|
|
|
|
|
QTimer::singleShot(5000, this, [=]() {
|
|
|
|
|
auto data = QByteArray::fromHex("0");
|
|
|
|
|
std::shared_ptr<NFCTagData> tag = std::make_shared<NFCTagData>();
|
|
|
|
|
NFCTagData::Record record;
|
|
|
|
|
record.mimeType = "application/bch-seed";
|
|
|
|
|
auto pool = Streaming::pool(data.size());
|
|
|
|
|
pool->write(data.constData(), data.size());
|
|
|
|
|
record.payload = pool->commit();
|
|
|
|
|
tag->addRecord(record);
|
|
|
|
|
onTagRead(tag);
|
|
|
|
|
});
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BackupSyncModuleInfo::loadSettings(const Streaming::ConstBuffer &data)
|
|
|
|
|
{
|
|
|
|
|
// delay the basic setup a little to ensure that the
|
|
|
|
|
// FloweePay instance has been created.
|
|
|
|
|
QTimer::singleShot(0, this, [=]() {
|
|
|
|
|
// Delay more to ensure all wallets are created.
|
|
|
|
|
connect(FloweePay::instance(), &FloweePay::loadComplete, this, [=]() {
|
|
|
|
|
// now load wallet data from blob
|
|
|
|
|
Streaming::MessageParser parser(data);
|
|
|
|
|
const auto wallets = FloweePay::instance()->wallets();
|
|
|
|
|
WalletData *wd = nullptr;
|
|
|
|
|
while (parser.next() == Streaming::FoundTag) {
|
|
|
|
|
if (parser.tag() == Module::WalletSegment) {
|
|
|
|
|
wd = nullptr; // avoid corruption when a wallet was deleted.
|
|
|
|
|
int id = parser.intData();
|
|
|
|
|
for (const std::shared_ptr<Wallet> &wallet : wallets) {
|
|
|
|
|
if (wallet->segment()->segmentId() == id) {
|
|
|
|
|
wd = new WalletData(wallet, this);
|
|
|
|
|
m_walletData.append(wd);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (parser.tag() == Module::PrivateKey) {
|
|
|
|
|
if (wd)
|
|
|
|
|
wd->setPrivKeyData(parser.bytesDataBuffer());
|
|
|
|
|
}
|
|
|
|
|
else if (parser.tag() == Module::LastSavedTime) {
|
|
|
|
|
if (wd)
|
|
|
|
|
wd->setLastSavedTime(parser.longData());
|
|
|
|
|
}
|
|
|
|
|
else if (parser.tag() == Module::NeedsExport) {
|
|
|
|
|
if (wd && parser.boolData())
|
|
|
|
|
wd->markDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
m_loaded = true;
|
|
|
|
|
|
|
|
|
|
setupWallets();
|
|
|
|
|
|
|
|
|
|
connect (FloweePay::instance(), &FloweePay::walletsChanged,
|
|
|
|
|
this, [=]() { setupWallets(); });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Streaming::ConstBuffer BackupSyncModuleInfo::saveSettings(std::shared_ptr<Streaming::BufferPool> &pool) const
|
|
|
|
|
{
|
2026-01-14 17:36:54 +01:00
|
|
|
pool->reserve(m_walletData.size() * 25);
|
2025-11-12 13:54:22 +01:00
|
|
|
Streaming::MessageBuilder builder(pool);
|
|
|
|
|
for (auto *wd : m_walletData) {
|
|
|
|
|
builder.add(Module::WalletSegment, wd->wallet()->segment()->segmentId());
|
|
|
|
|
if (wd->wallet()->encryption() != Wallet::NotEncrypted && wd->privKey().isValid()) {
|
|
|
|
|
const auto &key = wd->privKey();
|
|
|
|
|
builder.addByteArray(Module::PrivateKey, key.begin(), key.size());
|
|
|
|
|
}
|
|
|
|
|
builder.add(Module::LastSavedTime, static_cast<uint64_t>(wd->lastSavedTime()));
|
|
|
|
|
if (wd->needsSave())
|
|
|
|
|
builder.add(Module::NeedsExport, true);
|
|
|
|
|
}
|
|
|
|
|
return builder.buffer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BackupSyncModuleInfo::setupWallets()
|
|
|
|
|
{
|
|
|
|
|
bool addedOne = false;
|
|
|
|
|
for (const auto &wallet : FloweePay::instance()->wallets()) {
|
|
|
|
|
bool known = false;
|
|
|
|
|
for (const auto *wd : std::as_const(m_walletData)) {
|
|
|
|
|
if (wd->wallet() == wallet) {
|
|
|
|
|
known = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!known) {
|
|
|
|
|
if (!wallet->isHDWallet())
|
|
|
|
|
continue;
|
|
|
|
|
// ignore archived wallets.
|
|
|
|
|
if (wallet->segment()->priority() >= PrivacySegment::OnlyManual)
|
|
|
|
|
continue;
|
|
|
|
|
auto path = HDMasterKey::deriveFromString(wallet->derivationPath().toStdString());
|
|
|
|
|
if (path.size() != 3) {
|
2026-01-29 13:34:33 +01:00
|
|
|
logInfo(10050) << "Notice non standard derived wallet" << wallet->name();
|
2025-11-12 13:54:22 +01:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addedOne = true;
|
|
|
|
|
// wallets get this enabled by default.
|
|
|
|
|
AccountConfig accountConfig(wallet);
|
|
|
|
|
accountConfig.setCloudStoreDetails(true);
|
|
|
|
|
// create our wrapper.
|
|
|
|
|
auto *wd = new WalletData(wallet, this);
|
|
|
|
|
m_walletData.append(wd);
|
|
|
|
|
// as this is either an existing wallet because this feature is new,
|
|
|
|
|
// or this is a new wallet just imported or created, lets check for prior backups.
|
|
|
|
|
wd->restore(WalletData::JustRestore);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (addedOne)
|
|
|
|
|
emit walletDataChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ModuleManager *BackupSyncModuleInfo::mmanager() const
|
|
|
|
|
{
|
|
|
|
|
return qobject_cast<ModuleManager*>(parent());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QList<WalletData *> BackupSyncModuleInfo::walletData() const
|
|
|
|
|
{
|
|
|
|
|
return m_walletData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool BackupSyncModuleInfo::nfcAvailable() const
|
|
|
|
|
{
|
|
|
|
|
return FloweePay::instance()->nfcStatus() != WalletEnums::NFC_Unsupported;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void BackupSyncModuleInfo::onTagRead(const std::shared_ptr<NFCTagData> &data)
|
|
|
|
|
{
|
|
|
|
|
assert(data.get());
|
2026-01-29 13:34:33 +01:00
|
|
|
logInfo(10050) << "Found an NFC tag with" << data.get()->records().size() << "record(s)";
|
2025-11-12 13:54:22 +01:00
|
|
|
auto records = data->records();
|
|
|
|
|
assert(!records.isEmpty());
|
|
|
|
|
if (records.first().mimeType == "application/bch-seed") {
|
|
|
|
|
const auto payload = records.first().payload;
|
2026-01-29 13:34:33 +01:00
|
|
|
logInfo(10050) << "NFC: Going to try to restore the seed.";
|
2025-11-12 13:54:22 +01:00
|
|
|
auto *qmlData = new QMLData(this);
|
|
|
|
|
auto *backup = SeedsBackup::fromData(payload);
|
|
|
|
|
if (backup && !backup->paths().isEmpty()) {
|
|
|
|
|
qmlData->setSeeds(backup);
|
|
|
|
|
const auto seedPhrase = backup->generateSeedPhrase();
|
|
|
|
|
qmlData->setSeed(seedPhrase);
|
|
|
|
|
const auto paths = backup->paths();
|
|
|
|
|
|
|
|
|
|
// iterate through existing wallets and check if the seed matches one of the wallets
|
|
|
|
|
for (const auto &wallet : FloweePay::instance()->wallets()) {
|
|
|
|
|
if (wallet->isHDWallet()) {
|
|
|
|
|
if (wallet->hdWalletMnemonic() == seedPhrase) {
|
|
|
|
|
bool fullMatch = false;
|
|
|
|
|
for (const auto &item : paths) {
|
|
|
|
|
if (item.path == wallet->derivationPath()) {
|
|
|
|
|
fullMatch = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
qmlData->addWallet(wallet, fullMatch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (qmlData->seedKnown()) {
|
|
|
|
|
mmanager()->requestAddPage("qrc:/backup-sync/WalletExists.qml", qmlData);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mmanager()->requestAddPage("qrc:/backup-sync/ImportSeed.qml", qmlData);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
else {
|
2026-01-29 13:34:33 +01:00
|
|
|
logWarning(10050) << "NFC failed restoring, data didn't parse or too new version";
|
2025-11-12 13:54:22 +01:00
|
|
|
// TODO FloweePay::setNotification()
|
|
|
|
|
// would be nice to show an "NFC tag data not understood" or so
|
|
|
|
|
delete qmlData;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// TODO
|
|
|
|
|
// if we get here, some other mime type was found, likely initiating a payment.
|
|
|
|
|
}
|