catcrafts.net/tests/ShouldParseEurcChains/main.cpp

234 lines
12 KiB
C++
Raw Normal View History

2026-08-15 00:54:05 +02:00
/*
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");
2026-08-19 23:55:01 +02:00
// 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");
2026-08-15 00:54:05 +02:00
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");
2026-08-20 01:36:42 +02:00
// 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()));
}
2026-08-15 00:54:05 +02:00
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");
2026-08-19 23:55:01 +02:00
// 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");
}
2026-08-20 00:09:08 +02:00
// ── 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");
2026-08-15 00:54:05 +02:00
if (failures != 0) {
std::println(std::cerr, "{} check(s) failed", failures);
return 1;
}
return 0;
}