#undef NDEBUG #include"Boss/Mod/SwapManager.hpp" #include"Boss/Msg/AcceptSwapQuotation.hpp" #include"Boss/Msg/DbResource.hpp" #include"Boss/Msg/JsonCout.hpp" #include"Boss/Msg/ListfundsResult.hpp" #include"Boss/Msg/PayInvoice.hpp" #include"Boss/Msg/ProvideSwapQuotation.hpp" #include"Boss/Msg/RequestNewaddr.hpp" #include"Boss/Msg/ResponseNewaddr.hpp" #include"Boss/Msg/SolicitSwapQuotation.hpp" #include"Boss/Msg/SwapCreation.hpp" #include"Boss/Msg/SwapRequest.hpp" #include"Boss/Msg/SwapResponse.hpp" #include"Ev/Io.hpp" #include"Ev/foreach.hpp" #include"Ev/start.hpp" #include"Ev/yield.hpp" #include"Json/Out.hpp" #include"S/Bus.hpp" #include"Sqlite3.hpp" #include"Util/make_unique.hpp" #include"Uuid.hpp" #include #include #include #include #include #include #include #include namespace { /*----------------------------------------------------------------------------- Mocks -----------------------------------------------------------------------------*/ class MockLog { public: MockLog(S::Bus& bus) { bus.subscribe([](Boss::Msg::JsonCout const& j) { std::cout << j.obj.output() << std::endl; return Ev::lift(); }); } }; class MockNewaddr { private: std::queue addresses; public: MockNewaddr(S::Bus& bus) { bus.subscribe([ this, &bus ](Boss::Msg::RequestNewaddr const& r) { assert(!addresses.empty()); auto addr = addresses.front(); addresses.pop(); return bus.raise(Boss::Msg::ResponseNewaddr{ addr, r.requester }); }); } void add(std::string addr) { addresses.emplace(std::move(addr)); } bool empty() const { return addresses.empty(); } }; struct MockProvider {}; std::vector> providers; class MockSwapQuotation { private: /* A vector, since we want to check what happens when multiple * quotations exist. */ std::queue> fees; public: MockSwapQuotation(S::Bus& bus) { using Boss::Msg::ProvideSwapQuotation; using Boss::Msg::SolicitSwapQuotation; bus.subscribe([ this , &bus ](SolicitSwapQuotation const& s) { assert(!fees.empty()); auto set = std::move(fees.front()); fees.pop(); /* Ensure there are enough providers. */ while (set.size() > providers.size()) { providers.push_back( Util::make_unique() ); } /* Create the messages. */ auto messages = std::vector(); for (auto i = std::size_t(0); i < set.size(); ++i) { messages.emplace_back(ProvideSwapQuotation{ set[i], s.solicitor, providers[i].get(), "MockSwapQuotation" }); } /* Send them. */ auto f = [&bus](ProvideSwapQuotation q) { return bus.raise(std::move(q)); }; return Ev::foreach(f, std::move(messages)); }); } void add(std::vector set) { fees.emplace(std::move(set)); } bool empty() const { return fees.empty(); } }; class MockSwapCreation { public: struct Entry { /* The expected address, if not match, fail. */ std::string address; /* Details. */ bool success; std::string invoice; Sha256::Hash hash; std::uint32_t timeout; }; private: std::queue entries; std::set invoices; public: MockSwapCreation(S::Bus& bus) { using Boss::Msg::AcceptSwapQuotation; using Boss::Msg::PayInvoice; using Boss::Msg::SwapCreation; bus.subscribe([ this , &bus ](AcceptSwapQuotation const& q) { assert(!entries.empty()); auto entry = std::move(entries.front()); entries.pop(); /* Validate. */ assert(q.onchain_address == entry.address); auto found = false; for (auto const& p : providers) { if (p.get() == q.provider) { found = true; break; } } assert(found); /* If success, add invoice to set of invoices * we expect. */ if (entry.success) invoices.insert(entry.invoice); return bus.raise(SwapCreation{ entry.success, entry.invoice, entry.hash, entry.timeout, q.solicitor, q.provider }); }); bus.subscribe([this](PayInvoice const& p) { auto it = invoices.find(p.invoice); assert(it != invoices.end()); invoices.erase(it); return Ev::lift(); }); } void add(Entry e) { entries.emplace(std::move(e)); } bool okay() const { return entries.empty() && invoices.empty(); } }; class MockListfunds { private: S::Bus& bus; public: MockListfunds(S::Bus& bus_) : bus(bus_) { } Ev::Io listfunds(std::vector> const& addr_amount) { /* Generate first via Json::Out. */ auto outputs = Json::Out(); auto arr = outputs.start_array(); for (auto const& e : addr_amount) { auto obj = arr.start_object(); obj.field("address", e.first); obj.field("amount_msat", std::string(e.second)); obj.end_object(); } arr.end_array(); /* Convert to Jsmn::Object. */ auto outputs_s = outputs.output(); auto is = std::istringstream(std::move(outputs_s)); auto outputs_j = Jsmn::Object(); is >> outputs_j; return bus.raise(Boss::Msg::ListfundsResult{ std::move(outputs_j), Jsmn::Object() }); } }; class MockRequester { private: S::Bus& bus; Sqlite3::Db db; std::unordered_set pendings; std::unordered_map finished; public: MockRequester(S::Bus& bus_) : bus(bus_) { bus.subscribe([this](Boss::Msg::DbResource const& r) { db = r.db; return Ev::lift(); }); bus.subscribe([this](Boss::Msg::SwapResponse const& r) { auto tx = std::move(*r.dbtx); auto it = pendings.find(r.id); assert(it != pendings.end()); pendings.erase(it); auto it_m = finished.find(r.id); assert(it_m == finished.end()); finished[r.id] = r.success; tx.commit(); return Ev::lift(); }); } Ev::Io start_swap(Uuid uuid, Ln::Amount min, Ln::Amount max) { return db.transact().then([this, uuid, min, max ](Sqlite3::Tx tx) { pendings.insert(uuid); auto sh_tx = std::make_shared( std::move(tx) ); return bus.raise(Boss::Msg::SwapRequest{ sh_tx, uuid, min, max }).then([sh_tx]() { assert(sh_tx); assert(!*sh_tx); return Ev::lift(); }); }); } void expect_finished(Uuid uuid, bool expect_success) { auto it = finished.find(uuid); assert(it != finished.end()); assert(it->second == expect_success); } }; Ev::Io wait() { auto act = Ev::yield(); for (auto i = std::size_t(0); i < 200; ++i) act = std::move(act).then([]() { return Ev::yield(); }); return act; } /*----------------------------------------------------------------------------- Test Harness -----------------------------------------------------------------------------*/ S::Bus bus; Sqlite3::Db db(":memory:"); MockLog logger(bus); MockNewaddr addresses(bus); MockSwapQuotation quotations(bus); MockSwapCreation swapsetups(bus); MockListfunds onchain(bus); MockRequester requester(bus); /* Object under test. */ Boss::Mod::SwapManager swapper(bus); /*----------------------------------------------------------------------------- Tests -----------------------------------------------------------------------------*/ Ev::Io test_simple() { Uuid uuid = Uuid::random(); return Ev::lift().then([&, uuid]() { addresses.add("address1"); quotations.add({Ln::Amount::sat(1000), Ln::Amount::sat(100)}); /* Fail the first provider, but succeed the next one. */ swapsetups.add({"address1", false, "", Sha256::Hash(), 0}); swapsetups.add({"address1", true, "invoice1" , Sha256::Hash("ec8103d3f57a561fd213bf6bb45b2cd0e1b37738232c7d1fd13b1a7729bf0982") , 12345 }); return requester.start_swap( uuid , Ln::Amount::btc(0.001) , Ln::Amount::btc(0.012) ); }).then([&]() { return wait(); }).then([&]() { assert(addresses.empty()); assert(quotations.empty()); assert(swapsetups.okay()); return onchain.listfunds({ { "unrelated-address" , Ln::Amount::sat(42) } }); }).then([&]() { return onchain.listfunds({ { "unrelated-address" , Ln::Amount::sat(42) } , { "address1" , Ln::Amount::sat(12100) } }); }).then([&]() { return wait(); }).then([&, uuid]() { requester.expect_finished(uuid, true); return Ev::lift(); }); } } /*----------------------------------------------------------------------------- Setup -----------------------------------------------------------------------------*/ int main() { auto code = Ev::lift().then([&]() { return bus.raise(Boss::Msg::DbResource{db}); }).then([&]() { return test_simple(); }).then([&]() { return Ev::lift(0); }); return Ev::start(code); }