crypto payment message
All checks were successful
Deploy / build-deploy (push) Successful in 3m13s

This commit is contained in:
Jorijn van der Graaf 2026-08-20 02:50:28 +02:00
commit 1a38cfe5d7
7 changed files with 169 additions and 17 deletions

View file

@ -115,6 +115,7 @@ struct AdvanceResult {
};
std::optional<AdvanceResult> PollAndAdvance(const OrderRecord& order);
bool ArrivalPollAllowed(std::string_view token, std::chrono::seconds interval);
bool MoneySeenRecently(std::string_view token);
std::string NowIso8601();
// Common headers on every HTML response.
@ -317,6 +318,7 @@ HTTPResponse RenderPage(std::string_view target) {
std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
pay.minutesLeft = (instr->deadlineUnix - now) / 60;
pay.seen = MoneySeenRecently(order->token);
for (auto& c : instr->chains) {
pay.chains.push_back({ std::move(c.name), std::move(c.contract),
std::move(c.link), std::move(c.note) });
@ -894,6 +896,34 @@ HTTPResponse HandleCheckout(const HTTPRequest& req, const Route& route) {
std::mutex gArrivalPollMutex;
std::unordered_map<std::string, std::chrono::steady_clock::time_point> gLastArrivalPoll;
// Orders whose payment the rail has SEEN in flight (visible on the network,
// finality pending), stamped by whichever poll noticed — the reconciler or an
// arrival check. Read at render time so the page can acknowledge the money
// without the render path ever dialing a provider itself: the page shows the
// most recent poll's knowledge, which is exactly as fresh as "paid" would be.
// Entries only ever accumulate truth ("seen" is never un-seen by one flaky
// probe) and age out; a settled or lapsed order stops rendering the section
// that reads this, so stale entries are harmless.
std::unordered_map<std::string, std::chrono::steady_clock::time_point> gMoneySeen;
void RememberMoneySeen(std::string_view token) {
const auto now = std::chrono::steady_clock::now();
std::lock_guard lock(gArrivalPollMutex);
std::erase_if(gMoneySeen, [&](const auto& entry) {
return now - entry.second > std::chrono::hours(1);
});
gMoneySeen[std::string(token)] = now;
}
bool MoneySeenRecently(std::string_view token) {
const auto now = std::chrono::steady_clock::now();
std::lock_guard lock(gArrivalPollMutex);
const auto it = gMoneySeen.find(std::string(token));
// Ten minutes covers several finality epochs; a payment seen longer ago
// that still has not settled is a claim this page should stop making.
return it != gMoneySeen.end() && now - it->second < std::chrono::minutes(10);
}
bool ArrivalPollAllowed(std::string_view token, std::chrono::seconds interval) {
const auto now = std::chrono::steady_clock::now();
std::lock_guard lock(gArrivalPollMutex);
@ -924,6 +954,9 @@ std::optional<AdvanceResult> PollAndAdvance(const OrderRecord& order) {
if (!rail) return std::nullopt;
const std::optional<PaidStatus> paid = rail->CheckPaid(order.payId, order.totalMinor);
if (!paid.has_value()) return std::nullopt;
if (paid->state == PayState::Pending && paid->seen) {
RememberMoneySeen(order.token);
}
if (paid->state == PayState::Paid) {
if (AppendOrderStatus(order.token, "paid", NowIso8601(), paid->method)) {
// The invoice number exists from the moment the money does —