Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,41 @@ open `http://127.0.0.1:8420` in your browser

0. after opening the web interface in your browser, import your private key or create a new one directly in the modal window
1. enter a 6 digit PIN code to encrypt (AES 256 GCM) your wallet
2. your wallet file is stored in `data/wallet.oct`
2. encrypted wallet files are stored under `data/`
3. the PIN is required on every startup to unlock

The current client also supports multiple encrypted wallet accounts under
`data/` with a manifest at `data/accounts.json`. Legacy `wallet.json` files can
be imported/migrated through the startup flow.

## network settings

Open the web UI settings page to choose the active network before compiling,
deploying, viewing, or calling contracts.

Presets:

- `devnet`: RPC `http://165.227.225.79:8080`, explorer
`https://devnet.octrascan.io`
- `mainnet`: RPC `https://octra.network`, explorer `https://octrascan.io`
- `custom`: manually entered RPC, explorer, and bridge signer URL

The settings page posts to `/api/settings`, persists the selected RPC/explorer
inside the encrypted wallet file, updates the active RPC client, and clears the
transaction cache when the RPC changes.

Useful API endpoints for tooling:

- `GET /api/wallet`: current address, public key, RPC, explorer, and bridge
signer settings.
- `POST /api/wallet/derive-address`: derive the Octra address for a supplied
private key without switching accounts.
- `POST /api/settings`: update RPC, explorer, and bridge signer values.
- `POST /api/contract/compile-project`: compile an AppliedML project payload.
- `POST /api/contract/deploy`: deploy compiled contract bytecode.
- `POST /api/contract/call`: submit a state-changing contract call.
- `GET /api/contract/view`: run a contract view call.


we adhere to a policy of completely eliminating third-party software where possible, we have zero tolerance for vendor dependencies, we only included well-known libs and point implementations in the build, the rest was completely written from scratch by hand to avoid the use of third-party code for security reasons

Expand Down
124 changes: 108 additions & 16 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
#include <chrono>
#include <optional>
#include <unordered_map>
#include <unordered_set>
#ifdef _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
Expand Down Expand Up @@ -106,6 +107,8 @@ static std::mutex g_token_history_runtime_mtx;

static std::unordered_map<std::string, std::vector<uint8_t>> g_pk_cache;
static std::mutex g_pk_mtx;
static std::unordered_set<std::string> g_bg_pvac_checked;
static std::mutex g_bg_pvac_mtx;

static std::optional<std::vector<uint8_t>> pk_cache_get(const std::string& addr) {
std::lock_guard<std::mutex> lk(g_pk_mtx);
Expand Down Expand Up @@ -389,17 +392,47 @@ static json submit_tx(const octra::Transaction& tx) {
return res;
}

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);
static bool rpc_lookup_failed_transiently(const std::string& error) {
return error.find("connection failed") != std::string::npos
|| error.find("parse error") != std::string::npos
|| error.find("non-json") != std::string::npos;
}

static bool has_registered_view_pubkey(const octra::RpcResult& vr) {
if (vr.ok && vr.result.is_object() && vr.result.contains("view_pubkey")
&& !vr.result["view_pubkey"].is_null() && vr.result["view_pubkey"].is_string())
return;
&& !vr.result["view_pubkey"].is_null() && vr.result["view_pubkey"].is_string()
&& !vr.result["view_pubkey"].get<std::string>().empty()) {
return true;
}
return false;
}

static bool ensure_pubkey_registered_on(octra::RpcClient& rpc,
const std::string& addr,
const uint8_t sk[64],
const std::string& pub_b64,
const char* prefix = "") {
auto vr = rpc.get_view_pubkey(addr);
if (has_registered_view_pubkey(vr)) return true;
if (!vr.ok && rpc_lookup_failed_transiently(vr.error)) {
fprintf(stderr, "%spubkey lookup failed for %s: %s; skipping register\n",
prefix, addr.c_str(), vr.error.c_str());
return false;
}
std::string msg = "register_pubkey:" + addr;
std::string sig = octra::ed25519_sign_detached(
reinterpret_cast<const uint8_t*>(msg.data()), msg.size(), sk);
auto rr = g_rpc.register_public_key(addr, pub_b64, sig);
if (rr.ok) fprintf(stderr, "pubkey registered for %s\n", addr.c_str());
else fprintf(stderr, "pubkey register failed for %s: %s\n", addr.c_str(), rr.error.c_str());
auto rr = rpc.register_public_key(addr, pub_b64, sig);
if (rr.ok) {
fprintf(stderr, "%spubkey registered for %s\n", prefix, addr.c_str());
return true;
}
fprintf(stderr, "%spubkey register failed for %s: %s\n", prefix, addr.c_str(), rr.error.c_str());
return false;
}

