fix(core): refresh transaction history on the Mining tab
The Mining tab shows earnings derived from transaction history, but it was not treated as a wallet-data page, so newly mined or paid-out earnings did not appear until the user left the tab and came back. A coinbase row also changes category in place (immature becomes generate at maturity, or orphan on a reorg), and the Recent merge kept both the old and the new row, double-counting the earnings. - The Mining tab now gets the cheap between-blocks Recent poll: one listtransactions per new block, bounded by the 15 s transaction-age timer, once history has loaded. It never runs a full routine scan and never pre-empts an owed dirty cycle; tracked sends still take their normal cycle on any page. - The shielded-payout probe runs only while the pool miner is running, since pool payouts are shielded receives while a solo miner's earnings are transparent coinbase rows. - The Recent merge drops the previous row of the other coinbase category when a coinbase transitions, so maturing or reorged rewards are no longer counted twice. A non-coinbase row sharing the txid is left alone. Behaviour change: mining earnings update on the Mining tab per block instead of only on tab re-entry, and maturing or reorged coinbase rows no longer double-count. Tests: the per-block poll gate (one poll per block, due only after history loads, held while dirty or addresses are dirty), the Mining-page cycle shaping, and the coinbase transition merge for immature to generate, generate to orphan, and a shared-txid non-coinbase row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1240,6 +1240,70 @@ void testRefreshSchedulerDeferFor()
|
||||
}
|
||||
}
|
||||
|
||||
// C119: the Mining tab's per-block Recent poll and the coinbase category transition in the Recent merge.
|
||||
void testMiningTabTxPoll()
|
||||
{
|
||||
using namespace dragonx::services;
|
||||
using Refresh = dragonx::services::NetworkRefreshService;
|
||||
using json = nlohmann::json;
|
||||
MiningTxPollInputs p; p.tip = 100; p.lastPolledTip = 99; p.txAgeDue = true; p.historyLoaded = true;
|
||||
EXPECT_TRUE(miningTxPollDue(p));
|
||||
p.lastPolledTip = 100; EXPECT_FALSE(miningTxPollDue(p)); // one poll per block
|
||||
p.lastPolledTip = 99; p.txAgeDue = false; EXPECT_FALSE(miningTxPollDue(p));
|
||||
p.txAgeDue = true; p.historyLoaded = false; EXPECT_FALSE(miningTxPollDue(p));
|
||||
p.historyLoaded = true; p.dirty = true; EXPECT_FALSE(miningTxPollDue(p));
|
||||
p.dirty = false; p.addressesDirty = true; EXPECT_FALSE(miningTxPollDue(p));
|
||||
TxCycleInputs in; in.newTip = true; in.routineScanWork = true; in.heightKnown = true; in.gateOpen = true;
|
||||
EXPECT_TRUE(decideTxCycle(in) == TxCycle::Full);
|
||||
TxCycleInputs shaped = shapeTxCycleForMiningPage(in);
|
||||
shaped.recentDue = true;
|
||||
EXPECT_TRUE(decideTxCycle(shaped) == TxCycle::RecentWithProbe);
|
||||
shaped.gateOpen = false;
|
||||
EXPECT_TRUE(decideTxCycle(shaped) == TxCycle::Recent);
|
||||
TxCycleInputs tracked; tracked.newTip = true; tracked.sendTracking = true; tracked.heightKnown = true;
|
||||
EXPECT_TRUE(decideTxCycle(shapeTxCycleForMiningPage(tracked)) == TxCycle::Light);
|
||||
TxCycleInputs expl; expl.explicitScanWork = true; expl.heightKnown = true;
|
||||
EXPECT_TRUE(decideTxCycle(shapeTxCycleForMiningPage(expl)) == TxCycle::Full);
|
||||
|
||||
{ // immature -> generate at maturity: one row, the fresh category
|
||||
Refresh::TransactionRefreshSnapshot snap;
|
||||
dragonx::TransactionInfo imm; imm.txid = "cb1"; imm.type = "immature"; imm.amount = 3.0; imm.timestamp = 50; imm.confirmations = 99;
|
||||
snap.previousTransactions = {imm};
|
||||
MockRefreshRpc rpc;
|
||||
rpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "cb1"}, {"category", "generate"}, {"amount", 3.0}, {"time", 50}, {"confirmations", 100}} }));
|
||||
auto r = Refresh::collectRecentTransactionRefreshResult(rpc, snap, 200);
|
||||
EXPECT_EQ(r.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(r.transactions[0].type, std::string("generate"));
|
||||
}
|
||||
{ // generate -> orphan on a reorg
|
||||
Refresh::TransactionRefreshSnapshot snap;
|
||||
dragonx::TransactionInfo gen; gen.txid = "cb2"; gen.type = "generate"; gen.amount = 3.0; gen.timestamp = 50; gen.confirmations = 100;
|
||||
snap.previousTransactions = {gen};
|
||||
MockRefreshRpc rpc;
|
||||
rpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "cb2"}, {"category", "orphan"}, {"amount", 3.0}, {"time", 50}, {"confirmations", 0}} }));
|
||||
auto r = Refresh::collectRecentTransactionRefreshResult(rpc, snap, 200);
|
||||
EXPECT_EQ(r.transactions.size(), static_cast<size_t>(1));
|
||||
EXPECT_EQ(r.transactions[0].type, std::string("orphan"));
|
||||
}
|
||||
{ // a non-coinbase row sharing the txid is left alone
|
||||
Refresh::TransactionRefreshSnapshot snap;
|
||||
dragonx::TransactionInfo rec; rec.txid = "cb3"; rec.type = "receive"; rec.amount = 1.0; rec.timestamp = 40; rec.address = "zs-x";
|
||||
dragonx::TransactionInfo imm; imm.txid = "cb3"; imm.type = "immature"; imm.amount = 3.0; imm.timestamp = 50;
|
||||
snap.previousTransactions = {rec, imm};
|
||||
MockRefreshRpc rpc;
|
||||
rpc.addResponse("listtransactions", json::array({
|
||||
json{{"txid", "cb3"}, {"category", "generate"}, {"amount", 3.0}, {"time", 50}, {"confirmations", 100}} }));
|
||||
auto r = Refresh::collectRecentTransactionRefreshResult(rpc, snap, 200);
|
||||
EXPECT_EQ(r.transactions.size(), static_cast<size_t>(2));
|
||||
int receives = 0, gens = 0;
|
||||
for (const auto& t : r.transactions) { receives += t.type == "receive"; gens += t.type == "generate"; }
|
||||
EXPECT_EQ(receives, 1);
|
||||
EXPECT_EQ(gens, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void testRefreshScheduler()
|
||||
{
|
||||
using dragonx::services::RefreshScheduler;
|
||||
@@ -17532,6 +17596,7 @@ int main()
|
||||
testAmountFormatting();
|
||||
testSpendableFiltering();
|
||||
testRefreshScheduler();
|
||||
testMiningTabTxPoll();
|
||||
testIdleTracker();
|
||||
testEncryptionPendingPolicy();
|
||||
testWalletLockReconcilePolicy();
|
||||
|
||||
Reference in New Issue
Block a user