Add triangles-cli: JSON-RPC client (port bitcoin-cli pattern)

Triangles never had a CLI client (bitcoin-cli analog). This adds
triangles-cli as a third build target alongside trianglesd and
triangles-qt.

- src/triangles-cli.cpp: self-contained JSON-RPC 1.0 client.
  Reads triangles.conf for credentials, supports -rpcuser/-rpcpassword
  /-rpcconnect/-rpcport/-testnet/-datadir/-conf flags. Implements
  -getinfo (synthesized summary from getnetworkinfo/getblockchaininfo
  /getwalletinfo) and raw method dispatch. JSON via json_spirit compat
  shim (json_compat.h), HTTP via boost::asio, base64 auth inline.
  No util.cpp / wallet.cpp / net.cpp / triangles_common link dep —
  keeps the binary small (~600 KB Linux, ~1.5 MB Windows).

- CMake: new option(BUILD_CLI ON) + add_executable(triangles-cli)
  in src/CMakeLists.txt. Status line added.

- CI: BUILD_CLI=ON added to build-windows-daemon and build-linux-daemon
  jobs. triangles-cli.exe bundled into windows-daemon artifact
  alongside trianglesd.exe. triangles-cli added to linux-daemon .deb
  package (with launcher in /usr/bin).

- Default ON; set BUILD_CLI=OFF to skip.

