feat(lite): lite wallet foundation (inherited working-tree state)
Preserve the previously-uncommitted lite wallet implementation and related dev WIP under version control: - src/wallet/ lite services: client bridge, bridge runtime, connection, lifecycle, sync, gateway, result parsers, state mapper, artifact contract/resolver, refresh services, UI adapters, wallet_backend/capabilities. (Includes two small M1 fixes: lifecycle walletReady now parses the response; default chain name -> "main".) - src/chat/ chat protocol; tests/fixtures/ (lite + hushchat); tools/hushchat_fixture_check.cpp; scripts/build-lite-backend-artifact.sh. - Pre-existing modified app_network/security/wizard, network_refresh_service, sidebar, mining_tab, bootstrap dialog, and version headers captured as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
217
src/wallet/lite_wallet_state_mapper.cpp
Normal file
217
src/wallet/lite_wallet_state_mapper.cpp
Normal file
@@ -0,0 +1,217 @@
|
||||
#include "wallet/lite_wallet_state_mapper.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace dragonx::wallet {
|
||||
namespace {
|
||||
|
||||
LiteWalletAppAddressModel mapAddress(const std::string& address, LiteWalletAppAddressKind kind)
|
||||
{
|
||||
LiteWalletAppAddressModel model;
|
||||
model.address = address;
|
||||
model.kind = kind;
|
||||
return model;
|
||||
}
|
||||
|
||||
LiteWalletAppSpendableOutputModel mapSpendableOutput(const LiteSpendableOutput& output)
|
||||
{
|
||||
LiteWalletAppSpendableOutputModel model;
|
||||
model.kind = output.kind;
|
||||
model.address = output.address;
|
||||
model.createdInTxid = output.createdInTxid;
|
||||
model.createdInBlock = output.createdInBlock;
|
||||
model.valueZatoshis = output.value;
|
||||
model.spent = output.spent;
|
||||
model.unconfirmedSpent = output.unconfirmedSpent;
|
||||
model.pending = output.pending;
|
||||
model.spendable = output.spendable;
|
||||
return model;
|
||||
}
|
||||
|
||||
LiteWalletAppTransactionKind mapTransactionKind(LiteTransactionDirection direction)
|
||||
{
|
||||
switch (direction) {
|
||||
case LiteTransactionDirection::Send: return LiteWalletAppTransactionKind::Send;
|
||||
case LiteTransactionDirection::Receive: return LiteWalletAppTransactionKind::Receive;
|
||||
case LiteTransactionDirection::Unknown: return LiteWalletAppTransactionKind::Unknown;
|
||||
}
|
||||
return LiteWalletAppTransactionKind::Unknown;
|
||||
}
|
||||
|
||||
LiteWalletAppTransactionOutputModel mapTransactionOutput(const LiteTransactionOutput& output)
|
||||
{
|
||||
LiteWalletAppTransactionOutputModel model;
|
||||
model.address = output.address;
|
||||
model.valueZatoshis = output.value;
|
||||
model.memo = output.memo;
|
||||
return model;
|
||||
}
|
||||
|
||||
LiteWalletAppTransactionModel mapTransaction(const LiteTransactionRecord& transaction)
|
||||
{
|
||||
LiteWalletAppTransactionModel model;
|
||||
model.txid = transaction.txid;
|
||||
model.kind = mapTransactionKind(transaction.direction);
|
||||
model.timestamp = transaction.datetime;
|
||||
model.blockHeight = transaction.blockHeight;
|
||||
model.unconfirmed = transaction.unconfirmed;
|
||||
model.address = transaction.address;
|
||||
model.amountZatoshis = transaction.amount;
|
||||
model.signedAmountZatoshis = model.kind == LiteWalletAppTransactionKind::Send
|
||||
? -transaction.amount
|
||||
: transaction.amount;
|
||||
model.memo = transaction.memo;
|
||||
model.position = transaction.position;
|
||||
|
||||
model.outgoingOutputs.reserve(transaction.outgoingMetadata.size());
|
||||
for (const auto& output : transaction.outgoingMetadata) {
|
||||
model.outgoingOutputs.push_back(mapTransactionOutput(output));
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
bool modelHasAnyMappedField(const LiteWalletAppRefreshModel& model)
|
||||
{
|
||||
return model.hasChainInfo || model.hasHeight || model.hasBalance || model.hasAddresses ||
|
||||
model.hasSpendableOutputs || model.hasTransactions || model.hasSyncStatus;
|
||||
}
|
||||
|
||||
void addIssue(LiteWalletStateMapResult& result, LiteWalletStateMapIssue issue, std::string message)
|
||||
{
|
||||
result.issues.push_back(LiteWalletStateMapIssueInfo{issue, std::move(message)});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char* liteWalletAppAddressKindName(LiteWalletAppAddressKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case LiteWalletAppAddressKind::Shielded: return "shielded";
|
||||
case LiteWalletAppAddressKind::Transparent: return "transparent";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* liteWalletAppTransactionKindName(LiteWalletAppTransactionKind kind)
|
||||
{
|
||||
switch (kind) {
|
||||
case LiteWalletAppTransactionKind::Unknown: return "unknown";
|
||||
case LiteWalletAppTransactionKind::Send: return "send";
|
||||
case LiteWalletAppTransactionKind::Receive: return "receive";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const char* liteWalletStateMapIssueName(LiteWalletStateMapIssue issue)
|
||||
{
|
||||
switch (issue) {
|
||||
case LiteWalletStateMapIssue::EmptyBundle: return "EmptyBundle";
|
||||
case LiteWalletStateMapIssue::IncompleteBundle: return "IncompleteBundle";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
LiteWalletStateMapResult mapLiteWalletRefreshBundle(const LiteWalletRefreshBundle& bundle)
|
||||
{
|
||||
LiteWalletStateMapResult result;
|
||||
result.stateMutationAllowed = false;
|
||||
result.model.complete = bundle.complete;
|
||||
result.model.successfulCommandCount = bundle.successfulCommandCount;
|
||||
|
||||
if (bundle.hasInfo) {
|
||||
result.model.hasChainInfo = true;
|
||||
result.model.chain.chainName = bundle.info.chainName;
|
||||
result.model.chain.version = bundle.info.version;
|
||||
result.model.chain.vendor = bundle.info.vendor;
|
||||
result.model.chain.latestBlockHeight = bundle.info.latestBlockHeight;
|
||||
result.model.chain.difficulty = bundle.info.difficulty;
|
||||
result.model.chain.longestChain = bundle.info.longestChain;
|
||||
result.model.chain.notarized = bundle.info.notarized;
|
||||
}
|
||||
|
||||
if (bundle.hasHeight) {
|
||||
result.model.hasHeight = true;
|
||||
result.model.height.height = bundle.height.height;
|
||||
}
|
||||
|
||||
if (bundle.hasBalance) {
|
||||
result.model.hasBalance = true;
|
||||
result.model.balance.transparentZatoshis = bundle.balance.transparentBalance;
|
||||
result.model.balance.shieldedZatoshis = bundle.balance.shieldedBalance;
|
||||
result.model.balance.unconfirmedZatoshis = bundle.balance.unconfirmedBalance;
|
||||
result.model.balance.verifiedShieldedZatoshis = bundle.balance.verifiedShieldedBalance;
|
||||
result.model.balance.spendableShieldedZatoshis = bundle.balance.spendableShieldedBalance;
|
||||
result.model.balance.totalZatoshis = bundle.balance.transparentBalance + bundle.balance.shieldedBalance;
|
||||
}
|
||||
|
||||
if (bundle.hasAddresses) {
|
||||
result.model.hasAddresses = true;
|
||||
result.model.addresses.reserve(bundle.addresses.zAddresses.size() + bundle.addresses.tAddresses.size());
|
||||
for (const auto& address : bundle.addresses.zAddresses) {
|
||||
result.model.addresses.push_back(mapAddress(address, LiteWalletAppAddressKind::Shielded));
|
||||
}
|
||||
for (const auto& address : bundle.addresses.tAddresses) {
|
||||
result.model.addresses.push_back(mapAddress(address, LiteWalletAppAddressKind::Transparent));
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.hasNotes) {
|
||||
result.model.hasSpendableOutputs = true;
|
||||
const auto outputCount = bundle.notes.unspentNotes.size() + bundle.notes.utxos.size() +
|
||||
bundle.notes.pendingNotes.size() + bundle.notes.pendingUtxos.size();
|
||||
result.model.spendableOutputs.reserve(outputCount);
|
||||
for (const auto& output : bundle.notes.unspentNotes) {
|
||||
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
|
||||
}
|
||||
for (const auto& output : bundle.notes.utxos) {
|
||||
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
|
||||
}
|
||||
for (const auto& output : bundle.notes.pendingNotes) {
|
||||
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
|
||||
}
|
||||
for (const auto& output : bundle.notes.pendingUtxos) {
|
||||
result.model.spendableOutputs.push_back(mapSpendableOutput(output));
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.hasTransactions) {
|
||||
result.model.hasTransactions = true;
|
||||
result.model.transactions.reserve(bundle.transactions.transactions.size());
|
||||
for (const auto& transaction : bundle.transactions.transactions) {
|
||||
result.model.transactions.push_back(mapTransaction(transaction));
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.hasSyncStatus) {
|
||||
result.model.hasSyncStatus = true;
|
||||
result.model.sync.walletHeight = bundle.syncStatus.syncedBlocks;
|
||||
result.model.sync.chainHeight = bundle.syncStatus.totalBlocks;
|
||||
result.model.sync.progress = bundle.syncStatus.progress;
|
||||
result.model.sync.complete = bundle.syncStatus.complete;
|
||||
}
|
||||
|
||||
if (!modelHasAnyMappedField(result.model)) {
|
||||
addIssue(result, LiteWalletStateMapIssue::EmptyBundle, "lite refresh bundle has no mappable fields");
|
||||
result.error = result.issues.back().message;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!bundle.complete) {
|
||||
addIssue(result, LiteWalletStateMapIssue::IncompleteBundle, "lite refresh bundle is partial");
|
||||
}
|
||||
|
||||
result.ok = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
LiteWalletStateMapResult mapLiteWalletRefreshResult(const LiteWalletRefreshResult& result)
|
||||
{
|
||||
return mapLiteWalletRefreshBundle(result.bundle);
|
||||
}
|
||||
|
||||
LiteWalletStateMapResult mapLiteWalletRefreshServiceResult(const LiteWalletRefreshServiceResult& result)
|
||||
{
|
||||
return mapLiteWalletRefreshBundle(result.bundle);
|
||||
}
|
||||
|
||||
} // namespace dragonx::wallet
|
||||
Reference in New Issue
Block a user