static void ensure_pubkey_registered(const std::string& addr, const uint8_t sk[64], const std::string& pub_b64) {
ensure_pubkey_registered_on(g_rpc, addr, sk, pub_b64);
}

static bool g_pvac_foreign = false;
Expand Down Expand Up @@ -431,6 +464,10 @@ static void ensure_pvac_registered() {
g_wallet.addr.c_str());
return;
}
if (!pr.ok && rpc_lookup_failed_transiently(pr.error)) {
fprintf(stderr, "pvac pubkey lookup failed: %s; skipping register\n", pr.error.c_str());
return;
}
auto pk_raw = g_pvac.serialize_pubkey();
std::string pk_blob(pk_raw.begin(), pk_raw.end());
std::string pk_b64 = g_pvac.serialize_pubkey_b64();
Expand Down Expand Up @@ -867,6 +904,33 @@ int main(int argc, char** argv) {
}
});

svr.Post("/api/wallet/derive-address", [](const httplib::Request& req, httplib::Response& res) {
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 priv = body.value("priv", "");
if (priv.empty()) {
res.status = 400;
res.set_content(err_json("priv required").dump(), "application/json");
return;
}
try {
auto w = octra::wallet_from_private_key(priv);
json j;
j["address"] = w.addr;
j["public_key"] = w.pub_b64;
octra::secure_zero(w.sk, 64);
octra::secure_zero(w.pk, 32);
res.set_content(j.dump(), "application/json");
} catch (const std::exception& e) {
res.status = 400;
res.set_content(err_json(e.what()).dump(), "application/json");
}
});

