From 7107e4fde0c64989ecffe226a14f86ed9b2f93fc Mon Sep 17 00:00:00 2001 From: xenon <132082796+Officialxenoeth@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:57:53 +0100 Subject: [PATCH 01/13] Add CMake workflow for single platform builds This workflow configures, builds, and tests a CMake project on a single platform using GitHub Actions. --- .github/workflows/cmake-single-platform.yml | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/cmake-single-platform.yml diff --git a/.github/workflows/cmake-single-platform.yml b/.github/workflows/cmake-single-platform.yml new file mode 100644 index 0000000..0ac33d9 --- /dev/null +++ b/.github/workflows/cmake-single-platform.yml @@ -0,0 +1,39 @@ +# This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml +name: CMake on a single platform + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) + BUILD_TYPE: Release + +jobs: + build: + # The CMake configure and build commands are platform agnostic and should work equally well on Windows or Mac. + # You can convert this to a matrix build if you need cross-platform coverage. + # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Configure CMake + # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. + # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type + run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} + + - name: Build + # Build your program with the given configuration + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Test + working-directory: ${{github.workspace}}/build + # Execute tests defined by the CMake configuration. + # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail + run: ctest -C ${{env.BUILD_TYPE}} + From a1e37774bc5ec74ffc9c590b0a6dea9432aa89a1 Mon Sep 17 00:00:00 2001 From: xcvoierg Date: Thu, 16 Apr 2026 09:56:13 -0700 Subject: [PATCH 02/13] plz work --- main.cpp | 611 +++++++++++++++++++++++++++++++++---------------- rpc_client.hpp | 281 ++++++++++++++++++++--- 2 files changed, 661 insertions(+), 231 deletions(-) diff --git a/main.cpp b/main.cpp index 37003a8..ac3fd81 100644 --- a/main.cpp +++ b/main.cpp @@ -76,6 +76,13 @@ static std::atomic g_wallet_loaded{false}; static std::string g_wallet_path = "data/wallet.oct"; static std::string g_pin; static TxCache g_txcache; +static json g_fee_cache; +static double g_fee_cache_ts = 0.0; +static std::mutex g_fee_mtx; +static json g_token_cache; +static double g_token_cache_ts = 0.0; +static std::string g_token_cache_addr; +static std::mutex g_token_mtx; static void handle_signal(int) { octra::secure_zero(g_wallet.sk, 64); @@ -264,33 +271,36 @@ struct EncBalResult { int64_t decrypted; }; -static EncBalResult get_encrypted_balance() { +static EncBalResult get_encrypted_balance(octra::OpTimer* t = nullptr) { std::string sig = octra::sign_balance_request(g_wallet.addr, g_wallet.sk); + if (t) t->step("encbal_sign_request"); auto r = g_rpc.get_encrypted_balance(g_wallet.addr, sig, g_wallet.pub_b64); + if (t) t->reset_step(); if (!r.ok || !r.result.is_object()) return {"0", 0}; std::string cipher = r.result.value("cipher", "0"); if (!g_pvac_ok || cipher.empty() || cipher == "0") return {cipher, 0}; int64_t dec = g_pvac.get_balance(cipher); + if (t) t->step("encbal_local_get_balance"); return {cipher, dec}; } static void init_wallet_subsystems() { g_rpc.set_url(g_wallet.rpc_url); ensure_pubkey_registered(g_wallet.addr, g_wallet.sk, g_wallet.pub_b64); - g_pvac_ok = g_pvac.init(g_wallet.priv_b64); - if (g_pvac_ok) { - fprintf(stderr, "pvac initialized\n"); - ensure_pvac_registered(); - } else { - fprintf(stderr, "pvac init failed (libpvac not loaded?)\n"); + { + octra::ScopedTimer t("pvac.init"); + g_pvac_ok = g_pvac.init(g_wallet.priv_b64); + if (g_pvac_ok) { + ensure_pvac_registered(); + } } g_txcache.close(); std::string cache_path = "data/txcache_" + g_wallet.addr.substr(3, 8); - if (g_txcache.open(cache_path)) { - fprintf(stderr, "txcache opened: %s\n", cache_path.c_str()); - g_txcache.ensure_rpc(g_wallet.rpc_url); - } else { - fprintf(stderr, "txcache open failed: %s\n", cache_path.c_str()); + { + octra::ScopedTimer t("txcache.open"); + if (g_txcache.open(cache_path)) { + g_txcache.ensure_rpc(g_wallet.rpc_url); + } } g_wallet_loaded = true; } @@ -321,6 +331,13 @@ int main(int argc, char** argv) { handle_signal(0); return TRUE; }, TRUE); + HANDLE hOut = GetStdHandle(STD_ERROR_HANDLE); + if (hOut != INVALID_HANDLE_VALUE) { + DWORD mode = 0; + if (GetConsoleMode(hOut, &mode)) { + SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); + } + } #else struct rlimit rl = {0, 0}; setrlimit(RLIMIT_CORE, &rl); @@ -340,12 +357,8 @@ int main(int argc, char** argv) { httplib::Server svr; svr.set_read_timeout(300, 0); svr.set_write_timeout(300, 0); - - - // svr.set_keep_alive_timeout(5); svr.set_keep_alive_max_count(100); - // svr.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { res.set_header("X-Frame-Options", "DENY"); @@ -440,13 +453,13 @@ int main(int argc, char** argv) { bool has_leg = octra::has_legacy_wallet(); bool has_enc = octra::has_encrypted_wallet(); if (has_leg && !has_enc && addr_hint.empty()) { + octra::ScopedTimer t("wallet.migrate"); g_wallet = octra::migrate_wallet(pin); g_wallet_path = octra::WALLET_FILE; - fprintf(stderr, "wallet migrated: %s\n", g_wallet.addr.c_str()); } else { + octra::ScopedTimer t("wallet.unlock"); g_wallet = octra::load_wallet_encrypted(unlock_path, pin); g_wallet_path = unlock_path; - fprintf(stderr, "wallet unlocked: %s\n", g_wallet.addr.c_str()); } try { @@ -477,6 +490,9 @@ int main(int argc, char** argv) { }); svr.Post("/api/wallet/lock", [](const httplib::Request&, httplib::Response& res) { + auto logout_t0 = std::chrono::steady_clock::now(); + { char tw[16]; octra::get_wall_hms(tw, sizeof(tw)); + fprintf(stderr, "[%s] [logout] starting logout (0.000 ms)\n", tw); } std::lock_guard lock(g_mtx); if (!g_wallet_loaded) { res.status = 409; @@ -498,7 +514,12 @@ int main(int argc, char** argv) { g_wallet.priv_b64.clear(); g_wallet.pub_b64.clear(); g_wallet.addr.clear(); - fprintf(stderr, "wallet locked\n"); + { double ms = std::chrono::duration( + std::chrono::steady_clock::now() - logout_t0).count(); + char tw[16]; octra::get_wall_hms(tw, sizeof(tw)); + const char* esc; const char* rst; + octra::timing_ms_esc(ms, &esc, &rst); + fprintf(stderr, "[%s] [logout] wallet locked %s(%.3f ms)%s\n", tw, esc, ms, rst); } json j; j["ok"] = true; res.set_content(j.dump(), "application/json"); @@ -663,13 +684,16 @@ int main(int argc, char** argv) { svr.Get("/api/wallet", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD json j; - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["rpc_url"] = g_wallet.rpc_url; - j["explorer_url"] = g_wallet.explorer_url; - j["has_master_seed"] = g_wallet.has_master_seed(); - j["hd_index"] = g_wallet.hd_index; - j["hd_version"] = g_wallet.hd_version; + { + std::lock_guard lock(g_mtx); + j["address"] = g_wallet.addr; + j["public_key"] = g_wallet.pub_b64; + j["rpc_url"] = g_wallet.rpc_url; + j["explorer_url"] = g_wallet.explorer_url; + j["has_master_seed"] = g_wallet.has_master_seed(); + j["hd_index"] = g_wallet.hd_index; + j["hd_version"] = g_wallet.hd_version; + } res.set_content(j.dump(), "application/json"); }); @@ -875,11 +899,6 @@ int main(int argc, char** argv) { bool pvac_ok; { std::lock_guard lock(g_mtx); - if (!g_wallet_loaded) { - res.status = 503; - res.set_content(err_json("no wallet loaded").dump(), "application/json"); - return; - } addr = g_wallet.addr; pub_b64 = g_wallet.pub_b64; sig_bal = octra::sign_balance_request(addr, g_wallet.sk); @@ -1014,19 +1033,47 @@ int main(int argc, char** argv) { }); svr.Get("/api/fee", [](const httplib::Request&, httplib::Response& res) { - json fees; - std::vector ops = {"standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call"}; + double now = (double)time(nullptr); + { + std::lock_guard lock(g_fee_mtx); + if (!g_fee_cache.empty() && (now - g_fee_cache_ts) < 30.0) { + res.set_content(g_fee_cache.dump(), "application/json"); + return; + } + } + + static const std::vector ops = { + "standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call" + }; + + std::vector params; + params.reserve(ops.size()); for (auto& op : ops) { - auto r = g_rpc.call("octra_recommendedFee", nlohmann::json::array({op}), 5); - if (r.ok) fees[op] = r.result; - else fees[op] = {{"minimum", "1000"}, {"recommended", "1000"}, {"fast", "2000"}}; + params.push_back(nlohmann::json::array({op})); + } + + auto results = g_rpc.call_batch(ops, params, 10); + + json fees; + for (size_t i = 0; i < ops.size(); ++i) { + if (i < results.size() && results[i].ok) { + fees[ops[i]] = results[i].result; + } else { + fees[ops[i]] = {{"minimum", "1000"}, {"recommended", "1000"}, {"fast", "2000"}}; + } + } + + { + std::lock_guard lock(g_fee_mtx); + g_fee_cache = fees; + g_fee_cache_ts = now; } res.set_content(fees.dump(), "application/json"); }); svr.Post("/api/send", [](const httplib::Request& req, httplib::Response& res) { + octra::OpTimer t("send", "standard send started"); WALLET_GUARD - std::lock_guard lock(g_mtx); json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1045,24 +1092,30 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } - auto bi = get_nonce_balance(); int nonce = bi.nonce; + std::lock_guard lock(g_mtx); + t.mutex_acquired(); + auto bi = get_nonce_balance(); + t.step("get_nonce_balance"); octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = to; tx.amount = std::to_string(raw); - tx.nonce = nonce + 1; + tx.nonce = bi.nonce + 1; tx.ou = parse_ou(body, (raw < 1000000000) ? "10000" : "30000"); tx.timestamp = now_ts(); tx.op_type = "standard"; std::string msg = body.value("message", ""); if (!msg.empty()) tx.message = msg; sign_tx_fields(tx); + t.step("sign_tx"); auto result = submit_tx(tx); + t.step("submit_tx"); if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/key_switch", [](const httplib::Request& req, httplib::Response& res) { + octra::ScopedTimer timer("key_switch"); WALLET_GUARD std::lock_guard lock(g_mtx); auto nb = get_nonce_balance(); @@ -1099,7 +1152,10 @@ int main(int argc, char** argv) { for (int i = 0; i < 8; i++) snprintf(hex + i*2, 3, "%02x", old_hash[i]); tx.message = "encryption key switch | new_key:" + std::string(hex); - sign_tx_fields(tx); + { + octra::ScopedTimer s("key_switch.sign"); + sign_tx_fields(tx); + } auto result = submit_tx(tx); if (result.contains("error")) { res.status = 500; @@ -1111,9 +1167,8 @@ int main(int argc, char** argv) { }); svr.Post("/api/encrypt", [](const httplib::Request& req, httplib::Response& res) { + octra::OpTimer t("encrypt", "encryption started"); WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1126,47 +1181,55 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } + std::lock_guard lock(g_mtx); + t.mutex_acquired(); + PVAC_GUARD ensure_pvac_registered(); - uint8_t seed[32]; + t.step("ensure_pvac_registered"); + uint8_t seed[32], blinding[32]; octra::random_bytes(seed, 32); + octra::random_bytes(blinding, 32); pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); + t.step("pvac_encrypt"); std::string cipher_str = g_pvac.encode_cipher(ct); - - uint8_t blinding[32]; - octra::random_bytes(blinding, 32); + t.step("encode_cipher"); auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); + t.step("pedersen_commit+encode"); pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); + t.step("zero_proof_make"); std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_zero_proof(zkp); g_pvac.free_cipher(ct); - + t.step("zero_proof_encode+free"); json enc_data; enc_data["cipher"] = cipher_str; enc_data["amount_commitment"] = amt_commit_b64; enc_data["zero_proof"] = zp_str; enc_data["blinding"] = octra::base64_encode(blinding, 32); - - auto bi = get_nonce_balance(); int nonce = bi.nonce; + auto bi = get_nonce_balance(); + t.step("get_nonce_balance"); octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = g_wallet.addr; tx.amount = std::to_string(raw); - tx.nonce = nonce + 1; + tx.nonce = bi.nonce + 1; tx.ou = parse_ou(body, "10000"); tx.timestamp = now_ts(); tx.op_type = "encrypt"; tx.encrypted_data = enc_data.dump(); + t.step("build_tx+json_dump"); sign_tx_fields(tx); + t.step("sign_tx"); auto result = submit_tx(tx); + t.step("submit_tx"); if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/decrypt", [](const httplib::Request& req, httplib::Response& res) { + octra::OpTimer t("decrypt", "decryption started"); WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1179,7 +1242,10 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } - auto eb = get_encrypted_balance(); + std::lock_guard lock(g_mtx); + t.mutex_acquired(); + PVAC_GUARD + auto eb = get_encrypted_balance(&t); if (eb.decrypted < raw) { res.status = 400; char buf[128]; @@ -1189,40 +1255,45 @@ int main(int argc, char** argv) { return; } ensure_pvac_registered(); + t.step("ensure_pvac_registered"); json steps = json::array(); steps.push_back("[1/5] FHE encrypt amount (PVAC-HFHE)"); - uint8_t seed[32]; + uint8_t seed[32], blinding[32]; octra::random_bytes(seed, 32); pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); + t.step("pvac_encrypt"); std::string cipher_str = g_pvac.encode_cipher(ct); + t.step("encode_cipher"); steps.push_back("[2/5] bound zero proof"); - - uint8_t blinding[32]; octra::random_bytes(blinding, 32); auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); + t.step("pedersen_commit+encode"); pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); + t.step("zero_proof_make"); std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_zero_proof(zkp); + t.step("zero_proof_encode"); steps.push_back("[3/5] range proof"); - pvac_cipher current_ct = g_pvac.decode_cipher(eb.cipher); + t.step("decode_cipher"); pvac_cipher new_bal_ct = pvac_ct_sub(g_pvac.pk(), current_ct, ct); + t.step("ct_sub"); uint64_t new_bal_value = (uint64_t)(eb.decrypted - raw); - pvac_agg_range_proof arp = pvac_make_aggregated_range_proof( - g_pvac.pk(), g_pvac.sk(), new_bal_ct, new_bal_value); + pvac_agg_range_proof arp = pvac_make_aggregated_range_proof(g_pvac.pk(), g_pvac.sk(), new_bal_ct, new_bal_value); + t.step("make_aggregated_range_proof"); size_t arp_len = 0; uint8_t* arp_data = pvac_serialize_agg_range_proof(arp, &arp_len); - std::string rp_bal_str = std::string("rp_v1|") + - octra::base64_encode(arp_data, arp_len); + std::string rp_bal_str = std::string("rp_v1|") + octra::base64_encode(arp_data, arp_len); pvac_free_bytes(arp_data); pvac_free_agg_range_proof(arp); pvac_free_cipher(new_bal_ct); pvac_free_cipher(current_ct); g_pvac.free_cipher(ct); + t.step("serialize_agg_rp+base64+free_ciphers"); json enc_data; enc_data["cipher"] = cipher_str; @@ -1232,31 +1303,32 @@ int main(int argc, char** argv) { enc_data["range_proof_balance"] = rp_bal_str; steps.push_back("[4/5] building decrypt transaction"); - - auto bi = get_nonce_balance(); int nonce = bi.nonce; + auto bi = get_nonce_balance(); + t.step("get_nonce_balance"); octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = g_wallet.addr; tx.amount = std::to_string(raw); - tx.nonce = nonce + 1; + tx.nonce = bi.nonce + 1; tx.ou = parse_ou(body, "10000"); tx.timestamp = now_ts(); tx.op_type = "decrypt"; tx.encrypted_data = enc_data.dump(); + t.step("build_tx+json_dump"); sign_tx_fields(tx); + t.step("sign_tx"); auto result = submit_tx(tx); + t.step("submit_tx"); steps.push_back("[5/5] submitted to node"); result["steps"] = steps; - if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/stealth/send", [](const httplib::Request& req, httplib::Response& res) { + octra::OpTimer t("stealth", "stealth send started"); WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1270,135 +1342,218 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid params").dump(), "application/json"); return; } - - auto vr = g_rpc.get_view_pubkey(to); - if (!vr.ok || !vr.result.is_object() || !vr.result.contains("view_pubkey") - || vr.result["view_pubkey"].is_null() || !vr.result["view_pubkey"].is_string()) { - res.status = 400; - res.set_content(err_json("recipient has no view pubkey - they must register pvac first").dump(), "application/json"); - return; - } - std::string their_vpub_b64 = vr.result["view_pubkey"].get(); - auto their_vpub_raw = octra::base64_decode(their_vpub_b64); - if (their_vpub_raw.size() != 32) { - res.status = 400; - res.set_content(err_json("invalid view pubkey").dump(), "application/json"); - return; + std::string from_addr, from_pub_b64, eb_sig; + { + std::lock_guard lock(g_mtx); + from_addr = g_wallet.addr; + from_pub_b64 = g_wallet.pub_b64; + eb_sig = octra::sign_balance_request(from_addr, g_wallet.sk); } + t.step("snapshot_wallet+sign_balance"); + json steps = json::array(); + steps.push_back("[1/8] ECDH x25519 key exchange"); try { + auto vr = g_rpc.get_view_pubkey(to); + t.reset_step(); + if (!vr.ok || !vr.result.is_object() || !vr.result.contains("view_pubkey") + || vr.result["view_pubkey"].is_null() || !vr.result["view_pubkey"].is_string()) { + res.status = 400; + res.set_content(err_json("recipient has no view pubkey - they must register pvac first").dump(), "application/json"); + return; + } + std::vector their_vpub_raw = octra::base64_decode(vr.result["view_pubkey"].get()); + if (their_vpub_raw.size() != 32) { + res.status = 400; + res.set_content(err_json("invalid view pubkey").dump(), "application/json"); + return; + } - json steps = json::array(); + uint8_t eph_sk[32], eph_pk[32]; + octra::random_bytes(eph_sk, 32); + crypto_scalarmult_base(eph_pk, eph_sk); + t.step("eph_keygen"); - steps.push_back("[1/8] ECDH x25519 key exchange"); - uint8_t eph_sk[32], eph_pk[32]; - octra::random_bytes(eph_sk, 32); - crypto_scalarmult_base(eph_pk, eph_sk); - auto shared = octra::ecdh_shared_secret(eph_sk, their_vpub_raw.data()); - - steps.push_back("[2/8] stealth tag + claim key derivation"); - auto stag = octra::compute_stealth_tag(shared); - auto claim_sec = octra::compute_claim_secret(shared); - auto claim_pub = octra::compute_claim_pub(claim_sec, to); - - steps.push_back("[3/7] checking encrypted balance"); - auto eb = get_encrypted_balance(); - if (eb.decrypted < raw) { - res.status = 400; - char buf[128]; - snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", - (long)eb.decrypted, (long)raw); - res.set_content(err_json(buf).dump(), "application/json"); - return; - } + auto shared = octra::ecdh_shared_secret(eph_sk, their_vpub_raw.data()); + t.step("ecdh"); - steps.push_back("[4/7] FHE encrypt delta (PVAC-HFHE)"); - ensure_pvac_registered(); - uint8_t r_blind[32]; - octra::random_bytes(r_blind, 32); - std::string enc_amount = octra::encrypt_stealth_amount(shared, (uint64_t)raw, r_blind); - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct_delta = g_pvac.encrypt((uint64_t)raw, seed); - std::string delta_cipher_str = g_pvac.encode_cipher(ct_delta); - auto commitment = g_pvac.commit_ct(ct_delta); - std::string commitment_b64 = octra::base64_encode(commitment.data(), 32); + steps.push_back("[2/8] stealth tag + claim key derivation"); + auto stag = octra::compute_stealth_tag(shared); + std::string stag_hex = octra::hex_encode(stag.data(), 16); + t.step("compute_stealth_tag"); - steps.push_back("[5/7] range proofs (parallel) - Bulletproofs R1CS"); - pvac_cipher current_ct = g_pvac.decode_cipher(eb.cipher); - pvac_cipher new_ct = g_pvac.ct_sub(current_ct, ct_delta); - uint64_t new_val = (uint64_t)(eb.decrypted - raw); - - pvac_range_proof rp_delta = nullptr; - pvac_range_proof rp_bal = nullptr; - - std::thread t_rp_delta([&]() { - rp_delta = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), ct_delta, (uint64_t)raw); - }); - std::thread t_rp_bal([&]() { - rp_bal = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), new_ct, new_val); - }); - t_rp_delta.join(); - t_rp_bal.join(); - - steps.push_back("[6/7] encoding proofs"); - std::string rp_delta_str = g_pvac.encode_range_proof(rp_delta); - std::string rp_bal_str = g_pvac.encode_range_proof(rp_bal); - g_pvac.free_range_proof(rp_delta); - g_pvac.free_range_proof(rp_bal); - g_pvac.free_cipher(ct_delta); - g_pvac.free_cipher(current_ct); - g_pvac.free_cipher(new_ct); - - steps.push_back("[7/8] Pedersen commitment + AES-GCM envelope"); - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_blind); - std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); + auto claim_sec = octra::compute_claim_secret(shared); + t.step("compute_claim_secret"); - steps.push_back("[8/8] building stealth transaction"); - json stealth_data; - stealth_data["version"] = 5; - stealth_data["delta_cipher"] = delta_cipher_str; - stealth_data["commitment"] = commitment_b64; - stealth_data["range_proof_delta"] = rp_delta_str; - stealth_data["range_proof_balance"] = rp_bal_str; - stealth_data["eph_pub"] = octra::base64_encode(eph_pk, 32); - stealth_data["stealth_tag"] = octra::hex_encode(stag.data(), 16); - stealth_data["enc_amount"] = enc_amount; - stealth_data["claim_pub"] = octra::hex_encode(claim_pub.data(), 32); - stealth_data["amount_commitment"] = amt_commit_b64; + auto claim_pub = octra::compute_claim_pub(claim_sec, to); + std::string claim_pub_hex = octra::hex_encode(claim_pub.data(), 32); + t.step("compute_claim_pub"); - auto bi = get_nonce_balance(); int nonce = bi.nonce; - octra::Transaction tx; - tx.from = g_wallet.addr; - tx.to_ = "stealth"; - tx.amount = "0"; - tx.nonce = nonce + 1; - tx.ou = parse_ou(body, "5000"); - tx.timestamp = now_ts(); - tx.op_type = "stealth"; - tx.encrypted_data = stealth_data.dump(); - sign_tx_fields(tx); - auto result = submit_tx(tx); - if (result.contains("error")) res.status = 500; - result["steps"] = steps; - res.set_content(result.dump(), "application/json"); + steps.push_back("[3/8] checking encrypted balance"); + auto eb_r = g_rpc.get_encrypted_balance(from_addr, eb_sig, from_pub_b64); + t.reset_step(); + if (!eb_r.ok || !eb_r.result.is_object()) { + res.status = 500; + res.set_content(err_json("failed to fetch encrypted balance").dump(), "application/json"); + return; + } + std::string eb_cipher = eb_r.result.value("cipher", "0"); + if (eb_cipher.empty() || eb_cipher == "0") { + res.status = 400; + res.set_content(err_json("no encrypted balance available").dump(), "application/json"); + return; + } + + std::lock_guard lock(g_mtx); + t.mutex_acquired(); + PVAC_GUARD + + int64_t eb_decrypted = g_pvac.get_balance(eb_cipher); + t.step("encbal_local_get_balance"); + if (eb_decrypted < raw) { + char buf[128]; + snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", + (long)eb_decrypted, (long)raw); + res.status = 400; + res.set_content(err_json(buf).dump(), "application/json"); + return; + } + + steps.push_back("[4/8] FHE encrypt delta (PVAC-HFHE)"); + ensure_pvac_registered(); + t.step("ensure_pvac_registered"); + uint8_t r_blind[32]; + octra::random_bytes(r_blind, 32); + std::string enc_amount = octra::encrypt_stealth_amount(shared, (uint64_t)raw, r_blind); + t.step("stealth_aes_envelope"); + + uint8_t seed[32]; + octra::random_bytes(seed, 32); + pvac_cipher ct_delta = g_pvac.encrypt((uint64_t)raw, seed); + t.step("pvac_encrypt_delta"); + + std::string delta_cipher_str = g_pvac.encode_cipher(ct_delta); + t.step("encode_delta_cipher"); + + auto commitment = g_pvac.commit_ct(ct_delta); + std::string commitment_b64 = octra::base64_encode(commitment.data(), 32); + t.step("commit_ct_delta+encode"); + + pvac_cipher current_ct = g_pvac.decode_cipher(eb_cipher); + t.step("decode_cipher"); + + pvac_cipher new_ct = g_pvac.ct_sub(current_ct, ct_delta); + t.step("ct_sub"); + + uint64_t new_val = (uint64_t)(eb_decrypted - raw); + pvac_range_proof rp_delta_proof = nullptr; + pvac_range_proof rp_bal_proof = nullptr; + { + auto rp_start = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point delta_done{}, bal_done{}; + std::thread thr_delta([&]() { + rp_delta_proof = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), ct_delta, (uint64_t)raw); + delta_done = std::chrono::steady_clock::now(); + }); + std::thread thr_bal([&]() { + rp_bal_proof = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), new_ct, new_val); + bal_done = std::chrono::steady_clock::now(); + }); + thr_delta.join(); + thr_bal.join(); + auto rp_end = std::chrono::steady_clock::now(); + double wall_ms = std::chrono::duration(rp_end - rp_start).count(); + double td_ms = std::chrono::duration(delta_done - rp_start).count(); + double tb_ms = std::chrono::duration(bal_done - rp_start).count(); + char rp_buf[256]; + snprintf(rp_buf, sizeof(rp_buf), + "range_proofs parallel_wall=%.3f ms thread_delta=%.3f ms thread_balance=%.3f ms", + wall_ms, td_ms, tb_ms); + t.step_msg(rp_buf); + } + + steps.push_back("[5/8] range proofs (parallel) - Bulletproofs R1CS"); + + std::string rp_delta_str = g_pvac.encode_range_proof(rp_delta_proof); + t.step("encode_range_proof_delta"); + + std::string rp_bal_str = g_pvac.encode_range_proof(rp_bal_proof); + t.step("encode_range_proof_balance"); + + g_pvac.free_range_proof(rp_delta_proof); + g_pvac.free_range_proof(rp_bal_proof); + g_pvac.free_cipher(current_ct); + g_pvac.free_cipher(new_ct); + g_pvac.free_cipher(ct_delta); + t.step("free_range_proofs+ciphers"); + + steps.push_back("[6/8] encoding proofs"); + + uint8_t r_amt[32]; + octra::random_bytes(r_amt, 32); + auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_amt); + std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); + t.step("pedersen_amount_commitment"); + + steps.push_back("[7/8] Pedersen commitment + AES-GCM envelope"); + + json stealth_data; + stealth_data["version"] = 5; + stealth_data["delta_cipher"] = delta_cipher_str; + stealth_data["commitment"] = commitment_b64; + stealth_data["range_proof_delta"] = rp_delta_str; + stealth_data["range_proof_balance"] = rp_bal_str; + stealth_data["eph_pub"] = octra::base64_encode(eph_pk, 32); + stealth_data["stealth_tag"] = stag_hex; + stealth_data["enc_amount"] = enc_amount; + stealth_data["claim_pub"] = claim_pub_hex; + stealth_data["amount_commitment"] = amt_commit_b64; + t.step("build_stealth_json"); + + steps.push_back("[8/8] building stealth transaction"); + + auto bi = get_nonce_balance(); + t.step("get_nonce_balance"); + + octra::Transaction tx; + tx.from = from_addr; + tx.to_ = "stealth"; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "5000"); + tx.timestamp = now_ts(); + tx.op_type = "stealth"; + tx.encrypted_data = stealth_data.dump(); + t.step("build_tx+json_dump"); + + sign_tx_fields(tx); + t.step("sign_tx"); + + auto result = submit_tx(tx); + t.step("submit_tx"); + + if (result.contains("error")) res.status = 500; + result["steps"] = steps; + res.set_content(result.dump(), "application/json"); } catch (const std::exception& e) { - fprintf(stderr, "[stealth/send] exception: %s\n", e.what()); res.status = 500; res.set_content(err_json(std::string("stealth send failed: ") + e.what()).dump(), "application/json"); } catch (...) { - fprintf(stderr, "[stealth/send] unknown exception\n"); res.status = 500; res.set_content(err_json("stealth send failed: unknown error").dump(), "application/json"); } }); svr.Get("/api/stealth/scan", [](const httplib::Request&, httplib::Response& res) { + octra::ScopedTimer timer("stealth.scan"); WALLET_GUARD - std::lock_guard lock(g_mtx); - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); + uint8_t view_sk[32]; + { + uint8_t view_pk[32]; + std::lock_guard lock(g_mtx); + octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); + } auto r = g_rpc.get_stealth_outputs(0); json outputs = json::array(); if (!r.ok || !r.result.is_object() || !r.result.contains("outputs")) { @@ -1440,9 +1595,8 @@ int main(int argc, char** argv) { }); svr.Post("/api/stealth/claim", [](const httplib::Request& req, httplib::Response& res) { + octra::OpTimer t("claim", "claim started"); WALLET_GUARD - std::lock_guard lock(g_mtx); - PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1454,18 +1608,21 @@ int main(int argc, char** argv) { res.set_content(err_json("ids required").dump(), "application/json"); return; } - + std::lock_guard lock(g_mtx); + t.mutex_acquired(); + PVAC_GUARD uint8_t view_sk[32], view_pk[32]; octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); + t.step("derive_view_keypair"); + auto sr = g_rpc.get_stealth_outputs(0); + t.step("get_stealth_outputs"); if (!sr.ok || !sr.result.is_object()) { res.status = 500; res.set_content(err_json("failed to fetch outputs").dump(), "application/json"); return; } - ensure_pvac_registered(); - std::vector req_ids; for (auto& id : body["ids"]) { if (id.is_string()) req_ids.push_back(id.get()); @@ -1473,7 +1630,13 @@ int main(int argc, char** argv) { } auto bi = get_nonce_balance(); int nonce = bi.nonce; + t.reset_step(); + + ensure_pvac_registered(); + t.step("ensure_pvac_registered"); + json results = json::array(); + char sn[96]; for (auto& out : sr.result["outputs"]) { std::string out_id = out.contains("id") ? @@ -1485,26 +1648,49 @@ int main(int argc, char** argv) { if (!wanted) continue; if (out.value("claimed", 0) != 0) { results.push_back({{"id", out_id}, {"ok", false}, {"error", "already claimed"}}); + t.reset_step(); continue; } try { auto eph_raw = octra::base64_decode(out["eph_pub"].get()); if (eph_raw.size() != 32) throw std::runtime_error("bad eph_pub"); auto shared = octra::ecdh_shared_secret(view_sk, eph_raw.data()); + snprintf(sn, sizeof(sn), "id=%s ecdh", out_id.c_str()); + t.step(sn); + auto dec = octra::decrypt_stealth_amount(shared, out.value("enc_amount", "")); if (!dec.has_value()) throw std::runtime_error("decrypt failed"); + snprintf(sn, sizeof(sn), "id=%s decrypt_stealth_amount", out_id.c_str()); + t.step(sn); + auto cs = octra::compute_claim_secret(shared); + snprintf(sn, sizeof(sn), "id=%s compute_claim_secret", out_id.c_str()); + t.step(sn); uint8_t seed[32]; octra::random_bytes(seed, 32); pvac_cipher ct_claim = g_pvac.encrypt(dec->amount, seed); + snprintf(sn, sizeof(sn), "id=%s pvac_encrypt", out_id.c_str()); + t.step(sn); + std::string claim_cipher_str = g_pvac.encode_cipher(ct_claim); + snprintf(sn, sizeof(sn), "id=%s encode_cipher", out_id.c_str()); + t.step(sn); + auto commit = g_pvac.commit_ct(ct_claim); std::string commit_b64 = octra::base64_encode(commit.data(), 32); + snprintf(sn, sizeof(sn), "id=%s commit_ct+encode", out_id.c_str()); + t.step(sn); + pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct_claim, dec->amount, dec->blinding.data()); + snprintf(sn, sizeof(sn), "id=%s zero_proof_make", out_id.c_str()); + t.step(sn); + std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_cipher(ct_claim); g_pvac.free_zero_proof(zkp); + snprintf(sn, sizeof(sn), "id=%s zero_proof_encode+free", out_id.c_str()); + t.step(sn); json claim_data; claim_data["version"] = 5; @@ -1513,6 +1699,8 @@ int main(int argc, char** argv) { claim_data["commitment"] = commit_b64; claim_data["claim_secret"] = octra::hex_encode(cs.data(), 32); claim_data["zero_proof"] = zp_str; + snprintf(sn, sizeof(sn), "id=%s build_claim_json", out_id.c_str()); + t.step(sn); nonce++; octra::Transaction tx; @@ -1524,8 +1712,17 @@ int main(int argc, char** argv) { tx.timestamp = now_ts(); tx.op_type = "claim"; tx.encrypted_data = claim_data.dump(); + snprintf(sn, sizeof(sn), "id=%s build_tx+json_dump", out_id.c_str()); + t.step(sn); + sign_tx_fields(tx); + snprintf(sn, sizeof(sn), "id=%s sign_tx", out_id.c_str()); + t.step(sn); + auto sr2 = submit_tx(tx); + snprintf(sn, sizeof(sn), "id=%s submit_tx", out_id.c_str()); + t.step(sn); + if (sr2.contains("error")) { results.push_back({{"id", out_id}, {"ok", false}, {"error", sr2["error"]}}); } else { @@ -1533,6 +1730,7 @@ int main(int argc, char** argv) { } } catch (const std::exception& e) { results.push_back({{"id", out_id}, {"ok", false}, {"error", e.what()}}); + t.reset_step(); } } json j; @@ -1588,14 +1786,17 @@ int main(int argc, char** argv) { svr.Get("/api/keys", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); json j; - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["view_pubkey"] = octra::base64_encode(view_pk, 32); - j["has_master_seed"] = g_wallet.has_master_seed(); - octra::secure_zero(view_sk, 32); + { + std::lock_guard lock(g_mtx); + uint8_t view_sk[32], view_pk[32]; + octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); + j["address"] = g_wallet.addr; + j["public_key"] = g_wallet.pub_b64; + j["view_pubkey"] = octra::base64_encode(view_pk, 32); + j["has_master_seed"] = g_wallet.has_master_seed(); + octra::secure_zero(view_sk, 32); + } res.set_content(j.dump(), "application/json"); }); @@ -1741,9 +1942,8 @@ int main(int argc, char** argv) { res.set_content(err_json("bytecode required").dump(), "application/json"); return; } - int nonce_val = 0; auto bi = get_nonce_balance(); - nonce_val = bi.nonce + 1; + int nonce_val = bi.nonce + 1; auto r = g_rpc.compute_contract_address(bytecode, g_wallet.addr, nonce_val); if (!r.ok) { res.status = 400; @@ -2004,17 +2204,21 @@ int main(int argc, char** argv) { res.set_content(r.result.dump(), "application/json"); }); - static json g_token_cache; - static double g_token_cache_ts = 0; - static std::string g_token_cache_addr; - svr.Get("/api/tokens", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD + std::string wallet_addr; + { + std::lock_guard lock(g_mtx); + wallet_addr = g_wallet.addr; + } double now = (double)time(nullptr); - if (!g_token_cache.empty() && g_token_cache_addr == g_wallet.addr - && (now - g_token_cache_ts) < 30.0) { - res.set_content(g_token_cache.dump(), "application/json"); - return; + { + std::lock_guard lock(g_token_mtx); + if (!g_token_cache.empty() && g_token_cache_addr == wallet_addr + && (now - g_token_cache_ts) < 30.0) { + res.set_content(g_token_cache.dump(), "application/json"); + return; + } } auto lr = g_rpc.list_contracts(); json tokens = json::array(); @@ -2029,7 +2233,7 @@ int main(int argc, char** argv) { if (sym.empty() || sym == "0") continue; if (sym.size() > 10) sym = sym.substr(0, 10); auto br = g_rpc.contract_call_view(addr, "balance_of", - json::array({g_wallet.addr}), g_wallet.addr); + json::array({wallet_addr}), wallet_addr); std::string bal = (br.ok && br.result.contains("result") && !br.result["result"].is_null()) ? br.result.value("result", "0") : "0"; if (bal == "0" || bal.empty()) continue; @@ -2057,10 +2261,13 @@ int main(int argc, char** argv) { json j; j["tokens"] = tokens; j["count"] = tokens.size(); - j["wallet_address"] = g_wallet.addr; - g_token_cache = j; - g_token_cache_ts = now; - g_token_cache_addr = g_wallet.addr; + j["wallet_address"] = wallet_addr; + { + std::lock_guard lock(g_token_mtx); + g_token_cache = j; + g_token_cache_ts = now; + g_token_cache_addr = wallet_addr; + } res.set_content(j.dump(), "application/json"); }); diff --git a/rpc_client.hpp b/rpc_client.hpp index c0fbba3..8d99808 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -30,12 +30,146 @@ #include #include #include +#include +#include +#include #include "lib/json.hpp" #include "lib/httplib.h" namespace octra { +inline void timing_ms_esc(double ms, const char** open, const char** reset) { + if (ms >= 10000.0) { + *open = "\033[31m"; + *reset = "\033[0m"; + } else if (ms >= 1000.0) { + *open = "\033[33m"; + *reset = "\033[0m"; + } else { + *open = ""; + *reset = ""; + } +} + +inline void get_wall_hms(char* buf, size_t cap) { + using std::chrono::system_clock; + std::time_t t = system_clock::to_time_t(system_clock::now()); + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &t); +#else + localtime_r(&t, &tm); +#endif + snprintf(buf, cap, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +inline void log_event(const char* msg) { + char buf[16]; + get_wall_hms(buf, sizeof(buf)); + fprintf(stderr, "[%s] %s\n", buf, msg); +} + +struct ScopedTimer { + char wall[16]; + const char* label; + const char* dot; + std::chrono::steady_clock::time_point start; + + explicit ScopedTimer(const char* l) : label(l), start(std::chrono::steady_clock::now()) { + const char* p = l; + while (*p && *p != '.') ++p; + dot = *p == '.' ? p : nullptr; + get_wall_hms(wall, sizeof(wall)); + if (dot) + fprintf(stderr, "[%s] [%.*s] %s started\n", wall, (int)(dot - label), label, dot + 1); + else + fprintf(stderr, "[%s] [%s] started\n", wall, label); + } + + ~ScopedTimer() { + double ms = std::chrono::duration( + std::chrono::steady_clock::now() - start).count(); + char wall_now[16]; + get_wall_hms(wall_now, sizeof(wall_now)); + const char* esc; + const char* reset; + timing_ms_esc(ms, &esc, &reset); + if (dot) + fprintf(stderr, "[%s] [%.*s] %s %s(%.3f ms)%s\n", wall_now, (int)(dot - label), label, dot + 1, esc, ms, reset); + else + fprintf(stderr, "[%s] [%s] %s(%.3f ms)%s\n", wall_now, label, esc, ms, reset); + } +}; + +struct OpTimer { + const char* op; + std::chrono::steady_clock::time_point wall_start; + std::chrono::steady_clock::time_point step_start; + std::chrono::steady_clock::time_point op_start; + bool has_op_start; + + explicit OpTimer(const char* name, const char* desc) + : op(name) + , wall_start(std::chrono::steady_clock::now()) + , step_start(wall_start) + , op_start(wall_start) + , has_op_start(false) + { + char tw[16]; get_wall_hms(tw, sizeof(tw)); + fprintf(stderr, "[%s] [%s] %s (0.000 ms)\n", tw, op, desc); + } + + void mutex_acquired() { + auto now = std::chrono::steady_clock::now(); + _log_step("mutex_wait", wall_start, now); + op_start = now; + step_start = now; + has_op_start = true; + } + + void step(const char* name) { + auto now = std::chrono::steady_clock::now(); + _log_step(name, step_start, now); + step_start = now; + } + + void step_msg(const char* msg) { + char tw[16]; get_wall_hms(tw, sizeof(tw)); + fprintf(stderr, "[%s] [%s] %s\n", tw, op, msg); + step_start = std::chrono::steady_clock::now(); + } + + void reset_step() { + step_start = std::chrono::steady_clock::now(); + } + + ~OpTimer() { + auto now = std::chrono::steady_clock::now(); + char tw[16]; get_wall_hms(tw, sizeof(tw)); + const char* esc; const char* reset; + if (has_op_start) { + double ms = std::chrono::duration(now - op_start).count(); + timing_ms_esc(ms, &esc, &reset); + fprintf(stderr, "[%s] [%s] total %s(%.3f ms)%s\n", tw, op, esc, ms, reset); + } + double wall_ms = std::chrono::duration(now - wall_start).count(); + timing_ms_esc(wall_ms, &esc, &reset); + fprintf(stderr, "[%s] [%s] handler_wall_total %s(%.3f ms)%s\n", tw, op, esc, wall_ms, reset); + } + +private: + void _log_step(const char* name, + std::chrono::steady_clock::time_point from, + std::chrono::steady_clock::time_point to) { + double ms = std::chrono::duration(to - from).count(); + char tw[16]; get_wall_hms(tw, sizeof(tw)); + const char* esc; const char* reset; + timing_ms_esc(ms, &esc, &reset); + fprintf(stderr, "[%s] [%s] %s %s(%.3f ms)%s\n", tw, op, name, esc, ms, reset); + } +}; + struct RpcResult { bool ok; nlohmann::json result; @@ -49,6 +183,33 @@ class RpcClient { int port_; std::atomic id_{0}; + static std::string rpc_start_label(const std::string& method, const std::string& hint) { + if (!hint.empty()) + return std::string("calling the contract ") + hint + "..."; + return method + "..."; + } + + static void rpc_log_start(const std::string& method, const std::string& hint = "") { + char tw[16]; + get_wall_hms(tw, sizeof(tw)); + std::string label = rpc_start_label(method, hint); + fprintf(stderr, "[%s] [rpc] %s started\n", tw, label.c_str()); + } + + static void rpc_log_one_line(const std::string& method, double ms, bool ok, const std::string& err, + const std::string& hint = "") { + char tw[16]; + get_wall_hms(tw, sizeof(tw)); + const char* esc; + const char* reset; + timing_ms_esc(ms, &esc, &reset); + std::string label = rpc_start_label(method, hint); + if (ok) + fprintf(stderr, "[%s] [rpc] %s ok %s(%.3f ms)%s\n", tw, label.c_str(), esc, ms, reset); + else + fprintf(stderr, "[%s] [rpc] %s failed %s(%.3f ms)%s: %s\n", tw, label.c_str(), esc, ms, reset, err.c_str()); + } + void parse_url(const std::string& url) { std::string u = url; ssl_ = false; @@ -82,30 +243,26 @@ class RpcClient { RpcResult call(const std::string& method, const nlohmann::json& params = nlohmann::json::array(), - int timeout_sec = 30) { + int timeout_sec = 30, + const std::string& hint = "") { + auto t0 = std::chrono::steady_clock::now(); nlohmann::json req; req["jsonrpc"] = "2.0"; req["method"] = method; req["params"] = params; req["id"] = ++id_; std::string body = req.dump(); - httplib::Headers hdrs = {{"Content-Type", "application/json"}}; - if (ssl_) { - httplib::SSLClient cli(host_, port_); - cli.set_connection_timeout(timeout_sec, 0); - cli.set_read_timeout(timeout_sec, 0); - cli.enable_server_certificate_verification(false); - auto res = cli.Post(path_, hdrs, body, "application/json"); - if (!res) return {false, {}, "connection failed"}; - return parse_response(res->body); - } else { - httplib::Client cli(host_, port_); - cli.set_connection_timeout(timeout_sec, 0); - cli.set_read_timeout(timeout_sec, 0); - auto res = cli.Post(path_, hdrs, body, "application/json"); - if (!res) return {false, {}, "connection failed"}; - return parse_response(res->body); + rpc_log_start(method, hint); + auto res = post_json(body, timeout_sec); + double ms = std::chrono::duration( + std::chrono::steady_clock::now() - t0).count(); + if (!res) { + rpc_log_one_line(method, ms, false, "connection failed", hint); + return {false, {}, "connection failed"}; } + RpcResult out = parse_response(res->body); + rpc_log_one_line(method, ms, out.ok, out.error, hint); + return out; } RpcResult get_balance(const std::string& addr) { @@ -172,9 +329,6 @@ class RpcClient { return call("octra_compileAml", {source}, 10); } - - // rpc compl - RpcResult compile_aml_multi(const nlohmann::json& files, const std::string& main_path) { nlohmann::json payload; payload["files"] = files; @@ -200,7 +354,8 @@ class RpcClient { const std::string& method, const nlohmann::json& params, const std::string& caller) { - return call("contract_call", {addr, method, params, caller}, 15); + return call("contract_call", {addr, method, params, caller}, 15, + "(" + addr + ") with (" + method + ")"); } RpcResult list_contracts() { @@ -223,18 +378,86 @@ class RpcClient { return call("octra_transactionsByAddress", {addr, limit, offset}, 15); } + std::vector call_batch( + const std::vector& methods, + const std::vector& params_list = {}, + int timeout_sec = 10) { + auto t0 = std::chrono::steady_clock::now(); + nlohmann::json batch = nlohmann::json::array(); + size_t count = methods.size(); + for (size_t i = 0; i < count; ++i) { + nlohmann::json req; + req["jsonrpc"] = "2.0"; + req["method"] = methods[i]; + req["params"] = (i < params_list.size()) ? params_list[i] : nlohmann::json::array(); + req["id"] = static_cast(i + 1); + batch.push_back(std::move(req)); + } + std::string body = batch.dump(); + rpc_log_start("batch(" + std::to_string(count) + ")", ""); + auto res = post_json(body, timeout_sec); + double ms = std::chrono::duration( + std::chrono::steady_clock::now() - t0).count(); + std::vector out(count, {false, {}, "no response"}); + if (!res) { + rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, false, "connection failed"); + return out; + } + try { + auto arr = nlohmann::json::parse(res->body); + if (arr.is_array()) { + for (auto& item : arr) { + if (!item.contains("id") || !item["id"].is_number_integer()) continue; + int id = item["id"].get(); + if (id < 1 || id > static_cast(count)) continue; + if (item.contains("result")) { + out[id - 1] = {true, item["result"], ""}; + } else if (item.contains("error")) { + auto& e = item["error"]; + std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); + out[id - 1] = {false, {}, msg}; + } + } + } + } catch (const std::exception& ex) { + rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, false, std::string("parse error: ") + ex.what()); + return out; + } + rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, true, ""); + return out; + } + private: + httplib::Result post_json(const std::string& body, int timeout_sec) { + httplib::Headers hdrs = {{"Content-Type", "application/json"}}; + if (ssl_) { + httplib::SSLClient cli(host_, port_); + cli.set_connection_timeout(timeout_sec, 0); + cli.set_read_timeout(timeout_sec, 0); + cli.enable_server_certificate_verification(false); + return cli.Post(path_, hdrs, body, "application/json"); + } + httplib::Client cli(host_, port_); + cli.set_connection_timeout(timeout_sec, 0); + cli.set_read_timeout(timeout_sec, 0); + return cli.Post(path_, hdrs, body, "application/json"); + } + + RpcResult parse_response_obj(const nlohmann::json& j) { + if (j.contains("result")) + return {true, j["result"], ""}; + if (j.contains("error")) { + auto& e = j["error"]; + std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); + return {false, {}, msg}; + } + return {false, {}, "unknown rpc response"}; + } + RpcResult parse_response(const std::string& body) { try { auto j = nlohmann::json::parse(body); - if (j.contains("result")) - return {true, j["result"], ""}; - if (j.contains("error")) { - auto& e = j["error"]; - std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); - return {false, {}, msg}; - } - return {false, {}, "unknown rpc response"}; + return parse_response_obj(j); } catch (const std::exception& ex) { return {false, {}, std::string("parse error: ") + ex.what()}; } From 9eca601d3664550f2d1118f9d9a3be6b6177d7e7 Mon Sep 17 00:00:00 2001 From: xcvoierg Date: Mon, 20 Apr 2026 14:36:20 -0700 Subject: [PATCH 03/13] fix --- main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/main.cpp b/main.cpp index ac3fd81..728664a 100644 --- a/main.cpp +++ b/main.cpp @@ -1489,9 +1489,7 @@ int main(int argc, char** argv) { steps.push_back("[6/8] encoding proofs"); - uint8_t r_amt[32]; - octra::random_bytes(r_amt, 32); - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_amt); + auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_blind); std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); t.step("pedersen_amount_commitment"); From 1fc3302db02ee31bd553d1794751048d174d8282 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:14:49 +0000 Subject: [PATCH 04/13] bridge, minor updates, etc --- Makefile | 40 ++- crypto_utils.hpp | 64 +++- main.cpp | 671 ++++++++++++++++------------------- rpc_client.hpp | 302 ++++------------ setup.bat | 85 +++-- setup.sh | 126 +++++-- static/bridge.html | 846 +++++++++++++++++++++++++++++++++++++++++++++ static/index.html | 16 +- static/style.css | 8 + static/wallet.js | 11 +- wallet.hpp | 4 + 11 files changed, 1502 insertions(+), 671 deletions(-) create mode 100644 static/bridge.html diff --git a/Makefile b/Makefile index 24eae93..f3aa985 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,43 @@ endif CXXFLAGS+=-I$(PVAC_DIR) LIBPVAC:=$(PVAC_BUILD)/libpvac.$(SHARED_EXT) -all: $(TARGET) +all: check-deps $(TARGET) + +LEVELDB_PATHS:=/usr/include/leveldb/db.h /usr/local/include/leveldb/db.h /opt/homebrew/include/leveldb/db.h /opt/local/include/leveldb/db.h $(LDB_PREFIX)/include/leveldb/db.h +OPENSSL_PATHS:=/usr/include/openssl/evp.h /usr/local/include/openssl/evp.h /opt/homebrew/include/openssl/evp.h /opt/local/include/openssl/evp.h $(SSL_PREFIX)/include/openssl/evp.h + +HAVE_LEVELDB:=$(shell for p in $(LEVELDB_PATHS); do [ -f "$$p" ] && { echo yes; exit 0; }; done; echo no) +HAVE_OPENSSL:=$(shell for p in $(OPENSSL_PATHS); do [ -f "$$p" ] && { echo yes; exit 0; }; done; echo no) + +check-deps: +ifeq ($(OCTRA_SKIP_AUTOSETUP),) +ifeq ($(HAVE_LEVELDB)$(HAVE_OPENSSL),yesyes) + @true +else + @echo 'missing dependencies (leveldb=$(HAVE_LEVELDB) openssl=$(HAVE_OPENSSL))' + @echo 'running ./setup.sh --deps-only to install...' + @echo '' + @if [ -x ./setup.sh ]; then \ + ./setup.sh --deps-only || { \ + echo ''; \ + echo 'auto-install failed. install manually:'; \ + echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ + echo 'brew install leveldb openssl@3 (macos)'; \ + exit 1; \ + }; \ + else \ + echo 'setup.sh not found. install manually:'; \ + echo ' sudo apt install libleveldb-dev libssl-dev'; \ + exit 1; \ + fi + @ok=no; for p in $(LEVELDB_PATHS); do [ -f "$$p" ] && ok=yes; done; \ + [ "$$ok" = "yes" ] || { echo 'error: leveldb still missing after setup.sh'; exit 1; } + @ok=no; for p in $(OPENSSL_PATHS); do [ -f "$$p" ] && ok=yes; done; \ + [ "$$ok" = "yes" ] || { echo 'error: openssl still missing after setup.sh'; exit 1; } +endif +else + @true +endif $(PVAC_BUILD): @mkdir -p $(PVAC_BUILD) @@ -104,4 +140,4 @@ clean: run: $(TARGET) ./$(TARGET) 8420 -.PHONY: all clean run +.PHONY: all clean run check-deps diff --git a/crypto_utils.hpp b/crypto_utils.hpp index f2215dc..a7c0b73 100644 --- a/crypto_utils.hpp +++ b/crypto_utils.hpp @@ -41,6 +41,7 @@ #endif #include #include +#include extern "C" { #include "lib/tweetnacl.h" @@ -98,7 +99,7 @@ static const uint32_t K[64] = { 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 }; -} // namespace detail +} inline std::array sha256(const uint8_t* data, size_t len) { using namespace detail; @@ -147,7 +148,7 @@ inline std::array sha256(const uint8_t* data, size_t len) { std::array out; for (int i = 0; i < 8; i++) { - out[i * 4] = (uint8_t)(h[i] >> 24); + out[i * 4] = (uint8_t)(h[i] >> 24); out[i * 4 + 1] = (uint8_t)(h[i] >> 16); out[i * 4 + 2] = (uint8_t)(h[i] >> 8); out[i * 4 + 3] = (uint8_t)(h[i]); @@ -266,6 +267,59 @@ inline void ed25519_pk_to_curve25519(const uint8_t ed_sk[64], uint8_t x_pk[32]) crypto_scalarmult_base(x_pk, x_sk); } + + +// dont touch + +inline bool ed25519_pub_to_x25519(const uint8_t ed_pub[32], uint8_t x_pub[32]) { + BN_CTX* ctx = BN_CTX_new(); + BIGNUM *p = BN_new(), *y = BN_new(), *one = BN_new(); + BIGNUM *one_plus_y = BN_new(), *one_minus_y = BN_new(); + BIGNUM *u = BN_new(), *inv = BN_new(), *p_minus_2 = BN_new(); + bool ok = false; + if (!ctx || !p || !y || !one || !one_plus_y || !one_minus_y || !u || !inv || !p_minus_2) goto done; + + if (!BN_set_bit(p, 255)) goto done; + if (!BN_sub_word(p, 19)) goto done; + BN_one(one); + + { + uint8_t y_be[32]; + for (int i = 0; i < 32; ++i) y_be[i] = ed_pub[31 - i]; + y_be[0] &= 0x7F; + if (!BN_bin2bn(y_be, 32, y)) goto done; + } + + if (!BN_mod_add(one_plus_y, one, y, p, ctx)) goto done; + if (!BN_mod_sub(one_minus_y, one, y, p, ctx)) goto done; + if (!BN_copy(p_minus_2, p)) goto done; + if (!BN_sub_word(p_minus_2, 2)) goto done; + if (!BN_mod_exp(inv, one_minus_y, p_minus_2, p, ctx)) goto done; + if (!BN_mod_mul(u, one_plus_y, inv, p, ctx)) goto done; + + { + uint8_t u_be[32] = {0}; + if (BN_bn2binpad(u, u_be, 32) != 32) goto done; + for (int i = 0; i < 32; ++i) x_pub[i] = u_be[31 - i]; + } + ok = true; + +done: + BN_free(p_minus_2); + BN_free(inv); + BN_free(u); + BN_free(one_minus_y); + BN_free(one_plus_y); + BN_free(one); + BN_free(y); + BN_free(p); + BN_CTX_free(ctx); + return ok; +} + + +// !! + inline void secure_zero(void* ptr, size_t len) { volatile uint8_t* p = static_cast(ptr); while (len--) *p++ = 0; @@ -401,7 +455,7 @@ inline std::string generate_mnemonic_12() { uint8_t entropy[16]; randombytes(entropy, 16); auto hash = sha256(entropy, 16); - uint8_t bits[17]; // 128 + 8 = 136 bits available + uint8_t bits[17]; memcpy(bits, entropy, 16); bits[16] = hash[0]; secure_zero(entropy, 16); @@ -449,7 +503,7 @@ inline bool validate_mnemonic(const std::string& mnemonic) { inline bool looks_like_mnemonic(const std::string& input) { int spaces = 0; for (char c : input) if (c == ' ') spaces++; - return spaces >= 11; // at least 12 words + return spaces >= 11; } -} // namespace octra \ No newline at end of file +} \ No newline at end of file diff --git a/main.cpp b/main.cpp index 728664a..e2f1800 100644 --- a/main.cpp +++ b/main.cpp @@ -37,6 +37,8 @@ #include #include #include +#include +#include #ifdef _WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN @@ -76,13 +78,27 @@ static std::atomic g_wallet_loaded{false}; static std::string g_wallet_path = "data/wallet.oct"; static std::string g_pin; static TxCache g_txcache; -static json g_fee_cache; + +static nlohmann::json g_fee_cache; static double g_fee_cache_ts = 0.0; static std::mutex g_fee_mtx; -static json g_token_cache; -static double g_token_cache_ts = 0.0; -static std::string g_token_cache_addr; -static std::mutex g_token_mtx; + +static std::unordered_map> g_pk_cache; +static std::mutex g_pk_mtx; + +static std::optional> pk_cache_get(const std::string& addr) { + std::lock_guard lk(g_pk_mtx); + auto it = g_pk_cache.find(addr); + if (it == g_pk_cache.end()) return std::nullopt; + return it->second; +} + +static void pk_cache_put(const std::string& addr, const std::vector& pk) { + if (pk.size() != 32) return; + std::lock_guard lk(g_pk_mtx); + if (g_pk_cache.size() > 2048) g_pk_cache.clear(); + g_pk_cache[addr] = pk; +} static void handle_signal(int) { octra::secure_zero(g_wallet.sk, 64); @@ -271,36 +287,33 @@ struct EncBalResult { int64_t decrypted; }; -static EncBalResult get_encrypted_balance(octra::OpTimer* t = nullptr) { +static EncBalResult get_encrypted_balance() { std::string sig = octra::sign_balance_request(g_wallet.addr, g_wallet.sk); - if (t) t->step("encbal_sign_request"); auto r = g_rpc.get_encrypted_balance(g_wallet.addr, sig, g_wallet.pub_b64); - if (t) t->reset_step(); if (!r.ok || !r.result.is_object()) return {"0", 0}; std::string cipher = r.result.value("cipher", "0"); if (!g_pvac_ok || cipher.empty() || cipher == "0") return {cipher, 0}; int64_t dec = g_pvac.get_balance(cipher); - if (t) t->step("encbal_local_get_balance"); return {cipher, dec}; } static void init_wallet_subsystems() { g_rpc.set_url(g_wallet.rpc_url); ensure_pubkey_registered(g_wallet.addr, g_wallet.sk, g_wallet.pub_b64); - { - octra::ScopedTimer t("pvac.init"); - g_pvac_ok = g_pvac.init(g_wallet.priv_b64); - if (g_pvac_ok) { - ensure_pvac_registered(); - } + g_pvac_ok = g_pvac.init(g_wallet.priv_b64); + if (g_pvac_ok) { + fprintf(stderr, "pvac initialized\n"); + ensure_pvac_registered(); + } else { + fprintf(stderr, "pvac init failed (libpvac not loaded?)\n"); } g_txcache.close(); std::string cache_path = "data/txcache_" + g_wallet.addr.substr(3, 8); - { - octra::ScopedTimer t("txcache.open"); - if (g_txcache.open(cache_path)) { - g_txcache.ensure_rpc(g_wallet.rpc_url); - } + if (g_txcache.open(cache_path)) { + fprintf(stderr, "txcache opened: %s\n", cache_path.c_str()); + g_txcache.ensure_rpc(g_wallet.rpc_url); + } else { + fprintf(stderr, "txcache open failed: %s\n", cache_path.c_str()); } g_wallet_loaded = true; } @@ -331,13 +344,6 @@ int main(int argc, char** argv) { handle_signal(0); return TRUE; }, TRUE); - HANDLE hOut = GetStdHandle(STD_ERROR_HANDLE); - if (hOut != INVALID_HANDLE_VALUE) { - DWORD mode = 0; - if (GetConsoleMode(hOut, &mode)) { - SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING); - } - } #else struct rlimit rl = {0, 0}; setrlimit(RLIMIT_CORE, &rl); @@ -357,8 +363,12 @@ int main(int argc, char** argv) { httplib::Server svr; svr.set_read_timeout(300, 0); svr.set_write_timeout(300, 0); + + + // svr.set_keep_alive_timeout(5); svr.set_keep_alive_max_count(100); + // svr.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { res.set_header("X-Frame-Options", "DENY"); @@ -453,13 +463,13 @@ int main(int argc, char** argv) { bool has_leg = octra::has_legacy_wallet(); bool has_enc = octra::has_encrypted_wallet(); if (has_leg && !has_enc && addr_hint.empty()) { - octra::ScopedTimer t("wallet.migrate"); g_wallet = octra::migrate_wallet(pin); g_wallet_path = octra::WALLET_FILE; + fprintf(stderr, "wallet migrated: %s\n", g_wallet.addr.c_str()); } else { - octra::ScopedTimer t("wallet.unlock"); g_wallet = octra::load_wallet_encrypted(unlock_path, pin); g_wallet_path = unlock_path; + fprintf(stderr, "wallet unlocked: %s\n", g_wallet.addr.c_str()); } try { @@ -490,9 +500,6 @@ int main(int argc, char** argv) { }); svr.Post("/api/wallet/lock", [](const httplib::Request&, httplib::Response& res) { - auto logout_t0 = std::chrono::steady_clock::now(); - { char tw[16]; octra::get_wall_hms(tw, sizeof(tw)); - fprintf(stderr, "[%s] [logout] starting logout (0.000 ms)\n", tw); } std::lock_guard lock(g_mtx); if (!g_wallet_loaded) { res.status = 409; @@ -514,12 +521,7 @@ int main(int argc, char** argv) { g_wallet.priv_b64.clear(); g_wallet.pub_b64.clear(); g_wallet.addr.clear(); - { double ms = std::chrono::duration( - std::chrono::steady_clock::now() - logout_t0).count(); - char tw[16]; octra::get_wall_hms(tw, sizeof(tw)); - const char* esc; const char* rst; - octra::timing_ms_esc(ms, &esc, &rst); - fprintf(stderr, "[%s] [logout] wallet locked %s(%.3f ms)%s\n", tw, esc, ms, rst); } + fprintf(stderr, "wallet locked\n"); json j; j["ok"] = true; res.set_content(j.dump(), "application/json"); @@ -684,16 +686,14 @@ int main(int argc, char** argv) { svr.Get("/api/wallet", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD json j; - { - std::lock_guard lock(g_mtx); - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["rpc_url"] = g_wallet.rpc_url; - j["explorer_url"] = g_wallet.explorer_url; - j["has_master_seed"] = g_wallet.has_master_seed(); - j["hd_index"] = g_wallet.hd_index; - j["hd_version"] = g_wallet.hd_version; - } + j["address"] = g_wallet.addr; + j["public_key"] = g_wallet.pub_b64; + j["rpc_url"] = g_wallet.rpc_url; + j["explorer_url"] = g_wallet.explorer_url; + j["bridge_signer_url"] = g_wallet.bridge_signer_url; + j["has_master_seed"] = g_wallet.has_master_seed(); + j["hd_index"] = g_wallet.hd_index; + j["hd_version"] = g_wallet.hd_version; res.set_content(j.dump(), "application/json"); }); @@ -899,6 +899,11 @@ int main(int argc, char** argv) { bool pvac_ok; { std::lock_guard lock(g_mtx); + if (!g_wallet_loaded) { + res.status = 503; + res.set_content(err_json("no wallet loaded").dump(), "application/json"); + return; + } addr = g_wallet.addr; pub_b64 = g_wallet.pub_b64; sig_bal = octra::sign_balance_request(addr, g_wallet.sk); @@ -1033,7 +1038,7 @@ int main(int argc, char** argv) { }); svr.Get("/api/fee", [](const httplib::Request&, httplib::Response& res) { - double now = (double)time(nullptr); + double now = now_ts(); { std::lock_guard lock(g_fee_mtx); if (!g_fee_cache.empty() && (now - g_fee_cache_ts) < 30.0) { @@ -1041,39 +1046,28 @@ int main(int argc, char** argv) { return; } } - - static const std::vector ops = { - "standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call" - }; - + std::vector ops = {"standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call"}; + std::vector methods(ops.size(), "octra_recommendedFee"); std::vector params; params.reserve(ops.size()); - for (auto& op : ops) { - params.push_back(nlohmann::json::array({op})); - } - - auto results = g_rpc.call_batch(ops, params, 10); - + for (auto& op : ops) params.push_back(nlohmann::json::array({op})); + auto results = g_rpc.call_batch(methods, params, 10); json fees; for (size_t i = 0; i < ops.size(); ++i) { - if (i < results.size() && results[i].ok) { - fees[ops[i]] = results[i].result; - } else { - fees[ops[i]] = {{"minimum", "1000"}, {"recommended", "1000"}, {"fast", "2000"}}; - } + if (i < results.size() && results[i].ok) fees[ops[i]] = results[i].result; + else fees[ops[i]] = {{"minimum", "1000"}, {"recommended", "1000"}, {"fast", "2000"}}; } - { std::lock_guard lock(g_fee_mtx); - g_fee_cache = fees; + g_fee_cache = fees; g_fee_cache_ts = now; } res.set_content(fees.dump(), "application/json"); }); svr.Post("/api/send", [](const httplib::Request& req, httplib::Response& res) { - octra::OpTimer t("send", "standard send started"); WALLET_GUARD + std::lock_guard lock(g_mtx); json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1092,30 +1086,24 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } - std::lock_guard lock(g_mtx); - t.mutex_acquired(); - auto bi = get_nonce_balance(); - t.step("get_nonce_balance"); + auto bi = get_nonce_balance(); int nonce = bi.nonce; octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = to; tx.amount = std::to_string(raw); - tx.nonce = bi.nonce + 1; + tx.nonce = nonce + 1; tx.ou = parse_ou(body, (raw < 1000000000) ? "10000" : "30000"); tx.timestamp = now_ts(); tx.op_type = "standard"; std::string msg = body.value("message", ""); if (!msg.empty()) tx.message = msg; sign_tx_fields(tx); - t.step("sign_tx"); auto result = submit_tx(tx); - t.step("submit_tx"); if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/key_switch", [](const httplib::Request& req, httplib::Response& res) { - octra::ScopedTimer timer("key_switch"); WALLET_GUARD std::lock_guard lock(g_mtx); auto nb = get_nonce_balance(); @@ -1152,10 +1140,7 @@ int main(int argc, char** argv) { for (int i = 0; i < 8; i++) snprintf(hex + i*2, 3, "%02x", old_hash[i]); tx.message = "encryption key switch | new_key:" + std::string(hex); - { - octra::ScopedTimer s("key_switch.sign"); - sign_tx_fields(tx); - } + sign_tx_fields(tx); auto result = submit_tx(tx); if (result.contains("error")) { res.status = 500; @@ -1167,8 +1152,9 @@ int main(int argc, char** argv) { }); svr.Post("/api/encrypt", [](const httplib::Request& req, httplib::Response& res) { - octra::OpTimer t("encrypt", "encryption started"); WALLET_GUARD + std::lock_guard lock(g_mtx); + PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1181,55 +1167,47 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } - std::lock_guard lock(g_mtx); - t.mutex_acquired(); - PVAC_GUARD ensure_pvac_registered(); - t.step("ensure_pvac_registered"); - uint8_t seed[32], blinding[32]; + uint8_t seed[32]; octra::random_bytes(seed, 32); - octra::random_bytes(blinding, 32); pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); - t.step("pvac_encrypt"); std::string cipher_str = g_pvac.encode_cipher(ct); - t.step("encode_cipher"); + + uint8_t blinding[32]; + octra::random_bytes(blinding, 32); auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - t.step("pedersen_commit+encode"); pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); - t.step("zero_proof_make"); std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_zero_proof(zkp); g_pvac.free_cipher(ct); - t.step("zero_proof_encode+free"); + json enc_data; enc_data["cipher"] = cipher_str; enc_data["amount_commitment"] = amt_commit_b64; enc_data["zero_proof"] = zp_str; enc_data["blinding"] = octra::base64_encode(blinding, 32); - auto bi = get_nonce_balance(); - t.step("get_nonce_balance"); + + auto bi = get_nonce_balance(); int nonce = bi.nonce; octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = g_wallet.addr; tx.amount = std::to_string(raw); - tx.nonce = bi.nonce + 1; + tx.nonce = nonce + 1; tx.ou = parse_ou(body, "10000"); tx.timestamp = now_ts(); tx.op_type = "encrypt"; tx.encrypted_data = enc_data.dump(); - t.step("build_tx+json_dump"); sign_tx_fields(tx); - t.step("sign_tx"); auto result = submit_tx(tx); - t.step("submit_tx"); if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/decrypt", [](const httplib::Request& req, httplib::Response& res) { - octra::OpTimer t("decrypt", "decryption started"); WALLET_GUARD + std::lock_guard lock(g_mtx); + PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1242,10 +1220,7 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid amount (max 6 decimals, no extra dots)").dump(), "application/json"); return; } - std::lock_guard lock(g_mtx); - t.mutex_acquired(); - PVAC_GUARD - auto eb = get_encrypted_balance(&t); + auto eb = get_encrypted_balance(); if (eb.decrypted < raw) { res.status = 400; char buf[128]; @@ -1255,45 +1230,40 @@ int main(int argc, char** argv) { return; } ensure_pvac_registered(); - t.step("ensure_pvac_registered"); json steps = json::array(); steps.push_back("[1/5] FHE encrypt amount (PVAC-HFHE)"); - uint8_t seed[32], blinding[32]; + uint8_t seed[32]; octra::random_bytes(seed, 32); pvac_cipher ct = g_pvac.encrypt((uint64_t)raw, seed); - t.step("pvac_encrypt"); std::string cipher_str = g_pvac.encode_cipher(ct); - t.step("encode_cipher"); steps.push_back("[2/5] bound zero proof"); + + uint8_t blinding[32]; octra::random_bytes(blinding, 32); auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, blinding); std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - t.step("pedersen_commit+encode"); pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct, (uint64_t)raw, blinding); - t.step("zero_proof_make"); std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_zero_proof(zkp); - t.step("zero_proof_encode"); steps.push_back("[3/5] range proof"); + pvac_cipher current_ct = g_pvac.decode_cipher(eb.cipher); - t.step("decode_cipher"); pvac_cipher new_bal_ct = pvac_ct_sub(g_pvac.pk(), current_ct, ct); - t.step("ct_sub"); uint64_t new_bal_value = (uint64_t)(eb.decrypted - raw); - pvac_agg_range_proof arp = pvac_make_aggregated_range_proof(g_pvac.pk(), g_pvac.sk(), new_bal_ct, new_bal_value); - t.step("make_aggregated_range_proof"); + pvac_agg_range_proof arp = pvac_make_aggregated_range_proof( + g_pvac.pk(), g_pvac.sk(), new_bal_ct, new_bal_value); size_t arp_len = 0; uint8_t* arp_data = pvac_serialize_agg_range_proof(arp, &arp_len); - std::string rp_bal_str = std::string("rp_v1|") + octra::base64_encode(arp_data, arp_len); + std::string rp_bal_str = std::string("rp_v1|") + + octra::base64_encode(arp_data, arp_len); pvac_free_bytes(arp_data); pvac_free_agg_range_proof(arp); pvac_free_cipher(new_bal_ct); pvac_free_cipher(current_ct); g_pvac.free_cipher(ct); - t.step("serialize_agg_rp+base64+free_ciphers"); json enc_data; enc_data["cipher"] = cipher_str; @@ -1303,31 +1273,28 @@ int main(int argc, char** argv) { enc_data["range_proof_balance"] = rp_bal_str; steps.push_back("[4/5] building decrypt transaction"); - auto bi = get_nonce_balance(); - t.step("get_nonce_balance"); + + auto bi = get_nonce_balance(); int nonce = bi.nonce; octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = g_wallet.addr; tx.amount = std::to_string(raw); - tx.nonce = bi.nonce + 1; + tx.nonce = nonce + 1; tx.ou = parse_ou(body, "10000"); tx.timestamp = now_ts(); tx.op_type = "decrypt"; tx.encrypted_data = enc_data.dump(); - t.step("build_tx+json_dump"); sign_tx_fields(tx); - t.step("sign_tx"); auto result = submit_tx(tx); - t.step("submit_tx"); steps.push_back("[5/5] submitted to node"); result["steps"] = steps; + if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/stealth/send", [](const httplib::Request& req, httplib::Response& res) { - octra::OpTimer t("stealth", "stealth send started"); WALLET_GUARD json body; try { body = json::parse(req.body); } catch (...) { @@ -1342,6 +1309,7 @@ int main(int argc, char** argv) { res.set_content(err_json("invalid params").dump(), "application/json"); return; } + std::string from_addr, from_pub_b64, eb_sig; { std::lock_guard lock(g_mtx); @@ -1349,202 +1317,170 @@ int main(int argc, char** argv) { from_pub_b64 = g_wallet.pub_b64; eb_sig = octra::sign_balance_request(from_addr, g_wallet.sk); } - t.step("snapshot_wallet+sign_balance"); - - json steps = json::array(); - steps.push_back("[1/8] ECDH x25519 key exchange"); - try { - auto vr = g_rpc.get_view_pubkey(to); - t.reset_step(); - if (!vr.ok || !vr.result.is_object() || !vr.result.contains("view_pubkey") - || vr.result["view_pubkey"].is_null() || !vr.result["view_pubkey"].is_string()) { - res.status = 400; - res.set_content(err_json("recipient has no view pubkey - they must register pvac first").dump(), "application/json"); - return; - } - std::vector their_vpub_raw = octra::base64_decode(vr.result["view_pubkey"].get()); - if (their_vpub_raw.size() != 32) { - res.status = 400; - res.set_content(err_json("invalid view pubkey").dump(), "application/json"); - return; - } - - uint8_t eph_sk[32], eph_pk[32]; - octra::random_bytes(eph_sk, 32); - crypto_scalarmult_base(eph_pk, eph_sk); - t.step("eph_keygen"); - - auto shared = octra::ecdh_shared_secret(eph_sk, their_vpub_raw.data()); - t.step("ecdh"); - - steps.push_back("[2/8] stealth tag + claim key derivation"); - auto stag = octra::compute_stealth_tag(shared); - std::string stag_hex = octra::hex_encode(stag.data(), 16); - t.step("compute_stealth_tag"); - auto claim_sec = octra::compute_claim_secret(shared); - t.step("compute_claim_secret"); - - auto claim_pub = octra::compute_claim_pub(claim_sec, to); - std::string claim_pub_hex = octra::hex_encode(claim_pub.data(), 32); - t.step("compute_claim_pub"); - - steps.push_back("[3/8] checking encrypted balance"); - auto eb_r = g_rpc.get_encrypted_balance(from_addr, eb_sig, from_pub_b64); - t.reset_step(); - if (!eb_r.ok || !eb_r.result.is_object()) { - res.status = 500; - res.set_content(err_json("failed to fetch encrypted balance").dump(), "application/json"); - return; - } - std::string eb_cipher = eb_r.result.value("cipher", "0"); - if (eb_cipher.empty() || eb_cipher == "0") { + std::vector their_signing_pk; + if (auto cached = pk_cache_get(to)) { + their_signing_pk = *cached; + } else { + auto pr = g_rpc.get_public_key(to); + if (!pr.ok || !pr.result.is_object() || !pr.result.contains("public_key") + || pr.result["public_key"].is_null() || !pr.result["public_key"].is_string()) { res.status = 400; - res.set_content(err_json("no encrypted balance available").dump(), "application/json"); + res.set_content(err_json("recipient has no public key registered").dump(), "application/json"); return; } - - std::lock_guard lock(g_mtx); - t.mutex_acquired(); - PVAC_GUARD - - int64_t eb_decrypted = g_pvac.get_balance(eb_cipher); - t.step("encbal_local_get_balance"); - if (eb_decrypted < raw) { - char buf[128]; - snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", - (long)eb_decrypted, (long)raw); + their_signing_pk = octra::base64_decode(pr.result["public_key"].get()); + if (their_signing_pk.size() != 32) { res.status = 400; - res.set_content(err_json(buf).dump(), "application/json"); + res.set_content(err_json("invalid signing pubkey size").dump(), "application/json"); return; } + pk_cache_put(to, their_signing_pk); + } + uint8_t their_vpub[32]; + if (!octra::ed25519_pub_to_x25519(their_signing_pk.data(), their_vpub)) { + res.status = 400; + res.set_content(err_json("ed25519→x25519 conversion failed").dump(), "application/json"); + return; + } + std::vector their_vpub_raw(their_vpub, their_vpub + 32); - steps.push_back("[4/8] FHE encrypt delta (PVAC-HFHE)"); - ensure_pvac_registered(); - t.step("ensure_pvac_registered"); - uint8_t r_blind[32]; - octra::random_bytes(r_blind, 32); - std::string enc_amount = octra::encrypt_stealth_amount(shared, (uint64_t)raw, r_blind); - t.step("stealth_aes_envelope"); - - uint8_t seed[32]; - octra::random_bytes(seed, 32); - pvac_cipher ct_delta = g_pvac.encrypt((uint64_t)raw, seed); - t.step("pvac_encrypt_delta"); - - std::string delta_cipher_str = g_pvac.encode_cipher(ct_delta); - t.step("encode_delta_cipher"); - - auto commitment = g_pvac.commit_ct(ct_delta); - std::string commitment_b64 = octra::base64_encode(commitment.data(), 32); - t.step("commit_ct_delta+encode"); - - pvac_cipher current_ct = g_pvac.decode_cipher(eb_cipher); - t.step("decode_cipher"); - - pvac_cipher new_ct = g_pvac.ct_sub(current_ct, ct_delta); - t.step("ct_sub"); - - uint64_t new_val = (uint64_t)(eb_decrypted - raw); - pvac_range_proof rp_delta_proof = nullptr; - pvac_range_proof rp_bal_proof = nullptr; - { - auto rp_start = std::chrono::steady_clock::now(); - std::chrono::steady_clock::time_point delta_done{}, bal_done{}; - std::thread thr_delta([&]() { - rp_delta_proof = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), ct_delta, (uint64_t)raw); - delta_done = std::chrono::steady_clock::now(); - }); - std::thread thr_bal([&]() { - rp_bal_proof = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), new_ct, new_val); - bal_done = std::chrono::steady_clock::now(); - }); - thr_delta.join(); - thr_bal.join(); - auto rp_end = std::chrono::steady_clock::now(); - double wall_ms = std::chrono::duration(rp_end - rp_start).count(); - double td_ms = std::chrono::duration(delta_done - rp_start).count(); - double tb_ms = std::chrono::duration(bal_done - rp_start).count(); - char rp_buf[256]; - snprintf(rp_buf, sizeof(rp_buf), - "range_proofs parallel_wall=%.3f ms thread_delta=%.3f ms thread_balance=%.3f ms", - wall_ms, td_ms, tb_ms); - t.step_msg(rp_buf); - } - - steps.push_back("[5/8] range proofs (parallel) - Bulletproofs R1CS"); - - std::string rp_delta_str = g_pvac.encode_range_proof(rp_delta_proof); - t.step("encode_range_proof_delta"); - - std::string rp_bal_str = g_pvac.encode_range_proof(rp_bal_proof); - t.step("encode_range_proof_balance"); - - g_pvac.free_range_proof(rp_delta_proof); - g_pvac.free_range_proof(rp_bal_proof); - g_pvac.free_cipher(current_ct); - g_pvac.free_cipher(new_ct); - g_pvac.free_cipher(ct_delta); - t.step("free_range_proofs+ciphers"); - - steps.push_back("[6/8] encoding proofs"); - - auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_blind); - std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - t.step("pedersen_amount_commitment"); - - steps.push_back("[7/8] Pedersen commitment + AES-GCM envelope"); + try { - json stealth_data; - stealth_data["version"] = 5; - stealth_data["delta_cipher"] = delta_cipher_str; - stealth_data["commitment"] = commitment_b64; - stealth_data["range_proof_delta"] = rp_delta_str; - stealth_data["range_proof_balance"] = rp_bal_str; - stealth_data["eph_pub"] = octra::base64_encode(eph_pk, 32); - stealth_data["stealth_tag"] = stag_hex; - stealth_data["enc_amount"] = enc_amount; - stealth_data["claim_pub"] = claim_pub_hex; - stealth_data["amount_commitment"] = amt_commit_b64; - t.step("build_stealth_json"); + json steps = json::array(); - steps.push_back("[8/8] building stealth transaction"); + steps.push_back("[1/8] ECDH x25519 key exchange"); + uint8_t eph_sk[32], eph_pk[32]; + octra::random_bytes(eph_sk, 32); + crypto_scalarmult_base(eph_pk, eph_sk); + auto shared = octra::ecdh_shared_secret(eph_sk, their_vpub_raw.data()); + + steps.push_back("[2/8] stealth tag + claim key derivation"); + auto stag = octra::compute_stealth_tag(shared); + auto claim_sec = octra::compute_claim_secret(shared); + auto claim_pub = octra::compute_claim_pub(claim_sec, to); + + steps.push_back("[3/8] checking encrypted balance"); + auto eb_r = g_rpc.get_encrypted_balance(from_addr, eb_sig, from_pub_b64); + if (!eb_r.ok || !eb_r.result.is_object()) { + res.status = 500; + res.set_content(err_json("failed to fetch encrypted balance").dump(), "application/json"); + return; + } + std::string eb_cipher = eb_r.result.value("cipher", "0"); + if (eb_cipher.empty() || eb_cipher == "0") { + res.status = 400; + res.set_content(err_json("no encrypted balance available").dump(), "application/json"); + return; + } - auto bi = get_nonce_balance(); - t.step("get_nonce_balance"); + std::lock_guard lock(g_mtx); + PVAC_GUARD + if (!g_wallet_loaded || g_wallet.addr != from_addr) { + res.status = 409; + res.set_content(err_json("wallet state changed during send").dump(), "application/json"); + return; + } - octra::Transaction tx; - tx.from = from_addr; - tx.to_ = "stealth"; - tx.amount = "0"; - tx.nonce = bi.nonce + 1; - tx.ou = parse_ou(body, "5000"); - tx.timestamp = now_ts(); - tx.op_type = "stealth"; - tx.encrypted_data = stealth_data.dump(); - t.step("build_tx+json_dump"); + int64_t eb_decrypted = g_pvac_ok ? g_pvac.get_balance(eb_cipher) : 0; + if (eb_decrypted < raw) { + res.status = 400; + char buf[128]; + snprintf(buf, sizeof(buf), "insufficient encrypted balance: have %ld, need %ld", + (long)eb_decrypted, (long)raw); + res.set_content(err_json(buf).dump(), "application/json"); + return; + } - sign_tx_fields(tx); - t.step("sign_tx"); + steps.push_back("[4/8] FHE encrypt delta (PVAC-HFHE)"); + ensure_pvac_registered(); + uint8_t r_blind[32]; + octra::random_bytes(r_blind, 32); + std::string enc_amount = octra::encrypt_stealth_amount(shared, (uint64_t)raw, r_blind); + uint8_t seed[32]; + octra::random_bytes(seed, 32); + pvac_cipher ct_delta = g_pvac.encrypt((uint64_t)raw, seed); + std::string delta_cipher_str = g_pvac.encode_cipher(ct_delta); + auto commitment = g_pvac.commit_ct(ct_delta); + std::string commitment_b64 = octra::base64_encode(commitment.data(), 32); + + steps.push_back("[5/8] range proofs (parallel) - Bulletproofs R1CS"); + pvac_cipher current_ct = g_pvac.decode_cipher(eb_cipher); + pvac_cipher new_ct = g_pvac.ct_sub(current_ct, ct_delta); + uint64_t new_val = (uint64_t)(eb_decrypted - raw); + + pvac_range_proof rp_delta = nullptr; + pvac_range_proof rp_bal = nullptr; + + std::thread t_rp_delta([&]() { + rp_delta = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), ct_delta, (uint64_t)raw); + }); + std::thread t_rp_bal([&]() { + rp_bal = pvac_make_range_proof(g_pvac.pk(), g_pvac.sk(), new_ct, new_val); + }); + t_rp_delta.join(); + t_rp_bal.join(); + + steps.push_back("[6/8] encoding proofs"); + std::string rp_delta_str = g_pvac.encode_range_proof(rp_delta); + std::string rp_bal_str = g_pvac.encode_range_proof(rp_bal); + g_pvac.free_range_proof(rp_delta); + g_pvac.free_range_proof(rp_bal); + + steps.push_back("[7/8] Pedersen commitment + AES-GCM envelope"); + auto amt_commit = g_pvac.pedersen_commit((uint64_t)raw, r_blind); + std::string amt_commit_b64 = octra::base64_encode(amt_commit.data(), 32); - auto result = submit_tx(tx); - t.step("submit_tx"); + pvac_zero_proof send_zkp = g_pvac.make_zero_proof_bound(ct_delta, (uint64_t)raw, r_blind); + std::string send_zp_str = g_pvac.encode_zero_proof(send_zkp); + g_pvac.free_zero_proof(send_zkp); + + g_pvac.free_cipher(ct_delta); + g_pvac.free_cipher(current_ct); + g_pvac.free_cipher(new_ct); + + steps.push_back("[8/8] building stealth transaction"); + json stealth_data; + stealth_data["version"] = 5; + stealth_data["delta_cipher"] = delta_cipher_str; + stealth_data["commitment"] = commitment_b64; + stealth_data["range_proof_delta"] = rp_delta_str; + stealth_data["range_proof_balance"] = rp_bal_str; + stealth_data["eph_pub"] = octra::base64_encode(eph_pk, 32); + stealth_data["stealth_tag"] = octra::hex_encode(stag.data(), 16); + stealth_data["enc_amount"] = enc_amount; + stealth_data["claim_pub"] = octra::hex_encode(claim_pub.data(), 32); + stealth_data["amount_commitment"] = amt_commit_b64; + stealth_data["send_zero_proof"] = send_zp_str; - if (result.contains("error")) res.status = 500; - result["steps"] = steps; - res.set_content(result.dump(), "application/json"); + auto bi = get_nonce_balance(); int nonce = bi.nonce; + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = "stealth"; + tx.amount = "0"; + tx.nonce = nonce + 1; + tx.ou = parse_ou(body, "5000"); + tx.timestamp = now_ts(); + tx.op_type = "stealth"; + tx.encrypted_data = stealth_data.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + result["steps"] = steps; + res.set_content(result.dump(), "application/json"); } catch (const std::exception& e) { + fprintf(stderr, "[stealth/send] exception: %s\n", e.what()); res.status = 500; res.set_content(err_json(std::string("stealth send failed: ") + e.what()).dump(), "application/json"); } catch (...) { + fprintf(stderr, "[stealth/send] unknown exception\n"); res.status = 500; res.set_content(err_json("stealth send failed: unknown error").dump(), "application/json"); } }); svr.Get("/api/stealth/scan", [](const httplib::Request&, httplib::Response& res) { - octra::ScopedTimer timer("stealth.scan"); WALLET_GUARD uint8_t view_sk[32]; { @@ -1593,8 +1529,9 @@ int main(int argc, char** argv) { }); svr.Post("/api/stealth/claim", [](const httplib::Request& req, httplib::Response& res) { - octra::OpTimer t("claim", "claim started"); WALLET_GUARD + std::lock_guard lock(g_mtx); + PVAC_GUARD json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1606,21 +1543,18 @@ int main(int argc, char** argv) { res.set_content(err_json("ids required").dump(), "application/json"); return; } - std::lock_guard lock(g_mtx); - t.mutex_acquired(); - PVAC_GUARD + uint8_t view_sk[32], view_pk[32]; octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); - t.step("derive_view_keypair"); - auto sr = g_rpc.get_stealth_outputs(0); - t.step("get_stealth_outputs"); if (!sr.ok || !sr.result.is_object()) { res.status = 500; res.set_content(err_json("failed to fetch outputs").dump(), "application/json"); return; } + ensure_pvac_registered(); + std::vector req_ids; for (auto& id : body["ids"]) { if (id.is_string()) req_ids.push_back(id.get()); @@ -1628,13 +1562,7 @@ int main(int argc, char** argv) { } auto bi = get_nonce_balance(); int nonce = bi.nonce; - t.reset_step(); - - ensure_pvac_registered(); - t.step("ensure_pvac_registered"); - json results = json::array(); - char sn[96]; for (auto& out : sr.result["outputs"]) { std::string out_id = out.contains("id") ? @@ -1646,49 +1574,26 @@ int main(int argc, char** argv) { if (!wanted) continue; if (out.value("claimed", 0) != 0) { results.push_back({{"id", out_id}, {"ok", false}, {"error", "already claimed"}}); - t.reset_step(); continue; } try { auto eph_raw = octra::base64_decode(out["eph_pub"].get()); if (eph_raw.size() != 32) throw std::runtime_error("bad eph_pub"); auto shared = octra::ecdh_shared_secret(view_sk, eph_raw.data()); - snprintf(sn, sizeof(sn), "id=%s ecdh", out_id.c_str()); - t.step(sn); - auto dec = octra::decrypt_stealth_amount(shared, out.value("enc_amount", "")); if (!dec.has_value()) throw std::runtime_error("decrypt failed"); - snprintf(sn, sizeof(sn), "id=%s decrypt_stealth_amount", out_id.c_str()); - t.step(sn); - auto cs = octra::compute_claim_secret(shared); - snprintf(sn, sizeof(sn), "id=%s compute_claim_secret", out_id.c_str()); - t.step(sn); uint8_t seed[32]; octra::random_bytes(seed, 32); pvac_cipher ct_claim = g_pvac.encrypt(dec->amount, seed); - snprintf(sn, sizeof(sn), "id=%s pvac_encrypt", out_id.c_str()); - t.step(sn); - std::string claim_cipher_str = g_pvac.encode_cipher(ct_claim); - snprintf(sn, sizeof(sn), "id=%s encode_cipher", out_id.c_str()); - t.step(sn); - auto commit = g_pvac.commit_ct(ct_claim); std::string commit_b64 = octra::base64_encode(commit.data(), 32); - snprintf(sn, sizeof(sn), "id=%s commit_ct+encode", out_id.c_str()); - t.step(sn); - pvac_zero_proof zkp = g_pvac.make_zero_proof_bound(ct_claim, dec->amount, dec->blinding.data()); - snprintf(sn, sizeof(sn), "id=%s zero_proof_make", out_id.c_str()); - t.step(sn); - std::string zp_str = g_pvac.encode_zero_proof(zkp); g_pvac.free_cipher(ct_claim); g_pvac.free_zero_proof(zkp); - snprintf(sn, sizeof(sn), "id=%s zero_proof_encode+free", out_id.c_str()); - t.step(sn); json claim_data; claim_data["version"] = 5; @@ -1697,8 +1602,6 @@ int main(int argc, char** argv) { claim_data["commitment"] = commit_b64; claim_data["claim_secret"] = octra::hex_encode(cs.data(), 32); claim_data["zero_proof"] = zp_str; - snprintf(sn, sizeof(sn), "id=%s build_claim_json", out_id.c_str()); - t.step(sn); nonce++; octra::Transaction tx; @@ -1710,17 +1613,8 @@ int main(int argc, char** argv) { tx.timestamp = now_ts(); tx.op_type = "claim"; tx.encrypted_data = claim_data.dump(); - snprintf(sn, sizeof(sn), "id=%s build_tx+json_dump", out_id.c_str()); - t.step(sn); - sign_tx_fields(tx); - snprintf(sn, sizeof(sn), "id=%s sign_tx", out_id.c_str()); - t.step(sn); - auto sr2 = submit_tx(tx); - snprintf(sn, sizeof(sn), "id=%s submit_tx", out_id.c_str()); - t.step(sn); - if (sr2.contains("error")) { results.push_back({{"id", out_id}, {"ok", false}, {"error", sr2["error"]}}); } else { @@ -1728,7 +1622,6 @@ int main(int argc, char** argv) { } } catch (const std::exception& e) { results.push_back({{"id", out_id}, {"ok", false}, {"error", e.what()}}); - t.reset_step(); } } json j; @@ -1784,17 +1677,14 @@ int main(int argc, char** argv) { svr.Get("/api/keys", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD + uint8_t view_sk[32], view_pk[32]; + octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); json j; - { - std::lock_guard lock(g_mtx); - uint8_t view_sk[32], view_pk[32]; - octra::derive_view_keypair(g_wallet.sk, view_sk, view_pk); - j["address"] = g_wallet.addr; - j["public_key"] = g_wallet.pub_b64; - j["view_pubkey"] = octra::base64_encode(view_pk, 32); - j["has_master_seed"] = g_wallet.has_master_seed(); - octra::secure_zero(view_sk, 32); - } + j["address"] = g_wallet.addr; + j["public_key"] = g_wallet.pub_b64; + j["view_pubkey"] = octra::base64_encode(view_pk, 32); + j["has_master_seed"] = g_wallet.has_master_seed(); + octra::secure_zero(view_sk, 32); res.set_content(j.dump(), "application/json"); }); @@ -1940,8 +1830,9 @@ int main(int argc, char** argv) { res.set_content(err_json("bytecode required").dump(), "application/json"); return; } + int nonce_val = 0; auto bi = get_nonce_balance(); - int nonce_val = bi.nonce + 1; + nonce_val = bi.nonce + 1; auto r = g_rpc.compute_contract_address(bytecode, g_wallet.addr, nonce_val); if (!r.ok) { res.status = 400; @@ -2077,6 +1968,46 @@ int main(int argc, char** argv) { res.set_content(result.dump(), "application/json"); }); + svr.Post("/api/bridge/signer", [](const httplib::Request& req, httplib::Response& res) { + std::string signer_url; + { + std::lock_guard lock(g_mtx); + signer_url = g_wallet.bridge_signer_url; + } + if (signer_url.empty()) { + const char* env_url = std::getenv("OCTRA_BRIDGE_SIGNER_URL"); + if (env_url) signer_url = env_url; + } + if (signer_url.empty()) { + signer_url = "https://relayer-002838819188.octra.network"; + } + try { + auto body = json::parse(req.body); + std::string method = body.value("method", ""); + if (method != "bridgeStatus" && method != "bridgeHeader" && + method != "bridgeMessagesByEpoch" && method != "bridgeProofByLeafIndex" && + method != "bridgeClaimCalldata") { + res.status = 400; + res.set_content("{\"error\":\"method not allowed\"}", "application/json"); + return; + } + } catch (...) { + res.status = 400; + res.set_content("{\"error\":\"invalid json\"}", "application/json"); + return; + } + httplib::Client cli(signer_url); + cli.set_connection_timeout(10, 0); + cli.set_read_timeout(15, 0); + auto r = cli.Post("/", req.body, "application/json"); + if (r && r->status == 200) { + res.set_content(r->body, "application/json"); + } else { + res.status = 502; + res.set_content("{\"error\":\"bridge signer unavailable\"}", "application/json"); + } + }); + svr.Get("/api/contract/view", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::string addr = req.get_param_value("address"); @@ -2202,21 +2133,17 @@ int main(int argc, char** argv) { res.set_content(r.result.dump(), "application/json"); }); + static json g_token_cache; + static double g_token_cache_ts = 0; + static std::string g_token_cache_addr; + svr.Get("/api/tokens", [](const httplib::Request&, httplib::Response& res) { WALLET_GUARD - std::string wallet_addr; - { - std::lock_guard lock(g_mtx); - wallet_addr = g_wallet.addr; - } double now = (double)time(nullptr); - { - std::lock_guard lock(g_token_mtx); - if (!g_token_cache.empty() && g_token_cache_addr == wallet_addr - && (now - g_token_cache_ts) < 30.0) { - res.set_content(g_token_cache.dump(), "application/json"); - return; - } + if (!g_token_cache.empty() && g_token_cache_addr == g_wallet.addr + && (now - g_token_cache_ts) < 30.0) { + res.set_content(g_token_cache.dump(), "application/json"); + return; } auto lr = g_rpc.list_contracts(); json tokens = json::array(); @@ -2231,7 +2158,7 @@ int main(int argc, char** argv) { if (sym.empty() || sym == "0") continue; if (sym.size() > 10) sym = sym.substr(0, 10); auto br = g_rpc.contract_call_view(addr, "balance_of", - json::array({wallet_addr}), wallet_addr); + json::array({g_wallet.addr}), g_wallet.addr); std::string bal = (br.ok && br.result.contains("result") && !br.result["result"].is_null()) ? br.result.value("result", "0") : "0"; if (bal == "0" || bal.empty()) continue; @@ -2259,13 +2186,10 @@ int main(int argc, char** argv) { json j; j["tokens"] = tokens; j["count"] = tokens.size(); - j["wallet_address"] = wallet_addr; - { - std::lock_guard lock(g_token_mtx); - g_token_cache = j; - g_token_cache_ts = now; - g_token_cache_addr = wallet_addr; - } + j["wallet_address"] = g_wallet.addr; + g_token_cache = j; + g_token_cache_ts = now; + g_token_cache_addr = g_wallet.addr; res.set_content(j.dump(), "application/json"); }); @@ -2327,6 +2251,7 @@ int main(int argc, char** argv) { } std::string new_rpc = body.value("rpc_url", ""); std::string new_explorer = body.value("explorer_url", ""); + std::string new_bridge_signer = body.value("bridge_signer_url", ""); if (new_rpc.empty()) { res.status = 400; res.set_content(err_json("rpc_url required").dump(), "application/json"); @@ -2336,6 +2261,7 @@ int main(int argc, char** argv) { try { std::string old_rpc = g_wallet.rpc_url; if (!new_explorer.empty()) g_wallet.explorer_url = new_explorer; + g_wallet.bridge_signer_url = new_bridge_signer; octra::save_settings(g_wallet_path, g_wallet, new_rpc, g_pin); g_rpc.set_url(g_wallet.rpc_url); if (old_rpc != g_wallet.rpc_url) { @@ -2354,6 +2280,7 @@ int main(int argc, char** argv) { j["ok"] = true; j["rpc_url"] = g_wallet.rpc_url; j["explorer_url"] = g_wallet.explorer_url; + j["bridge_signer_url"] = g_wallet.bridge_signer_url; j["cache_cleared"] = cache_cleared; res.set_content(j.dump(), "application/json"); }); diff --git a/rpc_client.hpp b/rpc_client.hpp index 8d99808..98210f7 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -30,146 +30,12 @@ #include #include #include -#include -#include -#include #include "lib/json.hpp" #include "lib/httplib.h" namespace octra { -inline void timing_ms_esc(double ms, const char** open, const char** reset) { - if (ms >= 10000.0) { - *open = "\033[31m"; - *reset = "\033[0m"; - } else if (ms >= 1000.0) { - *open = "\033[33m"; - *reset = "\033[0m"; - } else { - *open = ""; - *reset = ""; - } -} - -inline void get_wall_hms(char* buf, size_t cap) { - using std::chrono::system_clock; - std::time_t t = system_clock::to_time_t(system_clock::now()); - std::tm tm{}; -#ifdef _WIN32 - localtime_s(&tm, &t); -#else - localtime_r(&t, &tm); -#endif - snprintf(buf, cap, "%02d:%02d:%02d", tm.tm_hour, tm.tm_min, tm.tm_sec); -} - -inline void log_event(const char* msg) { - char buf[16]; - get_wall_hms(buf, sizeof(buf)); - fprintf(stderr, "[%s] %s\n", buf, msg); -} - -struct ScopedTimer { - char wall[16]; - const char* label; - const char* dot; - std::chrono::steady_clock::time_point start; - - explicit ScopedTimer(const char* l) : label(l), start(std::chrono::steady_clock::now()) { - const char* p = l; - while (*p && *p != '.') ++p; - dot = *p == '.' ? p : nullptr; - get_wall_hms(wall, sizeof(wall)); - if (dot) - fprintf(stderr, "[%s] [%.*s] %s started\n", wall, (int)(dot - label), label, dot + 1); - else - fprintf(stderr, "[%s] [%s] started\n", wall, label); - } - - ~ScopedTimer() { - double ms = std::chrono::duration( - std::chrono::steady_clock::now() - start).count(); - char wall_now[16]; - get_wall_hms(wall_now, sizeof(wall_now)); - const char* esc; - const char* reset; - timing_ms_esc(ms, &esc, &reset); - if (dot) - fprintf(stderr, "[%s] [%.*s] %s %s(%.3f ms)%s\n", wall_now, (int)(dot - label), label, dot + 1, esc, ms, reset); - else - fprintf(stderr, "[%s] [%s] %s(%.3f ms)%s\n", wall_now, label, esc, ms, reset); - } -}; - -struct OpTimer { - const char* op; - std::chrono::steady_clock::time_point wall_start; - std::chrono::steady_clock::time_point step_start; - std::chrono::steady_clock::time_point op_start; - bool has_op_start; - - explicit OpTimer(const char* name, const char* desc) - : op(name) - , wall_start(std::chrono::steady_clock::now()) - , step_start(wall_start) - , op_start(wall_start) - , has_op_start(false) - { - char tw[16]; get_wall_hms(tw, sizeof(tw)); - fprintf(stderr, "[%s] [%s] %s (0.000 ms)\n", tw, op, desc); - } - - void mutex_acquired() { - auto now = std::chrono::steady_clock::now(); - _log_step("mutex_wait", wall_start, now); - op_start = now; - step_start = now; - has_op_start = true; - } - - void step(const char* name) { - auto now = std::chrono::steady_clock::now(); - _log_step(name, step_start, now); - step_start = now; - } - - void step_msg(const char* msg) { - char tw[16]; get_wall_hms(tw, sizeof(tw)); - fprintf(stderr, "[%s] [%s] %s\n", tw, op, msg); - step_start = std::chrono::steady_clock::now(); - } - - void reset_step() { - step_start = std::chrono::steady_clock::now(); - } - - ~OpTimer() { - auto now = std::chrono::steady_clock::now(); - char tw[16]; get_wall_hms(tw, sizeof(tw)); - const char* esc; const char* reset; - if (has_op_start) { - double ms = std::chrono::duration(now - op_start).count(); - timing_ms_esc(ms, &esc, &reset); - fprintf(stderr, "[%s] [%s] total %s(%.3f ms)%s\n", tw, op, esc, ms, reset); - } - double wall_ms = std::chrono::duration(now - wall_start).count(); - timing_ms_esc(wall_ms, &esc, &reset); - fprintf(stderr, "[%s] [%s] handler_wall_total %s(%.3f ms)%s\n", tw, op, esc, wall_ms, reset); - } - -private: - void _log_step(const char* name, - std::chrono::steady_clock::time_point from, - std::chrono::steady_clock::time_point to) { - double ms = std::chrono::duration(to - from).count(); - char tw[16]; get_wall_hms(tw, sizeof(tw)); - const char* esc; const char* reset; - timing_ms_esc(ms, &esc, &reset); - fprintf(stderr, "[%s] [%s] %s %s(%.3f ms)%s\n", tw, op, name, esc, ms, reset); - } -}; - struct RpcResult { bool ok; nlohmann::json result; @@ -183,33 +49,6 @@ class RpcClient { int port_; std::atomic id_{0}; - static std::string rpc_start_label(const std::string& method, const std::string& hint) { - if (!hint.empty()) - return std::string("calling the contract ") + hint + "..."; - return method + "..."; - } - - static void rpc_log_start(const std::string& method, const std::string& hint = "") { - char tw[16]; - get_wall_hms(tw, sizeof(tw)); - std::string label = rpc_start_label(method, hint); - fprintf(stderr, "[%s] [rpc] %s started\n", tw, label.c_str()); - } - - static void rpc_log_one_line(const std::string& method, double ms, bool ok, const std::string& err, - const std::string& hint = "") { - char tw[16]; - get_wall_hms(tw, sizeof(tw)); - const char* esc; - const char* reset; - timing_ms_esc(ms, &esc, &reset); - std::string label = rpc_start_label(method, hint); - if (ok) - fprintf(stderr, "[%s] [rpc] %s ok %s(%.3f ms)%s\n", tw, label.c_str(), esc, ms, reset); - else - fprintf(stderr, "[%s] [rpc] %s failed %s(%.3f ms)%s: %s\n", tw, label.c_str(), esc, ms, reset, err.c_str()); - } - void parse_url(const std::string& url) { std::string u = url; ssl_ = false; @@ -243,26 +82,30 @@ class RpcClient { RpcResult call(const std::string& method, const nlohmann::json& params = nlohmann::json::array(), - int timeout_sec = 30, - const std::string& hint = "") { - auto t0 = std::chrono::steady_clock::now(); + int timeout_sec = 30) { nlohmann::json req; req["jsonrpc"] = "2.0"; req["method"] = method; req["params"] = params; req["id"] = ++id_; std::string body = req.dump(); - rpc_log_start(method, hint); - auto res = post_json(body, timeout_sec); - double ms = std::chrono::duration( - std::chrono::steady_clock::now() - t0).count(); - if (!res) { - rpc_log_one_line(method, ms, false, "connection failed", hint); - return {false, {}, "connection failed"}; + httplib::Headers hdrs = {{"Content-Type", "application/json"}}; + if (ssl_) { + httplib::SSLClient cli(host_, port_); + cli.set_connection_timeout(timeout_sec, 0); + cli.set_read_timeout(timeout_sec, 0); + cli.enable_server_certificate_verification(false); + auto res = cli.Post(path_, hdrs, body, "application/json"); + if (!res) return {false, {}, "connection failed"}; + return parse_response(res->body); + } else { + httplib::Client cli(host_, port_); + cli.set_connection_timeout(timeout_sec, 0); + cli.set_read_timeout(timeout_sec, 0); + auto res = cli.Post(path_, hdrs, body, "application/json"); + if (!res) return {false, {}, "connection failed"}; + return parse_response(res->body); } - RpcResult out = parse_response(res->body); - rpc_log_one_line(method, ms, out.ok, out.error, hint); - return out; } RpcResult get_balance(const std::string& addr) { @@ -285,6 +128,10 @@ class RpcClient { return call("octra_viewPubkey", {addr}); } + RpcResult get_public_key(const std::string& addr) { + return call("octra_publicKey", {addr}); + } + RpcResult get_encrypted_balance(const std::string& addr, const std::string& sig_b64, const std::string& pub_b64) { @@ -354,8 +201,7 @@ class RpcClient { const std::string& method, const nlohmann::json& params, const std::string& caller) { - return call("contract_call", {addr, method, params, caller}, 15, - "(" + addr + ") with (" + method + ")"); + return call("contract_call", {addr, method, params, caller}, 15); } RpcResult list_contracts() { @@ -378,13 +224,13 @@ class RpcClient { return call("octra_transactionsByAddress", {addr, limit, offset}, 15); } - std::vector call_batch( - const std::vector& methods, - const std::vector& params_list = {}, - int timeout_sec = 10) { - auto t0 = std::chrono::steady_clock::now(); - nlohmann::json batch = nlohmann::json::array(); + std::vector call_batch(const std::vector& methods, + const std::vector& params_list = {}, + int timeout_sec = 10) { size_t count = methods.size(); + std::vector out(count, {false, {}, "no response"}); + if (count == 0) return out; + nlohmann::json batch = nlohmann::json::array(); for (size_t i = 0; i < count; ++i) { nlohmann::json req; req["jsonrpc"] = "2.0"; @@ -394,70 +240,60 @@ class RpcClient { batch.push_back(std::move(req)); } std::string body = batch.dump(); - rpc_log_start("batch(" + std::to_string(count) + ")", ""); - auto res = post_json(body, timeout_sec); - double ms = std::chrono::duration( - std::chrono::steady_clock::now() - t0).count(); - std::vector out(count, {false, {}, "no response"}); - if (!res) { - rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, false, "connection failed"); - return out; - } - try { - auto arr = nlohmann::json::parse(res->body); - if (arr.is_array()) { - for (auto& item : arr) { - if (!item.contains("id") || !item["id"].is_number_integer()) continue; - int id = item["id"].get(); - if (id < 1 || id > static_cast(count)) continue; - if (item.contains("result")) { - out[id - 1] = {true, item["result"], ""}; - } else if (item.contains("error")) { - auto& e = item["error"]; - std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); - out[id - 1] = {false, {}, msg}; - } - } - } - } catch (const std::exception& ex) { - rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, false, std::string("parse error: ") + ex.what()); - return out; - } - rpc_log_one_line("batch(" + std::to_string(count) + ")", ms, true, ""); - return out; - } - -private: - httplib::Result post_json(const std::string& body, int timeout_sec) { httplib::Headers hdrs = {{"Content-Type", "application/json"}}; + std::string resp_body; if (ssl_) { httplib::SSLClient cli(host_, port_); cli.set_connection_timeout(timeout_sec, 0); cli.set_read_timeout(timeout_sec, 0); cli.enable_server_certificate_verification(false); - return cli.Post(path_, hdrs, body, "application/json"); + auto r = cli.Post(path_, hdrs, body, "application/json"); + if (!r) { for (auto& o : out) o.error = "connection failed"; return out; } + resp_body = r->body; + } else { + httplib::Client cli(host_, port_); + cli.set_connection_timeout(timeout_sec, 0); + cli.set_read_timeout(timeout_sec, 0); + auto r = cli.Post(path_, hdrs, body, "application/json"); + if (!r) { for (auto& o : out) o.error = "connection failed"; return out; } + resp_body = r->body; } - httplib::Client cli(host_, port_); - cli.set_connection_timeout(timeout_sec, 0); - cli.set_read_timeout(timeout_sec, 0); - return cli.Post(path_, hdrs, body, "application/json"); - } - - RpcResult parse_response_obj(const nlohmann::json& j) { - if (j.contains("result")) - return {true, j["result"], ""}; - if (j.contains("error")) { - auto& e = j["error"]; - std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); - return {false, {}, msg}; + try { + auto arr = nlohmann::json::parse(resp_body); + if (!arr.is_array()) { + for (auto& o : out) o.error = "batch response not array"; + return out; + } + for (auto& item : arr) { + if (!item.contains("id") || !item["id"].is_number_integer()) continue; + int id = item["id"].get(); + if (id < 1 || id > static_cast(count)) continue; + if (item.contains("result")) { + out[id - 1] = {true, item["result"], ""}; + } else if (item.contains("error")) { + auto& e = item["error"]; + std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); + out[id - 1] = {false, {}, msg}; + } + } + } catch (const std::exception& ex) { + for (auto& o : out) o.error = std::string("parse error: ") + ex.what(); } - return {false, {}, "unknown rpc response"}; + return out; } +private: RpcResult parse_response(const std::string& body) { try { auto j = nlohmann::json::parse(body); - return parse_response_obj(j); + if (j.contains("result")) + return {true, j["result"], ""}; + if (j.contains("error")) { + auto& e = j["error"]; + std::string msg = e.is_object() ? e.value("message", "rpc error") : e.dump(); + return {false, {}, msg}; + } + return {false, {}, "unknown rpc response"}; } catch (const std::exception& ex) { return {false, {}, std::string("parse error: ") + ex.what()}; } diff --git a/setup.bat b/setup.bat index eabb77b..9b6518e 100644 --- a/setup.bat +++ b/setup.bat @@ -2,69 +2,94 @@ chcp 65001 >nul 2>&1 setlocal EnableDelayedExpansion - set "MSYS2_DIR=" -if exist "C:\msys64\usr\bin\bash.exe" ( - set "MSYS2_DIR=C:\msys64" -) -if exist "%USERPROFILE%\msys64\usr\bin\bash.exe" ( - set "MSYS2_DIR=%USERPROFILE%\msys64" -) +if exist "C:\msys64\usr\bin\bash.exe" set "MSYS2_DIR=C:\msys64" +if exist "%USERPROFILE%\msys64\usr\bin\bash.exe" set "MSYS2_DIR=%USERPROFILE%\msys64" +if exist "D:\msys64\usr\bin\bash.exe" set "MSYS2_DIR=D:\msys64" +if exist "C:\tools\msys64\usr\bin\bash.exe" set "MSYS2_DIR=C:\tools\msys64" if defined MSYS2_DIR ( echo [1/3] MSYS2 found at !MSYS2_DIR! goto :install_deps ) -echo [1/3] MSYS2 not found. Installing... +echo [1/3] MSYS2 not found. installing... echo. where winget >nul 2>&1 if %errorlevel% equ 0 ( - echo Installing MSYS2 via winget... + echo installing MSYS2 via winget... winget install --id MSYS2.MSYS2 --accept-source-agreements --accept-package-agreements -e -) else ( - echo winget not available. - echo. - echo Please download and install MSYS2 manually from: - echo https://www.msys2.org/ - echo. - echo After installing, run this script again. - pause - exit /b 1 + goto :verify_msys2 +) + +where choco >nul 2>&1 +if %errorlevel% equ 0 ( + echo installing MSYS2 via chocolatey... + choco install -y msys2 + goto :verify_msys2 +) + +where scoop >nul 2>&1 +if %errorlevel% equ 0 ( + echo installing MSYS2 via scoop... + scoop bucket add extras + scoop install msys2 + goto :verify_msys2 ) -if exist "C:\msys64\usr\bin\bash.exe" ( - set "MSYS2_DIR=C:\msys64" -) else ( - echo MSYS2 installation failed. Please install manually from https://www.msys2.org/ +echo no package manager found (winget / choco / scoop). +echo. +echo please install MSYS2 manually from: +echo https://www.msys2.org/ +echo. +echo after installing, re-run this script. +pause +exit /b 1 + +:verify_msys2 +if exist "C:\msys64\usr\bin\bash.exe" set "MSYS2_DIR=C:\msys64" +if exist "%USERPROFILE%\msys64\usr\bin\bash.exe" set "MSYS2_DIR=%USERPROFILE%\msys64" + +if not defined MSYS2_DIR ( + echo MSYS2 installation did not land in a known location. + echo please install manually from https://www.msys2.org/ pause exit /b 1 ) :install_deps -echo Installing compiler and OpenSSL... -"!MSYS2_DIR!\usr\bin\bash.exe" -lc "pacman -S --noconfirm --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-openssl mingw-w64-x86_64-leveldb make" +echo installing compiler + dependencies (openssl, leveldb)... +"!MSYS2_DIR!\usr\bin\bash.exe" -lc "pacman -Syu --noconfirm --needed" +"!MSYS2_DIR!\usr\bin\bash.exe" -lc "pacman -S --noconfirm --needed mingw-w64-x86_64-gcc mingw-w64-x86_64-openssl mingw-w64-x86_64-leveldb mingw-w64-x86_64-pkgconf make" + +if %errorlevel% neq 0 ( + echo dependency installation failed. + pause + exit /b 1 +) echo. -echo [2/3] Building Octra Wallet... +echo [2/3] building octra wallet... set "WALLET_DIR=%~dp0" -"!MSYS2_DIR!\usr\bin\bash.exe" -lc "export PATH=/mingw64/bin:$PATH && cd '%WALLET_DIR:\=/%' && make clean 2>/dev/null; make" +set "WALLET_DIR_UNIX=%WALLET_DIR:\=/%" + +"!MSYS2_DIR!\usr\bin\bash.exe" -lc "export PATH=/mingw64/bin:$PATH && cd '%WALLET_DIR_UNIX%' && make clean 2>/dev/null; make" if not exist "%WALLET_DIR%octra_wallet.exe" ( echo. - echo Build failed. Please check errors above. + echo build failed. check errors above. pause exit /b 1 ) echo. -echo [3/3] done! +echo [3/3] done echo. echo start the wallet: echo octra_wallet.exe echo. -echo then open http://127.0.0.1:8420 in your browser. +echo then open http://127.0.0.1:8420 in your browser echo. -pause +pause \ No newline at end of file diff --git a/setup.sh b/setup.sh index ff0d49d..027ee87 100755 --- a/setup.sh +++ b/setup.sh @@ -1,13 +1,42 @@ #!/usr/bin/env bash +set -e + +MODE="full" +for arg in "$@"; do + case "$arg" in + --deps-only|--no-build) MODE="deps" ;; + --help|-h) + echo "usage: $0 [--deps-only]" + echo "(no args) install deps + build" + echo "--deps-only install deps only (no make)" + exit 0 + ;; + esac +done OS="$(uname -s)" +if [ "$(id -u)" = "0" ]; then + SUDO="" +else + if command -v sudo &>/dev/null; then + SUDO="sudo" + else + SUDO="" + fi +fi + case "$OS" in Darwin) echo "[1/3] macOS detected" if ! command -v brew &>/dev/null; then - echo "homebrew not found. Installing..." + echo "homebrew not found. installing..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + if [ -d /opt/homebrew/bin ]; then + eval "$(/opt/homebrew/bin/brew shellenv)" + elif [ -d /usr/local/bin ] && [ -x /usr/local/bin/brew ]; then + eval "$(/usr/local/bin/brew shellenv)" + fi fi for pkg in openssl@3 leveldb; do if ! brew list $pkg &>/dev/null; then @@ -17,52 +46,105 @@ case "$OS" in echo "$pkg already installed" fi done - if ! command -v g++ &>/dev/null; then + if ! xcode-select -p &>/dev/null; then echo "installing Xcode command line tools..." xcode-select --install 2>/dev/null || true + echo "a GUI installer may have opened. re-run this script after it finishes." + exit 0 fi ;; Linux) echo "[1/3] linux detected" + if [ -f /etc/os-release ]; then + . /etc/os-release + echo "distro: ${ID:-unknown} ${VERSION_ID:-}" + fi if command -v apt-get &>/dev/null; then - echo "Installing dependencies (apt)..." - sudo apt-get update -qq 2>/dev/null || true - sudo apt-get install -y -qq g++ libssl-dev libleveldb-dev make + echo "installing dependencies (apt)..." + $SUDO apt-get update -qq + $SUDO DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + build-essential g++ libssl-dev libleveldb-dev pkg-config make curl elif command -v dnf &>/dev/null; then - echo "Installing dependencies (dnf)..." - sudo dnf install -y gcc-c++ openssl-devel leveldb-devel make + echo "installing dependencies (dnf)..." + $SUDO dnf install -y gcc-c++ openssl-devel leveldb-devel make pkgconfig + elif command -v yum &>/dev/null; then + echo "installing dependencies (yum)..." + $SUDO yum install -y gcc-c++ openssl-devel leveldb-devel make pkgconfig + elif command -v zypper &>/dev/null; then + echo "installing dependencies (zypper)..." + $SUDO zypper install -y gcc-c++ libopenssl-devel leveldb-devel make pkg-config elif command -v pacman &>/dev/null; then - echo "Installing dependencies (pacman)..." - sudo pacman -S --noconfirm gcc openssl leveldb make + echo "installing dependencies (pacman)..." + $SUDO pacman -S --noconfirm --needed gcc openssl leveldb make pkgconf elif command -v apk &>/dev/null; then - echo "Installing dependencies (apk)..." - sudo apk add g++ openssl-dev leveldb-dev make + echo "installing dependencies (apk)..." + $SUDO apk add --no-cache g++ openssl-dev leveldb-dev make pkgconfig musl-dev linux-headers + elif command -v emerge &>/dev/null; then + echo "installing dependencies (emerge)..." + $SUDO emerge --noreplace dev-libs/openssl dev-libs/leveldb sys-devel/gcc sys-devel/make + elif command -v xbps-install &>/dev/null; then + echo "installing dependencies (xbps)..." + $SUDO xbps-install -Sy gcc openssl-devel leveldb-devel make pkgconf else - echo "Unknown package manager. Please install: g++, libssl-dev, libleveldb-dev, make" + echo "unknown package manager. install manually: g++, libssl-dev, libleveldb-dev, make, pkg-config" exit 1 fi ;; + FreeBSD) + echo "[1/3] FreeBSD detected" + $SUDO pkg install -y gcc openssl leveldb gmake pkgconf + ;; + OpenBSD) + echo "[1/3] OpenBSD detected" + $SUDO pkg_add -I g++ openssl leveldb gmake + ;; + NetBSD) + echo "[1/3] NetBSD detected" + $SUDO pkgin install -y gcc openssl leveldb gmake pkg-config + ;; + MINGW*|MSYS*|CYGWIN*) + echo "detected windows shell ($OS). run setup.bat from cmd.exe instead." + exit 1 + ;; *) - echo "Unsupported OS: $OS" - echo "on windows, use setup.bat instead." + echo "unsupported OS: $OS" + echo "on windows use setup.bat. please install manually: g++, libssl-dev, libleveldb-dev, make" exit 1 ;; esac +if [ "$MODE" = "deps" ]; then + echo "" + echo "[2/2] dependencies installed (deps-only mode)" + exit 0 +fi + echo "" -echo "[2/3] building octra wallet and other things" -make clean 2>/dev/null || true -if ! make; then +echo "[2/3] building octra wallet" + +if ! command -v make &>/dev/null; then + if command -v gmake &>/dev/null; then + MAKE=gmake + else + echo "neither make nor gmake found" + exit 1 + fi +else + MAKE=make +fi + +OCTRA_SKIP_AUTOSETUP=1 $MAKE clean 2>/dev/null || true +if ! OCTRA_SKIP_AUTOSETUP=1 $MAKE; then echo "" - echo "build failed." + echo "build failed" exit 1 fi echo "" -echo "[3/3] done!" +echo "[3/3] done" echo "" echo "start the wallet:" -echo "./octra_wallet" -echo "" -echo "then open http://127.0.0.1:8420 in your browser." +echo " ./octra_wallet" echo "" +echo "then open http://127.0.0.1:8420 in your browser" +echo "" \ No newline at end of file diff --git a/static/bridge.html b/static/bridge.html new file mode 100644 index 0000000..cce887f --- /dev/null +++ b/static/bridge.html @@ -0,0 +1,846 @@ + + + + + + +octra bridge + + + +
+
+

octra bridge

+ v1 experimental +
+ +
+
+
+ + octra + not connected +
+ +
+
+
+ + ethereum + not connected +
+ +
+
+ +
+ + +
+ +
+
+
from
+
octra
+
OCT
+
+
->
+
+
to
+
ethereum
+
wOCT
+
+
+ +
+
lock OCT on octra, receive wOCT on ethereum
+
+ +
+
+ +
+ + +
OCT
+
+
+ +
+ +
+ +
+
+ +
+ +
+ 0 + wOCT +
+
+ +
+
fee0
+
estimated time~2 min
+
+ + +
+ +
+ + +
+
OCT: -
+
wOCT: -
+
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/static/index.html b/static/index.html index d7e4538..abbed67 100644 --- a/static/index.html +++ b/static/index.html @@ -114,6 +114,7 @@
connecting...
+
@@ -280,6 +281,13 @@
switch to a token to see transactions
+
+
apps
+
+ +
+
+
balance: - @@ -483,7 +491,7 @@
-
node settings
+
network settings
@@ -493,6 +501,10 @@
+
+ + +
@@ -538,4 +550,4 @@
- + \ No newline at end of file diff --git a/static/style.css b/static/style.css index 0b42f10..b645a99 100644 --- a/static/style.css +++ b/static/style.css @@ -210,6 +210,14 @@ header .logout-btn:hover { background: #7A8BA3; } .view { display: none; } .view.active { display: block; } + + + +#view-apps.active { + padding: clamp(16px, 2.5vw, 32px) clamp(12px, 3vw, 40px); + box-sizing: border-box; +} + .section { margin: 0; padding: 0; diff --git a/static/wallet.js b/static/wallet.js index 6f73cbb..d1e0722 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -25,9 +25,6 @@ 2025-2026 Julia L. */ - -// mini-IDE pop up promt window with all things inside [lambda0xe] - function idePrompt(title, message, defaultVal) { return new Promise(function(resolve) { var ov = document.createElement('div'); @@ -2347,6 +2344,7 @@ async function loadSettings() { var w = await api('GET', '/wallet'); $('settings-rpc').value = w.rpc_url || 'http://46.101.86.250:8080'; $('settings-explorer').value = w.explorer_url || 'https://octrascan.io'; + $('settings-bridge-signer').value = w.bridge_signer_url || 'https://relayer-002838819188.octra.network'; } catch (e) {} loadAccountList(); } @@ -2534,9 +2532,10 @@ async function doSaveSettings() { clearResult('settings-result'); var rpc = $('settings-rpc').value.trim(); var explorer = $('settings-explorer').value.trim(); + var bridgeSigner = $('settings-bridge-signer').value.trim(); if (!rpc) { showResult('settings-result', false, 'rpc url required'); return; } try { - var resp = await api('POST', '/settings', { rpc_url: rpc, explorer_url: explorer }); + var resp = await api('POST', '/settings', { rpc_url: rpc, explorer_url: explorer, bridge_signer_url: bridgeSigner }); if (explorer) _explorerUrl = explorer.replace(/\/+$/, ''); try { _rpcHost = new URL(rpc).hostname; } catch(e) { _rpcHost = rpc; } if (resp && resp.cache_cleared) { @@ -2815,6 +2814,7 @@ async function loadWalletInfo() { $('hdr-addr').innerHTML = '' + _walletAddr + ''; $('hdr-logout').style.display = ''; $('hdr-dev').style.display = ''; + $('hdr-apps').style.display = ''; fetchFees(); loadDashboard(); } catch (e) { @@ -2833,6 +2833,7 @@ async function doLogout() { _hasMasterSeed = false; $('hdr-logout').style.display = 'none'; $('hdr-dev').style.display = 'none'; + $('hdr-apps').style.display = 'none'; $('hdr-addr').textContent = 'locked'; $('hdr-status').textContent = 'locked'; $('hdr-status').className = 'right'; @@ -2943,4 +2944,4 @@ $('modal-pin-confirm').addEventListener('keydown', function(e) { }); initEditor(); -init(); +init(); \ No newline at end of file diff --git a/wallet.hpp b/wallet.hpp index e269fcd..eb42b6e 100644 --- a/wallet.hpp +++ b/wallet.hpp @@ -59,6 +59,7 @@ struct Wallet { std::string addr; std::string rpc_url; std::string explorer_url = "https://octrascan.io"; + std::string bridge_signer_url; uint8_t sk[64]; uint8_t pk[32]; std::string pub_b64; @@ -226,6 +227,7 @@ inline void save_wallet_encrypted(const std::string& path, j["addr"] = w.addr; j["rpc"] = w.rpc_url; j["explorer"] = w.explorer_url; + j["bridge_signer"] = w.bridge_signer_url; if (!w.master_seed_b64.empty()) { j["master_seed"] = w.master_seed_b64; j["hd_index"] = w.hd_index; @@ -269,6 +271,7 @@ inline Wallet load_wallet_encrypted(const std::string& path, w.addr = j.at("addr").get(); w.rpc_url = j.value("rpc", "http://46.101.86.250:8080"); w.explorer_url = j.value("explorer", "https://octrascan.io"); + w.bridge_signer_url = j.value("bridge_signer", ""); w.master_seed_b64 = j.value("master_seed", ""); w.mnemonic = j.value("mnemonic", ""); w.hd_index = j.value("hd_index", 0); @@ -553,4 +556,5 @@ inline std::vector scan_and_merge_oct_files() { #endif return entries; } + } \ No newline at end of file From fc8a857ccaa9eb49aa616f7abe57d207e48a1811 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:41:59 +0000 Subject: [PATCH 05/13] added history for bridge, recovery, and search for lost txs --- Makefile | 20 +- main.cpp | 7 +- setup.sh | 12 +- static/bridge.html | 571 ++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 542 insertions(+), 68 deletions(-) diff --git a/Makefile b/Makefile index f3aa985..9cc419a 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ else ifneq ($(IS_WIN),) SHARED_EXT:=a SHARED_FLAGS:= SSL_PREFIX:=$(shell echo $$MINGW_PREFIX) +LDB_PREFIX:=$(shell echo $$MINGW_PREFIX) CXXFLAGS+=-I$(SSL_PREFIX)/include -DCPPHTTPLIB_OPENSSL_SUPPORT LDFLAGS:=-static -L$(SSL_PREFIX)/lib -L$(PVAC_BUILD) -lpvac -lleveldb -lssl -lcrypto -lws2_32 -lbcrypt -lcrypt32 -lgdi32 -lz TARGET:=octra_wallet.exe @@ -74,8 +75,8 @@ LIBPVAC:=$(PVAC_BUILD)/libpvac.$(SHARED_EXT) all: check-deps $(TARGET) -LEVELDB_PATHS:=/usr/include/leveldb/db.h /usr/local/include/leveldb/db.h /opt/homebrew/include/leveldb/db.h /opt/local/include/leveldb/db.h $(LDB_PREFIX)/include/leveldb/db.h -OPENSSL_PATHS:=/usr/include/openssl/evp.h /usr/local/include/openssl/evp.h /opt/homebrew/include/openssl/evp.h /opt/local/include/openssl/evp.h $(SSL_PREFIX)/include/openssl/evp.h +LEVELDB_PATHS:=/usr/include/leveldb/db.h /usr/local/include/leveldb/db.h /opt/homebrew/include/leveldb/db.h /opt/local/include/leveldb/db.h /mingw64/include/leveldb/db.h /ucrt64/include/leveldb/db.h /clang64/include/leveldb/db.h $(LDB_PREFIX)/include/leveldb/db.h +OPENSSL_PATHS:=/usr/include/openssl/evp.h /usr/local/include/openssl/evp.h /opt/homebrew/include/openssl/evp.h /opt/local/include/openssl/evp.h /mingw64/include/openssl/evp.h /ucrt64/include/openssl/evp.h /clang64/include/openssl/evp.h $(SSL_PREFIX)/include/openssl/evp.h HAVE_LEVELDB:=$(shell for p in $(LEVELDB_PATHS); do [ -f "$$p" ] && { echo yes; exit 0; }; done; echo no) HAVE_OPENSSL:=$(shell for p in $(OPENSSL_PATHS); do [ -f "$$p" ] && { echo yes; exit 0; }; done; echo no) @@ -92,19 +93,22 @@ else ./setup.sh --deps-only || { \ echo ''; \ echo 'auto-install failed. install manually:'; \ - echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ - echo 'brew install leveldb openssl@3 (macos)'; \ + echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ + echo 'brew install leveldb openssl@3 (macos)'; \ + echo 'setup.bat from cmd.exe (windows)'; \ exit 1; \ }; \ else \ echo 'setup.sh not found. install manually:'; \ - echo ' sudo apt install libleveldb-dev libssl-dev'; \ + echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ + echo 'brew install leveldb openssl@3 (macos)'; \ + echo 'setup.bat from cmd.exe (windows)'; \ exit 1; \ fi @ok=no; for p in $(LEVELDB_PATHS); do [ -f "$$p" ] && ok=yes; done; \ - [ "$$ok" = "yes" ] || { echo 'error: leveldb still missing after setup.sh'; exit 1; } + [ "$$ok" = "yes" ] || { echo 'error: leveldb still missing. on windows run setup.bat from cmd.exe'; exit 1; } @ok=no; for p in $(OPENSSL_PATHS); do [ -f "$$p" ] && ok=yes; done; \ - [ "$$ok" = "yes" ] || { echo 'error: openssl still missing after setup.sh'; exit 1; } + [ "$$ok" = "yes" ] || { echo 'error: openssl still missing. on windows run setup.bat from cmd.exe'; exit 1; } endif else @true @@ -140,4 +144,4 @@ clean: run: $(TARGET) ./$(TARGET) 8420 -.PHONY: all clean run check-deps +.PHONY: all clean run check-deps \ No newline at end of file diff --git a/main.cpp b/main.cpp index e2f1800..972bfe0 100644 --- a/main.cpp +++ b/main.cpp @@ -374,7 +374,12 @@ int main(int argc, char** argv) { res.set_header("X-Frame-Options", "DENY"); res.set_header("X-Content-Type-Options", "nosniff"); res.set_header("Content-Security-Policy", - "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"); + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "connect-src 'self' http://127.0.0.1:* http://178.62.60.204:8090 https://*.octra.network https://*.publicnode.com https://*.infura.io wss: ws:; " + "frame-ancestors 'none'"); res.set_header("Cache-Control", "no-store"); }); diff --git a/setup.sh b/setup.sh index 027ee87..2136a49 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ case "$OS" in if command -v apt-get &>/dev/null; then echo "installing dependencies (apt)..." $SUDO apt-get update -qq - $SUDO DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + $SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ build-essential g++ libssl-dev libleveldb-dev pkg-config make curl elif command -v dnf &>/dev/null; then echo "installing dependencies (dnf)..." @@ -103,6 +103,12 @@ case "$OS" in $SUDO pkgin install -y gcc openssl leveldb gmake pkg-config ;; MINGW*|MSYS*|CYGWIN*) + if [ "$MODE" = "deps" ]; then + echo "[1/1] detected windows shell ($OS) in deps-only mode" + echo "on windows, dependencies should be installed via setup.bat from cmd.exe" + echo "if you already ran setup.bat, this is fine — continuing" + exit 0 + fi echo "detected windows shell ($OS). run setup.bat from cmd.exe instead." exit 1 ;; @@ -144,7 +150,7 @@ echo "" echo "[3/3] done" echo "" echo "start the wallet:" -echo " ./octra_wallet" +echo "./octra_wallet" echo "" echo "then open http://127.0.0.1:8420 in your browser" -echo "" \ No newline at end of file +echo "" diff --git a/static/bridge.html b/static/bridge.html index cce887f..a02b9aa 100644 --- a/static/bridge.html +++ b/static/bridge.html @@ -3,7 +3,7 @@ - + octra bridge @@ -174,6 +193,15 @@

octra bridge

+ +
OCT: -
wOCT: -
@@ -219,6 +247,7 @@

connect wallet

var WOCT_ADDR = '0x4647e1fE715c9e23959022C2416C71867F5a6E80'; var ETH_BRIDGE = '0xE7eD69b852fd2a1406080B26A37e8E04e7dA4caE'; var SIGNER_URL = '/api/bridge/signer'; +var RECOVERY_URL = 'https://relayer-002838819188.octra.network/recovery.json'; var SEPOLIA_CHAIN_ID = '0xaa36a7'; var OCT_DECIMALS = 6; @@ -312,8 +341,15 @@

connect wallet

try { localStorage.setItem('bridge_eth_wallet', w.name); } catch(e) {} await refreshBalances(); validateForm(); + try { recoveryFetch(true); } catch(e) {} _ethProvider.on('accountsChanged', function(accs) { - if (accs.length) { _ethAddr = accs[0]; $('eth-addr').textContent = _ethAddr.substring(0, 8) + '...' + _ethAddr.slice(-4); refreshBalances(); validateForm(); } + if (accs.length) { + _ethAddr = accs[0]; + $('eth-addr').textContent = _ethAddr.substring(0, 8) + '...' + _ethAddr.slice(-4); + refreshBalances(); + validateForm(); + try { recoveryFetch(true); } catch(e) {} + } }); } catch(e) { showStatus('err', w.name + ': ' + e.message); } } @@ -420,6 +456,8 @@

connect wallet

var _pendingClaim = null; +var _activeHistoryId = null; + async function doForward() { var amt = $('bridge-amount').value.trim(); var recip = $('recipient').value.trim(); @@ -429,7 +467,7 @@

connect wallet

clearStatus(); _pendingClaim = null; - try { localStorage.removeItem('bridge_pending_claim'); } catch(e) {} + _activeHistoryId = null; var oldClaimBtn = $('claim-btn'); if (oldClaimBtn) oldClaimBtn.remove(); showProgress([ @@ -446,8 +484,24 @@

connect wallet

if (!r.tx_hash) throw new Error('no tx_hash'); setStep('lock', 'done'); setStep('confirm', 'active'); showStatus('info', 'locked! ' + r.tx_hash.substring(0, 16) + '...'); + + _activeHistoryId = 'lock_' + r.tx_hash.substring(0, 10) + '_' + Date.now(); + historyAdd({ + id: _activeHistoryId, + locked_at: Date.now(), + lock_tx_hash: r.tx_hash, + epoch: 0, + recipient: recip, + amount_raw: rawAmt, + amt_display: amt, + status: 'pending_header' + }); + var receipt = await waitReceipt(r.tx_hash, 60); - if (!receipt || !receipt.success) throw new Error('lock transaction failed'); + if (!receipt || !receipt.success) { + if (_activeHistoryId) historyUpdate(_activeHistoryId, {last_error:'lock tx not confirmed in 60s, check refresh status later'}); + throw new Error('lock transaction failed'); + } setStep('confirm', 'done'); setStep('header', 'active'); showStatus('info', 'OCT locked. waiting for bridge header (~1-2 min)...'); @@ -457,13 +511,15 @@

connect wallet

epochId = txInfo.epoch || 0; } + if (_activeHistoryId) historyUpdate(_activeHistoryId, {epoch: epochId}); + var claimData = await waitForClaimData(epochId, recip, rawAmt); - if (!claimData) throw new Error('bridge header not found. try claim later.'); + if (!claimData) throw new Error('bridge header not yet available, pls check history below - it will auto resume when header lands on eth'); showStatus('info', 'signer returned header. verifying it landed on ethereum...'); var simOk = false; var simAttempts = 0; - var simMaxAttempts = 18; + var simMaxAttempts = 60; var lastSimErr = ''; while (simAttempts < simMaxAttempts) { try { @@ -481,11 +537,13 @@

connect wallet

} } if (!simOk) { - throw new Error('header not verified on ethereum after 90s. last error: ' + lastSimErr); + if (_activeHistoryId) historyUpdate(_activeHistoryId, {claim_data:claimData, last_error:lastSimErr}); + throw new Error('header not verified on ethereum after 5 min. your lock is in history below - click "refresh status" there once relayer submits. last error: ' + lastSimErr); } setStep('header', 'done'); setStep('claim', 'active'); _pendingClaim = claimData; + if (_activeHistoryId) historyUpdate(_activeHistoryId, {status:'claimable', claim_data:claimData}); showClaimButton(amt); showStatus('info', 'bridge header verified on ethereum. click button to claim your wOCT:'); } catch(e) { showStatus('err', e.message); setCurrentStepFail(); } @@ -503,7 +561,6 @@

connect wallet

btn.style.cssText = 'width:100%;padding:12px;font-size:13px;font-weight:600;border:none;cursor:pointer;background:#3B567F;color:#fff;margin-top:10px;font-family:Tahoma,arial,sans-serif'; btn.onclick = doClaim; area.appendChild(btn); - try { localStorage.setItem('bridge_pending_claim', JSON.stringify({ claim: _pendingClaim, amt: amt })); } catch(e) {} } async function waitForReceipt(txHash, timeoutMs) { @@ -561,7 +618,7 @@

connect wallet

}); } catch(simErr) { if (claimBtn) { claimBtn.disabled = false; claimBtn.textContent = 'claim on ethereum (retry)'; } - showStatus('err', 'claim would revert right now. header may have been evicted or state changed. pending claim preserved — click button to retry in a few seconds.'); + showStatus('err', 'claim would revert right now. header may have been evicted or state changed. pending claim preserved - click button to retry in a few seconds.'); return; } @@ -584,13 +641,13 @@

connect wallet

if (!receipt) { if (claimBtn) { claimBtn.disabled = false; claimBtn.textContent = 'claim on ethereum (retry)'; } - showStatus('err', 'tx not confirmed in 5 min. it may still land later, or was dropped. pending claim is preserved — refresh page to retry.'); + showStatus('err', 'tx not confirmed in 5 min. it may still land later, or was dropped. pending claim is preserved - refresh page to retry.'); return; } if (receipt.status !== '0x1') { if (claimBtn) { claimBtn.disabled = false; claimBtn.textContent = 'claim on ethereum (retry)'; } - showStatus('err', 'tx reverted on-chain. view on etherscan — pending claim preserved, click button to retry.'); + showStatus('err', 'tx reverted on-chain. view on etherscan - pending claim preserved, click button to retry.'); return; } @@ -598,63 +655,444 @@

connect wallet

if (claimBtn) claimBtn.remove(); showStatus('ok', 'wOCT claimed! view on etherscan'); _pendingClaim = null; - try { localStorage.removeItem('bridge_pending_claim'); } catch(e) {} + if (_activeHistoryId) { + historyUpdate(_activeHistoryId, {status:'claimed', claim_tx_hash:claimTx}); + _activeHistoryId = null; + } await refreshBalances(); } -async function checkPendingClaim() { +function historyLoad() { + try { return JSON.parse(localStorage.getItem('bridge_history') || '[]'); } catch(e) { return []; } +} + +function historySave(arr) { + try { localStorage.setItem('bridge_history', JSON.stringify(arr.slice(0, 50))); } catch(e) {} +} + +var _historyShowAll = false; +var _historyPageSize = 5; + +function historyToggleShowAll() { + _historyShowAll = !_historyShowAll; + historyRender(); +} + +function historyGet(id) { + var arr = historyLoad(); + for (var i = 0; i < arr.length; i++) if (arr[i].id === id) return arr[i]; + return null; +} + +function historyAdd(entry) { + var arr = historyLoad(); + arr.unshift(entry); + historySave(arr); + historyRender(); + return entry; +} + +function historyUpdate(id, patch) { + var arr = historyLoad(); + for (var i = 0; i < arr.length; i++) { + if (arr[i].id === id) { + for (var k in patch) arr[i][k] = patch[k]; + break; + } + } + historySave(arr); + historyRender(); +} + +function historyRemove(id) { + var arr = historyLoad().filter(function(e) { return e.id !== id; }); + historySave(arr); + historyRender(); +} + +function historyRender() { + var arr = historyLoad(); + var sec = document.getElementById('history-section'); + var list = document.getElementById('history-list'); + if (!sec || !list) return; + sec.style.display = 'block'; + list.innerHTML = ''; + if (arr.length === 0) { + list.innerHTML = '
no bridge activity yet - your locks will appear here
'; + return; + } + var labels = { + pending_header: 'waiting', claimable: 'claim', claiming: 'claiming...', + claimed: 'claimed', expired: 'expired', failed: 'failed', + burning: 'burning', burn_pending: 'unlocking...', unlocked: 'unlocked' + }; + var visible = _historyShowAll ? arr : arr.slice(0, _historyPageSize); + for (var i = 0; i < visible.length; i++) { + var e = visible[i]; + var d = new Date(e.locked_at); + var hh = String(d.getHours()).padStart(2, '0'); + var mm = String(d.getMinutes()).padStart(2, '0'); + var toShort = e.recipient ? (e.recipient.substr(0, 6) + '...' + e.recipient.substr(-4)) : ''; + var cls = 'pending'; + if (e.status === 'claimable') cls = 'claimable'; + else if (e.status === 'claimed' || e.status === 'unlocked') cls = 'claimed'; + else if (e.status === 'expired') cls = 'expired'; + else if (e.status === 'failed') cls = 'failed'; + else if (e.status === 'claiming' || e.status === 'burn_pending') cls = 'claiming'; + var isReverse = e.direction === 'e2o'; + var srcToken = isReverse ? 'wOCT' : 'OCT'; + var topLine = '
' + e.amt_display + ' ' + srcToken + ' -> ' + toShort + '
'; + var midLine; + if (isReverse) { + var burnLink = e.burn_tx_hash ? ('' + hh + ':' + mm + '') : (hh + ':' + mm); + var burnTag = e.burn_tx_hash ? (' | burn ' + e.burn_tx_hash.slice(0, 8) + '...') : ''; + var unlockInfo = (e.status === 'unlocked' && e.recipient) ? (' | view') : ''; + midLine = '
' + burnLink + burnTag + unlockInfo + '
'; + } else { + var lockLink = e.lock_tx_hash ? ('' + hh + ':' + mm + '') : (hh + ':' + mm); + var claimInfo = e.claim_tx_hash ? (' | view') : ''; + var epTag = e.epoch ? (' | ep ' + e.epoch) : ' | ep pending'; + midLine = '
' + lockLink + epTag + claimInfo + '
'; + } + var row = document.createElement('div'); + row.className = 'hs-item'; + row.innerHTML = + '
' + topLine + midLine + '
' + + '
' + (labels[e.status] || e.status) + '
'; + var statusEl = row.querySelector('.hs-status'); + if (e.status === 'claimable') { + statusEl.onclick = (function(id) { return function() { historyClaim(id); }; })(e.id); + } + list.appendChild(row); + } + if (arr.length > _historyPageSize) { + var toggle = document.createElement('div'); + toggle.style.cssText = 'text-align:center;padding:8px 0;font-size:10px;color:#3B567F;cursor:pointer;border-top:1px solid #F0F2F5;margin-top:4px'; + if (_historyShowAll) { + toggle.textContent = 'show less'; + } else { + toggle.textContent = 'show all (' + (arr.length - _historyPageSize) + ' more)'; + } + toggle.onclick = historyToggleShowAll; + list.appendChild(toggle); + } +} + +function historyClearOld() { + var arr = historyLoad(); + var now = Date.now(); + var kept = arr.filter(function(e) { + if (e.status === 'claimed' || e.status === 'unlocked') return now - e.locked_at < 86400000; + if (e.status === 'expired' || e.status === 'failed') return false; + return true; + }); + historySave(kept); + historyRender(); +} + +async function historyCheckE2o(entry) { + if (!entry.burn_tx_hash) { historyUpdate(entry.id, {last_checked:Date.now()}); return; } + if (!_ethProvider) { historyUpdate(entry.id, {last_checked:Date.now()}); return; } try { - var saved = localStorage.getItem('bridge_pending_claim'); - if (!saved) return; - var data = JSON.parse(saved); - _pendingClaim = data.claim; - showProgress([ - { id: 'lock', text: 'OCT locked' }, - { id: 'confirm', text: 'confirmed' }, - { id: 'header', text: 'header on ethereum' }, - { id: 'claim', text: 'claim wOCT' } - ]); - setStep('lock', 'done'); setStep('confirm', 'done'); setStep('header', 'active'); - showStatus('info', 'restoring pending claim. verifying header on ethereum...'); - - if (!_ethProvider || !_ethAddr) { - showStatus('info', 'pending claim restored. connect metamask to verify header and claim.'); + var r = await _ethProvider.request({method:'eth_getTransactionReceipt', params:[entry.burn_tx_hash]}); + if (!r) { historyUpdate(entry.id, {last_checked:Date.now()}); return; } + if (r.status === '0x0') { + historyUpdate(entry.id, {status:'failed', last_error:'burn tx reverted', last_checked:Date.now()}); return; } + if (entry.status === 'burning') { + historyUpdate(entry.id, {status:'burn_pending', last_checked:Date.now(), last_error:null}); + } else { + historyUpdate(entry.id, {last_checked:Date.now()}); + } + } catch(e) { + historyUpdate(entry.id, {last_checked:Date.now(), last_error:(e.message || 'receipt check failed')}); + } +} - var simOk = false; - var simAttempts = 0; - var simMaxAttempts = 6; - var lastErr = ''; - while (simAttempts < simMaxAttempts) { +async function historyCheckOne(entry) { + if (entry.status === 'claimed' || entry.status === 'claiming' || entry.status === 'unlocked') return; + if (Date.now() - entry.locked_at > 86400000) { + if (entry.status !== 'expired') historyUpdate(entry.id, {status:'expired', last_checked:Date.now()}); + return; + } + if (entry.direction === 'e2o') return historyCheckE2o(entry); + if (!entry.epoch && entry.lock_tx_hash) { + try { + var txi = await wcli('GET', '/transaction?hash=' + entry.lock_tx_hash); + if (txi && txi.epoch) { + historyUpdate(entry.id, {epoch: txi.epoch}); + entry.epoch = txi.epoch; + } else { + historyUpdate(entry.id, {last_checked:Date.now(), last_error:'tx not finalized yet'}); + return; + } + } catch(e) { + historyUpdate(entry.id, {last_checked:Date.now(), last_error:'epoch lookup failed'}); + return; + } + } + if (!entry.epoch) return; + try { + var rpcBody = JSON.stringify({jsonrpc:'2.0',id:1,method:'bridgeHeader',params:[entry.epoch]}); + var resp = await fetch(SIGNER_URL, {method:'POST', headers:{'Content-Type':'application/json'}, body:rpcBody}).catch(function(){return null;}); + if (!resp || !resp.ok) { historyUpdate(entry.id, {last_checked:Date.now(), last_error:'signer unreachable'}); return; } + var data = await resp.json(); + if (!data.result || !data.result.message_count) { historyUpdate(entry.id, {last_checked:Date.now()}); return; } + var claimData = await buildClaimCalldata(entry.epoch, entry.recipient, entry.amount_raw, data.result); + if (!claimData) { historyUpdate(entry.id, {last_checked:Date.now(), last_error:'no claim calldata'}); return; } + if (_ethProvider && _ethAddr) { try { - await _ethProvider.request({ - method: 'eth_call', - params: [{ from: _ethAddr, to: ETH_BRIDGE, data: _pendingClaim.calldata }, 'latest'] - }); - simOk = true; - break; - } catch(e) { - lastErr = (e && (e.message || JSON.stringify(e))) || 'unknown'; - simAttempts++; - showStatus('info', 'header not yet on ethereum. retrying ' + simAttempts + '/' + simMaxAttempts + '...'); - await new Promise(function(r) { setTimeout(r, 5000); }); + await _ethProvider.request({method:'eth_call', params:[{from:_ethAddr, to:ETH_BRIDGE, data:claimData.calldata}, 'latest']}); + historyUpdate(entry.id, {status:'claimable', claim_data:claimData, last_checked:Date.now(), last_error:null}); + } catch(err) { + var m = (err && (err.message || String(err))) || ''; + var d = (err && err.data) ? (typeof err.data === 'string' ? err.data : (err.data.data || err.data.originalError && err.data.originalError.data || '')) : ''; + var ml = m.toLowerCase(); + var isReplay = ml.indexOf('already') >= 0 || ml.indexOf('replay') >= 0 || d === '0xb5a78004' || m.indexOf('0xb5a78004') >= 0; + var isUnknownHeader = d === '0xa2ad39b9' || m.indexOf('0xa2ad39b9') >= 0; + var isCapExceeded = d === '0xa4875a49' || m.indexOf('0xa4875a49') >= 0; + var isInvalidProof = d === '0x09bde339' || m.indexOf('0x09bde339') >= 0; + if (isReplay) { + historyUpdate(entry.id, {status:'claimed', last_checked:Date.now(), last_error:null}); + } else if (isUnknownHeader) { + historyUpdate(entry.id, {claim_data:claimData, last_checked:Date.now(), last_error:'header not yet on ethereum'}); + } else if (isCapExceeded) { + historyUpdate(entry.id, {claim_data:claimData, last_checked:Date.now(), last_error:'bridge mint cap too low for this amount'}); + } else if (isInvalidProof) { + historyUpdate(entry.id, {claim_data:claimData, last_checked:Date.now(), last_error:'merkle proof invalid (signer out of sync)'}); + } else { + historyUpdate(entry.id, {claim_data:claimData, last_checked:Date.now(), last_error:m}); + } } + } else { + historyUpdate(entry.id, {claim_data:claimData, last_checked:Date.now()}); } - if (!simOk) { - setCurrentStepFail(); - showStatus('err', 'header not verified on ethereum. pending claim kept — try again later (last: ' + lastErr + ')'); + } catch(e) { + historyUpdate(entry.id, {last_checked:Date.now(), last_error:e.message}); + } +} + +async function historyCheckAll() { + var arr = historyLoad(); + for (var i = 0; i < arr.length; i++) { + var e = arr[i]; + if (e.status === 'claimed' || e.status === 'expired' || e.status === 'claiming' || e.status === 'unlocked') continue; + await historyCheckOne(e); + } +} + +var _historyPollTimer = null; +var _historyPollBusy = false; + +function historyHasActive() { + var arr = historyLoad(); + for (var i = 0; i < arr.length; i++) { + var s = arr[i].status; + if (s !== 'claimed' && s !== 'unlocked' && s !== 'expired' && s !== 'failed') return true; + } + return false; +} + +function startHistoryAutoPoll() { + if (_historyPollTimer) return; + _historyPollTimer = setInterval(async function() { + if (_historyPollBusy) return; + if (document.hidden) return; + if (!historyHasActive()) return; + _historyPollBusy = true; + try { await historyCheckAll(); } catch(e) {} + _historyPollBusy = false; + }, 10000); +} + +async function historyRefreshAll() { + showStatus('info', 'refreshing history...'); + await historyCheckAll(); + var arr = historyLoad(); + var pending = arr.filter(function(e){return e.status==='pending_header';}).length; + var claimable = arr.filter(function(e){return e.status==='claimable';}).length; + showStatus('info', 'history refreshed: ' + claimable + ' claimable, ' + pending + ' waiting'); +} + +async function historyClaim(id) { + var entry = historyGet(id); + if (!entry) return; + if (!entry.claim_data) { showStatus('err', 'no claim data cached, click refresh status'); return; } + if (!_ethProvider || !_ethAddr) { showStatus('err', 'connect metamask first'); return; } + historyUpdate(id, {status:'claiming'}); + showStatus('info', 'submitting claim: ' + entry.amt_display + ' wOCT ep ' + entry.epoch); + try { + try { + await _ethProvider.request({method:'eth_call', params:[{from:_ethAddr, to:ETH_BRIDGE, data:entry.claim_data.calldata}, 'latest']}); + } catch(simErr) { + var em = (simErr && (simErr.message || String(simErr))) || ''; + var ed = (simErr && simErr.data) ? (typeof simErr.data === 'string' ? simErr.data : (simErr.data.data || simErr.data.originalError && simErr.data.originalError.data || '')) : ''; + var eml = em.toLowerCase(); + var simReplay = eml.indexOf('already') >= 0 || eml.indexOf('replay') >= 0 || ed === '0xb5a78004' || em.indexOf('0xb5a78004') >= 0; + if (simReplay) { + historyUpdate(id, {status:'claimed', last_error:null}); + showStatus('info', 'already claimed on-chain (detected during sim). marked as claimed.'); + await refreshBalances(); + return; + } + historyUpdate(id, {status:'claimable', last_error:em}); + showStatus('err', 'claim would revert: ' + em); return; } - setStep('header', 'done'); setStep('claim', 'active'); - showStatus('info', 'bridge header verified. click button to claim:'); - showClaimButton(data.amt); - } catch(e) { showStatus('err', 'failed to restore pending claim: ' + e.message); } + var gasFees = await getSafeGas(); + var txReq = {from:_ethAddr, to:ETH_BRIDGE, data:entry.claim_data.calldata}; + for (var k in gasFees) txReq[k] = gasFees[k]; + var txHash = await _ethProvider.request({method:'eth_sendTransaction', params:[txReq]}); + historyUpdate(id, {claim_tx_hash:txHash}); + showStatus('info', 'tx submitted: ' + txHash.slice(0,10) + '...'); + var receipt = await waitForReceipt(txHash, 300000); + if (!receipt) { + historyUpdate(id, {status:'claimable'}); + showStatus('err', 'tx not confirmed in 5 min, will retry on refresh'); + return; + } + if (receipt.status === '0x0') { + historyUpdate(id, {status:'claimable', last_error:'tx reverted'}); + showStatus('err', 'tx reverted. view'); + return; + } + historyUpdate(id, {status:'claimed', claim_tx_hash:txHash}); + showStatus('ok', 'wOCT claimed! view'); + await refreshBalances(); + } catch(e) { + historyUpdate(id, {status:'claimable', last_error:e.message}); + showStatus('err', 'claim failed: ' + e.message); + } +} + +function historyMigrateLegacy() { + try { + var oldLock = localStorage.getItem('bridge_pending_lock'); + if (oldLock) { + var l = JSON.parse(oldLock); + if (!historyGet('legacy_lock_' + l.epoch)) { + historyAdd({ + id: 'legacy_lock_' + l.epoch + '_' + Date.now(), + locked_at: l.started_at || Date.now(), + lock_tx_hash: l.lock_tx_hash || '', + epoch: l.epoch, + recipient: l.recipient, + amount_raw: l.amount_raw, + amt_display: l.amt_display || String(l.amount_raw), + status: 'pending_header' + }); + } + localStorage.removeItem('bridge_pending_lock'); + } + } catch(e) {} + try { + var oldClaim = localStorage.getItem('bridge_pending_claim'); + if (oldClaim) { + var c = JSON.parse(oldClaim); + var ep = c.claim && c.claim.epoch_id ? c.claim.epoch_id : 0; + if (ep) { + historyAdd({ + id: 'legacy_claim_' + ep + '_' + Date.now(), + locked_at: Date.now(), + lock_tx_hash: '', + epoch: ep, + recipient: _ethAddr || '', + amount_raw: 0, + amt_display: c.amt || '?', + status: 'claimable', + claim_data: c.claim + }); + } + localStorage.removeItem('bridge_pending_claim'); + } + } catch(e) {} +} + +function historyHasMessage(mid, epoch, recipLower, amountRaw) { + if (!mid && !(epoch && recipLower && amountRaw)) return false; + var arr = historyLoad(); + for (var i = 0; i < arr.length; i++) { + var e = arr[i]; + if (mid && e.message_id && e.message_id.toLowerCase() === mid.toLowerCase()) return true; + if (mid && e.claim_data && e.claim_data.message && e.claim_data.message.message_id && e.claim_data.message.message_id.toLowerCase() === mid.toLowerCase()) return true; + if (epoch && recipLower && amountRaw) { + if (e.epoch === epoch && (e.recipient || '').toLowerCase() === recipLower && String(e.amount_raw) === String(amountRaw)) return true; + } + } + return false; +} + +async function recoveryFetch(silent) { + if (!_ethAddr) { + if (!silent) showStatus('err', 'connect metamask first to scan for recoverable bridges'); + return 0; + } + var target = _ethAddr.toLowerCase(); + try { + if (!silent) showStatus('info', 'scanning recovery feed...'); + var resp = await fetch(RECOVERY_URL, {method:'GET', cache:'no-store'}); + if (!resp.ok) { if (!silent) showStatus('err', 'recovery feed unreachable (' + resp.status + ')'); return 0; } + var data = await resp.json(); + var by = data && data.by_recipient ? data.by_recipient : {}; + var bucket = by[target] || by[_ethAddr] || []; + if (!Array.isArray(bucket) || bucket.length === 0) { + if (!silent) showStatus('info', 'no recoverable bridges found for ' + _ethAddr.substring(0,8) + '...' + _ethAddr.slice(-4)); + return 0; + } + var added = 0; + for (var i = 0; i < bucket.length; i++) { + var m = bucket[i]; + if (!m || typeof m !== 'object') continue; + var mid = m.message_id || ''; + var ep = typeof m.epoch === 'number' ? m.epoch : parseInt(m.epoch, 10); + var amtRaw = String(m.amount_raw || '0'); + if (!ep || !amtRaw || amtRaw === '0') continue; + if (historyHasMessage(mid, ep, target, amtRaw)) continue; + var amtDisplay = fmtU(amtRaw, OCT_DECIMALS); + var lockedAt = m.found_at ? (m.found_at * 1000) : Date.now(); + historyAdd({ + id: 'recovered_' + (mid ? mid.substring(2, 12) : ep + '_' + i) + '_' + Date.now(), + locked_at: lockedAt, + lock_tx_hash: m.tx_hash || '', + epoch: ep, + recipient: _ethAddr, + amount_raw: amtRaw, + amt_display: amtDisplay, + status: 'pending_header', + message_id: mid, + recovered: true + }); + added += 1; + } + if (added > 0) { + if (!silent) showStatus('ok', 'imported ' + added + ' recoverable bridge' + (added === 1 ? '' : 's') + ', checking status...'); + await historyCheckAll(); + var arr2 = historyLoad(); + var claimable = arr2.filter(function(e){return e.status==='claimable';}).length; + if (!silent) showStatus('ok', 'recovery done: ' + claimable + ' ready to claim now'); + } else { + if (!silent) showStatus('info', 'all found bridges are already in your history'); + } + return added; + } catch(e) { + if (!silent) showStatus('err', 'recovery failed: ' + (e.message || 'unknown')); + return 0; + } +} + +async function checkPendingClaim() { + try { + historyMigrateLegacy(); + historyRender(); + await historyCheckAll(); + startHistoryAutoPoll(); + } catch(e) { showStatus('err', 'history check failed: ' + e.message); } } async function waitForClaimData(epochId, recipient, rawAmt) { var start = Date.now(); - while (Date.now() - start < 180000) { + while (Date.now() - start < 300000) { try { var rpcBody = JSON.stringify({jsonrpc:'2.0',id:1,method:'bridgeHeader',params:[epochId]}); var resp = await fetch(SIGNER_URL, { @@ -710,6 +1148,7 @@

connect wallet

{ id: 'unlock', text: 'unlocking OCT on octra...' } ]); setStep('approve', 'active'); + var burnHistoryId = null; try { var gas1 = await getSafeGas(); var approveData = '0x095ea7b3' + ETH_BRIDGE.substring(2).toLowerCase().padStart(64, '0') + BigInt(rawAmt).toString(16).padStart(64, '0'); @@ -719,7 +1158,7 @@

connect wallet

}); var approveReceipt = await waitForReceipt(approveTx, 300000); if (!approveReceipt || approveReceipt.status !== '0x1') { - showStatus('err', 'approve tx failed or dropped. your wOCT is still in the wallet — try again.'); + showStatus('err', 'approve tx failed or dropped. your wOCT is still in the wallet - try again.'); setCurrentStepFail(); btn.classList.remove('loading'); validateForm(); return; @@ -734,21 +1173,36 @@

connect wallet

method: 'eth_sendTransaction', params: [{ from: _ethAddr, to: ETH_BRIDGE, data: burnData, gas: '0x40000', maxFeePerGas: gas2.maxFeePerGas, maxPriorityFeePerGas: gas2.maxPriorityFeePerGas }] }); + burnHistoryId = 'burn_' + burnTx.slice(2, 10) + '_' + Date.now(); + historyAdd({ + id: burnHistoryId, + direction: 'e2o', + locked_at: Date.now(), + burn_tx_hash: burnTx, + approve_tx_hash: approveTx, + recipient: recip, + amount_raw: rawAmt, + amt_display: amt, + status: 'burning' + }); var explorerBase2 = (typeof ETH_EXPLORER !== 'undefined' && ETH_EXPLORER) ? ETH_EXPLORER : 'https://etherscan.io'; showStatus('info', 'burn tx submitted: ' + burnTx.slice(0, 10) + '... waiting for confirmation'); var burnReceipt = await waitForReceipt(burnTx, 300000); if (!burnReceipt) { - showStatus('err', 'burn tx not confirmed in 5 min. may still land later or was dropped. your wOCT is still in the wallet — try again.'); + showStatus('err', 'burn tx not confirmed in 5 min. may still land later or was dropped. your wOCT is still in the wallet - try again.'); setCurrentStepFail(); + if (burnHistoryId) historyUpdate(burnHistoryId, {status:'failed', last_error:'not confirmed in 5 min'}); btn.classList.remove('loading'); validateForm(); return; } if (burnReceipt.status !== '0x1') { - showStatus('err', 'burn tx reverted on-chain. view on etherscan — your wOCT is still in the wallet.'); + showStatus('err', 'burn tx reverted on-chain. view on etherscan - your wOCT is still in the wallet.'); setCurrentStepFail(); + if (burnHistoryId) historyUpdate(burnHistoryId, {status:'failed', last_error:'tx reverted'}); btn.classList.remove('loading'); validateForm(); return; } + if (burnHistoryId) historyUpdate(burnHistoryId, {status:'burn_pending'}); setStep('burn', 'done'); setStep('unlock', 'active'); showStatus('info', 'wOCT burned. waiting for OCT unlock on octra... view tx'); var prevOct = _octBalance; @@ -756,11 +1210,16 @@

connect wallet

if (unlocked) { setStep('unlock', 'done'); showStatus('ok', 'OCT unlocked! view on octra'); + if (burnHistoryId) historyUpdate(burnHistoryId, {status:'unlocked'}); await refreshBalances(); } else { showStatus('info', 'wOCT burned. OCT unlock may take a few minutes.'); } - } catch(e) { showStatus('err', e.message); setCurrentStepFail(); } + } catch(e) { + showStatus('err', e.message); + setCurrentStepFail(); + if (burnHistoryId) historyUpdate(burnHistoryId, {status:'failed', last_error:(e.message || 'unknown')}); + } btn.classList.remove('loading'); validateForm(); } From 0c1303c6cd2abc804cb8097570604357ce3f082b Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Thu, 23 Apr 2026 23:43:37 +0000 Subject: [PATCH 06/13] added history for bridge, recovery, and search for lost txs --- Makefile | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 9cc419a..2ef2f6b 100644 --- a/Makefile +++ b/Makefile @@ -93,16 +93,16 @@ else ./setup.sh --deps-only || { \ echo ''; \ echo 'auto-install failed. install manually:'; \ - echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ - echo 'brew install leveldb openssl@3 (macos)'; \ - echo 'setup.bat from cmd.exe (windows)'; \ + echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ + echo 'brew install leveldb openssl@3 (macos)'; \ + echo 'setup.bat from cmd.exe (windows)'; \ exit 1; \ }; \ else \ echo 'setup.sh not found. install manually:'; \ - echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ - echo 'brew install leveldb openssl@3 (macos)'; \ - echo 'setup.bat from cmd.exe (windows)'; \ + echo 'sudo apt install libleveldb-dev libssl-dev (debian/ubuntu)'; \ + echo 'brew install leveldb openssl@3 (macos)'; \ + echo 'setup.bat from cmd.exe (windows)'; \ exit 1; \ fi @ok=no; for p in $(LEVELDB_PATHS); do [ -f "$$p" ] && ok=yes; done; \ From f9c73e1341c32606cc72d361be2580f4fce9fb05 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Fri, 15 May 2026 21:31:40 +0000 Subject: [PATCH 07/13] mimi updates --- crypto_utils.hpp | 7 - lib/pvac_bridge.hpp | 8 +- lib/txcache.hpp | 44 +- main.cpp | 777 +++++++- pvac/include/pvac/core/pvac_compress.hpp | 15 +- pvac/include/pvac/core/types.hpp | 56 +- pvac/include/pvac/crypto/keygen.hpp | 12 +- pvac/include/pvac/crypto/ristretto255.hpp | 5 +- pvac/include/pvac/ops/arithmetic.hpp | 4 +- pvac/include/pvac/ops/decrypt.hpp | 17 +- pvac/include/pvac/ops/range_proof.hpp | 27 +- pvac/include/pvac/ops/verify_zero_circuit.hpp | 4 +- pvac/pvac_c_api.cpp | 76 +- pvac/pvac_c_api.h | 3 + pvac/pvac_serialize.hpp | 63 +- rpc_client.hpp | 26 +- setup.sh | 8 +- static/bridge.html | 171 +- static/circles.html | 1694 +++++++++++++++++ static/index.html | 13 +- static/style.css | 14 +- static/swap.js | 9 +- static/wallet.js | 674 ++++++- 23 files changed, 3449 insertions(+), 278 deletions(-) create mode 100644 static/circles.html diff --git a/crypto_utils.hpp b/crypto_utils.hpp index a7c0b73..089f1f0 100644 --- a/crypto_utils.hpp +++ b/crypto_utils.hpp @@ -267,10 +267,6 @@ inline void ed25519_pk_to_curve25519(const uint8_t ed_sk[64], uint8_t x_pk[32]) crypto_scalarmult_base(x_pk, x_sk); } - - -// dont touch - inline bool ed25519_pub_to_x25519(const uint8_t ed_pub[32], uint8_t x_pub[32]) { BN_CTX* ctx = BN_CTX_new(); BIGNUM *p = BN_new(), *y = BN_new(), *one = BN_new(); @@ -317,9 +313,6 @@ inline bool ed25519_pub_to_x25519(const uint8_t ed_pub[32], uint8_t x_pub[32]) { return ok; } - -// !! - inline void secure_zero(void* ptr, size_t len) { volatile uint8_t* p = static_cast(ptr); while (len--) *p++ = 0; diff --git a/lib/pvac_bridge.hpp b/lib/pvac_bridge.hpp index 8059f73..2b8c9cd 100644 --- a/lib/pvac_bridge.hpp +++ b/lib/pvac_bridge.hpp @@ -125,13 +125,17 @@ class PvacBridge { std::array commit_ct(pvac_cipher ct) { std::array out; - pvac_commit_ct(pk_, ct, out.data()); + size_t out_len = 0; + int rc = pvac_commit_ct_v2(pk_, ct, out.data(), out.size(), &out_len); + if (rc != 0 || out_len != out.size()) throw std::runtime_error("pvac_commit_ct_v2 failed"); return out; } std::array pedersen_commit(uint64_t amount, const uint8_t blinding[32]) { std::array out; - pvac_pedersen_commit(amount, blinding, out.data()); + size_t out_len = 0; + int rc = pvac_pedersen_commit_v2(amount, blinding, out.data(), out.size(), &out_len); + if (rc != 0 || out_len != out.size()) throw std::runtime_error("pvac_pedersen_commit_v2 failed"); return out; } diff --git a/lib/txcache.hpp b/lib/txcache.hpp index 054f1f9..2f1d3d0 100644 --- a/lib/txcache.hpp +++ b/lib/txcache.hpp @@ -25,7 +25,6 @@ 2025-2026 Julia L. */ - #pragma once #include #include @@ -73,6 +72,17 @@ class TxCache { } } + void ensure_schema(const std::string& schema) { + auto stored = get("meta:schema"); + if (stored != schema) { + if (!stored.empty()) + fprintf(stderr, "txcache: schema mismatch (%s != %s), clearing\n", + stored.c_str(), schema.c_str()); + clear(); + put("meta:schema", schema); + } + } + void put(const std::string& key, const std::string& val) { if (db_) db_->Put(leveldb::WriteOptions(), key, val); } @@ -92,39 +102,40 @@ class TxCache { put("total:" + addr, std::to_string(total)); } - void store_tx(const nlohmann::json& tx) { + void store_tx(const std::string& addr, const nlohmann::json& tx) { std::string hash = tx.value("hash", ""); - if (hash.empty()) return; + if (hash.empty() || addr.empty()) return; put("tx:" + hash, tx.dump()); double ts = tx.value("timestamp", 0.0); char idx[128]; - snprintf(idx, sizeof(idx), "idx:%020.6f:%s", 9999999999.0 - ts, hash.c_str()); + snprintf(idx, sizeof(idx), "idx:%s:%020.6f:%s", addr.c_str(), 9999999999.0 - ts, hash.c_str()); put(idx, hash); } - void store_txs(const nlohmann::json& txs) { + void store_txs(const std::string& addr, const nlohmann::json& txs) { if (!db_) return; leveldb::WriteBatch batch; for (auto& tx : txs) { std::string hash = tx.value("hash", ""); - if (hash.empty()) continue; + if (hash.empty() || addr.empty()) continue; batch.Put("tx:" + hash, tx.dump()); double ts = tx.value("timestamp", 0.0); - char idx[128]; - snprintf(idx, sizeof(idx), "idx:%020.6f:%s", 9999999999.0 - ts, hash.c_str()); + char idx[192]; + snprintf(idx, sizeof(idx), "idx:%s:%020.6f:%s", addr.c_str(), 9999999999.0 - ts, hash.c_str()); batch.Put(idx, hash); } db_->Write(leveldb::WriteOptions(), &batch); } - nlohmann::json load_page(int limit, int offset) { + nlohmann::json load_page(const std::string& addr, int limit, int offset) { nlohmann::json result = nlohmann::json::array(); - if (!db_) return result; + if (!db_ || addr.empty()) return result; + std::string prefix = "idx:" + addr + ":"; auto it = db_->NewIterator(leveldb::ReadOptions()); int pos = 0; - for (it->Seek("idx:"); it->Valid(); it->Next()) { + for (it->Seek(prefix); it->Valid(); it->Next()) { auto k = it->key().ToString(); - if (k.substr(0, 4) != "idx:") break; + if (k.compare(0, prefix.size(), prefix) != 0) break; if (pos < offset) { pos++; continue; } auto hash = it->value().ToString(); std::string val; @@ -138,12 +149,13 @@ class TxCache { return result; } - int count_idx() { - if (!db_) return 0; + int count_idx(const std::string& addr) { + if (!db_ || addr.empty()) return 0; + std::string prefix = "idx:" + addr + ":"; int n = 0; auto it = db_->NewIterator(leveldb::ReadOptions()); - for (it->Seek("idx:"); it->Valid(); it->Next()) { - if (it->key().ToString().substr(0, 4) != "idx:") break; + for (it->Seek(prefix); it->Valid(); it->Next()) { + if (it->key().ToString().compare(0, prefix.size(), prefix) != 0) break; n++; } delete it; diff --git a/main.cpp b/main.cpp index 972bfe0..4938ec7 100644 --- a/main.cpp +++ b/main.cpp @@ -83,6 +83,27 @@ static nlohmann::json g_fee_cache; static double g_fee_cache_ts = 0.0; static std::mutex g_fee_mtx; +struct HistoryRuntimeState { + double last_top_refresh_ts = 0.0; + json rejected = json::array(); + int total = 0; + std::unordered_map pages; + std::unordered_map page_ts; +}; + +static std::unordered_map g_history_runtime; +static std::mutex g_history_runtime_mtx; + +struct TokenHistoryRuntimeState { + double ts = 0.0; + json rows = json::array(); + int incoming = 0; + int outgoing = 0; +}; + +static std::unordered_map g_token_history_runtime; +static std::mutex g_token_history_runtime_mtx; + static std::unordered_map> g_pk_cache; static std::mutex g_pk_mtx; @@ -93,6 +114,13 @@ static std::optional> pk_cache_get(const std::string& addr) return it->second; } +static std::string current_public_rpc_url() { + if (g_wallet_loaded) return g_wallet.rpc_url; + const char* env_rpc = std::getenv("OCTRA_RPC_URL"); + if (env_rpc && *env_rpc) return env_rpc; + return "http://127.0.0.1:8080"; +} + static void pk_cache_put(const std::string& addr, const std::vector& pk) { if (pk.size() != 32) return; std::lock_guard lk(g_pk_mtx); @@ -120,6 +148,120 @@ static json err_json(const std::string& msg) { return {{"error", msg}}; } +static bool tx_status_is_pending_like(const json& tx) { + const std::string status = tx.value("status", "pending"); + return status.empty() || status == "pending"; +} + +static json history_tx_from_lookup(const json& lookup, const json& fallback) { + json tx = fallback; + tx["hash"] = lookup.value("tx_hash", fallback.value("hash", "")); + tx["from"] = lookup.value("from", fallback.value("from", "")); + tx["to_"] = lookup.value("to", lookup.value("to_", fallback.value("to_", fallback.value("to", "")))); + tx["amount_raw"] = lookup.value("amount_raw", lookup.value("amount", fallback.value("amount_raw", "0"))); + tx["op_type"] = lookup.value("op_type", fallback.value("op_type", "standard")); + tx["status"] = lookup.value("status", fallback.value("status", "pending")); + + double ts = fallback.value("timestamp", 0.0); + if (lookup.contains("timestamp") && lookup["timestamp"].is_number()) + ts = lookup["timestamp"].get(); + else if (lookup.contains("rejected_at") && lookup["rejected_at"].is_number()) + ts = lookup["rejected_at"].get(); + tx["timestamp"] = ts; + + if (lookup.contains("message") && lookup["message"].is_string() && !lookup["message"].get().empty()) + tx["message"] = lookup["message"]; + if (lookup.contains("encrypted_data") && lookup["encrypted_data"].is_string() && !lookup["encrypted_data"].get().empty()) + tx["encrypted_data"] = lookup["encrypted_data"]; + if (lookup.contains("epoch")) + tx["epoch"] = lookup["epoch"]; + else if (lookup.contains("epoch_id")) + tx["epoch"] = lookup["epoch_id"]; + if (lookup.contains("block_height")) + tx["block_height"] = lookup["block_height"]; + + if (lookup.contains("error") && lookup["error"].is_object()) { + tx["reject_reason"] = lookup["error"].value("reason", ""); + tx["reject_type"] = lookup["error"].value("type", ""); + } else { + tx.erase("reject_reason"); + tx.erase("reject_type"); + } + return tx; +} + +static bool reconcile_history_rows(const std::string& addr, json& txs) { + if (!txs.is_array() || txs.empty()) return false; + std::vector methods; + std::vector params_list; + std::vector positions; + for (size_t i = 0; i < txs.size(); ++i) { + if (!txs[i].is_object() || !tx_status_is_pending_like(txs[i])) continue; + const std::string hash = txs[i].value("hash", ""); + if (hash.empty()) continue; + methods.push_back("octra_transaction"); + params_list.push_back(json::array({hash})); + positions.push_back(i); + } + if (methods.empty()) return false; + auto results = g_rpc.call_batch(methods, params_list, 10); + bool changed = false; + for (size_t i = 0; i < results.size() && i < positions.size(); ++i) { + if (!results[i].ok || !results[i].result.is_object()) continue; + const std::string status = results[i].result.value("status", ""); + if (status.empty() || status == "pending") continue; + json updated = history_tx_from_lookup(results[i].result, txs[positions[i]]); + txs[positions[i]] = updated; + if (g_txcache.is_open()) g_txcache.store_tx(addr, updated); + changed = true; + } + return changed; +} + +static HistoryRuntimeState history_runtime_get(const std::string& addr) { + std::lock_guard lk(g_history_runtime_mtx); + auto it = g_history_runtime.find(addr); + if (it == g_history_runtime.end()) return {}; + return it->second; +} + +static void history_runtime_put(const std::string& addr, const HistoryRuntimeState& state) { + std::lock_guard lk(g_history_runtime_mtx); + g_history_runtime[addr] = state; +} + +static void history_runtime_clear(const std::string& addr) { + std::lock_guard lk(g_history_runtime_mtx); + g_history_runtime.erase(addr); +} + +static void history_runtime_clear_all() { + std::lock_guard lk(g_history_runtime_mtx); + g_history_runtime.clear(); +} + +static std::optional token_history_runtime_get(const std::string& addr) { + std::lock_guard lk(g_token_history_runtime_mtx); + auto it = g_token_history_runtime.find(addr); + if (it == g_token_history_runtime.end()) return std::nullopt; + return it->second; +} + +static void token_history_runtime_put(const std::string& addr, const TokenHistoryRuntimeState& state) { + std::lock_guard lk(g_token_history_runtime_mtx); + g_token_history_runtime[addr] = state; +} + +static void token_history_runtime_clear(const std::string& addr) { + std::lock_guard lk(g_token_history_runtime_mtx); + g_token_history_runtime.erase(addr); +} + +static void token_history_runtime_clear_all() { + std::lock_guard lk(g_token_history_runtime_mtx); + g_token_history_runtime.clear(); +} + static std::string parse_ou(const json& body, const std::string& fallback) { std::string val = body.value("ou", ""); if (val.empty()) return fallback; @@ -217,7 +359,33 @@ static json submit_tx(const octra::Transaction& tx) { auto r = g_rpc.submit_tx(j); if (!r.ok) return err_json(r.error); json res; - res["tx_hash"] = r.result.value("tx_hash", ""); + std::string tx_hash = r.result.value("tx_hash", ""); + res["tx_hash"] = tx_hash; + if (!tx_hash.empty()) { + json cached; + cached["hash"] = tx_hash; + cached["from"] = tx.from; + cached["to_"] = tx.to_; + cached["amount_raw"] = tx.amount; + cached["op_type"] = tx.op_type.empty() ? "standard" : tx.op_type; + cached["status"] = "pending"; + cached["timestamp"] = tx.timestamp; + if (!tx.encrypted_data.empty()) cached["encrypted_data"] = tx.encrypted_data; + if (!tx.message.empty()) cached["message"] = tx.message; + if (g_txcache.is_open()) { + bool known = g_txcache.has_tx(tx_hash); + g_txcache.store_tx(g_wallet.addr, cached); + if (!known) { + int cached_total = g_txcache.get_total(g_wallet.addr); + g_txcache.set_total(g_wallet.addr, cached_total + 1); + } + } + history_runtime_clear(g_wallet.addr); + token_history_runtime_clear(g_wallet.addr); + } else { + history_runtime_clear(g_wallet.addr); + token_history_runtime_clear(g_wallet.addr); + } return res; } @@ -298,12 +466,12 @@ static EncBalResult get_encrypted_balance() { } static void init_wallet_subsystems() { + const char* env_rpc = std::getenv("OCTRA_RPC_URL"); + if (env_rpc && *env_rpc) g_wallet.rpc_url = env_rpc; g_rpc.set_url(g_wallet.rpc_url); - ensure_pubkey_registered(g_wallet.addr, g_wallet.sk, g_wallet.pub_b64); g_pvac_ok = g_pvac.init(g_wallet.priv_b64); if (g_pvac_ok) { fprintf(stderr, "pvac initialized\n"); - ensure_pvac_registered(); } else { fprintf(stderr, "pvac init failed (libpvac not loaded?)\n"); } @@ -311,6 +479,7 @@ static void init_wallet_subsystems() { std::string cache_path = "data/txcache_" + g_wallet.addr.substr(3, 8); if (g_txcache.open(cache_path)) { fprintf(stderr, "txcache opened: %s\n", cache_path.c_str()); + g_txcache.ensure_schema("v2_addr_idx_slim_history"); g_txcache.ensure_rpc(g_wallet.rpc_url); } else { fprintf(stderr, "txcache open failed: %s\n", cache_path.c_str()); @@ -363,23 +532,33 @@ int main(int argc, char** argv) { httplib::Server svr; svr.set_read_timeout(300, 0); svr.set_write_timeout(300, 0); - - - // svr.set_keep_alive_timeout(5); svr.set_keep_alive_max_count(100); - // - svr.set_post_routing_handler([](const httplib::Request&, httplib::Response& res) { - res.set_header("X-Frame-Options", "DENY"); + svr.set_post_routing_handler([](const httplib::Request& req, httplib::Response& res) { + bool is_circle_resource = req.path.rfind("/oct/", 0) == 0; + if (!is_circle_resource) { + res.set_header("X-Frame-Options", "DENY"); + } res.set_header("X-Content-Type-Options", "nosniff"); - res.set_header("Content-Security-Policy", - "default-src 'self'; " - "script-src 'self' 'unsafe-inline'; " - "style-src 'self' 'unsafe-inline'; " - "img-src 'self' data: https:; " - "connect-src 'self' http://127.0.0.1:* http://178.62.60.204:8090 https://*.octra.network https://*.publicnode.com https://*.infura.io wss: ws:; " - "frame-ancestors 'none'"); + if (is_circle_resource) { + res.set_header("Content-Security-Policy", + "default-src 'self' data: blob:; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: blob: https:; " + "connect-src 'none'; " + "object-src 'none'; " + "base-uri 'self'"); + } else { + res.set_header("Content-Security-Policy", + "default-src 'self'; " + "script-src 'self' 'unsafe-inline'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "connect-src 'self' http://127.0.0.1:* http://178.62.60.204:8090 https://*.octra.network https://*.publicnode.com https://*.infura.io wss: ws:; " + "frame-ancestors 'none'"); + } res.set_header("Cache-Control", "no-store"); }); @@ -576,7 +755,7 @@ int main(int argc, char** argv) { } g_pin = pin; octra::try_mlock(&g_pin[0], g_pin.size()); - fprintf(stderr, "wallet created: %s → %s\n", g_wallet.addr.c_str(), g_wallet_path.c_str()); + fprintf(stderr, "wallet created: %s -> %s\n", g_wallet.addr.c_str(), g_wallet_path.c_str()); init_wallet_subsystems(); } catch (const std::exception& e) { res.status = 500; @@ -699,6 +878,7 @@ int main(int argc, char** argv) { j["has_master_seed"] = g_wallet.has_master_seed(); j["hd_index"] = g_wallet.hd_index; j["hd_version"] = g_wallet.hd_version; + res.set_header("Access-Control-Allow-Origin", "*"); res.set_content(j.dump(), "application/json"); }); @@ -945,6 +1125,7 @@ int main(int argc, char** argv) { } if (g_pvac_foreign) j["pvac_foreign"] = true; + res.set_header("Access-Control-Allow-Origin", "*"); res.set_content(j.dump(), "application/json"); }); @@ -953,6 +1134,13 @@ int main(int argc, char** argv) { int limit = 20, offset = 0; if (req.has_param("limit")) limit = std::stoi(req.get_param_value("limit")); if (req.has_param("offset")) offset = std::stoi(req.get_param_value("offset")); + if (limit < 1) limit = 1; + if (limit > 500) limit = 500; + if (offset < 0) offset = 0; + const std::string addr = g_wallet.addr; + const double now = now_ts(); + const std::string page_key = std::to_string(limit) + ":" + std::to_string(offset); + HistoryRuntimeState runtime = history_runtime_get(addr); auto convert_row = [](const json& row, const std::string& status) -> json { json tx; @@ -960,68 +1148,261 @@ int main(int argc, char** argv) { tx["from"] = row.value("from", ""); tx["to_"] = row.value("to", row.value("to_", "")); tx["amount_raw"] = row.value("amount", row.value("amount_raw", "0")); - tx["op_type"] = row.value("op_type", "standard"); + const std::string op_type = row.value("op_type", "standard"); + tx["op_type"] = op_type; tx["status"] = status; if (row.contains("timestamp")) tx["timestamp"] = row["timestamp"]; - if (row.contains("encrypted_data") && row["encrypted_data"].is_string()) - tx["encrypted_data"] = row["encrypted_data"]; - if (row.contains("message") && row["message"].is_string()) - tx["message"] = row["message"]; + const std::string enc_tag = row.value("encrypted_data", ""); + if (op_type == "call" && enc_tag == "transfer") { + tx["encrypted_data"] = "transfer"; + if (row.contains("message") && row["message"].is_string()) + tx["message"] = row["message"]; + } if (row.contains("reason") && row["reason"].is_string()) tx["reject_reason"] = row["reason"]; return tx; }; json txs = json::array(); + json rejected = json::array(); + int total = 0; + bool served = false; + + auto runtime_page_it = runtime.pages.find(page_key); + auto runtime_page_ts_it = runtime.page_ts.find(page_key); + const bool runtime_page_present = runtime_page_it != runtime.pages.end(); + const bool runtime_top_fresh = offset == 0 + && runtime_page_present + && runtime_page_ts_it != runtime.page_ts.end() + && (now - runtime_page_ts_it->second) < 10.0; + const bool runtime_page_cached = offset > 0 && runtime_page_present; + if (runtime_top_fresh || runtime_page_cached) { + txs = runtime_page_it->second; + rejected = offset == 0 ? runtime.rejected : json::array(); + total = runtime.total; + served = true; + } if (g_txcache.is_open()) { - auto r = g_rpc.get_txs_by_address(g_wallet.addr, 1, 0); - int node_total = 0; - json rejected_buf = json::array(); - if (r.ok && r.result.is_object()) { - node_total = r.result.value("total", 0); - if (r.result.contains("rejected")) - for (auto& row : r.result["rejected"]) - rejected_buf.push_back(convert_row(row, "rejected")); + const int cached_total = g_txcache.get_total(addr); + const bool top_page_fresh = offset == 0 + && cached_total > 0 + && (now - runtime.last_top_refresh_ts) < 10.0; + const bool page_cached = cached_total > offset; + if (!served && ((offset > 0 && page_cached) || top_page_fresh)) { + json cached_page = g_txcache.load_page(addr, limit, offset); + const bool cached_page_ok = !cached_page.empty() || cached_total == 0; + if (cached_page_ok) { + txs = cached_page; + rejected = offset == 0 ? runtime.rejected : json::array(); + total = cached_total; + runtime.pages[page_key] = txs; + runtime.page_ts[page_key] = now; + runtime.total = total; + history_runtime_put(addr, runtime); + served = true; + } + } + if (!served && page_cached) { + txs = g_txcache.load_page(addr, limit, offset); + rejected = offset == 0 ? runtime.rejected : json::array(); + total = cached_total; + runtime.pages[page_key] = txs; + runtime.page_ts[page_key] = now; + runtime.total = total; + history_runtime_put(addr, runtime); + served = true; } - int cached = g_txcache.get_total(g_wallet.addr); - if (node_total > cached) { - int delta = node_total - cached; - auto dr = g_rpc.get_txs_by_address(g_wallet.addr, delta, 0); - if (dr.ok && dr.result.is_object() && dr.result.contains("transactions")) { - json to_store = json::array(); - for (auto& row : dr.result["transactions"]) { - std::string h = row.value("hash", ""); - if (!h.empty() && !g_txcache.has_tx(h)) - to_store.push_back(convert_row(row, "confirmed")); + } + if (!served) { + int fetch_limit = limit; + if (offset == 0) fetch_limit = std::max(limit, 50); + auto fresh = g_rpc.get_txs_by_address(addr, fetch_limit, offset); + if (fresh.ok && fresh.result.is_object()) { + total = fresh.result.value("total", g_txcache.is_open() ? g_txcache.get_total(addr) : 0); + if (total < 0) total = 0; + if (fresh.result.contains("transactions")) { + json fetched_rows = json::array(); + for (auto& row : fresh.result["transactions"]) { + json converted = convert_row(row, "confirmed"); + fetched_rows.push_back(converted); } - if (!to_store.empty()) { - g_txcache.store_txs(to_store); - g_txcache.set_total(g_wallet.addr, cached + (int)to_store.size()); + if (g_txcache.is_open()) { + if (!fetched_rows.empty()) g_txcache.store_txs(addr, fetched_rows); + g_txcache.set_total(addr, total); } - rejected_buf = json::array(); - if (dr.result.contains("rejected")) - for (auto& row : dr.result["rejected"]) - rejected_buf.push_back(convert_row(row, "rejected")); + for (int i = 0; i < static_cast(fetched_rows.size()) && i < limit; ++i) + txs.push_back(fetched_rows[i]); } - } - json cached_txs = g_txcache.load_page(limit, offset); - for (auto& tx : cached_txs) txs.push_back(tx); - for (auto& tx : rejected_buf) txs.push_back(tx); - } else { - auto r = g_rpc.get_txs_by_address(g_wallet.addr, limit, offset); - if (r.ok && r.result.is_object()) { - if (r.result.contains("transactions")) - for (auto& row : r.result["transactions"]) - txs.push_back(convert_row(row, "confirmed")); - if (r.result.contains("rejected")) - for (auto& row : r.result["rejected"]) - txs.push_back(convert_row(row, "rejected")); + if (fresh.result.contains("rejected")) + for (auto& row : fresh.result["rejected"]) + rejected.push_back(convert_row(row, "rejected")); + if (offset == 0) { + runtime.last_top_refresh_ts = now; + runtime.rejected = rejected; + } + runtime.pages[page_key] = txs; + runtime.page_ts[page_key] = now; + runtime.total = total; + history_runtime_put(addr, runtime); } } + if (reconcile_history_rows(addr, txs)) { + runtime.pages[page_key] = txs; + runtime.page_ts[page_key] = now; + history_runtime_put(addr, runtime); + } + json j; j["transactions"] = txs; + j["rejected"] = rejected; + j["count"] = txs.size(); + j["offset"] = offset; + j["limit"] = limit; + j["total"] = total; + j["has_more"] = total > (offset + static_cast(txs.size())); + res.set_content(j.dump(), "application/json"); + }); + + svr.Get("/api/token-history", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + int limit = 50, offset = 0; + bool force = false; + if (req.has_param("limit")) limit = std::stoi(req.get_param_value("limit")); + if (req.has_param("offset")) offset = std::stoi(req.get_param_value("offset")); + if (req.has_param("force")) { + auto v = req.get_param_value("force"); + force = (v == "1" || v == "true"); + } + if (limit < 1) limit = 1; + if (limit > 500) limit = 500; + if (offset < 0) offset = 0; + const std::string addr = g_wallet.addr; + const double now = now_ts(); + + auto classify = [&](const json& tx) -> std::pair { + if (tx.value("op_type", "") != "call") return {false, false}; + if (tx.value("encrypted_data", "") != "transfer") return {false, false}; + bool incoming = false; + try { + if (tx.contains("message") && tx["message"].is_string()) { + auto parsed = json::parse(tx["message"].get()); + if (parsed.is_array() && !parsed.empty() && parsed[0].is_string()) { + std::string recipient = parsed[0].get(); + if (recipient == addr) incoming = true; + } + } + } catch (...) {} + return {true, incoming}; + }; + + if (!force) { + if (auto cached = token_history_runtime_get(addr)) { + if (now - cached->ts < 30.0) { + json page = json::array(); + for (int i = offset; i < static_cast(cached->rows.size()) && static_cast(page.size()) < limit; ++i) + page.push_back(cached->rows[i]); + json j; + j["transactions"] = page; + j["count"] = page.size(); + j["offset"] = offset; + j["limit"] = limit; + j["total"] = cached->rows.size(); + j["has_more"] = offset + static_cast(page.size()) < static_cast(cached->rows.size()); + j["incoming"] = cached->incoming; + j["outgoing"] = cached->outgoing; + res.set_content(j.dump(), "application/json"); + return; + } + } + } + + auto direct = g_rpc.get_token_txs_by_address(addr, limit, offset); + if (direct.ok && direct.result.is_object()) { + TokenHistoryRuntimeState state; + state.ts = now; + state.rows = direct.result.value("transactions", json::array()); + state.incoming = direct.result.value("incoming", 0); + state.outgoing = direct.result.value("outgoing", 0); + token_history_runtime_put(addr, state); + res.set_content(direct.result.dump(), "application/json"); + return; + } + + json rows = json::array(); + int incoming = 0; + int outgoing = 0; + std::set seen_hashes; + std::vector token_addrs; + + auto toks = g_rpc.tokens_by_address(addr); + if (toks.ok && toks.result.is_array()) { + for (auto& tok : toks.result) { + if (!tok.is_object()) continue; + std::string token_addr = tok.value("address", ""); + if (!token_addr.empty()) + token_addrs.push_back(token_addr); + } + } + + for (const auto& token_addr : token_addrs) { + int batch_offset = 0; + bool keep_going = true; + while (keep_going) { + auto batch = g_rpc.get_txs_by_address(token_addr, 200, batch_offset); + if (!batch.ok || !batch.result.is_object()) break; + auto txs = batch.result.value("transactions", json::array()); + for (auto& tx : txs) { + auto [is_token, is_incoming] = classify(tx); + if (!is_token) continue; + std::string tx_token = tx.value("to", tx.value("to_", "")); + if (tx_token != token_addr) continue; + bool is_outgoing = tx.value("from", "") == addr; + if (!is_incoming && !is_outgoing) continue; + std::string hash = tx.value("hash", ""); + if (!hash.empty() && !seen_hashes.insert(hash).second) continue; + if (is_incoming) incoming++; + else outgoing++; + rows.push_back(tx); + } + bool has_more = batch.result.value("has_more", false); + if (!has_more || txs.empty()) keep_going = false; + batch_offset += static_cast(txs.size()); + if (batch_offset >= 100000) keep_going = false; + } + } + + if (!rows.empty()) { + std::vector sorted; + sorted.reserve(rows.size()); + for (auto& row : rows) sorted.push_back(row); + std::sort(sorted.begin(), sorted.end(), [](const json& a, const json& b) { + return a.value("timestamp", 0.0) > b.value("timestamp", 0.0); + }); + rows = json::array(); + for (const auto& row : sorted) rows.push_back(row); + } + + TokenHistoryRuntimeState state; + state.ts = now; + state.rows = rows; + state.incoming = incoming; + state.outgoing = outgoing; + token_history_runtime_put(addr, state); + + json page = json::array(); + for (int i = offset; i < static_cast(rows.size()) && static_cast(page.size()) < limit; ++i) + page.push_back(rows[i]); + json j; + j["transactions"] = page; + j["count"] = page.size(); + j["offset"] = offset; + j["limit"] = limit; + j["total"] = rows.size(); + j["has_more"] = offset + static_cast(page.size()) < static_cast(rows.size()); + j["incoming"] = incoming; + j["outgoing"] = outgoing; res.set_content(j.dump(), "application/json"); }); @@ -1345,7 +1726,7 @@ int main(int argc, char** argv) { uint8_t their_vpub[32]; if (!octra::ed25519_pub_to_x25519(their_signing_pk.data(), their_vpub)) { res.status = 400; - res.set_content(err_json("ed25519→x25519 conversion failed").dump(), "application/json"); + res.set_content(err_json("ed25519->x25519 conversion failed").dump(), "application/json"); return; } std::vector their_vpub_raw(their_vpub, their_vpub + 32); @@ -1677,6 +2058,12 @@ int main(int argc, char** argv) { j["reject_reason"] = t["error"].value("reason", ""); j["reject_type"] = t["error"].value("type", ""); } + if (g_txcache.is_open() && g_wallet_loaded) { + const std::string from = j.value("from", ""); + const std::string to = j.value("to_", ""); + if (from == g_wallet.addr || to == g_wallet.addr) + g_txcache.store_tx(g_wallet.addr, j); + } res.set_content(j.dump(), "application/json"); }); @@ -1690,11 +2077,17 @@ int main(int argc, char** argv) { j["view_pubkey"] = octra::base64_encode(view_pk, 32); j["has_master_seed"] = g_wallet.has_master_seed(); octra::secure_zero(view_sk, 32); + res.set_header("Access-Control-Allow-Origin", "*"); res.set_content(j.dump(), "application/json"); }); svr.Post("/api/keys/private", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD +#ifndef OCTRA_WEBCLI_ENABLE_KEY_EXPORT + res.status = 403; + res.set_content(err_json("key export is disabled in this build; rebuild with -DOCTRA_WEBCLI_ENABLE_KEY_EXPORT to enable").dump(), "application/json"); + return; +#else json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -1702,6 +2095,12 @@ int main(int argc, char** argv) { return; } std::string pin = body.value("pin", ""); + std::string confirm = body.value("confirm", ""); + if (confirm != "I_UNDERSTAND_KEY_EXPORT_RISK") { + res.status = 403; + res.set_content(err_json("missing or invalid confirmation; pass confirm=\"I_UNDERSTAND_KEY_EXPORT_RISK\" in body").dump(), "application/json"); + return; + } try { octra::load_wallet_encrypted(g_wallet_path, pin); } catch (...) { res.status = 403; res.set_content(err_json("wrong PIN").dump(), "application/json"); @@ -1710,7 +2109,9 @@ int main(int argc, char** argv) { json j; j["private_key"] = g_wallet.priv_b64; j["mnemonic"] = g_wallet.mnemonic; + j["warning"] = "treat these values as plaintext secret; never paste into shared transcripts, screen-shares, or untrusted machines"; res.set_content(j.dump(), "application/json"); +#endif }); svr.Post("/api/contract/compile", [](const httplib::Request& req, httplib::Response& res) { @@ -2038,6 +2439,7 @@ int main(int argc, char** argv) { svr.Post("/api/fhe/encrypt", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); if (!g_pvac_ok) { res.status = 500; res.set_content(err_json("pvac not available").dump(), "application/json"); @@ -2055,14 +2457,25 @@ int main(int argc, char** argv) { pvac_cipher ct = g_pvac.encrypt(static_cast(value), seed); auto data = g_pvac.serialize_cipher(ct); std::string b64 = octra::base64_encode(data.data(), data.size()); + uint8_t blinding[32]; + octra::random_bytes(blinding, 32); + auto amount_commitment = g_pvac.pedersen_commit(static_cast(value), blinding); + std::string amount_commitment_b64 = octra::base64_encode(amount_commitment.data(), 32); + pvac_zero_proof proof = g_pvac.make_zero_proof_bound(ct, static_cast(value), blinding); + std::string zero_proof = g_pvac.encode_zero_proof(proof); + g_pvac.free_zero_proof(proof); g_pvac.free_cipher(ct); json result; result["ciphertext"] = b64; + result["amount_commitment"] = amount_commitment_b64; + result["zero_proof"] = zero_proof; + result["proof_kind"] = "bound_zero_v1"; res.set_content(result.dump(), "application/json"); }); svr.Post("/api/fhe/decrypt", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); if (!g_pvac_ok) { res.status = 500; res.set_content(err_json("pvac not available").dump(), "application/json"); @@ -2121,6 +2534,236 @@ int main(int argc, char** argv) { res.set_content(r.result.dump(), "application/json"); }); + svr.Get("/api/circle/info", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_info(circle_id); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.get_param_value("circle_id"); + std::string path = req.get_param_value("path"); + if (circle_id.empty() || path.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and path required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset(circle_id, path); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.get_param_value("circle_id"); + std::string path = req.get_param_value("path"); + if (circle_id.empty() || path.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and path required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext(circle_id, path); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext_by_key", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.get_param_value("circle_id"); + std::string resource_key = req.get_param_value("resource_key"); + if (circle_id.empty() || resource_key.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and resource_key required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext_by_resource_key(circle_id, resource_key); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/circle/deploy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string runtime = body.value("runtime", "octb"); + std::string privacy_class = body.value("privacy_class", "sealed"); + std::string browser_mode = body.value("browser_mode", "native_sealed"); + std::string resource_mode = body.value("resource_mode", "sealed_read"); + std::string code_b64 = body.value("code_b64", ""); + std::string policy_hash = body.value("policy_hash", ""); + std::string members_root = body.value("members_root", ""); + std::string export_policy = body.value("export_policy", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + auto read_limit = [&](const char* key, const char* fallback) -> std::string { + if (!body.contains("limits") || !body["limits"].is_object()) { + return fallback; + } + auto limits = body["limits"]; + if (!limits.contains(key)) { + return fallback; + } + if (limits[key].is_string()) { + return limits[key].get(); + } + if (limits[key].is_number_integer()) { + return std::to_string(limits[key].get()); + } + return fallback; + }; + json payload; + payload["runtime"] = runtime; + payload["privacy_class"] = privacy_class; + payload["browser_mode"] = browser_mode; + payload["resource_mode"] = resource_mode; + payload["limits"] = { + {"max_stable_bytes", read_limit("max_stable_bytes", "33554432")}, + {"max_assets_bytes", read_limit("max_assets_bytes", "33554432")}, + {"max_inline_value", read_limit("max_inline_value", "65536")}, + {"max_wasm_bytes", read_limit("max_wasm_bytes", "33554432")} + }; + if (!code_b64.empty()) payload["code_b64"] = code_b64; + if (!policy_hash.empty()) payload["policy_hash"] = policy_hash; + if (!members_root.empty()) payload["members_root"] = members_root; + if (!export_policy.empty()) payload["export_policy"] = export_policy; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "200000"); + tx.timestamp = now_ts(); + tx.op_type = "deploy_circle"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + else result["circle_id"] = circle_id; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/asset_encrypted", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string path = body.value("path", ""); + std::string content_type = body.value("content_type", ""); + std::string ciphertext_b64 = body.value("ciphertext_b64", ""); + std::string key_id = body.value("key_id", ""); + std::string plaintext_hash = body.value("plaintext_hash", ""); + std::string encoding = body.value("encoding", ""); + std::string padding_class = body.value("padding_class", ""); + if (circle_id.empty() || path.empty() || content_type.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, path, content_type, ciphertext_b64, key_id, and plaintext_hash required").dump(), "application/json"); + return; + } + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "5000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_asset_put_encrypted"; + tx.encrypted_data = ciphertext_b64; + json payload; + payload["path"] = path; + payload["content_type"] = content_type; + payload["key_id"] = key_id; + payload["plaintext_hash"] = plaintext_hash; + if (!encoding.empty()) payload["encoding"] = encoding; + if (!padding_class.empty()) payload["padding_class"] = padding_class; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Get(R"(/oct/([^/]+)(/.*)?)", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.matches.size() > 1 ? req.matches[1].str() : ""; + std::string raw_path = req.matches.size() > 2 ? req.matches[2].str() : ""; + std::string path = raw_path.empty() ? "/index.html" : raw_path; + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset(circle_id, path); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + auto content_type = r.result.value("content_type", "application/octet-stream"); + auto body_b64 = r.result.value("body_b64", ""); + auto raw = octra::base64_decode(body_b64); + std::string body(raw.begin(), raw.end()); + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_header("Cache-Control", "no-store"); + res.set_header("X-Content-Type-Options", "nosniff"); + res.set_content(body, content_type.c_str()); + }); + svr.Get("/api/contract/receipt", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::string hash = req.get_param_value("hash"); @@ -2150,6 +2793,14 @@ int main(int argc, char** argv) { res.set_content(g_token_cache.dump(), "application/json"); return; } + auto fast = g_rpc.tokens_by_address(g_wallet.addr); + if (fast.ok && fast.result.contains("tokens")) { + g_token_cache = fast.result; + g_token_cache_ts = now; + g_token_cache_addr = g_wallet.addr; + res.set_content(fast.result.dump(), "application/json"); + return; + } auto lr = g_rpc.list_contracts(); json tokens = json::array(); if (lr.ok && lr.result.contains("contracts")) { @@ -2272,6 +2923,8 @@ int main(int argc, char** argv) { if (old_rpc != g_wallet.rpc_url) { g_txcache.clear(); g_txcache.put("meta:rpc_url", g_wallet.rpc_url); + history_runtime_clear_all(); + token_history_runtime_clear_all(); cache_cleared = true; fprintf(stderr, "txcache cleared: rpc changed %s -> %s\n", old_rpc.c_str(), g_wallet.rpc_url.c_str()); @@ -2374,4 +3027,4 @@ int main(int argc, char** argv) { printf("octra_wallet listening on http://127.0.0.1:%d\n", port); svr.listen("127.0.0.1", port); return 0; -} \ No newline at end of file +} diff --git a/pvac/include/pvac/core/pvac_compress.hpp b/pvac/include/pvac/core/pvac_compress.hpp index afe4670..79e6a05 100644 --- a/pvac/include/pvac/core/pvac_compress.hpp +++ b/pvac/include/pvac/core/pvac_compress.hpp @@ -1,7 +1,3 @@ -// lambda0xe note: -// mini public key arith compressor for pub key -// (17 megabytes packed into 3 megabytes) - #pragma once #include @@ -91,7 +87,7 @@ struct RangeEncoder { void encode_bit(int bit) { uint32_t p = pred.predict(); uint32_t mid = lo + ((hi - lo) >> 16) * p - + ((hi - lo & 0xffff) * p >> 16); + + (((hi - lo) & 0xffff) * p >> 16); if (bit) hi = mid; else @@ -144,7 +140,7 @@ struct RangeDecoder { int decode_bit() { uint32_t p = pred.predict(); uint32_t mid = lo + ((hi - lo) >> 16) * p - + ((hi - lo & 0xffff) * p >> 16); + + (((hi - lo) & 0xffff) * p >> 16); int bit = 0; if (code <= mid) { bit = 1; @@ -208,8 +204,11 @@ inline std::vector unpack(const uint8_t* data, size_t len) { std::vector out; out.reserve(orig_sz); int b; - while ((b = dec.decode_byte_or_eof()) >= 0) + while ((b = dec.decode_byte_or_eof()) >= 0) { + if (out.size() >= orig_sz) + throw std::runtime_error("pvac_compress: output exceeds declared size"); out.push_back((uint8_t)b); + } if (out.size() != orig_sz) throw std::runtime_error("pvac_compress: size mismatch"); return out; @@ -224,4 +223,4 @@ inline bool is_packed(const uint8_t* data, size_t len) { } } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/core/types.hpp b/pvac/include/pvac/core/types.hpp index 27914ef..c4530b5 100644 --- a/pvac/include/pvac/core/types.hpp +++ b/pvac/include/pvac/core/types.hpp @@ -122,6 +122,60 @@ struct PubKey { std::vector powg_B; }; +inline bool is_valid_cipher_shape(const Cipher& cipher) { + if (cipher.slots == 0) + return false; + if (!cipher.c0.empty() && cipher.c0.size() != cipher.slots) + return false; + for (size_t layer_id = 0; layer_id < cipher.L.size(); ++layer_id) { + const auto& layer = cipher.L[layer_id]; + if (layer.rule != RRule::BASE && layer.rule != RRule::PROD) + return false; + if (layer.rule == RRule::PROD && (layer.pa >= cipher.L.size() || layer.pb >= cipher.L.size())) + return false; + if (!layer.PC.empty() && layer.PC.size() != cipher.slots) + return false; + } + for (const auto& edge : cipher.E) { + if (edge.layer_id >= cipher.L.size()) + return false; + if (edge.ch != SGN_P && edge.ch != SGN_M) + return false; + if (edge.w.size() != cipher.slots) + return false; + } + return true; +} + +inline bool is_valid_pubkey_shape(const PubKey& pk) { + if (pk.prm.B <= 0 || pk.prm.m_bits <= 0 || pk.prm.n_bits <= 0) + return false; + if (pk.H.size() != static_cast(pk.prm.n_bits)) + return false; + if (pk.ubk.perm.size() != static_cast(pk.prm.m_bits) || + pk.ubk.inv.size() != static_cast(pk.prm.m_bits)) + return false; + if (pk.powg_B.size() != static_cast(pk.prm.B)) + return false; + for (const auto& column : pk.H) { + if (column.nbits != static_cast(pk.prm.m_bits)) + return false; + } + return true; +} + +inline bool is_cipher_compatible_with_pubkey(const PubKey& pk, const Cipher& cipher) { + if (!is_valid_pubkey_shape(pk) || !is_valid_cipher_shape(cipher)) + return false; + for (const auto& edge : cipher.E) { + if (edge.idx >= pk.powg_B.size()) + return false; + if (edge.s.nbits != static_cast(pk.prm.m_bits)) + return false; + } + return true; +} + struct SecKey { std::array prf_k; std::vector lpn_s_bits; @@ -147,4 +201,4 @@ inline Fp rand_fp_nonzero() { } } } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/crypto/keygen.hpp b/pvac/include/pvac/crypto/keygen.hpp index 74cdf18..59e7e33 100644 --- a/pvac/include/pvac/crypto/keygen.hpp +++ b/pvac/include/pvac/crypto/keygen.hpp @@ -36,10 +36,13 @@ inline std::vector factor_small(int n) { inline void keygen(const Params & prm, PubKey & pk, SecKey & sk) { pk.prm = prm; + if (pk.prm.B == 0) + throw std::runtime_error("pvac: keygen: prm.B is zero"); + u128 pm1 = (((u128)1) << 127) - 2; if ((pm1 % (u128)pk.prm.B) != 0) { - std::abort(); + throw std::runtime_error("pvac: keygen: prm.B does not divide pm1"); } pk.canon_tag = csprng_u64(); @@ -140,10 +143,13 @@ inline void keygen(const Params & prm, PubKey & pk, SecKey & sk) { inline void keygen_from_seed(const Params& prm, PubKey& pk, SecKey& sk, const uint8_t wallet_privkey[32]) { pk.prm = prm; + if (pk.prm.B == 0) + throw std::runtime_error("pvac: keygen_from_seed: prm.B is zero"); + u128 pm1 = (((u128)1) << 127) - 2; if ((pm1 % (u128)pk.prm.B) != 0) { - std::abort(); + throw std::runtime_error("pvac: keygen_from_seed: prm.B does not divide pm1"); } uint8_t master[32]; @@ -245,4 +251,4 @@ inline void keygen_from_seed(const Params& prm, PubKey& pk, SecKey& sk, const ui } } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/crypto/ristretto255.hpp b/pvac/include/pvac/crypto/ristretto255.hpp index 86edb21..71570d4 100644 --- a/pvac/include/pvac/crypto/ristretto255.hpp +++ b/pvac/include/pvac/crypto/ristretto255.hpp @@ -384,9 +384,6 @@ inline Scalar sc_from_fp_signed(const Fp& x) { } inline Scalar sc_random() { - uint8_t buf[64]; - for (int i = 0; i < 64; i++) buf[i] = (uint8_t)(csprng_u64() >> (i % 8 * 8)); - uint64_t r[8]; for (int i = 0; i < 8; i++) r[i] = csprng_u64(); return sc_reduce512(r); @@ -759,4 +756,4 @@ inline RistrettoPoint pedersen_commit_fp(const Fp& value, const Scalar& blinding return pedersen_commit(sc_from_fp(value), blinding); } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/ops/arithmetic.hpp b/pvac/include/pvac/ops/arithmetic.hpp index 15b32dd..11f9166 100644 --- a/pvac/include/pvac/ops/arithmetic.hpp +++ b/pvac/include/pvac/ops/arithmetic.hpp @@ -280,6 +280,8 @@ inline Cipher ct_neg(const PubKey& pk, const Cipher& A) { } inline Cipher ct_add(const PubKey& pk, const Cipher& A, const Cipher& B) { + if (A.slots != B.slots) + throw std::runtime_error("pvac: ct_add: slot count mismatch between operands"); Cipher C; C.slots = A.slots; C.c0 = A.c0.empty() ? B.c0 : B.c0.empty() ? A.c0 : field::Op::add(A.c0, B.c0); @@ -472,4 +474,4 @@ inline Cipher ct_sub_const(const PubKey& pk, const Cipher& A, int64_t k) { return ct_add_const(pk, A, -k); } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/ops/decrypt.hpp b/pvac/include/pvac/ops/decrypt.hpp index 4f26c5f..d375acd 100644 --- a/pvac/include/pvac/ops/decrypt.hpp +++ b/pvac/include/pvac/ops/decrypt.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "../core/types.hpp" #include "../crypto/lpn.hpp" @@ -17,12 +18,13 @@ inline std::vector layer_R_cached( std::vector& st, std::vector>& cache ) { - if ((size_t)lid >= C.L.size()) std::abort(); + if ((size_t)lid >= C.L.size()) + throw std::runtime_error("pvac: layer_R_cached: layer id out of range"); if (st[lid] == 2) return cache[lid]; if (st[lid] == 1) { - std::abort(); + throw std::runtime_error("pvac: layer_R_cached: cycle in layer dependency graph"); } st[lid] = 1; @@ -42,6 +44,8 @@ inline std::vector layer_R_cached( } inline std::vector dec_values(const PubKey& pk, const SecKey& sk, const Cipher& C) { + if (!is_cipher_compatible_with_pubkey(pk, C)) + throw std::runtime_error("pvac: cipher/pubkey mismatch"); size_t L = C.L.size(); size_t S = C.slots; @@ -73,7 +77,16 @@ inline std::vector dec_values(const PubKey& pk, const SecKey& sk, const Ciph } inline Fp dec_value(const PubKey& pk, const SecKey& sk, const Cipher& C) { + if (C.slots != 1) + throw std::runtime_error("pvac: dec_value: cipher has multi-slot payload; use dec_values for vector decryption or dec_value_slot0 to explicitly coerce"); return dec_values(pk, sk, C)[0]; } +inline Fp dec_value_slot0(const PubKey& pk, const SecKey& sk, const Cipher& C) { + auto v = dec_values(pk, sk, C); + if (v.empty()) + throw std::runtime_error("pvac: dec_value_slot0: cipher decrypts to empty value vector"); + return v[0]; } + +} \ No newline at end of file diff --git a/pvac/include/pvac/ops/range_proof.hpp b/pvac/include/pvac/ops/range_proof.hpp index dc934f6..dfb2a69 100644 --- a/pvac/include/pvac/ops/range_proof.hpp +++ b/pvac/include/pvac/ops/range_proof.hpp @@ -43,9 +43,6 @@ inline RangeProof make_range_proof( for (size_t i = 0; i < RANGE_BITS; ++i) { uint64_t b_i = (value >> i) & 1; rp.ct_bit[i] = enc_value(pk, sk, b_i); - - - //!! auto ct_b_m1 = ct_sub_const(pk, rp.ct_bit[i], (uint64_t)1); uint8_t mul_seed[32]; for (int k = 0; k < 32; ++k) @@ -60,8 +57,6 @@ inline RangeProof make_range_proof( rp.ct_bit[i] = enc_value(pk, sk, b_i); auto ct_b_m1 = ct_sub_const(pk, rp.ct_bit[i], (uint64_t)1); uint8_t mul_seed[32]; - - // ! for (int k = 0; k < 32; ++k) mul_seed[k] = (uint8_t)((i * 37 + k * 13 + 0xA0) & 0xFF); auto ct_check = ct_mul_seeded(pk, rp.ct_bit[i], ct_b_m1, mul_seed); @@ -109,14 +104,15 @@ inline bool verify_range( const Cipher& ct_value, const RangeProof& rp ) { + if (!is_cipher_compatible_with_pubkey(pk, ct_value)) return false; if (rp.ct_bit.size() != RANGE_BITS) return false; if (rp.bit_proofs.size() != RANGE_BITS) return false; + for (const auto& ct_bit : rp.ct_bit) + if (!is_cipher_compatible_with_pubkey(pk, ct_bit)) + return false; unsigned hw = std::thread::hardware_concurrency(); - - - // ! unsigned n_threads = (hw > 1) ? std::min(hw, (unsigned)RANGE_BITS) : 1; std::vector results(RANGE_BITS, false); @@ -162,7 +158,6 @@ inline bool verify_range( for (size_t i = 1; i < RANGE_BITS; ++i) { Fp power_of_two; - if (i < 64) { power_of_two = fp_from_u64(1ULL << i); } else { @@ -182,9 +177,10 @@ inline bool verify_range( return true; } + struct AggregatedRangeProof { - std::vector ct_bit;// 64 encrypted bits (needed by verifier) - bp::R1CSProof proof; // single R1CS proof covering all 65 circuits + std::vector ct_bit; + bp::R1CSProof proof; }; namespace detail { @@ -294,6 +290,7 @@ inline AggregatedRangeProof make_aggregated_range_proof( std::vector bit_data(RANGE_BITS); + // Phase 1: parallel — encrypt bits + compute ct_check + decrypt layers unsigned hw = std::thread::hardware_concurrency(); unsigned n_threads = (hw > 1) ? std::min(hw, (unsigned)RANGE_BITS) : 1; @@ -334,6 +331,7 @@ inline AggregatedRangeProof make_aggregated_range_proof( lc_data.A, lc_data.bases, &lc_data.rinv, &sk); + // Phase 4: prove bp::Transcript transcript("pvac.range_proof.aggregated"); detail::append_transcript_params(transcript, bit_data, lc_data); @@ -346,7 +344,11 @@ inline bool verify_aggregated_range( const Cipher& ct_value, const AggregatedRangeProof& arp ) { + if (!is_cipher_compatible_with_pubkey(pk, ct_value)) return false; if (arp.ct_bit.size() != RANGE_BITS) return false; + for (const auto& ct_bit : arp.ct_bit) + if (!is_cipher_compatible_with_pubkey(pk, ct_bit)) + return false; std::vector vdata(RANGE_BITS); for (size_t i = 0; i < RANGE_BITS; ++i) { @@ -394,6 +396,7 @@ inline bool verify_aggregated_range( } v_offset += nB * S; } + { size_t nB = lc_data.bases.size(); size_t S = ct_lc_diff.slots; @@ -418,4 +421,4 @@ inline bool verify_aggregated_range( return bp::r1cs_verify(transcript, cs, arp.proof); } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/ops/verify_zero_circuit.hpp b/pvac/include/pvac/ops/verify_zero_circuit.hpp index 61e226b..f0cb3ff 100644 --- a/pvac/include/pvac/ops/verify_zero_circuit.hpp +++ b/pvac/include/pvac/ops/verify_zero_circuit.hpp @@ -187,6 +187,7 @@ inline bool verify_zero( const PubKey& pk, const Cipher& ct, const ZeroProof& proof ) { + if (!is_cipher_compatible_with_pubkey(pk, ct)) return false; size_t nL = ct.L.size(); size_t S = ct.slots; @@ -267,6 +268,7 @@ inline bool verify_zero_bound( const ZeroProof& proof, const RistrettoPoint& amount_commitment ) { + if (!is_cipher_compatible_with_pubkey(pk, ct)) return false; size_t nL = ct.L.size(); size_t S = ct.slots; @@ -304,4 +306,4 @@ inline bool verify_zero_bound( return bp::r1cs_verify(transcript, cs, proof.proof); } -} +} \ No newline at end of file diff --git a/pvac/pvac_c_api.cpp b/pvac/pvac_c_api.cpp index 3bd0e4a..d7931dc 100644 --- a/pvac/pvac_c_api.cpp +++ b/pvac/pvac_c_api.cpp @@ -47,15 +47,26 @@ pvac_cipher pvac_enc_zero_seeded(pvac_pubkey pk, pvac_seckey sk, } uint64_t pvac_dec_value(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct) { - pvac::Fp r = pvac::dec_value(*PK(pk), *SK(sk), *CT(ct)); - return r.lo; + try { + pvac::Fp r = pvac::dec_value(*PK(pk), *SK(sk), *CT(ct)); + return r.lo; + } catch (...) { + return 0; + } } void pvac_dec_value_fp(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct, uint64_t* lo_out, uint64_t* hi_out) { - pvac::Fp r = pvac::dec_value(*PK(pk), *SK(sk), *CT(ct)); - *lo_out = r.lo; - *hi_out = r.hi; + if (!lo_out || !hi_out) + return; + try { + pvac::Fp r = pvac::dec_value(*PK(pk), *SK(sk), *CT(ct)); + *lo_out = r.lo; + *hi_out = r.hi; + } catch (...) { + *lo_out = 0; + *hi_out = 0; + } } pvac_cipher pvac_enc_value_fp_seeded(pvac_pubkey pk, pvac_seckey sk, @@ -134,10 +145,24 @@ pvac_cipher pvac_ct_square_seeded(pvac_pubkey pk, pvac_cipher ct, const uint8_t } void pvac_commit_ct(pvac_pubkey pk, pvac_cipher ct, uint8_t out[32]) { + if (!out) + return; auto h = pvac::commit_ct(*PK(pk), *CT(ct)); std::memcpy(out, h.data(), 32); } +int pvac_commit_ct_v2(pvac_pubkey pk, pvac_cipher ct, uint8_t *out, size_t out_cap, size_t *out_len) { + if (out_len) *out_len = 32; + if (!out || out_cap < 32) return -1; + try { + auto h = pvac::commit_ct(*PK(pk), *CT(ct)); + std::memcpy(out, h.data(), 32); + return 0; + } catch (...) { + return -2; + } +} + pvac_zero_proof pvac_make_zero_proof(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct) { auto* zp = new pvac::ZeroProof(); *zp = pvac::make_zero_proof(*PK(pk), *SK(sk), *CT(ct)); @@ -145,7 +170,11 @@ pvac_zero_proof pvac_make_zero_proof(pvac_pubkey pk, pvac_seckey sk, pvac_cipher } int pvac_verify_zero(pvac_pubkey pk, pvac_cipher ct, pvac_zero_proof proof) { - return pvac::verify_zero(*PK(pk), *CT(ct), *ZP(proof)) ? 1 : 0; + try { + return pvac::verify_zero(*PK(pk), *CT(ct), *ZP(proof)) ? 1 : 0; + } catch (...) { + return 0; + } } pvac_zero_proof pvac_make_zero_proof_bound(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct, @@ -160,16 +189,37 @@ int pvac_verify_zero_bound(pvac_pubkey pk, pvac_cipher ct, pvac_zero_proof proof const uint8_t amount_commitment[32]) { pvac::RistrettoPoint commit; std::memcpy(commit.data(), amount_commitment, 32); - return pvac::verify_zero_bound(*PK(pk), *CT(ct), *ZP(proof), commit) ? 1 : 0; + try { + return pvac::verify_zero_bound(*PK(pk), *CT(ct), *ZP(proof), commit) ? 1 : 0; + } catch (...) { + return 0; + } } void pvac_pedersen_commit(uint64_t amount, const uint8_t blinding[32], uint8_t out[32]) { + if (!out) + return; pvac::Scalar val = pvac::bp::sc_from_u64(amount); pvac::Scalar blind = pvac::sc_reduce256(blinding); pvac::RistrettoPoint pt = pvac::pedersen_commit(val, blind); std::memcpy(out, pt.data(), 32); } +int pvac_pedersen_commit_v2(uint64_t amount, const uint8_t blinding[32], + uint8_t *out, size_t out_cap, size_t *out_len) { + if (out_len) *out_len = 32; + if (!out || out_cap < 32) return -1; + try { + pvac::Scalar val = pvac::bp::sc_from_u64(amount); + pvac::Scalar blind = pvac::sc_reduce256(blinding); + pvac::RistrettoPoint pt = pvac::pedersen_commit(val, blind); + std::memcpy(out, pt.data(), 32); + return 0; + } catch (...) { + return -2; + } +} + pvac_range_proof pvac_make_range_proof(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct, uint64_t value) { auto* rp = new pvac::RangeProof(); @@ -178,7 +228,11 @@ pvac_range_proof pvac_make_range_proof(pvac_pubkey pk, pvac_seckey sk, } int pvac_verify_range(pvac_pubkey pk, pvac_cipher ct, pvac_range_proof proof) { - return pvac::verify_range(*PK(pk), *CT(ct), *RP(proof)) ? 1 : 0; + try { + return pvac::verify_range(*PK(pk), *CT(ct), *RP(proof)) ? 1 : 0; + } catch (...) { + return 0; + } } uint8_t* pvac_serialize_cipher(pvac_cipher ct, size_t* len) { @@ -307,7 +361,11 @@ pvac_agg_range_proof pvac_make_aggregated_range_proof(pvac_pubkey pk, pvac_secke } int pvac_verify_aggregated_range(pvac_pubkey pk, pvac_cipher ct, pvac_agg_range_proof proof) { - return pvac::verify_aggregated_range(*PK(pk), *CT(ct), *ARP(proof)) ? 1 : 0; + try { + return pvac::verify_aggregated_range(*PK(pk), *CT(ct), *ARP(proof)) ? 1 : 0; + } catch (...) { + return 0; + } } uint8_t* pvac_serialize_agg_range_proof(pvac_agg_range_proof arp, size_t* len) { diff --git a/pvac/pvac_c_api.h b/pvac/pvac_c_api.h index ed60895..72d34dd 100644 --- a/pvac/pvac_c_api.h +++ b/pvac/pvac_c_api.h @@ -36,6 +36,7 @@ pvac_cipher pvac_ct_sub_const(pvac_pubkey pk, pvac_cipher ct, uint64_t k); pvac_cipher pvac_ct_div_const(pvac_pubkey pk, pvac_cipher ct, uint64_t k_lo, uint64_t k_hi); pvac_cipher pvac_ct_square_seeded(pvac_pubkey pk, pvac_cipher ct, const uint8_t seed[32]); void pvac_commit_ct(pvac_pubkey pk, pvac_cipher ct, uint8_t out[32]); +int pvac_commit_ct_v2(pvac_pubkey pk, pvac_cipher ct, uint8_t *out, size_t out_cap, size_t *out_len); pvac_zero_proof pvac_make_zero_proof(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct); int pvac_verify_zero(pvac_pubkey pk, pvac_cipher ct, pvac_zero_proof proof); @@ -46,6 +47,8 @@ int pvac_verify_zero_bound(pvac_pubkey pk, pvac_cipher ct, pvac_zero_proof proof const uint8_t amount_commitment[32]); void pvac_pedersen_commit(uint64_t amount, const uint8_t blinding[32], uint8_t out[32]); +int pvac_pedersen_commit_v2(uint64_t amount, const uint8_t blinding[32], + uint8_t *out, size_t out_cap, size_t *out_len); pvac_range_proof pvac_make_range_proof(pvac_pubkey pk, pvac_seckey sk, pvac_cipher ct, uint64_t value); diff --git a/pvac/pvac_serialize.hpp b/pvac/pvac_serialize.hpp index 5d83bf5..6d620b9 100644 --- a/pvac/pvac_serialize.hpp +++ b/pvac/pvac_serialize.hpp @@ -166,6 +166,9 @@ struct Reader { bv.nbits = u64(); size_t nw = u64(); check_count(nw, 8); + size_t expected_nw = static_cast((bv.nbits + 63) / 64); + if (!failed && nw != expected_nw) + fail("pvac_ser: bitvec word count mismatch"); if (failed) return bv; bv.w.resize(nw); for (size_t i = 0; i < nw; ++i) bv.w[i] = u64(); @@ -197,6 +200,46 @@ struct Reader { } }; +inline void validate_cipher_structure(const pvac::Cipher& cipher) { + if (cipher.slots == 0) + throw std::runtime_error("pvac_ser: cipher slots must be positive"); + if (!cipher.c0.empty() && cipher.c0.size() != cipher.slots) + throw std::runtime_error("pvac_ser: c0/slots size mismatch"); + for (size_t layer_id = 0; layer_id < cipher.L.size(); ++layer_id) { + const auto& layer = cipher.L[layer_id]; + if (layer.rule != pvac::RRule::BASE && layer.rule != pvac::RRule::PROD) + throw std::runtime_error("pvac_ser: invalid layer rule"); + if (layer.rule == pvac::RRule::PROD && (layer.pa >= cipher.L.size() || layer.pb >= cipher.L.size())) + throw std::runtime_error("pvac_ser: invalid product parent"); + if (!layer.PC.empty() && layer.PC.size() != cipher.slots) + throw std::runtime_error("pvac_ser: layer PC/slots size mismatch"); + } + for (const auto& edge : cipher.E) { + if (edge.layer_id >= cipher.L.size()) + throw std::runtime_error("pvac_ser: edge layer out of range"); + if (edge.ch != pvac::SGN_P && edge.ch != pvac::SGN_M) + throw std::runtime_error("pvac_ser: invalid edge sign"); + if (edge.w.size() != cipher.slots) + throw std::runtime_error("pvac_ser: edge weight/slots size mismatch"); + } +} + +inline void validate_pubkey_structure(const pvac::PubKey& pk) { + if (pk.prm.B <= 0 || pk.prm.m_bits <= 0 || pk.prm.n_bits <= 0) + throw std::runtime_error("pvac_ser: invalid public key dimensions"); + if (pk.H.size() != static_cast(pk.prm.n_bits)) + throw std::runtime_error("pvac_ser: H column count mismatch"); + if (pk.ubk.perm.size() != static_cast(pk.prm.m_bits) || + pk.ubk.inv.size() != static_cast(pk.prm.m_bits)) + throw std::runtime_error("pvac_ser: UBK size mismatch"); + if (pk.powg_B.size() != static_cast(pk.prm.B)) + throw std::runtime_error("pvac_ser: powg_B size mismatch"); + for (const auto& column : pk.H) { + if (column.nbits != static_cast(pk.prm.m_bits)) + throw std::runtime_error("pvac_ser: H bitvec length mismatch"); + } +} + inline void write_params(Writer& w, const pvac::Params& prm) { w.i32(prm.B); w.i32(prm.m_bits); @@ -343,6 +386,7 @@ inline pvac::Cipher deserialize_cipher(const uint8_t* data, size_t len) { for (size_t i = 0; i < nE; ++i) C.E[i] = read_edge(r); } if (r.failed) throw std::runtime_error(r.error); + validate_cipher_structure(C); return C; } @@ -414,6 +458,7 @@ inline pvac::PubKey deserialize_pubkey_raw(const uint8_t* data, size_t len) { } if (r.failed) throw std::runtime_error(r.error); + validate_pubkey_structure(pk); return pk; } @@ -577,6 +622,9 @@ inline pvac::Cipher read_cipher_raw(Reader& r, uint8_t ver = VERSION_V2) { C.E.resize(nE); for (size_t i = 0; i < nE; ++i) C.E[i] = read_edge(r); } + if (r.failed) + throw std::runtime_error(r.error); + validate_cipher_structure(C); return C; } @@ -602,7 +650,10 @@ inline pvac::RangeProof deserialize_range_proof(const uint8_t* data, size_t len) pvac::RangeProof rp; size_t nbits = r.u64(); - r.check_count(nbits, 8); + if (nbits != pvac::RANGE_BITS) + r.fail("pvac_ser: unexpected range proof bit length"); + if (!r.failed) + r.check_count(nbits, 8); if (!r.failed) { rp.ct_bit.resize(nbits); @@ -623,8 +674,6 @@ inline pvac::RangeProof deserialize_range_proof(const uint8_t* data, size_t len) return rp; } -// ═══ Aggregated Range Proof ═══ - inline std::vector serialize_agg_range_proof(const pvac::AggregatedRangeProof& arp) { Writer w; w.header(TAG_AGG_RANGE_PROOF); @@ -641,7 +690,10 @@ inline pvac::AggregatedRangeProof deserialize_agg_range_proof(const uint8_t* dat pvac::AggregatedRangeProof arp; size_t nbits = r.u64(); - r.check_count(nbits, 8); + if (nbits != pvac::RANGE_BITS) + r.fail("pvac_ser: unexpected range proof bit length"); + if (!r.failed) + r.check_count(nbits, 8); if (!r.failed) { arp.ct_bit.resize(nbits); for (size_t i = 0; i < nbits && !r.failed; ++i) @@ -664,6 +716,7 @@ struct RangeProofAny { }; inline RangeProofAny deserialize_range_proof_any(const uint8_t* data, size_t len) { + // Header layout: MAGIC[4] + VERSION[1] + TAG[1] if (len < 6) throw std::runtime_error("pvac_ser: range proof too short"); uint8_t tag = data[5]; RangeProofAny result; @@ -679,4 +732,4 @@ inline RangeProofAny deserialize_range_proof_any(const uint8_t* data, size_t len return result; } -} +} \ No newline at end of file diff --git a/rpc_client.hpp b/rpc_client.hpp index 98210f7..3c0405c 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -193,6 +193,22 @@ class RpcClient { return call("vm_contract", {addr}); } + RpcResult circle_info(const std::string& circle_id) { + return call("circle_info", {circle_id}, 10); + } + + RpcResult circle_asset(const std::string& circle_id, const std::string& path) { + return call("circle_asset", {circle_id, path}, 10); + } + + RpcResult circle_asset_ciphertext(const std::string& circle_id, const std::string& path) { + return call("circle_asset_ciphertext", {circle_id, path}, 10); + } + + RpcResult circle_asset_ciphertext_by_resource_key(const std::string& circle_id, const std::string& resource_key) { + return call("circle_asset_ciphertext_by_resource_key", {circle_id, resource_key}, 10); + } + RpcResult contract_receipt(const std::string& hash) { return call("contract_receipt", {hash}); } @@ -208,6 +224,10 @@ class RpcClient { return call("octra_listContracts", nlohmann::json::array(), 10); } + RpcResult tokens_by_address(const std::string& addr) { + return call("octra_tokensByAddress", {addr}, 15); + } + RpcResult contract_storage(const std::string& addr, const std::string& key) { return call("octra_contractStorage", {addr, key}); } @@ -224,6 +244,10 @@ class RpcClient { return call("octra_transactionsByAddress", {addr, limit, offset}, 15); } + RpcResult get_token_txs_by_address(const std::string& addr, int limit = 50, int offset = 0) { + return call("octra_tokenTransfersByAddress", {addr, limit, offset}, 30); + } + std::vector call_batch(const std::vector& methods, const std::vector& params_list = {}, int timeout_sec = 10) { @@ -300,4 +324,4 @@ class RpcClient { } }; -} // namespace octra \ No newline at end of file +} \ No newline at end of file diff --git a/setup.sh b/setup.sh index 2136a49..a5d4fe5 100755 --- a/setup.sh +++ b/setup.sh @@ -7,8 +7,8 @@ for arg in "$@"; do --deps-only|--no-build) MODE="deps" ;; --help|-h) echo "usage: $0 [--deps-only]" - echo "(no args) install deps + build" - echo "--deps-only install deps only (no make)" + echo " (no args) install deps + build" + echo " --deps-only install deps only (no make)" exit 0 ;; esac @@ -106,7 +106,7 @@ case "$OS" in if [ "$MODE" = "deps" ]; then echo "[1/1] detected windows shell ($OS) in deps-only mode" echo "on windows, dependencies should be installed via setup.bat from cmd.exe" - echo "if you already ran setup.bat, this is fine — continuing" + echo "if you already ran setup.bat, this is fine - continuing" exit 0 fi echo "detected windows shell ($OS). run setup.bat from cmd.exe instead." @@ -153,4 +153,4 @@ echo "start the wallet:" echo "./octra_wallet" echo "" echo "then open http://127.0.0.1:8420 in your browser" -echo "" +echo "" \ No newline at end of file diff --git a/static/bridge.html b/static/bridge.html index a02b9aa..1c71c03 100644 --- a/static/bridge.html +++ b/static/bridge.html @@ -243,20 +243,48 @@

connect wallet

+ + diff --git a/static/index.html b/static/index.html index abbed67..ad7f0c1 100644 --- a/static/index.html +++ b/static/index.html @@ -114,6 +114,7 @@
connecting...
+ @@ -141,7 +142,7 @@
staging
-
-
recent transactions
+
recent transactions (0)
loading...
@@ -277,8 +278,8 @@
-
token transactions
-
switch to a token to see transactions
+
token transactions (0 / 0, in 0, out 0)
+
loading token transactions...
@@ -428,7 +429,7 @@
@@ -479,7 +480,7 @@
-
transaction history
+
transaction history (0 / 0)
loading...
@@ -548,6 +549,6 @@ - + \ No newline at end of file diff --git a/static/style.css b/static/style.css index b645a99..c02dd13 100644 --- a/static/style.css +++ b/static/style.css @@ -210,9 +210,6 @@ header .logout-btn:hover { background: #7A8BA3; } .view { display: none; } .view.active { display: block; } - - - #view-apps.active { padding: clamp(16px, 2.5vw, 32px) clamp(12px, 3vw, 40px); box-sizing: border-box; @@ -274,6 +271,11 @@ table { color: #3B567F; } +.pending-text { + font-size: 11px; + color: #8C9DB6; +} + td, th { white-space: nowrap; overflow: hidden; @@ -649,7 +651,7 @@ td { background: #F6F7F9; border: 1px solid #E5E9EF; padding: 8px; - margin: 12px 8px 8px; + margin: 12px 0 8px; font-size: 11px; word-break: break-all; white-space: pre-wrap; @@ -1070,7 +1072,6 @@ td { font-size: 11px; margin-top: 4px; } -/* --- IDE icons (SVG mask, Font Awesome Solid) --- */ .ide-icon { display: inline-block; width: 14px; height: 14px; @@ -1346,6 +1347,9 @@ td { } .modal-message { font-size: 12px; color: #5A6A7D; margin-bottom: 12px; line-height: 1.5; + white-space: pre-wrap; + overflow-wrap: anywhere; + word-break: break-word; } .modal-input { width: 100%; padding: 6px 8px; border: 1px solid #C0C6D0; diff --git a/static/swap.js b/static/swap.js index c245d36..a08517c 100644 --- a/static/swap.js +++ b/static/swap.js @@ -25,12 +25,9 @@ 2025-2026 Julia L. */ - - -/* only for dev-net testing, does not work for main net */ -var SWAP_ADDR = 'octBjnQBicZs6iMwcRxrdzLYAzyVTi91KEiA8RGkVjco2w6'; -var TOKEN_ADDR = 'oct6J37Wx7Rb1putvfwFrFbGUStE8hGzsb33fhLgUdpTx6d'; -var SCANNER_URL = 'https://devnet.octrascan.io'; +var SWAP_ADDR = ''; +var TOKEN_ADDR = ''; +var SCANNER_URL = ''; var TOKEN_SYMBOL = 'tUSD'; var TOKEN_DECIMALS = 6; var OCT_DECIMALS = 6; diff --git a/static/wallet.js b/static/wallet.js index d1e0722..e346114 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -117,14 +117,357 @@ var _compiledAbi = null; var _fees = {}; var _rpcHost = ''; var _hasMasterSeed = false; +var _addressRuntime = {}; +var _tokenMetaInflight = {}; +var HISTORY_CACHE_TTL_MS = 12000; +var HISTORY_STALE_REFRESH_MS = 3000; +var BALANCE_CACHE_TTL_MS = 5000; +var TOKEN_CACHE_TTL_MS = 15000; +var TOKEN_STALE_REFRESH_MS = 3000; +var PERSISTED_CACHE_TTL_MS = 300000; + +function ensureAddressRuntime(addr) { + if (!addr) return null; + if (!_addressRuntime[addr]) { + _addressRuntime[addr] = { + balance: null, + balanceTs: 0, + balanceInflight: null, + historyPages: {}, + historyTs: {}, + historyInflight: {}, + tokens: [], + tokensLoaded: false, + tokensTs: 0, + tokensInflight: null, + tokenHistory: null, + tokenHistoryTs: 0, + tokenHistoryInflight: null + }; + } + return _addressRuntime[addr]; +} + +function clearAddressRuntime(addr) { + if (!addr) return; + delete _addressRuntime[addr]; +} + +function clearAllAddressRuntime() { + _addressRuntime = {}; + _tokenMetaInflight = {}; +} + +function persistedCachePrefix(addr) { + return 'octra_webcli:v2:' + (_rpcHost || 'rpc') + ':' + addr + ':'; +} + +function persistedRead(key) { + try { + var raw = sessionStorage.getItem(key); + if (!raw) return null; + return JSON.parse(raw); + } catch (e) { + return null; + } +} + +function persistedWrite(key, value) { + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch (e) {} +} + +function persistedRemovePrefix(prefix) { + try { + for (var i = sessionStorage.length - 1; i >= 0; i--) { + var key = sessionStorage.key(i); + if (key && key.indexOf(prefix) === 0) sessionStorage.removeItem(key); + } + } catch (e) {} +} + +function persistBalance(addr, balance) { + if (!addr || !balance) return; + persistedWrite(persistedCachePrefix(addr) + 'balance', { + ts: Date.now(), + balance: balance + }); +} + +function restorePersistedBalance(addr) { + if (!addr) return null; + var cached = persistedRead(persistedCachePrefix(addr) + 'balance'); + if (!cached || !cached.balance || !cached.ts) return null; + if ((Date.now() - cached.ts) > PERSISTED_CACHE_TTL_MS) return null; + return cached.balance; +} + +function persistHistoryPage(addr, limit, offset, response) { + if (!addr || !response) return; + persistedWrite(persistedCachePrefix(addr) + 'history:' + historyPageKey(limit, offset), { + ts: Date.now(), + response: response + }); +} + +function restorePersistedHistoryPage(addr, limit, offset) { + if (!addr) return null; + var cached = persistedRead(persistedCachePrefix(addr) + 'history:' + historyPageKey(limit, offset)); + if (!cached || !cached.response || !cached.ts) return null; + if ((Date.now() - cached.ts) > PERSISTED_CACHE_TTL_MS) return null; + return cached; +} + +function persistTokens(addr, tokens) { + if (!addr) return; + persistedWrite(persistedCachePrefix(addr) + 'tokens', { + ts: Date.now(), + tokens: tokens || [] + }); +} + +function persistTokenHistory(addr, payload) { + if (!addr || !payload) return; + persistedWrite(persistedCachePrefix(addr) + 'token-history', { + ts: Date.now(), + payload: payload + }); +} + +function restorePersistedTokens(addr) { + if (!addr) return null; + var cached = persistedRead(persistedCachePrefix(addr) + 'tokens'); + if (!cached || !cached.ts || !Array.isArray(cached.tokens)) return null; + if ((Date.now() - cached.ts) > PERSISTED_CACHE_TTL_MS) return null; + return cached; +} + +function restorePersistedTokenHistory(addr) { + if (!addr) return null; + var cached = persistedRead(persistedCachePrefix(addr) + 'token-history'); + if (!cached || !cached.ts || !cached.payload) return null; + if ((Date.now() - cached.ts) > PERSISTED_CACHE_TTL_MS) return null; + return cached; +} + +function dropPersistedAddressRuntime(addr) { + if (!addr) return; + persistedRemovePrefix(persistedCachePrefix(addr)); +} + +function dropAllPersistedRuntime() { + persistedRemovePrefix('octra_webcli:'); +} + +function historyPageKey(limit, offset) { + return String(limit) + ':' + String(offset); +} + +function peekHistoryPage(addr, limit, offset) { + var state = ensureAddressRuntime(addr); + if (!state) return null; + var key = historyPageKey(limit, offset); + if (!state.historyPages[key]) { + var persisted = restorePersistedHistoryPage(addr, limit, offset); + if (persisted) { + state.historyPages[key] = persisted.response; + state.historyTs[key] = persisted.ts; + } + } + if (!state.historyPages[key]) return null; + return { + response: state.historyPages[key], + ts: state.historyTs[key] || 0 + }; +} +function cacheHistoryPage(addr, limit, offset, response) { + var state = ensureAddressRuntime(addr); + if (!state) return response; + var key = historyPageKey(limit, offset); + state.historyPages[key] = response; + state.historyTs[key] = Date.now(); + persistHistoryPage(addr, limit, offset, response); + return response; +} -var _ideProject = null; // { id, name, created, template } -var _ideFiles = {}; // { path: content } +async function reconcileHistoryResponse(addr, limit, offset, response) { + if (!addr || !response || !Array.isArray(response.transactions) || response.transactions.length === 0) { + return response; + } + var pending = []; + for (var i = 0; i < response.transactions.length; i++) { + var tx = response.transactions[i]; + if (!tx || !tx.hash) continue; + var st = tx.status || 'pending'; + if (st === 'pending') pending.push({ index: i, hash: tx.hash }); + } + if (pending.length === 0) return response; + var nextTxs = response.transactions.slice(); + var changed = false; + await Promise.all(pending.map(async function(entry) { + try { + var fresh = await api('GET', '/tx?hash=' + encodeURIComponent(entry.hash)); + var st = fresh.status || 'pending'; + if (!st || st === 'pending') return; + var merged = Object.assign({}, nextTxs[entry.index], fresh); + merged.hash = fresh.hash || nextTxs[entry.index].hash || entry.hash; + merged.to_ = fresh.to_ || fresh.to || nextTxs[entry.index].to_ || nextTxs[entry.index].to || ''; + merged.amount_raw = fresh.amount_raw || fresh.amount || nextTxs[entry.index].amount_raw || '0'; + merged.status = st; + nextTxs[entry.index] = merged; + changed = true; + } catch (e) {} + })); + if (!changed) return response; + var nextResponse = Object.assign({}, response, { transactions: nextTxs }); + cacheHistoryPage(addr, limit, offset, nextResponse); + return nextResponse; +} + +async function fetchHistoryPage(limit, offset, force) { + var addr = _walletAddr; + if (!addr) return { transactions: [] }; + var state = ensureAddressRuntime(addr); + var key = historyPageKey(limit, offset); + var cached = peekHistoryPage(addr, limit, offset); + if (!force && cached && (Date.now() - cached.ts) < HISTORY_CACHE_TTL_MS) { + return reconcileHistoryResponse(addr, limit, offset, cached.response); + } + if (state.historyInflight[key]) return state.historyInflight[key]; + state.historyInflight[key] = api('GET', '/history?limit=' + limit + '&offset=' + offset) + .then(function(response) { + return cacheHistoryPage(addr, limit, offset, response); + }) + .then(function(response) { + return reconcileHistoryResponse(addr, limit, offset, response); + }) + .finally(function() { + delete state.historyInflight[key]; + }); + return state.historyInflight[key]; +} + +function cacheAddressTokens(addr, tokens) { + var state = ensureAddressRuntime(addr); + if (!state) return; + state.tokens = tokens || []; + state.tokensLoaded = true; + state.tokensTs = Date.now(); + persistTokens(addr, state.tokens); +} + +function restoreAddressTokens(addr) { + var state = ensureAddressRuntime(addr); + if (!state) return false; + if (!state.tokensLoaded) { + var persisted = restorePersistedTokens(addr); + if (persisted) { + state.tokens = persisted.tokens.slice(); + state.tokensLoaded = true; + state.tokensTs = persisted.ts; + } + } + if (!state.tokensLoaded) return false; + _tokens = state.tokens.slice(); + _tokensLoaded = true; + hydrateTokenMaps(_tokens); + return true; +} + +function hydrateTokenMaps(tokens) { + for (var i = 0; i < tokens.length; i++) { + _tokenSymbols[tokens[i].address] = tokens[i].symbol; + _tokenDecimals[tokens[i].address] = tokens[i].decimals || '0'; + } +} + +function tokenHistorySummary(payload) { + return { + transactions: (payload && payload.transactions) ? payload.transactions : [], + total: (payload && payload.total) ? payload.total : 0, + incoming: (payload && payload.incoming) ? payload.incoming : 0, + outgoing: (payload && payload.outgoing) ? payload.outgoing : 0, + has_more: !!(payload && payload.has_more) + }; +} + +async function fetchAddressTokens(force) { + var addr = _walletAddr; + if (!addr) return []; + var state = ensureAddressRuntime(addr); + if (!state) return []; + if (!force && state.tokensLoaded && (Date.now() - state.tokensTs) < TOKEN_CACHE_TTL_MS) { + return state.tokens.slice(); + } + if (state.tokensInflight) return state.tokensInflight; + state.tokensInflight = api('GET', '/tokens') + .then(function(res) { + var tokens = (res && res.tokens) ? res.tokens : []; + cacheAddressTokens(addr, tokens); + return tokens.slice(); + }) + .finally(function() { + state.tokensInflight = null; + }); + return state.tokensInflight; +} + +async function fetchTokenHistory(force) { + var addr = _walletAddr; + if (!addr) return tokenHistorySummary(null); + var state = ensureAddressRuntime(addr); + if (!state) return tokenHistorySummary(null); + if (!force && state.tokenHistory && (Date.now() - state.tokenHistoryTs) < HISTORY_CACHE_TTL_MS) { + return state.tokenHistory; + } + if (!force && !state.tokenHistory) { + var persisted = restorePersistedTokenHistory(addr); + if (persisted) { + var summary = tokenHistorySummary(persisted.payload); + var hasTokens = state.tokensLoaded && state.tokens && state.tokens.length > 0; + if (!(hasTokens && summary.total === 0)) { + state.tokenHistory = summary; + state.tokenHistoryTs = persisted.ts; + return state.tokenHistory; + } + } + } + if (state.tokenHistoryInflight) return state.tokenHistoryInflight; + var suffix = force ? '&force=1' : ''; + state.tokenHistoryInflight = api('GET', '/token-history?limit=200&offset=0' + suffix) + .then(function(response) { + var summary = tokenHistorySummary(response); + state.tokenHistory = summary; + state.tokenHistoryTs = Date.now(); + persistTokenHistory(addr, summary); + return summary; + }) + .finally(function() { + state.tokenHistoryInflight = null; + }); + return state.tokenHistoryInflight; +} + +function invalidateCurrentAddressState() { + if (!_walletAddr) return; + dropPersistedAddressRuntime(_walletAddr); + clearAddressRuntime(_walletAddr); + ensureAddressRuntime(_walletAddr); + _cachedBal = null; + _historyOffset = 0; + _tokens = []; + _tokensLoaded = false; +} + + +var _ideProject = null; +var _ideFiles = {}; var _ideActiveFile = null; var _ideOpenTabs = []; var _ideSaveTimer = null; -var _ideMode = false; // false=single file, true=project mode +var _ideMode = false; var ProjectStore = (function() { var DB_NAME = 'octra_ide'; @@ -787,37 +1130,66 @@ async function bgStealthScan() { } catch (e) {} } -async function fetchBalance() { - try { - var bal = await api('GET', '/balance'); - _cachedBal = bal; - var pub = bal.public_balance || '0'; - var enc = bal.encrypted_balance || '0'; - _encryptedBalanceRaw = parseInt(enc) || 0; - var MAX_SANE_ENC = 100000000 * 1000000; - var encCorrupt = (_encryptedBalanceRaw < 0 || _encryptedBalanceRaw > MAX_SANE_ENC); - if (encCorrupt) _encryptedBalanceRaw = 0; - if ($('btn-key-switch')) $('btn-key-switch').style.display = encCorrupt ? '' : 'none'; - if ($('st-balance')) $('st-balance').textContent = fmtOct(pub); - if ($('st-enc-balance')) $('st-enc-balance').textContent = encCorrupt - ? 'corrupted ciphertext' : fmtOct(enc); - if ($('st-nonce')) $('st-nonce').textContent = bal.nonce || '0'; - if ($('st-staging')) $('st-staging').textContent = bal.staging || '0'; - if ($('send-bal')) $('send-bal').textContent = fmtOct(pub); - if ($('enc-pub-bal')) $('enc-pub-bal').textContent = fmtOct(pub); - if ($('enc-enc-bal')) $('enc-enc-bal').textContent = encCorrupt - ? 'corrupted ciphertext' : fmtOct(enc); - if ($('st-enc-bal-info')) $('st-enc-bal-info').textContent = encCorrupt - ? 'corrupted ciphertext' : fmtOct(enc); - if ($('ct-bal')) $('ct-bal').textContent = fmtOct(pub); - $('hdr-status').textContent = _rpcHost ? 'online | ' + networkLabel(_rpcHost) : 'online'; - $('hdr-status').className = 'right online'; - return bal; - } catch (e) { - $('hdr-status').textContent = 'offline'; - $('hdr-status').className = 'right error'; - return null; +function applyBalanceData(bal) { + _cachedBal = bal; + var pub = bal.public_balance || '0'; + var enc = bal.encrypted_balance || '0'; + _encryptedBalanceRaw = parseInt(enc) || 0; + var MAX_SANE_ENC = 100000000 * 1000000; + var encCorrupt = (_encryptedBalanceRaw < 0 || _encryptedBalanceRaw > MAX_SANE_ENC); + if (encCorrupt) _encryptedBalanceRaw = 0; + if ($('btn-key-switch')) $('btn-key-switch').style.display = encCorrupt ? '' : 'none'; + if ($('st-balance')) $('st-balance').textContent = fmtOct(pub); + if ($('st-enc-balance')) $('st-enc-balance').textContent = encCorrupt + ? 'corrupted ciphertext' : fmtOct(enc); + if ($('st-nonce')) $('st-nonce').textContent = bal.nonce || '0'; + if ($('st-staging')) $('st-staging').textContent = bal.staging || '0'; + if ($('send-bal')) $('send-bal').textContent = fmtOct(pub); + if ($('enc-pub-bal')) $('enc-pub-bal').textContent = fmtOct(pub); + if ($('enc-enc-bal')) $('enc-enc-bal').textContent = encCorrupt + ? 'corrupted ciphertext' : fmtOct(enc); + if ($('st-enc-bal-info')) $('st-enc-bal-info').textContent = encCorrupt + ? 'corrupted ciphertext' : fmtOct(enc); + if ($('ct-bal')) $('ct-bal').textContent = fmtOct(pub); + $('hdr-status').textContent = _rpcHost ? 'online | ' + networkLabel(_rpcHost) : 'online'; + $('hdr-status').className = 'right online'; +} + +async function fetchBalance(force) { + var state = ensureAddressRuntime(_walletAddr); + if (state && state.balanceInflight) return state.balanceInflight; + if (!force && state && state.balance && (Date.now() - state.balanceTs) < BALANCE_CACHE_TTL_MS) { + applyBalanceData(state.balance); + return state.balance; + } + if (!force && state && !state.balance) { + var persisted = restorePersistedBalance(_walletAddr); + if (persisted) { + state.balance = persisted; + state.balanceTs = Date.now(); + applyBalanceData(persisted); + } } + var request = api('GET', '/balance') + .then(function(bal) { + if (state) { + state.balance = bal; + state.balanceTs = Date.now(); + } + persistBalance(_walletAddr, bal); + applyBalanceData(bal); + return bal; + }) + .catch(function() { + $('hdr-status').textContent = 'offline'; + $('hdr-status').className = 'right error'; + return null; + }) + .finally(function() { + if (state) state.balanceInflight = null; + }); + if (state) state.balanceInflight = request; + return request; } async function api(method, path, body) { @@ -1034,8 +1406,8 @@ function opTag(op) { function statusTag(st) { if (st === 'confirmed') return 'confirmed'; if (st === 'rejected') return 'rejected'; - if (st === 'pending') return 'pending'; - return '' + escapeHtml(st || 'pending') + ''; + if (st === 'pending') return 'pending'; + return '' + escapeHtml(st || 'pending') + ''; } function showResult(elId, ok, msg) { @@ -1094,8 +1466,8 @@ function clearDecryptLog() { function txStatusTag(st) { if (st === 'rejected') return 'rejected'; if (st === 'confirmed') return 'confirmed'; - if (st === 'pending') return 'pending'; - return '' + escapeHtml(st || 'pending') + ''; + if (st === 'pending') return 'pending'; + return '' + escapeHtml(st || 'pending') + ''; } function txAmt(tx) { @@ -1207,6 +1579,7 @@ function dashTxLimit() { } function renderDashTxs(txs) { + $('dash-tx-count').textContent = String(txs.length); var h = ''; var cards = '
'; for (var i = 0; i < txs.length; i++) { @@ -1220,13 +1593,27 @@ function renderDashTxs(txs) { } async function loadDashboard() { - await fetchBalance(); + fetchBalance(false); loadTokenSymbols(); try { var lim = dashTxLimit(); - var hist = await api('GET', '/history?limit=' + lim + '&offset=0'); + var cached = peekHistoryPage(_walletAddr, lim, 0); + if (cached) { + var cachedTxs = cached.response.transactions || []; + if (cachedTxs.length === 0) { + $('dash-tx-count').textContent = '0'; + $('dash-txs').innerHTML = '
no transactions yet
'; + $('dash-more').innerHTML = ''; + } else { + renderDashTxs(cachedTxs); + fetchMissingSymbols(cachedTxs).then(function() { renderDashTxs(cachedTxs); }); + } + if ((Date.now() - cached.ts) <= HISTORY_STALE_REFRESH_MS) return; + } + var hist = await fetchHistoryPage(lim, 0, false); var txs = hist.transactions || []; if (txs.length === 0) { + $('dash-tx-count').textContent = '0'; $('dash-txs').innerHTML = '
no transactions yet
'; $('dash-more').innerHTML = ''; return; @@ -1234,6 +1621,7 @@ async function loadDashboard() { renderDashTxs(txs); fetchMissingSymbols(txs).then(function() { renderDashTxs(txs); }); } catch (e) { + $('dash-tx-count').textContent = '0'; $('dash-txs').innerHTML = '
no transactions yet
'; $('dash-more').innerHTML = ''; } @@ -1258,6 +1646,7 @@ async function doSend() { if (fee) body.ou = fee; var res = await api('POST', '/send', body); var txHash = res.hash || res.tx_hash || ''; + invalidateCurrentAddressState(); showResult('send-result', true, 'sent ' + amount + ' oct - tx: ' + txLink(txHash)); $('send-to').value = ''; $('send-amount').value = ''; @@ -1298,6 +1687,7 @@ async function doKeySwitch() { try { var res = await api('POST', '/key_switch', {}); var txHash = res.hash || res.tx_hash || ''; + invalidateCurrentAddressState(); var h2 = '
key switch submitted
'; h2 += '
tx: ' + txLinkExt(txHash) + '
'; h2 += '
'; @@ -1320,6 +1710,7 @@ async function doEncrypt() { if (encFee) encBody.ou = encFee; var res = await api('POST', '/encrypt', encBody); var txHash = res.hash || res.tx_hash || ''; + invalidateCurrentAddressState(); showResult('enc-result', true, 'encrypted ' + amount + ' oct - tx: ' + txLink(txHash)); $('enc-amount').value = ''; loadDashboard(); @@ -1346,6 +1737,7 @@ async function doDecrypt() { var decFee = $('dec-fee') ? $('dec-fee').value.trim() : ''; if (decFee) decBody.ou = decFee; var res = await api('POST', '/decrypt', decBody); + invalidateCurrentAddressState(); if (res.steps) { for (var i = 0; i < res.steps.length; i++) logDecrypt(res.steps[i], 'log-info'); } @@ -1389,6 +1781,7 @@ async function doStealthSend() { var stFee = $('stealth-fee') ? $('stealth-fee').value.trim() : ''; if (stFee) stBody.ou = stFee; var res = await api('POST', '/stealth/send', stBody); + invalidateCurrentAddressState(); if (res.steps) { for (var i = 0; i < res.steps.length; i++) logStealth(res.steps[i], 'log-info'); } @@ -1470,6 +1863,7 @@ async function doStealthClaim(ids) { logStealth('claiming ' + ids.length + ' output(s)...', 'log-info'); try { var res = await api('POST', '/stealth/claim', { ids: ids }); + invalidateCurrentAddressState(); logStealth('claim complete', 'log-ok'); if (res.results) { for (var i = 0; i < res.results.length; i++) { @@ -1851,6 +2245,7 @@ async function doDeploy() { var res = await api('POST', '/contract/deploy', body); var addr = res.contract_address || ''; var hash = res.tx_hash || ''; + invalidateCurrentAddressState(); showResult('ct-deploy-result', true, 'deployed to ' + escapeHtml(addr) + ' - tx: ' + txLink(hash) + ' (verifying source...)'); $('ct-call-addr').value = addr; @@ -1895,8 +2290,9 @@ async function doContractCall() { if (callFee) callBody.ou = callFee; var res = await api('POST', '/contract/call', callBody); var hash = res.tx_hash || ''; + invalidateCurrentAddressState(); showResult('ct-call-result', true, 'call submitted - tx: ' + txLink(hash)); - consoleLog('event', 'call ' + method + '() → tx ' + (hash ? hash.slice(0,16) + '...' : '')); + consoleLog('event', 'call ' + method + '() -> tx ' + (hash ? hash.slice(0,16) + '...' : '')); loadDashboard(); } catch (e) { showResult('ct-call-result', false, e.message); @@ -1955,10 +2351,10 @@ async function doContractView() { showResult('ct-call-result', true, 'result (encrypted): ' + escapeHtml(String(val)).substring(0, 40) + '...' + '
decrypted: ' + decrypted + ''); - consoleLog('log', 'view ' + method + '() → ' + decrypted + ' (decrypted)'); + consoleLog('log', 'view ' + method + '() -> ' + decrypted + ' (decrypted)'); } else { showResult('ct-call-result', true, 'result: ' + escapeHtml(String(val)) + ''); - consoleLog('log', 'view ' + method + '() → ' + String(val)); + consoleLog('log', 'view ' + method + '() -> ' + String(val)); } if (res.storage) updateStorageView(res.storage); if (res.events && res.events.length > 0) { @@ -2057,15 +2453,13 @@ async function doVerifyContract() { } async function loadTokenSymbols() { - if (_tokensLoaded) return; + var cached = restoreAddressTokens(_walletAddr); + var state = ensureAddressRuntime(_walletAddr); + if (cached && state && (Date.now() - state.tokensTs) <= TOKEN_STALE_REFRESH_MS) return; try { - var res = await api('GET', '/tokens'); - _tokens = res.tokens || []; + _tokens = await fetchAddressTokens(false); _tokensLoaded = true; - for (var i = 0; i < _tokens.length; i++) { - _tokenSymbols[_tokens[i].address] = _tokens[i].symbol; - _tokenDecimals[_tokens[i].address] = _tokens[i].decimals || '0'; - } + hydrateTokenMaps(_tokens); } catch(e) {} } @@ -2081,31 +2475,80 @@ async function fetchMissingSymbols(txs) { var unknowns = Object.keys(need); if (unknowns.length === 0) return; await Promise.all(unknowns.map(function(ca) { - return Promise.all([ + if (_tokenMetaInflight[ca]) return _tokenMetaInflight[ca]; + _tokenMetaInflight[ca] = Promise.all([ api('GET', '/contract-storage?address=' + encodeURIComponent(ca) + '&key=symbol').then(function(r) { if (r && r.value) _tokenSymbols[ca] = String(r.value).slice(0, 32); }).catch(function() {}), api('GET', '/contract-storage?address=' + encodeURIComponent(ca) + '&key=decimals').then(function(r) { if (r && r.value) _tokenDecimals[ca] = String(r.value); }).catch(function() {}) - ]); + ]).finally(function() { + delete _tokenMetaInflight[ca]; + }); + return _tokenMetaInflight[ca]; })); } async function loadTokens() { $('tok-list').innerHTML = '
loading tokens...
'; + var restored = restoreAddressTokens(_walletAddr); + var state = ensureAddressRuntime(_walletAddr); + if (restored) { + renderTokenList(); + if (state && (Date.now() - state.tokensTs) <= TOKEN_STALE_REFRESH_MS) { + loadTokenTxs(); + return; + } + } try { - var res = await api('GET', '/tokens'); - _tokens = res.tokens || []; + _tokens = await fetchAddressTokens(false); _tokensLoaded = true; - for (var i = 0; i < _tokens.length; i++) { - _tokenSymbols[_tokens[i].address] = _tokens[i].symbol; - _tokenDecimals[_tokens[i].address] = _tokens[i].decimals || '0'; - } - renderTokenList(); - loadTokenTxs(); + hydrateTokenMaps(_tokens); } catch (e) { - $('tok-list').innerHTML = '
' + e.message + '
'; + if (!restored) $('tok-list').innerHTML = '
' + e.message + '
'; + loadTokenTxs(); + return; + } + renderTokenList(); + loadTokenTxs(); +} + +async function loadTokenTxs() { + var el = $('tok-txs'); + if (!el) return; + var gen = ++_tokTxGen; + try { + var hist = await fetchTokenHistory(false); + if ((!hist.total || hist.total === 0) && _tokens && _tokens.length > 0) { + hist = await fetchTokenHistory(true); + } + if (gen !== _tokTxGen) return; + var filtered = hist.transactions || []; + $('tok-tx-count').textContent = String(filtered.length); + $('tok-tx-total').textContent = String(hist.total || filtered.length); + $('tok-tx-in').textContent = String(hist.incoming || 0); + $('tok-tx-out').textContent = String(hist.outgoing || 0); + if (filtered.length === 0) { + el.innerHTML = '
no token transactions yet
'; + return; + } + await fetchMissingSymbols(filtered); + var h = '
hashfromtoamountstatustime
'; + var cards = '
'; + for (var j = 0; j < filtered.length; j++) { + h += txRow(filtered[j]); + cards += txCardHtml(filtered[j]); + } + h += '
hashfromtoamountstatustime
'; + cards += ''; + el.innerHTML = h + cards; + } catch(e) { + $('tok-tx-count').textContent = '0'; + $('tok-tx-total').textContent = '0'; + $('tok-tx-in').textContent = '0'; + $('tok-tx-out').textContent = '0'; + el.innerHTML = '
no token transactions yet
'; } } @@ -2187,6 +2630,7 @@ async function doTokenTransfer() { if (tokFee) tokBody.ou = tokFee; var res = await api('POST', '/token/transfer', tokBody); var txHash = res.hash || res.tx_hash || ''; + invalidateCurrentAddressState(); showResult('tok-transfer-result', true, 'sent ' + humanAmt + ' ' + _selectedToken.symbol + ' - tx: ' + txLink(txHash)); $('tok-to').value = ''; @@ -2197,39 +2641,8 @@ async function doTokenTransfer() { } } -async function loadTokenTxs() { - var el = $('tok-txs'); - if (!el) return; - var gen = ++_tokTxGen; - try { - var filtered = []; - var hist = await api('GET', '/history?limit=500&offset=0'); - if (gen !== _tokTxGen) return; - var txs = hist.transactions || []; - for (var i = 0; i < txs.length; i++) { - var t = txs[i]; - if (t.op_type === 'call' && t.encrypted_data === 'transfer') filtered.push(t); - } - if (filtered.length === 0) { - el.innerHTML = '
no token transactions yet
'; - return; - } - await fetchMissingSymbols(filtered); - var h = ''; - var cards = '
'; - for (var i = 0; i < filtered.length; i++) { - h += txRow(filtered[i]); - cards += txCardHtml(filtered[i]); - } - h += '
hashfromtoamountstatustime
'; - cards += ''; - el.innerHTML = h + cards; - } catch(e) { - el.innerHTML = '
no token transactions yet
'; - } -} - function renderHistoryTxs(txs) { + $('hist-count').textContent = String(_historyOffset + txs.length); var h = ''; var cards = '
'; for (var i = 0; i < txs.length; i++) { @@ -2242,22 +2655,41 @@ function renderHistoryTxs(txs) { } async function loadHistory() { - $('history-list').innerHTML = '
loading...
'; + var cached = peekHistoryPage(_walletAddr, _historyLimit, _historyOffset); + if (!cached) $('history-list').innerHTML = '
loading...
'; $('history-more').innerHTML = ''; loadTokenSymbols(); try { - var res = await api('GET', '/history?limit=' + _historyLimit + '&offset=' + _historyOffset); + if (cached) { + var cachedTxs = cached.response.transactions || []; + $('hist-total').textContent = String(cached.response.total || cachedTxs.length); + if (cachedTxs.length === 0 && _historyOffset === 0) { + $('hist-count').textContent = '0'; + $('history-list').innerHTML = '
no transactions yet
'; + } else { + renderHistoryTxs(cachedTxs); + if (cached.response.has_more) { + $('history-more').innerHTML = ''; + } + fetchMissingSymbols(cachedTxs).then(function() { renderHistoryTxs(cachedTxs); }); + } + if ((Date.now() - cached.ts) <= HISTORY_STALE_REFRESH_MS) return; + } + var res = await fetchHistoryPage(_historyLimit, _historyOffset, false); var txs = res.transactions || []; + $('hist-total').textContent = String(res.total || txs.length); if (txs.length === 0 && _historyOffset === 0) { + $('hist-count').textContent = '0'; $('history-list').innerHTML = '
no transactions yet
'; return; } renderHistoryTxs(txs); - if (txs.length >= _historyLimit) { + if (res.has_more) { $('history-more').innerHTML = ''; } fetchMissingSymbols(txs).then(function() { renderHistoryTxs(txs); }); } catch (e) { + $('hist-count').textContent = '0'; $('history-list').innerHTML = '
' + e.message + '
'; } } @@ -2271,8 +2703,9 @@ async function loadHistoryAppend() { var btn = $('history-more').querySelector('button'); if (btn) { btn.disabled = true; btn.textContent = 'loading...'; } try { - var res = await api('GET', '/history?limit=' + _historyLimit + '&offset=' + _historyOffset); + var res = await fetchHistoryPage(_historyLimit, _historyOffset, false); var txs = res.transactions || []; + $('hist-total').textContent = String(res.total || txs.length); if (txs.length === 0) { $('history-more').innerHTML = '
no more transactions
'; return; @@ -2286,7 +2719,26 @@ async function loadHistoryAppend() { } if (cardList) cardList.insertAdjacentHTML('beforeend', txCardHtml(txs[i])); } - if (txs.length >= _historyLimit) { + fetchMissingSymbols(txs).then(function() { + if (tbl) { + for (var j = 0; j < txs.length; j++) { + var rowIndex = tbl.rows.length - txs.length + j; + if (rowIndex > 0 && tbl.rows[rowIndex]) tbl.rows[rowIndex].innerHTML = txRow(txs[j]).replace(/<\/?tr>/g, ''); + } + } + if (cardList) { + var all = $('history-list').querySelector('.card-list'); + if (all) { + var cards = all.querySelectorAll('.tx-card'); + for (var k = 0; k < txs.length; k++) { + var cardIndex = cards.length - txs.length + k; + if (cardIndex >= 0 && cards[cardIndex]) cards[cardIndex].outerHTML = txCardHtml(txs[k]); + } + } + } + }); + $('hist-count').textContent = String(_historyOffset + txs.length); + if (res.has_more) { $('history-more').innerHTML = ''; } else { $('history-more').innerHTML = ''; @@ -2539,6 +2991,8 @@ async function doSaveSettings() { if (explorer) _explorerUrl = explorer.replace(/\/+$/, ''); try { _rpcHost = new URL(rpc).hostname; } catch(e) { _rpcHost = rpc; } if (resp && resp.cache_cleared) { + clearAllAddressRuntime(); + dropAllPersistedRuntime(); _cachedBal = null; _historyOffset = 0; _tokens = []; @@ -2551,7 +3005,7 @@ async function doSaveSettings() { fetchBalance(); if (document.querySelector('.nav-tabs a.active[data-view="dashboard"]')) loadDashboard(); - showResult('settings-result', true, 'saved · cache cleared'); + showResult('settings-result', true, 'saved | cache cleared'); } else { showResult('settings-result', true, 'saved'); } @@ -2807,13 +3261,23 @@ async function modalFinishSetup() { async function loadWalletInfo() { try { var w = await api('GET', '/wallet'); + var prevAddr = _walletAddr; _walletAddr = w.address || w.addr || ''; + ensureAddressRuntime(_walletAddr); + if (prevAddr !== _walletAddr) { + _cachedBal = null; + _historyOffset = 0; + _tokens = []; + _tokensLoaded = false; + restoreAddressTokens(_walletAddr); + } if (w.explorer_url) _explorerUrl = w.explorer_url.replace(/\/+$/, ''); if (w.rpc_url) try { _rpcHost = new URL(w.rpc_url).hostname; } catch(e) { _rpcHost = w.rpc_url; } _hasMasterSeed = !!w.has_master_seed; $('hdr-addr').innerHTML = '' + _walletAddr + ''; $('hdr-logout').style.display = ''; $('hdr-dev').style.display = ''; + $('hdr-circles').style.display = ''; $('hdr-apps').style.display = ''; fetchFees(); loadDashboard(); @@ -2827,12 +3291,18 @@ async function loadWalletInfo() { async function doLogout() { try { await api('POST', '/wallet/lock', {}); } catch (e) {} if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; } + clearAllAddressRuntime(); _walletAddr = ''; _cachedBal = null; _encryptedBalanceRaw = 0; _hasMasterSeed = false; + _tokens = []; + _tokensLoaded = false; + _tokenSymbols = {}; + _tokenDecimals = {}; $('hdr-logout').style.display = 'none'; $('hdr-dev').style.display = 'none'; + $('hdr-circles').style.display = 'none'; $('hdr-apps').style.display = 'none'; $('hdr-addr').textContent = 'locked'; $('hdr-status').textContent = 'locked'; @@ -2845,11 +3315,15 @@ function startRefreshTimer() { if (_refreshTimer) return; bgStealthScan(); _refreshTimer = setInterval(function() { - fetchBalance(); + fetchBalance(true); bgStealthScan(); fetchFees(); var dash = $('view-dashboard'); + var tok = $('view-tokens'); + var hist = $('view-history'); if (dash && dash.classList.contains('active')) loadDashboard(); + if (tok && tok.classList.contains('active')) loadTokens(); + if (hist && hist.classList.contains('active') && _historyOffset === 0) loadHistory(); }, 15000); } @@ -2868,7 +3342,7 @@ function showAccountPicker(wallets) { var sub = hasAddr ? a.addr.substring(0, 12) + '...' + a.addr.substring(a.addr.length - 6) : a.file; - var hdTag = a.hd ? ' · hd' : ''; + var hdTag = a.hd ? ' | hd' : ''; var dataAttr = hasAddr ? 'data-addr="' + a.addr + '"' : 'data-file="' + a.file + '"'; From 27748c886f8ab06a94bd553ac266ef7af1695df0 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Sat, 23 May 2026 19:21:25 +0000 Subject: [PATCH 08/13] minor updates in circles --- README.md | 2 +- lib/circle_hfhe_receipt.hpp | 230 +++ lib/tx_builder.hpp | 18 +- main.cpp | 3719 +++++++++++++++++++++++++++++++++-- rpc_client.hpp | 362 ++++ setup.sh | 31 +- static/circles.html | 633 +++++- static/wallet.js | 7 +- 8 files changed, 4779 insertions(+), 223 deletions(-) create mode 100644 lib/circle_hfhe_receipt.hpp diff --git a/README.md b/README.md index aacf2b2..36c4ca1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # octra wallet (webcli) -![Version](https://img.shields.io/badge/version-0.04.10--alpha-blue) +![Version](https://img.shields.io/badge/version-0.04.12--alpha-blue) a full-fledged web client based on a local server for working with the octra network (compatible with both **DEVNET** and **MAINNET ALPHA**). diff --git a/lib/circle_hfhe_receipt.hpp b/lib/circle_hfhe_receipt.hpp new file mode 100644 index 0000000..183e1e3 --- /dev/null +++ b/lib/circle_hfhe_receipt.hpp @@ -0,0 +1,230 @@ + +/* + This file is part of Octra Wallet (webcli). + + Octra Wallet is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + Octra Wallet is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Octra Wallet. If not, see . + + This program is released under the GPL with the additional exemption + that compiling, linking, and/or using OpenSSL is allowed. + You are free to remove this exemption from derived works. + + Copyright 2025-2026 Octra Labs + 2026 lambda0xe +*/ + + + +#pragma once + + + + + + +#include +#include + +#include "json.hpp" +#include "tx_builder.hpp" + +namespace octra { + + + + +struct CircleHfheReceiptContext { + std::string circle_id; + std::string caller_addr; + std::string key_id; + std::string intent_id; + std::string verb; + std::string proof_kind; + std::string policy_hash; + std::string ciphertext_hash; + std::string amount_commitment_hash; +}; + +inline std::string circle_hfhe_receipt_subject(const CircleHfheReceiptContext& ctx) { + return "octra_circle_hfhe_receipt_v1|" + + ctx.verb + "|" + + ctx.proof_kind + "|" + + ctx.circle_id + "|" + + ctx.caller_addr + "|" + + ctx.key_id + "|" + + ctx.intent_id + "|" + + ctx.policy_hash + "|" + + ctx.ciphertext_hash + "|" + + ctx.amount_commitment_hash; +} + +inline std::string circle_hfhe_hash_b64_payload(const std::string& encoded, + std::string& error) { + auto raw = base64_decode(encoded); + if (raw.empty()) { + error = "invalid base64 payload"; + return ""; + } + return hex_encode(sha256(raw.data(), raw.size()).data(), 32); +} + +inline std::string circle_hfhe_hash_ciphertext(const std::string& ciphertext_b64, + std::string& error) { + return circle_hfhe_hash_b64_payload(ciphertext_b64, error); +} + +inline std::string circle_hfhe_hash_commitment(const std::string& amount_commitment_b64, + std::string& error) { + auto raw = base64_decode(amount_commitment_b64); + if (raw.size() != 32) { + error = "invalid amount commitment"; + return ""; + } + return hex_encode(sha256(raw.data(), raw.size()).data(), 32); +} + +inline std::string derive_address_from_pubkey_b64(const std::string& pub_b64) { + auto raw = base64_decode(pub_b64); + if (raw.size() != 32) { + return ""; + } + auto h = sha256(raw.data(), raw.size()); + std::string b58 = base58_encode(h.data(), 32); + while (b58.size() < 44) { + b58 = "1" + b58; + } + return "oct" + b58; +} + +inline bool ed25519_verify_detached(const std::string& message, + const std::string& sig_b64, + const std::string& pub_b64) { + auto sig = base64_decode(sig_b64); + auto pub = base64_decode(pub_b64); + if (sig.size() != 64 || pub.size() != 32) { + return false; + } + std::vector signed_msg(sig.size() + message.size()); + std::memcpy(signed_msg.data(), sig.data(), sig.size()); + std::memcpy(signed_msg.data() + sig.size(), message.data(), message.size()); + std::vector opened(message.size() + 64); + unsigned long long opened_len = 0; + return crypto_sign_open( + opened.data(), + &opened_len, + signed_msg.data(), + signed_msg.size(), + pub.data()) == 0; +} + +inline nlohmann::json make_circle_hfhe_receipt_json(const CircleHfheReceiptContext& ctx, + const std::string& signer_addr, + const std::string& signer_pub_b64, + const uint8_t signer_sk[64]) { + + + + + nlohmann::json receipt; + receipt["version"] = "octra_circle_hfhe_receipt_v1"; + receipt["verb"] = ctx.verb; + receipt["proof_kind"] = ctx.proof_kind; + receipt["circle_id"] = ctx.circle_id; + + receipt["caller_addr"] = ctx.caller_addr; + receipt["key_id"] = ctx.key_id; + receipt["intent_id"] = ctx.intent_id; + receipt["policy_hash"] = ctx.policy_hash; + + + receipt["ciphertext_hash"] = ctx.ciphertext_hash; + receipt["amount_commitment_hash"] = ctx.amount_commitment_hash; + receipt["signer_addr"] = signer_addr; + receipt["signer_pubkey"] = signer_pub_b64; + + + + + + const std::string subject = circle_hfhe_receipt_subject(ctx); + receipt["signature"] = ed25519_sign_detached( + reinterpret_cast(subject.data()), + subject.size(), + signer_sk); + return receipt; +} + +inline bool verify_circle_hfhe_receipt_json(const nlohmann::json& receipt, + const CircleHfheReceiptContext& ctx, + std::string& error) { + if (!receipt.is_object()) { + error = "proof_receipt must be an object"; + return false; + } + + + + + + const std::string version = receipt.value("version", ""); + const std::string verb = receipt.value("verb", ""); + const std::string proof_kind = receipt.value("proof_kind", ""); + const std::string circle_id = receipt.value("circle_id", ""); + + const std::string caller_addr = receipt.value("caller_addr", ""); + const std::string key_id = receipt.value("key_id", ""); + const std::string intent_id = receipt.value("intent_id", ""); + const std::string policy_hash = receipt.value("policy_hash", ""); + const std::string ciphertext_hash = receipt.value("ciphertext_hash", ""); + const std::string amount_commitment_hash = receipt.value("amount_commitment_hash", ""); + const std::string signer_addr = receipt.value("signer_addr", ""); + const std::string signer_pubkey = receipt.value("signer_pubkey", ""); + const std::string signature = receipt.value("signature", ""); + // done here btw + + + + + if (version != "octra_circle_hfhe_receipt_v1") { + error = "invalid proof receipt version"; + return false; + } + if (verb != ctx.verb || proof_kind != ctx.proof_kind || circle_id != ctx.circle_id || + caller_addr != ctx.caller_addr || key_id != ctx.key_id || intent_id != ctx.intent_id || + policy_hash != ctx.policy_hash || ciphertext_hash != ctx.ciphertext_hash || + + amount_commitment_hash != ctx.amount_commitment_hash) { + error = "proof receipt context mismatch"; + return false; + } + + + const std::string derived_addr = derive_address_from_pubkey_b64(signer_pubkey); + if (derived_addr.empty() || derived_addr != signer_addr) { + error = "proof receipt signer binding invalid"; + return false; + } + if (!ed25519_verify_detached(circle_hfhe_receipt_subject(ctx), signature, signer_pubkey)) { + + + // + + error = "proof receipt signature verification failed"; // would be necessary to expand it with more support later + + + return false; + } + return true; +} + +} \ No newline at end of file diff --git a/lib/tx_builder.hpp b/lib/tx_builder.hpp index c51fdf5..5a60656 100644 --- a/lib/tx_builder.hpp +++ b/lib/tx_builder.hpp @@ -69,7 +69,10 @@ inline std::string json_escape(const std::string& s) { case '\n': r += "\\n"; break; case '\r': r += "\\r"; break; case '\t': r += "\\t"; break; - default: r += c; + + + + default: r += c; } } return r; @@ -148,6 +151,19 @@ inline std::string sha256_hex(const std::string& data) { return hex; } +inline std::string sign_circle_read_request(const std::string& op, + const std::string& circle_id, + const std::string& addr, + const std::string& subject, + const uint8_t sk[64]) { + std::string msg = op + "|" + circle_id + "|" + addr; + if (!subject.empty()) { + msg += "|" + subject; + } + return ed25519_sign_detached( + reinterpret_cast(msg.data()), msg.size(), sk); +} + inline std::string sign_register_request(const std::string& addr, const std::string& pk_blob, const uint8_t sk[64]) { diff --git a/main.cpp b/main.cpp index 4938ec7..407345e 100644 --- a/main.cpp +++ b/main.cpp @@ -61,6 +61,7 @@ extern "C" { #include "crypto_utils.hpp" #include "wallet.hpp" #include "rpc_client.hpp" +#include "lib/circle_hfhe_receipt.hpp" #include "lib/tx_builder.hpp" #include "lib/pvac_bridge.hpp" #include "lib/stealth.hpp" @@ -121,6 +122,37 @@ static std::string current_public_rpc_url() { return "http://127.0.0.1:8080"; } +struct RelayProxyResult { + bool ok = false; + int status = 0; + std::string body; + std::string error; +}; + +static std::string current_circle_relayer_url() { + const char* env_relayer = std::getenv("OCTRA_CIRCLE_RELAYER_URL"); + if (env_relayer && *env_relayer) return env_relayer; + return "http://127.0.0.1:9494"; +} + +static RelayProxyResult relay_http_get(const std::string& path) { + httplib::Client cli(current_circle_relayer_url()); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(30, 0); + auto r = cli.Get(path.c_str()); + if (!r) return {false, 502, "", "relay unavailable"}; + return {true, r->status, r->body, ""}; +} + +static RelayProxyResult relay_http_post(const std::string& path, const std::string& body) { + httplib::Client cli(current_circle_relayer_url()); + cli.set_connection_timeout(5, 0); + cli.set_read_timeout(30, 0); + auto r = cli.Post(path.c_str(), body, "application/json"); + if (!r) return {false, 502, "", "relay unavailable"}; + return {true, r->status, r->body, ""}; +} + static void pk_cache_put(const std::string& addr, const std::vector& pk) { if (pk.size() != 32) return; std::lock_guard lk(g_pk_mtx); @@ -272,6 +304,25 @@ static std::string parse_ou(const json& body, const std::string& fallback) { return fallback; } +static constexpr size_t CIRCLE_ASSET_MAX_RAW_BYTES = 33554432; +static constexpr size_t CIRCLE_ASSET_MAX_B64_BYTES = ((CIRCLE_ASSET_MAX_RAW_BYTES + 2) / 3) * 4; + +static size_t circle_asset_decoded_size_upper_bound(size_t wire_len) { + return ((wire_len + 3) / 4) * 3; +} + +static int64_t circle_asset_ou_from_b64_len(size_t wire_len) { + const size_t raw_upper_bound = circle_asset_decoded_size_upper_bound(wire_len); + if (raw_upper_bound <= 4096) return 5000; + if (raw_upper_bound <= 16384) return 10000; + if (raw_upper_bound <= 32768) return 20000; + if (raw_upper_bound <= 131072) return 40000; + if (raw_upper_bound <= 524288) return 80000; + if (raw_upper_bound <= 2097152) return 160000; + if (raw_upper_bound <= 8388608) return 320000; + return 640000; +} + static const int64_t MAX_OCT_RAW = 1000000000LL * 1000000LL; static int64_t parse_amount_raw(const json& body) { @@ -343,6 +394,472 @@ static void sign_tx_fields(octra::Transaction& tx) { tx.public_key = g_wallet.pub_b64; } +static std::string sign_circle_read_request(const std::string& op, + const std::string& circle_id, + const std::string& subject = "") { + return octra::sign_circle_read_request( + op, + circle_id, + g_wallet.addr, + subject, + g_wallet.sk); +} + +static std::string sign_circle_view_request(const std::string& circle_id, + const std::string& method, + const json& params, + bool include_storage) { + const std::string params_hash = octra::sha256_hex(params.dump()); + const std::string subject = + method + "|" + params_hash + "|" + (include_storage ? "1" : "0"); + return sign_circle_read_request("octra_circle_view", circle_id, subject); +} + +static octra::RpcResult circle_info_auth_rpc(const std::string& circle_id) { + octra::RpcClient rpc(current_public_rpc_url()); + return rpc.circle_info_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_info", circle_id)); +} + +static octra::RpcResult circle_hfhe_policy_auth_rpc(const std::string& circle_id) { + octra::RpcClient rpc(current_public_rpc_url()); + return rpc.circle_hfhe_policy_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_hfhe_policy", circle_id)); +} + +static octra::RpcResult circle_key_policy_auth_rpc(const std::string& circle_id, + const std::string& key_id) { + octra::RpcClient rpc(current_public_rpc_url()); + return rpc.circle_key_policy_auth( + circle_id, + key_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_key_policy", circle_id, key_id)); +} + +static octra::RpcResult circle_outbox_status_auth_rpc(const std::string& circle_id, + const std::string& intent_id) { + octra::RpcClient rpc(current_public_rpc_url()); + return rpc.circle_outbox_status_auth( + circle_id, + intent_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_outbox_status", circle_id, intent_id)); +} + +static bool circle_string_list_contains(const json& values, const std::string& target) { + if (!values.is_array()) return false; + for (const auto& value : values) { + if (value.is_string() && value.get() == target) { + return true; + } + } + return false; +} + +static bool circle_hfhe_mode_allows(const std::string& mode, + const std::string& owner, + const std::string& caller, + const std::string& subject, + const std::vector& active_relays) { + const bool caller_is_active_relay = + std::find(active_relays.begin(), active_relays.end(), caller) != active_relays.end(); + if (mode == "deny") return false; + if (mode == "owner_only") return caller == owner; + if (mode == "caller_self") return caller == subject; + if (mode == "owner_or_caller") return caller == owner || caller == subject; + if (mode == "any_registered") return !caller.empty(); + if (mode == "active_relay") return caller_is_active_relay; + if (mode == "owner_or_active_relay") return caller == owner || caller_is_active_relay; + return false; +} + +static bool circle_hfhe_pk_allowed(const json& policy, const std::string& requested_addr) { + if (!policy.contains("pk_allowlist") || policy["pk_allowlist"].is_null()) { + return true; + } + return circle_string_list_contains(policy["pk_allowlist"], requested_addr); +} + +static bool circle_key_policy_live(const std::string& circle_id, + const std::string& key_id, + std::string& error) { + auto r = circle_key_policy_auth_rpc(circle_id, key_id); + if (!r.ok) { + error = r.error.empty() ? "circle key policy read failed" : r.error; + return false; + } + if (!r.result.contains("live") || !r.result["live"].is_boolean()) { + error = "circle key policy live status unavailable"; + return false; + } + if (!r.result["live"].get()) { + error = "circle key policy is not live"; + return false; + } + return true; +} + +static bool circle_hfhe_active_relays(const std::string& circle_id, + const std::string& intent_id, + std::vector& active_relays, + std::string& error) { + auto status_r = circle_outbox_status_auth_rpc(circle_id, intent_id); + if (!status_r.ok) { + error = status_r.error.empty() ? "circle outbox status read failed" : status_r.error; + return false; + } + if (status_r.result.value("status", "") != "claimed") { + error = "circle outbox intent is not actively claimed"; + return false; + } + if (!status_r.result.value("claim_ready", false)) { + error = "circle outbox intent relay quorum is not ready"; + return false; + } + active_relays.clear(); + const auto active_claims = status_r.result.value("active_claims", json::array()); + for (const auto& claim : active_claims) { + if (claim.is_object()) { + const std::string relay_id = claim.value("relay_id", ""); + if (!relay_id.empty()) { + active_relays.push_back(relay_id); + } + } + } + if (active_relays.empty()) { + error = "circle outbox active relays are unavailable"; + return false; + } + return true; +} + +static bool circle_hfhe_authorize(const std::string& circle_id, + const std::string& mode_key, + const std::string& requested_addr, + const std::string& key_id, + const std::string& intent_id, + std::string& error) { + auto info_r = circle_info_auth_rpc(circle_id); + if (!info_r.ok) { + error = info_r.error.empty() ? "circle info read failed" : info_r.error; + return false; + } + auto policy_r = circle_hfhe_policy_auth_rpc(circle_id); + if (!policy_r.ok) { + error = policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error; + return false; + } + json policy = policy_r.result.value("policy", json::object()); + if (mode_key == "load_pk_mode" && !circle_hfhe_pk_allowed(policy, requested_addr)) { + error = "requested pubkey address is not allowed by circle hfhe policy"; + return false; + } + const std::string owner = info_r.result.value("owner", ""); + const std::string default_mode = + mode_key == "load_pk_mode" ? "caller_self" : "owner_only"; + const std::string mode = policy.value(mode_key, default_mode); + std::vector active_relays; + if (mode == "active_relay" || mode == "owner_or_active_relay") { + if (intent_id.empty()) { + error = "intent_id required by circle hfhe relay-scoped policy"; + return false; + } + if (!circle_hfhe_active_relays(circle_id, intent_id, active_relays, error)) { + return false; + } + } + const std::string subject = + mode_key == "load_pk_mode" ? requested_addr : g_wallet.addr; + if (!circle_hfhe_mode_allows(mode, owner, g_wallet.addr, subject, active_relays)) { + error = "circle hfhe policy denied this operation"; + return false; + } + const bool require_live_key_policy = policy.value("require_live_key_policy", true); + if (require_live_key_policy) { + if (key_id.empty()) { + error = "key_id required by circle hfhe policy"; + return false; + } + if (!circle_key_policy_live(circle_id, key_id, error)) { + return false; + } + } + return true; +} + +static bool circle_decode_zero_proof(const std::string& encoded, + pvac_zero_proof& proof, + std::string& error) { + proof = nullptr; + if (encoded.rfind(octra::ZKZP_PREFIX, 0) != 0) { + error = "invalid zero proof prefix"; + return false; + } + auto raw = octra::base64_decode(encoded.substr(std::strlen(octra::ZKZP_PREFIX))); + if (raw.empty()) { + error = "invalid zero proof encoding"; + return false; + } + proof = pvac_deserialize_zero_proof(raw.data(), raw.size()); + if (!proof) { + error = "invalid zero proof"; + return false; + } + return true; +} + +static bool circle_verify_zero_with_wallet(const std::string& ciphertext_b64, + const std::string& zero_proof_b64, + std::string& error) { + auto raw = octra::base64_decode(ciphertext_b64); + if (raw.empty()) { + error = "invalid ciphertext"; + return false; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + error = "invalid ciphertext"; + return false; + } + pvac_zero_proof proof = nullptr; + if (!circle_decode_zero_proof(zero_proof_b64, proof, error)) { + g_pvac.free_cipher(ct); + return false; + } + bool ok = pvac_verify_zero(g_pvac.pk(), ct, proof) != 0; + pvac_free_zero_proof(proof); + g_pvac.free_cipher(ct); + if (!ok) error = "zero proof verification failed"; + return ok; +} + +static bool circle_verify_bound_with_wallet(const std::string& ciphertext_b64, + const std::string& zero_proof_b64, + const std::string& amount_commitment_b64, + std::string& error) { + auto raw = octra::base64_decode(ciphertext_b64); + if (raw.empty()) { + error = "invalid ciphertext"; + return false; + } + auto commitment = octra::base64_decode(amount_commitment_b64); + if (commitment.size() != 32) { + error = "invalid amount commitment"; + return false; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + error = "invalid ciphertext"; + return false; + } + pvac_zero_proof proof = nullptr; + if (!circle_decode_zero_proof(zero_proof_b64, proof, error)) { + g_pvac.free_cipher(ct); + return false; + } + bool ok = pvac_verify_zero_bound(g_pvac.pk(), ct, proof, commitment.data()) != 0; + pvac_free_zero_proof(proof); + g_pvac.free_cipher(ct); + if (!ok) error = "bound proof verification failed"; + return ok; +} + +static bool circle_verify_range_with_wallet(const std::string& ciphertext_b64, + const std::string& range_proof_b64, + std::string& error) { + if (ciphertext_b64.rfind(octra::HFHE_PREFIX, 0) != 0) { + error = "invalid ciphertext"; + return false; + } + if (range_proof_b64.rfind(octra::RP_PREFIX, 0) != 0) { + error = "invalid range proof"; + return false; + } + auto raw = octra::base64_decode(ciphertext_b64.substr(strlen(octra::HFHE_PREFIX))); + if (raw.empty()) { + error = "invalid ciphertext"; + return false; + } + auto proof_raw = octra::base64_decode(range_proof_b64.substr(strlen(octra::RP_PREFIX))); + if (proof_raw.empty()) { + error = "invalid range proof"; + return false; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + error = "invalid ciphertext"; + return false; + } + bool ok = pvac_verify_range_any(g_pvac.pk(), ct, proof_raw.data(), proof_raw.size()) != 0; + g_pvac.free_cipher(ct); + if (!ok) error = "range proof verification failed"; + return ok; +} + +static std::string circle_hfhe_policy_hash(const json& policy) { + return octra::sha256_hex(policy.dump()); +} + +static std::string circle_hfhe_receipt_class_value(const json& policy) { + std::string receipt_class = policy.value("proof_receipt_class", ""); + if (!receipt_class.empty()) { + return receipt_class; + } + if (policy.value("require_receipt_transport_binding", false)) { + return "transport_bound"; + } + return "detached"; +} + +static bool circle_hfhe_receipt_required(const std::string& proof_kind) { + return proof_kind == "zero_receipt_v1" || + proof_kind == "range_receipt_v1" || + proof_kind == "bound_zero_receipt_v1"; +} + +static bool circle_hfhe_proof_requires_commitment(const std::string& proof_kind) { + return proof_kind == "bound_zero_v1" || proof_kind == "bound_zero_receipt_v1"; +} + +static bool circle_hfhe_proof_is_range(const std::string& proof_kind) { + return proof_kind == "range_v1" || proof_kind == "range_receipt_v1"; +} + +static bool circle_hfhe_receipt_transport_bound(const json& policy, + const std::string& intent_id, + std::string& error) { + const std::string receipt_class = circle_hfhe_receipt_class_value(policy); + if ((receipt_class == "transport_bound" || receipt_class == "relay_witnessed") && + intent_id.empty()) { + error = "intent_id required by circle hfhe receipt binding policy"; + return false; + } + return true; +} + +static bool circle_hfhe_receipt_signer_allowed(const std::string& circle_id, + const json& policy, + const std::string& caller_addr, + const std::string& signer_addr, + const std::string& intent_id, + std::string& error) { + auto info_r = circle_info_auth_rpc(circle_id); + if (!info_r.ok) { + error = info_r.error.empty() ? "circle info read failed" : info_r.error; + return false; + } + const std::string owner = info_r.result.value("owner", ""); + const std::string mode = policy.value("proof_receipt_signer_mode", "caller_self"); + const std::string receipt_class = circle_hfhe_receipt_class_value(policy); + std::vector active_relays; + if (mode == "active_relay" || mode == "owner_or_active_relay" || + receipt_class == "relay_witnessed") { + if (intent_id.empty()) { + error = "intent_id required by circle hfhe receipt signer policy"; + return false; + } + if (!circle_hfhe_active_relays(circle_id, intent_id, active_relays, error)) { + return false; + } + } + if (receipt_class == "relay_witnessed" && + std::find(active_relays.begin(), active_relays.end(), signer_addr) == active_relays.end()) { + error = "circle hfhe receipt signer must be an active relay"; + return false; + } + if (!circle_hfhe_mode_allows(mode, owner, signer_addr, caller_addr, active_relays)) { + error = "circle hfhe receipt signer is not allowed by policy"; + return false; + } + return true; +} + +static bool circle_hfhe_receipt_context(const std::string& circle_id, + const std::string& verb, + const std::string& caller_addr, + const std::string& key_id, + const std::string& intent_id, + const std::string& proof_kind, + const json& policy, + const std::string& ciphertext_b64, + const std::string& amount_commitment_b64, + octra::CircleHfheReceiptContext& ctx, + std::string& error) { + if (!circle_hfhe_receipt_transport_bound(policy, intent_id, error)) { + return false; + } + std::string ciphertext_hash = octra::circle_hfhe_hash_ciphertext(ciphertext_b64, error); + if (ciphertext_hash.empty()) { + return false; + } + std::string amount_commitment_hash; + if (!amount_commitment_b64.empty()) { + amount_commitment_hash = octra::circle_hfhe_hash_commitment(amount_commitment_b64, error); + if (amount_commitment_hash.empty()) { + return false; + } + } + ctx = { + circle_id, + caller_addr, + key_id, + intent_id, + verb, + proof_kind, + circle_hfhe_policy_hash(policy), + ciphertext_hash, + amount_commitment_hash + }; + return true; +} + +static bool circle_verify_proof_receipt(const std::string& circle_id, + const std::string& verb, + const std::string& caller_addr, + const std::string& key_id, + const std::string& intent_id, + const std::string& proof_kind, + const json& policy, + const std::string& ciphertext_b64, + const std::string& amount_commitment_b64, + const json& receipt, + std::string& error) { + octra::CircleHfheReceiptContext ctx; + if (!circle_hfhe_receipt_context( + circle_id, + verb, + caller_addr, + key_id, + intent_id, + proof_kind, + policy, + ciphertext_b64, + amount_commitment_b64, + ctx, + error)) { + return false; + } + if (!octra::verify_circle_hfhe_receipt_json(receipt, ctx, error)) { + return false; + } + return circle_hfhe_receipt_signer_allowed( + circle_id, + policy, + ctx.caller_addr, + receipt.value("signer_addr", ""), + ctx.intent_id, + error); +} + static json submit_tx(const octra::Transaction& tx) { json j; j["from"] = tx.from; @@ -389,6 +906,27 @@ static json submit_tx(const octra::Transaction& tx) { return res; } +static json submit_program_call_tx(const std::string& target, + const std::string& op_type, + const std::string& method, + const json& params, + const json& body, + const std::string& default_ou) { + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = target; + tx.amount = body.value("amount", "0"); + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = op_type; + tx.encrypted_data = method; + tx.message = params.dump(); + sign_tx_fields(tx); + return submit_tx(tx); +} + static void ensure_pubkey_registered(const std::string& addr, const uint8_t sk[64], const std::string& pub_b64) { auto vr = g_rpc.get_view_pubkey(addr); if (vr.ok && vr.result.is_object() && vr.result.contains("view_pubkey") @@ -2083,11 +2621,6 @@ int main(int argc, char** argv) { svr.Post("/api/keys/private", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD -#ifndef OCTRA_WEBCLI_ENABLE_KEY_EXPORT - res.status = 403; - res.set_content(err_json("key export is disabled in this build; rebuild with -DOCTRA_WEBCLI_ENABLE_KEY_EXPORT to enable").dump(), "application/json"); - return; -#else json body; try { body = json::parse(req.body); } catch (...) { res.status = 400; @@ -2111,7 +2644,6 @@ int main(int argc, char** argv) { j["mnemonic"] = g_wallet.mnemonic; j["warning"] = "treat these values as plaintext secret; never paste into shared transcripts, screen-shares, or untrusted machines"; res.set_content(j.dump(), "application/json"); -#endif }); svr.Post("/api/contract/compile", [](const httplib::Request& req, httplib::Response& res) { @@ -2374,6 +2906,166 @@ int main(int argc, char** argv) { res.set_content(result.dump(), "application/json"); }); + svr.Get("/api/program/info", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string addr = req.get_param_value("address"); + if (circle_id.empty() && addr.empty()) { + res.status = 400; + res.set_content(err_json("address or circle_id required").dump(), "application/json"); + return; + } + auto r = circle_id.empty() + ? g_rpc.vm_contract(addr) + : g_rpc.circle_program_info_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_program_info", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/program/view", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string addr = body.value("address", ""); + std::string method = body.value("method", ""); + if ((circle_id.empty() && addr.empty()) || method.empty()) { + res.status = 400; + res.set_content(err_json("method and address or circle_id required").dump(), "application/json"); + return; + } + json params = json::array(); + if (body.contains("params")) params = body["params"]; + auto r = circle_id.empty() + ? g_rpc.contract_call_view(addr, method, params, g_wallet.addr) + : g_rpc.circle_view_auth( + circle_id, + method, + params, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_view_request(circle_id, method, params, false), + false); + if (!r.ok) { + res.status = 400; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/program/call", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string addr = body.value("address", ""); + std::string method = body.value("method", ""); + if ((circle_id.empty() && addr.empty()) || method.empty()) { + res.status = 400; + res.set_content(err_json("method and address or circle_id required").dump(), "application/json"); + return; + } + std::string params_str = "[]"; + if (body.contains("params")) params_str = body["params"].dump(); + std::string amount_str = body.value("amount", "0"); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id.empty() ? addr : circle_id; + tx.amount = amount_str; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = circle_id.empty() ? "call" : "circle_call"; + tx.encrypted_data = method; + tx.message = params_str; + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Get("/api/program/storage", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string addr = req.get_param_value("address"); + std::string key = req.get_param_value("key"); + bool dump = req.has_param("dump") && req.get_param_value("dump") == "1"; + if (circle_id.empty() && (addr.empty() || key.empty())) { + res.status = 400; + res.set_content(err_json("address and key or circle_id required").dump(), "application/json"); + return; + } + if (!circle_id.empty() && key.empty() && dump) { + auto r = g_rpc.circle_storage_dump_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_storage_dump", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + return; + } + if (!circle_id.empty() && key.empty()) { + res.status = 400; + res.set_content(err_json("circle storage key required unless dump=1").dump(), "application/json"); + return; + } + auto r = circle_id.empty() + ? g_rpc.contract_storage(addr, key) + : g_rpc.circle_storage_auth( + circle_id, + key, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_storage", circle_id, key)); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/program/abi", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string addr = req.get_param_value("address"); + if (addr.empty()) { + res.status = 400; + res.set_content(err_json("address required").dump(), "application/json"); + return; + } + auto r = g_rpc.contract_abi(addr); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + svr.Post("/api/bridge/signer", [](const httplib::Request& req, httplib::Response& res) { std::string signer_url; { @@ -2414,44 +3106,131 @@ int main(int argc, char** argv) { } }); - svr.Get("/api/contract/view", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - std::string addr = req.get_param_value("address"); - std::string method = req.get_param_value("method"); - if (addr.empty() || method.empty()) { - res.status = 400; - res.set_content(err_json("address and method required").dump(), "application/json"); + svr.Get("/api/relay/health", [](const httplib::Request&, httplib::Response& res) { + auto relay = relay_http_get("/health"); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); return; } - std::string params_str = req.get_param_value("params"); - json params = json::array(); - if (!params_str.empty()) { - try { params = json::parse(params_str); } catch (...) {} - } - auto r = g_rpc.contract_call_view(addr, method, params, g_wallet.addr); - if (!r.ok) { - res.status = 400; - res.set_content(err_json(r.error).dump(), "application/json"); + res.status = relay.status; + res.set_content(relay.body, "application/json"); + }); + + svr.Get("/api/relay/status", [](const httplib::Request& req, httplib::Response& res) { + std::string request_id = req.get_param_value("request_id"); + std::string path = "/status"; + if (!request_id.empty()) path += "?request_id=" + request_id; + auto relay = relay_http_get(path); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); return; } - res.set_content(r.result.dump(), "application/json"); + res.status = relay.status; + res.set_content(relay.body, "application/json"); }); - svr.Post("/api/fhe/encrypt", [](const httplib::Request& req, httplib::Response& res) { - WALLET_GUARD - res.set_header("Access-Control-Allow-Origin", "*"); - if (!g_pvac_ok) { - res.status = 500; - res.set_content(err_json("pvac not available").dump(), "application/json"); + svr.Post("/api/relay/request", [](const httplib::Request& req, httplib::Response& res) { + auto relay = relay_http_post("/request", req.body); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); return; } - auto body = json::parse(req.body, nullptr, false); - if (body.is_discarded() || !body.contains("value")) { + res.status = relay.status; + res.set_content(relay.body, "application/json"); + }); + + svr.Get("/api/relay/response", [](const httplib::Request& req, httplib::Response& res) { + std::string request_id = req.get_param_value("request_id"); + if (request_id.empty()) { res.status = 400; - res.set_content(err_json("missing value").dump(), "application/json"); + res.set_content(err_json("request_id required").dump(), "application/json"); return; } - int64_t value = body["value"].get(); + auto relay = relay_http_get("/response/" + request_id); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); + return; + } + res.status = relay.status; + res.set_content(relay.body, "application/json"); + }); + + svr.Get("/api/relay/receipt", [](const httplib::Request& req, httplib::Response& res) { + std::string request_id = req.get_param_value("request_id"); + if (request_id.empty()) { + res.status = 400; + res.set_content(err_json("request_id required").dump(), "application/json"); + return; + } + auto relay = relay_http_get("/receipt/" + request_id); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); + return; + } + res.status = relay.status; + res.set_content(relay.body, "application/json"); + }); + + svr.Get("/api/relay/ingress", [](const httplib::Request& req, httplib::Response& res) { + std::string request_id = req.get_param_value("request_id"); + if (request_id.empty()) { + res.status = 400; + res.set_content(err_json("request_id required").dump(), "application/json"); + return; + } + auto relay = relay_http_get("/ingress/" + request_id); + if (!relay.ok) { + res.status = relay.status ? relay.status : 502; + res.set_content(err_json(relay.error).dump(), "application/json"); + return; + } + res.status = relay.status; + res.set_content(relay.body, "application/json"); + }); + + svr.Get("/api/contract/view", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string addr = req.get_param_value("address"); + std::string method = req.get_param_value("method"); + if (addr.empty() || method.empty()) { + res.status = 400; + res.set_content(err_json("address and method required").dump(), "application/json"); + return; + } + std::string params_str = req.get_param_value("params"); + json params = json::array(); + if (!params_str.empty()) { + try { params = json::parse(params_str); } catch (...) {} + } + auto r = g_rpc.contract_call_view(addr, method, params, g_wallet.addr); + if (!r.ok) { + res.status = 400; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/fhe/encrypt", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("value")) { + res.status = 400; + res.set_content(err_json("missing value").dump(), "application/json"); + return; + } + int64_t value = body["value"].get(); uint8_t seed[32]; octra::random_bytes(seed, 32); pvac_cipher ct = g_pvac.encrypt(static_cast(value), seed); @@ -2491,133 +3270,2698 @@ int main(int argc, char** argv) { auto raw = octra::base64_decode(b64); if (raw.empty()) { res.status = 400; - res.set_content(err_json("invalid base64").dump(), "application/json"); + res.set_content(err_json("invalid base64").dump(), "application/json"); + return; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + uint64_t lo = 0, hi = 0; + g_pvac.decrypt_fp(ct, lo, hi); + g_pvac.free_cipher(ct); + int64_t val; + if (hi == 0) { + val = static_cast(lo); + } else { + __uint128_t p = (__uint128_t(1) << 127) - 1; + __uint128_t full = (__uint128_t(hi) << 64) | lo; + if (full > p / 2) val = -static_cast(p - full); + else val = static_cast(lo); + } + json result; + result["value"] = val; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/load_pk", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string requested_addr = body.value("addr", g_wallet.addr); + std::string key_id = body.value("key_id", ""); + std::string intent_id = body.value("intent_id", ""); + if (circle_id.empty() || requested_addr.empty()) { + res.status = 400; + res.set_content(err_json("circle_id and addr required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize(circle_id, "load_pk_mode", requested_addr, key_id, intent_id, error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.get_pvac_pubkey(requested_addr); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/encrypt", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("value")) { + res.status = 400; + res.set_content(err_json("missing value").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + std::string intent_id = body.value("intent_id", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize(circle_id, "encrypt_mode", g_wallet.addr, key_id, intent_id, error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto policy_r = circle_hfhe_policy_auth_rpc(circle_id); + if (!policy_r.ok) { + res.status = 400; + res.set_content(err_json(policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error).dump(), "application/json"); + return; + } + int64_t value = body["value"].get(); + uint8_t seed[32]; + octra::random_bytes(seed, 32); + pvac_cipher ct = g_pvac.encrypt(static_cast(value), seed); + auto data = g_pvac.serialize_cipher(ct); + std::string b64 = octra::base64_encode(data.data(), data.size()); + uint8_t blinding[32]; + octra::random_bytes(blinding, 32); + auto amount_commitment = g_pvac.pedersen_commit(static_cast(value), blinding); + std::string amount_commitment_b64 = octra::base64_encode(amount_commitment.data(), 32); + json result; + result["ciphertext"] = b64; + auto policy = policy_r.result.value("policy", json::object()); + std::string encrypt_proof = policy.value("encrypt_proof", "bound_zero_v1"); + if (encrypt_proof == "bound_zero_v1" || encrypt_proof == "bound_zero_receipt_v1") { + pvac_zero_proof proof = + g_pvac.make_zero_proof_bound(ct, static_cast(value), blinding); + std::string zero_proof = g_pvac.encode_zero_proof(proof); + g_pvac.free_zero_proof(proof); + result["amount_commitment"] = amount_commitment_b64; + result["zero_proof"] = zero_proof; + result["proof_kind"] = encrypt_proof; + } else if (encrypt_proof == "range_v1" || encrypt_proof == "range_receipt_v1") { + pvac_range_proof proof = + g_pvac.make_range_proof(ct, static_cast(value)); + std::string range_proof = g_pvac.encode_range_proof(proof); + g_pvac.free_range_proof(proof); + result["range_proof"] = range_proof; + result["proof_kind"] = encrypt_proof; + } else if (encrypt_proof == "zero_receipt_v1") { + pvac_zero_proof proof = g_pvac.make_zero_proof(ct); + std::string zero_proof = g_pvac.encode_zero_proof(proof); + g_pvac.free_zero_proof(proof); + result["zero_proof"] = zero_proof; + result["proof_kind"] = encrypt_proof; + } else if (encrypt_proof == "none") { + result["proof_kind"] = "none"; + } else { + g_pvac.free_cipher(ct); + res.status = 400; + res.set_content(err_json("unsupported circle hfhe encrypt proof policy").dump(), "application/json"); + return; + } + g_pvac.free_cipher(ct); + if (circle_hfhe_receipt_required(encrypt_proof)) { + std::string receipt_commitment = + circle_hfhe_proof_requires_commitment(encrypt_proof) + ? amount_commitment_b64 + : ""; + octra::CircleHfheReceiptContext receipt_ctx; + if (!circle_hfhe_receipt_signer_allowed( + circle_id, + policy, + g_wallet.addr, + g_wallet.addr, + intent_id, + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + if (!circle_hfhe_receipt_context( + circle_id, + "encrypt", + g_wallet.addr, + key_id, + intent_id, + encrypt_proof, + policy, + b64, + receipt_commitment, + receipt_ctx, + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + result["proof_receipt"] = + octra::make_circle_hfhe_receipt_json( + receipt_ctx, + g_wallet.addr, + g_wallet.pub_b64, + g_wallet.sk); + } + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/decrypt", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("ciphertext")) { + res.status = 400; + res.set_content(err_json("missing ciphertext").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + std::string intent_id = body.value("intent_id", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize(circle_id, "decrypt_mode", g_wallet.addr, key_id, intent_id, error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto policy_r = circle_hfhe_policy_auth_rpc(circle_id); + if (!policy_r.ok) { + res.status = 400; + res.set_content(err_json(policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error).dump(), "application/json"); + return; + } + auto policy = policy_r.result.value("policy", json::object()); + std::string decrypt_proof = policy.value("decrypt_proof", "none"); + if (decrypt_proof == "bound_zero_v1" || decrypt_proof == "bound_zero_receipt_v1") { + std::string zero_proof = body.value("zero_proof", ""); + std::string amount_commitment = body.value("amount_commitment", ""); + if (zero_proof.empty() || amount_commitment.empty()) { + res.status = 400; + res.set_content(err_json("zero_proof and amount_commitment required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_bound_with_wallet(body["ciphertext"].get(), zero_proof, amount_commitment, error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + if (circle_hfhe_receipt_required(decrypt_proof)) { + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + circle_id, + "encrypt", + g_wallet.addr, + key_id, + intent_id, + decrypt_proof, + policy, + body["ciphertext"].get(), + amount_commitment, + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } + } else if (circle_hfhe_proof_is_range(decrypt_proof)) { + std::string range_proof = body.value("range_proof", ""); + if (range_proof.empty()) { + res.status = 400; + res.set_content(err_json("range_proof required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_range_with_wallet( + body["ciphertext"].get(), + range_proof, + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + if (circle_hfhe_receipt_required(decrypt_proof)) { + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + circle_id, + "encrypt", + g_wallet.addr, + key_id, + intent_id, + decrypt_proof, + policy, + body["ciphertext"].get(), + "", + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } + } else if (decrypt_proof == "zero_receipt_v1") { + std::string zero_proof = body.value("zero_proof", ""); + if (zero_proof.empty()) { + res.status = 400; + res.set_content(err_json("zero_proof required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_zero_with_wallet(body["ciphertext"].get(), zero_proof, error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + circle_id, + "encrypt", + g_wallet.addr, + key_id, + intent_id, + decrypt_proof, + policy, + body["ciphertext"].get(), + "", + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } else if (decrypt_proof != "none") { + res.status = 400; + res.set_content(err_json("unsupported circle hfhe decrypt proof policy").dump(), "application/json"); + return; + } + std::string b64 = body["ciphertext"].get(); + auto raw = octra::base64_decode(b64); + if (raw.empty()) { + res.status = 400; + res.set_content(err_json("invalid base64").dump(), "application/json"); + return; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + uint64_t lo = 0, hi = 0; + g_pvac.decrypt_fp(ct, lo, hi); + g_pvac.free_cipher(ct); + int64_t val; + if (hi == 0) { + val = static_cast(lo); + } else { + __uint128_t p = (__uint128_t(1) << 127) - 1; + __uint128_t full = (__uint128_t(hi) << 64) | lo; + if (full > p / 2) val = -static_cast(p - full); + else val = static_cast(lo); + } + json result; + result["value"] = val; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/commit", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("ciphertext")) { + res.status = 400; + res.set_content(err_json("circle_id and ciphertext required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "commit_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto raw = octra::base64_decode(body["ciphertext"].get()); + if (raw.empty()) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + auto commitment = g_pvac.commit_ct(ct); + g_pvac.free_cipher(ct); + json result; + result["ciphertext_commitment"] = octra::base64_encode(commitment.data(), commitment.size()); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/pedersen", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("value")) { + res.status = 400; + res.set_content(err_json("circle_id and value required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "pedersen_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + int64_t value = body["value"].get(); + std::array blinding = {}; + bool provided_blinding = false; + if (body.contains("blinding")) { + std::string blinding_b64 = body["blinding"].get(); + auto raw = octra::base64_decode(blinding_b64); + if (raw.size() != blinding.size()) { + res.status = 400; + res.set_content(err_json("blinding must decode to 32 bytes").dump(), "application/json"); + return; + } + std::copy(raw.begin(), raw.end(), blinding.begin()); + provided_blinding = true; + } else { + octra::random_bytes(blinding.data(), blinding.size()); + } + auto amount_commitment = + g_pvac.pedersen_commit(static_cast(value), blinding.data()); + json result; + result["amount_commitment"] = octra::base64_encode(amount_commitment.data(), amount_commitment.size()); + result["blinding"] = octra::base64_encode(blinding.data(), blinding.size()); + result["generated_blinding"] = !provided_blinding; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/serialize_cipher", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("ciphertext")) { + res.status = 400; + res.set_content(err_json("circle_id and ciphertext required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "cipher_serde_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto raw = octra::base64_decode(body["ciphertext"].get()); + if (raw.empty()) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + res.status = 400; + res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + return; + } + auto serialized = g_pvac.serialize_cipher(ct); + g_pvac.free_cipher(ct); + json result; + result["serialized_cipher"] = octra::base64_encode(serialized.data(), serialized.size()); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/deserialize_cipher", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("serialized_cipher")) { + res.status = 400; + res.set_content(err_json("circle_id and serialized_cipher required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "cipher_serde_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto raw = octra::base64_decode(body["serialized_cipher"].get()); + if (raw.empty()) { + res.status = 400; + res.set_content(err_json("invalid serialized cipher").dump(), "application/json"); + return; + } + pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); + if (!ct) { + res.status = 400; + res.set_content(err_json("invalid serialized cipher").dump(), "application/json"); + return; + } + auto normalized = g_pvac.serialize_cipher(ct); + g_pvac.free_cipher(ct); + json result; + result["ciphertext"] = octra::base64_encode(normalized.data(), normalized.size()); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/verify_zero", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("ciphertext") || !body.contains("zero_proof")) { + res.status = 400; + res.set_content(err_json("circle_id, ciphertext and zero_proof required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "verify_zero_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto policy_r = circle_hfhe_policy_auth_rpc(body["circle_id"].get()); + if (!policy_r.ok) { + res.status = 400; + res.set_content(err_json(policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error).dump(), "application/json"); + return; + } + auto policy = policy_r.result.value("policy", json::object()); + std::string encrypt_proof = policy.value("encrypt_proof", "bound_zero_v1"); + if (encrypt_proof == "zero_receipt_v1") { + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + body["circle_id"].get(), + "encrypt", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + encrypt_proof, + policy, + body["ciphertext"].get(), + "", + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } + bool ok = circle_verify_zero_with_wallet( + body["ciphertext"].get(), + body["zero_proof"].get(), + error); + if (!ok && !error.empty()) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + res.set_content(json({{"ok", ok}}).dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/verify_range", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("ciphertext") || !body.contains("range_proof")) { + res.status = 400; + res.set_content(err_json("circle_id, ciphertext and range_proof required").dump(), "application/json"); + return; + } + std::string circle_id = body["circle_id"].get(); + std::string key_id = body.value("key_id", ""); + std::string intent_id = body.value("intent_id", ""); + std::string error; + if (!circle_hfhe_authorize( + circle_id, + "verify_range_mode", + g_wallet.addr, + key_id, + intent_id, + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto policy_r = circle_hfhe_policy_auth_rpc(circle_id); + if (!policy_r.ok) { + res.status = 400; + res.set_content(err_json(policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error).dump(), "application/json"); + return; + } + auto policy = policy_r.result.value("policy", json::object()); + std::string encrypt_proof = policy.value("encrypt_proof", "bound_zero_v1"); + if (encrypt_proof == "range_receipt_v1") { + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + circle_id, + "encrypt", + g_wallet.addr, + key_id, + intent_id, + encrypt_proof, + policy, + body["ciphertext"].get(), + "", + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } + bool ok = circle_verify_range_with_wallet( + body["ciphertext"].get(), + body["range_proof"].get(), + error); + if (!ok && !error.empty()) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + res.set_content(json({{"ok", ok}}).dump(), "application/json"); + }); + + svr.Post("/api/circle/fhe/verify_bound", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + res.set_header("Access-Control-Allow-Origin", "*"); + if (!g_pvac_ok) { + res.status = 500; + res.set_content(err_json("pvac not available").dump(), "application/json"); + return; + } + auto body = json::parse(req.body, nullptr, false); + if (body.is_discarded() || !body.contains("circle_id") || !body.contains("ciphertext") || !body.contains("zero_proof") || !body.contains("amount_commitment")) { + res.status = 400; + res.set_content(err_json("circle_id, ciphertext, zero_proof and amount_commitment required").dump(), "application/json"); + return; + } + std::string error; + if (!circle_hfhe_authorize( + body["circle_id"].get(), + "verify_bound_mode", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + error)) { + res.status = 403; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + auto policy_r = circle_hfhe_policy_auth_rpc(body["circle_id"].get()); + if (!policy_r.ok) { + res.status = 400; + res.set_content(err_json(policy_r.error.empty() ? "circle hfhe policy read failed" : policy_r.error).dump(), "application/json"); + return; + } + auto policy = policy_r.result.value("policy", json::object()); + std::string encrypt_proof = policy.value("encrypt_proof", "bound_zero_v1"); + if (encrypt_proof == "bound_zero_receipt_v1") { + if (!body.contains("proof_receipt")) { + res.status = 400; + res.set_content(err_json("proof_receipt required by circle hfhe policy").dump(), "application/json"); + return; + } + if (!circle_verify_proof_receipt( + body["circle_id"].get(), + "encrypt", + g_wallet.addr, + body.value("key_id", ""), + body.value("intent_id", ""), + encrypt_proof, + policy, + body["ciphertext"].get(), + body["amount_commitment"].get(), + body["proof_receipt"], + error)) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + } + bool ok = circle_verify_bound_with_wallet( + body["ciphertext"].get(), + body["zero_proof"].get(), + body["amount_commitment"].get(), + error); + if (!ok && !error.empty()) { + res.status = 400; + res.set_content(err_json(error).dump(), "application/json"); + return; + } + res.set_content(json({{"ok", ok}}).dump(), "application/json"); + }); + + svr.Get("/api/contract/info", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string addr = req.get_param_value("address"); + if (addr.empty()) { + res.status = 400; + res.set_content(err_json("address required").dump(), "application/json"); + return; + } + auto r = g_rpc.vm_contract(addr); + if (!r.ok) { + res.status = 404; + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/info", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_info(circle_id); + if (!r.ok) { + r = rpc.circle_info_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_info", circle_id)); + } + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/slot_policy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string slot_ref = req.get_param_value("slot_ref"); + if (circle_id.empty() || slot_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and slot_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_slot_policy_auth( + circle_id, + slot_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_slot_policy", circle_id, slot_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/state_policy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string state_ref = req.get_param_value("state_ref"); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_state_policy_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_state_policy", circle_id, state_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/state_descriptor", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string state_ref = req.get_param_value("state_ref"); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_state_descriptor_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_state_descriptor", circle_id, state_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/balance_cell", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string state_ref = req.get_param_value("state_ref"); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_balance_cell_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_balance_cell", circle_id, state_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/register_cell", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string state_ref = req.get_param_value("state_ref"); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_register_cell_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_register_cell", circle_id, state_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/balance_binding", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string subject_addr = req.get_param_value("subject_addr"); + if (circle_id.empty() || subject_addr.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and subject_addr required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_balance_binding_auth( + circle_id, + subject_addr, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_balance_binding", circle_id, subject_addr)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/register_binding", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string register_ref = req.get_param_value("register_ref"); + if (circle_id.empty() || register_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and register_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_register_binding_auth( + circle_id, + register_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_register_binding", circle_id, register_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/balance_workflow", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string workflow_ref = req.get_param_value("workflow_ref"); + if (circle_id.empty() || workflow_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and workflow_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_balance_workflow_auth( + circle_id, + workflow_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_balance_workflow", circle_id, workflow_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/register_workflow", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string workflow_ref = req.get_param_value("workflow_ref"); + if (circle_id.empty() || workflow_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and workflow_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_register_workflow_auth( + circle_id, + workflow_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_register_workflow", circle_id, workflow_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_summary", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string object_ref = req.get_param_value("object_ref"); + if (circle_id.empty() || object_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and object_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_summary_auth( + circle_id, + object_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_summary", circle_id, object_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_members", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string object_ref = req.get_param_value("object_ref"); + if (circle_id.empty() || object_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and object_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_members_auth( + circle_id, + object_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_members", circle_id, object_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_detail", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string object_ref = req.get_param_value("object_ref"); + if (circle_id.empty() || object_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and object_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_detail_auth( + circle_id, + object_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_detail", circle_id, object_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_member", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string object_ref = req.get_param_value("object_ref"); + std::string member_ref = req.get_param_value("member_ref"); + if (circle_id.empty() || object_ref.empty() || member_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, object_ref and member_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_member_auth( + circle_id, + object_ref, + member_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_member", circle_id, object_ref + "|" + member_ref)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_refs", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_refs_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_refs", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/object_list", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_object_list_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_object_list", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/circle/object_policy_define", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string object_ref = body.value("object_ref", ""); + std::string transition_mode = body.value("transition_mode", ""); + std::string required_proof_kind = body.value("required_proof_kind", ""); + if (circle_id.empty() || object_ref.empty() || transition_mode.empty() || required_proof_kind.empty() + || !body.contains("member_quorum") || !body["member_quorum"].is_number_integer() + || !body.contains("allow_detach") || !body["allow_detach"].is_boolean() + || !body.contains("allow_root_state_rotation") || !body["allow_root_state_rotation"].is_boolean()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, object_ref, transition_mode, required_proof_kind, member_quorum, allow_detach, and allow_root_state_rotation required").dump(), "application/json"); + return; + } + json params = json::array({ + object_ref, + body.value("delivery_key_id", ""), + body.value("activate_after_epoch", 0), + body.value("expire_after_epoch", 0), + transition_mode, + required_proof_kind, + body["member_quorum"].get(), + body["allow_detach"].get(), + body["allow_root_state_rotation"].get() + }); + auto result = submit_program_call_tx( + circle_id, + "circle_call", + body.value("method", "define_object_policy_native"), + params, + body, + "1000"); + if (result.contains("error")) res.status = 500; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/object_bind", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string object_ref = body.value("object_ref", ""); + std::string state_ref = body.value("state_ref", ""); + std::string transition_ref = body.value("transition_ref", ""); + std::string status = body.value("status", ""); + if (circle_id.empty() || object_ref.empty() || state_ref.empty() || transition_ref.empty() || status.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, object_ref, state_ref, transition_ref, and status required").dump(), "application/json"); + return; + } + json params = json::array({object_ref, state_ref, transition_ref, status}); + auto result = submit_program_call_tx( + circle_id, + "circle_call", + body.value("method", "bind_object_native"), + params, + body, + "1000"); + if (result.contains("error")) res.status = 500; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/object_member_attach", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string object_ref = body.value("object_ref", ""); + std::string member_ref = body.value("member_ref", ""); + std::string state_ref = body.value("state_ref", ""); + std::string member_kind = body.value("member_kind", ""); + std::string state_class = body.value("state_class", ""); + std::string codec = body.value("codec", ""); + std::string status = body.value("status", ""); + if (circle_id.empty() || object_ref.empty() || member_ref.empty() || state_ref.empty() + || member_kind.empty() || state_class.empty() || codec.empty() || status.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, object_ref, member_ref, state_ref, member_kind, state_class, codec, and status required").dump(), "application/json"); + return; + } + json params = json::array({object_ref, member_ref, state_ref, member_kind, state_class, codec, status}); + auto result = submit_program_call_tx( + circle_id, + "circle_call", + body.value("method", "attach_object_member_native"), + params, + body, + "1000"); + if (result.contains("error")) res.status = 500; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/object_member_detach", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string object_ref = body.value("object_ref", ""); + std::string member_ref = body.value("member_ref", ""); + if (circle_id.empty() || object_ref.empty() || member_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, object_ref, and member_ref required").dump(), "application/json"); + return; + } + json params = json::array({object_ref, member_ref}); + auto result = submit_program_call_tx( + circle_id, + "circle_call", + body.value("method", "detach_object_member_native"), + params, + body, + "1000"); + if (result.contains("error")) res.status = 500; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/object_transition_apply", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body = json::parse(req.body, nullptr, false); + if (body.is_discarded()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string transition_ref = body.value("transition_ref", ""); + std::string object_ref = body.value("object_ref", ""); + std::string next_state_ref = body.value("next_state_ref", ""); + std::string status = body.value("status", ""); + std::string intent_id = body.value("intent_id", ""); + if (circle_id.empty() || transition_ref.empty() || object_ref.empty() || next_state_ref.empty() || status.empty() || intent_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id, transition_ref, object_ref, next_state_ref, status, and intent_id required").dump(), "application/json"); + return; + } + json params = json::array({ + transition_ref, + object_ref, + body.value("previous_state_ref", ""), + next_state_ref, + body.value("member_bundle", ""), + body.value("touched_members_hash", ""), + body.value("proof_kind", ""), + body.value("proof_receipt_hash", ""), + status, + intent_id + }); + auto result = submit_program_call_tx( + circle_id, + "circle_call", + body.value("method", "apply_object_transition_native"), + params, + body, + "1000"); + if (result.contains("error")) res.status = 500; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(result.dump(), "application/json"); + }); + + svr.Get("/api/circle/transport_policy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_transport_policy_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_transport_policy", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/hfhe_policy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + if (circle_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_hfhe_policy_auth( + circle_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_hfhe_policy", circle_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/key_policy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string key_id = req.get_param_value("key_id"); + if (circle_id.empty() || key_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and key_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_key_policy_auth( + circle_id, + key_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_key_policy", circle_id, key_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/outbox_intent", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string intent_id = req.get_param_value("intent_id"); + if (circle_id.empty() || intent_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and intent_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_outbox_intent_auth( + circle_id, + intent_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_outbox_intent", circle_id, intent_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/outbox_claim", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string intent_id = req.get_param_value("intent_id"); + if (circle_id.empty() || intent_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and intent_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_outbox_claim_auth( + circle_id, + intent_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_outbox_claim", circle_id, intent_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/outbox_status", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string intent_id = req.get_param_value("intent_id"); + if (circle_id.empty() || intent_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and intent_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_outbox_status_auth( + circle_id, + intent_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_outbox_status", circle_id, intent_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/ingress_packet", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string intent_id = req.get_param_value("intent_id"); + if (circle_id.empty() || intent_id.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and intent_id required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_ingress_packet_auth( + circle_id, + intent_id, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_ingress_packet", circle_id, intent_id)); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset", [](const httplib::Request& req, httplib::Response& res) { + std::string circle_id = req.get_param_value("circle_id"); + std::string path = req.get_param_value("path"); + if (circle_id.empty() || path.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and path required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset(circle_id, path); + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string path = req.get_param_value("path"); + if (circle_id.empty() || path.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and path required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext(circle_id, path); + if (!r.ok) { + r = rpc.circle_asset_ciphertext_auth( + circle_id, + path, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext", circle_id, "path|" + path)); + } + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext_by_key", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string resource_key = req.get_param_value("resource_key"); + if (circle_id.empty() || resource_key.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and resource_key required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext_by_resource_key(circle_id, resource_key); + if (!r.ok) { + r = rpc.circle_asset_ciphertext_by_resource_key_auth( + circle_id, + resource_key, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_resource_key", circle_id, "resource_key|" + resource_key)); + } + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext_by_slot", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string slot_ref = req.get_param_value("slot_ref"); + if (circle_id.empty() || slot_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and slot_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext_by_slot_ref(circle_id, slot_ref); + if (!r.ok) { + r = rpc.circle_asset_ciphertext_by_slot_ref_auth( + circle_id, + slot_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_slot_ref", circle_id, "slot_ref|" + slot_ref)); + } + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Get("/api/circle/asset_ciphertext_by_state", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::string circle_id = req.get_param_value("circle_id"); + std::string state_ref = req.get_param_value("state_ref"); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + octra::RpcClient rpc(current_public_rpc_url()); + auto r = rpc.circle_asset_ciphertext_by_state_ref(circle_id, state_ref); + if (!r.ok) { + r = rpc.circle_asset_ciphertext_by_state_ref_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_state_ref", circle_id, "state_ref|" + state_ref)); + } + if (!r.ok) { + res.status = 404; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(err_json(r.error).dump(), "application/json"); + return; + } + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_content(r.result.dump(), "application/json"); + }); + + svr.Post("/api/circle/deploy", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + auto read_string_or = [&](const char* key, const char* fallback) -> std::string { + if (!body.contains(key) || body[key].is_null()) { + return fallback; + } + if (body[key].is_string()) { + return body[key].get(); + } + return fallback; + }; + auto read_optional_string = [&](const char* key) -> std::string { + return read_string_or(key, ""); + }; + std::string circle_id = read_string_or("circle_id", ""); + std::string runtime = read_string_or("runtime", "octb"); + std::string privacy_class = read_string_or("privacy_class", "sealed"); + std::string browser_mode = read_string_or("browser_mode", "native_sealed"); + std::string resource_mode = read_string_or("resource_mode", "sealed_read"); + std::string code_b64 = read_optional_string("code_b64"); + std::string policy_hash = read_optional_string("policy_hash"); + std::string members_root = read_optional_string("members_root"); + std::string export_policy = read_optional_string("export_policy"); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + auto read_limit = [&](const char* key, const char* fallback) -> std::string { + if (!body.contains("limits") || !body["limits"].is_object()) { + return fallback; + } + auto limits = body["limits"]; + if (!limits.contains(key)) { + return fallback; + } + if (limits[key].is_string()) { + return limits[key].get(); + } + if (limits[key].is_number_integer()) { + return std::to_string(limits[key].get()); + } + return fallback; + }; + json payload; + payload["runtime"] = runtime; + payload["privacy_class"] = privacy_class; + payload["browser_mode"] = browser_mode; + payload["resource_mode"] = resource_mode; + payload["limits"] = { + {"max_stable_bytes", read_limit("max_stable_bytes", "33554432")}, + {"max_assets_bytes", read_limit("max_assets_bytes", "33554432")}, + {"max_inline_value", read_limit("max_inline_value", "65536")}, + {"max_wasm_bytes", read_limit("max_wasm_bytes", "33554432")} + }; + if (!code_b64.empty()) payload["code_b64"] = code_b64; + if (!policy_hash.empty()) payload["policy_hash"] = policy_hash; + if (!members_root.empty()) payload["members_root"] = members_root; + if (!export_policy.empty()) payload["export_policy"] = export_policy; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "200000"); + tx.timestamp = now_ts(); + tx.op_type = "deploy_circle"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + else result["circle_id"] = circle_id; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/asset_encrypted", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string path = body.value("path", ""); + std::string slot_ref = body.value("slot_ref", ""); + std::string state_ref = body.value("state_ref", ""); + std::string content_type = body.value("content_type", ""); + std::string ciphertext_b64 = body.value("ciphertext_b64", ""); + std::string key_id = body.value("key_id", ""); + std::string plaintext_hash = body.value("plaintext_hash", ""); + std::string encoding = body.value("encoding", ""); + std::string padding_class = body.value("padding_class", ""); + auto read_optional_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string activate_after_epoch = read_optional_scalar("activate_after_epoch"); + std::string expire_after_epoch = read_optional_scalar("expire_after_epoch"); + std::string metadata_mode = body.value("metadata_mode", ""); + if (circle_id.empty() || content_type.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, content_type, ciphertext_b64, key_id, and plaintext_hash required").dump(), "application/json"); + return; + } + int locator_count = 0; + if (!path.empty()) locator_count += 1; + if (!slot_ref.empty()) locator_count += 1; + if (!state_ref.empty()) locator_count += 1; + if (locator_count != 1) { + res.status = 400; + res.set_content(err_json("provide exactly one of path, slot_ref, or state_ref").dump(), "application/json"); + return; + } + if (ciphertext_b64.size() > CIRCLE_ASSET_MAX_B64_BYTES) { + res.status = 400; + res.set_content(err_json("circle asset body exceeds max encoded size").dump(), "application/json"); + return; + } + const std::string default_ou = std::to_string(circle_asset_ou_from_b64_len(ciphertext_b64.size())); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = "circle_asset_put_encrypted"; + tx.encrypted_data = ciphertext_b64; + json payload; + if (!path.empty()) payload["path"] = path; + if (!slot_ref.empty()) payload["slot_ref"] = slot_ref; + if (!state_ref.empty()) payload["state_ref"] = state_ref; + payload["content_type"] = content_type; + payload["key_id"] = key_id; + payload["plaintext_hash"] = plaintext_hash; + if (!encoding.empty()) payload["encoding"] = encoding; + if (!padding_class.empty()) payload["padding_class"] = padding_class; + if (!activate_after_epoch.empty()) payload["activate_after_epoch"] = activate_after_epoch; + if (!expire_after_epoch.empty()) payload["expire_after_epoch"] = expire_after_epoch; + if (!metadata_mode.empty()) payload["metadata_mode"] = metadata_mode; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/asset_plain", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string path = body.value("path", ""); + std::string content_type = body.value("content_type", ""); + std::string body_b64 = body.value("body_b64", ""); + std::string encoding = body.value("encoding", ""); + if (circle_id.empty() || path.empty() || content_type.empty() || body_b64.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, path, content_type, and body_b64 required").dump(), "application/json"); + return; + } + if (body_b64.size() > CIRCLE_ASSET_MAX_B64_BYTES) { + res.status = 400; + res.set_content(err_json("circle asset body exceeds max encoded size").dump(), "application/json"); + return; + } + const std::string default_ou = std::to_string(circle_asset_ou_from_b64_len(body_b64.size())); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = "circle_asset_put"; + tx.encrypted_data = body_b64; + json payload; + payload["path"] = path; + payload["content_type"] = content_type; + if (!encoding.empty()) payload["encoding"] = encoding; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/sealed_slot_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string slot_ref = body.value("slot_ref", ""); + std::string state_ref = body.value("state_ref", ""); + std::string content_type = body.value("content_type", ""); + std::string ciphertext_b64 = body.value("ciphertext_b64", ""); + std::string key_id = body.value("key_id", ""); + std::string plaintext_hash = body.value("plaintext_hash", ""); + std::string encoding = body.value("encoding", ""); + std::string padding_class = body.value("padding_class", ""); + auto read_optional_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string activate_after_epoch = read_optional_scalar("activate_after_epoch"); + std::string expire_after_epoch = read_optional_scalar("expire_after_epoch"); + std::string metadata_mode = body.value("metadata_mode", ""); + if (circle_id.empty() || content_type.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, content_type, ciphertext_b64, key_id, and plaintext_hash required").dump(), "application/json"); + return; + } + if ((slot_ref.empty() && state_ref.empty()) || (!slot_ref.empty() && !state_ref.empty())) { + res.status = 400; + res.set_content(err_json("provide exactly one of slot_ref or state_ref").dump(), "application/json"); + return; + } + if (ciphertext_b64.size() > CIRCLE_ASSET_MAX_B64_BYTES) { + res.status = 400; + res.set_content(err_json("circle asset body exceeds max encoded size").dump(), "application/json"); + return; + } + const std::string default_ou = std::to_string(circle_asset_ou_from_b64_len(ciphertext_b64.size())); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = "circle_sealed_slot_put"; + tx.encrypted_data = ciphertext_b64; + json payload; + if (!slot_ref.empty()) payload["slot_ref"] = slot_ref; + if (!state_ref.empty()) payload["state_ref"] = state_ref; + payload["content_type"] = content_type; + payload["key_id"] = key_id; + payload["plaintext_hash"] = plaintext_hash; + if (!encoding.empty()) payload["encoding"] = encoding; + if (!padding_class.empty()) payload["padding_class"] = padding_class; + if (!activate_after_epoch.empty()) payload["activate_after_epoch"] = activate_after_epoch; + if (!expire_after_epoch.empty()) payload["expire_after_epoch"] = expire_after_epoch; + if (!metadata_mode.empty()) payload["metadata_mode"] = metadata_mode; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/slot_policy_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string slot_ref = body.value("slot_ref", ""); + std::string state_ref = body.value("state_ref", ""); + std::string delivery_key_id = body.value("delivery_key_id", ""); + auto read_optional_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string activate_after_epoch = read_optional_scalar("activate_after_epoch"); + std::string expire_after_epoch = read_optional_scalar("expire_after_epoch"); + bool tombstone = body.value("tombstone", false); + bool revoked = body.value("revoked", false); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + if ((slot_ref.empty() && state_ref.empty()) || (!slot_ref.empty() && !state_ref.empty())) { + res.status = 400; + res.set_content(err_json("provide exactly one of slot_ref or state_ref").dump(), "application/json"); + return; + } + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_slot_policy_put"; + json payload; + if (!slot_ref.empty()) payload["slot_ref"] = slot_ref; + if (!state_ref.empty()) payload["state_ref"] = state_ref; + if (!delivery_key_id.empty()) payload["delivery_key_id"] = delivery_key_id; + if (!activate_after_epoch.empty()) payload["activate_after_epoch"] = activate_after_epoch; + if (!expire_after_epoch.empty()) payload["expire_after_epoch"] = expire_after_epoch; + if (tombstone) payload["tombstone"] = tombstone; + if (revoked) payload["revoked"] = revoked; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/state_descriptor_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string state_ref = body.value("state_ref", ""); + if (circle_id.empty() || state_ref.empty()) { + res.status = 400; + res.set_content(err_json("circle_id and state_ref required").dump(), "application/json"); + return; + } + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_state_descriptor_put"; + json payload; + payload["state_ref"] = state_ref; + if (body.contains("state_class")) payload["state_class"] = body["state_class"]; + if (body.contains("codec")) payload["codec"] = body["codec"]; + if (body.contains("schema_hash")) payload["schema_hash"] = body["schema_hash"]; + if (body.contains("subject_addr")) payload["subject_addr"] = body["subject_addr"]; + if (body.contains("hfhe_profile")) payload["hfhe_profile"] = body["hfhe_profile"]; + if (body.contains("mutable_state")) payload["mutable_state"] = body["mutable_state"]; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/balance_cell_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string state_ref = body.value("state_ref", ""); + std::string ciphertext_b64 = body.value("ciphertext_b64", ""); + std::string key_id = body.value("key_id", ""); + std::string plaintext_hash = body.value("plaintext_hash", ""); + std::string ciphertext_commitment = body.value("ciphertext_commitment", ""); + std::string amount_commitment = body.value("amount_commitment", ""); + if (circle_id.empty() || state_ref.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty() || ciphertext_commitment.empty() || amount_commitment.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, state_ref, ciphertext_b64, key_id, plaintext_hash, ciphertext_commitment, and amount_commitment required").dump(), "application/json"); + return; + } + if (ciphertext_b64.size() > CIRCLE_ASSET_MAX_B64_BYTES) { + res.status = 400; + res.set_content(err_json("circle asset body exceeds max encoded size").dump(), "application/json"); + return; + } + auto read_optional_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string activate_after_epoch = read_optional_scalar("activate_after_epoch"); + std::string expire_after_epoch = read_optional_scalar("expire_after_epoch"); + const std::string default_ou = std::to_string(circle_asset_ou_from_b64_len(ciphertext_b64.size())); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = "circle_balance_cell_put"; + tx.encrypted_data = ciphertext_b64; + json payload; + payload["state_ref"] = state_ref; + payload["key_id"] = key_id; + payload["plaintext_hash"] = plaintext_hash; + payload["ciphertext_commitment"] = ciphertext_commitment; + payload["amount_commitment"] = amount_commitment; + if (body.contains("content_type")) payload["content_type"] = body["content_type"]; + if (body.contains("encoding")) payload["encoding"] = body["encoding"]; + if (body.contains("padding_class")) payload["padding_class"] = body["padding_class"]; + if (body.contains("delivery_key_id")) payload["delivery_key_id"] = body["delivery_key_id"]; + if (!activate_after_epoch.empty()) payload["activate_after_epoch"] = activate_after_epoch; + if (!expire_after_epoch.empty()) payload["expire_after_epoch"] = expire_after_epoch; + if (body.contains("metadata_mode")) payload["metadata_mode"] = body["metadata_mode"]; + if (body.contains("codec")) payload["codec"] = body["codec"]; + if (body.contains("schema_hash")) payload["schema_hash"] = body["schema_hash"]; + if (body.contains("subject_addr")) payload["subject_addr"] = body["subject_addr"]; + if (body.contains("mutable_state")) payload["mutable_state"] = body["mutable_state"]; + if (body.contains("hfhe_profile")) payload["hfhe_profile"] = body["hfhe_profile"]; + if (body.contains("proof_kind")) payload["proof_kind"] = body["proof_kind"]; + if (body.contains("proof_receipt_hash")) payload["proof_receipt_hash"] = body["proof_receipt_hash"]; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/register_cell_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string state_ref = body.value("state_ref", ""); + std::string ciphertext_b64 = body.value("ciphertext_b64", ""); + std::string key_id = body.value("key_id", ""); + std::string plaintext_hash = body.value("plaintext_hash", ""); + std::string ciphertext_commitment = body.value("ciphertext_commitment", ""); + if (circle_id.empty() || state_ref.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty() || ciphertext_commitment.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, state_ref, ciphertext_b64, key_id, plaintext_hash, and ciphertext_commitment required").dump(), "application/json"); + return; + } + if (ciphertext_b64.size() > CIRCLE_ASSET_MAX_B64_BYTES) { + res.status = 400; + res.set_content(err_json("circle asset body exceeds max encoded size").dump(), "application/json"); + return; + } + auto read_optional_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string activate_after_epoch = read_optional_scalar("activate_after_epoch"); + std::string expire_after_epoch = read_optional_scalar("expire_after_epoch"); + const std::string default_ou = std::to_string(circle_asset_ou_from_b64_len(ciphertext_b64.size())); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, default_ou); + tx.timestamp = now_ts(); + tx.op_type = "circle_register_cell_put"; + tx.encrypted_data = ciphertext_b64; + json payload; + payload["state_ref"] = state_ref; + payload["key_id"] = key_id; + payload["plaintext_hash"] = plaintext_hash; + payload["ciphertext_commitment"] = ciphertext_commitment; + if (body.contains("content_type")) payload["content_type"] = body["content_type"]; + if (body.contains("encoding")) payload["encoding"] = body["encoding"]; + if (body.contains("padding_class")) payload["padding_class"] = body["padding_class"]; + if (body.contains("delivery_key_id")) payload["delivery_key_id"] = body["delivery_key_id"]; + if (!activate_after_epoch.empty()) payload["activate_after_epoch"] = activate_after_epoch; + if (!expire_after_epoch.empty()) payload["expire_after_epoch"] = expire_after_epoch; + if (body.contains("metadata_mode")) payload["metadata_mode"] = body["metadata_mode"]; + if (body.contains("codec")) payload["codec"] = body["codec"]; + if (body.contains("schema_hash")) payload["schema_hash"] = body["schema_hash"]; + if (body.contains("subject_addr")) payload["subject_addr"] = body["subject_addr"]; + if (body.contains("mutable_state")) payload["mutable_state"] = body["mutable_state"]; + if (body.contains("hfhe_profile")) payload["hfhe_profile"] = body["hfhe_profile"]; + if (body.contains("proof_kind")) payload["proof_kind"] = body["proof_kind"]; + if (body.contains("proof_receipt_hash")) payload["proof_receipt_hash"] = body["proof_receipt_hash"]; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/transport_policy_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + json payload = json::object(); + if (body.contains("relay_mode")) payload["relay_mode"] = body["relay_mode"]; + if (body.contains("lease_class")) payload["lease_class"] = body["lease_class"]; + if (body.contains("claim_strategy")) payload["claim_strategy"] = body["claim_strategy"]; + if (body.contains("claim_topology")) payload["claim_topology"] = body["claim_topology"]; + if (body.contains("quorum_mode")) payload["quorum_mode"] = body["quorum_mode"]; + if (body.contains("ingress_strategy")) payload["ingress_strategy"] = body["ingress_strategy"]; + if (body.contains("quorum_threshold")) payload["quorum_threshold"] = body["quorum_threshold"]; + if (body.contains("quorum_weight_threshold")) payload["quorum_weight_threshold"] = body["quorum_weight_threshold"]; + if (body.contains("max_active_claims")) payload["max_active_claims"] = body["max_active_claims"]; + if (body.contains("relay_allowlist")) payload["relay_allowlist"] = body["relay_allowlist"]; + if (body.contains("relay_weights")) payload["relay_weights"] = body["relay_weights"]; + if (body.contains("max_claim_window_epochs")) payload["max_claim_window_epochs"] = body["max_claim_window_epochs"]; + if (body.contains("max_response_bytes")) payload["max_response_bytes"] = body["max_response_bytes"]; + if (body.contains("require_response_ciphertext")) payload["require_response_ciphertext"] = body["require_response_ciphertext"]; + if (body.contains("require_external_receipt")) payload["require_external_receipt"] = body["require_external_receipt"]; + if (body.contains("accepted_result_codes")) payload["accepted_result_codes"] = body["accepted_result_codes"]; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_transport_policy_put"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/hfhe_policy_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + json payload = json::object(); + if (body.contains("load_pk_mode")) payload["load_pk_mode"] = body["load_pk_mode"]; + if (body.contains("encrypt_mode")) payload["encrypt_mode"] = body["encrypt_mode"]; + if (body.contains("decrypt_mode")) payload["decrypt_mode"] = body["decrypt_mode"]; + if (body.contains("cipher_arithmetic_mode")) payload["cipher_arithmetic_mode"] = body["cipher_arithmetic_mode"]; + if (body.contains("commit_mode")) payload["commit_mode"] = body["commit_mode"]; + if (body.contains("pedersen_mode")) payload["pedersen_mode"] = body["pedersen_mode"]; + if (body.contains("cipher_serde_mode")) payload["cipher_serde_mode"] = body["cipher_serde_mode"]; + if (body.contains("pubkey_serde_mode")) payload["pubkey_serde_mode"] = body["pubkey_serde_mode"]; + if (body.contains("verify_zero_mode")) payload["verify_zero_mode"] = body["verify_zero_mode"]; + if (body.contains("verify_range_mode")) payload["verify_range_mode"] = body["verify_range_mode"]; + if (body.contains("verify_bound_mode")) payload["verify_bound_mode"] = body["verify_bound_mode"]; + if (body.contains("proof_receipt_signer_mode")) payload["proof_receipt_signer_mode"] = body["proof_receipt_signer_mode"]; + if (body.contains("proof_receipt_class")) payload["proof_receipt_class"] = body["proof_receipt_class"]; + if (body.contains("pk_allowlist")) payload["pk_allowlist"] = body["pk_allowlist"]; + if (body.contains("require_live_key_policy")) payload["require_live_key_policy"] = body["require_live_key_policy"]; + if (body.contains("require_receipt_transport_binding")) payload["require_receipt_transport_binding"] = body["require_receipt_transport_binding"]; + if (body.contains("encrypt_proof")) payload["encrypt_proof"] = body["encrypt_proof"]; + if (body.contains("decrypt_proof")) payload["decrypt_proof"] = body["decrypt_proof"]; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_hfhe_policy_put"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/key_grant", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + if (circle_id.empty() || key_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id and key_id required").dump(), "application/json"); + return; + } + json payload = json::object(); + payload["key_id"] = key_id; + if (body.contains("activate_after_epoch")) payload["activate_after_epoch"] = body["activate_after_epoch"]; + if (body.contains("expire_after_epoch")) payload["expire_after_epoch"] = body["expire_after_epoch"]; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_key_grant"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/key_extend", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + if (circle_id.empty() || key_id.empty() || !body.contains("expire_after_epoch")) { + res.status = 400; + res.set_content(err_json("circle_id, key_id, and expire_after_epoch required").dump(), "application/json"); + return; + } + json payload = json::object(); + payload["key_id"] = key_id; + payload["expire_after_epoch"] = body["expire_after_epoch"]; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_key_extend"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/key_revoke", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); return; } - pvac_cipher ct = g_pvac.deserialize_cipher(raw.data(), raw.size()); - if (!ct) { + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + if (circle_id.empty() || key_id.empty()) { res.status = 400; - res.set_content(err_json("invalid ciphertext").dump(), "application/json"); + res.set_content(err_json("circle_id and key_id required").dump(), "application/json"); return; } - uint64_t lo = 0, hi = 0; - g_pvac.decrypt_fp(ct, lo, hi); - g_pvac.free_cipher(ct); - int64_t val; - if (hi == 0) { - val = static_cast(lo); - } else { - __uint128_t p = (__uint128_t(1) << 127) - 1; - __uint128_t full = (__uint128_t(hi) << 64) | lo; - if (full > p / 2) val = -static_cast(p - full); - else val = static_cast(lo); - } - json result; - result["value"] = val; + json payload = json::object(); + payload["key_id"] = key_id; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_key_revoke"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; res.set_content(result.dump(), "application/json"); }); - svr.Get("/api/contract/info", [](const httplib::Request& req, httplib::Response& res) { + svr.Post("/api/circle/key_erase", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD - std::string addr = req.get_param_value("address"); - if (addr.empty()) { + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { res.status = 400; - res.set_content(err_json("address required").dump(), "application/json"); + res.set_content(err_json("invalid json").dump(), "application/json"); return; } - auto r = g_rpc.vm_contract(addr); - if (!r.ok) { - res.status = 404; - res.set_content(err_json(r.error).dump(), "application/json"); + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + if (circle_id.empty() || key_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id and key_id required").dump(), "application/json"); return; } - res.set_content(r.result.dump(), "application/json"); + json payload = json::object(); + payload["key_id"] = key_id; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_key_erase"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); }); - svr.Get("/api/circle/info", [](const httplib::Request& req, httplib::Response& res) { - std::string circle_id = req.get_param_value("circle_id"); - if (circle_id.empty()) { + svr.Post("/api/circle/key_policy_put", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { res.status = 400; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json("circle_id required").dump(), "application/json"); + res.set_content(err_json("invalid json").dump(), "application/json"); return; } - octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_info(circle_id); - if (!r.ok) { - res.status = 404; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json(r.error).dump(), "application/json"); + std::string circle_id = body.value("circle_id", ""); + std::string key_id = body.value("key_id", ""); + if (circle_id.empty() || key_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id and key_id required").dump(), "application/json"); return; } - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(r.result.dump(), "application/json"); + json payload = json::object(); + payload["key_id"] = key_id; + if (body.contains("activate_after_epoch")) payload["activate_after_epoch"] = body["activate_after_epoch"]; + if (body.contains("expire_after_epoch")) payload["expire_after_epoch"] = body["expire_after_epoch"]; + payload["revoked"] = body.value("revoked", false); + payload["erased"] = body.value("erased", false); + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "1000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_key_policy_put"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); }); - svr.Get("/api/circle/asset", [](const httplib::Request& req, httplib::Response& res) { - std::string circle_id = req.get_param_value("circle_id"); - std::string path = req.get_param_value("path"); - if (circle_id.empty() || path.empty()) { + svr.Post("/api/circle/relay_claim", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { res.status = 400; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json("circle_id and path required").dump(), "application/json"); + res.set_content(err_json("invalid json").dump(), "application/json"); return; } - octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset(circle_id, path); - if (!r.ok) { - res.status = 404; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json(r.error).dump(), "application/json"); + std::string circle_id = body.value("circle_id", ""); + std::string intent_id = body.value("intent_id", ""); + std::string claim_epoch; + std::string claim_expiry_epoch; + if (body.contains("claim_epoch")) { + claim_epoch = body["claim_epoch"].is_string() + ? body["claim_epoch"].get() + : std::to_string(body["claim_epoch"].get()); + } + if (body.contains("claim_expiry_epoch")) { + claim_expiry_epoch = body["claim_expiry_epoch"].is_string() + ? body["claim_expiry_epoch"].get() + : std::to_string(body["claim_expiry_epoch"].get()); + } + if (circle_id.empty() || intent_id.empty() || claim_epoch.empty() || claim_expiry_epoch.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, intent_id, claim_epoch, and claim_expiry_epoch required").dump(), "application/json"); return; } - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(r.result.dump(), "application/json"); + const std::string subject = + "octra_circle_relay_claim|" + circle_id + "|" + intent_id + "|" + + g_wallet.addr + "|" + claim_epoch + "|" + claim_expiry_epoch; + const std::string signature = octra::ed25519_sign_detached( + reinterpret_cast(subject.data()), + subject.size(), + g_wallet.sk); + json payload; + payload["intent_id"] = intent_id; + payload["relay_id"] = g_wallet.addr; + payload["claim_epoch"] = claim_epoch; + payload["claim_expiry_epoch"] = claim_expiry_epoch; + payload["signature"] = signature; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "2000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_relay_claim"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); }); - svr.Get("/api/circle/asset_ciphertext", [](const httplib::Request& req, httplib::Response& res) { - std::string circle_id = req.get_param_value("circle_id"); - std::string path = req.get_param_value("path"); - if (circle_id.empty() || path.empty()) { + svr.Post("/api/circle/relay_cancel", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { res.status = 400; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json("circle_id and path required").dump(), "application/json"); - return; - } - octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext(circle_id, path); - if (!r.ok) { - res.status = 404; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json(r.error).dump(), "application/json"); + res.set_content(err_json("invalid json").dump(), "application/json"); return; } - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(r.result.dump(), "application/json"); - }); - - svr.Get("/api/circle/asset_ciphertext_by_key", [](const httplib::Request& req, httplib::Response& res) { - std::string circle_id = req.get_param_value("circle_id"); - std::string resource_key = req.get_param_value("resource_key"); - if (circle_id.empty() || resource_key.empty()) { + std::string circle_id = body.value("circle_id", ""); + std::string intent_id = body.value("intent_id", ""); + std::string related_key_id = body.value("related_key_id", ""); + std::string cancel_epoch; + if (body.contains("cancel_epoch")) { + cancel_epoch = body["cancel_epoch"].is_string() + ? body["cancel_epoch"].get() + : std::to_string(body["cancel_epoch"].get()); + } + std::string reason = body.value("reason", "relay_cancelled"); + const std::array allowed_reasons = { + "relay_cancelled", + "owner_cancelled", + "intent_expired", + "claim_expired", + "claim_set_exhausted", + "delivery_key_inactive", + "delivery_key_expired", + "delivery_key_revoked", + "delivery_key_erased" + }; + if (circle_id.empty() || intent_id.empty() || cancel_epoch.empty()) { res.status = 400; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json("circle_id and resource_key required").dump(), "application/json"); + res.set_content(err_json("circle_id, intent_id, and cancel_epoch required").dump(), "application/json"); return; } - octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext_by_resource_key(circle_id, resource_key); - if (!r.ok) { - res.status = 404; - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(err_json(r.error).dump(), "application/json"); + if (std::find(allowed_reasons.begin(), allowed_reasons.end(), reason) == allowed_reasons.end()) { + res.status = 400; + res.set_content(err_json("invalid relay cancel reason").dump(), "application/json"); return; } - res.set_header("Access-Control-Allow-Origin", "*"); - res.set_content(r.result.dump(), "application/json"); + const std::string subject = + "octra_circle_relay_cancel|" + circle_id + "|" + intent_id + "|" + + g_wallet.addr + "|" + cancel_epoch + "|" + reason + "|" + related_key_id; + const std::string signature = octra::ed25519_sign_detached( + reinterpret_cast(subject.data()), + subject.size(), + g_wallet.sk); + json payload; + payload["intent_id"] = intent_id; + payload["relay_id"] = g_wallet.addr; + payload["cancel_epoch"] = cancel_epoch; + payload["reason"] = reason; + payload["signature"] = signature; + if (!related_key_id.empty()) payload["related_key_id"] = related_key_id; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "2000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_relay_cancel"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); }); - svr.Post("/api/circle/deploy", [](const httplib::Request& req, httplib::Response& res) { + svr.Post("/api/circle/outbox_open", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::lock_guard lock(g_mtx); res.set_header("Access-Control-Allow-Origin", "*"); @@ -2628,68 +5972,61 @@ int main(int argc, char** argv) { return; } std::string circle_id = body.value("circle_id", ""); - std::string runtime = body.value("runtime", "octb"); - std::string privacy_class = body.value("privacy_class", "sealed"); - std::string browser_mode = body.value("browser_mode", "native_sealed"); - std::string resource_mode = body.value("resource_mode", "sealed_read"); - std::string code_b64 = body.value("code_b64", ""); - std::string policy_hash = body.value("policy_hash", ""); - std::string members_root = body.value("members_root", ""); - std::string export_policy = body.value("export_policy", ""); - if (circle_id.empty()) { - res.status = 400; - res.set_content(err_json("circle_id required").dump(), "application/json"); - return; - } - auto read_limit = [&](const char* key, const char* fallback) -> std::string { - if (!body.contains("limits") || !body["limits"].is_object()) { - return fallback; - } - auto limits = body["limits"]; - if (!limits.contains(key)) { - return fallback; + std::string intent_id = body.value("intent_id", ""); + std::string relay_policy_hash = body.value("relay_policy_hash", ""); + std::string payload_hash = body.value("payload_hash", ""); + std::string ciphertext_blob_hash = body.value("ciphertext_blob_hash", ""); + std::string delivery_key_id = body.value("delivery_key_id", ""); + std::string route_hint = body.value("route_hint", ""); + std::string callback_policy_hash = body.value("callback_policy_hash", ""); + auto read_required_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; } - if (limits[key].is_string()) { - return limits[key].get(); + if (body[key].is_string()) { + return body[key].get(); } - if (limits[key].is_number_integer()) { - return std::to_string(limits[key].get()); + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); } - return fallback; - }; - json payload; - payload["runtime"] = runtime; - payload["privacy_class"] = privacy_class; - payload["browser_mode"] = browser_mode; - payload["resource_mode"] = resource_mode; - payload["limits"] = { - {"max_stable_bytes", read_limit("max_stable_bytes", "33554432")}, - {"max_assets_bytes", read_limit("max_assets_bytes", "33554432")}, - {"max_inline_value", read_limit("max_inline_value", "65536")}, - {"max_wasm_bytes", read_limit("max_wasm_bytes", "33554432")} + return ""; }; - if (!code_b64.empty()) payload["code_b64"] = code_b64; - if (!policy_hash.empty()) payload["policy_hash"] = policy_hash; - if (!members_root.empty()) payload["members_root"] = members_root; - if (!export_policy.empty()) payload["export_policy"] = export_policy; + std::string expiry_epoch = read_required_scalar("expiry_epoch"); + std::string max_response_bytes = read_required_scalar("max_response_bytes"); + std::string fee_budget = read_required_scalar("fee_budget"); + if (circle_id.empty() || intent_id.empty() || expiry_epoch.empty() || relay_policy_hash.empty() || payload_hash.empty() || max_response_bytes.empty() || fee_budget.empty()) { + res.status = 400; + res.set_content(err_json("circle_id, intent_id, expiry_epoch, relay_policy_hash, payload_hash, max_response_bytes, and fee_budget required").dump(), "application/json"); + return; + } auto bi = get_nonce_balance(); octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = circle_id; tx.amount = "0"; tx.nonce = bi.nonce + 1; - tx.ou = parse_ou(body, "200000"); + tx.ou = parse_ou(body, "3000"); tx.timestamp = now_ts(); - tx.op_type = "deploy_circle"; + tx.op_type = "circle_outbox_open"; + json payload; + payload["intent_id"] = intent_id; + payload["expiry_epoch"] = expiry_epoch; + payload["relay_policy_hash"] = relay_policy_hash; + payload["payload_hash"] = payload_hash; + payload["max_response_bytes"] = max_response_bytes; + payload["fee_budget"] = fee_budget; + if (!ciphertext_blob_hash.empty()) payload["ciphertext_blob_hash"] = ciphertext_blob_hash; + if (!delivery_key_id.empty()) payload["delivery_key_id"] = delivery_key_id; + if (!route_hint.empty()) payload["route_hint"] = route_hint; + if (!callback_policy_hash.empty()) payload["callback_policy_hash"] = callback_policy_hash; tx.message = payload.dump(); sign_tx_fields(tx); auto result = submit_tx(tx); if (result.contains("error")) res.status = 500; - else result["circle_id"] = circle_id; res.set_content(result.dump(), "application/json"); }); - svr.Post("/api/circle/asset_encrypted", [](const httplib::Request& req, httplib::Response& res) { + svr.Post("/api/circle/ingress_commit", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::lock_guard lock(g_mtx); res.set_header("Access-Control-Allow-Origin", "*"); @@ -2700,16 +6037,30 @@ int main(int argc, char** argv) { return; } std::string circle_id = body.value("circle_id", ""); - std::string path = body.value("path", ""); - std::string content_type = body.value("content_type", ""); - std::string ciphertext_b64 = body.value("ciphertext_b64", ""); - std::string key_id = body.value("key_id", ""); - std::string plaintext_hash = body.value("plaintext_hash", ""); - std::string encoding = body.value("encoding", ""); - std::string padding_class = body.value("padding_class", ""); - if (circle_id.empty() || path.empty() || content_type.empty() || ciphertext_b64.empty() || key_id.empty() || plaintext_hash.empty()) { + std::string intent_id = body.value("intent_id", ""); + std::string relay_id = body.value("relay_id", ""); + std::string response_payload_hash = body.value("response_payload_hash", ""); + std::string response_ciphertext_blob_hash = body.value("response_ciphertext_blob_hash", ""); + std::string external_receipt_hash = body.value("external_receipt_hash", ""); + std::string signature = body.value("signature", ""); + auto read_required_scalar = [&](const char* key) -> std::string { + if (!body.contains(key)) { + return ""; + } + if (body[key].is_string()) { + return body[key].get(); + } + if (body[key].is_number_integer()) { + return std::to_string(body[key].get()); + } + return ""; + }; + std::string ingress_nonce = read_required_scalar("ingress_nonce"); + std::string response_size_bytes = read_required_scalar("response_size_bytes"); + int result_code = body.value("result_code", 0); + if (circle_id.empty() || intent_id.empty() || relay_id.empty() || ingress_nonce.empty() || response_payload_hash.empty() || response_size_bytes.empty() || signature.empty()) { res.status = 400; - res.set_content(err_json("circle_id, path, content_type, ciphertext_b64, key_id, and plaintext_hash required").dump(), "application/json"); + res.set_content(err_json("circle_id, intent_id, relay_id, ingress_nonce, response_payload_hash, response_size_bytes, and signature required").dump(), "application/json"); return; } auto bi = get_nonce_balance(); @@ -2718,17 +6069,19 @@ int main(int argc, char** argv) { tx.to_ = circle_id; tx.amount = "0"; tx.nonce = bi.nonce + 1; - tx.ou = parse_ou(body, "5000"); + tx.ou = parse_ou(body, "3000"); tx.timestamp = now_ts(); - tx.op_type = "circle_asset_put_encrypted"; - tx.encrypted_data = ciphertext_b64; + tx.op_type = "circle_ingress_commit"; json payload; - payload["path"] = path; - payload["content_type"] = content_type; - payload["key_id"] = key_id; - payload["plaintext_hash"] = plaintext_hash; - if (!encoding.empty()) payload["encoding"] = encoding; - if (!padding_class.empty()) payload["padding_class"] = padding_class; + payload["intent_id"] = intent_id; + payload["relay_id"] = relay_id; + payload["ingress_nonce"] = ingress_nonce; + payload["result_code"] = result_code; + payload["response_payload_hash"] = response_payload_hash; + payload["response_size_bytes"] = response_size_bytes; + payload["signature"] = signature; + if (!response_ciphertext_blob_hash.empty()) payload["response_ciphertext_blob_hash"] = response_ciphertext_blob_hash; + if (!external_receipt_hash.empty()) payload["external_receipt_hash"] = external_receipt_hash; tx.message = payload.dump(); sign_tx_fields(tx); auto result = submit_tx(tx); @@ -3027,4 +6380,4 @@ int main(int argc, char** argv) { printf("octra_wallet listening on http://127.0.0.1:%d\n", port); svr.listen("127.0.0.1", port); return 0; -} +} \ No newline at end of file diff --git a/rpc_client.hpp b/rpc_client.hpp index 3c0405c..987a600 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -197,6 +197,328 @@ class RpcClient { return call("circle_info", {circle_id}, 10); } + RpcResult circle_info_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleInfoAuth", {circle_id, addr, pub_b64, sig_b64}, 10); + } + + RpcResult circle_program_info(const std::string& circle_id) { + return call("octra_circleProgramInfo", {circle_id}, 15); + } + + RpcResult circle_program_info_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleProgramInfoAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_view(const std::string& circle_id, + const std::string& method, + const nlohmann::json& params, + const std::string& caller, + bool include_storage = false) { + return call("octra_circleView", {circle_id, method, params, caller, include_storage}, 15); + } + + RpcResult circle_view_auth(const std::string& circle_id, + const std::string& method, + const nlohmann::json& params, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64, + bool include_storage = false) { + return call("octra_circleViewAuth", {circle_id, method, params, addr, pub_b64, sig_b64, include_storage}, 15); + } + + RpcResult circle_slot_policy(const std::string& circle_id, const std::string& slot_ref) { + return call("octra_circleSlotPolicy", {circle_id, slot_ref}, 15); + } + + RpcResult circle_slot_policy_auth(const std::string& circle_id, + const std::string& slot_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleSlotPolicyAuth", {circle_id, slot_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_state_policy(const std::string& circle_id, const std::string& state_ref) { + return call("octra_circleStatePolicy", {circle_id, state_ref}, 15); + } + + RpcResult circle_state_policy_auth(const std::string& circle_id, + const std::string& state_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleStatePolicyAuth", {circle_id, state_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_state_descriptor(const std::string& circle_id, const std::string& state_ref) { + return call("octra_circleStateDescriptor", {circle_id, state_ref}, 15); + } + + RpcResult circle_state_descriptor_auth(const std::string& circle_id, + const std::string& state_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleStateDescriptorAuth", {circle_id, state_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_balance_cell(const std::string& circle_id, const std::string& state_ref) { + return call("octra_circleBalanceCell", {circle_id, state_ref}, 15); + } + + RpcResult circle_balance_cell_auth(const std::string& circle_id, + const std::string& state_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleBalanceCellAuth", {circle_id, state_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_register_cell(const std::string& circle_id, const std::string& state_ref) { + return call("octra_circleRegisterCell", {circle_id, state_ref}, 15); + } + + RpcResult circle_register_cell_auth(const std::string& circle_id, + const std::string& state_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleRegisterCellAuth", {circle_id, state_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_balance_binding(const std::string& circle_id, const std::string& subject_addr) { + return call("octra_circleBalanceBinding", {circle_id, subject_addr}, 15); + } + + RpcResult circle_balance_binding_auth(const std::string& circle_id, + const std::string& subject_addr, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleBalanceBindingAuth", {circle_id, subject_addr, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_register_binding(const std::string& circle_id, const std::string& register_ref) { + return call("octra_circleRegisterBinding", {circle_id, register_ref}, 15); + } + + RpcResult circle_register_binding_auth(const std::string& circle_id, + const std::string& register_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleRegisterBindingAuth", {circle_id, register_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_balance_workflow(const std::string& circle_id, const std::string& workflow_ref) { + return call("octra_circleBalanceWorkflow", {circle_id, workflow_ref}, 15); + } + + RpcResult circle_balance_workflow_auth(const std::string& circle_id, + const std::string& workflow_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleBalanceWorkflowAuth", {circle_id, workflow_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_register_workflow(const std::string& circle_id, const std::string& workflow_ref) { + return call("octra_circleRegisterWorkflow", {circle_id, workflow_ref}, 15); + } + + RpcResult circle_register_workflow_auth(const std::string& circle_id, + const std::string& workflow_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleRegisterWorkflowAuth", {circle_id, workflow_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_summary(const std::string& circle_id, const std::string& object_ref) { + return call("octra_circleObjectSummary", {circle_id, object_ref}, 15); + } + + RpcResult circle_object_summary_auth(const std::string& circle_id, + const std::string& object_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectSummaryAuth", {circle_id, object_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_members(const std::string& circle_id, const std::string& object_ref) { + return call("octra_circleObjectMembers", {circle_id, object_ref}, 15); + } + + RpcResult circle_object_members_auth(const std::string& circle_id, + const std::string& object_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectMembersAuth", {circle_id, object_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_detail(const std::string& circle_id, const std::string& object_ref) { + return call("octra_circleObjectDetail", {circle_id, object_ref}, 15); + } + + RpcResult circle_object_detail_auth(const std::string& circle_id, + const std::string& object_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectDetailAuth", {circle_id, object_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_member(const std::string& circle_id, + const std::string& object_ref, + const std::string& member_ref) { + return call("octra_circleObjectMember", {circle_id, object_ref, member_ref}, 15); + } + + RpcResult circle_object_member_auth(const std::string& circle_id, + const std::string& object_ref, + const std::string& member_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectMemberAuth", {circle_id, object_ref, member_ref, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_refs(const std::string& circle_id) { + return call("octra_circleObjectRefs", {circle_id}, 15); + } + + RpcResult circle_object_refs_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectRefsAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_object_list(const std::string& circle_id) { + return call("octra_circleObjectList", {circle_id}, 15); + } + + RpcResult circle_object_list_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleObjectListAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_transport_policy(const std::string& circle_id) { + return call("octra_circleTransportPolicy", {circle_id}, 15); + } + + RpcResult circle_transport_policy_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleTransportPolicyAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_hfhe_policy(const std::string& circle_id) { + return call("octra_circleHfhePolicy", {circle_id}, 15); + } + + RpcResult circle_hfhe_policy_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleHfhePolicyAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_key_policy(const std::string& circle_id, const std::string& key_id) { + return call("octra_circleKeyPolicy", {circle_id, key_id}, 15); + } + + RpcResult circle_key_policy_auth(const std::string& circle_id, + const std::string& key_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleKeyPolicyAuth", {circle_id, key_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_storage(const std::string& circle_id, const std::string& key) { + return call("octra_circleStorage", {circle_id, key}, 15); + } + + RpcResult circle_storage_auth(const std::string& circle_id, + const std::string& key, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleStorageAuth", {circle_id, key, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_storage_dump(const std::string& circle_id) { + return call("octra_circleStorageDump", {circle_id}, 15); + } + + RpcResult circle_storage_dump_auth(const std::string& circle_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleStorageDumpAuth", {circle_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_outbox_intent(const std::string& circle_id, const std::string& intent_id) { + return call("octra_circleOutboxIntent", {circle_id, intent_id}, 15); + } + + RpcResult circle_outbox_claim(const std::string& circle_id, const std::string& intent_id) { + return call("octra_circleOutboxClaim", {circle_id, intent_id}, 15); + } + + RpcResult circle_outbox_claim_auth(const std::string& circle_id, + const std::string& intent_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleOutboxClaimAuth", {circle_id, intent_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_outbox_intent_auth(const std::string& circle_id, + const std::string& intent_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleOutboxIntentAuth", {circle_id, intent_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_outbox_status(const std::string& circle_id, const std::string& intent_id) { + return call("octra_circleOutboxStatus", {circle_id, intent_id}, 15); + } + + RpcResult circle_outbox_status_auth(const std::string& circle_id, + const std::string& intent_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleOutboxStatusAuth", {circle_id, intent_id, addr, pub_b64, sig_b64}, 15); + } + + RpcResult circle_ingress_packet(const std::string& circle_id, const std::string& intent_id) { + return call("octra_circleIngressPacket", {circle_id, intent_id}, 15); + } + + RpcResult circle_ingress_packet_auth(const std::string& circle_id, + const std::string& intent_id, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleIngressPacketAuth", {circle_id, intent_id, addr, pub_b64, sig_b64}, 15); + } + RpcResult circle_asset(const std::string& circle_id, const std::string& path) { return call("circle_asset", {circle_id, path}, 10); } @@ -205,10 +527,50 @@ class RpcClient { return call("circle_asset_ciphertext", {circle_id, path}, 10); } + RpcResult circle_asset_ciphertext_auth(const std::string& circle_id, + const std::string& path, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleAssetCiphertextAuth", {circle_id, path, addr, pub_b64, sig_b64}, 10); + } + RpcResult circle_asset_ciphertext_by_resource_key(const std::string& circle_id, const std::string& resource_key) { return call("circle_asset_ciphertext_by_resource_key", {circle_id, resource_key}, 10); } + RpcResult circle_asset_ciphertext_by_resource_key_auth(const std::string& circle_id, + const std::string& resource_key, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleAssetCiphertextByResourceKeyAuth", {circle_id, resource_key, addr, pub_b64, sig_b64}, 10); + } + + RpcResult circle_asset_ciphertext_by_slot_ref(const std::string& circle_id, const std::string& slot_ref) { + return call("circle_asset_ciphertext_by_slot_ref", {circle_id, slot_ref}, 10); + } + + RpcResult circle_asset_ciphertext_by_slot_ref_auth(const std::string& circle_id, + const std::string& slot_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleAssetCiphertextBySlotRefAuth", {circle_id, slot_ref, addr, pub_b64, sig_b64}, 10); + } + + RpcResult circle_asset_ciphertext_by_state_ref(const std::string& circle_id, const std::string& state_ref) { + return call("octra_circleAssetCiphertextByStateRef", {circle_id, state_ref}, 10); + } + + RpcResult circle_asset_ciphertext_by_state_ref_auth(const std::string& circle_id, + const std::string& state_ref, + const std::string& addr, + const std::string& pub_b64, + const std::string& sig_b64) { + return call("octra_circleAssetCiphertextByStateRefAuth", {circle_id, state_ref, addr, pub_b64, sig_b64}, 10); + } + RpcResult contract_receipt(const std::string& hash) { return call("contract_receipt", {hash}); } diff --git a/setup.sh b/setup.sh index a5d4fe5..f1f35ff 100755 --- a/setup.sh +++ b/setup.sh @@ -7,8 +7,8 @@ for arg in "$@"; do --deps-only|--no-build) MODE="deps" ;; --help|-h) echo "usage: $0 [--deps-only]" - echo " (no args) install deps + build" - echo " --deps-only install deps only (no make)" + echo "(no args) install deps + build" + echo "--deps-only install deps only (no make)" exit 0 ;; esac @@ -38,14 +38,33 @@ case "$OS" in eval "$(/usr/local/bin/brew shellenv)" fi fi - for pkg in openssl@3 leveldb; do - if ! brew list $pkg &>/dev/null; then + ensure_brew_formula() { + local pkg="$1" + local check_path="$2" + if ! brew list --versions "$pkg" &>/dev/null; then echo "installing $pkg..." - brew install $pkg + brew install "$pkg" else echo "$pkg already installed" fi - done + if [ ! -e "$check_path" ]; then + echo "$pkg looks incomplete or broken (missing $check_path)" + echo "reinstalling $pkg..." + brew reinstall "$pkg" + fi + if [ ! -e "$check_path" ]; then + echo "error: $pkg is still incomplete after reinstall" + echo "expected file: $check_path" + exit 1 + fi + } + OPENSSL_PREFIX="$(brew --prefix openssl@3 2>/dev/null || true)" + [ -n "$OPENSSL_PREFIX" ] || OPENSSL_PREFIX="/opt/homebrew/opt/openssl@3" + LEVELDB_PREFIX="$(brew --prefix leveldb 2>/dev/null || true)" + [ -n "$LEVELDB_PREFIX" ] || LEVELDB_PREFIX="/opt/homebrew/opt/leveldb" + ensure_brew_formula "openssl@3" "$OPENSSL_PREFIX/lib/libssl.3.dylib" + ensure_brew_formula "leveldb" "$LEVELDB_PREFIX/lib/libleveldb.1.dylib" + brew postinstall openssl@3 >/dev/null 2>&1 || true if ! xcode-select -p &>/dev/null; then echo "installing Xcode command line tools..." xcode-select --install 2>/dev/null || true diff --git a/static/circles.html b/static/circles.html index 036ce79..bd0d1c3 100644 --- a/static/circles.html +++ b/static/circles.html @@ -314,6 +314,61 @@ overflow: hidden; } +.modal-overlay { + position: fixed; + inset: 0; + z-index: 3200; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: rgba(59, 86, 127, 0.36); +} + +.modal-box { + width: min(520px, calc(100vw - 40px)); + border: 1px solid #C0C6D0; + background: #FFFFFF; + box-shadow: 0 12px 28px rgba(59, 86, 127, 0.18); + padding: 18px; +} + +.modal-title { + color: #3B567F; + font-size: 12px; + font-weight: bold; + text-transform: lowercase; + margin-bottom: 10px; +} + +.modal-message { + color: #3B567F; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; +} + +.modal-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; +} + +.modal-btn { + border: 1px solid #C0C6D0; + background: #FFFFFF; + color: #3B567F; + font: inherit; + padding: 8px 14px; + cursor: pointer; + text-transform: lowercase; +} + +.modal-btn-primary { + background: #EAF0F7; +} + @media (max-width: 980px) { .circle-browser-fields, .circle-authoring-grid, @@ -510,6 +565,7 @@ const h256Hex = async (tag, parts) => hexOfBytes(await h256Raw(tag, parts)) const resourceKeyOfPath = (circleId, canonicalPath) => h256Hex('octra:circle_resource_key:v1', [utf8Bytes(circleId), utf8Bytes(canonicalPath)]) +const resourceKeyOfSlotRef = (circleId, slotRef) => h256Hex('octra:circle_resource_key:slot:v1', [utf8Bytes(circleId), utf8Bytes(slotRef)]) const base58Encode = (bytes) => { const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' @@ -819,14 +875,101 @@ const bridgeMethodsForInfo = (info) => [ 'circle.context', + 'program.info', + 'program.view', + 'program.call', + 'program.storage', + 'sealed_slot.read', + 'sealed_slot.put', + 'sealed_state.read', + 'sealed_state.put', + 'slot_policy.read', + 'slot_policy.put', + 'state_policy.read', + 'state_policy.put', + 'state_descriptor.read', + 'state_descriptor.put', + 'balance_cell.read', + 'balance_cell.put', + 'balance_binding.read', + 'balance_workflow.read', + 'object.refs.read', + 'object.list.read', + 'object.detail.read', + 'object.member.read', + 'object.summary.read', + 'object.members.read', + 'object.policy.define', + 'object.bind', + 'object.member.attach', + 'object.member.detach', + 'object.transition.apply', + 'register_cell.read', + 'register_cell.put', + 'register_binding.read', + 'register_workflow.read', + 'transport_policy.read', + 'transport_policy.put', + 'hfhe_policy.read', + 'hfhe_policy.put', + 'key_policy.read', + 'key_policy.put', + 'key.grant', + 'key.extend', + 'key.revoke', + 'key.erase', + 'outbox.open', + 'outbox.intent', + 'outbox.claim', + 'outbox.status', + 'relay.claim', + 'relay.cancel', + 'ingress.commit', + 'ingress.packet', 'wallet.info', 'wallet.balance', 'wallet.keys', + 'wallet.send', + 'fhe.load_pk', 'fhe.encrypt', - 'fhe.decrypt' + 'fhe.decrypt', + 'fhe.commit', + 'fhe.pedersen', + 'fhe.serialize_cipher', + 'fhe.deserialize_cipher', + 'fhe.verify_zero', + 'fhe.verify_range', + 'fhe.verify_bound', + 'relay.request', + 'relay.status', + 'relay.response', + 'relay.receipt', + 'relay.ingress', + 'relay.health' ] const bridgeGrantTextOf = (context, method) => { + if (method === 'program.info' || method === 'program.view' || method === 'program.storage' || method === 'program.abi') { + return `allow this circle to read onchain program state for this session?\n\n${context.uri}` + } + if (method === 'program.call') { + return `allow this circle to submit onchain program calls for this session?\n\n${context.uri}` + } + if (method === 'sealed_slot.put' || method === 'sealed_state.put' || method === 'slot_policy.put' || method === 'state_policy.put' || method === 'state_descriptor.put' || method === 'balance_cell.put' || method === 'register_cell.put' || method === 'outbox.open' || method === 'ingress.commit') { + return `allow this circle to submit low-level circle runtime transactions for this session?\n\n${context.uri}` + } + if (method === 'object.policy.define' || method === 'object.bind' || method === 'object.member.attach' || method === 'object.member.detach' || method === 'object.transition.apply') { + return `allow this circle to submit private object runtime writes for this session?\n\n${context.uri}` + } + if (method === 'sealed_slot.read' || method === 'sealed_state.read' || method === 'slot_policy.read' || method === 'state_policy.read' || method === 'state_descriptor.read' || method === 'balance_cell.read' || method === 'balance_binding.read' || method === 'balance_workflow.read' || method === 'object.refs.read' || method === 'object.list.read' || method === 'object.detail.read' || method === 'object.member.read' || method === 'object.summary.read' || method === 'object.members.read' || method === 'register_cell.read' || method === 'register_binding.read' || method === 'register_workflow.read' || method === 'outbox.intent' || method === 'outbox.status' || method === 'ingress.packet') { + return `allow this circle to read low-level circle runtime state for this session?\n\n${context.uri}` + } + if (method === 'transport_policy.put' || method === 'hfhe_policy.put' || method === 'key_policy.put' || method === 'key.grant' || method === 'key.extend' || method === 'key.revoke' || method === 'key.erase' || method === 'relay.claim' || method === 'relay.cancel') { + return `allow this circle to modify low-level circle policy or relay transport state for this session?\n\n${context.uri}` + } + if (method === 'transport_policy.read' || method === 'hfhe_policy.read' || method === 'key_policy.read' || method === 'outbox.claim') { + return `allow this circle to read low-level circle policy or relay state for this session?\n\n${context.uri}` + } if (method === 'wallet.info') { return `allow this circle to read the active wallet address for this session?\n\n${context.uri}` } @@ -836,15 +979,72 @@ if (method === 'wallet.keys') { return `allow this circle to read the wallet view public key for this session?\n\n${context.uri}` } + if (method === 'wallet.send') { + return `allow this circle to send OCT through the active wallet for this session?\n\n${context.uri}` + } if (method === 'fhe.encrypt') { return `allow this circle to request FHE encryption for this session?\n\n${context.uri}` } + if (method === 'fhe.load_pk') { + return `allow this circle to request PVAC public keys for this session?\n\n${context.uri}` + } if (method === 'fhe.decrypt') { return `allow this circle to request FHE decryption for this session?\n\n${context.uri}` } + if (method === 'fhe.commit') { + return `allow this circle to request FHE ciphertext commitments for this session?\n\n${context.uri}` + } + if (method === 'fhe.pedersen') { + return `allow this circle to request FHE amount commitments for this session?\n\n${context.uri}` + } + if (method === 'fhe.serialize_cipher') { + return `allow this circle to request canonical FHE cipher serialization for this session?\n\n${context.uri}` + } + if (method === 'fhe.deserialize_cipher') { + return `allow this circle to request canonical FHE cipher decoding for this session?\n\n${context.uri}` + } + if (method === 'fhe.verify_zero') { + return `allow this circle to request FHE zero-proof verification for this session?\n\n${context.uri}` + } + if (method === 'fhe.verify_range') { + return `allow this circle to request FHE range-proof verification for this session?\n\n${context.uri}` + } + if (method === 'fhe.verify_bound') { + return `allow this circle to request FHE bound-proof verification for this session?\n\n${context.uri}` + } + if (method.startsWith('relay.')) { + return `allow this circle to use the local relay membrane for this session?\n\n${context.uri}` + } return `allow this circle to use runtime access method ${method} for this session?\n\n${context.uri}` } +const bridgeGrantScopeOf = (method) => { + if (method === 'circle.context') return 'circle.context' + if (method === 'program.call') return 'program.call' + if (method === 'program.info' || method === 'program.view' || method === 'program.storage' || method === 'program.abi') return 'program.read' + if (method === 'sealed_slot.put' || method === 'sealed_state.put' || method === 'slot_policy.put' || method === 'state_policy.put' || method === 'state_descriptor.put' || method === 'balance_cell.put' || method === 'register_cell.put' || method === 'outbox.open' || method === 'ingress.commit') return 'circle.write' + if (method === 'object.policy.define' || method === 'object.bind' || method === 'object.member.attach' || method === 'object.member.detach' || method === 'object.transition.apply') return 'circle.object.write' + if (method === 'sealed_slot.read' || method === 'sealed_state.read' || method === 'slot_policy.read' || method === 'state_policy.read' || method === 'state_descriptor.read' || method === 'balance_cell.read' || method === 'balance_binding.read' || method === 'balance_workflow.read' || method === 'object.refs.read' || method === 'object.list.read' || method === 'object.detail.read' || method === 'object.member.read' || method === 'object.summary.read' || method === 'object.members.read' || method === 'register_cell.read' || method === 'register_binding.read' || method === 'register_workflow.read' || method === 'outbox.intent' || method === 'outbox.status' || method === 'ingress.packet') return 'circle.read' + if (method === 'transport_policy.put' || method === 'hfhe_policy.put' || method === 'key_policy.put' || method === 'key.grant' || method === 'key.extend' || method === 'key.revoke' || method === 'key.erase' || method === 'relay.claim' || method === 'relay.cancel') return 'circle.policy.write' + if (method === 'transport_policy.read' || method === 'hfhe_policy.read' || method === 'key_policy.read' || method === 'outbox.claim') return 'circle.policy.read' + if (method === 'wallet.send') return 'wallet.send' + if (method === 'wallet.info') return 'wallet.info' + if (method === 'wallet.balance') return 'wallet.balance' + if (method === 'wallet.keys') return 'wallet.keys' + if (method === 'fhe.load_pk') return 'fhe.load_pk' + if (method === 'fhe.encrypt') return 'fhe.encrypt' + if (method === 'fhe.decrypt') return 'fhe.decrypt' + if (method === 'fhe.commit') return 'fhe.commit' + if (method === 'fhe.pedersen') return 'fhe.pedersen' + if (method === 'fhe.serialize_cipher') return 'fhe.serialize_cipher' + if (method === 'fhe.deserialize_cipher') return 'fhe.deserialize_cipher' + if (method === 'fhe.verify_zero') return 'fhe.verify_zero' + if (method === 'fhe.verify_range') return 'fhe.verify_range' + if (method === 'fhe.verify_bound') return 'fhe.verify_bound' + if (method.startsWith('relay.')) return 'relay.access' + return method +} + const ensureBridgeGrant = async (method) => { if (!activeBridgeContext) { throw new Error('sealed bridge inactive') @@ -852,7 +1052,7 @@ if (method === 'circle.context') { return } - const grantKey = `${activeBridgeContext.circle_id}:${method}` + const grantKey = `${activeBridgeContext.circle_id}:${bridgeGrantScopeOf(method)}` if (bridgeGrantState.get(grantKey)) { return } @@ -874,6 +1074,291 @@ if (method === 'circle.context') { return activeBridgeContext } + const bridgeCircleId = activeBridgeContext.circle_id || '' + const requireBridgeCircleId = () => { + if (!bridgeCircleId) { + throw new Error('sealed bridge has no active circle target') + } + if (payload.address) { + throw new Error('bridge target override denied') + } + if (payload.circle_id && payload.circle_id !== bridgeCircleId) { + throw new Error('bridge target override denied') + } + return bridgeCircleId + } + if (method === 'program.info') { + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/program/info?circle_id=${encodeURIComponent(effectiveCircleId)}`) + } + if (method === 'program.view') { + const effectiveCircleId = requireBridgeCircleId() + const nextPayload = { ...payload, circle_id: effectiveCircleId } + return postJson('/api/program/view', nextPayload) + } + if (method === 'program.call') { + const effectiveCircleId = requireBridgeCircleId() + const nextPayload = { ...payload, circle_id: effectiveCircleId } + return postJson('/api/program/call', nextPayload) + } + if (method === 'program.storage') { + if (!payload.key) { + throw new Error('program.storage requires key') + } + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/program/storage?circle_id=${encodeURIComponent(effectiveCircleId)}&key=${encodeURIComponent(payload.key)}`) + } + if (method === 'program.abi') { + throw new Error('program.abi disabled in native sealed mode') + } + if (method === 'sealed_slot.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.slot_ref) { + throw new Error('sealed_slot.read requires slot_ref') + } + return fetchJson(`/api/circle/asset_ciphertext_by_slot?circle_id=${encodeURIComponent(effectiveCircleId)}&slot_ref=${encodeURIComponent(payload.slot_ref)}`) + } + if (method === 'sealed_slot.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/sealed_slot_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'sealed_state.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.state_ref) { + throw new Error('sealed_state.read requires state_ref') + } + return fetchJson(`/api/circle/asset_ciphertext_by_state?circle_id=${encodeURIComponent(effectiveCircleId)}&state_ref=${encodeURIComponent(payload.state_ref)}`) + } + if (method === 'sealed_state.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/sealed_slot_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'slot_policy.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.slot_ref) { + throw new Error('slot_policy.read requires circle_id and slot_ref') + } + return fetchJson(`/api/circle/slot_policy?circle_id=${encodeURIComponent(effectiveCircleId)}&slot_ref=${encodeURIComponent(payload.slot_ref)}`) + } + if (method === 'slot_policy.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/slot_policy_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'state_policy.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.state_ref) { + throw new Error('state_policy.read requires state_ref') + } + return fetchJson(`/api/circle/state_policy?circle_id=${encodeURIComponent(effectiveCircleId)}&state_ref=${encodeURIComponent(payload.state_ref)}`) + } + if (method === 'state_policy.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/slot_policy_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'state_descriptor.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.state_ref) { + throw new Error('state_descriptor.read requires state_ref') + } + return fetchJson(`/api/circle/state_descriptor?circle_id=${encodeURIComponent(effectiveCircleId)}&state_ref=${encodeURIComponent(payload.state_ref)}`) + } + if (method === 'state_descriptor.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/state_descriptor_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'balance_cell.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.state_ref) { + throw new Error('balance_cell.read requires state_ref') + } + return fetchJson(`/api/circle/balance_cell?circle_id=${encodeURIComponent(effectiveCircleId)}&state_ref=${encodeURIComponent(payload.state_ref)}`) + } + if (method === 'balance_binding.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.subject_addr) { + throw new Error('balance_binding.read requires subject_addr') + } + return fetchJson(`/api/circle/balance_binding?circle_id=${encodeURIComponent(effectiveCircleId)}&subject_addr=${encodeURIComponent(payload.subject_addr)}`) + } + if (method === 'balance_workflow.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.workflow_ref) { + throw new Error('balance_workflow.read requires workflow_ref') + } + return fetchJson(`/api/circle/balance_workflow?circle_id=${encodeURIComponent(effectiveCircleId)}&workflow_ref=${encodeURIComponent(payload.workflow_ref)}`) + } + if (method === 'object.refs.read') { + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/circle/object_refs?circle_id=${encodeURIComponent(effectiveCircleId)}`) + } + if (method === 'object.list.read') { + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/circle/object_list?circle_id=${encodeURIComponent(effectiveCircleId)}`) + } + if (method === 'object.detail.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.object_ref) { + throw new Error('object.detail.read requires object_ref') + } + return fetchJson(`/api/circle/object_detail?circle_id=${encodeURIComponent(effectiveCircleId)}&object_ref=${encodeURIComponent(payload.object_ref)}`) + } + if (method === 'object.member.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.object_ref || !payload.member_ref) { + throw new Error('object.member.read requires object_ref and member_ref') + } + return fetchJson(`/api/circle/object_member?circle_id=${encodeURIComponent(effectiveCircleId)}&object_ref=${encodeURIComponent(payload.object_ref)}&member_ref=${encodeURIComponent(payload.member_ref)}`) + } + if (method === 'object.summary.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.object_ref) { + throw new Error('object.summary.read requires object_ref') + } + return fetchJson(`/api/circle/object_summary?circle_id=${encodeURIComponent(effectiveCircleId)}&object_ref=${encodeURIComponent(payload.object_ref)}`) + } + if (method === 'object.members.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.object_ref) { + throw new Error('object.members.read requires object_ref') + } + return fetchJson(`/api/circle/object_members?circle_id=${encodeURIComponent(effectiveCircleId)}&object_ref=${encodeURIComponent(payload.object_ref)}`) + } + if (method === 'object.policy.define') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/object_policy_define', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'object.bind') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/object_bind', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'object.member.attach') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/object_member_attach', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'object.member.detach') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/object_member_detach', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'object.transition.apply') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/object_transition_apply', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'balance_cell.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/balance_cell_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'register_cell.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.state_ref) { + throw new Error('register_cell.read requires state_ref') + } + return fetchJson(`/api/circle/register_cell?circle_id=${encodeURIComponent(effectiveCircleId)}&state_ref=${encodeURIComponent(payload.state_ref)}`) + } + if (method === 'register_binding.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.register_ref) { + throw new Error('register_binding.read requires register_ref') + } + return fetchJson(`/api/circle/register_binding?circle_id=${encodeURIComponent(effectiveCircleId)}®ister_ref=${encodeURIComponent(payload.register_ref)}`) + } + if (method === 'register_workflow.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.workflow_ref) { + throw new Error('register_workflow.read requires workflow_ref') + } + return fetchJson(`/api/circle/register_workflow?circle_id=${encodeURIComponent(effectiveCircleId)}&workflow_ref=${encodeURIComponent(payload.workflow_ref)}`) + } + if (method === 'register_cell.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/register_cell_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'transport_policy.read') { + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/circle/transport_policy?circle_id=${encodeURIComponent(effectiveCircleId)}`) + } + if (method === 'transport_policy.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/transport_policy_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'hfhe_policy.read') { + const effectiveCircleId = requireBridgeCircleId() + return fetchJson(`/api/circle/hfhe_policy?circle_id=${encodeURIComponent(effectiveCircleId)}`) + } + if (method === 'hfhe_policy.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/hfhe_policy_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'key_policy.read') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.key_id) { + throw new Error('key_policy.read requires key_id') + } + return fetchJson(`/api/circle/key_policy?circle_id=${encodeURIComponent(effectiveCircleId)}&key_id=${encodeURIComponent(payload.key_id)}`) + } + if (method === 'key_policy.put') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/key_policy_put', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'key.grant') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/key_grant', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'key.extend') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/key_extend', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'key.revoke') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/key_revoke', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'key.erase') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/key_erase', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'outbox.open') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/outbox_open', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'outbox.intent') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.intent_id) { + throw new Error('outbox.intent requires circle_id and intent_id') + } + return fetchJson(`/api/circle/outbox_intent?circle_id=${encodeURIComponent(effectiveCircleId)}&intent_id=${encodeURIComponent(payload.intent_id)}`) + } + if (method === 'outbox.claim') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.intent_id) { + throw new Error('outbox.claim requires circle_id and intent_id') + } + return fetchJson(`/api/circle/outbox_claim?circle_id=${encodeURIComponent(effectiveCircleId)}&intent_id=${encodeURIComponent(payload.intent_id)}`) + } + if (method === 'outbox.status') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.intent_id) { + throw new Error('outbox.status requires circle_id and intent_id') + } + return fetchJson(`/api/circle/outbox_status?circle_id=${encodeURIComponent(effectiveCircleId)}&intent_id=${encodeURIComponent(payload.intent_id)}`) + } + if (method === 'relay.claim') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/relay_claim', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'relay.cancel') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/relay_cancel', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'ingress.commit') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/ingress_commit', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'ingress.packet') { + const effectiveCircleId = requireBridgeCircleId() + if (!payload.intent_id) { + throw new Error('ingress.packet requires circle_id and intent_id') + } + return fetchJson(`/api/circle/ingress_packet?circle_id=${encodeURIComponent(effectiveCircleId)}&intent_id=${encodeURIComponent(payload.intent_id)}`) + } if (method === 'wallet.info') { return fetchJson('/api/wallet') } @@ -883,11 +1368,76 @@ if (method === 'wallet.keys') { return fetchJson('/api/keys') } + if (method === 'wallet.send') { + return postJson('/api/send', payload) + } + if (method === 'fhe.load_pk') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/load_pk', { ...payload, circle_id: effectiveCircleId }) + } if (method === 'fhe.encrypt') { - return postJson('/api/fhe/encrypt', payload) + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/encrypt', { ...payload, circle_id: effectiveCircleId }) } if (method === 'fhe.decrypt') { - return postJson('/api/fhe/decrypt', payload) + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/decrypt', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.commit') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/commit', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.pedersen') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/pedersen', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.serialize_cipher') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/serialize_cipher', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.deserialize_cipher') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/deserialize_cipher', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.verify_zero') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/verify_zero', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.verify_range') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/verify_range', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'fhe.verify_bound') { + const effectiveCircleId = requireBridgeCircleId() + return postJson('/api/circle/fhe/verify_bound', { ...payload, circle_id: effectiveCircleId }) + } + if (method === 'relay.request') { + return postJson('/api/relay/request', payload) + } + if (method === 'relay.status') { + const suffix = payload.request_id ? `?request_id=${encodeURIComponent(payload.request_id)}` : '' + return fetchJson(`/api/relay/status${suffix}`) + } + if (method === 'relay.response') { + if (!payload.request_id) { + throw new Error('relay.response requires request_id') + } + return fetchJson(`/api/relay/response?request_id=${encodeURIComponent(payload.request_id)}`) + } + if (method === 'relay.receipt') { + if (!payload.request_id) { + throw new Error('relay.receipt requires request_id') + } + return fetchJson(`/api/relay/receipt?request_id=${encodeURIComponent(payload.request_id)}`) + } + if (method === 'relay.ingress') { + if (!payload.request_id) { + throw new Error('relay.ingress requires request_id') + } + return fetchJson(`/api/relay/ingress?request_id=${encodeURIComponent(payload.request_id)}`) + } + if (method === 'relay.health') { + return fetchJson('/api/relay/health') } throw new Error(`unsupported bridge method: ${method}`) } @@ -931,6 +1481,9 @@ return 0 } +const circleAssetMaxRawBytes = 33554432 +const circleAssetMaxB64Bytes = Math.ceil(circleAssetMaxRawBytes / 3) * 4 + const paddedFrame = (plaintextBytes, paddingClass) => { const bare = mergeBytes(u32be(plaintextBytes.length), plaintextBytes) const target = padTargetBytes(paddingClass) @@ -940,6 +1493,20 @@ return mergeBytes(bare, randomBytes(aligned - bare.length)) } +const circleAssetDecodedSizeUpperBound = wireLen => Math.ceil(wireLen / 4) * 3 + +const circleAssetFeeOfCiphertextB64 = ciphertextB64 => { + const rawUpperBound = circleAssetDecodedSizeUpperBound(ciphertextB64.length) + if (rawUpperBound <= 4096) return 5000 + if (rawUpperBound <= 16384) return 10000 + if (rawUpperBound <= 32768) return 20000 + if (rawUpperBound <= 131072) return 40000 + if (rawUpperBound <= 524288) return 80000 + if (rawUpperBound <= 2097152) return 160000 + if (rawUpperBound <= 8388608) return 320000 + return 640000 +} + const deriveReadKey = async (circleId, keyId, passphrase) => { const cacheKey = `${circleId}:${keyId}:${passphrase}` if (!keyCache.has(cacheKey)) { @@ -1008,9 +1575,9 @@ const loadPlainAsset = async (circleId, path) => fetchJson(`/api/circle/asset?circle_id=${encodeURIComponent(circleId)}&path=${encodeURIComponent(normalizeAssetPath(path))}`) -const loadSealedAsset = async (circleId, path, passphrase) => { +const loadSealedAsset = async (circleId, path, passphrase, versionToken = '') => { const normalizedPath = normalizeAssetPath(path) - const cacheKey = `${circleId}:${normalizedPath}:${passphrase}` + const cacheKey = `${circleId}:${versionToken}:${normalizedPath}:${passphrase}` if (!decryptedCache.has(cacheKey)) { decryptedCache.set(cacheKey, (async () => { const resourceKey = await resourceKeyOfPath(circleId, normalizedPath) @@ -1018,6 +1585,7 @@ const bytes = await decryptSealedBytes(circleId, asset, passphrase) return { ...asset, + canonical_path: asset.canonical_path || normalizedPath, bytes, text: isTextContent(asset.content_type) ? bytesToText(bytes) : '' } @@ -1041,7 +1609,7 @@ head.prepend(meta) } -const materializeCss = async (circleId, cssPath, cssText, passphrase, seen = new Set()) => { +const materializeCss = async (circleId, cssPath, cssText, passphrase, versionToken = '', seen = new Set()) => { const cssKey = `${circleId}:${cssPath}` if (seen.has(cssKey)) return '' const nextSeen = new Set(seen) @@ -1056,8 +1624,8 @@ : await (async () => { const resolved = resolveCirclePath(cssPath, source) if (!resolved || isDataSpec(resolved) || isBlockedRemoteSpec(resolved)) return '' - const imported = await loadSealedAsset(circleId, resolved, passphrase) - return await materializeCss(circleId, resolved, imported.text, passphrase, nextSeen) + const imported = await loadSealedAsset(circleId, resolved, passphrase, versionToken) + return await materializeCss(circleId, resolved, imported.text, passphrase, versionToken, nextSeen) })() result = `${result.slice(0, importMatch.index)}${replacement}${result.slice(importMatch.index + importMatch[0].length)}` importRegex.lastIndex = 0 @@ -1074,7 +1642,7 @@ if (!resolved || isBlockedRemoteSpec(resolved)) { replacement = 'url("data:,")' } else { - const asset = await loadSealedAsset(circleId, resolved, passphrase) + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) replacement = `url("${makeDataUrl(asset.content_type, asset.bytes)}")` } } @@ -1333,13 +1901,13 @@ return circleUriOf(circleId, resolved) } -const materializeSealedHtml = async (circleId, htmlPath, htmlText, passphrase, bridgeToken) => { +const materializeSealedHtml = async (circleId, htmlPath, htmlText, passphrase, bridgeToken, versionToken = '') => { const doc = new DOMParser().parseFromString(htmlText, 'text/html') injectSealedPolicy(doc) installSealedPrelude(doc, circleId, htmlPath, bridgeToken) const inlineStyles = Array.from(doc.querySelectorAll('style')) for (const node of inlineStyles) { - node.textContent = await materializeCss(circleId, htmlPath, node.textContent || '', passphrase) + node.textContent = await materializeCss(circleId, htmlPath, node.textContent || '', passphrase, versionToken) } const styleLinks = Array.from(doc.querySelectorAll('link[href]')) for (const node of styleLinks) { @@ -1353,9 +1921,9 @@ if (!resolved || isBlockedRemoteSpec(resolved)) { node.remove() } else { - const asset = await loadSealedAsset(circleId, resolved, passphrase) + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) const style = doc.createElement('style') - style.textContent = await materializeCss(circleId, resolved, asset.text, passphrase) + style.textContent = await materializeCss(circleId, resolved, asset.text, passphrase, versionToken) node.replaceWith(style) } } @@ -1364,7 +1932,7 @@ } else if (!isDataSpec(href)) { const resolved = resolveCirclePath(htmlPath, href) if (resolved) { - const asset = await loadSealedAsset(circleId, resolved, passphrase) + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) node.setAttribute('href', makeDataUrl(asset.content_type, asset.bytes)) } } @@ -1375,14 +1943,14 @@ if (isBlockedRemoteSpec(src)) { node.remove() } else { - const resolved = resolveCirclePath(htmlPath, src) - if (!resolved || isBlockedRemoteSpec(resolved)) { - node.remove() - } else { - const asset = await loadSealedAsset(circleId, resolved, passphrase) - const inline = doc.createElement('script') - inline.textContent = asset.text - node.replaceWith(inline) + const resolved = resolveCirclePath(htmlPath, src) + if (!resolved || isBlockedRemoteSpec(resolved)) { + node.remove() + } else { + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) + const inline = doc.createElement('script') + inline.textContent = asset.text + node.replaceWith(inline) } } } @@ -1397,7 +1965,7 @@ } else if (!isDataSpec(src)) { const resolved = resolveCirclePath(htmlPath, src) if (resolved && !isBlockedRemoteSpec(resolved)) { - const asset = await loadSealedAsset(circleId, resolved, passphrase) + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) node.setAttribute('src', makeDataUrl(asset.content_type, asset.bytes)) } } @@ -1410,7 +1978,7 @@ } else if (!isDataSpec(poster)) { const resolved = resolveCirclePath(htmlPath, poster) if (resolved && !isBlockedRemoteSpec(resolved)) { - const asset = await loadSealedAsset(circleId, resolved, passphrase) + const asset = await loadSealedAsset(circleId, resolved, passphrase, versionToken) node.setAttribute('poster', makeDataUrl(asset.content_type, asset.bytes)) } } @@ -1454,7 +2022,8 @@ } const renderSealedAsset = async (circleId, path, info, passphrase) => { - const asset = await loadSealedAsset(circleId, path, passphrase) + const versionToken = info.assets_root || info.stable_root || '' + const asset = await loadSealedAsset(circleId, path, passphrase, versionToken) setPreviewExpandAvailable(true) $('preview-head').textContent = `oct://${circleId}${asset.canonical_path} | sealed_read | ${asset.content_type} | key_id=${asset.key_id || 'none'}` const body = $('preview-body') @@ -1470,7 +2039,7 @@ activeBridgeWindow = frame.contentWindow } }) - frame.srcdoc = await materializeSealedHtml(circleId, asset.canonical_path, asset.text, passphrase, bridgeToken) + frame.srcdoc = await materializeSealedHtml(circleId, asset.canonical_path, asset.text, passphrase, bridgeToken, versionToken) activeBridgeContext = { circle_id: circleId, path: asset.canonical_path, @@ -1585,7 +2154,11 @@ try { const plaintext = new Uint8Array(await file.arrayBuffer()) const sealed = await encryptSealedBytes(circleId, keyId, passphrase, plaintext, paddingClass) - setStatus('upload-status', 'submitting tx...', false) + if (sealed.ciphertext_b64.length > circleAssetMaxB64Bytes) { + throw new Error('sealed asset exceeds 32mb circle limit') + } + const uploadOu = String(circleAssetFeeOfCiphertextB64(sealed.ciphertext_b64)) + setStatus('upload-status', `submitting tx | ou ${uploadOu}`, false) const result = await postJson('/api/circle/asset_encrypted', { circle_id: circleId, path, @@ -1595,7 +2168,7 @@ plaintext_hash: sealed.plaintext_hash, padding_class: paddingClass, ciphertext_b64: sealed.ciphertext_b64, - ou: '5000' + ou: uploadOu }) decryptedCache.clear() setStatus('upload-status', `submitted ${result.tx_hash || 'tx'}`, false) @@ -1691,4 +2264,4 @@ } - + \ No newline at end of file diff --git a/static/wallet.js b/static/wallet.js index e346114..44b04b4 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -2769,7 +2769,10 @@ async function revealPrivateKeys() { var pin = await modalPrompt('reveal private keys', 'enter 6-digit PIN', { pin: true, btnText: 'reveal' }); if (!pin || !/^\d{6}$/.test(pin)) return; try { - var res = await api('POST', '/keys/private', { pin: pin }); + var res = await api('POST', '/keys/private', { + pin: pin, + confirm: 'I_UNDERSTAND_KEY_EXPORT_RISK' + }); var pkCell = $('privkey-cell'); if (pkCell) { pkCell.className = 'mono'; @@ -2794,7 +2797,7 @@ async function revealPrivateKeys() { async function loadSettings() { try { var w = await api('GET', '/wallet'); - $('settings-rpc').value = w.rpc_url || 'http://46.101.86.250:8080'; + $('settings-rpc').value = w.rpc_url || 'https://octra.network/rpc'; $('settings-explorer').value = w.explorer_url || 'https://octrascan.io'; $('settings-bridge-signer').value = w.bridge_signer_url || 'https://relayer-002838819188.octra.network'; } catch (e) {} From a6673113c63d85ee5c259aa1406ef912bce850a3 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Mon, 8 Jun 2026 00:51:39 +0000 Subject: [PATCH 09/13] mini-security improvements --- crypto_utils.hpp | 20 + main.cpp | 532 +++++++++++++++--- pvac/include/pvac/core/types.hpp | 6 +- .../crypto/bulletproofs/inner_product.hpp | 5 +- .../crypto/bulletproofs/r1cs_verifier.hpp | 36 ++ pvac/include/pvac/crypto/ristretto255.hpp | 39 +- pvac/include/pvac/ops/verify_zero.hpp | 8 + pvac/include/pvac/ops/verify_zero_circuit.hpp | 15 + pvac/pvac_c_api.cpp | 8 + pvac/pvac_serialize.hpp | 25 +- rpc_client.hpp | 15 +- static/index.html | 48 +- static/templates/amm/main.aml | 22 +- static/templates/escrow/main.aml | 10 +- static/templates/multisig/main.aml | 47 +- static/templates/token/interfaces/IOCS01.aml | 20 +- static/templates/token/main.aml | 99 +++- static/templates/vault/main.aml | 19 +- static/wallet.js | 203 ++++++- 19 files changed, 975 insertions(+), 202 deletions(-) diff --git a/crypto_utils.hpp b/crypto_utils.hpp index 089f1f0..8f7b4b3 100644 --- a/crypto_utils.hpp +++ b/crypto_utils.hpp @@ -332,6 +332,26 @@ inline void keypair_from_seed(const uint8_t seed[32], uint8_t sk[64], uint8_t pk crypto_sign_seed_keypair(pk, sk, seed); } +inline std::string validate_pin(const std::string& pin) { + if (pin.empty()) return "PIN required"; + if (pin.size() < 8) return "PIN must be at least 8 characters"; + if (pin.size() > 64) return "PIN too long (max 64 characters)"; + if (pin.size() < 15) { + bool has_letter = false; + bool has_digit = false; + bool has_symbol = false; + for (unsigned char c : pin) { + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) has_letter = true; + else if (c >= '0' && c <= '9') has_digit = true; + else has_symbol = true; + } + if (!has_letter || !has_digit || !has_symbol) { + return "under 15 chars: must include a letter, a digit and a special symbol"; + } + } + return ""; +} + inline std::array derive_key_from_pin( const std::string& pin, const uint8_t salt[32], int iterations = 600000) { std::array key; diff --git a/main.cpp b/main.cpp index 407345e..de7e691 100644 --- a/main.cpp +++ b/main.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -160,6 +161,11 @@ static void pk_cache_put(const std::string& addr, const std::vector& pk g_pk_cache[addr] = pk; } +static void pk_cache_erase(const std::string& addr) { + std::lock_guard lk(g_pk_mtx); + g_pk_cache.erase(addr); +} + static void handle_signal(int) { octra::secure_zero(g_wallet.sk, 64); octra::secure_zero(g_wallet.pk, 32); @@ -180,6 +186,88 @@ static json err_json(const std::string& msg) { return {{"error", msg}}; } +static std::string lower_ascii(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return s; +} + +static bool starts_with(const std::string& s, const std::string& prefix) { + return s.rfind(prefix, 0) == 0; +} + +static bool is_loopback_host(std::string host, int port) { + host = lower_ascii(host); + std::string suffix = ":" + std::to_string(port); + if (host.size() > suffix.size() && + host.compare(host.size() - suffix.size(), suffix.size(), suffix) == 0) { + host.resize(host.size() - suffix.size()); + } + return host == "127.0.0.1" || host == "localhost" || host == "[::1]"; +} + +static bool is_allowed_webcli_origin(const std::string& origin, int port) { + std::string o = lower_ascii(origin); + std::string suffix = ":" + std::to_string(port); + return o == "http://127.0.0.1" + suffix || + o == "http://localhost" + suffix || + o == "http://[::1]" + suffix; +} + +static bool webcli_request_allowed(const httplib::Request& req, int port, std::string& reason) { + if (!starts_with(req.path, "/api/")) return true; + + std::string host = req.get_header_value("Host"); + if (!host.empty() && !is_loopback_host(host, port)) { + reason = "non-loopback host"; + return false; + } + + std::string fetch_site = lower_ascii(req.get_header_value("Sec-Fetch-Site")); + if (!fetch_site.empty() && fetch_site != "same-origin" && fetch_site != "none") { + reason = "cross-site fetch"; + return false; + } + + std::string origin = req.get_header_value("Origin"); + if (!origin.empty() && !is_allowed_webcli_origin(origin, port)) { + reason = "cross-origin request"; + return false; + } + + bool same_origin_fetch = !fetch_site.empty() && (fetch_site == "same-origin" || fetch_site == "none"); + bool same_origin_header = !origin.empty() && is_allowed_webcli_origin(origin, port); + bool state_changing = req.method == "POST" || req.method == "PUT" || req.method == "DELETE"; + if (state_changing && !same_origin_fetch && !same_origin_header) { + reason = "unverified origin on state-changing request"; + return false; + } + + return true; +} + +static void set_same_origin_cors_if_needed(const httplib::Request& req, + httplib::Response& res, + int port) { + std::string origin = req.get_header_value("Origin"); + if (!origin.empty() && is_allowed_webcli_origin(origin, port)) { + res.set_header("Access-Control-Allow-Origin", origin.c_str()); + res.set_header("Vary", "Origin"); + } +} + +static bool is_valid_http_url(const std::string& url) { + bool https = url.rfind("https://", 0) == 0; + bool http = url.rfind("http://", 0) == 0; + if (!https && !http) return false; + std::string rest = url.substr(https ? 8 : 7); + if (rest.empty()) return false; + if (rest.find(' ') != std::string::npos || rest.find('\t') != std::string::npos) return false; + std::string host = rest.substr(0, rest.find('/')); + return !host.empty(); +} + static bool tx_status_is_pending_like(const json& tx) { const std::string status = tx.value("status", "pending"); return status.empty() || status == "pending"; @@ -304,6 +392,10 @@ static std::string parse_ou(const json& body, const std::string& fallback) { return fallback; } +static bool is_octra_address(const std::string& addr) { + return addr.size() == 47 && addr.substr(0, 3) == "oct"; +} + static constexpr size_t CIRCLE_ASSET_MAX_RAW_BYTES = 33554432; static constexpr size_t CIRCLE_ASSET_MAX_B64_BYTES = ((CIRCLE_ASSET_MAX_RAW_BYTES + 2) / 3) * 4; @@ -347,7 +439,7 @@ static int64_t parse_amount_raw(const json& body) { if (integer_part.empty() && frac_part.empty()) return -1; for (char c : integer_part) if (c < '0' || c > '9') return -1; for (char c : frac_part) if (c < '0' || c > '9') return -1; - if (frac_part.size() > 6) frac_part = frac_part.substr(0, 6); + if (frac_part.size() > 6) return -1; while (frac_part.size() < 6) frac_part += '0'; int64_t ip = integer_part.empty() ? 0 : std::stoll(integer_part); if (ip > MAX_OCT_RAW / 1000000) return -1; @@ -1072,8 +1164,36 @@ int main(int argc, char** argv) { svr.set_write_timeout(300, 0); svr.set_keep_alive_timeout(5); svr.set_keep_alive_max_count(100); + svr.set_payload_max_length(32u * 1024u * 1024u); + + svr.set_pre_routing_handler([port](const httplib::Request& req, httplib::Response& res) { + std::string reason; + if (!webcli_request_allowed(req, port, reason)) { + res.status = 403; + res.set_header("Content-Type", "application/json"); + res.set_content(err_json("cross-origin webcli request blocked").dump(), "application/json"); + fprintf(stderr, "[csrf] blocked %s %s origin=%s sec-fetch-site=%s host=%s reason=%s\n", + req.method.c_str(), req.path.c_str(), + req.get_header_value("Origin", "-").c_str(), + req.get_header_value("Sec-Fetch-Site", "-").c_str(), + req.get_header_value("Host", "-").c_str(), + reason.c_str()); + return httplib::Server::HandlerResponse::Handled; + } + + if (starts_with(req.path, "/api/") && req.method == "OPTIONS") { + set_same_origin_cors_if_needed(req, res, port); + res.status = 204; + res.set_header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS"); + res.set_header("Access-Control-Allow-Headers", "Content-Type"); + res.set_header("Access-Control-Max-Age", "600"); + return httplib::Server::HandlerResponse::Handled; + } + + return httplib::Server::HandlerResponse::Unhandled; + }); - svr.set_post_routing_handler([](const httplib::Request& req, httplib::Response& res) { + svr.set_post_routing_handler([port](const httplib::Request& req, httplib::Response& res) { bool is_circle_resource = req.path.rfind("/oct/", 0) == 0; if (!is_circle_resource) { res.set_header("X-Frame-Options", "DENY"); @@ -1098,6 +1218,8 @@ int main(int argc, char** argv) { "frame-ancestors 'none'"); } res.set_header("Cache-Control", "no-store"); + res.headers.erase("Access-Control-Allow-Origin"); + set_same_origin_cors_if_needed(req, res, port); }); svr.set_mount_point("/", "static"); @@ -1161,9 +1283,9 @@ int main(int argc, char** argv) { std::string addr_hint = body.value("addr", ""); std::string file_hint = body.value("file", ""); std::string name_hint = body.value("name", ""); - if (pin.size() != 6 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) { + if (pin.empty()) { res.status = 400; - res.set_content(err_json("pin must be exactly 6 digits").dump(), "application/json"); + res.set_content(err_json("pin required").dump(), "application/json"); return; } @@ -1263,10 +1385,13 @@ int main(int argc, char** argv) { return; } std::string pin = body.value("pin", ""); - if (pin.size() != 6 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("pin must be exactly 6 digits").dump(), "application/json"); - return; + { + std::string verr = octra::validate_pin(pin); + if (!verr.empty()) { + res.status = 400; + res.set_content(err_json(verr).dump(), "application/json"); + return; + } } std::string name = body.value("name", "wallet"); std::string mnemonic; @@ -1325,10 +1450,13 @@ int main(int argc, char** argv) { res.set_content(err_json("priv or mnemonic required").dump(), "application/json"); return; } - if (pin.size() != 6 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("pin must be exactly 6 digits").dump(), "application/json"); - return; + { + std::string verr = octra::validate_pin(pin); + if (!verr.empty()) { + res.status = 400; + res.set_content(err_json(verr).dump(), "application/json"); + return; + } } std::string name = body.value("name", "imported"); bool is_mnemonic = false; @@ -1341,7 +1469,7 @@ int main(int argc, char** argv) { { std::string addr_v2 = octra::addr_from_mnemonic(mn, 2); std::string addr_v1 = octra::addr_from_mnemonic(mn, 1); - std::string rpc_url = g_wallet_loaded ? g_wallet.rpc_url : "http://46.101.86.250:8080"; + std::string rpc_url = g_wallet_loaded ? g_wallet.rpc_url : "https://octra.network/rpc"; octra::RpcClient probe; probe.set_url(rpc_url); auto r2 = probe.get_balance(addr_v2); @@ -1453,9 +1581,9 @@ int main(int argc, char** argv) { } std::string addr = body.value("addr", ""); std::string pin = body.value("pin", ""); - if (addr.empty() || pin.size() != 6) { + if (addr.empty() || pin.empty()) { res.status = 400; - res.set_content(err_json("addr and 6-digit pin required").dump(), "application/json"); + res.set_content(err_json("addr and pin required").dump(), "application/json"); return; } auto entries = octra::load_manifest(); @@ -1516,9 +1644,9 @@ int main(int argc, char** argv) { } std::string pin = body.value("pin", ""); std::string name = body.value("name", ""); - if (pin.size() != 6 || !std::all_of(pin.begin(), pin.end(), ::isdigit)) { + if (pin.empty()) { res.status = 400; - res.set_content(err_json("6-digit pin required").dump(), "application/json"); + res.set_content(err_json("pin required").dump(), "application/json"); return; } if (pin != g_pin) { @@ -1947,17 +2075,24 @@ int main(int argc, char** argv) { svr.Get("/api/contract-storage", [](const httplib::Request& req, httplib::Response& res) { auto addr = req.get_param_value("address"); auto key = req.get_param_value("key"); + auto limit = req.get_param_value("limit"); if (addr.empty() || key.empty()) { res.status = 400; res.set_content(err_json("address and key required").dump(), "application/json"); return; } - auto r = g_rpc.contract_storage(addr, key); + auto r = g_rpc.contract_storage(addr, key, limit); json j; - if (r.ok && r.result.contains("value") && !r.result["value"].is_null()) + if (r.ok && r.result.contains("value") && !r.result["value"].is_null()) { j["value"] = r.result["value"]; - else + j["size"] = r.result.value("size", 0); + j["truncated"] = r.result.value("truncated", false); + j["limit"] = r.result.value("limit", 0); + } else { j["value"] = nullptr; + j["size"] = 0; + j["truncated"] = false; + } res.set_content(j.dump(), "application/json"); }); @@ -1970,7 +2105,7 @@ int main(int argc, char** argv) { return; } } - std::vector ops = {"standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call"}; + std::vector ops = {"standard", "encrypt", "decrypt", "stealth", "claim", "deploy", "call", "program_exec", "multi_exec"}; std::vector methods(ops.size(), "octra_recommendedFee"); std::vector params; params.reserve(ops.size()); @@ -2261,6 +2396,12 @@ int main(int argc, char** argv) { } pk_cache_put(to, their_signing_pk); } + if (their_signing_pk.size() != 32 || octra::derive_address(their_signing_pk.data()) != to) { + pk_cache_erase(to); + res.status = 400; + res.set_content(err_json("recipient public key does not match address").dump(), "application/json"); + return; + } uint8_t their_vpub[32]; if (!octra::ed25519_pub_to_x25519(their_signing_pk.data(), their_vpub)) { res.status = 400; @@ -2703,6 +2844,8 @@ int main(int argc, char** argv) { j["version"] = r.result.value("version", ""); if (r.result.contains("abi")) j["abi"] = r.result["abi"]; if (r.result.contains("disasm")) j["disasm"] = r.result["disasm"]; + if (r.result.contains("verification")) j["verification"] = r.result["verification"]; + if (r.result.contains("certificate")) j["certificate"] = r.result["certificate"]; res.set_content(j.dump(), "application/json"); } catch (const std::exception& ex) { res.status = 500; @@ -2744,6 +2887,8 @@ int main(int argc, char** argv) { j["version"] = r.result.value("version", ""); if (r.result.contains("abi")) j["abi"] = r.result["abi"]; if (r.result.contains("disasm")) j["disasm"] = r.result["disasm"]; + if (r.result.contains("verification")) j["verification"] = r.result["verification"]; + if (r.result.contains("certificate")) j["certificate"] = r.result["certificate"]; res.set_content(j.dump(), "application/json"); } catch (const std::exception& ex) { res.status = 500; @@ -2994,7 +3139,7 @@ int main(int argc, char** argv) { tx.nonce = bi.nonce + 1; tx.ou = parse_ou(body, "1000"); tx.timestamp = now_ts(); - tx.op_type = circle_id.empty() ? "call" : "circle_call"; + tx.op_type = circle_id.empty() ? "program_exec" : "circle_call"; tx.encrypted_data = method; tx.message = params_str; sign_tx_fields(tx); @@ -3003,11 +3148,96 @@ int main(int argc, char** argv) { res.set_content(result.dump(), "application/json"); }); + svr.Post("/api/program/multi_exec", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + if (!body.contains("calls") || !body["calls"].is_array() || body["calls"].empty()) { + res.status = 400; + res.set_content(err_json("calls array required").dump(), "application/json"); + return; + } + if (body["calls"].size() > 8) { + res.status = 400; + res.set_content(err_json("too many calls").dump(), "application/json"); + return; + } + json calls = json::array(); + for (size_t i = 0; i < body["calls"].size(); ++i) { + const auto& item = body["calls"][i]; + if (!item.is_object()) { + res.status = 400; + res.set_content(err_json("call must be object").dump(), "application/json"); + return; + } + std::string target = item.value("address", ""); + if (target.empty()) target = item.value("to", ""); + std::string method = item.value("method", ""); + if (!is_octra_address(target) || method.empty()) { + res.status = 400; + res.set_content(err_json("call address and method required").dump(), "application/json"); + return; + } + json params = json::array(); + if (item.contains("params")) { + if (!item["params"].is_array()) { + res.status = 400; + res.set_content(err_json("call params must be array").dump(), "application/json"); + return; + } + params = item["params"]; + } + std::string amount = "0"; + if (item.contains("amount")) { + if (item["amount"].is_string()) amount = item["amount"].get(); + else if (item["amount"].is_number_integer() || item["amount"].is_number_unsigned()) amount = item["amount"].dump(); + else { + res.status = 400; + res.set_content(err_json("call amount must be string or integer").dump(), "application/json"); + return; + } + } + if (!amount.empty() && amount[0] == '-') { + res.status = 400; + res.set_content(err_json("call amount must not be negative").dump(), "application/json"); + return; + } + calls.push_back({ + {"to", target}, + {"method", method}, + {"params", params}, + {"amount", amount.empty() ? "0" : amount} + }); + } + json payload; + payload["calls"] = calls; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = "multi_exec"; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "8000"); + tx.timestamp = now_ts(); + tx.op_type = "multi_exec"; + tx.message = payload.dump(); + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + res.set_content(result.dump(), "application/json"); + }); + svr.Get("/api/program/storage", [](const httplib::Request& req, httplib::Response& res) { WALLET_GUARD std::string circle_id = req.get_param_value("circle_id"); std::string addr = req.get_param_value("address"); std::string key = req.get_param_value("key"); + std::string limit = req.get_param_value("limit"); bool dump = req.has_param("dump") && req.get_param_value("dump") == "1"; if (circle_id.empty() && (addr.empty() || key.empty())) { res.status = 400; @@ -3034,7 +3264,7 @@ int main(int argc, char** argv) { return; } auto r = circle_id.empty() - ? g_rpc.contract_storage(addr, key) + ? g_rpc.contract_storage(addr, key, limit) : g_rpc.circle_storage_auth( circle_id, key, @@ -4887,15 +5117,12 @@ int main(int argc, char** argv) { return; } octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext(circle_id, path); - if (!r.ok) { - r = rpc.circle_asset_ciphertext_auth( - circle_id, - path, - g_wallet.addr, - g_wallet.pub_b64, - sign_circle_read_request("octra_circle_asset_ciphertext", circle_id, "path|" + path)); - } + auto r = rpc.circle_asset_ciphertext_auth( + circle_id, + path, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext", circle_id, "path|" + path)); if (!r.ok) { res.status = 404; res.set_header("Access-Control-Allow-Origin", "*"); @@ -4917,15 +5144,12 @@ int main(int argc, char** argv) { return; } octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext_by_resource_key(circle_id, resource_key); - if (!r.ok) { - r = rpc.circle_asset_ciphertext_by_resource_key_auth( - circle_id, - resource_key, - g_wallet.addr, - g_wallet.pub_b64, - sign_circle_read_request("octra_circle_asset_ciphertext_by_resource_key", circle_id, "resource_key|" + resource_key)); - } + auto r = rpc.circle_asset_ciphertext_by_resource_key_auth( + circle_id, + resource_key, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_resource_key", circle_id, "resource_key|" + resource_key)); if (!r.ok) { res.status = 404; res.set_header("Access-Control-Allow-Origin", "*"); @@ -4947,15 +5171,12 @@ int main(int argc, char** argv) { return; } octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext_by_slot_ref(circle_id, slot_ref); - if (!r.ok) { - r = rpc.circle_asset_ciphertext_by_slot_ref_auth( - circle_id, - slot_ref, - g_wallet.addr, - g_wallet.pub_b64, - sign_circle_read_request("octra_circle_asset_ciphertext_by_slot_ref", circle_id, "slot_ref|" + slot_ref)); - } + auto r = rpc.circle_asset_ciphertext_by_slot_ref_auth( + circle_id, + slot_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_slot_ref", circle_id, "slot_ref|" + slot_ref)); if (!r.ok) { res.status = 404; res.set_header("Access-Control-Allow-Origin", "*"); @@ -4977,15 +5198,12 @@ int main(int argc, char** argv) { return; } octra::RpcClient rpc(current_public_rpc_url()); - auto r = rpc.circle_asset_ciphertext_by_state_ref(circle_id, state_ref); - if (!r.ok) { - r = rpc.circle_asset_ciphertext_by_state_ref_auth( - circle_id, - state_ref, - g_wallet.addr, - g_wallet.pub_b64, - sign_circle_read_request("octra_circle_asset_ciphertext_by_state_ref", circle_id, "state_ref|" + state_ref)); - } + auto r = rpc.circle_asset_ciphertext_by_state_ref_auth( + circle_id, + state_ref, + g_wallet.addr, + g_wallet.pub_b64, + sign_circle_read_request("octra_circle_asset_ciphertext_by_state_ref", circle_id, "state_ref|" + state_ref)); if (!r.ok) { res.status = 404; res.set_header("Access-Control-Allow-Origin", "*"); @@ -5018,7 +5236,7 @@ int main(int argc, char** argv) { auto read_optional_string = [&](const char* key) -> std::string { return read_string_or(key, ""); }; - std::string circle_id = read_string_or("circle_id", ""); + std::string caller_circle_id = read_string_or("circle_id", ""); std::string runtime = read_string_or("runtime", "octb"); std::string privacy_class = read_string_or("privacy_class", "sealed"); std::string browser_mode = read_string_or("browser_mode", "native_sealed"); @@ -5027,11 +5245,6 @@ int main(int argc, char** argv) { std::string policy_hash = read_optional_string("policy_hash"); std::string members_root = read_optional_string("members_root"); std::string export_policy = read_optional_string("export_policy"); - if (circle_id.empty()) { - res.status = 400; - res.set_content(err_json("circle_id required").dump(), "application/json"); - return; - } auto read_limit = [&](const char* key, const char* fallback) -> std::string { if (!body.contains("limits") || !body["limits"].is_object()) { return fallback; @@ -5048,22 +5261,97 @@ int main(int argc, char** argv) { } return fallback; }; - json payload; - payload["runtime"] = runtime; - payload["privacy_class"] = privacy_class; - payload["browser_mode"] = browser_mode; - payload["resource_mode"] = resource_mode; - payload["limits"] = { - {"max_stable_bytes", read_limit("max_stable_bytes", "33554432")}, - {"max_assets_bytes", read_limit("max_assets_bytes", "33554432")}, - {"max_inline_value", read_limit("max_inline_value", "65536")}, - {"max_wasm_bytes", read_limit("max_wasm_bytes", "33554432")} + std::string max_stable_bytes = read_limit("max_stable_bytes", "33554432"); + std::string max_assets_bytes = read_limit("max_assets_bytes", "33554432"); + std::string max_inline_value = read_limit("max_inline_value", "65536"); + std::string max_wasm_bytes = read_limit("max_wasm_bytes", "33554432"); + auto json_str_of = [](const std::string& s) { + json tmp = s; + return tmp.dump(); + }; + auto str_or_null = [&json_str_of](const std::string& s) { + return s.empty() ? std::string("null") : json_str_of(s); }; - if (!code_b64.empty()) payload["code_b64"] = code_b64; - if (!policy_hash.empty()) payload["policy_hash"] = policy_hash; - if (!members_root.empty()) payload["members_root"] = members_root; - if (!export_policy.empty()) payload["export_policy"] = export_policy; + std::string canonical_payload; + canonical_payload.reserve(512); + canonical_payload += "{"; + canonical_payload += "\"runtime\":" + json_str_of(runtime) + ","; + canonical_payload += "\"privacy_class\":" + json_str_of(privacy_class) + ","; + canonical_payload += "\"browser_mode\":" + json_str_of(browser_mode) + ","; + canonical_payload += "\"resource_mode\":" + json_str_of(resource_mode) + ","; + canonical_payload += "\"code_b64\":" + str_or_null(code_b64) + ","; + canonical_payload += "\"policy_hash\":" + str_or_null(policy_hash) + ","; + canonical_payload += "\"members_root\":" + str_or_null(members_root) + ","; + canonical_payload += "\"export_policy\":" + str_or_null(export_policy) + ","; + canonical_payload += "\"limits\":{"; + canonical_payload += "\"max_stable_bytes\":\"" + max_stable_bytes + "\","; + canonical_payload += "\"max_assets_bytes\":\"" + max_assets_bytes + "\","; + canonical_payload += "\"max_inline_value\":\"" + max_inline_value + "\","; + canonical_payload += "\"max_wasm_bytes\":\"" + max_wasm_bytes + "\""; + canonical_payload += "}}"; auto bi = get_nonce_balance(); + uint64_t deploy_nonce = (uint64_t)(bi.nonce + 1); + auto h256_raw_fn = [](const std::string& tag, const std::vector& parts) { + std::string buf; + buf.reserve(tag.size() + 1 + parts.size() * 4 + 256); + buf += tag; + buf += '\0'; + for (const auto& p : parts) { + uint32_t n = (uint32_t)p.size(); + char b[4]; + b[0] = (char)((n >> 24) & 0xff); + b[1] = (char)((n >> 16) & 0xff); + b[2] = (char)((n >> 8) & 0xff); + b[3] = (char)(n & 0xff); + buf.append(b, 4); + buf += p; + } + auto h = octra::sha256(buf); + return std::string(reinterpret_cast(h.data()), 32); + }; + auto h256_hex_fn = [&h256_raw_fn](const std::string& tag, const std::vector& parts) { + auto raw = h256_raw_fn(tag, parts); + static const char hc[] = "0123456789abcdef"; + std::string out; + out.reserve(64); + for (unsigned char c : raw) { + out += hc[c >> 4]; + out += hc[c & 0xf]; + } + return out; + }; + std::string payload_hash_hex = h256_hex_fn("octra:circle_deploy_payload:v1", {canonical_payload}); + std::string nonce_be(8, '\0'); + { + uint64_t n = deploy_nonce; + for (int i = 7; i >= 0; --i) { nonce_be[i] = (char)(n & 0xff); n >>= 8; } + } + std::string seed = h256_raw_fn("octra:circle_deploy_id:v1", + {g_wallet.addr, nonce_be, payload_hash_hex}); + std::string b58 = octra::base58_encode( + reinterpret_cast(seed.data()), seed.size()); + std::string b58_part; + if (b58.size() >= 44) { + b58_part = b58.substr(0, 44); + } else if (b58.empty()) { + b58_part.assign(44, '1'); + } else { + std::string ext = b58; + size_t i = 0; + while (ext.size() < 44) { ext += b58[i % b58.size()]; ++i; } + b58_part = ext.substr(0, 44); + } + std::string derived_circle_id = "oct" + b58_part; + if (!caller_circle_id.empty() && caller_circle_id != derived_circle_id) { + res.status = 400; + json err; + err["error"] = "circle_id mismatch with derived address; omit circle_id to auto-derive"; + err["caller_circle_id"] = caller_circle_id; + err["derived_circle_id"] = derived_circle_id; + res.set_content(err.dump(), "application/json"); + return; + } + std::string circle_id = derived_circle_id; octra::Transaction tx; tx.from = g_wallet.addr; tx.to_ = circle_id; @@ -5072,6 +5360,50 @@ int main(int argc, char** argv) { tx.ou = parse_ou(body, "200000"); tx.timestamp = now_ts(); tx.op_type = "deploy_circle"; + tx.message = canonical_payload; + sign_tx_fields(tx); + auto result = submit_tx(tx); + if (result.contains("error")) res.status = 500; + else { + result["circle_id"] = circle_id; + result["derived_circle_id"] = derived_circle_id; + } + res.set_content(result.dump(), "application/json"); + }); + + svr.Post("/api/circle/program_update", [](const httplib::Request& req, httplib::Response& res) { + WALLET_GUARD + std::lock_guard lock(g_mtx); + res.set_header("Access-Control-Allow-Origin", "*"); + json body; + try { body = json::parse(req.body); } catch (...) { + res.status = 400; + res.set_content(err_json("invalid json").dump(), "application/json"); + return; + } + std::string circle_id = body.value("circle_id", ""); + std::string code_b64 = body.value("code_b64", ""); + if (circle_id.empty()) { + res.status = 400; + res.set_content(err_json("circle_id required").dump(), "application/json"); + return; + } + if (code_b64.empty()) { + res.status = 400; + res.set_content(err_json("code_b64 required").dump(), "application/json"); + return; + } + json payload; + payload["code_b64"] = code_b64; + auto bi = get_nonce_balance(); + octra::Transaction tx; + tx.from = g_wallet.addr; + tx.to_ = circle_id; + tx.amount = "0"; + tx.nonce = bi.nonce + 1; + tx.ou = parse_ou(body, "200000"); + tx.timestamp = now_ts(); + tx.op_type = "circle_program_update"; tx.message = payload.dump(); sign_tx_fields(tx); auto result = submit_tx(tx); @@ -6100,6 +6432,18 @@ int main(int argc, char** argv) { return; } octra::RpcClient rpc(current_public_rpc_url()); + auto info = rpc.circle_info(circle_id); + if (info.ok && info.result.value("resource_mode", "") == "sealed_read") { + const std::string uri = "oct://" + circle_id + path; + const std::string location = + "/circles.html?uri=" + httplib::detail::encode_query_param(uri) + + "&passphrase=" + httplib::detail::encode_query_param("octra-circle-demo"); + res.status = 302; + res.set_header("Access-Control-Allow-Origin", "*"); + res.set_header("Cache-Control", "no-store"); + res.set_header("Location", location); + return; + } auto r = rpc.circle_asset(circle_id, path); if (!r.ok) { res.status = 404; @@ -6266,6 +6610,21 @@ int main(int argc, char** argv) { res.set_content(err_json("rpc_url required").dump(), "application/json"); return; } + if (!is_valid_http_url(new_rpc)) { + res.status = 400; + res.set_content(err_json("invalid rpc_url: must be http or https with a host").dump(), "application/json"); + return; + } + if (!new_explorer.empty() && !is_valid_http_url(new_explorer)) { + res.status = 400; + res.set_content(err_json("invalid explorer_url: must be http or https with a host").dump(), "application/json"); + return; + } + if (!new_bridge_signer.empty() && !is_valid_http_url(new_bridge_signer)) { + res.status = 400; + res.set_content(err_json("invalid bridge_signer_url: must be http or https with a host").dump(), "application/json"); + return; + } bool cache_cleared = false; try { std::string old_rpc = g_wallet.rpc_url; @@ -6307,15 +6666,18 @@ int main(int argc, char** argv) { } std::string cur_pin = body.value("current_pin", ""); std::string new_pin = body.value("new_pin", ""); - if (cur_pin.size() != 6 || !std::all_of(cur_pin.begin(), cur_pin.end(), ::isdigit)) { + if (cur_pin.empty()) { res.status = 400; - res.set_content(err_json("current PIN must be 6 digits").dump(), "application/json"); + res.set_content(err_json("current PIN required").dump(), "application/json"); return; } - if (new_pin.size() != 6 || !std::all_of(new_pin.begin(), new_pin.end(), ::isdigit)) { - res.status = 400; - res.set_content(err_json("new PIN must be 6 digits").dump(), "application/json"); - return; + { + std::string verr = octra::validate_pin(new_pin); + if (!verr.empty()) { + res.status = 400; + res.set_content(err_json("new PIN: " + verr).dump(), "application/json"); + return; + } } if (cur_pin != g_pin) { res.status = 403; diff --git a/pvac/include/pvac/core/types.hpp b/pvac/include/pvac/core/types.hpp index c4530b5..057d110 100644 --- a/pvac/include/pvac/core/types.hpp +++ b/pvac/include/pvac/core/types.hpp @@ -131,7 +131,11 @@ inline bool is_valid_cipher_shape(const Cipher& cipher) { const auto& layer = cipher.L[layer_id]; if (layer.rule != RRule::BASE && layer.rule != RRule::PROD) return false; - if (layer.rule == RRule::PROD && (layer.pa >= cipher.L.size() || layer.pb >= cipher.L.size())) + + if (layer.rule == RRule::PROD && (layer.pa >= layer_id || layer.pb >= layer_id)) + return false; + if (layer.rule == RRule::PROD && !layer.PC.empty()) + return false; if (!layer.PC.empty() && layer.PC.size() != cipher.slots) return false; diff --git a/pvac/include/pvac/crypto/bulletproofs/inner_product.hpp b/pvac/include/pvac/crypto/bulletproofs/inner_product.hpp index 4ca7e82..47f1002 100644 --- a/pvac/include/pvac/crypto/bulletproofs/inner_product.hpp +++ b/pvac/include/pvac/crypto/bulletproofs/inner_product.hpp @@ -27,7 +27,8 @@ inline RistrettoPoint multi_scalar_mul( std::vector pts(n); for (size_t i = 0; i < n; i++) - rist_decode(pts[i], points[i]); + + pts[i] = rist_decode_or_throw(points[i]); std::vector> sbytes(n); for (size_t i = 0; i < n; i++) @@ -315,4 +316,4 @@ inline bool ipp_verify( } } -} +} \ No newline at end of file diff --git a/pvac/include/pvac/crypto/bulletproofs/r1cs_verifier.hpp b/pvac/include/pvac/crypto/bulletproofs/r1cs_verifier.hpp index 4aea8bc..cae8cf4 100644 --- a/pvac/include/pvac/crypto/bulletproofs/r1cs_verifier.hpp +++ b/pvac/include/pvac/crypto/bulletproofs/r1cs_verifier.hpp @@ -22,6 +22,37 @@ struct ConstraintSystem { } }; +inline bool rist_valid(const RistrettoPoint& point) { + ExtPoint decoded; + return rist_decode(decoded, point); +} + +inline bool ipp_points_valid(const InnerProductProof& proof) { + if (proof.L.size() != proof.R.size()) return false; + for (const auto& point : proof.L) + if (!rist_valid(point)) + return false; + for (const auto& point : proof.R) + if (!rist_valid(point)) + return false; + return true; +} + +inline bool r1cs_points_valid(const R1CSProof& proof) { + if (!rist_valid(proof.A_I1)) return false; + if (!rist_valid(proof.A_O1)) return false; + if (!rist_valid(proof.S1)) return false; + if (!rist_valid(proof.T_1)) return false; + if (!rist_valid(proof.T_3)) return false; + if (!rist_valid(proof.T_4)) return false; + if (!rist_valid(proof.T_5)) return false; + if (!rist_valid(proof.T_6)) return false; + for (const auto& point : proof.V) + if (!rist_valid(point)) + return false; + return ipp_points_valid(proof.ipp); +} + inline bool ipp_verify_with_y( Transcript& transcript, const RistrettoPoint& P, @@ -30,6 +61,10 @@ inline bool ipp_verify_with_y( size_t n, const std::vector& y_inv_n ) { + if (!rist_valid(P)) return false; + if (!rist_valid(Q)) return false; + if (!ipp_points_valid(proof)) return false; + size_t lg = proof.L.size(); if (proof.R.size() != lg) return false; if ((1ULL << lg) != n) return false; @@ -93,6 +128,7 @@ inline bool r1cs_verify( const size_t N = cs.padded_gates(); if (proof.V.size() != m) return false; + if (!r1cs_points_valid(proof)) return false; transcript.append_u64("n", N); transcript.append_u64("m", m); diff --git a/pvac/include/pvac/crypto/ristretto255.hpp b/pvac/include/pvac/crypto/ristretto255.hpp index 71570d4..644a103 100644 --- a/pvac/include/pvac/crypto/ristretto255.hpp +++ b/pvac/include/pvac/crypto/ristretto255.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../core/hash.hpp" #include "../core/field.hpp" @@ -238,6 +239,16 @@ inline Scalar sc_from_bytes(const uint8_t s[32]) { return r; } +// same + +inline bool sc_is_canonical(const Scalar& a) { + for (int i = 3; i >= 0; --i) { + if (a.v[i] < SC_L[i]) return true; + if (a.v[i] > SC_L[i]) return false; + } + return false; +} + inline void sc_tobytes(uint8_t s[32], const Scalar& a) { for (int i = 0; i < 4; i++) { for (int j = 0; j < 8; j++) @@ -655,23 +666,29 @@ inline bool rist_decode(ExtPoint& P, const RistrettoPoint& bytes) { return true; } +// lambda fixed, don't touch, add as mandatory checks for everyone + +inline ExtPoint rist_decode_or_throw(const RistrettoPoint& bytes) { + ExtPoint P; + if (!rist_decode(P, bytes)) + throw std::runtime_error("pvac: invalid Ristretto point encoding"); + return P; +} + inline RistrettoPoint rist_add(const RistrettoPoint& a, const RistrettoPoint& b) { - ExtPoint P, Q; - rist_decode(P, a); - rist_decode(Q, b); + ExtPoint P = rist_decode_or_throw(a); + ExtPoint Q = rist_decode_or_throw(b); return rist_encode(ext_add(P, Q)); } inline RistrettoPoint rist_sub(const RistrettoPoint& a, const RistrettoPoint& b) { - ExtPoint P, Q; - rist_decode(P, a); - rist_decode(Q, b); + ExtPoint P = rist_decode_or_throw(a); + ExtPoint Q = rist_decode_or_throw(b); return rist_encode(ext_sub(P, Q)); } inline RistrettoPoint rist_scalarmul(const RistrettoPoint& pt, const Scalar& s) { - ExtPoint P; - rist_decode(P, pt); + ExtPoint P = rist_decode_or_throw(pt); return rist_encode(ext_scalarmul(P, s)); } @@ -743,8 +760,10 @@ inline RistrettoPoint pedersen_commit(const Scalar& value, const Scalar& blindin ExtPoint G_pt, H_pt; RistrettoPoint G_enc = rist_G(); RistrettoPoint H_enc = rist_H(); - rist_decode(G_pt, G_enc); - rist_decode(H_pt, H_enc); + if (!rist_decode(G_pt, G_enc)) + throw std::runtime_error("pvac: invalid Pedersen G generator"); + if (!rist_decode(H_pt, H_enc)) + throw std::runtime_error("pvac: invalid Pedersen H generator"); ExtPoint vG = ext_scalarmul(G_pt, value); ExtPoint bH = ext_scalarmul(H_pt, blinding); diff --git a/pvac/include/pvac/ops/verify_zero.hpp b/pvac/include/pvac/ops/verify_zero.hpp index 58a0fa8..4a16f5e 100644 --- a/pvac/include/pvac/ops/verify_zero.hpp +++ b/pvac/include/pvac/ops/verify_zero.hpp @@ -18,7 +18,15 @@ inline std::vector> compute_layer_coeffs( std::vector> A(nL, std::vector(S, Fp{0,0})); for (const auto& e : ct.E) { + + if (e.layer_id >= nL) + throw std::runtime_error("pvac: edge layer out of range"); + if (e.idx >= pk.powg_B.size()) + throw std::runtime_error("pvac: edge public-key index out of range"); + if (e.w.size() != S) + throw std::runtime_error("pvac: edge weight/slots size mismatch"); Fp gp = pk.powg_B[e.idx]; + int sg = sgn_val(e.ch); for (size_t j = 0; j < S; j++) { Fp term = fp_mul(e.w[j], gp); diff --git a/pvac/include/pvac/ops/verify_zero_circuit.hpp b/pvac/include/pvac/ops/verify_zero_circuit.hpp index f0cb3ff..c9c89c9 100644 --- a/pvac/include/pvac/ops/verify_zero_circuit.hpp +++ b/pvac/include/pvac/ops/verify_zero_circuit.hpp @@ -48,6 +48,21 @@ inline CircuitWiring build_circuit( size_t S = ct.slots; size_t nB = bases.size(); + if (A.size() != nL) + throw std::runtime_error("pvac: coefficient/layer size mismatch"); + for (size_t lid = 0; lid < nL; ++lid) { + if (A[lid].size() != S) + throw std::runtime_error("pvac: coefficient/slots size mismatch"); + const auto& layer = ct.L[lid]; + if (layer.rule == RRule::PROD && + (layer.pa >= lid || layer.pb >= lid)) + throw std::runtime_error("pvac: invalid product parent"); + if (layer.rule == RRule::PROD && !layer.PC.empty()) + throw std::runtime_error("pvac: product layer must not contain PC"); + if (layer.rule != RRule::BASE && layer.rule != RRule::PROD) + throw std::runtime_error("pvac: invalid layer rule"); + } + CircuitWiring w; w.layer_vars.resize(nL); for (size_t lid = 0; lid < nL; lid++) diff --git a/pvac/pvac_c_api.cpp b/pvac/pvac_c_api.cpp index d7931dc..5c667e1 100644 --- a/pvac/pvac_c_api.cpp +++ b/pvac/pvac_c_api.cpp @@ -190,8 +190,16 @@ int pvac_verify_zero_bound(pvac_pubkey pk, pvac_cipher ct, pvac_zero_proof proof pvac::RistrettoPoint commit; std::memcpy(commit.data(), amount_commitment, 32); try { + + // !!!!! + pvac::ExtPoint decoded_commit; + if (!pvac::rist_decode(decoded_commit, commit)) + return 0; return pvac::verify_zero_bound(*PK(pk), *CT(ct), *ZP(proof), commit) ? 1 : 0; } catch (...) { + + + return 0; } } diff --git a/pvac/pvac_serialize.hpp b/pvac/pvac_serialize.hpp index 6d620b9..1ce690b 100644 --- a/pvac/pvac_serialize.hpp +++ b/pvac/pvac_serialize.hpp @@ -19,6 +19,7 @@ static constexpr uint8_t TAG_SECKEY = 2; static constexpr uint8_t TAG_RANGE_PROOF = 4; static constexpr uint8_t TAG_AGG_RANGE_PROOF = 5; static constexpr uint8_t TAG_ZERO_PROOF = 6; +static constexpr uint64_t MAX_BITVEC_BITS = 1ULL << 20; struct Writer { std::vector buf; @@ -152,12 +153,20 @@ struct Reader { pvac::Scalar scalar() { uint8_t b[32]; raw(b, 32); - return pvac::sc_from_bytes(b); + pvac::Scalar scalar = pvac::sc_from_bytes(b); + if (!failed && !pvac::sc_is_canonical(scalar)) + fail("pvac_ser: non-canonical scalar encoding"); + return failed ? pvac::sc_zero() : scalar; } pvac::RistrettoPoint rist_point() { pvac::RistrettoPoint pt; raw(pt.data(), 32); + if (!failed) { + pvac::ExtPoint decoded; + if (!pvac::rist_decode(decoded, pt)) + fail("pvac_ser: invalid Ristretto point encoding"); + } return pt; } @@ -165,6 +174,8 @@ struct Reader { pvac::BitVec bv; bv.nbits = u64(); size_t nw = u64(); + if (!failed && bv.nbits > MAX_BITVEC_BITS) + fail("pvac_ser: bitvec too large"); check_count(nw, 8); size_t expected_nw = static_cast((bv.nbits + 63) / 64); if (!failed && nw != expected_nw) @@ -209,8 +220,11 @@ inline void validate_cipher_structure(const pvac::Cipher& cipher) { const auto& layer = cipher.L[layer_id]; if (layer.rule != pvac::RRule::BASE && layer.rule != pvac::RRule::PROD) throw std::runtime_error("pvac_ser: invalid layer rule"); - if (layer.rule == pvac::RRule::PROD && (layer.pa >= cipher.L.size() || layer.pb >= cipher.L.size())) + if (layer.rule == pvac::RRule::PROD && + (layer.pa >= layer_id || layer.pb >= layer_id)) throw std::runtime_error("pvac_ser: invalid product parent"); + if (layer.rule == pvac::RRule::PROD && !layer.PC.empty()) + throw std::runtime_error("pvac_ser: product layer must not contain PC"); if (!layer.PC.empty() && layer.PC.size() != cipher.slots) throw std::runtime_error("pvac_ser: layer PC/slots size mismatch"); } @@ -320,7 +334,7 @@ inline pvac::Layer read_layer(Reader& r, uint8_t ver = VERSION_V2) { if (r.failed) return L; L.PC.resize(nPC); for (size_t i = 0; i < nPC; i++) - r.raw(L.PC[i].data(), 32); + L.PC[i] = r.rist_point(); } return L; @@ -350,6 +364,11 @@ inline pvac::Edge read_edge(Reader& r) { } inline std::vector serialize_cipher(const pvac::Cipher& C) { + // ! check as well (next week) + validate_cipher_structure(C); + + + Writer w; w.header(TAG_CIPHER); w.u64(C.slots); diff --git a/rpc_client.hpp b/rpc_client.hpp index 987a600..257b408 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -94,7 +94,8 @@ class RpcClient { httplib::SSLClient cli(host_, port_); cli.set_connection_timeout(timeout_sec, 0); cli.set_read_timeout(timeout_sec, 0); - cli.enable_server_certificate_verification(false); + if (cli.ssl_context()) SSL_CTX_set_default_verify_paths(cli.ssl_context()); + cli.enable_server_certificate_verification(true); auto res = cli.Post(path_, hdrs, body, "application/json"); if (!res) return {false, {}, "connection failed"}; return parse_response(res->body); @@ -220,7 +221,7 @@ class RpcClient { const nlohmann::json& params, const std::string& caller, bool include_storage = false) { - return call("octra_circleView", {circle_id, method, params, caller, include_storage}, 15); + return call("octra_circleView", {circle_id, method, params, caller, include_storage}, 600); } RpcResult circle_view_auth(const std::string& circle_id, @@ -230,7 +231,7 @@ class RpcClient { const std::string& pub_b64, const std::string& sig_b64, bool include_storage = false) { - return call("octra_circleViewAuth", {circle_id, method, params, addr, pub_b64, sig_b64, include_storage}, 15); + return call("octra_circleViewAuth", {circle_id, method, params, addr, pub_b64, sig_b64, include_storage}, 600); } RpcResult circle_slot_policy(const std::string& circle_id, const std::string& slot_ref) { @@ -590,8 +591,9 @@ class RpcClient { return call("octra_tokensByAddress", {addr}, 15); } - RpcResult contract_storage(const std::string& addr, const std::string& key) { - return call("octra_contractStorage", {addr, key}); + RpcResult contract_storage(const std::string& addr, const std::string& key, const std::string& limit = "") { + if (limit.empty()) return call("octra_contractStorage", {addr, key}); + return call("octra_contractStorage", {addr, key, limit}); } RpcResult contract_abi(const std::string& addr) { @@ -632,7 +634,8 @@ class RpcClient { httplib::SSLClient cli(host_, port_); cli.set_connection_timeout(timeout_sec, 0); cli.set_read_timeout(timeout_sec, 0); - cli.enable_server_certificate_verification(false); + if (cli.ssl_context()) SSL_CTX_set_default_verify_paths(cli.ssl_context()); + cli.enable_server_certificate_verification(true); auto r = cli.Post(path_, hdrs, body, "application/json"); if (!r) { for (auto& o : out) o.error = "connection failed"; return out; } resp_body = r->body; diff --git a/static/index.html b/static/index.html index ad7f0c1..32be453 100644 --- a/static/index.html +++ b/static/index.html @@ -79,8 +79,8 @@
@@ -320,14 +326,16 @@

                 
+        
       
- +
@@ -461,8 +461,8 @@
- - + +
@@ -482,7 +482,7 @@
- +
@@ -519,7 +519,7 @@
- +
@@ -531,7 +531,7 @@
change PIN
-
+
@@ -549,7 +549,7 @@ new PIN must be at least 8 characters. 15 or more recommended; your password manager can generate a strong one.
- +
@@ -557,16 +557,16 @@
- ← back + ← back
transaction
loading...
- + - + \ No newline at end of file diff --git a/static/style.css b/static/style.css index c02dd13..f325b62 100644 --- a/static/style.css +++ b/static/style.css @@ -648,14 +648,16 @@ td { .back-link:hover { color: #3B567F; } .msg-box { - background: #F6F7F9; - border: 1px solid #E5E9EF; - padding: 8px; - margin: 12px 0 8px; - font-size: 11px; - word-break: break-all; - white-space: pre-wrap; - color: #3B567F; +background: #F6F7F9; + border: 1px solid #E5E9EF; + padding: 8px; + margin: 12px 0 8px; + margin-left: 12px; + margin-right: 12px; + font-size: 11px; + word-break: break-all; + white-space: pre-wrap; + color: #3B567F; } @media (max-width: 700px) { @@ -1370,4 +1372,7 @@ td { .modal-btn-primary { background: #3B567F; color: #fff; border-color: #3B567F; } -.modal-btn-primary:hover { background: #2C4060; } \ No newline at end of file +.modal-btn-primary:hover { background: #2C4060; } +.acct-btn { background: #E5E9EF; } +.acct-btn:hover { background: #D0D7E2; } +.account-card:hover { background: #2A3F5F; color: #fff; } \ No newline at end of file diff --git a/static/swap.html b/static/swap.html index 046943d..e2c1bf4 100644 --- a/static/swap.html +++ b/static/swap.html @@ -82,7 +82,7 @@

OCT swap

unlock your wallet to continue

- +
- - + +
- - + +
OCT
@@ -127,7 +127,7 @@

OCT / tUSD

price impact-
fee0.1 OCT
- +
@@ -143,11 +143,11 @@

OCT / tUSD

confirm swap

- + \ No newline at end of file diff --git a/static/swap.js b/static/swap.js index a08517c..759b204 100644 --- a/static/swap.js +++ b/static/swap.js @@ -25,9 +25,9 @@ 2025-2026 Julia L. */ -var SWAP_ADDR = ''; -var TOKEN_ADDR = ''; -var SCANNER_URL = ''; +var SWAP_ADDR = 'octBjnQBicZs6iMwcRxrdzLYAzyVTi91KEiA8RGkVjco2w6'; +var TOKEN_ADDR = 'oct6J37Wx7Rb1putvfwFrFbGUStE8hGzsb33fhLgUdpTx6d'; +var SCANNER_URL = 'https://devnet.octrascan.io'; var TOKEN_SYMBOL = 'tUSD'; var TOKEN_DECIMALS = 6; var OCT_DECIMALS = 6; @@ -440,4 +440,18 @@ $('pin-input').addEventListener('keydown', function(e) { if (e.key === 'Enter') doUnlock(); }); -checkWallet(); \ No newline at end of file +checkWallet(); + +(function() { + var actions = { doUnlock, setDir, onInputChange, setMax, doSwap, cancelSwap, confirmSwap }; + function run(e, attr) { + var el = e.target.closest('[' + attr + ']'); + if (!el) return; + var fn = actions[el.getAttribute(attr)]; + if (!fn) return; + if (el.getAttribute('data-prevent') === '1') e.preventDefault(); + fn(el.getAttribute('data-arg'), el, e); + } + document.addEventListener('click', function(e) { run(e, 'data-action'); }); + document.addEventListener('input', function(e) { run(e, 'data-input'); }); +})(); \ No newline at end of file diff --git a/static/wallet.js b/static/wallet.js index 2c5c325..9feb8e8 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -699,8 +699,8 @@ function ideRenderProjectBar() { } bar.style.display = 'flex'; bar.innerHTML = '' + escapeHtml(_ideProject.name) + '' + - '' + - ''; + '' + + ''; } function ideRenderFileTree() { @@ -728,16 +728,16 @@ function ideRenderFileTree() { - var html = '
files ' + - '' + - '
'; + var html = '
files ' + + '' + + '
'; rootFiles.forEach(function(p) { var cls = p === _ideActiveFile ? ' active' : ''; - html += '
' + + html += '
' + '' + escapeHtml(p) + '
'; }); Object.keys(dirs).sort().forEach(function(dir) { @@ -745,7 +745,7 @@ function ideRenderFileTree() { dirs[dir].sort().forEach(function(p) { var fname = p.substring(p.indexOf('/') + 1); var cls = p === _ideActiveFile ? ' active' : ''; - html += '
' + + html += '
' + '' + escapeHtml(fname) + '
'; }); }); @@ -764,9 +764,9 @@ function ideRenderTabs() { _ideOpenTabs.forEach(function(path) { var name = path.indexOf('/') >= 0 ? path.substring(path.lastIndexOf('/') + 1) : path; var cls = path === _ideActiveFile ? ' active' : ''; - html += '
' + + html += '
' + escapeHtml(name) + - '×' + + '×' + '
'; }); bar.innerHTML = html; @@ -890,13 +890,13 @@ async function showProjectPicker() { var html = '
projects
'; html += ''; html += '
'; - html += ''; - html += ''; - html += ''; + html += ''; + html += ''; + html += ''; html += '
'; html += '
'; - html += ''; - html += ''; + html += ''; + html += ''; html += '
'; @@ -904,10 +904,10 @@ async function showProjectPicker() { html += '
'; html += '
recent
'; projects.forEach(function(p) { - html += '
' + + html += '
' + '' + escapeHtml(p.name) + '' + '' + new Date(p.created).toLocaleDateString() + '' + - '' + + '' + '
'; }); html += '
'; @@ -1233,8 +1233,8 @@ async function api(method, path, body) { var text = await res.text(); if (!text || text.length === 0) throw new Error('empty response from RPC (possible timeout)'); var j; - try { j = JSON.parse(text); } catch (e) { throw new Error('invalid server response: ' + text.substring(0, 200)); } - if (!res.ok) throw new Error(j.error || j.message || 'request failed'); + try { j = JSON.parse(text); } catch (e) { throw new Error('invalid server response: ' + escapeHtml(text.substring(0, 200))); } + if (!res.ok) throw new Error(escapeHtml(j.error || j.message || 'request failed')); return j; } @@ -1417,7 +1417,7 @@ function addrLink(addr) { function txLink(hash) { if (!hash) return '-'; if (!/^[a-f0-9]{64}$/.test(hash)) return '' + escapeHtml(hash) + ''; - return '' + short(hash) + ''; + return '' + short(hash) + ''; } function opTag(op) { @@ -1457,7 +1457,7 @@ function validAddr(addr) { function logStealth(msg, cls) { var el = $('stealth-log'); if (!el) { - var btn = document.querySelector('button[onclick="doStealthSend()"]'); + var btn = document.querySelector('button[data-action="doStealthSend"]'); if (!btn) return; var row = btn.closest('.action-row') || btn.parentNode; el = document.createElement('div'); @@ -1476,7 +1476,7 @@ function clearStealthLog() { function logDecrypt(msg, cls) { var el = $('decrypt-log'); if (!el) { - var btn = document.querySelector('button[onclick="doDecrypt()"]'); + var btn = document.querySelector('button[data-action="doDecrypt"]'); if (!btn) return; var row = btn.closest('.action-row') || btn.parentNode; el = document.createElement('div'); @@ -1529,7 +1529,7 @@ function txRow(tx) { h += '
'; h += ''; h += ''; - h += ''; + h += ''; h += ''; h += ''; h += ''; @@ -1544,7 +1544,7 @@ function txCardHtml(tx) { c += '
tx' + txLink(tx.hash) + '
'; c += '
from' + addrLink(tx.from) + '
'; c += '
to' + addrLink(toAddr) + '
'; - c += '
amount' + a.amt + '
'; + c += '
amount' + escapeHtml(a.amt) + '
'; c += '
status' + txStatusTag(st) + '
'; c += '
time' + fmtDate(tx.timestamp) + '
'; c += ''; @@ -1563,8 +1563,8 @@ async function showTx(hash) { var fullHash = res.hash || hash; - var explorerLink = _explorerUrl + '/tx.html?hash=' + fullHash; - h += ''; + var explorerLink = _explorerUrl + '/tx.html?hash=' + encodeURIComponent(fullHash); + h += ''; h += ''; if (res.reject_reason) h += ''; h += ''; @@ -1580,8 +1580,8 @@ async function showTx(hash) { if (res.ou) h += ''; h += ''; - if (res.signature) h += ''; - if (res.public_key) h += ''; + if (res.signature) h += ''; + if (res.public_key) h += ''; h += '
hashfromtoamountstatustime
' + txLink(tx.hash) + '' + addrLink(tx.from) + '' + addrLink(toAddr) + '' + a.amt + '' + escapeHtml(a.amt) + '' + txStatusTag(st) + '' + fmtDate(tx.timestamp) + '
hash' + fullHash + ' explorer
hash' + escapeHtml(fullHash) + ' explorer
status' + txStatusTag(st) + '
reason' + escapeHtml(res.reject_reason) + '
from' + addrLink(res.from || '') + '
ou (fee)' + fmtOct(res.ou) + '
time' + fmtDate(res.timestamp) + '
signature' + res.signature + '
public key' + res.public_key + '
signature' + escapeHtml(res.signature) + '
public key' + escapeHtml(res.public_key) + '
'; if (res.message && res.message !== 'null' && res.message !== '') { h += '
message
'; @@ -1589,7 +1589,7 @@ async function showTx(hash) { } $('tx-detail').innerHTML = h; } catch (e) { - $('tx-detail').innerHTML = '
' + e.message + '
'; + $('tx-detail').innerHTML = '
' + escapeHtml(e.message) + '
'; } } @@ -1599,6 +1599,14 @@ function escapeHtml(s) { return d.innerHTML; } +function escapeAttr(s) { + return String(s == null ? '' : s) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + function dashTxLimit() { var h = window.innerHeight; var overhead = 280; @@ -1618,7 +1626,7 @@ function renderDashTxs(txs) { h += ''; cards += ''; $('dash-txs').innerHTML = h + cards; - $('dash-more').innerHTML = ''; + $('dash-more').innerHTML = ''; } async function loadDashboard() { @@ -1669,7 +1677,9 @@ async function doSend() { if (!amount || isNaN(parseFloat(amount)) || parseFloat(amount) <= 0) { showResult('send-result', false, 'invalid amount'); return; } if (!validateFee('send-fee', 'standard')) { feeError('send-result', 'send-fee', 'standard'); return; } try { - var body = { to: to, amount: amount }; + var pin = await modalPrompt('confirm send', 'enter PIN to send ' + amount + ' oct to ' + to, { pin: true, btnText: 'send' }); + if (!pin) { showResult('send-result', false, 'send cancelled'); return; } + var body = { to: to, amount: amount, pin: pin }; if (msg) body.message = msg; var fee = $('send-fee') ? $('send-fee').value.trim() : ''; if (fee) body.ou = fee; @@ -1711,10 +1721,12 @@ async function doKeySwitch() { $('modal-overlay').style.display = 'none'; }; $('ks-confirm').onclick = async function() { + var pin = await modalPrompt('confirm key switch', 'enter PIN to switch encryption key', { pin: true, btnText: 'switch' }); + if (!pin) return; $('ks-confirm').disabled = true; $('ks-confirm').textContent = 'submitting...'; try { - var res = await api('POST', '/key_switch', {}); + var res = await api('POST', '/key_switch', { pin: pin }); var txHash = res.hash || res.tx_hash || ''; invalidateCurrentAddressState(); var h2 = '
key switch submitted
'; @@ -1723,7 +1735,7 @@ async function doKeySwitch() { $('modal-result').innerHTML = h2; $('ks-close').onclick = function() { $('modal-overlay').style.display = 'none'; fetchBalance(); }; } catch (e) { - $('modal-result').innerHTML = '
' + e.message + '
'; + $('modal-result').innerHTML = '
' + escapeHtml(e.message) + '
'; } }; } @@ -1734,7 +1746,9 @@ async function doEncrypt() { if (!amount || !/^\d+(\.\d{1,6})?$/.test(amount) || parseFloat(amount) <= 0) { showResult('enc-result', false, 'invalid amount'); return; } if (!validateFee('enc-fee', 'encrypt')) { feeError('enc-result', 'enc-fee', 'encrypt'); return; } try { - var encBody = { amount: amount }; + var pin = await modalPrompt('confirm encrypt', 'enter PIN to encrypt ' + amount + ' oct', { pin: true, btnText: 'encrypt' }); + if (!pin) { showResult('enc-result', false, 'encrypt cancelled'); return; } + var encBody = { amount: amount, pin: pin }; var encFee = $('enc-fee') ? $('enc-fee').value.trim() : ''; if (encFee) encBody.ou = encFee; var res = await api('POST', '/encrypt', encBody); @@ -1762,13 +1776,15 @@ async function doDecrypt() { logDecrypt('amount: ' + amount + ' oct', 'log-info'); logDecrypt('', ''); try { - var decBody = { amount: amount }; + var pin = await modalPrompt('confirm decrypt', 'enter PIN to decrypt ' + amount + ' oct', { pin: true, btnText: 'decrypt' }); + if (!pin) { logDecrypt('decrypt cancelled', 'log-err'); return; } + var decBody = { amount: amount, pin: pin }; var decFee = $('dec-fee') ? $('dec-fee').value.trim() : ''; if (decFee) decBody.ou = decFee; var res = await api('POST', '/decrypt', decBody); invalidateCurrentAddressState(); if (res.steps) { - for (var i = 0; i < res.steps.length; i++) logDecrypt(res.steps[i], 'log-info'); + for (var i = 0; i < res.steps.length; i++) logDecrypt(escapeHtml(res.steps[i]), 'log-info'); } logDecrypt('', ''); logDecrypt('decrypt complete', 'log-ok'); @@ -1806,13 +1822,15 @@ async function doStealthSend() { logStealth('amount: ' + amount + ' oct', 'log-info'); logStealth('', ''); try { - var stBody = { to: to, amount: amount }; + var pin = await modalPrompt('confirm stealth send', 'enter PIN to send ' + amount + ' oct to ' + to, { pin: true, btnText: 'send' }); + if (!pin) { logStealth('stealth send cancelled', 'log-err'); return; } + var stBody = { to: to, amount: amount, pin: pin }; var stFee = $('stealth-fee') ? $('stealth-fee').value.trim() : ''; if (stFee) stBody.ou = stFee; var res = await api('POST', '/stealth/send', stBody); invalidateCurrentAddressState(); if (res.steps) { - for (var i = 0; i < res.steps.length; i++) logStealth(res.steps[i], 'log-info'); + for (var i = 0; i < res.steps.length; i++) logStealth(escapeHtml(res.steps[i]), 'log-info'); } logStealth('', ''); logStealth('stealth send complete', 'log-ok'); @@ -1871,11 +1889,11 @@ async function doStealthScan() { } updateStealthBadge(unclaimed); if (unclaimed > 0) { - h += '
'; + h += '
'; } $('stealth-outputs').innerHTML = h; } catch (e) { - $('stealth-outputs').innerHTML = '
' + e.message + '
'; + $('stealth-outputs').innerHTML = '
' + escapeHtml(e.message) + '
'; } } @@ -1897,7 +1915,7 @@ async function doStealthClaim(ids) { if (res.results) { for (var i = 0; i < res.results.length; i++) { var r = res.results[i]; - logStealth(r.id + ': ' + (r.ok ? 'ok' : 'failed - ' + (r.error || '')), r.ok ? 'log-ok' : 'log-err'); + logStealth(escapeHtml(r.id) + ': ' + (r.ok ? 'ok' : 'failed - ' + escapeHtml(r.error || '')), r.ok ? 'log-ok' : 'log-err'); if (r.ok) _pendingClaimIds[String(r.id)] = true; } } @@ -2629,7 +2647,7 @@ async function loadTokens() { _tokensLoaded = true; hydrateTokenMaps(_tokens); } catch (e) { - if (!restored) $('tok-list').innerHTML = '
' + e.message + '
'; + if (!restored) $('tok-list').innerHTML = '
' + escapeHtml(e.message) + '
'; loadTokenTxs(); return; } @@ -2697,7 +2715,7 @@ function renderTokenList() { h += '' + short(t.address) + ''; h += ''; h += '
'; - h += ''; + h += ''; h += '
'; h += ''; } @@ -2748,7 +2766,9 @@ async function doTokenTransfer() { if (!rawAmount) { showResult('tok-transfer-result', false, 'invalid amount'); return; } if (!validateFee('tok-fee', 'call')) { feeError('tok-transfer-result', 'tok-fee', 'call'); return; } try { - var tokBody = { token: _selectedToken.address, to: to, amount: rawAmount }; + var pin = await modalPrompt('confirm token transfer', 'enter PIN to transfer ' + humanAmt + ' to ' + to, { pin: true, btnText: 'transfer' }); + if (!pin) { showResult('tok-transfer-result', false, 'transfer cancelled'); return; } + var tokBody = { token: _selectedToken.address, to: to, amount: rawAmount, pin: pin }; var tokFee = $('tok-fee') ? $('tok-fee').value.trim() : ''; if (tokFee) tokBody.ou = tokFee; var res = await api('POST', '/token/transfer', tokBody); @@ -2792,7 +2812,7 @@ async function loadHistory() { } else { renderHistoryTxs(cachedTxs); if (cached.response.has_more) { - $('history-more').innerHTML = ''; + $('history-more').innerHTML = ''; } fetchMissingSymbols(cachedTxs).then(function() { renderHistoryTxs(cachedTxs); }); } @@ -2808,12 +2828,12 @@ async function loadHistory() { } renderHistoryTxs(txs); if (res.has_more) { - $('history-more').innerHTML = ''; + $('history-more').innerHTML = ''; } fetchMissingSymbols(txs).then(function() { renderHistoryTxs(txs); }); } catch (e) { $('hist-count').textContent = '0'; - $('history-list').innerHTML = '
' + e.message + '
'; + $('history-list').innerHTML = '
' + escapeHtml(e.message) + '
'; } } @@ -2862,12 +2882,12 @@ async function loadHistoryAppend() { }); $('hist-count').textContent = String(_historyOffset + txs.length); if (res.has_more) { - $('history-more').innerHTML = ''; + $('history-more').innerHTML = ''; } else { $('history-more').innerHTML = ''; } } catch (e) { - $('history-more').innerHTML = '
' + e.message + '
'; + $('history-more').innerHTML = '
' + escapeHtml(e.message) + '
'; } } @@ -2877,14 +2897,14 @@ async function showKeys() { var res = await api('GET', '/keys'); var h = ''; h += ''; - h += ''; + h += ''; h += ''; - h += ''; - h += ''; + h += ''; + h += ''; h += '
address' + (res.address || '') + '
public key' + (res.public_key || '') + '
public key' + escapeHtml(res.public_key || '') + '
view pubkey' + (res.view_pubkey || '-') + '
private key****** (click to reveal)
seed phrase' + (res.has_master_seed ? '****** (click to reveal)' : 'not set - imported via private key only') + '
private key****** (click to reveal)
seed phrase' + (res.has_master_seed ? '****** (click to reveal)' : 'not set - imported via private key only') + '
'; $('keys-table').innerHTML = h; } catch (e) { - $('keys-table').innerHTML = '
' + e.message + '
'; + $('keys-table').innerHTML = '
' + escapeHtml(e.message) + '
'; } } @@ -2937,8 +2957,7 @@ async function loadAccountList() { el.innerHTML = '
no accounts
'; return; } - var btnStyle = 'display:inline-block;width:96px;padding:8px;margin:0;background:#E5E9EF;border:none;border-top:1px solid #D0D7E2;border-bottom:1px solid #D0D7E2;margin-right:4px;color:#3B567F;font-family:Tahoma,arial,sans-serif;font-size:11px;font-weight:bold;letter-spacing:1px;cursor:pointer;text-align:center;text-transform:lowercase'; - var btnHover = 'onmouseenter="this.style.background=\'#D0D7E2\'" onmouseleave="this.style.background=\'#E5E9EF\'"'; + var btnStyle = 'display:inline-block;width:96px;padding:8px;margin:0;border:none;border-top:1px solid #D0D7E2;border-bottom:1px solid #D0D7E2;margin-right:4px;color:#3B567F;font-family:Tahoma,arial,sans-serif;font-size:11px;font-weight:bold;letter-spacing:1px;cursor:pointer;text-align:center;text-transform:lowercase'; var html = ''; html += ''; html += ''; @@ -2957,17 +2976,16 @@ async function loadAccountList() { } } var name = a.name || 'unnamed'; - var escapedName = name.replace(/'/g, "\\'"); html += ''; - html += ''; - html += ''; + html += ''; + html += ''; html += ''; } html += '
' + badge + '' + name + '' + hdLabel + '' + a.addr + '' + badge + '' + escapeHtml(name) + '' + hdLabel + '' + escapeHtml(a.addr) + ''; if (!a.active) { - html += ''; + html += ''; } else { - html += ''; + html += ''; } - html += ''; + html += ''; html += '
'; @@ -2977,10 +2995,10 @@ async function loadAccountList() { var ah = '
'; if (resp.has_master_seed) { var idx = resp.next_hd_index || 0; - ah += ''; + ah += ''; ah += 'or'; } - ah += ''; + ah += ''; ah += '
'; actEl.innerHTML = ah; } @@ -3141,7 +3159,9 @@ async function doSaveSettings() { var bridgeSigner = $('settings-bridge-signer').value.trim(); if (!rpc) { showResult('settings-result', false, 'rpc url required'); return; } try { - var resp = await api('POST', '/settings', { rpc_url: rpc, explorer_url: explorer, bridge_signer_url: bridgeSigner }); + var pin = await modalPrompt('confirm settings change', 'enter PIN to change network endpoints', { pin: true, btnText: 'save' }); + if (!pin) { showResult('settings-result', false, 'settings change cancelled'); return; } + var resp = await api('POST', '/settings', { rpc_url: rpc, explorer_url: explorer, bridge_signer_url: bridgeSigner, pin: pin }); if (explorer) _explorerUrl = explorer.replace(/\/+$/, ''); try { _rpcHost = new URL(rpc).hostname; } catch(e) { _rpcHost = rpc; } if (resp && resp.cache_cleared) { @@ -3502,17 +3522,16 @@ function showAccountPicker(wallets) { : a.file; var hdTag = a.hd ? ' | hd' : ''; var dataAttr = hasAddr - ? 'data-addr="' + a.addr + '"' - : 'data-file="' + a.file + '"'; - html += ''; html += '
'; - html += '+ import or create new wallet'; + html += '+ import or create new wallet'; html += '
'; $('modal-result').innerHTML = html; $('modal-overlay').style.display = 'flex'; @@ -3575,5 +3594,44 @@ $('modal-pin-confirm').addEventListener('keydown', function(e) { if (e.key === 'Enter') modalFinishSetup(); }); +function wireDelegation() { + var actions = { + modalShowImport, modalCreate, modalDoImport, modalBack, modalMnemonicDone, + modalUnlock, modalBackFromPin, modalFinishSetup, doLogout, doKeySwitch, + doSend, doEncrypt, doDecrypt, doStealthSend, doStealthScan, doTokenTransfer, + closeTokenTransfer, onLangChange, editorUpdateWithLiveCompile, doCompile, + loadTemplate, doPreviewDeploy, doDeploy, doContractCall, doContractView, + doFheEncrypt, doFheDecrypt, doContractInfo, doContractReceipt, doVerifyContract, + doSaveSettings, doChangePin, goBack, switchView, switchImportTab, switchBottomTab, + ideCloseProject, ideExportZip, ideNewFile, ideOpenFile, ideCloseTab, ideNewProject, + ideLoadProject, ideDeleteProject, showTx, claimSelected, openTokenTransfer, + loadMoreHistory, revealPrivateKeys, doSwitchAccount, doChangePinForWallet, + doDeriveAccount, showImportAnother, showImportOptions, + navTo: function(a) { window.location.href = a; }, + openTab: function(a) { window.open(a, '_blank'); }, + selectSelf: function(a, el) { el.select(); }, + importFiles: function(a, el) { ideImportFiles(el.files); }, + fileMenu: function(a, el, e) { ideFileMenu(e, a); }, + renameAccount: function(a, el) { doRenameAccount(el.getAttribute('data-arg'), el.getAttribute('data-name')); }, + pickWallet: function(a, el) { pickWallet(el); } + }; + function run(e, attr) { + var el = e.target.closest('[' + attr + ']'); + if (!el) return; + var fn = actions[el.getAttribute(attr)]; + if (!fn) return; + if (el.getAttribute('data-prevent') === '1') e.preventDefault(); + fn(el.getAttribute('data-arg'), el, e); + } + document.addEventListener('click', function(e) { run(e, 'data-action'); }); + document.addEventListener('change', function(e) { run(e, 'data-change'); }); + document.addEventListener('input', function(e) { run(e, 'data-input'); }); + document.addEventListener('contextmenu', function(e) { run(e, 'data-context'); }); + document.addEventListener('submit', function(e) { if (e.target.closest('[data-nosubmit]')) e.preventDefault(); }); + var ed = $('ct-source'); + if (ed) ed.addEventListener('scroll', editorSync); +} + +wireDelegation(); initEditor(); init(); \ No newline at end of file From d1e4589717de07507c82574b133217127a230687 Mon Sep 17 00:00:00 2001 From: "d.a." <117524908+lambda0xE@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:40:40 +0000 Subject: [PATCH 12/13] bridge fix --- main.cpp | 1 - static/bridge.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/main.cpp b/main.cpp index fd59162..c107f24 100644 --- a/main.cpp +++ b/main.cpp @@ -3061,7 +3061,6 @@ int main(int argc, char** argv) { res.set_content(err_json("address and method required").dump(), "application/json"); return; } - if (!wallet_pin_ok(body, res)) return; std::string params_str = "[]"; if (body.contains("params")) params_str = body["params"].dump(); std::string amount_str = body.value("amount", "0"); diff --git a/static/bridge.html b/static/bridge.html index 8488b33..e222c86 100644 --- a/static/bridge.html +++ b/static/bridge.html @@ -3,7 +3,7 @@ - + octra bridge