fix(fullnode): work around daemon note-selection fee-gap on shielded sends
dragonxd's z_sendmany picks notes to cover the recipient total (nTotalOut) but not the miner fee, then rejects the build unless the selected notes cover amount+fee (rpcwallet.cpp:5312 vs asyncrpcoperation_sendmany.cpp:278). So a shielded send whose largest notes sum exactly to the amount fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance — e.g. sending exactly 2.0 from an address whose biggest note is 2.0. Since the failure is async (reported via the opid poll), detect it there: when a shielded send fails with that message and the selected total H >= the requested amount (selection covered the amount but stopped one note short of the fee — vs a genuine shortfall where H < amount), re-issue the send once with a tiny self-output (= fee) back to the from-address. That lifts the daemon's selection target past the boundary so it grabs another note and can cover the fee; the recipient still receives the exact amount. Retries are tracked so a second failure surfaces normally (no loop). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
17
src/app.h
17
src/app.h
@@ -411,7 +411,18 @@ private:
|
||||
const std::string& from,
|
||||
const std::string& to,
|
||||
double amount,
|
||||
const std::string& memo);
|
||||
const std::string& memo,
|
||||
double fee = 0.0);
|
||||
// Work around a dragonxd note-selection bug: its z_sendmany picks notes to cover the recipient
|
||||
// total but not the miner fee, so a shielded send whose largest notes sum exactly to the amount
|
||||
// fails with "Insufficient shielded funds, have H, need H+fee" despite ample balance. When a
|
||||
// failed opid matches that (H >= the requested amount), re-issue the send once with a tiny
|
||||
// self-output that lifts the daemon's selection target past the boundary so it grabs another
|
||||
// note; the recipient still receives the exact amount. Returns true if a retry was issued.
|
||||
bool maybeRetrySendForFeeGap(const std::string& opid, const std::string& rawMsg);
|
||||
void resendWithFeeGapWorkaround(const std::string& from, const std::string& to,
|
||||
double amount, double fee, const std::string& memo,
|
||||
std::function<void(bool, const std::string&)> callback);
|
||||
void markPendingSendTransactionSucceeded(const std::string& opid,
|
||||
const std::string& txid);
|
||||
void removePendingSendTransactions(const std::vector<std::string>& opids,
|
||||
@@ -618,9 +629,13 @@ private:
|
||||
std::string to;
|
||||
std::string memo;
|
||||
double amount = 0.0;
|
||||
double fee = 0.0;
|
||||
std::int64_t timestamp = 0;
|
||||
};
|
||||
std::unordered_map<std::string, PendingSendInfo> pending_send_info_;
|
||||
// Opids issued as a fee-gap auto-retry (see maybeRetrySendForFeeGap). Tracked so a retry that
|
||||
// fails again is reported to the user instead of looping.
|
||||
std::unordered_set<std::string> send_feegap_retried_opids_;
|
||||
// z_sendmany UI callbacks held until the opid reaches a terminal status, so the
|
||||
// user isn't told "sent successfully" before the tx is actually built/broadcast.
|
||||
std::unordered_map<std::string, std::function<void(bool, const std::string&)>>
|
||||
|
||||
Reference in New Issue
Block a user