1
0
forked from GRIN/grim

9 Commits

Author SHA1 Message Date
ardocrat ee8841590a ci: recursive submodules 2026-06-23 15:42:43 +03:00
ardocrat 20db758bc2 wallet: update to last version, removing separate node module, added ability to finalize tx over tor 2026-06-23 15:35:17 +03:00
ardocrat 3981ebe3ed fix: repost pay tx over tor, do not try to send if tor is starting 2026-06-22 20:05:51 +03:00
ardocrat 161faba9a0 Merge remote-tracking branch 'refs/remotes/jwinterm/feat/auto-tor-reply-on-invoice-scan' 2026-06-22 18:44:01 +03:00
jwinterm 53bc6d3e3e wallet: auto-reply over Tor when scanning an Invoice1 slatepack
Mirrors the existing Standard1 + Send-task flow for the invoice
direction. When a customer scans an Invoice1 slatepack QR with an
embedded sender address (now standard for slatepacks produced by
grin-wallet's issue_invoice_tx), the patched pay() forwards the
sender address through create_slatepack_message, and the
OpenMessage handler — if the wallet's Tor service is running or
starting — pushes the signed Invoice2 to the sender's foreign-api
finalize_tx over Tor. The merchant's wallet finalizes + posts on
their side, so no local finalize/post is needed (cf. the existing
send_tor closure for Standard1 which does need that).

Backward-compatible: if the slatepack has no embedded sender
address (older clients) or the Tor service isn't up, the existing
write-slatepack-to-disk-for-paste-back fallback runs unchanged.
No protocol change, no new dependencies, no new failure modes —
the response slatepack file is always written before the Tor send
is attempted, so a Tor failure mid-flight is recoverable.

Closes the mobile-UX gap that required the customer to manually
copy the response slatepack from the wallet and paste it back to
the merchant's web/storefront interface. With this patch and a
foreign-api listener on the merchant side, scanning a Grin invoice
QR is now a single tap: scan → confirm → done.

send_tor() gains a `finalize: bool` parameter that selects between
the existing receive_tx body (for Standard1 sends) and a new
finalize_tx body (for the invoice-flow case). The same Tor SOCKS
plumbing handles both.

Real-world validation: end-to-end working today on a production
BTCPay deployment (Such Software's btcpayserver-plugin-grin v1.3.5)
— invoice QR scan with a patched build settles a merchant invoice
in ~1 confirmation window with zero customer interaction beyond
the scan.
2026-06-22 11:29:48 -04:00
ardocrat 8524084c47 wallet: ability to specify address for invoice to encrypt slatepack message 2026-06-20 15:12:43 +03:00
ardocrat a91d9016a8 node: ability to launch API, P2P and Stratum at all interfaces with IPv6 support 2026-06-19 14:46:49 +03:00
ardocrat 726a51bd0e tor: update to arti 0.43, do not store secret key, use new hyper to send requests 2026-06-15 14:53:52 +03:00
ardocrat 60d8dc7555 node + wallet: update to latest versions 2026-06-11 10:53:48 +03:00
29 changed files with 1563 additions and 1317 deletions
+1 -1
View File
@@ -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: |
+6 -6
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+17 -24
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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
+2
View File
@@ -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: Сохранить
+2
View File
@@ -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
+2
View File
@@ -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: 保存
Submodule node deleted from 386ac1ed5c
+37 -1
View File
@@ -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.
+7 -4
View File
@@ -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()),
);
+67 -31
View File
@@ -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());
}
}
+5 -4
View File
@@ -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 {
+29 -7
View File
@@ -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.
+88 -11
View File
@@ -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();
},
);
+11 -33
View File
@@ -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();
+11 -15
View File
@@ -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()));
}
});
}
+42 -38
View File
@@ -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
View File
@@ -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
View File
@@ -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 })
})
}
}
-2
View File
@@ -20,5 +20,3 @@ pub use tor::Tor;
mod types;
pub use types::*;
mod http;
+174 -87
View File
@@ -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
View File
@@ -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
View File
@@ -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