From cbcf6fa84569a3f5c30f5d3b97abbeee3c92df12 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Fri, 1 May 2026 17:28:33 +0900 Subject: [PATCH 1/8] new AMM code w/ modularized invariant calculation --- static/templates/amm/interfaces/IOCS01.aml | 10 ++ static/templates/amm/main.aml | 185 +++++++++++++++++---- 2 files changed, 160 insertions(+), 35 deletions(-) create mode 100644 static/templates/amm/interfaces/IOCS01.aml diff --git a/static/templates/amm/interfaces/IOCS01.aml b/static/templates/amm/interfaces/IOCS01.aml new file mode 100644 index 0000000..bcb5c4e --- /dev/null +++ b/static/templates/amm/interfaces/IOCS01.aml @@ -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 +} diff --git a/static/templates/amm/main.aml b/static/templates/amm/main.aml index d84a95d..8c08a20 100644 --- a/static/templates/amm/main.aml +++ b/static/templates/amm/main.aml @@ -1,51 +1,166 @@ -contract SimpleAMM { +import IOCS01 from "interfaces/IOCS01.aml" + +contract AMM { state { - token_a: address - token_b: address - reserve_a: int - reserve_b: int + token_x: address + token_y: address + reserve_x: int + reserve_y: int total_lp: int lp_balances: map[address]int + deployer: address + fee_rate: int // in basis points, i.e., 100 = 1% } - event Swap(who: address, token_in: address, amount_in: int, amount_out: int) - event AddLiquidity(who: address, a: int, b: int, lp: int) + event Swap(from: address, to: address, is_buy: bool, amount_in: int, amount_out: int) + event AddLiquidity(from: address, to: address, amount: int) + event RemoveLiquidity(from: address, to: address, amount: int) - constructor(a: address, b: address) { - self.token_a = a - self.token_b = b - self.reserve_a = 0 - self.reserve_b = 0 - self.total_lp = 0 + constructor(token_x: address, token_y: address, fee_rate: int) { + require(fee_rate >= 0 && fee_rate <= 9999, "invalid fee rate") + self.deployer = caller + self.token_x = token_x + self.token_y = token_y + self.fee_rate = fee_rate } - fn add_liquidity(amount_a: int, amount_b: int): int { - require(amount_a > 0 && amount_b > 0, "amounts must be positive") - let lp = amount_a * amount_b - self.reserve_a += amount_a - self.reserve_b += amount_b - self.total_lp += lp - self.lp_balances[caller] += lp - emit AddLiquidity(caller, amount_a, amount_b, lp) - return lp + nonreentrant fn swap(to: address, is_buy: bool, amount_in: int, amount_out: int): bool { + // pool status check + require(self.total_lp > 0, "pool not initialized") + + // validate inputs + require(amount_in > 0, "amount in must be positive") + require(amount_out > 0, "amount out must be positive") + if is_buy { + require(amount_out < self.reserve_x, "too much amount out") + } else { + require(amount_out < self.reserve_y, "too much amount out") + } + + // fee calculation + let amount_in_after_fee = amount_in * (10000 - self.fee_rate) / 10000 + require(amount_in_after_fee > 0, "amount in after fee must be positive") + // invariant check + let invariant_before = _invariant(self.reserve_x, self.reserve_y) + let invariant_after = 0 + if is_buy { + invariant_after = _invariant(self.reserve_x - amount_out, self.reserve_y + amount_in_after_fee) + } else { + invariant_after = _invariant(self.reserve_x + amount_in_after_fee, self.reserve_y - amount_out) + } + require(invariant_after >= invariant_before, "invariant") + + // pull the funds + // if is_buy { + // require(IOCS01(self.token_y).pull(caller, address(this), amount_in), "pull token y failed") + // } else { + // require(IOCS01(self.token_x).pull(caller, address(this), amount_in), "pull token x failed") + // } + + // update storage + if is_buy { + self.reserve_x -= amount_out + self.reserve_y += amount_in + } else { + self.reserve_x += amount_in + self.reserve_y -= amount_out + } + + // push the funds + // if is_buy { + // require(IOCS01(self.token_x).transfer(to, amount_out), "push token x failed") + // } else { + // require(IOCS01(self.token_y).transfer(to, amount_out), "push token y failed") + // } + + emit Swap(caller, to, is_buy, amount_in, amount_out) + return true } - fn swap_a_for_b(amount_in: int): int { - require(amount_in > 0, "zero input") - let out = (amount_in * self.reserve_b) / (self.reserve_a + amount_in) - require(out > 0, "output too small") - self.reserve_a += amount_in - self.reserve_b -= out - emit Swap(caller, self.token_a, amount_in, out) - return out + + nonreentrant fn add_liquidity(recipient: address, amount_x: int, amount_y: int): int { + // for initial liquidity injection, only the deployer is allowed + if self.total_lp == 0 { + require(caller == self.deployer, "only deployer can add initial liquidity") + } + + // input validation + require(amount_x > 0 && amount_y > 0, "amounts invalid") + + // take fee from amounts to prevent bypassing the swap fee + let amount_x_after_fee = amount_x * (10000 - self.fee_rate) / 10000 + let amount_y_after_fee = amount_y * (10000 - self.fee_rate) / 10000 + + // calculate invariant increase + let invariant_before = _invariant(self.reserve_x, self.reserve_y) + let invariant_after = _invariant(self.reserve_x + amount_x_after_fee, self.reserve_y + amount_y_after_fee) + require(invariant_before < invariant_after, "invariant did not increase") + let amount_mint = invariant_after + if invariant_before > 0 { + amount_mint = self.total_lp * (invariant_after - invariant_before) / invariant_before + } + require(amount_mint > 0, "amounts too small") + + // pull the funds + // TODO: uncomment once external contract call syntax is confirmed + // require(IOCS01(self.token_x).pull(caller, address(this), amount_x), "pull token x failed") + // require(IOCS01(self.token_y).pull(caller, address(this), amount_y), "pull token y failed") + + // update storage + self.reserve_x += amount_x + self.reserve_y += amount_y + self.total_lp += amount_mint + self.lp_balances[recipient] += amount_mint + emit AddLiquidity(caller, recipient, amount_mint) + return amount_mint + } + + nonreentrant fn remove_liquidity(recipient: address, amount_remove: int): int { + // input validation + require(amount_remove > 0, "amount invalid") + require(self.total_lp > 0, "no liquidity") + require(self.lp_balances[caller] >= amount_remove, "insufficient liquidity") + + // calculate proportional token amounts + let amount_x = self.reserve_x * amount_remove / self.total_lp + let amount_y = self.reserve_y * amount_remove / self.total_lp + require(amount_x > 0 && amount_y > 0, "amounts too small") + + // update storage + self.reserve_x -= amount_x + self.reserve_y -= amount_y + self.total_lp -= amount_remove + self.lp_balances[caller] -= amount_remove + + // push the funds + // TODO: uncomment once external contract call syntax is confirmed + // require(IOCS01(self.token_x).transfer(recipient, amount_x), "push token x failed") + // require(IOCS01(self.token_y).transfer(recipient, amount_y), "push token y failed") + + emit RemoveLiquidity(caller, recipient, amount_remove) + return amount_remove } - view fn get_reserves(): (int, int) { - return (self.reserve_a, self.reserve_b) + private view fn _invariant(reserve_x: int, reserve_y: int): int { + require(reserve_x >= 0 && reserve_y >= 0, "invalid reserves") + let invariant = _isqrt(reserve_x * reserve_y) + return invariant } - view fn get_price(): int { - require(self.reserve_a > 0, "no liquidity") - return (self.reserve_b * 1000000) / self.reserve_a + private view fn _isqrt(n: int): int { + require(n >= 0, "only nonnegative numbers") + let z = 0 + if n > 3 { + z = n + let x = n / 2 + 1 + while x < z { + z = x + x = (n / x + x) / 2 + } + } + if n > 0 && n <= 3 { + z = 1 + } + return z } } From 1d8fafa6f90c12a3a3120b27475e58f6fdcfd0a0 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Mon, 4 May 2026 20:24:52 +0900 Subject: [PATCH 2/8] Add wallet network presets --- static/index.html | 11 ++++- static/style.css | 14 +++++- static/wallet.js | 117 +++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 128 insertions(+), 14 deletions(-) diff --git a/static/index.html b/static/index.html index ad7f0c1..a9654ea 100644 --- a/static/index.html +++ b/static/index.html @@ -494,6 +494,15 @@
network settings
+
+ + +
choose a preset, then save before compile/deploy/call
+
@@ -551,4 +560,4 @@
- \ No newline at end of file + diff --git a/static/style.css b/static/style.css index c02dd13..664e5e6 100644 --- a/static/style.css +++ b/static/style.css @@ -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%; @@ -1370,4 +1382,4 @@ 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; } diff --git a/static/wallet.js b/static/wallet.js index e346114..fd36a57 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -116,6 +116,7 @@ var _tokTxGen = 0; var _compiledAbi = null; var _fees = {}; var _rpcHost = ''; +var _networkPreset = 'custom'; var _hasMasterSeed = false; var _addressRuntime = {}; var _tokenMetaInflight = {}; @@ -461,6 +462,23 @@ function invalidateCurrentAddressState() { _tokensLoaded = false; } +var NETWORK_PRESETS = { + devent: { + label: 'devent', + rpc_url: 'http://165.227.225.79:8080', + explorer_url: 'https://devnet.octrascan.io', + bridge_signer_url: 'https://relayer-002838819188.octra.network', + aliases: ['http://165.227.225.79:8080'] + }, + mainnet: { + label: 'mainnet', + rpc_url: 'https://octra.network', + explorer_url: 'https://octrascan.io', + bridge_signer_url: 'https://relayer-002838819188.octra.network', + aliases: ['https://octra.network', 'http://46.101.86.250:8080'] + } +}; + var _ideProject = null; var _ideFiles = {}; @@ -1093,9 +1111,75 @@ async function doVerifyProject() { } } +function normalizeEndpoint(url) { + return String(url || '').trim().replace(/\/+$/, '').toLowerCase(); +} + +function presetMatches(preset, rpc, explorer) { + var cfg = NETWORK_PRESETS[preset]; + if (!cfg) return false; + var nrpc = normalizeEndpoint(rpc); + var nexp = normalizeEndpoint(explorer); + if (nexp && nexp === normalizeEndpoint(cfg.explorer_url)) return true; + for (var i = 0; i < cfg.aliases.length; i++) { + if (nrpc === normalizeEndpoint(cfg.aliases[i])) return true; + } + return nrpc === normalizeEndpoint(cfg.rpc_url); +} + +function detectNetworkPreset(rpc, explorer) { + if (presetMatches('devent', rpc, explorer)) return 'devent'; + if (presetMatches('mainnet', rpc, explorer)) return 'mainnet'; + return 'custom'; +} + +function setNetworkFieldLock(locked) { + ['settings-rpc', 'settings-explorer', 'settings-bridge-signer'].forEach(function(id) { + var el = $(id); + if (!el) return; + el.readOnly = locked; + if (locked) el.classList.add('network-locked'); + else el.classList.remove('network-locked'); + }); +} + +function applyNetworkPresetFields(preset) { + var cfg = NETWORK_PRESETS[preset]; + var note = $('settings-network-note'); + if (!cfg) { + setNetworkFieldLock(false); + if (note) note.textContent = 'custom RPC/explorer values will be saved'; + return; + } + if ($('settings-rpc')) $('settings-rpc').value = cfg.rpc_url; + if ($('settings-explorer')) $('settings-explorer').value = cfg.explorer_url; + if ($('settings-bridge-signer')) $('settings-bridge-signer').value = cfg.bridge_signer_url; + setNetworkFieldLock(true); + if (note) note.textContent = cfg.label + ' preset loaded - save before compile/deploy/call'; +} + +function onNetworkPresetChange() { + var sel = $('settings-network'); + _networkPreset = sel ? sel.value : 'custom'; + applyNetworkPresetFields(_networkPreset); +} + +function clearNetworkCaches() { + _cachedBal = null; + _historyOffset = 0; + _tokens = []; + _tokensLoaded = false; + _fees = {}; + _encryptedBalanceRaw = 0; + _unclaimedCount = 0; + _tokenSymbols = {}; + _tokenDecimals = {}; +} + function networkLabel(host) { - if (host === '46.101.86.250') return 'main net'; - if (host === '165.227.225.79') return 'dev net'; + if (_networkPreset !== 'custom') return _networkPreset; + if (host === 'octra.network' || host === '46.101.86.250') return 'mainnet'; + if (host === '165.227.225.79') return 'devent'; if (host === 'localhost' || host === '127.0.0.1') return 'local'; return host; } @@ -2797,6 +2881,16 @@ async function loadSettings() { $('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'; + _networkPreset = detectNetworkPreset($('settings-rpc').value, $('settings-explorer').value); + if ($('settings-network')) $('settings-network').value = _networkPreset; + if (_networkPreset === 'custom') setNetworkFieldLock(false); + else setNetworkFieldLock(true); + var note = $('settings-network-note'); + if (note) { + note.textContent = _networkPreset === 'custom' + ? 'custom RPC/explorer values will be saved' + : _networkPreset + ' active - save after changing presets'; + } } catch (e) {} loadAccountList(); } @@ -2982,26 +3076,23 @@ function showImportAnother() { async function doSaveSettings() { clearResult('settings-result'); + var preset = $('settings-network') ? $('settings-network').value : 'custom'; + if (NETWORK_PRESETS[preset]) applyNetworkPresetFields(preset); 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, bridge_signer_url: bridgeSigner }); + _networkPreset = detectNetworkPreset(resp.rpc_url || rpc, resp.explorer_url || explorer); + if ($('settings-network')) $('settings-network').value = _networkPreset; + setNetworkFieldLock(_networkPreset !== 'custom'); 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 = []; - _tokensLoaded = false; - _fees = {}; - _encryptedBalanceRaw = 0; - _unclaimedCount = 0; - _tokenSymbols = {}; - _tokenDecimals = {}; + clearNetworkCaches(); fetchBalance(); if (document.querySelector('.nav-tabs a.active[data-view="dashboard"]')) loadDashboard(); @@ -3273,6 +3364,7 @@ async function loadWalletInfo() { } 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; } + _networkPreset = detectNetworkPreset(w.rpc_url || '', w.explorer_url || ''); _hasMasterSeed = !!w.has_master_seed; $('hdr-addr').innerHTML = '' + _walletAddr + ''; $('hdr-logout').style.display = ''; @@ -3300,6 +3392,7 @@ async function doLogout() { _tokensLoaded = false; _tokenSymbols = {}; _tokenDecimals = {}; + _networkPreset = 'custom'; $('hdr-logout').style.display = 'none'; $('hdr-dev').style.display = 'none'; $('hdr-circles').style.display = 'none'; @@ -3418,4 +3511,4 @@ $('modal-pin-confirm').addEventListener('keydown', function(e) { }); initEditor(); -init(); \ No newline at end of file +init(); From 9b8b068fc76bd26c0a248acca584d104f596a897 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Mon, 4 May 2026 20:31:02 +0900 Subject: [PATCH 3/8] Rename wallet preset to devnet --- static/index.html | 2 +- static/wallet.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/index.html b/static/index.html index a9654ea..ac6d55d 100644 --- a/static/index.html +++ b/static/index.html @@ -497,7 +497,7 @@
diff --git a/static/wallet.js b/static/wallet.js index fd36a57..ceaf6e3 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -463,8 +463,8 @@ function invalidateCurrentAddressState() { } var NETWORK_PRESETS = { - devent: { - label: 'devent', + devnet: { + label: 'devnet', rpc_url: 'http://165.227.225.79:8080', explorer_url: 'https://devnet.octrascan.io', bridge_signer_url: 'https://relayer-002838819188.octra.network', @@ -1128,7 +1128,7 @@ function presetMatches(preset, rpc, explorer) { } function detectNetworkPreset(rpc, explorer) { - if (presetMatches('devent', rpc, explorer)) return 'devent'; + if (presetMatches('devnet', rpc, explorer)) return 'devnet'; if (presetMatches('mainnet', rpc, explorer)) return 'mainnet'; return 'custom'; } @@ -1179,7 +1179,7 @@ function clearNetworkCaches() { function networkLabel(host) { if (_networkPreset !== 'custom') return _networkPreset; if (host === 'octra.network' || host === '46.101.86.250') return 'mainnet'; - if (host === '165.227.225.79') return 'devent'; + if (host === '165.227.225.79') return 'devnet'; if (host === 'localhost' || host === '127.0.0.1') return 'local'; return host; } From 6b4f2daa797590a51309f3ab58150b9f192276e0 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Tue, 5 May 2026 08:13:53 +0900 Subject: [PATCH 4/8] Lower contract deploy fee fallback --- main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.cpp b/main.cpp index 4938ec7..6f078de 100644 --- a/main.cpp +++ b/main.cpp @@ -2281,7 +2281,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; From 83d2d6881f286344ef32233176070579530f8db6 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Tue, 5 May 2026 08:18:09 +0900 Subject: [PATCH 5/8] Expose private key address derivation --- main.cpp | 27 +++++++++++++++++++++++++++ wallet.hpp | 15 ++++++++++----- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/main.cpp b/main.cpp index 6f078de..22752ef 100644 --- a/main.cpp +++ b/main.cpp @@ -867,6 +867,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; diff --git a/wallet.hpp b/wallet.hpp index eb42b6e..cb666dc 100644 --- a/wallet.hpp +++ b/wallet.hpp @@ -430,9 +430,7 @@ inline Wallet import_wallet_mnemonic(const std::string& path, return w; } -inline Wallet import_wallet(const std::string& path, - const std::string& priv_b64_raw, - const std::string& pin) { +inline Wallet wallet_from_private_key(const std::string& priv_b64_raw) { std::string clean; for (char c : priv_b64_raw) { if (c != '\n' && c != '\r' && c != ' ' && c != '\t') @@ -454,12 +452,19 @@ inline Wallet import_wallet(const std::string& path, w.priv_b64 = base64_encode(w.sk, 32); w.pub_b64 = base64_encode(w.pk, 32); w.rpc_url = "http://46.101.86.250:8080"; - save_wallet_encrypted(path, w, pin); try_mlock(w.sk, 64); try_mlock(w.pk, 32); return w; } +inline Wallet import_wallet(const std::string& path, + const std::string& priv_b64_raw, + const std::string& pin) { + Wallet w = wallet_from_private_key(priv_b64_raw); + save_wallet_encrypted(path, w, pin); + return w; +} + inline void save_settings(const std::string& path, Wallet& w, const std::string& new_rpc, const std::string& pin) { @@ -557,4 +562,4 @@ inline std::vector scan_and_merge_oct_files() { return entries; } -} \ No newline at end of file +} From c2bca8f6cf6b34860db67f104f2a3f4ea8aae3b9 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Tue, 5 May 2026 08:40:24 +0900 Subject: [PATCH 6/8] Update AMM template and OCT formatting --- static/templates/amm/main.aml | 35 +++++++++++++++++------------------ static/wallet.js | 20 +++++++++++++++----- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/static/templates/amm/main.aml b/static/templates/amm/main.aml index 8c08a20..0293421 100644 --- a/static/templates/amm/main.aml +++ b/static/templates/amm/main.aml @@ -40,6 +40,7 @@ contract AMM { // fee calculation let amount_in_after_fee = amount_in * (10000 - self.fee_rate) / 10000 require(amount_in_after_fee > 0, "amount in after fee must be positive") + // invariant check let invariant_before = _invariant(self.reserve_x, self.reserve_y) let invariant_after = 0 @@ -51,11 +52,11 @@ contract AMM { require(invariant_after >= invariant_before, "invariant") // pull the funds - // if is_buy { - // require(IOCS01(self.token_y).pull(caller, address(this), amount_in), "pull token y failed") - // } else { - // require(IOCS01(self.token_x).pull(caller, address(this), amount_in), "pull token x failed") - // } + if is_buy { + require(call(self.token_y, "pull", caller, self_addr, amount_in), "pull token y failed") + } else { + require(call(self.token_x, "pull", caller, self_addr, amount_in), "pull token x failed") + } // update storage if is_buy { @@ -67,11 +68,11 @@ contract AMM { } // push the funds - // if is_buy { - // require(IOCS01(self.token_x).transfer(to, amount_out), "push token x failed") - // } else { - // require(IOCS01(self.token_y).transfer(to, amount_out), "push token y failed") - // } + if is_buy { + require(call(self.token_x, "transfer", to, amount_out), "push token x failed") + } else { + require(call(self.token_y, "transfer", to, amount_out), "push token y failed") + } emit Swap(caller, to, is_buy, amount_in, amount_out) return true @@ -102,9 +103,8 @@ contract AMM { require(amount_mint > 0, "amounts too small") // pull the funds - // TODO: uncomment once external contract call syntax is confirmed - // require(IOCS01(self.token_x).pull(caller, address(this), amount_x), "pull token x failed") - // require(IOCS01(self.token_y).pull(caller, address(this), amount_y), "pull token y failed") + require(call(self.token_x, "pull", caller, self_addr, amount_x), "pull token x failed") + require(call(self.token_y, "pull", caller, self_addr, amount_y), "pull token y failed") // update storage self.reserve_x += amount_x @@ -133,9 +133,8 @@ contract AMM { self.lp_balances[caller] -= amount_remove // push the funds - // TODO: uncomment once external contract call syntax is confirmed - // require(IOCS01(self.token_x).transfer(recipient, amount_x), "push token x failed") - // require(IOCS01(self.token_y).transfer(recipient, amount_y), "push token y failed") + require(call(self.token_x, "transfer", recipient, amount_x), "push token x failed") + require(call(self.token_y, "transfer", recipient, amount_y), "push token y failed") emit RemoveLiquidity(caller, recipient, amount_remove) return amount_remove @@ -152,10 +151,10 @@ contract AMM { let z = 0 if n > 3 { z = n - let x = n / 2 + 1 + let x = (n + 1) / 2 while x < z { z = x - x = (n / x + x) / 2 + x = (x + n / x) / 2 } } if n > 0 && n <= 3 { diff --git a/static/wallet.js b/static/wallet.js index ceaf6e3..8d8bed5 100644 --- a/static/wallet.js +++ b/static/wallet.js @@ -1396,11 +1396,21 @@ function addCommas(s) { } function fmtOct(raw) { - var v = parseFloat(raw); - if (v === 0 || isNaN(v)) return '0 oct'; - var n = v / 1000000; - var s = n.toFixed(6).replace(/\.?0+$/, ''); - return addCommas(s) + ' oct'; + var s = String(raw == null ? '0' : raw).trim(); + var neg = false; + if (s.charAt(0) === '-') { + neg = true; + s = s.slice(1); + } + s = s.replace(/[^0-9]/g, '').replace(/^0+/, ''); + if (!s) s = '0'; + var isZero = s === '0'; + while (s.length <= 6) s = '0' + s; + var intPart = s.slice(0, s.length - 6); + var fracPart = s.slice(s.length - 6); + if (!intPart) intPart = '0'; + var sign = (neg && !isZero) ? '-' : ''; + return sign + addCommas(intPart + '.' + fracPart) + ' oct'; } function formatUnits(rawStr, decimals) { From 05d27a77176ee48d36079353175d3662137fd450 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Thu, 7 May 2026 10:06:43 +0900 Subject: [PATCH 7/8] Tame PVAC registration retries --- main.cpp | 95 ++++++++++++++++++++++++++++++++++++++++++-------- rpc_client.hpp | 30 +++++++++++++--- 2 files changed, 105 insertions(+), 20 deletions(-) diff --git a/main.cpp b/main.cpp index 22752ef..36ad15c 100644 --- a/main.cpp +++ b/main.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #ifdef _WIN32 #define NOMINMAX #define WIN32_LEAN_AND_MEAN @@ -106,6 +107,8 @@ static std::mutex g_token_history_runtime_mtx; static std::unordered_map> g_pk_cache; static std::mutex g_pk_mtx; +static std::unordered_set g_bg_pvac_checked; +static std::mutex g_bg_pvac_mtx; static std::optional> pk_cache_get(const std::string& addr) { std::lock_guard lk(g_pk_mtx); @@ -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().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(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; @@ -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(); @@ -3020,15 +3057,32 @@ 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 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().empty(); - if (!pvac_ok) { + if (pvac_ok) { + std::lock_guard 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(); @@ -3036,10 +3090,21 @@ int main(int argc, char** argv) { 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 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 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 (...) {} diff --git a/rpc_client.hpp b/rpc_client.hpp index 3c0405c..ac3517f 100644 --- a/rpc_client.hpp +++ b/rpc_client.hpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include "lib/json.hpp" #include "lib/httplib.h" @@ -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")); } } @@ -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(body.size(), 160)); + for (char& c : out) { + unsigned char uc = static_cast(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")) @@ -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) + }; } } }; -} \ No newline at end of file +} // namespace octra From 85514ddd29df2890fca4c2147885af6166060bf3 Mon Sep 17 00:00:00 2001 From: kosunghun317 Date: Thu, 7 May 2026 21:38:17 +0900 Subject: [PATCH 8/8] docs: document wallet network settings --- README.md | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index aacf2b2..22a0abb 100644 --- a/README.md +++ b/README.md @@ -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