svr.Get("/api/wallet", [](const httplib::Request&, httplib::Response& res) {
WALLET_GUARD
json j;
Expand Down Expand Up @@ -2281,7 +2345,7 @@ int main(int argc, char** argv) {
tx.to_ = contract_addr;
tx.amount = "0";
tx.nonce = nonce + 1;
tx.ou = parse_ou(body, "50000000");
tx.ou = parse_ou(body, "200000");
tx.timestamp = now_ts();
tx.op_type = "deploy";
tx.encrypted_data = bytecode;
Expand Down Expand Up @@ -2993,26 +3057,54 @@ int main(int argc, char** argv) {
auto entries = octra::load_manifest();
for (auto& e : entries) {
if (e.addr.empty() || e.file.empty()) continue;
auto ar = g_rpc.get_account(e.addr);
if (!ar.ok) continue;
try {
auto w = octra::load_wallet_encrypted(e.file, g_pin);
ensure_pubkey_registered(w.addr, w.sk, w.pub_b64);
auto pr = g_rpc.get_pvac_pubkey(e.addr);
std::string cache_key = w.rpc_url + "|" + w.addr;
{
std::lock_guard<std::mutex> lk(g_bg_pvac_mtx);
if (g_bg_pvac_checked.count(cache_key)) {
octra::secure_zero(w.sk, 64);
continue;
}
}

octra::RpcClient rpc(w.rpc_url);
auto ar = rpc.get_account(w.addr);
if (!ar.ok) {
octra::secure_zero(w.sk, 64);
continue;
}

ensure_pubkey_registered_on(rpc, w.addr, w.sk, w.pub_b64, "[bg] ");
auto pr = rpc.get_pvac_pubkey(w.addr);
bool pvac_ok = pr.ok && pr.result.is_object() && !pr.result["pvac_pubkey"].is_null()
&& pr.result["pvac_pubkey"].is_string() && !pr.result["pvac_pubkey"].get<std::string>().empty();
if (!pvac_ok) {
if (pvac_ok) {
std::lock_guard<std::mutex> lk(g_bg_pvac_mtx);
g_bg_pvac_checked.insert(cache_key);
} else if (pr.ok || !rpc_lookup_failed_transiently(pr.error)) {
octra::PvacBridge tmp_pvac;
if (tmp_pvac.init(w.priv_b64)) {
auto pk_raw = tmp_pvac.serialize_pubkey();
std::string pk_blob(pk_raw.begin(), pk_raw.end());
std::string pk_b64 = tmp_pvac.serialize_pubkey_b64();
std::string reg_sig = octra::sign_register_request(w.addr, pk_blob, w.sk);
std::string kat = compute_aes_kat_hex();
auto rr = g_rpc.register_pvac_pubkey(w.addr, pk_b64, reg_sig, w.pub_b64, kat);
if (rr.ok) fprintf(stderr, "[bg] pvac registered %s\n", w.addr.c_str());
else fprintf(stderr, "[bg] pvac failed %s: %s\n", w.addr.c_str(), rr.error.c_str());
auto rr = rpc.register_pvac_pubkey(w.addr, pk_b64, reg_sig, w.pub_b64, kat);
if (rr.ok) {
fprintf(stderr, "[bg] pvac registered %s\n", w.addr.c_str());
std::lock_guard<std::mutex> lk(g_bg_pvac_mtx);
g_bg_pvac_checked.insert(cache_key);
} else if (rr.error.find("already registered") != std::string::npos) {
std::lock_guard<std::mutex> lk(g_bg_pvac_mtx);
g_bg_pvac_checked.insert(cache_key);
} else {
fprintf(stderr, "[bg] pvac failed %s: %s\n", w.addr.c_str(), rr.error.c_str());
}
}
} else {
fprintf(stderr, "[bg] pvac lookup failed %s: %s; skipping register\n",
w.addr.c_str(), pr.error.c_str());
}
octra::secure_zero(w.sk, 64);
} catch (...) {}
Expand Down
30 changes: 25 additions & 5 deletions rpc_client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include <vector>
#include <atomic>
#include <memory>
#include <algorithm>
#include <cctype>
#include "lib/json.hpp"

#include "lib/httplib.h"
Expand Down Expand Up @@ -97,14 +99,14 @@ class RpcClient {
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);
return parse_response(res->body, res->status, res->get_header_value("Content-Type"));
} 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);
return parse_response(res->body, res->status, res->get_header_value("Content-Type"));
}
}

Expand Down Expand Up @@ -307,7 +309,18 @@ class RpcClient {
}

private:
RpcResult parse_response(const std::string& body) {
static std::string body_prefix(const std::string& body) {
std::string out = body.substr(0, std::min<size_t>(body.size(), 160));
for (char& c : out) {
unsigned char uc = static_cast<unsigned char>(c);
if (std::iscntrl(uc) || std::isspace(uc)) c = ' ';
}
return out;
}

RpcResult parse_response(const std::string& body,
int status = 0,
const std::string& content_type = "") {
try {
auto j = nlohmann::json::parse(body);
if (j.contains("result"))
Expand All @@ -319,9 +332,16 @@ class RpcClient {
}
return {false, {}, "unknown rpc response"};
} catch (const std::exception& ex) {
return {false, {}, std::string("parse error: ") + ex.what()};
return {
false,
{},
std::string("parse error: ") + ex.what()
+ " status=" + std::to_string(status)
+ " content_type=" + content_type
+ " body_prefix=" + body_prefix(body)
};
}
}
};

}
} // namespace octra
11 changes: 10 additions & 1 deletion static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,15 @@
<div class="view" id="view-settings">
<div class="section-title">network settings</div>
<div class="form-area">
<div class="form-row">
<label>network preset</label>
<select id="settings-network" onchange="onNetworkPresetChange()">
<option value="devnet">devnet</option>
<option value="mainnet">mainnet</option>
<option value="custom">custom</option>
</select>
<div class="settings-note" id="settings-network-note">choose a preset, then save before compile/deploy/call</div>
</div>
<div class="form-row">
<label>rpc url</label>
<input type="text" id="settings-rpc" placeholder="http://165.227.225.79:8080">
Expand Down Expand Up @@ -551,4 +560,4 @@
</div>
<script src="wallet.js?v=14"></script>
</body>
</html>
</html>
14 changes: 13 additions & 1 deletion static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,18 @@ td {
min-height: 60px;
}

.settings-note {
margin-top: 3px;
color: #8C9DB6;
font-size: 10px;
line-height: 150%;
}

.form-row input.network-locked {
background: #F6F7F9;
color: #516E9A;
}

.action-btn {
display: block;
width: 100%;
Expand Down Expand Up @@ -1370,4 +1382,4 @@ td {
.modal-btn-primary {
background: #3B567F; color: #fff; border-color: #3B567F;
}
.modal-btn-primary:hover { background: #2C4060; }
.modal-btn-primary:hover { background: #2C4060; }
10 changes: 10 additions & 0 deletions static/templates/amm/interfaces/IOCS01.aml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
interface IOCS01 {
fn transfer(to: address, amount: int): bool
fn grant(spender: address, amount: int): bool
fn pull(from: address, to: address, amount: int): bool
fn balance_of(addr: address): int
fn allowance(owner: address, spender: address): int
fn get_name(): string
fn get_symbol(): string
fn get_total_supply(): int
}
Loading