#include"Boss/Mod/EarningsTracker.hpp" #include"Boss/Msg/DbResource.hpp" #include"Boss/Msg/ForwardFee.hpp" #include"Boss/Msg/RequestEarningsInfo.hpp" #include"Boss/Msg/RequestMoveFunds.hpp" #include"Boss/Msg/ResponseEarningsInfo.hpp" #include"Boss/Msg/ResponseMoveFunds.hpp" #include"Boss/concurrent.hpp" #include"Ev/Io.hpp" #include"S/Bus.hpp" #include"Sqlite3.hpp" #include"Util/make_unique.hpp" #include namespace Boss { namespace Mod { class EarningsTracker::Impl { private: S::Bus& bus; Sqlite3::Db db; /* Information of a pending MoveFunds. */ struct Pending { Ln::NodeId source; Ln::NodeId destination; }; /* Maps a requester to the source-destination of the movefunds. */ std::map pendings; void start() { bus.subscribe([this](Msg::DbResource const& r) { db = r.db; return init(); }); bus.subscribe([this](Msg::ForwardFee const& f) { return forward_fee(f.in_id, f.out_id, f.fee); }); bus.subscribe([this](Msg::RequestMoveFunds const& req) { return request_move_funds(req); }); bus.subscribe([this](Msg::ResponseMoveFunds const& rsp) { return response_move_funds(rsp); }); bus.subscribe([this](Msg::RequestEarningsInfo const& req) { return Boss::concurrent(request_earnings_info(req)); }); } Ev::Io init() { return db.transact().then([](Sqlite3::Tx tx) { tx.query_execute(R"QRY( CREATE TABLE IF NOT EXISTS "EarningsTracker" ( node TEXT PRIMARY KEY , in_earnings INTEGER NOT NULL , in_expenditures INTEGER NOT NULL , out_earnings INTEGER NOT NULL , out_expenditures INTEGER NOT NULL ); )QRY"); tx.commit(); return Ev::lift(); }); } /* Ensure the given node has an entry. */ void ensure(Sqlite3::Tx& tx, Ln::NodeId const& node) { tx.query(R"QRY( INSERT OR IGNORE INTO "EarningsTracker" VALUES(:node, 0, 0, 0, 0); )QRY") .bind(":node", std::string(node)) .execute() ; } Ev::Io forward_fee( Ln::NodeId const& in , Ln::NodeId const& out , Ln::Amount fee ) { return db.transact().then([this, in, out, fee ](Sqlite3::Tx tx) { ensure(tx, in); ensure(tx, out); tx.query(R"QRY( UPDATE "EarningsTracker" SET in_earnings = in_earnings + :fee WHERE node = :node ; )QRY") .bind(":node", std::string(in)) .bind(":fee", fee.to_msat()) .execute() ; tx.query(R"QRY( UPDATE "EarningsTracker" SET out_earnings = out_earnings + :fee WHERE node = :node ; )QRY") .bind(":node", std::string(out)) .bind(":fee", fee.to_msat()) .execute() ; tx.commit(); return Ev::lift(); }); } Ev::Io request_move_funds(Boss::Msg::RequestMoveFunds const& req) { auto& pending = pendings[req.requester]; pending.source = req.source; pending.destination = req.destination; return Ev::lift(); } Ev::Io response_move_funds(Boss::Msg::ResponseMoveFunds const& rsp) { auto requester = rsp.requester; auto fee = rsp.fee_spent; return db.transact().then([this, requester, fee ](Sqlite3::Tx tx) { auto it = pendings.find(requester); if (it == pendings.end()) /* Not in our table, huh, weird. */ return Ev::lift(); auto& pending = it->second; ensure(tx, pending.source); ensure(tx, pending.destination); /* Source gets in-expenditures since it gets more * incoming capacity (for more earnings for the * incoming direction). */ tx.query(R"QRY( UPDATE "EarningsTracker" SET in_expenditures = in_expenditures + :fee WHERE node = :node ; )QRY") .bind(":node", std::string(pending.source)) .bind(":fee", fee.to_msat()) .execute() ; /* Destination gets out-expenditures for same * reason. */ tx.query(R"QRY( UPDATE "EarningsTracker" SET out_expenditures = out_expenditures + :fee WHERE node = :node ; )QRY") .bind( ":node" , std::string(pending.destination) ) .bind(":fee", fee.to_msat()) .execute() ; /* Erase it. */ pendings.erase(it); tx.commit(); return Ev::lift(); }); } Ev::Io request_earnings_info(Msg::RequestEarningsInfo const& req) { auto requester = req.requester; auto node = req.node; return db.transact().then([this, requester, node ](Sqlite3::Tx tx) { auto in_earnings = Ln::Amount::sat(0); auto in_expenditures = Ln::Amount::sat(0); auto out_earnings = Ln::Amount::sat(0); auto out_expenditures = Ln::Amount::sat(0); auto fetch = tx.query(R"QRY( SELECT in_earnings, in_expenditures , out_earnings, out_expenditures FROM "EarningsTracker" WHERE node = :node ; )QRY") .bind(":node", std::string(node)) .execute() ; for (auto& r : fetch) { in_earnings = Ln::Amount::msat( r.get(0) ); in_expenditures = Ln::Amount::msat( r.get(1) ); out_earnings = Ln::Amount::msat( r.get(2) ); out_expenditures = Ln::Amount::msat( r.get(3) ); } tx.commit(); return bus.raise(Msg::ResponseEarningsInfo{ requester, node, in_earnings, in_expenditures, out_earnings, out_expenditures }); }); } public: Impl() =delete; Impl(Impl&&) =delete; Impl(Impl const&) =delete; explicit Impl(S::Bus& bus_) : bus(bus_) { start(); } }; EarningsTracker::EarningsTracker(EarningsTracker&&) =default; EarningsTracker::~EarningsTracker() =default; EarningsTracker::EarningsTracker(S::Bus& bus) : pimpl(Util::make_unique(bus)) { } }}