Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee8841590a | |||
| 20db758bc2 | |||
| 3981ebe3ed | |||
| 161faba9a0 | |||
| 53bc6d3e3e | |||
| 8524084c47 | |||
| a91d9016a8 | |||
| 726a51bd0e | |||
| 60d8dc7555 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Check commit
|
||||
id: check
|
||||
run: |
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Get version
|
||||
id: version
|
||||
run: |
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Restore cargo cache
|
||||
id: cache-cargo-restore
|
||||
uses: actions/cache/restore@v5
|
||||
@@ -188,7 +188,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Restore cargo cache
|
||||
id: cache-cargo-restore
|
||||
uses: actions/cache/restore@v5
|
||||
@@ -255,7 +255,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Restore cargo cache
|
||||
id: cache-cargo-restore
|
||||
uses: actions/cache/restore@v5
|
||||
@@ -307,7 +307,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
sed -i -- 's#https://code\.gri\.mw#${{ secrets.REPO_HOST }}#g' .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- run: mkdir release
|
||||
- name: Restore cargo cache
|
||||
id: cache-cargo-restore
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: |
|
||||
(Get-content .gitmodules) | Foreach-Object {$_ -replace "https://code.gri.mw", "${{ secrets.REPO_HOST }}"} | Set-Content .gitmodules
|
||||
git submodule update --init
|
||||
git submodule update --init --recursive --remote
|
||||
- name: Update UpgradeCode
|
||||
shell: powershell
|
||||
run: |
|
||||
|
||||
+1
-4
@@ -1,10 +1,7 @@
|
||||
[submodule "node"]
|
||||
path = node
|
||||
url = https://code.gri.mw/ardocrat/node
|
||||
[submodule "wallet"]
|
||||
path = wallet
|
||||
url = https://code.gri.mw/ardocrat/wallet
|
||||
branch = grim
|
||||
branch = grim-staging
|
||||
[submodule "tor/webtunnel"]
|
||||
path = tor/webtunnel
|
||||
url = https://code.gri.mw/WEB/webtunnel
|
||||
|
||||
Generated
+695
-562
File diff suppressed because it is too large
Load Diff
+17
-24
@@ -29,14 +29,14 @@ panic = "abort"
|
||||
log = "0.4.27"
|
||||
|
||||
# node
|
||||
grin_api = { path = "node/api" }
|
||||
grin_chain = { path = "node/chain" }
|
||||
grin_config = { path = "node/config" }
|
||||
grin_core = { path = "node/core" }
|
||||
grin_p2p = { path = "node/p2p" }
|
||||
grin_servers = { path = "node/servers" }
|
||||
grin_keychain = { path = "node/keychain" }
|
||||
grin_util = { path = "node/util" }
|
||||
grin_api = { path = "wallet/grin/api" }
|
||||
grin_chain = { path = "wallet/grin/chain" }
|
||||
grin_config = { path = "wallet/grin/config" }
|
||||
grin_core = { path = "wallet/grin/core" }
|
||||
grin_p2p = { path = "wallet/grin/p2p" }
|
||||
grin_servers = { path = "wallet/grin/servers" }
|
||||
grin_keychain = { path = "wallet/grin/keychain" }
|
||||
grin_util = { path = "wallet/grin/util" }
|
||||
|
||||
# wallet
|
||||
grin_wallet_impls = { path = "wallet/impls" }
|
||||
@@ -53,10 +53,7 @@ rust-i18n = "3.1.5"
|
||||
|
||||
## other
|
||||
log4rs = "1.4.0"
|
||||
anyhow = "1.0.97"
|
||||
pin-project = "1.1.10"
|
||||
backtrace = "0.3.76"
|
||||
thiserror = "2.0.18"
|
||||
futures = "0.3.31"
|
||||
dirs = "6.0.0"
|
||||
sys-locale = "0.3.2"
|
||||
@@ -92,22 +89,18 @@ uuid = { version = "0.8.2", features = ["v4"] }
|
||||
num-bigint = "0.4.6"
|
||||
|
||||
## tor
|
||||
arti-client = { version = "0.42.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.42.0", features = ["static"] }
|
||||
tor-config = "0.42.0"
|
||||
fs-mistrust = "0.14.1"
|
||||
tor-hsservice = "0.42.0"
|
||||
tor-hsrproxy = "0.42.0"
|
||||
tor-keymgr = "0.42.0"
|
||||
tor-llcrypto = "0.42.0"
|
||||
tor-hscrypto = "0.42.0"
|
||||
tor-error = "0.42.0"
|
||||
arti-client = { version = "0.43.0", features = ["static", "pt-client", "onion-service-service", "onion-service-client"] }
|
||||
tor-rtcompat = { version = "0.43.0", features = ["static"] }
|
||||
tor-config = "0.43.0"
|
||||
fs-mistrust = "0.14.2"
|
||||
tor-hsservice = "0.43.0"
|
||||
tor-hsrproxy = "0.43.0"
|
||||
tor-keymgr = "0.43.0"
|
||||
tor-llcrypto = "0.43.0"
|
||||
tor-hscrypto = "0.43.0"
|
||||
sha2 = "0.10.8"
|
||||
ed25519-dalek = "2.1.1"
|
||||
curve25519-dalek = "4.1.3"
|
||||
hyper-tor = { version = "0.14.32", features = ["full"], package = "hyper" }
|
||||
tls-api = "0.12.0"
|
||||
tls-api-native-tls = "0.12.1"
|
||||
safelog = "0.8.1"
|
||||
|
||||
## stratum server
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: Verbindungsproblem
|
||||
disconnected: Verbindung getrennt
|
||||
receiver_address: 'Empfängeraddresse:'
|
||||
sender_address: 'Absenderadresse:'
|
||||
incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:'
|
||||
tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen.
|
||||
tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen.
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: 'Maximale Anzahl von ausgehenden Peer-Verbindungen:'
|
||||
reset_data_desc: Reset-Knotendaten. Verwenden Sie diese Funktion nur, wenn es Probleme mit der Synchronisation gibt.
|
||||
reset_data: Daten zurücksetzten
|
||||
ip_listen_all: Hören Sie auf allen Schnittstellen
|
||||
modal:
|
||||
cancel: Abbrechen
|
||||
save: Speichern
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: Connection error
|
||||
disconnected: Disconnected
|
||||
receiver_address: 'Address of the receiver:'
|
||||
sender_address: 'Address of the sender:'
|
||||
incorrect_addr_err: 'Entered address is incorrect:'
|
||||
tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled.
|
||||
tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously.
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: 'Maximum number of outbound peer connections:'
|
||||
reset_data_desc: Reset the node data. Use it with a caution only if there are problems with synchronization.
|
||||
reset_data: Reset data
|
||||
ip_listen_all: Listen on all interfaces
|
||||
modal:
|
||||
cancel: Cancel
|
||||
save: Save
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: Erreur de connexion
|
||||
disconnected: Déconnecté
|
||||
receiver_address: 'Adresse du destinataire:'
|
||||
sender_address: "Adresse de l'expéditeur:"
|
||||
incorrect_addr_err: 'Adresse entrée incorrecte:'
|
||||
tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée."
|
||||
tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée."
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: 'Nombre maximum de connexions de pairs sortants :'
|
||||
reset_data_desc: Réinitialisez les données du noeud. Utilisez-le avec prudence uniquement en cas de problème de synchronisation.
|
||||
reset_data: Réinitialisation des données
|
||||
ip_listen_all: Écoutez sur toutes les interfaces
|
||||
modal:
|
||||
cancel: Annuler
|
||||
save: Sauvegarder
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: Ошибка подключения
|
||||
disconnected: Отключено
|
||||
receiver_address: 'Адрес получателя:'
|
||||
sender_address: 'Адрес отправителя:'
|
||||
incorrect_addr_err: 'Введённый адрес неверен:'
|
||||
tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена.
|
||||
tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций.
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: 'Максимальное количество исходящих подключений к пирам:'
|
||||
reset_data_desc: Сбросить данные узла. Используйте с осторожностью, только при наличии проблем с синхронизацией.
|
||||
reset_data: Сброс данных
|
||||
ip_listen_all: Слушать на всех интерфейсах
|
||||
modal:
|
||||
cancel: Отмена
|
||||
save: Сохранить
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: Bagalanti hatasi
|
||||
disconnected: Baglanti yok
|
||||
receiver_address: 'Alicinin adresi:'
|
||||
sender_address: 'Gönderici adresi:'
|
||||
incorrect_addr_err: 'Girilen adres hatali:'
|
||||
tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi.
|
||||
tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi.
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: 'Maksimum giden Peer baglanti sayisi:'
|
||||
reset_data_desc: Node verisini sifirlama. Sadece senkronizasyonda sorun varsa dikkatli kullanin.
|
||||
reset_data: Verileri sifirlama
|
||||
ip_listen_all: Tüm arayüzlerde dinle
|
||||
modal:
|
||||
cancel: Iptal
|
||||
save: Kaydet
|
||||
|
||||
@@ -152,6 +152,7 @@ transport:
|
||||
conn_error: 连接错误
|
||||
disconnected: 已断开连接
|
||||
receiver_address: '接收者的地址:'
|
||||
sender_address: '发件人地址:'
|
||||
incorrect_addr_err: '输入的地址不正确:'
|
||||
tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消.
|
||||
tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易.
|
||||
@@ -302,6 +303,7 @@ network_settings:
|
||||
max_outbound_count: '最大出站网络对点连接数:'
|
||||
reset_data_desc: 重置节点数据。只有在出现同步问题时才需谨慎使用.
|
||||
reset_data: 重置数据
|
||||
ip_listen_all: 在所有接口上监听
|
||||
modal:
|
||||
cancel: 取消
|
||||
save: 保存
|
||||
|
||||
-1
Submodule node deleted from 386ac1ed5c
+37
-1
@@ -25,8 +25,8 @@ use std::thread;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::CAMERA_ROTATE;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::View;
|
||||
use crate::gui::views::types::{QrScanResult, QrScanState};
|
||||
use crate::gui::views::{Modal, View};
|
||||
use crate::wallet::WalletUtils;
|
||||
use crate::wallet::types::PhraseSize;
|
||||
|
||||
@@ -88,6 +88,42 @@ impl CameraContent {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
|
||||
/// Draw modal camera content.
|
||||
pub fn modal_ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
mut on_result: impl FnMut(Option<QrScanResult>),
|
||||
) {
|
||||
if let Some(result) = self.qr_scan_result() {
|
||||
on_result(Some(result));
|
||||
} else {
|
||||
ui.add_space(6.0);
|
||||
self.ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Show buttons to close modal or come back to sending input.
|
||||
ui.columns(2, |cols| {
|
||||
cols[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
cb.stop_camera();
|
||||
on_result(None);
|
||||
Modal::close();
|
||||
});
|
||||
});
|
||||
cols[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||
on_result(None);
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw camera image.
|
||||
fn image_ui(&mut self, ui: &mut egui::Ui, mut img: DynamicImage, rotation: u32) -> Rect {
|
||||
// Setup image rotation.
|
||||
|
||||
@@ -105,8 +105,9 @@ impl ContentContainer for ConnectionsContent {
|
||||
|ui| {
|
||||
let r = View::item_rounding(0, 1, true);
|
||||
View::item_button(ui, r, QR_CODE, None, || {
|
||||
let (api_address, api_port) = NodeConfig::get_api_address();
|
||||
if let Ok(c) = ShareConnectionContent::new(ShareConnection {
|
||||
url: format!("http://{}", NodeConfig::get_api_address()),
|
||||
url: format!("http://{}:{}", api_address, api_port),
|
||||
username: "grin".to_string(),
|
||||
secret: NodeConfig::get_api_secret(true).unwrap_or("".to_string()),
|
||||
}) {
|
||||
@@ -277,9 +278,11 @@ impl ConnectionsContent {
|
||||
ui.add_space(1.0);
|
||||
|
||||
// Setup node API address text.
|
||||
let api_address = NodeConfig::get_api_address();
|
||||
let address_text =
|
||||
format!("{} http://{}", COMPUTER_TOWER, api_address);
|
||||
let (api_address, api_port) = NodeConfig::get_api_address();
|
||||
let address_text = format!(
|
||||
"{} http://{}:{}",
|
||||
COMPUTER_TOWER, api_address, api_port
|
||||
);
|
||||
ui.label(
|
||||
RichText::new(address_text).size(15.0).color(Colors::gray()),
|
||||
);
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{RichText, ScrollArea};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{ARROW_COUNTER_CLOCKWISE, TRASH};
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
@@ -26,6 +23,9 @@ use crate::gui::views::types::{ContentContainer, ModalPosition};
|
||||
use crate::gui::views::{Content, Modal, View};
|
||||
use crate::node::{Node, NodeConfig};
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{RichText, ScrollArea};
|
||||
|
||||
/// Integrated node settings tab content.
|
||||
pub struct NetworkSettings {
|
||||
/// Integrated node general setup content.
|
||||
@@ -162,39 +162,75 @@ impl NetworkSettings {
|
||||
ips: &Vec<String>,
|
||||
on_change: impl FnOnce(&String),
|
||||
) {
|
||||
let mut selected_ip = saved_ip;
|
||||
|
||||
// Set first IP address as current if saved is not present at system.
|
||||
if !ips.contains(saved_ip) {
|
||||
selected_ip = ips.get(0).unwrap();
|
||||
let mut all = NodeConfig::ALL_INTERFACES.to_string();
|
||||
let all_ips = saved_ip == &all || saved_ip == &format!("[{}]", &all);
|
||||
if all_ips {
|
||||
all = saved_ip.clone();
|
||||
}
|
||||
|
||||
ui.add_space(2.0);
|
||||
let mut selected_ip = saved_ip.clone();
|
||||
|
||||
// Show available IP addresses on the system.
|
||||
let _ = ips
|
||||
.chunks(2)
|
||||
.map(|x| {
|
||||
if x.len() == 2 {
|
||||
ui.columns(2, |columns| {
|
||||
let ip_left = x.get(0).unwrap();
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::radio_value(ui, &mut selected_ip, ip_left, ip_left.to_string());
|
||||
let mut listen_all_changed = false;
|
||||
View::checkbox(ui, all_ips, t!("network_settings.ip_listen_all"), || {
|
||||
listen_all_changed = true;
|
||||
});
|
||||
if listen_all_changed {
|
||||
let new_ip = if all_ips {
|
||||
ips.get(0).unwrap_or(&all).clone()
|
||||
} else {
|
||||
all.clone()
|
||||
};
|
||||
selected_ip = new_ip;
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
if selected_ip != all {
|
||||
// Set first IP address as current if saved is not present at system.
|
||||
if !ips.contains(&saved_ip) {
|
||||
selected_ip = ips.get(0).unwrap().clone();
|
||||
}
|
||||
|
||||
// Show available IP addresses on the system.
|
||||
let _ = ips
|
||||
.chunks(2)
|
||||
.map(|x| {
|
||||
if x.len() == 2 {
|
||||
ui.columns(2, |columns| {
|
||||
let ip_left = x.get(0).unwrap();
|
||||
let val = if all_ips {
|
||||
&mut ip_left.clone()
|
||||
} else {
|
||||
&mut selected_ip
|
||||
};
|
||||
columns[0].vertical_centered(|ui| {
|
||||
View::radio_value(ui, val, ip_left.clone(), ip_left.to_string());
|
||||
});
|
||||
let ip_right = x.get(1).unwrap();
|
||||
let val = if all_ips {
|
||||
&mut ip_right.clone()
|
||||
} else {
|
||||
&mut selected_ip
|
||||
};
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::radio_value(ui, val, ip_right.clone(), ip_right.to_string());
|
||||
})
|
||||
});
|
||||
let ip_right = x.get(1).unwrap();
|
||||
columns[1].vertical_centered(|ui| {
|
||||
View::radio_value(ui, &mut selected_ip, ip_right, ip_right.to_string());
|
||||
})
|
||||
});
|
||||
} else {
|
||||
let ip = x.get(0).unwrap();
|
||||
View::radio_value(ui, &mut selected_ip, ip, ip.to_string());
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
} else {
|
||||
let ip = x.get(0).unwrap();
|
||||
let val = if all_ips {
|
||||
&mut ip.clone()
|
||||
} else {
|
||||
&mut selected_ip
|
||||
};
|
||||
View::radio_value(ui, val, ip.clone(), ip.to_string());
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
|
||||
if saved_ip != selected_ip {
|
||||
if saved_ip != &selected_ip {
|
||||
on_change(&selected_ip.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ const FTL_MODAL: &'static str = "node_ftl";
|
||||
|
||||
impl Default for NodeSetup {
|
||||
fn default() -> Self {
|
||||
let (api_ip, api_port) = NodeConfig::get_api_ip_port();
|
||||
let (api_ip, api_port) = NodeConfig::get_api_address();
|
||||
let is_api_port_available = NodeConfig::is_api_port_available(&api_ip, &api_port);
|
||||
Self {
|
||||
data_path_edit: NodeConfig::get_chain_data_path(),
|
||||
@@ -204,7 +204,7 @@ impl ContentContainer for NodeSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show API IP addresses to select.
|
||||
let (api_ip, api_port) = NodeConfig::get_api_ip_port();
|
||||
let (api_ip, api_port) = NodeConfig::get_api_address();
|
||||
NetworkSettings::ip_addrs_ui(ui, &api_ip, &self.available_ips, |selected_ip| {
|
||||
let api_available = NodeConfig::is_api_port_available(selected_ip, &api_port);
|
||||
self.is_api_port_available = api_available;
|
||||
@@ -416,7 +416,7 @@ impl NodeSetup {
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let (_, port) = NodeConfig::get_api_ip_port();
|
||||
let (_, port) = NodeConfig::get_api_address();
|
||||
View::button(
|
||||
ui,
|
||||
format!("{} {}", PLUG, &port),
|
||||
@@ -436,6 +436,7 @@ impl NodeSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
if !self.is_api_port_available {
|
||||
ui.add_space(6.0);
|
||||
// Show error when API server port is unavailable.
|
||||
ui.label(
|
||||
RichText::new(t!("network_settings.port_unavailable"))
|
||||
@@ -451,7 +452,7 @@ impl NodeSetup {
|
||||
fn api_port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut NodeSetup| {
|
||||
// Check if port is available.
|
||||
let (api_ip, _) = NodeConfig::get_api_ip_port();
|
||||
let (api_ip, _) = NodeConfig::get_api_address();
|
||||
let available = NodeConfig::is_api_port_available(&api_ip, &c.api_port_edit);
|
||||
c.api_port_available_edit = available;
|
||||
if available {
|
||||
|
||||
@@ -45,6 +45,9 @@ pub struct P2PSetup {
|
||||
/// Flag to check if p2p port is available.
|
||||
port_available_edit: bool,
|
||||
|
||||
/// IP Addresses available at system.
|
||||
available_ips: Vec<String>,
|
||||
|
||||
/// Flag to check if p2p port from saved config value is available.
|
||||
is_port_available: bool,
|
||||
|
||||
@@ -90,7 +93,8 @@ pub const MAX_OUTBOUND_MODAL: &'static str = "p2p_max_outbound";
|
||||
impl Default for P2PSetup {
|
||||
fn default() -> Self {
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
let is_port_available = NodeConfig::is_p2p_port_available(&port);
|
||||
let ip = NodeConfig::get_p2p_host();
|
||||
let is_port_available = NodeConfig::is_p2p_port_available(&ip, &port);
|
||||
let default_main_seeds = Node::MAINNET_DNS_SEEDS
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
@@ -102,9 +106,10 @@ impl Default for P2PSetup {
|
||||
Self {
|
||||
port_edit: port,
|
||||
port_available_edit: is_port_available,
|
||||
available_ips: NodeConfig::get_ip_addrs(),
|
||||
is_port_available,
|
||||
address_check: Bind::new(false),
|
||||
address_available: Some(true),
|
||||
is_port_available,
|
||||
peer_edit: "".to_string(),
|
||||
default_main_seeds,
|
||||
default_test_seeds,
|
||||
@@ -152,8 +157,8 @@ impl ContentContainer for P2PSetup {
|
||||
ui.add_space(6.0);
|
||||
|
||||
ui.vertical_centered(|ui| {
|
||||
// Show p2p port setup.
|
||||
self.port_ui(ui);
|
||||
// Show p2p address setup.
|
||||
self.address_ui(ui);
|
||||
|
||||
ui.add_space(6.0);
|
||||
View::horizontal_line(ui, Colors::item_stroke());
|
||||
@@ -229,8 +234,14 @@ impl ContentContainer for P2PSetup {
|
||||
const DNS_SEEDS_TITLE: &'static str = "DNS Seeds";
|
||||
|
||||
impl P2PSetup {
|
||||
/// Draw p2p port setup content.
|
||||
fn port_ui(&mut self, ui: &mut egui::Ui) {
|
||||
/// Draw p2p address setup content.
|
||||
fn address_ui(&mut self, ui: &mut egui::Ui) {
|
||||
// Show message when IP addresses are not available on the system.
|
||||
if self.available_ips.is_empty() {
|
||||
NetworkSettings::no_ip_address_ui(ui);
|
||||
return;
|
||||
}
|
||||
|
||||
ui.label(
|
||||
RichText::new(t!("network_settings.p2p_port"))
|
||||
.size(16.0)
|
||||
@@ -238,7 +249,17 @@ impl P2PSetup {
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
|
||||
let ip = NodeConfig::get_p2p_host();
|
||||
let port = NodeConfig::get_p2p_port();
|
||||
|
||||
NetworkSettings::ip_addrs_ui(ui, &ip, &self.available_ips, |selected_ip| {
|
||||
NodeConfig::save_p2p_host(selected_ip);
|
||||
let p2p_available = NodeConfig::is_p2p_port_available(selected_ip, &port);
|
||||
self.is_port_available = p2p_available;
|
||||
});
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
View::button(
|
||||
ui,
|
||||
format!("{} {}", PLUG, &port),
|
||||
@@ -272,7 +293,8 @@ impl P2PSetup {
|
||||
fn port_modal(&mut self, ui: &mut egui::Ui, modal: &Modal, cb: &dyn PlatformCallbacks) {
|
||||
let on_save = |c: &mut P2PSetup| {
|
||||
// Check if port is available.
|
||||
let available = NodeConfig::is_p2p_port_available(&c.port_edit);
|
||||
let ip = NodeConfig::get_p2p_host();
|
||||
let available = NodeConfig::is_p2p_port_available(&ip, &c.port_edit);
|
||||
c.port_available_edit = available;
|
||||
|
||||
// Save port at config if it's available.
|
||||
|
||||
@@ -12,25 +12,36 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::{Id, RichText};
|
||||
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
|
||||
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::platform::PlatformCallbacks;
|
||||
use crate::gui::views::{Modal, TextEdit, View};
|
||||
use crate::gui::views::{CameraContent, Modal, TextEdit, View};
|
||||
use crate::wallet::Wallet;
|
||||
use crate::wallet::types::WalletTask;
|
||||
use egui::{Id, RichText};
|
||||
use grin_core::core::{amount_from_hr_string, amount_to_hr_string};
|
||||
use grin_wallet_libwallet::SlatepackAddress;
|
||||
|
||||
/// Invoice request creation content.
|
||||
pub struct InvoiceRequestContent {
|
||||
/// Amount to receive.
|
||||
amount_edit: String,
|
||||
|
||||
/// Sender address.
|
||||
address_edit: String,
|
||||
/// Flag to check if entered address is incorrect.
|
||||
address_error: bool,
|
||||
|
||||
/// Address QR code scanner content.
|
||||
address_scan_content: Option<CameraContent>,
|
||||
}
|
||||
|
||||
impl Default for InvoiceRequestContent {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
amount_edit: "".to_string(),
|
||||
address_edit: "".to_string(),
|
||||
address_error: false,
|
||||
address_scan_content: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,14 +61,36 @@ impl InvoiceRequestContent {
|
||||
return;
|
||||
}
|
||||
if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) {
|
||||
m.amount_edit = "".to_string();
|
||||
wallet.task(WalletTask::Receive(a));
|
||||
let addr_str = m.address_edit.as_str();
|
||||
let addr = if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
wallet.task(WalletTask::Receive(a, addr));
|
||||
Modal::close();
|
||||
}
|
||||
};
|
||||
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Draw QR code scanner content if requested.
|
||||
if let Some(content) = self.address_scan_content.as_mut() {
|
||||
let mut close_scan = true;
|
||||
content.modal_ui(ui, cb, |result| {
|
||||
if let Some(result) = result {
|
||||
self.address_edit = result.text();
|
||||
} else {
|
||||
modal.enable_closing();
|
||||
close_scan = true;
|
||||
}
|
||||
});
|
||||
if close_scan {
|
||||
self.address_scan_content = None;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw amount input content.
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
@@ -72,11 +105,9 @@ impl InvoiceRequestContent {
|
||||
let amount_edit_before = self.amount_edit.clone();
|
||||
let mut amount_edit = TextEdit::new(Id::from(modal.id).with(wallet.get_config().id))
|
||||
.h_center()
|
||||
.numeric();
|
||||
.numeric()
|
||||
.focus(Modal::first_draw());
|
||||
amount_edit.ui(ui, &mut self.amount_edit, cb);
|
||||
if amount_edit.enter_pressed {
|
||||
on_continue(self);
|
||||
}
|
||||
|
||||
// Check value if input was changed.
|
||||
if amount_edit_before != self.amount_edit {
|
||||
@@ -109,7 +140,54 @@ impl InvoiceRequestContent {
|
||||
}
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
|
||||
// Show address error or input description.
|
||||
ui.vertical_centered(|ui| {
|
||||
if self.address_error {
|
||||
ui.label(
|
||||
RichText::new(t!("transport.incorrect_addr_err"))
|
||||
.size(17.0)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
} else {
|
||||
ui.label(
|
||||
RichText::new(t!("transport.sender_address"))
|
||||
.size(17.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Show address text edit.
|
||||
let addr_edit_before = self.address_edit.clone();
|
||||
let address_edit_id = Id::from(modal.id)
|
||||
.with("_address")
|
||||
.with(wallet.get_config().id);
|
||||
let mut address_edit = TextEdit::new(address_edit_id)
|
||||
.paste()
|
||||
.focus(false)
|
||||
.scan_qr();
|
||||
if amount_edit.enter_pressed {
|
||||
address_edit.focus_request();
|
||||
}
|
||||
address_edit.ui(ui, &mut self.address_edit, cb);
|
||||
// Check if scan button was pressed.
|
||||
if address_edit.scan_pressed {
|
||||
modal.disable_closing();
|
||||
self.address_scan_content = Some(CameraContent::default());
|
||||
}
|
||||
|
||||
ui.add_space(12.0);
|
||||
// Check value if input was changed.
|
||||
if addr_edit_before != self.address_edit {
|
||||
self.address_error = false;
|
||||
}
|
||||
// Continue on Enter press.
|
||||
if address_edit.enter_pressed {
|
||||
on_continue(self);
|
||||
}
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
@@ -121,7 +199,6 @@ impl InvoiceRequestContent {
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
self.amount_edit = "".to_string();
|
||||
Modal::close();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -90,40 +90,18 @@ impl SendRequestContent {
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Draw QR code scanner content if requested.
|
||||
if let Some(scanner) = self.address_scan_content.as_mut() {
|
||||
let on_stop = || {
|
||||
cb.stop_camera();
|
||||
modal.enable_closing();
|
||||
};
|
||||
|
||||
if let Some(result) = scanner.qr_scan_result() {
|
||||
self.address_edit = result.text();
|
||||
on_stop();
|
||||
if let Some(content) = self.address_scan_content.as_mut() {
|
||||
let mut close_scan = true;
|
||||
content.modal_ui(ui, cb, |result| {
|
||||
if let Some(result) = result {
|
||||
self.address_edit = result.text();
|
||||
} else {
|
||||
modal.enable_closing();
|
||||
close_scan = true;
|
||||
}
|
||||
});
|
||||
if close_scan {
|
||||
self.address_scan_content = None;
|
||||
} else {
|
||||
ui.add_space(6.0);
|
||||
scanner.ui(ui, cb);
|
||||
ui.add_space(6.0);
|
||||
|
||||
// Setup spacing between buttons.
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
|
||||
// Show buttons to close modal or come back to sending input.
|
||||
ui.columns(2, |cols| {
|
||||
cols[0].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("close"), Colors::white_or_black(false), || {
|
||||
on_stop();
|
||||
self.close();
|
||||
});
|
||||
});
|
||||
cols[1].vertical_centered_justified(|ui| {
|
||||
View::button(ui, t!("back"), Colors::white_or_black(false), || {
|
||||
on_stop();
|
||||
self.address_scan_content = None;
|
||||
});
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::gui::views::wallets::wallet::types::WalletContentContainer;
|
||||
use crate::gui::views::{Modal, QrCodeContent, View};
|
||||
use crate::tor::{Tor, TorConfig};
|
||||
use crate::wallet::Wallet;
|
||||
use crate::wallet::types::WalletTask;
|
||||
|
||||
/// Wallet transport panel content.
|
||||
pub struct WalletTransportContent {
|
||||
@@ -148,14 +149,12 @@ impl WalletTransportContent {
|
||||
|
||||
let service_id = &wallet.identifier();
|
||||
// Draw button to enable/disable Tor listener for current wallet.
|
||||
if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() {
|
||||
let port = wallet.foreign_api_port().unwrap();
|
||||
let key = wallet.secret_key().unwrap();
|
||||
if wallet.foreign_api_port().is_some() {
|
||||
if !Tor::is_service_starting(service_id) {
|
||||
if !Tor::is_service_running(service_id) {
|
||||
let r = CornerRadius::default();
|
||||
View::item_button(ui, r, POWER, Some(Colors::green()), || {
|
||||
Tor::start_service(port, key.clone(), service_id);
|
||||
wallet.task(WalletTask::StartTor);
|
||||
});
|
||||
} else {
|
||||
let r = CornerRadius::default();
|
||||
|
||||
@@ -633,24 +633,20 @@ impl WalletTransactionsContent {
|
||||
View::item_button(ui, rounding, icon, color, || {
|
||||
if repost {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
return;
|
||||
} else if let Some(action) = tx.action.as_ref() {
|
||||
match action {
|
||||
WalletTxAction::Finalizing => {
|
||||
wallet.task(WalletTask::Finalize(tx.data.id));
|
||||
}
|
||||
WalletTxAction::Posting => {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
}
|
||||
_ => {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
}
|
||||
}
|
||||
if action == &WalletTxAction::Finalizing {
|
||||
wallet.task(WalletTask::Finalize(tx.data.id));
|
||||
return;
|
||||
} else if action == &WalletTxAction::Posting {
|
||||
wallet.task(WalletTask::Post(tx.data.id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
} else {
|
||||
if let Some(a) = &tx.receiver {
|
||||
wallet.task(WalletTask::SendTor(tx.data.clone(), a.clone()));
|
||||
}
|
||||
wallet.task(WalletTask::FinalizeTor(tx.data.clone()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CornerRadius, Id, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
use grin_util::ToHex;
|
||||
use grin_wallet_libwallet::TxLogEntryType;
|
||||
use std::fs;
|
||||
|
||||
use crate::AppConfig;
|
||||
use crate::gui::Colors;
|
||||
use crate::gui::icons::{
|
||||
@@ -34,6 +27,13 @@ use crate::gui::views::{Modal, QrCodeContent, View};
|
||||
use crate::wallet::Wallet;
|
||||
use crate::wallet::types::{WalletTask, WalletTx};
|
||||
|
||||
use egui::scroll_area::ScrollBarVisibility;
|
||||
use egui::{Align, CornerRadius, Id, Layout, RichText, ScrollArea, StrokeKind};
|
||||
use grin_core::core::amount_to_hr_string;
|
||||
use grin_util::ToHex;
|
||||
use grin_wallet_libwallet::TxLogEntryType;
|
||||
use std::fs;
|
||||
|
||||
/// Transaction information [`Modal`] content.
|
||||
pub struct WalletTransactionContent {
|
||||
/// Transaction identifier.
|
||||
@@ -168,10 +168,12 @@ impl WalletTransactionContent {
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
if self.message.is_none() {
|
||||
let slatepack_path = wallet
|
||||
.get_config()
|
||||
.get_slate_path(tx.data.tx_slate_id.unwrap(), &tx.state);
|
||||
self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string()));
|
||||
if let Some(slate_state) = tx.data.tx_slate_state.as_ref() {
|
||||
let slatepack_path = wallet
|
||||
.get_config()
|
||||
.get_slate_path(tx.data.tx_slate_id.unwrap(), slate_state);
|
||||
self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string()));
|
||||
}
|
||||
}
|
||||
if let Some(m) = &self.message {
|
||||
if m.is_empty() {
|
||||
@@ -260,29 +262,31 @@ impl WalletTransactionContent {
|
||||
});
|
||||
|
||||
// Draw button to share response as file.
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
|
||||
View::colored_text_button(
|
||||
ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||
let name = format!("{}.{}.slatepack", slate_id, tx.state);
|
||||
let data = m.as_bytes().to_vec();
|
||||
cb.share_data(name, data).unwrap_or_default();
|
||||
// Show message input or close modal.
|
||||
if tx.can_finalize() {
|
||||
finalization_needed = true;
|
||||
} else {
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
if let Some(slate_id) = tx.data.tx_slate_id {
|
||||
if let Some(slate_state) = tx.data.tx_slate_state.as_ref() {
|
||||
ui.add_space(8.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
let share_text = format!("{} {}", FILE_TEXT, t!("share"));
|
||||
View::colored_text_button(
|
||||
ui,
|
||||
share_text,
|
||||
Colors::blue(),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
let name = format!("{}.{}.slatepack", slate_id, slate_state);
|
||||
let data = m.as_bytes().to_vec();
|
||||
cb.share_data(name, data).unwrap_or_default();
|
||||
// Show message input or close modal.
|
||||
if tx.can_finalize() {
|
||||
finalization_needed = true;
|
||||
} else {
|
||||
Modal::close();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if finalization_needed {
|
||||
Modal::new(MessageInputContent::MODAL_ID)
|
||||
@@ -370,13 +374,13 @@ impl WalletTransactionContent {
|
||||
info_item_ui(ui, kernel.0.to_hex(), label, true, cb);
|
||||
}
|
||||
// Show receiver or sender address.
|
||||
let addr = if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||
&tx.receiver
|
||||
let (addr, label) = if tx.data.tx_type == TxLogEntryType::TxSent {
|
||||
(&tx.receiver, t!("transport.receiver_address"))
|
||||
} else {
|
||||
&tx.sender
|
||||
(&tx.sender, t!("transport.sender_address"))
|
||||
};
|
||||
if let Some(addr) = addr {
|
||||
let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address"));
|
||||
let label = format!("{} {}", CIRCLE_HALF, label.replace(":", ""));
|
||||
info_item_ui(ui, addr.to_string(), label, true, cb);
|
||||
}
|
||||
}
|
||||
|
||||
+172
-74
@@ -16,7 +16,7 @@ use local_ip_address::list_afinet_netifas;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, ToSocketAddrs};
|
||||
use std::net::{IpAddr, SocketAddr, TcpListener, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -29,6 +29,7 @@ use grin_p2p::msg::PeerAddrs;
|
||||
use grin_p2p::{PeerAddr, Seeding};
|
||||
use grin_servers::common::types::ChainValidationMode;
|
||||
use rand::Rng;
|
||||
use url::Url;
|
||||
|
||||
use crate::node::Node;
|
||||
use crate::{AppConfig, Settings};
|
||||
@@ -136,6 +137,9 @@ pub struct NodeConfig {
|
||||
}
|
||||
|
||||
impl NodeConfig {
|
||||
/// To launch on all available interfaces (including IPv6).
|
||||
pub const ALL_INTERFACES: &str = "::";
|
||||
|
||||
/// Initialize config fields from provided [`ChainTypes`].
|
||||
pub fn for_chain_type(chain_type: &ChainTypes) -> Self {
|
||||
// Check secret files for current chain type.
|
||||
@@ -210,8 +214,7 @@ impl NodeConfig {
|
||||
(api, p2p)
|
||||
}
|
||||
};
|
||||
let api_addr = config.server.api_http_addr.split_once(":").unwrap().0;
|
||||
config.server.api_http_addr = format!("{}:{}", api_addr, api);
|
||||
config.server.api_http_addr = format!("127.0.0.1:{}", api);
|
||||
config.server.p2p_config.port = p2p;
|
||||
}
|
||||
|
||||
@@ -290,30 +293,18 @@ impl NodeConfig {
|
||||
|
||||
/// Check whether a port is available on the provided host.
|
||||
fn is_host_port_available(host: &String, port: &String) -> bool {
|
||||
if host == Self::ALL_INTERFACES {
|
||||
return true;
|
||||
}
|
||||
if let Ok(p) = port.parse::<u16>() {
|
||||
let ip_addr = Ipv4Addr::from_str(host.as_str()).unwrap();
|
||||
let ipv4 = SocketAddrV4::new(ip_addr, p);
|
||||
return TcpListener::bind(ipv4).is_ok();
|
||||
if let Ok(ip) = IpAddr::from_str(&host) {
|
||||
let addr = SocketAddr::new(ip, p);
|
||||
return TcpListener::bind(addr).is_ok();
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check whether a port is available across the system at all hosts.
|
||||
fn is_port_available(port: &String) -> bool {
|
||||
if let Ok(p) = port.parse::<u16>() {
|
||||
for ip in Self::get_ip_addrs() {
|
||||
let ip_addr = Ipv4Addr::from_str(ip.as_str()).unwrap();
|
||||
let ipv4 = SocketAddrV4::new(ip_addr, p);
|
||||
if TcpListener::bind(ipv4).is_err() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get chain data path.
|
||||
pub fn get_chain_data_path() -> String {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
@@ -327,6 +318,60 @@ impl NodeConfig {
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Get default Stratum server port.
|
||||
fn default_stratum_port() -> u16 {
|
||||
match AppConfig::chain_type() {
|
||||
ChainTypes::Mainnet => 3416,
|
||||
_ => 13416,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format address to support ipv6.
|
||||
fn format_address(ip: &String, port: &String) -> String {
|
||||
let addr = if ip.contains(Self::ALL_INTERFACES) {
|
||||
&format!("[{}]", ip)
|
||||
} else {
|
||||
ip
|
||||
};
|
||||
format!("{}:{}", addr, port)
|
||||
}
|
||||
|
||||
/// Parse host to support ipv6.
|
||||
fn parse_host(host: &String) -> String {
|
||||
if host.contains(Self::ALL_INTERFACES) {
|
||||
host.replace("[", "").replace("]", "")
|
||||
} else {
|
||||
host.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse saved address, returning default host or port on fail.
|
||||
fn parse_address_port(
|
||||
addr: &String,
|
||||
default_host: &str,
|
||||
default_port: u16,
|
||||
) -> (String, String) {
|
||||
let addr = if addr.contains("http") {
|
||||
addr.to_string()
|
||||
} else {
|
||||
format!("http://{}", addr)
|
||||
};
|
||||
if let Ok(url) = Url::parse(addr.as_str()) {
|
||||
let host = if let Some(h) = url.host() {
|
||||
Self::parse_host(&h.to_string())
|
||||
} else {
|
||||
default_host.to_string()
|
||||
};
|
||||
let port = if let Some(p) = url.port() {
|
||||
p.to_string()
|
||||
} else {
|
||||
default_port.to_string()
|
||||
};
|
||||
return (host, port);
|
||||
}
|
||||
(default_host.to_string(), default_port.to_string())
|
||||
}
|
||||
|
||||
/// Get stratum server IP address and port.
|
||||
pub fn get_stratum_address() -> (String, String) {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
@@ -339,13 +384,16 @@ impl NodeConfig {
|
||||
.stratum_server_addr
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let (addr, port) = saved_stratum_addr.split_once(":").unwrap();
|
||||
(addr.into(), port.into())
|
||||
Self::parse_address_port(
|
||||
saved_stratum_addr,
|
||||
"127.0.0.1",
|
||||
Self::default_stratum_port(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Save stratum server IP address and port.
|
||||
pub fn save_stratum_address(addr: &String, port: &String) {
|
||||
let addr_to_save = format!("{}:{}", addr, port);
|
||||
pub fn save_stratum_address(host: &String, port: &String) {
|
||||
let addr_to_save = Self::format_address(host, port);
|
||||
let mut w_config = Settings::node_config_to_update();
|
||||
w_config
|
||||
.node
|
||||
@@ -358,28 +406,32 @@ impl NodeConfig {
|
||||
}
|
||||
|
||||
/// Check if stratum server port is available across the system and config.
|
||||
pub fn is_stratum_port_available(ip: &String, port: &String) -> bool {
|
||||
if Node::get_stratum_stats().is_running {
|
||||
// Check if Stratum server with same address is running.
|
||||
let (cur_ip, cur_port) = Self::get_stratum_address();
|
||||
let same_running = ip == &cur_ip && port == &cur_port;
|
||||
return same_running || Self::is_not_running_stratum_port_available(ip, port);
|
||||
}
|
||||
Self::is_not_running_stratum_port_available(&ip, &port)
|
||||
}
|
||||
pub fn is_stratum_port_available(host: &String, port: &String) -> bool {
|
||||
let host = Self::parse_host(host);
|
||||
|
||||
/// Check if stratum port is available when server is not running.
|
||||
fn is_not_running_stratum_port_available(ip: &String, port: &String) -> bool {
|
||||
if Self::is_host_port_available(&ip, &port) {
|
||||
if &Self::get_p2p_port() != port {
|
||||
let (api_ip, api_port) = Self::get_api_ip_port();
|
||||
return if &api_ip == ip {
|
||||
&api_port != port
|
||||
} else {
|
||||
true
|
||||
};
|
||||
// Check if Stratum server with same address is running.
|
||||
if Node::get_stratum_stats().is_running {
|
||||
let (cur_ip, cur_port) = Self::get_stratum_address();
|
||||
let same_running = host == cur_ip && port == &cur_port;
|
||||
if same_running {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if address not conflicts with p2p and api.
|
||||
if Self::is_host_port_available(&host, &port) {
|
||||
let p2p_ip = Self::get_p2p_host();
|
||||
let p2p_port = Self::get_p2p_port();
|
||||
if p2p_ip == host && &p2p_port == port {
|
||||
return false;
|
||||
}
|
||||
let (api_ip, api_port) = Self::get_api_address();
|
||||
return if api_ip == host {
|
||||
&api_port != port
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -493,38 +545,55 @@ impl NodeConfig {
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Get API server address.
|
||||
pub fn get_api_address() -> String {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
r_config.node.server.api_http_addr.clone()
|
||||
/// Get default Stratum server port.
|
||||
fn default_api_port() -> u16 {
|
||||
match AppConfig::chain_type() {
|
||||
ChainTypes::Mainnet => 3413,
|
||||
_ => 13413,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get API server IP and port.
|
||||
pub fn get_api_ip_port() -> (String, String) {
|
||||
let saved_addr = Self::get_api_address();
|
||||
let (addr, port) = saved_addr.split_once(":").unwrap();
|
||||
(addr.into(), port.into())
|
||||
pub fn get_api_address() -> (String, String) {
|
||||
let r_config = Settings::node_config_to_read();
|
||||
let saved_api_addr = r_config.node.server.api_http_addr.clone();
|
||||
Self::parse_address_port(&saved_api_addr, "127.0.0.1", Self::default_api_port())
|
||||
}
|
||||
|
||||
/// Save API server IP address and port.
|
||||
pub fn save_api_address(addr: &String, port: &String) {
|
||||
let addr_to_save = format!("{}:{}", addr, port);
|
||||
let addr_to_save = Self::format_address(addr, port);
|
||||
let mut w_config = Settings::node_config_to_update();
|
||||
w_config.node.server.api_http_addr = addr_to_save;
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Check if api server port is available across the system and config.
|
||||
pub fn is_api_port_available(ip: &String, port: &String) -> bool {
|
||||
pub fn is_api_port_available(host: &String, port: &String) -> bool {
|
||||
let host = Self::parse_host(host);
|
||||
|
||||
// Check if API server with same address is running.
|
||||
if Node::is_running() {
|
||||
// Check if API server with same address is running.
|
||||
let same_running = NodeConfig::get_api_address() == format!("{}:{}", ip, port);
|
||||
if same_running || Self::is_host_port_available(ip, port) {
|
||||
return &Self::get_p2p_port() != port;
|
||||
let (cur_ip, cur_port) = Self::get_api_address();
|
||||
let same_running = host == cur_ip && port == &cur_port;
|
||||
if same_running {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if Self::is_host_port_available(ip, port) {
|
||||
return &Self::get_p2p_port() != port;
|
||||
}
|
||||
|
||||
// Check if address not conflicts with p2p and stratum.
|
||||
if Self::is_host_port_available(&host, port) {
|
||||
let p2p_ip = Self::get_p2p_host();
|
||||
let p2p_port = Self::get_p2p_port();
|
||||
if p2p_ip == host && &p2p_port == port {
|
||||
return false;
|
||||
}
|
||||
let (str_ip, str_port) = Self::get_stratum_address();
|
||||
return if str_ip == host {
|
||||
&str_port != port
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -662,6 +731,25 @@ impl NodeConfig {
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Get P2P server IP address.
|
||||
pub fn get_p2p_host() -> String {
|
||||
let host = Settings::node_config_to_read()
|
||||
.node
|
||||
.server
|
||||
.p2p_config
|
||||
.host
|
||||
.to_string();
|
||||
Self::parse_host(&host)
|
||||
}
|
||||
|
||||
/// Get P2P server IP address.
|
||||
pub fn save_p2p_host(host: &String) {
|
||||
let mut w_config = Settings::node_config_to_update();
|
||||
w_config.node.server.p2p_config.host =
|
||||
IpAddr::from_str(host).unwrap_or(IpAddr::from_str(Self::ALL_INTERFACES).unwrap());
|
||||
w_config.save();
|
||||
}
|
||||
|
||||
/// Get P2P server port.
|
||||
pub fn get_p2p_port() -> String {
|
||||
Settings::node_config_to_read()
|
||||
@@ -673,20 +761,30 @@ impl NodeConfig {
|
||||
}
|
||||
|
||||
/// Check if P2P server port is available across the system and config.
|
||||
pub fn is_p2p_port_available(port: &String) -> bool {
|
||||
if port.parse::<u16>().is_err() {
|
||||
return false;
|
||||
}
|
||||
let (_, api_port) = Self::get_api_ip_port();
|
||||
pub fn is_p2p_port_available(host: &String, port: &String) -> bool {
|
||||
let host = Self::parse_host(host);
|
||||
|
||||
// Check if P2P server with same address is running.
|
||||
if Node::is_running() {
|
||||
// Check if P2P server with same port is running.
|
||||
let same_running = &NodeConfig::get_p2p_port() == port;
|
||||
if same_running || Self::is_port_available(port) {
|
||||
return &api_port != port;
|
||||
let same_running =
|
||||
&NodeConfig::get_p2p_port() == port && NodeConfig::get_p2p_host() == host;
|
||||
if same_running {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else if Self::is_port_available(port) {
|
||||
return &api_port != port;
|
||||
}
|
||||
|
||||
// Check if address not conflicts with stratum and api.
|
||||
if Self::is_host_port_available(&host, &port) {
|
||||
let (str_ip, str_port) = Self::get_stratum_address();
|
||||
if str_ip == host && &str_port == port {
|
||||
return false;
|
||||
}
|
||||
let (api_ip, api_port) = Self::get_api_address();
|
||||
return if api_ip == host {
|
||||
&api_port != port
|
||||
} else {
|
||||
true
|
||||
};
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
-258
@@ -1,258 +0,0 @@
|
||||
// Copyright 2024 The Grim Developers
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::future::Future;
|
||||
use std::io::Error;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use arti_client::{DataStream, IntoTorAddr, TorClient};
|
||||
use hyper_tor::client::connect::{Connected, Connection};
|
||||
use hyper_tor::http::Uri;
|
||||
use hyper_tor::http::uri::Scheme;
|
||||
use hyper_tor::service::Service;
|
||||
use pin_project::pin_project;
|
||||
use thiserror::Error;
|
||||
use tls_api::TlsConnector as TlsConn; // This is different from tor_rtcompat::TlsConnector
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tor_config::deps::educe::Educe;
|
||||
use tor_rtcompat::Runtime;
|
||||
|
||||
/// Error making or using http connection
|
||||
///
|
||||
/// This error ends up being passed to hyper and bundled up into a [`hyper::Error`]
|
||||
#[derive(Error, Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum ConnectionError {
|
||||
/// Unsupported URI scheme
|
||||
#[error("unsupported URI scheme in {uri:?}")]
|
||||
UnsupportedUriScheme {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Missing hostname
|
||||
#[error("Missing hostname in {uri:?}")]
|
||||
MissingHostname {
|
||||
/// URI
|
||||
uri: Uri,
|
||||
},
|
||||
|
||||
/// Tor connection failed
|
||||
#[error("Tor connection failed")]
|
||||
Arti(#[from] arti_client::Error),
|
||||
|
||||
/// TLS connection failed
|
||||
#[error("TLS connection failed")]
|
||||
TLS(#[source] Arc<anyhow::Error>),
|
||||
}
|
||||
|
||||
/// We implement this for form's sake
|
||||
impl tor_error::HasKind for ConnectionError {
|
||||
#[rustfmt::skip]
|
||||
fn kind(&self) -> tor_error::ErrorKind {
|
||||
use ConnectionError as CE;
|
||||
use tor_error::ErrorKind as EK;
|
||||
match self {
|
||||
CE::UnsupportedUriScheme{..} => EK::NotImplemented,
|
||||
CE::MissingHostname{..} => EK::BadApiUsage,
|
||||
CE::Arti(e) => e.kind(),
|
||||
CE::TLS(_) => EK::RemoteProtocolViolation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **Main entrypoint**: `hyper` connector to make HTTP\[S] connections via Tor, using Arti.
|
||||
///
|
||||
/// An `ArtiHttpConnector` combines an Arti Tor client, and a TLS implementation,
|
||||
/// in a form that can be provided to hyper
|
||||
/// (e.g. to [`hyper::client::Builder`]'s `build` method)
|
||||
/// so that hyper can speak HTTP and HTTPS to origin servers via Tor.
|
||||
///
|
||||
/// TC is the TLS to used *across* Tor to connect to the origin server.
|
||||
/// For example, it could be a [`tls_api_native_tls::TlsConnector`].
|
||||
/// This is a different Rust type to the TLS used *by* Tor to connect to relays etc.
|
||||
/// It might even be a different underlying TLS implementation
|
||||
/// (although that is usually not a particularly good idea).
|
||||
#[derive(Educe)]
|
||||
#[educe(Clone)] // #[derive(Debug)] infers an unwanted bound TC: Clone
|
||||
pub struct ArtiHttpConnector<R: Runtime, TC: TlsConn> {
|
||||
/// The client
|
||||
client: TorClient<R>,
|
||||
|
||||
/// TLS for using across Tor.
|
||||
tls_conn: Arc<TC>,
|
||||
}
|
||||
|
||||
// #[derive(Clone)] infers a TC: Clone bound
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> ArtiHttpConnector<R, TC> {
|
||||
/// Make a new `ArtiHttpConnector` using an Arti `TorClient` object.
|
||||
pub fn new(client: TorClient<R>, tls_conn: TC) -> Self {
|
||||
let tls_conn = tls_conn.into();
|
||||
Self { client, tls_conn }
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper type that makes an Arti `DataStream` implement necessary traits to be used as
|
||||
/// a `hyper` connection object (mainly `Connection`).
|
||||
///
|
||||
/// This might represent a bare HTTP connection across Tor,
|
||||
/// or it might represent an HTTPS connection through Tor to an origin server,
|
||||
/// `TC::TlsStream` as the TLS layer.
|
||||
///
|
||||
/// An `ArtiHttpConnection` is constructed by hyper's use of the [`ArtiHttpConnector`]
|
||||
/// implementation of [`hyper::service::Service`],
|
||||
/// and then used by hyper as the transport for hyper's HTTP implementation.
|
||||
#[pin_project]
|
||||
pub struct ArtiHttpConnection<TC: TlsConn> {
|
||||
/// The stream
|
||||
#[pin]
|
||||
inner: MaybeHttpsStream<TC>,
|
||||
}
|
||||
|
||||
/// The actual stream; might be TLS, might not
|
||||
#[pin_project(project = MaybeHttpsStreamProj)]
|
||||
enum MaybeHttpsStream<TC: TlsConn> {
|
||||
/// http
|
||||
Http(Pin<Box<DataStream>>), // Tc:TlsStream is generally boxed; box this one too
|
||||
|
||||
/// https
|
||||
Https(#[pin] TC::TlsStream),
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> Connection for ArtiHttpConnection<TC> {
|
||||
fn connected(&self) -> Connected {
|
||||
Connected::new()
|
||||
}
|
||||
}
|
||||
|
||||
// These trait implementations just defer to the inner `DataStream`; the wrapper type is just
|
||||
// there to implement the `Connection` trait.
|
||||
impl<TC: TlsConn> AsyncRead for ArtiHttpConnection<TC> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_read(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<TC: TlsConn> AsyncWrite for ArtiHttpConnection<TC> {
|
||||
fn poll_write(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<Result<usize, Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_write(cx, buf),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_flush(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
match self.project().inner.project() {
|
||||
MaybeHttpsStreamProj::Http(ds) => ds.as_mut().poll_shutdown(cx),
|
||||
MaybeHttpsStreamProj::Https(t) => t.poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
/// Are we doing TLS?
|
||||
enum UseTls {
|
||||
/// No
|
||||
Bare,
|
||||
|
||||
/// Yes
|
||||
Tls,
|
||||
}
|
||||
|
||||
/// Convert uri to http\[s\] host and port, and whether to do tls
|
||||
fn uri_to_host_port_tls(uri: Uri) -> Result<(String, u16, UseTls), ConnectionError> {
|
||||
let use_tls = {
|
||||
// Scheme doesn't derive PartialEq so can't be matched on
|
||||
let scheme = uri.scheme();
|
||||
if scheme == Some(&Scheme::HTTP) {
|
||||
UseTls::Bare
|
||||
} else if scheme == Some(&Scheme::HTTPS) {
|
||||
UseTls::Tls
|
||||
} else {
|
||||
return Err(ConnectionError::UnsupportedUriScheme { uri });
|
||||
}
|
||||
};
|
||||
let host = match uri.host() {
|
||||
Some(h) => h,
|
||||
_ => return Err(ConnectionError::MissingHostname { uri }),
|
||||
};
|
||||
let port = uri.port().map(|x| x.as_u16()).unwrap_or(match use_tls {
|
||||
UseTls::Tls => 443,
|
||||
UseTls::Bare => 80,
|
||||
});
|
||||
|
||||
Ok((host.to_owned(), port, use_tls))
|
||||
}
|
||||
|
||||
impl<R: Runtime, TC: TlsConn> Service<Uri> for ArtiHttpConnector<R, TC> {
|
||||
type Response = ArtiHttpConnection<TC>;
|
||||
type Error = ConnectionError;
|
||||
#[allow(clippy::type_complexity)]
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Uri) -> Self::Future {
|
||||
// `TorClient` objects can be cloned cheaply (the cloned objects refer to the same
|
||||
// underlying handles required to make Tor connections internally).
|
||||
// We use this to avoid the returned future having to borrow `self`.
|
||||
let client = self.client.clone();
|
||||
let tls_conn = self.tls_conn.clone();
|
||||
Box::pin(async move {
|
||||
// Extract the host and port to connect to from the URI.
|
||||
let (host, port, use_tls) = uri_to_host_port_tls(req)?;
|
||||
// Initiate a new Tor connection, producing a `DataStream` if successful.
|
||||
let addr = (&host as &str, port)
|
||||
.into_tor_addr()
|
||||
.map_err(arti_client::Error::from)?;
|
||||
let ds = client.connect(addr).await?;
|
||||
|
||||
let inner = match use_tls {
|
||||
UseTls::Tls => {
|
||||
let conn = tls_conn
|
||||
.connect_impl_tls_stream(&host, ds)
|
||||
.await
|
||||
.map_err(|e| ConnectionError::TLS(e.into()))?;
|
||||
MaybeHttpsStream::Https(conn)
|
||||
}
|
||||
UseTls::Bare => MaybeHttpsStream::Http(Box::new(ds).into()),
|
||||
};
|
||||
|
||||
Ok(ArtiHttpConnection { inner })
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,3 @@ pub use tor::Tor;
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
||||
mod http;
|
||||
|
||||
+174
-87
@@ -20,7 +20,8 @@ use curve25519_dalek::digest::Digest;
|
||||
use ed25519_dalek::hazmat::ExpandedSecretKey;
|
||||
use fs_mistrust::Mistrust;
|
||||
use grin_util::secp::SecretKey;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use http_body_util::{BodyExt, Empty, Full};
|
||||
use hyper_util::rt::TokioIo;
|
||||
use lazy_static::lazy_static;
|
||||
use log::error;
|
||||
use parking_lot::RwLock;
|
||||
@@ -28,13 +29,10 @@ use safelog::DisplayRedacted;
|
||||
use sha2::Sha512;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::{fs, thread};
|
||||
use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder};
|
||||
use tls_api_native_tls::TlsConnector;
|
||||
use tor_hscrypto::pk::{HsIdKey, HsIdKeypair};
|
||||
use tor_hsrproxy::OnionServiceReverseProxy;
|
||||
use tor_hsrproxy::config::{
|
||||
@@ -46,12 +44,12 @@ use tor_hsservice::{
|
||||
};
|
||||
use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector};
|
||||
use tor_llcrypto::pk::ed25519::ExpandedKeypair;
|
||||
use tor_rtcompat::SpawnExt;
|
||||
use tor_rtcompat::tokio::TokioNativeTlsRuntime;
|
||||
use tor_rtcompat::{SleepProviderExt, SpawnExt, ToplevelBlockOn};
|
||||
|
||||
use crate::http::HttpClient;
|
||||
use crate::tor::http::ArtiHttpConnector;
|
||||
use crate::tor::{TorBridge, TorConfig, TorProxy};
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
lazy_static! {
|
||||
/// Static thread-aware state of Tor to be updated from separate thread.
|
||||
@@ -62,26 +60,16 @@ lazy_static! {
|
||||
pub struct Tor {
|
||||
runtime: TokioNativeTlsRuntime,
|
||||
/// Tor client and config.
|
||||
client_config: Arc<RwLock<Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)>>>,
|
||||
client_config: Arc<RwLock<Option<(Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig)>>>,
|
||||
/// Flag to check if client is launching.
|
||||
client_launching: Arc<AtomicBool>,
|
||||
|
||||
/// Mapping of running Onion services identifiers to proxy.
|
||||
run: Arc<
|
||||
RwLock<
|
||||
BTreeMap<
|
||||
String,
|
||||
(
|
||||
u16,
|
||||
SecretKey,
|
||||
Arc<RunningOnionService>,
|
||||
Arc<OnionServiceReverseProxy>,
|
||||
),
|
||||
>,
|
||||
>,
|
||||
RwLock<BTreeMap<String, (u16, Arc<RunningOnionService>, Arc<OnionServiceReverseProxy>)>>,
|
||||
>,
|
||||
/// Mapping of starting Onion services identifiers.
|
||||
start: Arc<RwLock<BTreeMap<String, (u16, SecretKey)>>>,
|
||||
/// Mapping of starting Onion services identifiers to port.
|
||||
start: Arc<RwLock<BTreeMap<String, u16>>>,
|
||||
/// Failed Onion services identifiers.
|
||||
fail: Arc<RwLock<BTreeSet<String>>>,
|
||||
/// Checking Onion services identifiers.
|
||||
@@ -141,7 +129,9 @@ impl Tor {
|
||||
}
|
||||
|
||||
/// Build bootstrapped client from provided config.
|
||||
fn build_client_bootstrap(config: TorClientConfig) -> Option<TorClient<TokioNativeTlsRuntime>> {
|
||||
fn build_client_bootstrap(
|
||||
config: TorClientConfig,
|
||||
) -> Option<Arc<TorClient<TokioNativeTlsRuntime>>> {
|
||||
let client_res = TorClient::with_runtime(TOR_STATE.runtime.clone())
|
||||
.config(config.clone())
|
||||
.create_unbootstrapped();
|
||||
@@ -315,35 +305,84 @@ impl Tor {
|
||||
error!("Tor: client not launched");
|
||||
return None;
|
||||
}
|
||||
// Create http tor-powered client to post data.
|
||||
let client = Self::client_config().unwrap().0.isolated_client();
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
// Create request.
|
||||
let req = hyper_tor::Request::builder()
|
||||
.method(hyper_tor::Method::POST)
|
||||
.uri(url)
|
||||
.body(hyper_tor::Body::from(body))
|
||||
.unwrap();
|
||||
// Send request.
|
||||
let mut resp = None;
|
||||
match http.request(req).await {
|
||||
Ok(r) => match hyper_tor::body::to_bytes(r).await {
|
||||
Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()),
|
||||
Err(e) => {
|
||||
error!("Tor: POST response parse error: {}", e);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("Tor: POST failed: {}", e);
|
||||
}
|
||||
let uri = if let Ok(url) = url.parse::<hyper::Uri>() {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if uri.is_none() {
|
||||
error!("Tor: bad URL {}", url);
|
||||
return None;
|
||||
}
|
||||
resp
|
||||
let uri = uri.unwrap();
|
||||
thread::spawn(move || {
|
||||
let client = Self::client_config().unwrap().0.isolated_client();
|
||||
let c = client.clone();
|
||||
client
|
||||
.runtime()
|
||||
.block_on(async move {
|
||||
let res = c
|
||||
.runtime()
|
||||
.timeout(Duration::from_millis(600000), async {
|
||||
if let Ok(stream) = c
|
||||
.connect((uri.host().unwrap(), uri.port_u16().unwrap_or(80)))
|
||||
.await
|
||||
{
|
||||
if let Ok((mut request_sender, connection)) =
|
||||
hyper::client::conn::http1::handshake(TokioIo::new(stream))
|
||||
.await
|
||||
{
|
||||
// Spawn a task to poll the connection and drive the HTTP state.
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
error!("Tor connection error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let req = hyper::Request::builder()
|
||||
.uri(uri)
|
||||
.method("POST")
|
||||
.body::<Full<Bytes>>(Full::from(body))
|
||||
.ok();
|
||||
if req.is_none() {
|
||||
return None;
|
||||
}
|
||||
let req = req.unwrap();
|
||||
let resp = request_sender.send_request(req).await.ok();
|
||||
if resp.is_none() {
|
||||
return None;
|
||||
}
|
||||
let resp = resp.unwrap();
|
||||
let body_resp = resp.into_body().collect().await.ok();
|
||||
if body_resp.is_none() {
|
||||
return None;
|
||||
}
|
||||
let body_resp = body_resp.unwrap();
|
||||
let body = body_resp.to_bytes().into();
|
||||
if let Ok(body_text) = String::from_utf8(body) {
|
||||
return Some(body_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.await;
|
||||
match res {
|
||||
Err(e) => {
|
||||
error!("Tor request error: {}", e);
|
||||
None
|
||||
}
|
||||
Ok(body) => Some(body),
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
.join()
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn client_config() -> Option<(TorClient<TokioNativeTlsRuntime>, TorClientConfig)> {
|
||||
fn client_config() -> Option<(Arc<TorClient<TokioNativeTlsRuntime>>, TorClientConfig)> {
|
||||
let r_client_config = TOR_STATE.client_config.read();
|
||||
r_client_config.clone()
|
||||
}
|
||||
@@ -393,7 +432,7 @@ impl Tor {
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
};
|
||||
let mut services: BTreeMap<String, (u16, SecretKey)> = TOR_STATE.start.read().clone();
|
||||
let mut services: BTreeMap<String, u16> = TOR_STATE.start.read().clone();
|
||||
for id in service_ids.clone() {
|
||||
if let Some(res) = Self::stop_service(&id) {
|
||||
services.insert(id, res);
|
||||
@@ -424,14 +463,14 @@ impl Tor {
|
||||
}
|
||||
// Start services.
|
||||
for id in services.keys() {
|
||||
let (port, key) = services.get(id).unwrap();
|
||||
Self::start_service(port.clone(), key.clone(), &id);
|
||||
let port = services.get(id).unwrap();
|
||||
Self::start_service(port.clone(), None, &id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stop running Onion service returning port and key.
|
||||
pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> {
|
||||
let mut port_key = None;
|
||||
/// Stop running Onion service returning port.
|
||||
pub fn stop_service(id: &String) -> Option<u16> {
|
||||
let mut port = None;
|
||||
{
|
||||
// Remove service from checking.
|
||||
let mut w_services = TOR_STATE.check.write();
|
||||
@@ -440,18 +479,18 @@ impl Tor {
|
||||
// Remove service from starting.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
if let Some((port, key)) = w_services.remove(id) {
|
||||
port_key = Some((port, key));
|
||||
if let Some(p) = w_services.remove(id) {
|
||||
port = Some(p);
|
||||
}
|
||||
}
|
||||
// Remove service from running.
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
if let Some((port, key, svc, proxy)) = w_services.remove(id) {
|
||||
if let Some((p, svc, proxy)) = w_services.remove(id) {
|
||||
proxy.shutdown();
|
||||
drop(proxy);
|
||||
drop(svc);
|
||||
port_key = Some((port, key));
|
||||
port = Some(p);
|
||||
}
|
||||
}
|
||||
// Remove client when no running services left.
|
||||
@@ -461,26 +500,31 @@ impl Tor {
|
||||
// Clear state.
|
||||
fs::remove_dir_all(TorConfig::state_path()).unwrap_or_default();
|
||||
}
|
||||
port_key
|
||||
port
|
||||
}
|
||||
|
||||
/// Start Onion service from listening local port and [`SecretKey`].
|
||||
pub fn start_service(port: u16, key: SecretKey, id: &String) {
|
||||
pub fn start_service(port: u16, wallet: Option<&Wallet>, id: &String) {
|
||||
// Check if service is already running.
|
||||
if Self::is_service_running(id) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
// Save starting service.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.insert(id.clone(), port);
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(id);
|
||||
}
|
||||
// Retrieve key from wallet if needed.
|
||||
let key = if let Some(w) = wallet {
|
||||
w.retrieve_secret_key().ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let service_id = id.clone();
|
||||
thread::spawn(move || {
|
||||
{
|
||||
// Save starting service.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.insert(service_id.clone(), (port, key.clone()));
|
||||
// Remove service from failed.
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
|
||||
let on_error = |service_id: String| {
|
||||
// Remove service from starting.
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
@@ -515,16 +559,19 @@ impl Tor {
|
||||
return;
|
||||
}
|
||||
let (client, config) = client_config.unwrap();
|
||||
let hs = HsNickname::new(service_id.clone()).unwrap();
|
||||
|
||||
// Add service key to keystore if provided.
|
||||
if let Some(key) = key {
|
||||
if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Launch Onion service.
|
||||
client
|
||||
.runtime()
|
||||
.spawn(async move {
|
||||
// Add service key to keystore.
|
||||
let hs = HsNickname::new(service_id.clone()).unwrap();
|
||||
if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) {
|
||||
on_error(service_id);
|
||||
return;
|
||||
}
|
||||
// Launch Onion service.
|
||||
let service_config = OnionServiceConfigBuilder::default()
|
||||
.nickname(hs.clone())
|
||||
.build()
|
||||
@@ -545,13 +592,18 @@ impl Tor {
|
||||
{
|
||||
let mut w_services = TOR_STATE.run.write();
|
||||
let id = service_id.clone();
|
||||
w_services.insert(id, (port, key.clone(), service, proxy));
|
||||
w_services.insert(id, (port, service, proxy));
|
||||
}
|
||||
// Remove service from starting.
|
||||
{
|
||||
let mut w_services = TOR_STATE.start.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
// Remove service from failed.
|
||||
{
|
||||
let mut w_services = TOR_STATE.fail.write();
|
||||
w_services.remove(&service_id);
|
||||
}
|
||||
// Check service availability.
|
||||
let addr = onion_addr.unwrap().display_unredacted().to_string();
|
||||
let url = format!("http://{}/", addr);
|
||||
@@ -600,17 +652,51 @@ impl Tor {
|
||||
}
|
||||
let duration = {
|
||||
// Send request.
|
||||
let tls_conn = TlsConnector::builder().unwrap().build().unwrap();
|
||||
let client_config = Self::client_config();
|
||||
if client_config.is_none() {
|
||||
return;
|
||||
}
|
||||
let uri = if let Ok(url) = url.parse::<hyper::Uri>() {
|
||||
Some(url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if uri.is_none() {
|
||||
return;
|
||||
}
|
||||
let uri = uri.unwrap();
|
||||
let client = client_config.unwrap().0.isolated_client();
|
||||
let conn = ArtiHttpConnector::new(client, tls_conn);
|
||||
let http =
|
||||
hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn);
|
||||
let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap();
|
||||
let check = http.get(uri.clone());
|
||||
|
||||
// Setup check request.
|
||||
let check = || async {
|
||||
if let Ok(stream) = client
|
||||
.connect((uri.host().unwrap(), uri.port_u16().unwrap_or(80)))
|
||||
.await
|
||||
{
|
||||
if let Ok((mut request_sender, connection)) =
|
||||
hyper::client::conn::http1::handshake(TokioIo::new(stream))
|
||||
.await
|
||||
{
|
||||
// Spawn a task to poll the connection and drive the HTTP state.
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = connection.await {
|
||||
error!("Tor connection error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
let req = hyper::Request::builder()
|
||||
.uri(uri)
|
||||
.body(Empty::<Bytes>::new())
|
||||
.ok();
|
||||
if let Some(req) = req {
|
||||
let res = request_sender.send_request(req).await;
|
||||
return Some(res);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
};
|
||||
|
||||
// Setup error callback.
|
||||
let mut on_error = |service_id: &String| -> bool {
|
||||
if !Self::check_running(service_id) {
|
||||
@@ -631,10 +717,11 @@ impl Tor {
|
||||
max_errors
|
||||
};
|
||||
// Check with timeout of 30s.
|
||||
match tokio::time::timeout(Duration::from_millis(30000), check).await {
|
||||
match tokio::time::timeout(Duration::from_millis(30000), check()).await
|
||||
{
|
||||
Ok(resp) => {
|
||||
match resp {
|
||||
Ok(_) => {
|
||||
Some(_) => {
|
||||
if !Self::check_running(&service_id) {
|
||||
break;
|
||||
}
|
||||
@@ -642,13 +729,13 @@ impl Tor {
|
||||
// Check again after 60s.
|
||||
Duration::from_millis(60000)
|
||||
}
|
||||
Err(e) => {
|
||||
None => {
|
||||
if on_error(&service_id) {
|
||||
break;
|
||||
}
|
||||
error!(
|
||||
"Tor check failed: {} for {}, errors: {}/{}",
|
||||
e, service_id, errors_count, MAX_ERRORS
|
||||
"Tor check failed for {}, errors: {}/{}",
|
||||
service_id, errors_count, MAX_ERRORS
|
||||
);
|
||||
// Check again after 5s.
|
||||
Duration::from_millis(5000)
|
||||
@@ -677,7 +764,7 @@ impl Tor {
|
||||
|
||||
/// Launch Onion service proxy.
|
||||
async fn run_service_proxy<S>(
|
||||
client: TorClient<TokioNativeTlsRuntime>,
|
||||
client: Arc<TorClient<TokioNativeTlsRuntime>>,
|
||||
addr: SocketAddr,
|
||||
request: S,
|
||||
nickname: HsNickname,
|
||||
|
||||
+11
-18
@@ -213,8 +213,6 @@ pub enum WalletTxAction {
|
||||
pub struct WalletTx {
|
||||
/// Information from database.
|
||||
pub data: TxLogEntry,
|
||||
/// State of transaction Slate.
|
||||
pub state: SlateState,
|
||||
/// Payment proof.
|
||||
pub(crate) proof: Option<PaymentProof>,
|
||||
|
||||
@@ -240,7 +238,6 @@ impl WalletTx {
|
||||
pub fn new(
|
||||
tx: TxLogEntry,
|
||||
proof: Option<PaymentProof>,
|
||||
wallet: &Wallet,
|
||||
height: Option<u64>,
|
||||
broadcasting_height: Option<u64>,
|
||||
action: Option<WalletTxAction>,
|
||||
@@ -263,9 +260,8 @@ impl WalletTx {
|
||||
sender = Some(addr);
|
||||
}
|
||||
}
|
||||
let mut t = Self {
|
||||
let t = Self {
|
||||
data: tx,
|
||||
state: SlateState::Unknown,
|
||||
proof,
|
||||
amount,
|
||||
receiver,
|
||||
@@ -275,15 +271,6 @@ impl WalletTx {
|
||||
action,
|
||||
action_error,
|
||||
};
|
||||
// Update Slate state for unconfirmed.
|
||||
if !t.data.confirmed
|
||||
&& (t.data.tx_type == TxLogEntryType::TxSent
|
||||
|| t.data.tx_type == TxLogEntryType::TxReceived)
|
||||
{
|
||||
if let Some(slate_id) = t.data.tx_slate_id {
|
||||
t.state = wallet.get_slate_state(slate_id, &t.data.tx_type)
|
||||
}
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
@@ -294,15 +281,16 @@ impl WalletTx {
|
||||
&& (!self.sending_tor() || self.action_error.is_some())
|
||||
&& (self.data.tx_type == TxLogEntryType::TxSent
|
||||
|| self.data.tx_type == TxLogEntryType::TxReceived)
|
||||
&& (self.state == SlateState::Invoice1 || self.state == SlateState::Standard1)
|
||||
&& (self.data.tx_slate_state == Some(SlateState::Invoice1)
|
||||
|| self.data.tx_slate_state == Some(SlateState::Standard1))
|
||||
}
|
||||
|
||||
/// Check if transaction was finalized.
|
||||
pub fn finalized(&self) -> bool {
|
||||
(self.data.tx_type == TxLogEntryType::TxSent
|
||||
|| self.data.tx_type == TxLogEntryType::TxReceived)
|
||||
&& self.state == SlateState::Invoice3
|
||||
|| self.state == SlateState::Standard3
|
||||
&& self.data.tx_slate_state == Some(SlateState::Invoice3)
|
||||
|| self.data.tx_slate_state == Some(SlateState::Standard3)
|
||||
}
|
||||
|
||||
/// Check if transaction is sending over Tor.
|
||||
@@ -418,9 +406,12 @@ pub enum WalletTask {
|
||||
/// * tx
|
||||
/// * receiver
|
||||
SendTor(TxLogEntry, SlatepackAddress),
|
||||
/// Finalize over Tor.
|
||||
/// * tx
|
||||
FinalizeTor(TxLogEntry),
|
||||
/// Invoice creation.
|
||||
/// * amount
|
||||
Receive(u64),
|
||||
Receive(u64, Option<SlatepackAddress>),
|
||||
/// Transaction finalization.
|
||||
/// * tx id
|
||||
Finalize(u32),
|
||||
@@ -433,4 +424,6 @@ pub enum WalletTask {
|
||||
/// Delete transaction.
|
||||
/// * tx id
|
||||
Delete(u32),
|
||||
/// Start Tor service.
|
||||
StartTor,
|
||||
}
|
||||
|
||||
+173
-131
@@ -39,6 +39,7 @@ use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient};
|
||||
use grin_wallet_libwallet::api_impl::owner::{
|
||||
cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs, verify_payment_proof,
|
||||
};
|
||||
use grin_wallet_libwallet::api_impl::types::update_tx_slate_state;
|
||||
use grin_wallet_libwallet::{
|
||||
Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState,
|
||||
SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType,
|
||||
@@ -113,8 +114,6 @@ pub struct Wallet {
|
||||
|
||||
/// Running wallet foreign API server and port.
|
||||
foreign_api_server: Arc<RwLock<Option<(ApiServer, u16)>>>,
|
||||
/// Wallet secret key for transport service.
|
||||
secret_key: Arc<RwLock<Option<SecretKey>>>,
|
||||
|
||||
/// Flag to check if wallet repairing and restoring missing outputs is needed.
|
||||
repair_needed: Arc<AtomicBool>,
|
||||
@@ -169,7 +168,6 @@ impl Wallet {
|
||||
closing: Arc::new(AtomicBool::new(false)),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
foreign_api_server: Arc::new(RwLock::new(None)),
|
||||
secret_key: Arc::new(RwLock::new(None)),
|
||||
repair_needed: Arc::new(AtomicBool::new(false)),
|
||||
repair_progress: Arc::new(AtomicU8::new(0)),
|
||||
files_moving: Arc::new(AtomicBool::new(false)),
|
||||
@@ -238,7 +236,8 @@ impl Wallet {
|
||||
/// Create [`HTTPNodeClient`] from provided config.
|
||||
fn create_node_client(config: &WalletConfig) -> Result<HTTPNodeClient, Error> {
|
||||
let integrated = || {
|
||||
let api_url = format!("http://{}", NodeConfig::get_api_address());
|
||||
let (api_address, api_port) = NodeConfig::get_api_address();
|
||||
let api_url = format!("http://{}:{}", api_address, api_port);
|
||||
let api_secret = NodeConfig::get_api_secret(true);
|
||||
(api_url, api_secret)
|
||||
};
|
||||
@@ -391,8 +390,8 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
// Update Slatepack address and secret key.
|
||||
self.update_secret_key_addr()?;
|
||||
// Update Slatepack address.
|
||||
self.update_slatepack_addr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -403,14 +402,17 @@ impl Wallet {
|
||||
r_key.clone()
|
||||
}
|
||||
|
||||
/// Get wallet [`SecretKey`] for transport.
|
||||
pub fn secret_key(&self) -> Option<SecretKey> {
|
||||
let r_key = self.secret_key.read();
|
||||
r_key.clone()
|
||||
/// Retrieve wallet Slatepack address for transport.
|
||||
fn update_slatepack_addr(&self) -> Result<(), Error> {
|
||||
let sec_key = self.retrieve_secret_key()?;
|
||||
let addr = SlatepackAddress::try_from(&sec_key)?;
|
||||
let mut w_address = self.slatepack_address.write();
|
||||
*w_address = Some(addr.to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve wallet [`SecretKey`] and Slatepack address for transport.
|
||||
fn update_secret_key_addr(&self) -> Result<(), Error> {
|
||||
/// Retrieve wallet [`SecretKey`] for transport.
|
||||
pub fn retrieve_secret_key(&self) -> Result<SecretKey, Error> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let mut w_lock = instance.lock();
|
||||
@@ -420,12 +422,7 @@ impl Wallet {
|
||||
let parent_key_id = w_inst.parent_key_id();
|
||||
let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0)
|
||||
.map_err(|e| Error::TorConfig(format!("{:?}", e)))?;
|
||||
let addr = SlatepackAddress::try_from(&sec_key)?;
|
||||
let mut w_key = self.secret_key.write();
|
||||
*w_key = Some(sec_key);
|
||||
let mut w_address = self.slatepack_address.write();
|
||||
*w_address = Some(addr.to_string());
|
||||
Ok(())
|
||||
Ok(sec_key)
|
||||
}
|
||||
|
||||
/// Get unique opened wallet identifier, including current account.
|
||||
@@ -667,6 +664,8 @@ impl Wallet {
|
||||
// Retrieve txs from database.
|
||||
let txs: Vec<TxLogEntry> = w
|
||||
.tx_log_iter()?
|
||||
.filter(|tx| tx.is_ok())
|
||||
.map(|tx| tx.unwrap())
|
||||
.filter(|tx_entry| tx_entry.parent_key_id == parent_key_id)
|
||||
// Filter transactions to not show txs without slate (usually unspent outputs).
|
||||
.filter(|tx| {
|
||||
@@ -709,6 +708,8 @@ impl Wallet {
|
||||
let parent_key_id = w.parent_key_id();
|
||||
// Retrieve txs from database.
|
||||
w.tx_log_iter()?
|
||||
.filter(|tx| tx.is_ok())
|
||||
.map(|tx| tx.unwrap())
|
||||
.filter(|tx_entry| tx_entry.parent_key_id == parent_key_id)
|
||||
.filter(|tx_entry| {
|
||||
if tx_entry.tx_type == TxLogEntryType::TxSent
|
||||
@@ -786,12 +787,6 @@ impl Wallet {
|
||||
let cur_service_id = self.identifier();
|
||||
Tor::stop_service(&cur_service_id);
|
||||
|
||||
// Clear secret key for previous account.
|
||||
{
|
||||
let mut w_key = self.secret_key.write();
|
||||
*w_key = None;
|
||||
}
|
||||
|
||||
// Set new active account.
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
@@ -808,8 +803,8 @@ impl Wallet {
|
||||
},
|
||||
)?;
|
||||
|
||||
// Update Slatepack address and secret key.
|
||||
self.update_secret_key_addr()?;
|
||||
// Update Slatepack address.
|
||||
self.update_slatepack_addr()?;
|
||||
|
||||
// Save account label into config.
|
||||
let mut w_config = self.config.write();
|
||||
@@ -942,7 +937,7 @@ impl Wallet {
|
||||
fn create_slatepack_message(
|
||||
&self,
|
||||
slate: &Slate,
|
||||
_: Option<SlatepackAddress>,
|
||||
address: Option<SlatepackAddress>,
|
||||
) -> Result<String, Error> {
|
||||
let mut message = "".to_string();
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
@@ -953,11 +948,11 @@ impl Wallet {
|
||||
self.keychain_mask().as_ref(),
|
||||
Some(&mut api),
|
||||
|api, m| {
|
||||
// let recipients = match dest {
|
||||
// Some(a) => vec![a],
|
||||
// None => vec![],
|
||||
// };
|
||||
message = api.create_slatepack_message(m, &slate, Some(0), vec![])?;
|
||||
let addrs = match address {
|
||||
Some(a) => vec![a],
|
||||
None => vec![],
|
||||
};
|
||||
message = api.create_slatepack_message(m, &slate, Some(0), addrs)?;
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
@@ -976,40 +971,6 @@ impl Wallet {
|
||||
fs::exists(slatepack_path).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get possible state from tx type.
|
||||
pub fn get_slate_state(&self, slate_id: Uuid, tx_type: &TxLogEntryType) -> SlateState {
|
||||
let mut slate = Slate::blank(1, false);
|
||||
slate.id = slate_id;
|
||||
slate.state = match tx_type {
|
||||
TxLogEntryType::TxReceived => SlateState::Invoice3,
|
||||
_ => SlateState::Standard3,
|
||||
};
|
||||
// Transaction was finalized.
|
||||
if self.slatepack_exists(&slate) {
|
||||
slate.state
|
||||
} else {
|
||||
slate.state = match tx_type {
|
||||
TxLogEntryType::TxReceived => SlateState::Standard2,
|
||||
_ => SlateState::Invoice2,
|
||||
};
|
||||
// Transaction signed to be finalized.
|
||||
if self.slatepack_exists(&slate) {
|
||||
slate.state
|
||||
} else {
|
||||
// Transaction just was created.
|
||||
slate.state = match tx_type {
|
||||
TxLogEntryType::TxReceived => SlateState::Invoice1,
|
||||
_ => SlateState::Standard1,
|
||||
};
|
||||
if self.slatepack_exists(&slate) {
|
||||
slate.state
|
||||
} else {
|
||||
SlateState::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate transaction fee for provided amount.
|
||||
fn calculate_fee(&self, a: u64) -> Result<u64, Error> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
@@ -1063,7 +1024,7 @@ impl Wallet {
|
||||
controller::owner_single_use(None, keychain_mask.as_ref(), Some(&mut api), |api, m| {
|
||||
let s = api.init_send_tx(m, args)?;
|
||||
// Create Slatepack message response.
|
||||
let _ = self.create_slatepack_message(&s, dest)?;
|
||||
let _ = self.create_slatepack_message(&s, None)?;
|
||||
// Lock outputs to for this transaction.
|
||||
api.tx_lock_outputs(m, &s)?;
|
||||
slate = Some(s);
|
||||
@@ -1076,27 +1037,47 @@ impl Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Send slate to Tor address.
|
||||
async fn send_tor(&self, id: u32, s: &Slate, addr: &SlatepackAddress) -> Result<Slate, Error> {
|
||||
/// Send slate to Tor address. When `finalize` is true, posts the slate to the
|
||||
/// peer's foreign-api `finalize_tx` (used by the invoice-flow payer to push the
|
||||
/// signed Invoice2 back to the merchant for broadcast); otherwise posts to
|
||||
/// `receive_tx` (standard send flow).
|
||||
async fn send_tor(
|
||||
&self,
|
||||
id: u32,
|
||||
s: &Slate,
|
||||
addr: &SlatepackAddress,
|
||||
finalize: bool,
|
||||
) -> Result<Slate, Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::SendingTor));
|
||||
|
||||
let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str();
|
||||
let url = format!("{}/v2/foreign", tor_addr);
|
||||
let slate_send = VersionedSlate::into_version(s.clone(), SlateVersion::V4)?;
|
||||
let body = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "receive_tx",
|
||||
"id": 1,
|
||||
"params": [
|
||||
slate_send,
|
||||
null,
|
||||
null
|
||||
]
|
||||
})
|
||||
let body = if finalize {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "finalize_tx",
|
||||
"id": 1,
|
||||
"params": [slate_send]
|
||||
})
|
||||
} else {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "receive_tx",
|
||||
"id": 1,
|
||||
"params": [
|
||||
slate_send,
|
||||
null,
|
||||
null
|
||||
]
|
||||
})
|
||||
}
|
||||
.to_string();
|
||||
// Wait Tor service to launch.
|
||||
while Tor::is_service_starting(&self.identifier()) {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
while !Tor::is_service_running(&self.identifier()) {
|
||||
return Err(Error::GenericError(
|
||||
"Tor service is not running".to_string(),
|
||||
));
|
||||
}
|
||||
// Send request to receiver.
|
||||
let req_res = Tor::post(body, url).await;
|
||||
@@ -1124,7 +1105,11 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Initialize an invoice transaction to receive amount, return request for funds sender.
|
||||
fn issue_invoice(&self, amount: u64) -> Result<Slate, Error> {
|
||||
fn issue_invoice(
|
||||
&self,
|
||||
amount: u64,
|
||||
address: Option<SlatepackAddress>,
|
||||
) -> Result<Slate, Error> {
|
||||
let args = IssueInvoiceTxArgs {
|
||||
dest_acct_name: None,
|
||||
amount,
|
||||
@@ -1136,13 +1121,13 @@ impl Wallet {
|
||||
let slate = api.issue_invoice_tx(self.keychain_mask().as_ref(), args)?;
|
||||
|
||||
// Create Slatepack message response.
|
||||
let _ = self.create_slatepack_message(&slate, None)?;
|
||||
let _ = self.create_slatepack_message(&slate, address)?;
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
|
||||
/// Handle message from the invoice issuer to send founds, return response for funds receiver.
|
||||
fn pay(&self, slate: &Slate) -> Result<Slate, Error> {
|
||||
fn pay(&self, slate: &Slate, dest: Option<SlatepackAddress>) -> Result<Slate, Error> {
|
||||
let config = self.get_config();
|
||||
let args = InitTxArgs {
|
||||
src_acct_name: None,
|
||||
@@ -1157,8 +1142,9 @@ impl Wallet {
|
||||
let slate = api.process_invoice_tx(self.keychain_mask().as_ref(), &slate, args)?;
|
||||
api.tx_lock_outputs(self.keychain_mask().as_ref(), &slate)?;
|
||||
|
||||
// Create Slatepack message response.
|
||||
let _ = self.create_slatepack_message(&slate, None)?;
|
||||
// Create Slatepack message response (kept as on-disk paste-back fallback even
|
||||
// when an auto-Tor reply is attempted by the caller).
|
||||
let _ = self.create_slatepack_message(&slate, dest)?;
|
||||
|
||||
Ok(slate)
|
||||
}
|
||||
@@ -1316,20 +1302,33 @@ impl Wallet {
|
||||
}
|
||||
|
||||
/// Get stored transaction Slate.
|
||||
fn get_tx_slate(&self, tx_id: u32) -> Option<Slate> {
|
||||
fn get_tx_slate(&self, tx_id: u32) -> Option<(Slate, Option<SlatepackAddress>)> {
|
||||
if let Some(tx) = self.retrieve_tx_by_id(Some(tx_id), None) {
|
||||
if let Some(slate_id) = tx.tx_slate_id {
|
||||
let slate_state = self.get_slate_state(slate_id, &tx.tx_type);
|
||||
let slatepack_path = self.get_config().get_slate_path(slate_id, &slate_state);
|
||||
let msg = fs::read_to_string(slatepack_path).unwrap_or("".to_string());
|
||||
if let Ok((slate, _)) = self.parse_slatepack(&msg) {
|
||||
return Some(slate);
|
||||
if let Some(slate_state) = tx.tx_slate_state {
|
||||
let slatepack_path = self.get_config().get_slate_path(slate_id, &slate_state);
|
||||
let msg = fs::read_to_string(slatepack_path).unwrap_or("".to_string());
|
||||
if let Ok((slate, dest)) = self.parse_slatepack(&msg) {
|
||||
return Some((slate, dest));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Update transaction slate state.
|
||||
fn update_slate_state(&self, slate: &Slate) -> Result<(), Error> {
|
||||
let r_inst = self.instance.as_ref().read();
|
||||
let instance = r_inst.clone().unwrap();
|
||||
let mut w_lock = instance.lock();
|
||||
let w = w_lock.lc_provider()?.wallet_inst()?;
|
||||
let keychain_mask = self.keychain_mask();
|
||||
let parent_key = w.parent_key_id();
|
||||
update_tx_slate_state(w, keychain_mask.as_ref(), &parent_key, slate)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete transaction from database.
|
||||
fn delete_tx(&self, id: u32) -> Result<(), Error> {
|
||||
self.on_tx_action(id, Some(WalletTxAction::Deleting));
|
||||
@@ -1347,7 +1346,7 @@ impl Wallet {
|
||||
batch.commit()?;
|
||||
|
||||
// Delete transaction files.
|
||||
if let Some(s) = slate {
|
||||
if let Some((s, _)) = slate {
|
||||
let slatepack_path = self.get_config().get_slate_path(s.id, &s.state);
|
||||
fs::remove_file(&slatepack_path).unwrap_or_default();
|
||||
let path = path::Path::new(&self.get_config().get_data_path())
|
||||
@@ -1491,6 +1490,8 @@ impl Wallet {
|
||||
// Find wallet transaction to update or create.
|
||||
let txs = w
|
||||
.tx_log_iter()?
|
||||
.filter(|tx| tx.is_ok())
|
||||
.map(|tx| tx.unwrap())
|
||||
.filter(|entry| {
|
||||
if let Some(excess) = entry.kernel_excess {
|
||||
return excess == proof.excess;
|
||||
@@ -1702,18 +1703,14 @@ fn start_sync(wallet: Wallet) -> Thread {
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Start unfailed Tor service if API server is running.
|
||||
let service_id = wallet.identifier();
|
||||
if wallet.auto_start_tor_listener()
|
||||
&& api_server_running
|
||||
&& !Tor::is_service_failed(&service_id)
|
||||
{
|
||||
let r_foreign_api = wallet.foreign_api_server.read();
|
||||
let api = r_foreign_api.as_ref().unwrap();
|
||||
if let Some(key) = wallet.secret_key() {
|
||||
Tor::start_service(api.1, key, &wallet.identifier());
|
||||
// Start unfailed Tor service if API server is running.
|
||||
let service_id = wallet.identifier();
|
||||
if wallet.auto_start_tor_listener()
|
||||
&& api_server_running && !Tor::is_service_failed(&service_id)
|
||||
{
|
||||
let r_foreign_api = wallet.foreign_api_server.read();
|
||||
let api = r_foreign_api.as_ref().unwrap();
|
||||
Tor::start_service(api.1, Some(&wallet), &service_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1751,8 +1748,9 @@ fn start_sync(wallet: Wallet) -> Thread {
|
||||
|
||||
/// Handle wallet task.
|
||||
async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
// Send amount over Tor.
|
||||
let send_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| match w
|
||||
.send_tor(tx.id, &s, r)
|
||||
.send_tor(tx.id, &s, r, false)
|
||||
.await
|
||||
{
|
||||
Ok(s) => match w.finalize(&s, tx.id) {
|
||||
@@ -1762,17 +1760,35 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("send tor post error: {:?}", e);
|
||||
error!("Send Tor post error: {:?}", e);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("send tor finalize error: {:?}", e);
|
||||
error!("Send Tor finalize error: {:?}", e);
|
||||
w.task(WalletTask::Cancel(tx.id));
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
error!("send tor error: {:?}", e);
|
||||
error!("Send Tor error: {:?}", e);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
};
|
||||
// Finalize tx over Tor.
|
||||
let finalize_tor = async |tx: TxLogEntry, s: &Slate, r: &SlatepackAddress| match w
|
||||
.send_tor(tx.id, &s, r, true)
|
||||
.await
|
||||
{
|
||||
Ok(s) => {
|
||||
w.on_tx_action(tx.id, None);
|
||||
let _ = w.update_slate_state(&s);
|
||||
let _ = w.create_slatepack_message(&s, None);
|
||||
sync_wallet_data(&w, false);
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Finalize Tor error: {:?}", e);
|
||||
w.on_tx_error(tx.id, Some(e));
|
||||
w.on_task_result(Some(tx), &t);
|
||||
}
|
||||
@@ -1808,21 +1824,34 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.message_opening.store(false, Ordering::Relaxed);
|
||||
return;
|
||||
}
|
||||
// Finalize over Tor if service is running.
|
||||
let maybe_finalize_tor = async |s: Slate| {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
let id = w.identifier();
|
||||
if Tor::is_service_running(&id) {
|
||||
if let Some(tx) = tx.as_ref() {
|
||||
if let Some(addr) = dest {
|
||||
w.message_opening.store(false, Ordering::Relaxed);
|
||||
finalize_tor(tx.clone(), &s, &addr).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Create response or finalize.
|
||||
match s.state {
|
||||
SlateState::Standard1 | SlateState::Invoice1 => {
|
||||
if s.state != SlateState::Standard1 {
|
||||
if let Ok(_) = w.pay(&s) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
} else {
|
||||
if let Ok(_) = w.receive(&s, dest) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
SlateState::Standard1 => {
|
||||
if let Ok(s) = w.receive(&s, None) {
|
||||
maybe_finalize_tor(s).await;
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
}
|
||||
SlateState::Invoice1 => {
|
||||
if let Ok(s) = w.pay(&s, None) {
|
||||
sync_wallet_data(&w, false);
|
||||
maybe_finalize_tor(s).await;
|
||||
w.on_task_result(tx, &t);
|
||||
}
|
||||
}
|
||||
SlateState::Standard2 | SlateState::Invoice2 => {
|
||||
@@ -1878,7 +1907,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
if let Some(tx) = tx {
|
||||
if let Some(addr) = r {
|
||||
let id = w.identifier();
|
||||
if Tor::is_service_running(&id) || Tor::is_service_starting(&id) {
|
||||
if Tor::is_service_running(&id) {
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
send_tor(tx, &s, addr).await;
|
||||
return;
|
||||
@@ -1893,13 +1922,20 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.send_creating.store(false, Ordering::Relaxed);
|
||||
}
|
||||
WalletTask::SendTor(tx, r) => {
|
||||
if let Some(s) = w.get_tx_slate(tx.id) {
|
||||
send_tor(tx.clone(), &s, r).await;
|
||||
if let Some((slate, _)) = w.get_tx_slate(tx.id) {
|
||||
send_tor(tx.clone(), &slate, r).await;
|
||||
}
|
||||
}
|
||||
WalletTask::Receive(a) => {
|
||||
WalletTask::FinalizeTor(tx) => {
|
||||
if let Some((slate, dest)) = w.get_tx_slate(tx.id) {
|
||||
if let Some(dest) = dest {
|
||||
finalize_tor(tx.clone(), &slate, &dest).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
WalletTask::Receive(amount, address) => {
|
||||
w.invoice_creating.store(true, Ordering::Relaxed);
|
||||
if let Ok(s) = w.issue_invoice(*a) {
|
||||
if let Ok(s) = w.issue_invoice(*amount, address.clone()) {
|
||||
sync_wallet_data(&w, false);
|
||||
let tx = w.retrieve_tx_by_id(None, Some(s.id));
|
||||
if let Some(tx) = tx {
|
||||
@@ -1909,7 +1945,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.invoice_creating.store(false, Ordering::Relaxed);
|
||||
}
|
||||
WalletTask::Finalize(id) => {
|
||||
if let Some(s) = w.get_tx_slate(*id) {
|
||||
if let Some((s, _)) = w.get_tx_slate(*id) {
|
||||
w.on_tx_error(*id, None);
|
||||
match w.finalize(&s, *id) {
|
||||
Ok(s) => match w.post(&s, Some(*id)) {
|
||||
@@ -1932,7 +1968,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
}
|
||||
}
|
||||
WalletTask::Post(id) => {
|
||||
if let Some(s) = w.get_tx_slate(*id) {
|
||||
if let Some((s, _)) = w.get_tx_slate(*id) {
|
||||
w.on_tx_error(*id, None);
|
||||
// Cleanup broadcasting tx height.
|
||||
let tx_height_store = TxHeightStore::new(w.get_config().get_extra_db_path());
|
||||
@@ -1987,6 +2023,13 @@ async fn handle_task(w: &Wallet, t: WalletTask) {
|
||||
w.on_tx_error(*id, Some(e));
|
||||
}
|
||||
},
|
||||
WalletTask::StartTor => {
|
||||
let r_foreign_api = w.foreign_api_server.read();
|
||||
if let Some(api) = r_foreign_api.as_ref() {
|
||||
let id = w.identifier();
|
||||
Tor::start_service(api.1, Some(w), &id);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2144,7 +2187,6 @@ fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> {
|
||||
let mut new = WalletTx::new(
|
||||
tx.clone(),
|
||||
proof.clone(),
|
||||
wallet,
|
||||
height,
|
||||
broadcasting_height,
|
||||
action,
|
||||
@@ -2224,7 +2266,7 @@ fn start_api_server(wallet: &Wallet) -> Result<(ApiServer, u16), Error> {
|
||||
return match TcpListener::bind((host, port.to_owned())) {
|
||||
Ok(_) => {
|
||||
let node_p2p_port = NodeConfig::get_p2p_port();
|
||||
let node_api_port = NodeConfig::get_api_ip_port().1;
|
||||
let node_api_port = NodeConfig::get_api_address().1;
|
||||
let free =
|
||||
port.to_string() != node_p2p_port && port.to_string() != node_api_port;
|
||||
if free {
|
||||
|
||||
+1
-1
Submodule wallet updated: 8847ee5157...5c54e7cf8d
Reference in New Issue
Block a user