All checks were successful
Deploy / build-deploy (push) Successful in 2m26s
213 lines
11 KiB
C++
213 lines
11 KiB
C++
/*
|
||
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");
|
||
|
||
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;
|
||
}
|