catcrafts.net/tests/ShouldParseEurcChains/main.cpp
Jorijn van der Graaf 45992c4f91
Some checks failed
Deploy / build-deploy (push) Failing after 4m58s
payment tests
2026-08-20 01:36:42 +02:00

234 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
catcrafts.net
Copyright (C) 2026 Catcrafts
The source code of this website is made available for viewing purposes only.
No permission is granted to copy, modify, distribute, or create derivative works.
*/
// The EURC rail's two parsers.
//
// The balance decoder is the line between "the buyer paid" and "the node
// errored", and the chains parser is the line between "watching a chain"
// and "silently not watching it". Both refuse rather than guess.
import std;
import Catcrafts.Shared;
import Catcrafts.Server;
using namespace Catcrafts;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what, std::string_view got = {}) {
if (ok) return;
++failures;
std::println(std::cerr, "FAIL: {}{}{}", what,
got.empty() ? "" : " got: ", got);
}
} // namespace
int main() {
using Server::ParseEthCallUint;
Check(ParseEthCallUint(R"({"jsonrpc":"2.0","id":1,"result":"0x0"})") == 0,
"eurc: zero balance");
Check(ParseEthCallUint(
R"({"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000022001230"})")
== 570430000,
"eurc: €570.43 as 6-decimal base units, full 32-byte word");
Check(ParseEthCallUint(R"({"result":"0xFF"})") == 255,
"eurc: uppercase hex accepted");
// 2^63 does not fit. It must not wrap to negative, and it must not
// saturate to INT64_MAX either: a saturated maximum satisfies the covering
// comparison in CheckPaid, so a node answering 0xffff…ff would mark any
// order paid. int64 base units is already past EURC's entire supply, so an
// unrepresentable balance is a broken or lying node — "unknown", which
// neither settles nor lapses.
Check(!ParseEthCallUint(R"({"result":"0x8000000000000000"})").has_value(),
"eurc: an unrepresentable balance is unknown, not a covering maximum");
Check(!ParseEthCallUint(
R"({"result":"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"})")
.has_value(),
"eurc: a lying node's 2^256-1 does not read as paid");
Check(!ParseEthCallUint(
R"({"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"x"}})")
.has_value(),
"eurc: an RPC error is not a zero balance");
Check(!ParseEthCallUint(R"({"result":"21ff361c"})").has_value(),
"eurc: missing 0x rejected");
Check(!ParseEthCallUint(R"({"result":"0x"})").has_value(),
"eurc: empty word rejected");
Check(!ParseEthCallUint(R"({"result":42})").has_value(),
"eurc: numeric result rejected — the wire type is a hex string");
Check(!ParseEthCallUint("garbage").has_value(), "eurc: malformed payload");
// The OUTBOUND half of the balance check, byte for byte: selector, then
// the address left-padded into exactly one 32-byte ABI word — 24 zero hex
// digits (12 bytes), then the 40 address digits. Pinned as a literal
// because this line once padded 24 BYTES instead of 12: the address slid
// past the argument word, every node cleanly answered balanceOf of a
// zero-balance garbage address, and paid orders read as unpaid forever —
// no error on either side. A wrong length here is money-losing even when
// every reply parses.
{
const std::string data = Server::BalanceOfCallData(
"0x311660cfd1d0c35616cf6dfc3932881b22bb46ec");
Check(data ==
"0x70a08231"
"000000000000000000000000"
"311660cfd1d0c35616cf6dfc3932881b22bb46ec",
"eurc: balanceOf calldata is selector + one 32-byte word", data);
Check(data.size() == 2 + 8 + 64,
"eurc: balanceOf calldata is exactly 4 + 32 bytes",
std::to_string(data.size()));
}
const auto chains = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://mainnet.base.org",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42",
"chain_id":8453,"note":"lowest fees"},
{"name":"ethereum","rpc":"https://ethereum-rpc.publicnode.com",
"contract":"0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c","chain_id":1}]})");
Check(chains.has_value() && chains->size() == 2, "eurc: chains file parses");
if (chains && chains->size() == 2) {
Check((*chains)[0].name == "base" && (*chains)[0].chainId == 8453,
"eurc: file order preserved — first entry is the recommendation");
Check((*chains)[0].contract == "0x60a3e35cc302bfa44cb288bc5a4f316fdb1adb42",
"eurc: contract lowercased for comparison");
Check((*chains)[0].note == "lowest fees", "eurc: note carried");
Check((*chains)[1].blockTag == "finalized" && (*chains)[1].decimals == 6,
"eurc: defaults");
}
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://x.org","contract":"0xdeadbeef"}]})").has_value(),
"eurc: short contract rejects the whole file");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"ftp://x.org",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: non-http rpc rejects the whole file");
Check(!Server::ParseEurcChains(R"({"chains":[]})").has_value(),
"eurc: empty chain list rejected");
Check(!Server::ParseEurcChains("garbage").has_value(),
"eurc: malformed chains file rejected");
// Two chains with one name would share a connection-map slot, so the
// second's requests would go to the first's host and one chain would never
// be watched at all.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"},
{"name":"base","rpc":"https://b.example",
"contract":"0x1aBaEA1f7C830bD89Acc67eC4af516284b1bC33c"}]})").has_value(),
"eurc: duplicate chain names reject the whole file");
// 18 decimals is legal ERC-20 but not representable here: cents × 10^16
// overflows int64 above €9.22, and the overflow answers "unknown", which
// neither settles nor lapses an order.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example","decimals":18,
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: decimals beyond what the arithmetic carries is refused");
// block_tag reaches the eth_call params array. A quote in it closed the
// JSON string and appended another param; a typo silenced the chain.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example","block_tag":"latest\",\"0xdead",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: a block_tag that injects JSON is refused");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example","block_tag":"finalised",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: a misspelled block_tag is refused, not silently never-settling");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example","block_tag":"",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: an empty block_tag is refused");
{
// A specific block number stays legitimate.
const auto ok = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example","block_tag":"0x1b4",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})");
Check(ok.has_value() && (*ok)[0].blockTag == "0x1b4",
"eurc: a hex block number is still accepted");
}
// ── corroboration: multiple endpoints per chain ──────────────────
{
// The single-"rpc" shorthand still parses, and lands as one endpoint
// with min_confirmations=1 (the pre-corroboration behaviour).
const auto one = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpc":"https://a.example",
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})");
Check(one.has_value() && (*one)[0].rpcUrls.size() == 1
&& (*one)[0].rpcUrls[0] == "https://a.example"
&& (*one)[0].minConfirmations == 1,
"eurc: the single-rpc shorthand still loads, one source, one confirmation");
// Two endpoints default to requiring agreement between both.
const auto two = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","https://b.example"],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})");
Check(two.has_value() && (*two)[0].rpcUrls.size() == 2
&& (*two)[0].minConfirmations == 2,
"eurc: two endpoints default to two confirmations");
// Three endpoints still default to two — a quorum, not unanimity, so
// one node being down does not stop the shop settling.
const auto three = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","https://b.example",
"https://c.example"],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})");
Check(three.has_value() && (*three)[0].minConfirmations == 2,
"eurc: three endpoints still need two agreeing, not all three");
// An explicit value is honoured.
const auto strict = Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","https://b.example",
"https://c.example"],"min_confirmations":3,
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})");
Check(strict.has_value() && (*strict)[0].minConfirmations == 3,
"eurc: an explicit min_confirmations is honoured");
}
// One endpoint cannot corroborate itself, so a repeated URL is refused
// rather than counted twice toward the quorum.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","https://a.example"],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: the same endpoint listed twice is refused");
// Asking for more agreement than there are sources would never settle
// anything — a silent never-pays shop, so it is a refusal at load.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","https://b.example"],
"min_confirmations":3,
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: more confirmations than endpoints is refused");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example"],"min_confirmations":0,
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: zero confirmations is refused");
// An empty rpcs list is not a chain that can be watched.
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":[],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: an empty endpoint list is refused");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example",42],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: a non-string endpoint is refused");
Check(!Server::ParseEurcChains(R"({"chains":[
{"name":"base","rpcs":["https://a.example","ftp://b.example"],
"contract":"0x60a3E35Cc302bFA44Cb288Bc5a4F316Fdb1adb42"}]})").has_value(),
"eurc: a non-http endpoint anywhere in the list is refused");
if (failures != 0) {
std::println(std::cerr, "{} check(s) failed", failures);
return 1;
}
return 0;
}