Closes the open 'triangles-cli.exe missing from Windows build'
follow-up (the binary wasn't missing — it never existed).

Patterned after Bitcoin Core bitcoin-cli and Dash Core dash-cli.
This commit is contained in:
Krystie
2026-06-18 18:33:30 -07:00
parent c3e4a456d8
commit 8aeb5133bf
4 changed files with 622 additions and 11 deletions
+35 -11
View File
@@ -268,6 +268,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \ -DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \ -DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \ -DBUILD_TESTS=OFF \
-DUSE_UPNP=ON -DUSE_UPNP=ON
@@ -275,14 +276,19 @@ jobs:
run: | run: |
cmake --build build -j$(nproc) cmake --build build -j$(nproc)
strip --strip-all build/bin/trianglesd.exe strip --strip-all build/bin/trianglesd.exe
strip --strip-all build/bin/triangles-cli.exe
- name: Package daemon with DLLs - name: Package daemon with DLLs
run: | run: |
mkdir -p daemon-dist/tor mkdir -p daemon-dist/tor
cp build/bin/trianglesd.exe daemon-dist/ cp build/bin/trianglesd.exe daemon-dist/
cp build/bin/triangles-cli.exe daemon-dist/
# Copy all linked DLLs from MSYS2 # Copy all linked DLLs from MSYS2 (covers both binaries; ldd union)
ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}' | while read dll; do {
ldd build/bin/trianglesd.exe | grep '/mingw64' | awk '{print $3}'
ldd build/bin/triangles-cli.exe | grep '/mingw64' | awk '{print $3}'
} | sort -u | while read dll; do
cp "$dll" daemon-dist/ 2>/dev/null || true cp "$dll" daemon-dist/ 2>/dev/null || true
done done
@@ -454,6 +460,7 @@ jobs:
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
-DBUILD_QT=OFF \ -DBUILD_QT=OFF \
-DBUILD_DAEMON=ON \ -DBUILD_DAEMON=ON \
-DBUILD_CLI=ON \
-DBUILD_TESTS=OFF \ -DBUILD_TESTS=OFF \
-DUSE_UPNP=ON -DUSE_UPNP=ON
@@ -461,7 +468,9 @@ jobs:
run: cmake --build build -j$(nproc) run: cmake --build build -j$(nproc)
- name: Strip binary - 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) - name: Build .deb package (fully self-contained)
run: | run: |
@@ -477,12 +486,17 @@ jobs:
mkdir -p ${PKG}/etc/systemd/system mkdir -p ${PKG}/etc/systemd/system
cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/ cp build/bin/trianglesd ${PKG}/usr/lib/cryptographic-triangles/
cp build/bin/triangles-cli ${PKG}/usr/lib/cryptographic-triangles/
cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/ cp tor-extract/tor/tor ${PKG}/usr/lib/cryptographic-triangles/tor/
chmod +x ${PKG}/usr/lib/cryptographic-triangles/tor/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 [ -d tor-extract/data ] && cp -r tor-extract/data ${PKG}/usr/lib/cryptographic-triangles/tor/data
# Bundle ALL shared library dependencies (except glibc/kernel) # Bundle ALL shared library dependencies (except glibc/kernel)
ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}' | while read lib; do # Union of ldd output from both binaries
{
ldd build/bin/trianglesd | grep '=> /' | awk '{print $3}'
ldd build/bin/triangles-cli | grep '=> /' | awk '{print $3}'
} | sort -u | while read lib; do
case "$lib" in 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*) /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 ;; # Skip glibc core — always present
@@ -495,7 +509,7 @@ jobs:
ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l ls ${PKG}/usr/lib/cryptographic-triangles/lib/ | wc -l
ls ${PKG}/usr/lib/cryptographic-triangles/lib/ ls ${PKG}/usr/lib/cryptographic-triangles/lib/
# Launcher with LD_LIBRARY_PATH # Launchers with LD_LIBRARY_PATH
cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER' cat > ${PKG}/usr/bin/trianglesd << 'LAUNCHER'
#!/bin/bash #!/bin/bash
INSTALL_DIR=/usr/lib/cryptographic-triangles INSTALL_DIR=/usr/lib/cryptographic-triangles
@@ -505,6 +519,15 @@ jobs:
sed -i 's/^ //' ${PKG}/usr/bin/trianglesd sed -i 's/^ //' ${PKG}/usr/bin/trianglesd
chmod +x ${PKG}/usr/bin/trianglesd 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
sed -i 's/^ //' ${PKG}/usr/bin/triangles-cli
chmod +x ${PKG}/usr/bin/triangles-cli
cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC' cat > ${PKG}/etc/systemd/system/trianglesd.service << 'SVC'
[Unit] [Unit]
Description=Cryptographic Triangles Daemon Description=Cryptographic Triangles Daemon
@@ -528,9 +551,9 @@ jobs:
Version: ${VERSION} Version: ${VERSION}
Architecture: amd64 Architecture: amd64
Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org> Maintainer: Cryptographic Triangles <dev@cryptographic-triangles.org>
Description: Cryptographic Triangles daemon with integrated Tor Description: Cryptographic Triangles daemon + CLI with integrated Tor
Fully self-contained headless node with all libraries, Tor, and systemd service. Fully self-contained headless node + JSON-RPC client with all libraries,
No external dependencies required — runs on any x86_64 Linux. Tor, and systemd service. No external dependencies required.
Section: finance Section: finance
Priority: optional Priority: optional
CTRL CTRL
@@ -540,9 +563,10 @@ jobs:
#!/bin/bash #!/bin/bash
systemctl daemon-reload systemctl daemon-reload
echo "" echo ""
echo "Cryptographic Triangles daemon installed." echo "Cryptographic Triangles daemon + CLI installed."
echo " Start: sudo systemctl start trianglesd" echo " Start daemon: sudo systemctl start trianglesd"
echo " On boot: sudo systemctl enable trianglesd" echo " On boot: sudo systemctl enable trianglesd"
echo " Use CLI: triangles-cli getinfo"
echo "" echo ""
POST POST
chmod +x ${PKG}/DEBIAN/postinst chmod +x ${PKG}/DEBIAN/postinst
+2
View File
@@ -47,6 +47,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# ── User-facing options ── # ── User-facing options ──
option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON) option(BUILD_QT "Build triangles-qt (Qt5 GUI wallet)" ON)
option(BUILD_DAEMON "Build trianglesd (headless daemon)" 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(BUILD_TESTS "Build test_triangles (Boost.Test unit tests)" ON)
option(USE_UPNP "Enable UPnP support via miniupnpc" ON) option(USE_UPNP "Enable UPnP support via miniupnpc" ON)
option(USE_IPV6 "Enable IPv6 support" ON) option(USE_IPV6 "Enable IPv6 support" ON)
@@ -185,6 +186,7 @@ message(STATUS "")
message(STATUS "Triangles ${PROJECT_VERSION} build configuration:") message(STATUS "Triangles ${PROJECT_VERSION} build configuration:")
message(STATUS " Build Qt GUI: ${BUILD_QT}") message(STATUS " Build Qt GUI: ${BUILD_QT}")
message(STATUS " Build daemon: ${BUILD_DAEMON}") message(STATUS " Build daemon: ${BUILD_DAEMON}")
message(STATUS " Build CLI: ${BUILD_CLI}")
message(STATUS " Build tests: ${BUILD_TESTS}") message(STATUS " Build tests: ${BUILD_TESTS}")
message(STATUS " UPnP: ${USE_UPNP}") message(STATUS " UPnP: ${USE_UPNP}")
message(STATUS " IPv6: ${USE_IPV6}") message(STATUS " IPv6: ${USE_IPV6}")
+33
View File
@@ -247,6 +247,39 @@ if(BUILD_DAEMON)
endif() endif()
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
)
# Boost components used by the CLI
find_package(Boost REQUIRED COMPONENTS filesystem system)
target_link_libraries(triangles-cli
PRIVATE
json_compat
Boost::filesystem
Boost::system
)
if(WIN32)
set_target_properties(triangles-cli PROPERTIES SUFFIX ".exe")
# boost::asio needs ws2_32 on Windows
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) # 5. Qt5 GUI wallet (triangles-qt)
# ═══════════════════════════════════════════════════════════════════════════════ # ═══════════════════════════════════════════════════════════════════════════════
+552
View File
@@ -0,0 +1,552 @@
// 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), boost (asio +
// program_options + filesystem + system), and OpenSSL (for base64).
// This keeps the CLI binary small (~600 KB stripped on Linux, ~1.5 MB on Windows).
//
// Connection parameters (highest precedence first):
// 1. Command line flags: -rpcuser/-rpcpassword/-rpcconnect/-rpcport
// 2. triangles.conf in the data directory (or -conf=<path>)
// 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 <command> 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 <addr> 1.5 "memo" Example: mixed types
// triangles-cli -getinfo Synthesized summary from multiple RPCs
// triangles-cli -raw <method> <args...> 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 <boost/asio.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iterator>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
using namespace std;
namespace asio = boost::asio;
using boost::asio::ip::tcp;
namespace fs = boost::filesystem;
using namespace json_spirit;
// ─────────────────────────────────────────────────────────────────────────────
// Minimal arg/config plumbing — self-contained, no util.cpp dep.
// ─────────────────────────────────────────────────────────────────────────────
static map<string, string> mapArgs;
static map<string, vector<string> > 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)) {
// Strip CR (Windows) and leading whitespace
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;
// Parse key = value
size_t eq = line.find('=', start);
if (eq == string::npos) continue;
string key = line.substr(start, eq - start);
string value = line.substr(eq + 1);
// Trim whitespace on both ends
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);
// Strip surrounding quotes
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);
}
}
}
// Cross-platform default data directory (matches the daemon's path)
static fs::path GetDefaultDataDir()
{
#ifdef WIN32
// %APPDATA%/CryptographicTriangles
const char* appdata = getenv("APPDATA");
if (appdata && *appdata) {
return fs::path(appdata) / "CryptographicTriangles";
}
return fs::path("C:/CryptographicTriangles");
#elif defined(__APPLE__)
// ~/Library/Application Support/CryptographicTriangles
const char* home = getenv("HOME");
if (home && *home) {
return fs::path(home) / "Library/Application Support/CryptographicTriangles";
}
return fs::path("/tmp/CryptographicTriangles");
#else
// ~/.cryptographic-triangles (matches daemon's GetDefaultDataDir)
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]);
// Bare "-" means: read remaining args from stdin
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)
{
// Load conf file FIRST (before pulling creds) so defaults from triangles.conf
// are visible. Command-line flags (already in mapArgs) take precedence because
// ReadConfigFile only inserts when key is absent.
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=<user> -rpcpassword=<pw> 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;
// Try parsing the arg as JSON. If it parses to a non-string literal, keep.
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;
// Skip leading flags
static const set<string> 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)
// ─────────────────────────────────────────────────────────────────────────────
static int CallRPC(const RPCConn& conn, const string& strMethod,
const Array& params, Value& result)
{
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);
asio::io_context io;
tcp::resolver resolver(io);
boost::system::error_code ec;
auto endpoints = resolver.resolve(conn.host, conn.port, ec);
if (ec) {
cerr << "triangles-cli: resolve " << conn.host << ":" << conn.port
<< " failed: " << ec.message() << "\n";
return 1;
}
tcp::socket sock(io);
sock.connect(*endpoints.begin(), ec);
if (ec) {
cerr << "triangles-cli: connect to " << conn.host << ":" << conn.port
<< " failed: " << ec.message() << "\n"
<< "(is trianglesd running and accepting JSON-RPC?)\n";
return 1;
}
ostringstream reqStream;
reqStream << "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: " << strRequest.size() << "\r\n"
<< "Connection: close\r\n"
<< "\r\n"
<< strRequest;
asio::streambuf requestBuf;
std::ostream os(&requestBuf);
os << reqStream.str();
asio::write(sock, requestBuf, ec);
if (ec) {
cerr << "triangles-cli: write failed: " << ec.message() << "\n";
return 1;
}
asio::streambuf responseBuf;
boost::system::error_code readEc;
while (asio::read(sock, responseBuf,
asio::transfer_at_least(1), readEc)) {
// keep reading until EOF or error
}
if (readEc && readEc != asio::error::eof) {
cerr << "triangles-cli: read failed: " << readEc.message() << "\n";
return 1;
}
std::istream rs(&responseBuf);
string line;
if (!std::getline(rs, line)) {
cerr << "triangles-cli: empty response\n";
return 1;
}
if (!line.empty() && line.back() == '\r') line.pop_back();
int status = 0;
{
istringstream iss(line);
string httpVer;
iss >> httpVer >> status;
}
if (status != 200) {
cerr << "triangles-cli: server returned HTTP " << status << "\n";
ostringstream body;
body << rs.rdbuf();
if (!body.str().empty()) cerr << body.str() << "\n";
return 1;
}
while (std::getline(rs, line) && line != "\r" && !line.empty()) {}
string body;
{
ostringstream oss;
oss << rs.rdbuf();
body = oss.str();
}
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] <command> [params]\n"
<< "\n"
<< " triangles-cli [options] help List commands (delegates to daemon)\n"
<< " triangles-cli [options] help <command> Help for one command (delegates to daemon)\n"
<< " triangles-cli -getinfo Show summary info from the daemon\n"
<< "\n"
<< "Options:\n"
<< " -conf=<file> Specify configuration file (default: triangles.conf)\n"
<< " -datadir=<dir> Specify data directory\n"
<< " -testnet Use testnet (RPC port 19112)\n"
<< " -rpcconnect=<ip> Send commands to node running on <ip> (default: 127.0.0.1)\n"
<< " -rpcport=<port> Connect to JSON-RPC on <port> (default: 19111 or testnet: 19112)\n"
<< " -rpcuser=<user> Username for JSON-RPC connections\n"
<< " -rpcpassword=<pw> 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 <address> <amount> [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;
}
// else fall through: delegate to daemon's help <command>
}
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;
}