#include"Boss/Mod/ChannelCreator/Dowser.hpp" #include"Boss/Mod/Rpc.hpp" #include"Ev/Io.hpp" #include"Ev/map.hpp" #include"Ev/yield.hpp" #include"Jsmn/Object.hpp" #include"Json/Out.hpp" #include"Ln/Amount.hpp" #include"Ln/NodeId.hpp" #include"Ln/Scid.hpp" #include #include #include namespace { /* Maximum number of routes to try. */ auto const dowser_limit = std::size_t(10); /* Maximum length of routes. */ auto const route_limit = std::size_t(3); } namespace Boss { namespace Mod { namespace ChannelCreator { class Dowser::Impl : public std::enable_shared_from_this { private: Boss::Mod::Rpc& rpc; Ln::NodeId proposal; Ln::NodeId patron; std::vector excludes; Ln::Amount amount; std::size_t tries; public: Impl( Boss::Mod::Rpc& rpc_ , Ln::NodeId const& self , Ln::NodeId const& proposal_ , Ln::NodeId const& patron_ ) : rpc(rpc_) , proposal(proposal_) , patron(patron_) , amount(Ln::Amount::sat(0)) , tries(0) { /* Exclude ourself from routing. */ excludes.emplace_back(std::string(self)); } Ev::Io run() { auto self = shared_from_this(); return self->core_run().then([self]() { return Ev::lift(self->amount); }); } private: struct RouteStep { Ln::Scid chan; Ln::NodeId node; }; Ev::Io core_run() { return Ev::yield().then([this]() { return getroute(); }).then([this](std::vector route) { if (route.empty()) /* Nothing to do now. */ return Ev::lift(); add_exclude(route); return get_capacity( std::move(route) ).then([this](Ln::Amount amt) { /* Deduct 1.5% from the capacity, to * factor in 1% reserve and 0.5% * default `maxfeepercent`. */ amount += amt * 0.985; ++tries; if (tries >= dowser_limit) return Ev::lift(); return core_run(); }); }); } Ev::Io> getroute() { auto exc = get_excludes(); auto params = Json::Out() .start_object() .field("id", std::string(patron)) .field("fromid", std::string(proposal)) .field("msatoshi" , std::string("1msat") ) /* I never had a decent grasp of * riskfactor. */ .field("riskfactor", 10.0) .field("exclude", exc) .field("fuzzpercent", 0.0) .field("maxhops", (double) route_limit) .end_object() ; return rpc.command( "getroute" , std::move(params) ).then([this](Jsmn::Object res) { auto rv = std::vector(); auto empty = std::vector(); /* Parse result. */ if (!res.is_object() || !res.has("route")) return Ev::lift(empty); auto route = res["route"]; if (!route.is_array()) return Ev::lift(empty); for (auto i = std::size_t(0); i < route.size(); ++i) { auto step = route[i]; if (!step.is_object()) return Ev::lift(empty); if (!step.has("id")) return Ev::lift(empty); if (!step.has("channel")) return Ev::lift(empty); auto id_j = step["id"]; auto chan_j = step["channel"]; if (!id_j.is_string()) return Ev::lift(empty); auto id_s = std::string(id_j); if (!Ln::NodeId::valid_string(id_s)) return Ev::lift(empty); auto id = Ln::NodeId(id_s); if (!chan_j.is_string()) return Ev::lift(empty); auto chan_s = std::string(chan_j); if (!Ln::Scid::valid_string(chan_s)) return Ev::lift(empty); auto chan = Ln::Scid(chan_s); rv.emplace_back(RouteStep{chan, id}); } return Ev::lift(std::move(rv)); }).catching([this](RpcError const& _) { /* Assume this is route-not-found. */ return Ev::lift(std::vector()); }); } Json::Out get_excludes() { auto json = Json::Out(); auto arr = json.start_array(); for (auto const& e : excludes) arr.entry(e); arr.end_array(); return json; } /* Given a route, adds an exclusion for the first part of * the route. */ void add_exclude(std::vector const& route) { assert(!route.empty()); if (route.size() == 1) { /* Exclude both directions of the first channel. */ auto s = std::string(route[0].chan); excludes.emplace_back(s + "/1"); excludes.emplace_back(s + "/0"); } else { /* Exclude first node. */ excludes.emplace_back(std::string(route[0].node)); } } /* Gets the capacity of a route. */ Ev::Io get_capacity(std::vector route) { assert(!route.empty()); using std::placeholders::_1; return Ev::map( std::bind(&Impl::get_1_capacity, this, _1) , route ).then([this](std::vector amounts) { /* Get the smallest amount. */ auto theoretical_capacity = *std::min_element( amounts.begin() , amounts.end() ); /* Divide by number of hops + 1. */ auto plausible_capacity = theoretical_capacity / (amounts.size() + 1); return Ev::lift(plausible_capacity); }); } /* Gets the capacity of a single route hop. */ Ev::Io get_1_capacity(RouteStep const& step) { auto scid = std::string(step.chan); return rpc.command( "listchannels" , Json::Out() .start_object() .field( "short_channel_id" , scid ) .end_object() ).then([this](Jsmn::Object res) { auto zero = Ln::Amount::sat(0); if (!res.is_object() || !res.has("channels")) return Ev::lift(zero); auto chans = res["channels"]; if (!chans.is_array()) return Ev::lift(zero); for (auto i = std::size_t(0); i < chans.size(); ++i) { auto chan = chans[i]; if ( !chan.is_object() || !chan.has("amount_msat") ) continue; auto amt_j = chan["amount_msat"]; if (!amt_j.is_string()) continue; auto amt_s = std::string(amt_j); return Ev::lift(Ln::Amount(amt_s)); } /* The channel *can* disappear between the time * we did `getroute` to the time we did * `listchannels`, so if we reach here, treat * this as 0-capacity. */ return Ev::lift(zero); }); } }; Dowser::Dowser( Boss::Mod::Rpc& rpc , Ln::NodeId const& self , Ln::NodeId const& proposal , Ln::NodeId const& patron ) : pimpl(std::make_shared(rpc, self, proposal, patron)) { } Ev::Io Dowser::run() { return pimpl->run(); } }}}