mirror of
https://github.com/ZmnSCPxj/clboss.git
synced 2026-08-14 12:43:19 +02:00
357 lines
8.7 KiB
C++
357 lines
8.7 KiB
C++
#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<assert.h>
|
|
#include<iostream>
|
|
#include<memory>
|
|
#include<queue>
|
|
#include<set>
|
|
#include<sstream>
|
|
#include<unordered_map>
|
|
#include<unordered_set>
|
|
|
|
namespace {
|
|
|
|
/*-----------------------------------------------------------------------------
|
|
Mocks
|
|
-----------------------------------------------------------------------------*/
|
|
|
|
class MockLog {
|
|
public:
|
|
MockLog(S::Bus& bus) {
|
|
bus.subscribe<Boss::Msg::JsonCout
|
|
>([](Boss::Msg::JsonCout const& j) {
|
|
std::cout << j.obj.output() << std::endl;
|
|
return Ev::lift();
|
|
});
|
|
}
|
|
};
|
|
class MockNewaddr {
|
|
private:
|
|
std::queue<std::string> addresses;
|
|
public:
|
|
MockNewaddr(S::Bus& bus) {
|
|
bus.subscribe<Boss::Msg::RequestNewaddr
|
|
>([ 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<std::unique_ptr<MockProvider>> providers;
|
|
class MockSwapQuotation {
|
|
private:
|
|
/* A vector, since we want to check what happens when multiple
|
|
* quotations exist.
|
|
*/
|
|
std::queue<std::vector<Ln::Amount>> fees;
|
|
public:
|
|
MockSwapQuotation(S::Bus& bus) {
|
|
using Boss::Msg::ProvideSwapQuotation;
|
|
using Boss::Msg::SolicitSwapQuotation;
|
|
bus.subscribe<SolicitSwapQuotation
|
|
>([ 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<MockProvider>()
|
|
);
|
|
}
|
|
/* Create the messages. */
|
|
auto messages = std::vector<ProvideSwapQuotation>();
|
|
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<Ln::Amount> 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<Entry> entries;
|
|
std::set<std::string> invoices;
|
|
public:
|
|
MockSwapCreation(S::Bus& bus) {
|
|
using Boss::Msg::AcceptSwapQuotation;
|
|
using Boss::Msg::PayInvoice;
|
|
using Boss::Msg::SwapCreation;
|
|
bus.subscribe<AcceptSwapQuotation
|
|
>([ 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<PayInvoice
|
|
>([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<void>
|
|
listfunds(std::vector<std::pair< std::string
|
|
, Ln::Amount
|
|
>> 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<Uuid> pendings;
|
|
std::unordered_map<Uuid, bool> finished;
|
|
|
|
public:
|
|
MockRequester(S::Bus& bus_) : bus(bus_) {
|
|
bus.subscribe<Boss::Msg::DbResource
|
|
>([this](Boss::Msg::DbResource const& r) {
|
|
db = r.db;
|
|
return Ev::lift();
|
|
});
|
|
bus.subscribe<Boss::Msg::SwapResponse
|
|
>([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<void> 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<Sqlite3::Tx>(
|
|
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<void> 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<void> 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);
|
|
}
|