diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml index c911ab3..c6ab31b 100644 --- a/.github/workflows/build-all.yml +++ b/.github/workflows/build-all.yml @@ -270,6 +270,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DBUILD_QT=OFF \ -DBUILD_DAEMON=ON \ + -DBUILD_CLI=ON \ -DBUILD_TESTS=OFF \ -DUSE_UPNP=ON @@ -277,16 +278,10 @@ jobs: run: | cmake --build build -j$(nproc) strip --strip-all build/bin/trianglesd.exe + strip --strip-all build/bin/triangles-cli.exe - name: Package daemon with DLLs - run: | - mkdir -p daemon-dist/tor - cp build/bin/trianglesd.exe daemon-dist/ - - # Copy all linked DLLs from MSYS2 - ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' | while read dll; do - cp "$dll" daemon-dist/ 2>/dev/null || true - done + run: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli - name: Bundle Tor for daemon shell: powershell @@ -456,6 +451,7 @@ jobs: -DCMAKE_BUILD_TYPE=Release \ -DBUILD_QT=OFF \ -DBUILD_DAEMON=ON \ + -DBUILD_CLI=ON \ -DBUILD_TESTS=OFF \ -DUSE_UPNP=ON @@ -463,93 +459,12 @@ jobs: run: cmake --build build -j$(nproc) - name: Strip binary - run: strip --strip-all build/bin/trianglesd + run: | + strip --strip-all build/bin/trianglesd + strip --strip-all build/bin/triangles-cli - name: Build .deb package (fully self-contained) - run: | - TOR_VERSION="15.0.9" - curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" -o tor-bundle.tar.gz - mkdir -p tor-extract && tar -xzf tor-bundle.tar.gz -C tor-extract - - PKG="cryptographic-triangles-daemon_${VERSION}_amd64" - mkdir -p ${PKG}/DEBIAN - mkdir -p ${PKG}/usr/lib/cryptographic-triangles/lib - mkdir -p ${PKG}/usr/lib/cryptographic-triangles/tor - mkdir -p ${PKG}/usr/bin - mkdir -p ${PKG}/etc/systemd/system - - cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/ - cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/ - chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/tor - [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data - - # Bundle ALL shared library dependencies (except glibc/kernel) - ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' | while read lib; do - case "$lib" in - /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) - ;; # Skip glibc core — always present - *) - cp -L "$lib" ${PKG}/usr/lib/cryptographic-triangles/lib/ 2>/dev/null || true - ;; - esac - done - echo "=== Bundled libs ===" - ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l - ls ${PKG}/usr/lib/cryptographic-triangles/lib/ - - # Launcher with LD_LIBRARY_PATH - cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER' - #!/bin/bash - INSTALL_DIR=/usr/lib/cryptographic-triangles - export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" - exec "${INSTALL_DIR}/trianglesd" "$@" - LAUNCHER - sed -i 's/^ //' ${PKG}/usr/bin/trianglesd - chmod +x ${PKG}/usr/bin/trianglesd - - cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC' - [Unit] - Description=Cryptographic Triangles Daemon - After=network-online.target - Wants=network-online.target - - [Service] - Type=simple - Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib - ExecStart=/usr/lib/cryptographic-triangles/trianglesd - Restart=on-failure - RestartSec=10 - - [Install] - WantedBy=multi-user.target - SVC - sed -i 's/^ //' ${PKG}/etc/systemd/system/trianglesd.service - - cat > ${PKG}/DEBIAN/control << CTRL - Package: cryptographic-triangles-daemon - Version: ${VERSION} - Architecture: amd64 - Maintainer: Cryptographic Triangles - Description: Cryptographic Triangles daemon with integrated Tor - Fully self-contained headless node with all libraries, Tor, and systemd service. - No external dependencies required — runs on any x86_64 Linux. - Section: finance - Priority: optional - CTRL - sed -i 's/^ //' ${PKG}/DEBIAN/control - - cat > ${PKG}/DEBIAN/postinst << 'POST' - #!/bin/bash - systemctl daemon-reload - echo "" - echo "Cryptographic Triangles daemon installed." - echo " Start: sudo systemctl start trianglesd" - echo " On boot: sudo systemctl enable trianglesd" - echo "" - POST - chmod +x ${PKG}/DEBIAN/postinst - - dpkg-deb --build ${PKG} + run: bash scripts/ci/package-linux-daemon.sh "${VERSION}" - name: Upload .deb uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 77ab044..e29ae2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -47,6 +47,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") # ── User-facing options ── option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON) option(BUILD_DAEMON "Build trianglesd (headless daemon)" ON) +option(BUILD_CLI "Build triangles-cli (JSON-RPC client)" ON) option(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON) option(USE_UPNP "Enable UPnP support via miniupnpc" ON) option(USE_IPV6 "Enable IPv6 support" ON) @@ -185,6 +186,7 @@ message(STATUS "") message(STATUS "Triangles ${PROJECT_VERSION} build configuration:") message(STATUS " Build Qt GUI: ${BUILD_QT}") message(STATUS " Build daemon: ${BUILD_DAEMON}") +message(STATUS " Build CLI: ${BUILD_CLI}") message(STATUS " Build tests: ${BUILD_TESTS}") message(STATUS " UPnP: ${USE_UPNP}") message(STATUS " IPv6: ${USE_IPV6}") diff --git a/scripts/ci/package-linux-daemon.sh b/scripts/ci/package-linux-daemon.sh new file mode 100755 index 0000000..ad19bb4 --- /dev/null +++ b/scripts/ci/package-linux-daemon.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# scripts/ci/package-linux-daemon.sh +# +# Linux packaging step for the triangles daemon + CLI .deb. +# Called from .github/workflows/build-all.yml build-linux-daemon step. +# +# Builds a self-contained .deb with trianglesd, triangles-cli, bundled libs, +# Tor, systemd service, and CLI launchers. Designed to be reproducible and +# debuggable outside the CI environment. +# +# Usage: bash scripts/ci/package-linux-daemon.sh + +set -euo pipefail + +VERSION="${1:-0.0.0}" +PKG="cryptographic-triangles-daemon_${VERSION}_amd64" +TOR_VERSION="${TOR_VERSION:-15.0.9}" + +echo ">>> Building .deb for triangles ${VERSION}" + +# Stage directories +rm -rf "${PKG}" +mkdir -p "${PKG}/DEBIAN" +mkdir -p "${PKG}/usr/lib/cryptographic-triangles/lib" +mkdir -p "${PKG}/usr/lib/cryptographic-triangles/tor" +mkdir -p "${PKG}/usr/bin" +mkdir -p "${PKG}/etc/systemd/system" + +# Download + extract Tor +TOR_TARBALL="tor-expert-bundle-linux-x86_64-${TOR_VERSION}.tar.gz" +if [ ! -f "${TOR_TARBALL}" ]; then + echo ">>> Downloading Tor ${TOR_VERSION}..." + curl -sL "https://archive.torproject.org/tor-package-archive/torbrowser/${TOR_VERSION}/${TOR_TARBALL}" -o "${TOR_TARBALL}" +fi +mkdir -p tor-extract +tar -xzf "${TOR_TARBALL}" -C tor-extract + +# Copy binaries +cp "build/bin/trianglesd" "${PKG}/usr/lib/cryptographic-triangles/" +cp "build/bin/triangles-cli" "${PKG}/usr/lib/cryptographic-triangles/" + +# Copy Tor +cp "tor-extract/tor/tor" "${PKG}/usr/lib/cryptographic-triangles/tor/" +chmod +x "${PKG}/usr/lib/cryptographic-triangles/tor/tor" +if [ -d "tor-extract/data" ]; then + cp -r "tor-extract/data" "${PKG}/usr/lib/cryptographic-triangles/tor/data" +fi + +# Bundle shared library dependencies (skip glibc/kernel — always present) +echo ">>> Bundling shared library dependencies..." +ALL_LIBS="$(mktemp)" +trap 'rm -f "${ALL_LIBS}"' EXIT + +for bin in trianglesd triangles-cli; do + ldd "build/bin/${bin}" 2>/dev/null \ + | grep '=> /' \ + | awk '{print $3}' \ + >> "${ALL_LIBS}" || true +done + +if [ -s "${ALL_LIBS}" ]; then + sort -u "${ALL_LIBS}" | while IFS= read -r lib; do + if [ -z "${lib}" ]; then continue; fi + case "${lib}" in + /lib/x86_64-linux-gnu/libc.so*|/lib/x86_64-linux-gnu/libm.so*|/lib/x86_64-linux-gnu/libpthread.so*|/lib/x86_64-linux-gnu/libdl.so*|/lib/x86_64-linux-gnu/librt.so*|/lib/x86_64-linux-gnu/ld-linux*|/lib64/ld-linux*) + ;; # Skip glibc core + *) + cp -L "${lib}" "${PKG}/usr/lib/cryptographic-triangles/lib/" 2>/dev/null || true + ;; + esac + done +fi + +echo ">>> Bundled libs:" +ls -la "${PKG}/usr/lib/cryptographic-triangles/lib/" | tail -n +2 | wc -l + +# Launchers (set LD_LIBRARY_PATH for bundled libs) +cat > "${PKG}/usr/bin/trianglesd" << 'LAUNCHER' +#!/bin/bash +INSTALL_DIR=/usr/lib/cryptographic-triangles +export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" +exec "${INSTALL_DIR}/trianglesd" "$@" +LAUNCHER +chmod +x "${PKG}/usr/bin/trianglesd" + +cat > "${PKG}/usr/bin/triangles-cli" << 'LAUNCHER' +#!/bin/bash +INSTALL_DIR=/usr/lib/cryptographic-triangles +export LD_LIBRARY_PATH="${INSTALL_DIR}/lib:${LD_LIBRARY_PATH}" +exec "${INSTALL_DIR}/triangles-cli" "$@" +LAUNCHER +chmod +x "${PKG}/usr/bin/triangles-cli" + +# systemd unit +cat > "${PKG}/etc/systemd/system/trianglesd.service" << 'SVC' +[Unit] +Description=Cryptographic Triangles Daemon +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +Environment=LD_LIBRARY_PATH=/usr/lib/cryptographic-triangles/lib +ExecStart=/usr/lib/cryptographic-triangles/trianglesd +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +SVC + +# DEBIAN/control +cat > "${PKG}/DEBIAN/control" << CTRL +Package: cryptographic-triangles-daemon +Version: ${VERSION} +Architecture: amd64 +Maintainer: Cryptographic Triangles +Description: Cryptographic Triangles daemon + CLI with integrated Tor + Fully self-contained headless node + JSON-RPC client with all libraries, + Tor, and systemd service. No external dependencies required. +Section: finance +Priority: optional +CTRL + +# DEBIAN/postinst +cat > "${PKG}/DEBIAN/postinst" << 'POST' +#!/bin/bash +systemctl daemon-reload +echo "" +echo "Cryptographic Triangles daemon + CLI installed." +echo " Start daemon: sudo systemctl start trianglesd" +echo " On boot: sudo systemctl enable trianglesd" +echo " Use CLI: triangles-cli getinfo" +echo "" +POST +chmod +x "${PKG}/DEBIAN/postinst" + +# Build the .deb +dpkg-deb --build "${PKG}" +echo ">>> Built: ${PKG}.deb" +ls -la "${PKG}.deb" +exit 0 diff --git a/scripts/ci/package-windows-daemon.sh b/scripts/ci/package-windows-daemon.sh new file mode 100755 index 0000000..4f22622 --- /dev/null +++ b/scripts/ci/package-windows-daemon.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# scripts/ci/package-windows-daemon.sh +# +# Windows MSYS2 packaging step for the triangles daemon + CLI. +# Called from .github/workflows/build-all.yml build-windows-daemon step. +# +# Why a script file instead of inline YAML: +# The GitHub Actions msys2 shell wrapper has shown inconsistent handling of +# multi-line inline run: blocks under `set -e -o pipefail` (silent exits with +# code 1). A committed script file bypasses the YAML → shell translation +# quirks and gives us a known-good artifact that we can also run locally in +# MSYS2 for debugging. +# +# Usage: bash scripts/ci/package-windows-daemon.sh [ ...] +# Example: bash scripts/ci/package-windows-daemon.sh daemon-dist trianglesd triangles-cli + +set -euo pipefail + +DIST="${1:-daemon-dist}" +shift +BINS=("$@") + +if [ "${#BINS[@]}" -eq 0 ]; then + echo "Usage: $0 [ ...]" >&2 + echo " e.g. $0 daemon-dist trianglesd triangles-cli" >&2 + exit 2 +fi + +echo ">>> Package step: bins=${BINS[*]} dist=${DIST}" + +# Make the dist directory +mkdir -p "${DIST}/tor" + +# Copy each binary to dist/ +for bin in "${BINS[@]}"; do + src="build/bin/${bin}.exe" + if [ ! -f "${src}" ]; then + echo "ERROR: ${src} not found" >&2 + exit 3 + fi + cp "${src}" "${DIST}/" + echo " copied ${src} -> ${DIST}/" +done + +# Copy linked DLLs (union of all binaries' dependencies, deduped) +echo ">>> Collecting DLLs from ldd output..." +ALL_DLLS="$(mktemp)" +trap 'rm -f "${ALL_DLLS}"' EXIT + +for bin in "${BINS[@]}"; do + src="build/bin/${bin}.exe" + ldd "${src}" 2>/dev/null \ + | grep '/mingw64' \ + | awk '{print $3}' \ + >> "${ALL_DLLS}" || true +done + +if [ ! -s "${ALL_DLLS}" ]; then + echo "WARNING: no /mingw64 DLLs found in ldd output for ${BINS[*]}" >&2 +else + echo ">>> Copying $(sort -u "${ALL_DLLS}" | wc -l) unique DLLs..." + sort -u "${ALL_DLLS}" | while IFS= read -r dll; do + if [ -n "${dll}" ] && [ -f "${dll}" ]; then + cp "${dll}" "${DIST}/" || echo "WARN: failed to copy ${dll}" >&2 + fi + done +fi + +echo ">>> Package complete: $(ls -1 "${DIST}" | wc -l) files in ${DIST}/" +ls -la "${DIST}/" +exit 0 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9b5fd86..6c1ee8e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -250,6 +250,39 @@ if(BUILD_DAEMON) endif() endif() +# ═══════════════════════════════════════════════════════════════════════════════ +# 4b. JSON-RPC client (triangles-cli) +# +# Self-contained: only links univalue + boost::asio + boost::program_options +# + boost::filesystem + OpenSSL (for base64 / future TLS). Does NOT link +# triangles_common, wallet, or net — keeps the binary small. +# ═══════════════════════════════════════════════════════════════════════════════ +if(BUILD_CLI) + add_executable(triangles-cli + triangles-cli.cpp + ) + # No Boost dependency: uses raw POSIX/Winsock sockets for HTTP. Only links + # the json_compat header-only shim and the platform's native socket lib + # (Winsock ws2_32 on Windows; libc on POSIX). Keeps the binary small and + # avoids per-platform Boost linking pain (MSYS2 uses versioned -mt- names; + # Homebrew doesn't ship the boost_system CMake config). + target_link_libraries(triangles-cli + PRIVATE + json_compat + ) + + if(WIN32) + set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe") + target_link_libraries(triangles-cli PRIVATE ws2_32) + endif() + + if(MSVC) + set_target_properties(triangles-cli PROPERTIES + VS_WINRT_COMPONENT "console" + ) + endif() +endif() + # ═══════════════════════════════════════════════════════════════════════════════ # 5. Qt5 GUI wallet (triangles-qt) # ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/triangles-cli.cpp b/src/triangles-cli.cpp new file mode 100644 index 0000000..f5ecf73 --- /dev/null +++ b/src/triangles-cli.cpp @@ -0,0 +1,616 @@ +// Copyright (c) 2014-2026 The Cryptographic Triangles developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. +// +// triangles-cli — JSON-RPC client for trianglesd. +// +// Talks to a running trianglesd over HTTP/1.1 with HTTP Basic auth and +// JSON-RPC 1.0. Patterned after bitcoin-cli (Bitcoin Core) and dash-cli. +// +// Build with -DBUILD_CLI=ON (default ON). +// +// Self-contained: does NOT link util.cpp / wallet.cpp / net.cpp / triangles_common. +// Only links json_compat (nlohmann/json via json_spirit shim) and the platform's +// native socket library (Winsock on Windows, libc on POSIX). No Boost dependency +// at all — keeps the binary small and avoids platform-specific link problems +// with boost::asio / libboost_system (MSYS2 names them with -mt- versioned +// suffixes; Homebrew doesn't ship the CMake config for the system component). +// +// Connection parameters (highest precedence first): +// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport +// 2. triangles.conf in the data directory (or -conf=) +// 3. Defaults: 127.0.0.1:19111 mainnet, 19112 testnet; no auth (must be set in conf) +// +// Usage: +// triangles-cli help List commands (delegates to daemon) +// triangles-cli help Help for one command +// triangles-cli getinfo Example: summary info +// triangles-cli getblockchaininfo Example: chain state +// triangles-cli getbalance Example: 0-arg call +// triangles-cli getbalance "*" 6 Example: positional args +// triangles-cli sendtoaddress 1.5 "memo" Example: mixed types +// triangles-cli -getinfo Synthesized summary from multiple RPCs +// triangles-cli -raw Print raw JSON response (no pretty-print) +// +// Any command-line arg that parses as a JSON literal (number, bool, null, +// object, array) is forwarded as that literal; otherwise it is sent as a JSON +// string. This matches bitcoin-cli semantics. + +#define TRIANGLES_CLI_VERSION "1.0.0" + +#include "json/json_compat.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Cross-platform socket includes +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include + #include + #pragma comment(lib, "ws2_32.lib") + using socket_t = SOCKET; + #define TRI_CLI_INVALID_SOCKET INVALID_SOCKET + #define TRI_CLI_CLOSE_SOCKET(s) closesocket(s) +#else + #include + #include + #include + #include + #include + #include + #include + #include + using socket_t = int; + #define TRI_CLI_INVALID_SOCKET (-1) + #define TRI_CLI_CLOSE_SOCKET(s) close(s) +#endif + +using namespace std; +namespace fs = std::filesystem; +using namespace json_spirit; + +// ───────────────────────────────────────────────────────────────────────────── +// Minimal arg/config plumbing — self-contained, no util.cpp dep. +// ───────────────────────────────────────────────────────────────────────────── + +static map mapArgs; +static map > mapMultiArgs; + +static string GetArg(const string& key, const string& def = "") +{ + auto it = mapArgs.find(key); + return (it != mapArgs.end()) ? it->second : def; +} + +static bool GetBoolArg(const string& key, bool def) +{ + auto it = mapArgs.find(key); + if (it == mapArgs.end()) return def; + string v = it->second; + if (v.empty()) return true; + return (v != "0" && v != "false" && v != "no"); +} + +static void ReadConfigFile(const string& path) +{ + ifstream f(path); + if (!f.good()) return; + string line; + while (getline(f, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + size_t start = line.find_first_not_of(" \t"); + if (start == string::npos) continue; + if (line[start] == '#') continue; + size_t eq = line.find('=', start); + if (eq == string::npos) continue; + string key = line.substr(start, eq - start); + string value = line.substr(eq + 1); + auto trim = [](string& s) { + size_t a = s.find_first_not_of(" \t"); + size_t b = s.find_last_not_of(" \t"); + if (a == string::npos) { s.clear(); return; } + s = s.substr(a, b - a + 1); + }; + trim(key); + trim(value); + if (value.size() >= 2 && + ((value.front() == '"' && value.back() == '"') || + (value.front() == '\'' && value.back() == '\''))) { + value = value.substr(1, value.size() - 2); + } + string dashKey = "-" + key; + if (mapArgs.count(dashKey) == 0) { + mapArgs[dashKey] = value; + mapMultiArgs[dashKey].push_back(value); + } + } +} + +static fs::path GetDefaultDataDir() +{ +#ifdef _WIN32 + const char* appdata = getenv("APPDATA"); + if (appdata && *appdata) { + return fs::path(appdata) / "CryptographicTriangles"; + } + return fs::path("C:/CryptographicTriangles"); +#elif defined(__APPLE__) + const char* home = getenv("HOME"); + if (home && *home) { + return fs::path(home) / "Library/Application Support/CryptographicTriangles"; + } + return fs::path("/tmp/CryptographicTriangles"); +#else + const char* home = getenv("HOME"); + if (home && *home) { + return fs::path(home) / ".cryptographic-triangles"; + } + return fs::path("/tmp/CryptographicTriangles"); +#endif +} + +static fs::path GetConfigFilePath() +{ + fs::path confPath = GetArg("-conf", "triangles.conf"); + if (confPath.is_absolute()) return confPath; + fs::path datadir = GetArg("-datadir", ""); + if (datadir.empty()) datadir = GetDefaultDataDir().string(); + return fs::path(datadir) / confPath; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Command-line parsing +// ───────────────────────────────────────────────────────────────────────────── + +static void ParseCommandLine(int argc, char* const argv[]) +{ + mapArgs.clear(); + mapMultiArgs.clear(); + for (int i = 1; i < argc; ++i) { + string str(argv[i]); + if (str == "-") { + mapMultiArgs["-"].push_back("-"); + continue; + } + string strKey, strVal; + size_t idx = str.find('='); + if (idx == string::npos) { + strKey = "-" + str; + strVal = "1"; + } else { + strKey = "-" + str.substr(0, idx); + strVal = str.substr(idx + 1); + } + mapArgs[strKey] = strVal; + mapMultiArgs[strKey].push_back(strVal); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// RPC connection parameters +// ───────────────────────────────────────────────────────────────────────────── + +struct RPCConn { + string host = "127.0.0.1"; + string port = "19111"; + string user; + string pass; +}; + +static int AppInitRPCConn(RPCConn& conn) +{ + fs::path confPath = GetConfigFilePath(); + if (!confPath.empty()) ReadConfigFile(confPath.string()); + + bool fTestNet = GetBoolArg("-testnet", false); + conn.port = GetArg("-rpcport", fTestNet ? "19112" : "19111"); + conn.host = GetArg("-rpcconnect", "127.0.0.1"); + conn.user = GetArg("-rpcuser", ""); + conn.pass = GetArg("-rpcpassword", ""); + + if (conn.user.empty() || conn.pass.empty()) { + cerr << "triangles-cli: missing RPC credentials. Set rpcuser/rpcpassword in triangles.conf\n" + << " or pass -rpcuser= -rpcpassword= on the command line.\n" + << " (RPC config file: " << confPath.string() << ")\n"; + return 1; + } + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// JSON-RPC param conversion +// ───────────────────────────────────────────────────────────────────────────── + +static Value ParseCLIParam(const string& arg) +{ + if (arg.empty()) { + return Value(string("")); + } + Value v; + if (read_string(arg, v) && v.type() != str_type) { + return v; + } + return Value(arg); +} + +static void ParseCommandLineRPCParams(int argc, char* const argv[], + Value& method, Array& params) +{ + method = Value(string("")); + params.clear(); + int i = 1; + static const set valFlags = { + "-conf", "-datadir", "-rpcconnect", "-rpcport", + "-rpcuser", "-rpcpassword" + }; + while (i < argc) { + string arg(argv[i]); + if (arg == "-" || arg.size() < 2 || arg[0] != '-') break; + if (valFlags.count(arg) && i + 1 < argc && + string(argv[i+1]).substr(0,1) != "-") { + i += 2; + } else { + ++i; + } + } + if (i >= argc) { + method = Value(string("help")); + return; + } + method = Value(string(argv[i])); + ++i; + while (i < argc) { + string arg(argv[i]); + if (arg == "-") { + string line; + while (getline(cin, line)) { + if (!line.empty() && line.back() == '\r') line.pop_back(); + params.push_back(ParseCLIParam(line)); + } + } else { + params.push_back(ParseCLIParam(arg)); + } + ++i; + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Base64 (RFC 4648) — for HTTP Basic auth +// ───────────────────────────────────────────────────────────────────────────── + +static const char b64_table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static string Base64Encode(const string& in) +{ + string out; + out.reserve(((in.size() + 2) / 3) * 4); + int val = 0, valb = -6; + for (unsigned char c : in) { + val = (val << 8) + c; + valb += 8; + while (valb >= 0) { + out.push_back(b64_table[(val >> valb) & 0x3F]); + valb -= 6; + } + } + if (valb > -6) out.push_back(b64_table[((val << 8) >> (valb + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +// ───────────────────────────────────────────────────────────────────────────── +// HTTP/1.1 JSON-RPC POST (plaintext) — using raw sockets (no Boost) +// ───────────────────────────────────────────────────────────────────────────── + +namespace { + +class SocketInit { +public: + SocketInit() { +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + } + ~SocketInit() { +#ifdef _WIN32 + WSACleanup(); +#endif + } +}; + +inline void close_socket(socket_t s) { + TRI_CLI_CLOSE_SOCKET(s); +} + +} // namespace + +static int CallRPC(const RPCConn& conn, const string& strMethod, + const Array& params, Value& result) +{ + SocketInit sockInit; + + Object req; + req.push_back(Pair("jsonrpc", Value(string("1.0")))); + req.push_back(Pair("id", Value(string("triangles-cli")))); + req.push_back(Pair("method", Value(strMethod))); + req.push_back(Pair("params", Value(params))); + string strRequest = write_string(Value(req), false) + "\n"; + + string strAuth = Base64Encode(conn.user + ":" + conn.pass); + + // Resolve host:port via getaddrinfo + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + struct addrinfo* addrRes = nullptr; + int rc = getaddrinfo(conn.host.c_str(), conn.port.c_str(), &hints, &addrRes); + if (rc != 0 || addrRes == nullptr) { + cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port + << " failed: " << gai_strerror(rc) << "\n"; + if (addrRes) freeaddrinfo(addrRes); + return 1; + } + + // Try each resolved address until one connects + socket_t sock = TRI_CLI_INVALID_SOCKET; + for (struct addrinfo* ai = addrRes; ai != nullptr; ai = ai->ai_next) { + sock = ::socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock == TRI_CLI_INVALID_SOCKET) { + continue; + } + if (::connect(sock, ai->ai_addr, ai->ai_addrlen) == 0) { + break; // connected + } + close_socket(sock); + sock = TRI_CLI_INVALID_SOCKET; + } + freeaddrinfo(addrRes); + if (sock == TRI_CLI_INVALID_SOCKET) { + cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port + << " failed\n" + << "(is trianglesd running and accepting JSON-RPC?)\n"; + return 1; + } + + // Build HTTP/1.1 request + string reqData = + "POST / HTTP/1.1\r\n" + "Host: " + conn.host + ":" + conn.port + "\r\n" + "Authorization: Basic " + strAuth + "\r\n" + "Content-Type: application/json\r\n" + "Content-Length: " + to_string(strRequest.size()) + "\r\n" + "Connection: close\r\n" + "\r\n" + strRequest; + + // Send + size_t totalSent = 0; + while (totalSent < reqData.size()) { + ssize_t n = ::send(sock, reqData.data() + totalSent, + reqData.size() - totalSent, 0); + if (n <= 0) { + cerr << "triangles-cli: write failed\n"; + close_socket(sock); + return 1; + } + totalSent += static_cast(n); + } + + // Read full response (until EOF) + string respData; + char buf[4096]; + while (true) { + ssize_t n = ::recv(sock, buf, sizeof(buf), 0); + if (n > 0) { + respData.append(buf, static_cast(n)); + } else if (n == 0) { + break; // EOF + } else { + // Error +#ifdef _WIN32 + int err = WSAGetLastError(); + if (err == WSAECONNRESET || err == WSAECONNABORTED) { + // Treat as EOF + break; + } +#else + if (errno == EINTR) continue; // interrupted, retry + if (errno == ECONNRESET) break; // peer closed +#endif + cerr << "triangles-cli: read failed\n"; + close_socket(sock); + return 1; + } + } + close_socket(sock); + + // Parse status line + size_t hdrEnd = respData.find("\r\n\r\n"); + if (hdrEnd == string::npos) { + cerr << "triangles-cli: malformed response (no header terminator)\n"; + return 1; + } + string statusLine = respData.substr(0, respData.find("\r\n")); + int status = 0; + { + istringstream iss(statusLine); + string httpVer; + iss >> httpVer >> status; + } + if (status != 200) { + cerr << "triangles-cli: server returned HTTP " << status << "\n"; + string body = respData.substr(hdrEnd + 4); + if (!body.empty()) cerr << body << "\n"; + return 1; + } + string body = respData.substr(hdrEnd + 4); + + Value reply; + if (!read_string(body, reply)) { + cerr << "triangles-cli: could not parse JSON response:\n" << body << "\n"; + return 1; + } + + if (reply.type() != obj_type) { + cerr << "triangles-cli: unexpected response (not an object):\n" + << write_string(reply, true) << "\n"; + return 1; + } + + Object replyObj = reply.get_obj(); + const Value& err = find_value(replyObj, "error"); + if (err.type() != null_type) { + cerr << "RPC error: " << write_string(err, false) << "\n"; + return 1; + } + const Value& res = find_value(replyObj, "result"); + result = res; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// triangles-cli -getinfo — synthesize a friendly summary from a few RPC calls +// ───────────────────────────────────────────────────────────────────────────── + +static int Getinfo(const RPCConn& conn, bool fPretty) +{ + Object info; + Value r; + Array emptyParams; + + if (CallRPC(conn, "getnetworkinfo", emptyParams, r) == 0) { + info.push_back(Pair("network", r)); + } + if (CallRPC(conn, "getblockchaininfo", emptyParams, r) == 0) { + Object chain = r.get_obj(); + info.push_back(Pair("blockchain", r)); + info.push_back(Pair("blocks", find_value(chain, "blocks"))); + info.push_back(Pair("headers", find_value(chain, "headers"))); + info.push_back(Pair("bestblockhash", find_value(chain, "bestblockhash"))); + info.push_back(Pair("difficulty", find_value(chain, "difficulty"))); + info.push_back(Pair("verificationprogress", + find_value(chain, "verificationprogress"))); + info.push_back(Pair("chain", find_value(chain, "chain"))); + } + if (CallRPC(conn, "getwalletinfo", emptyParams, r) == 0) { + Object wal = r.get_obj(); + info.push_back(Pair("wallet", r)); + info.push_back(Pair("balance", find_value(wal, "balance"))); + } + Object connObj; + connObj.push_back(Pair("rpcconnect", Value(conn.host))); + connObj.push_back(Pair("rpcport", Value(conn.port))); + info.push_back(Pair("connection", Value(connObj))); + cout << write_string(Value(info), fPretty) << "\n"; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// Help / version +// ───────────────────────────────────────────────────────────────────────────── + +static int CommandLineHelp(ostream& out) +{ + out << "Usage: triangles-cli [options] [params]\n" + << "\n" + << " triangles-cli [options] help List commands (delegates to daemon)\n" + << " triangles-cli [options] help Help for one command (delegates to daemon)\n" + << " triangles-cli -getinfo Show summary info from the daemon\n" + << "\n" + << "Options:\n" + << " -conf= Specify configuration file (default: triangles.conf)\n" + << " -datadir= Specify data directory\n" + << " -testnet Use testnet (RPC port 19112)\n" + << " -rpcconnect= Send commands to node running on (default: 127.0.0.1)\n" + << " -rpcport= Connect to JSON-RPC on (default: 19111 or testnet: 19112)\n" + << " -rpcuser= Username for JSON-RPC connections\n" + << " -rpcpassword= Password for JSON-RPC connections\n" + << " -stdin Read extra params from standard input, one per line\n" + << " -raw Print raw JSON response (no pretty-printing)\n" + << " -version Print version and exit\n" + << "\n" + << "Examples:\n" + << " triangles-cli getinfo\n" + << " triangles-cli getblockchaininfo\n" + << " triangles-cli getbalance\n" + << " triangles-cli getbalance \"*\" 6\n" + << " triangles-cli sendtoaddress
[comment]\n" + << " triangles-cli -getinfo\n" + << "\n"; + return 0; +} + +static int CommandLineVersion() +{ + cout << "triangles-cli version " << TRIANGLES_CLI_VERSION + << " (Cryptographic Triangles RPC client)\n"; + return 0; +} + +// ───────────────────────────────────────────────────────────────────────────── +// main +// ───────────────────────────────────────────────────────────────────────────── + +int main(int argc, char* argv[]) +{ + ParseCommandLine(argc, argv); + + if (argc < 2 || GetArg("-?", "") == "1" || GetArg("-h", "") == "1" || + GetArg("--help", "") == "1") { + CommandLineHelp(cerr); + return argc < 2 ? 1 : 0; + } + if (!GetArg("-version", "").empty() || !GetArg("--version", "").empty()) { + CommandLineVersion(); + return 0; + } + + RPCConn conn; + if (AppInitRPCConn(conn) != 0) return 1; + + Value method; + Array params; + ParseCommandLineRPCParams(argc, argv, method, params); + string strMethod = method.get_str(); + + if (strMethod == "help" || strMethod == "-help") { + if (params.empty()) { + CommandLineHelp(cout); + return 0; + } + } + + if (!GetArg("-getinfo", "").empty()) { + return Getinfo(conn, /*fPretty=*/true); + } + + bool fPretty = GetArg("-raw", "").empty(); + + Value result; + int nRet = CallRPC(conn, strMethod, params, result); + if (nRet == 0) { + cout << write_string(result, fPretty) << "\n"; + } + return nRet; +}