Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7b598558 | ||
|
|
69f3a355c7 |
@@ -150,7 +150,6 @@ impl RpcHandler {
|
||||
"min_steps": router.uci_get("tollgate.main.min_steps").ok().and_then(|v| v.parse::<u32>().ok()).unwrap_or(1),
|
||||
"currency": router.uci_get("tollgate.main.currency").unwrap_or_default(),
|
||||
"mint_url": router.uci_get("tollgate.main.mint_url").unwrap_or_default(),
|
||||
"payout_address":router.uci_get("tollgate.main.payout_address").unwrap_or_default(),
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({ "installed": false })
|
||||
@@ -200,15 +199,10 @@ impl RpcHandler {
|
||||
///
|
||||
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
||||
/// "price_sats": 10, "step_size_ms": 60000, "min_steps": 1,
|
||||
/// "mint_url": "<optional override>",
|
||||
/// "payout_address": "<optional Lightning address>" }`
|
||||
/// "mint_url": "<optional override>" }`
|
||||
///
|
||||
/// `mint_url` defaults to `http://<this node's IP>:3338` — the local Cashu
|
||||
/// mint that must be running as an Archy app before calling this endpoint.
|
||||
///
|
||||
/// `payout_address` sets the "owner" identity's Lightning address for
|
||||
/// TollGate's own built-in payout (see `config::apply_payout_identity`).
|
||||
/// Omitted or blank leaves whatever's already on the router untouched.
|
||||
pub(super) async fn handle_openwrt_provision_tollgate(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
@@ -246,31 +240,12 @@ impl RpcHandler {
|
||||
.unwrap_or_default();
|
||||
|
||||
let default_mint_url = format!("http://{}:{}", self.config.host_ip, LOCAL_MINT_PORT);
|
||||
// Trim trailing slash(es): tollgate-wrt matches a token's embedded
|
||||
// mint URL against this value with an exact string compare, and
|
||||
// Cashu wallets (Minibits included) encode mint URLs without a
|
||||
// trailing slash. A stray slash here means every otherwise-valid
|
||||
// token gets rejected as "untrusted mint" — confirmed live against
|
||||
// archy-x250-pa3 2026-09-07 with a manually-entered
|
||||
// "https://mint.minibits.cash/Bitcoin/".
|
||||
let mint_url = p
|
||||
.get("mint_url")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(&default_mint_url)
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
|
||||
// `None` (not sent, or sent blank) leaves whatever's already on the
|
||||
// router untouched — see apply_payout_identity's doc comment for why
|
||||
// that matters (an upstream-default placeholder otherwise survives
|
||||
// forever, since nothing else ever writes this field).
|
||||
let payout_address = p
|
||||
.get("payout_address")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string);
|
||||
|
||||
let config = TollGateConfig {
|
||||
ssid: "archipelago".to_string(),
|
||||
mint_url,
|
||||
@@ -281,7 +256,6 @@ impl RpcHandler {
|
||||
.unwrap_or(60_000),
|
||||
min_steps: p.get("min_steps").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
|
||||
enabled: p.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true),
|
||||
payout_address,
|
||||
};
|
||||
|
||||
// Blocking SSH session, and provision runs `opkg install` over it —
|
||||
|
||||
@@ -377,6 +377,23 @@ async fn write_staged_torrc(content: &str, staging: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod known_service_tests {
|
||||
use super::{is_protocol_service, known_service_port};
|
||||
|
||||
#[test]
|
||||
fn bitcoin_core_is_a_protocol_service_on_the_p2p_port() {
|
||||
// Regression: apps/bitcoin-core/manifest.yml uses id "bitcoin-core",
|
||||
// distinct from the legacy "bitcoin"/"bitcoin-knots" ids. Missing
|
||||
// here means auto-enrollment silently skips it (known_service_port
|
||||
// returns 0) and, separately, regenerate_torrc falls back to the
|
||||
// web-app HiddenServicePort-80 default instead of forwarding 8333
|
||||
// straight through.
|
||||
assert_eq!(known_service_port("bitcoin-core"), 8333);
|
||||
assert!(is_protocol_service("bitcoin-core"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod torrc_tests {
|
||||
use super::app_hidden_service_port_line;
|
||||
@@ -594,7 +611,7 @@ fn is_valid_v3_onion(s: &str) -> bool {
|
||||
pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
||||
match name {
|
||||
"archipelago" => 80,
|
||||
"bitcoin" | "bitcoin-knots" => 8333,
|
||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => 8333,
|
||||
"electrs" | "electrumx" => 50001,
|
||||
"lnd" => 8080,
|
||||
"btcpay" | "btcpay-server" | "btcpayserver" => 23000,
|
||||
@@ -619,7 +636,7 @@ pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 {
|
||||
pub(in crate::api::rpc) fn is_protocol_service(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"bitcoin" | "bitcoin-knots" | "electrs" | "electrumx" | "lnd"
|
||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" | "electrs" | "electrumx" | "lnd"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -657,9 +657,19 @@ fn apply_dynamic_metadata(app_id: &str, meta: &mut AppMetadata) {
|
||||
/// Map app_id to Tor hidden service directory name.
|
||||
/// "archipelago" is the main web UI (nginx port 80).
|
||||
/// Supports container names from deploy (archy-*, btcpay-server, etc.).
|
||||
///
|
||||
/// This must match what enrollment actually names the hidden service dir
|
||||
/// with — both the install-time auto-enroll (`install.rs`) and the manual
|
||||
/// `tor.create-service` RPC write `HiddenServiceDir` using the raw
|
||||
/// `package_id`/`name` verbatim, with no canonicalization. So `bitcoin-core`
|
||||
/// gets its own identity arm rather than folding into the "bitcoin" alias:
|
||||
/// aliasing it here without also canonicalizing the write side would point
|
||||
/// this lookup at `hidden_service_bitcoin`, which never gets created — the
|
||||
/// on-disk dir is always `hidden_service_bitcoin-core` for this app id.
|
||||
fn tor_service_name(app_id: &str) -> Option<&'static str> {
|
||||
match app_id {
|
||||
"archipelago" => Some("archipelago"),
|
||||
"bitcoin-core" => Some("bitcoin-core"),
|
||||
"bitcoin" | "bitcoin-knots" | "bitcoind" => Some("bitcoin"),
|
||||
"electrumx" | "electrs" | "electrum" => Some("electrumx"),
|
||||
"lnd" | "lnd-ui" => Some("lnd"),
|
||||
@@ -906,6 +916,28 @@ mod launch_url_port_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tor_service_name_tests {
|
||||
use super::tor_service_name;
|
||||
|
||||
#[test]
|
||||
fn bitcoin_core_resolves_to_its_own_hidden_service_dir() {
|
||||
// Regression: enrollment (install.rs, tor.create-service) writes
|
||||
// HiddenServiceDir/tor-hostnames entries using the raw package_id
|
||||
// verbatim, never canonicalized. Aliasing "bitcoin-core" to the
|
||||
// shared "bitcoin" name here would point reads at a directory
|
||||
// enrollment never creates.
|
||||
assert_eq!(tor_service_name("bitcoin-core"), Some("bitcoin-core"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_bitcoin_ids_share_the_bitcoin_alias() {
|
||||
assert_eq!(tor_service_name("bitcoin"), Some("bitcoin"));
|
||||
assert_eq!(tor_service_name("bitcoin-knots"), Some("bitcoin"));
|
||||
assert_eq!(tor_service_name("bitcoind"), Some("bitcoin"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod extract_lan_address_tests {
|
||||
use super::extract_lan_address;
|
||||
|
||||
@@ -22,46 +22,6 @@ use crate::wallet::ecash;
|
||||
///
|
||||
/// Returns the total sats swept in (0 if there was nothing to do, including
|
||||
/// when no router is configured or it doesn't have TollGate installed).
|
||||
///
|
||||
/// # KNOWN BROKEN as of 2026-09-07 — do not "fix" by adding `--json` without
|
||||
/// reading the rest of this comment first.
|
||||
///
|
||||
/// Confirmed live against archy-x250-pa3, two stacked bugs in the upstream
|
||||
/// `tollgate` CLI, not in this function:
|
||||
///
|
||||
/// 1. **This call never actually drains anything.** `tollgate wallet drain
|
||||
/// cashu` (no flags — what this function runs) prints an interactive
|
||||
/// `Are you sure? (y/N)` confirmation and reads stdin for the answer.
|
||||
/// `Router::run` executes over SSH with no PTY and empty stdin, so it
|
||||
/// always reads EOF, defaults to "N", and prints "Operation cancelled." —
|
||||
/// **with exit code 0**. The `drain_code != 0` check below can never catch
|
||||
/// this, so every single tick silently falls through to "no `Token:`
|
||||
/// lines found" → `Ok(0)`. No error, no log line (even at `warn!`), just
|
||||
/// quiet total inaction, forever. This has presumably never swept a
|
||||
/// single sat on any node.
|
||||
///
|
||||
/// 2. **The obvious fix is worse.** `tollgate --json wallet drain cashu`
|
||||
/// *does* skip the confirmation prompt — but confirmed live: when the
|
||||
/// wallet's internal per-mint registry holds more than one entry for what
|
||||
/// is really the same mint (here: `https://mint.minibits.cash/Bitcoin` vs.
|
||||
/// a stale `.../Bitcoin/` — leftover from before the trailing-slash
|
||||
/// `mint_url` fix elsewhere in this codebase; `wallet.db` still had a
|
||||
/// proof/registry entry keyed under the old slashed URL even after
|
||||
/// `config.json` was corrected), the CLI appears to complete a real swap
|
||||
/// against the *good* entry — spending and irreversibly consuming the
|
||||
/// original proofs, per how Cashu swaps work — then hits the second,
|
||||
/// empty, stale-keyed entry, reports the whole command as
|
||||
/// `"success": false`, and **never prints or persists the resulting
|
||||
/// token anywhere** (checked every location its own "will be saved to a
|
||||
/// file" warning implies: `/etc/tollgate/ecash/`, `/root`, `/tmp`,
|
||||
/// nothing). Balance went from 50 sats to 0 across that one call. The
|
||||
/// funds are gone — there is no undo once a swap is submitted to the
|
||||
/// mint.
|
||||
///
|
||||
/// Do not wire `--json` into this function until upstream fixes partial
|
||||
/// per-mint failure handling in `drain cashu` to preserve/return whatever it
|
||||
/// already successfully drained. Until then, the current silent-no-op
|
||||
/// behavior, while useless, is at least safe.
|
||||
pub async fn sweep_once(data_dir: &Path) -> Result<u64> {
|
||||
let cfg = net_router::load_router_config(data_dir).await?;
|
||||
if !cfg.configured {
|
||||
|
||||
@@ -23,14 +23,6 @@ pub struct TollGateConfig {
|
||||
pub min_steps: u32,
|
||||
/// Whether the TollGate service should be running and enabled at boot.
|
||||
pub enabled: bool,
|
||||
/// Operator's own Lightning address for the daemon's built-in payout
|
||||
/// (the "owner" entry in `/etc/tollgate/identities.json`, `profit_share`
|
||||
/// weight 0.79 in the upstream default). `None` leaves whatever is
|
||||
/// already on the router untouched — which, on a router whose TollGate
|
||||
/// wasn't provisioned through this project, is an unmodified upstream
|
||||
/// placeholder nobody actually controls (confirmed live against
|
||||
/// archy-x250-pa3 2026-09-07: shipped as `tollgate@minibits.cash`).
|
||||
pub payout_address: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for TollGateConfig {
|
||||
@@ -42,7 +34,6 @@ impl Default for TollGateConfig {
|
||||
step_size_ms: 60_000,
|
||||
min_steps: 1,
|
||||
enabled: true,
|
||||
payout_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,27 +46,19 @@ impl Default for TollGateConfig {
|
||||
/// tollgate.main.enabled` etc.); changing pricing or the mint here has no
|
||||
/// effect on what the daemon advertises or accepts.
|
||||
pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
||||
let step_size = cfg.step_size_ms.to_string();
|
||||
let min_steps = cfg.min_steps.to_string();
|
||||
let price_sats = cfg.price_sats.to_string();
|
||||
|
||||
let mut pairs = vec![
|
||||
("tollgate.main", "tollgate"),
|
||||
("tollgate.main.enabled", if cfg.enabled { "1" } else { "0" }),
|
||||
("tollgate.main.metric", "milliseconds"),
|
||||
("tollgate.main.step_size", step_size.as_str()),
|
||||
("tollgate.main.min_steps", min_steps.as_str()),
|
||||
("tollgate.main.price_per_step", price_sats.as_str()),
|
||||
("tollgate.main.currency", "sat"),
|
||||
("tollgate.main.mint_url", &cfg.mint_url),
|
||||
];
|
||||
// Status-display only (see doc comment above) — only written when the
|
||||
// caller actually supplied one, so a reconfigure that doesn't touch
|
||||
// payout leaves whatever's already there alone.
|
||||
if let Some(addr) = &cfg.payout_address {
|
||||
pairs.push(("tollgate.main.payout_address", addr));
|
||||
}
|
||||
router.uci_apply("tollgate", &pairs)?;
|
||||
router.uci_apply(
|
||||
"tollgate",
|
||||
&[
|
||||
("tollgate.main", "tollgate"),
|
||||
("tollgate.main.enabled", if cfg.enabled { "1" } else { "0" }),
|
||||
("tollgate.main.metric", "milliseconds"),
|
||||
("tollgate.main.step_size", &cfg.step_size_ms.to_string()),
|
||||
("tollgate.main.min_steps", &cfg.min_steps.to_string()),
|
||||
("tollgate.main.price_per_step", &cfg.price_sats.to_string()),
|
||||
("tollgate.main.currency", "sat"),
|
||||
("tollgate.main.mint_url", &cfg.mint_url),
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -114,61 +97,3 @@ pub fn apply_daemon_config(router: &Router, cfg: &TollGateConfig) -> Result<()>
|
||||
.context("upload /etc/tollgate/config.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the operator's own payout Lightning address in
|
||||
/// `/etc/tollgate/identities.json` — the "owner" entry under
|
||||
/// `public_identities` (`profit_share` weight 0.79 in the upstream default;
|
||||
/// the other entries there are revenue-share addresses for the upstream
|
||||
/// project's own maintainers and must never be touched by this function).
|
||||
///
|
||||
/// No-op when `payout_address` is `None` — the UI only sends one when the
|
||||
/// operator has actually filled the field in, so a reconfigure of price/mint
|
||||
/// alone never overwrites this. Merges into whatever identities.json already
|
||||
/// exists (same reasoning as `apply_daemon_config`: `owned_identities` holds
|
||||
/// the merchant's own private key and must survive untouched); creates an
|
||||
/// "owner" entry if none exists yet rather than erroring, since a router
|
||||
/// whose TollGate wasn't provisioned through this project may have any
|
||||
/// upstream-default shape here.
|
||||
///
|
||||
/// Must run before the daemon restart in `restart_services` — like
|
||||
/// `config.json`, `tollgate-wrt` only reads `identities.json` at startup.
|
||||
pub fn apply_payout_identity(router: &Router, payout_address: Option<&str>) -> Result<()> {
|
||||
let Some(address) = payout_address else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let existing = router.run_ok("cat /etc/tollgate/identities.json 2>/dev/null || echo '{}'")?;
|
||||
let mut doc: serde_json::Value =
|
||||
serde_json::from_str(existing.trim()).unwrap_or_else(|_| serde_json::json!({}));
|
||||
|
||||
let identities = doc
|
||||
.as_object_mut()
|
||||
.context("identities.json root is not a JSON object")?
|
||||
.entry("public_identities")
|
||||
.or_insert_with(|| serde_json::json!([]));
|
||||
let identities = identities
|
||||
.as_array_mut()
|
||||
.context("identities.json public_identities is not an array")?;
|
||||
|
||||
match identities
|
||||
.iter_mut()
|
||||
.find(|i| i.get("name").and_then(|n| n.as_str()) == Some("owner"))
|
||||
{
|
||||
Some(owner) => {
|
||||
owner["lightning_address"] = serde_json::json!(address);
|
||||
}
|
||||
None => {
|
||||
identities.push(serde_json::json!({
|
||||
"name": "owner",
|
||||
"pubkey": "not currently used",
|
||||
"lightning_address": address,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&doc).context("serialize identities.json")?;
|
||||
router
|
||||
.upload_file("/etc/tollgate/identities.json", json_str.as_bytes())
|
||||
.context("upload /etc/tollgate/identities.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -59,17 +59,10 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
|
||||
|
||||
config::apply(router, config)?;
|
||||
wifi::provision_ssid(router, config)?;
|
||||
// Must come after provision_ssid (creates the `tollgate` network this
|
||||
// folds the upstream installer's own default AP onto) — see
|
||||
// regate_upstream_default_aps for why this is needed at all.
|
||||
wifi::regate_upstream_default_aps(router)
|
||||
.context("re-gate upstream tollgate-module-basic-go default AP(s)")?;
|
||||
// Must come after provision_ssid (which creates br-tollgate) and before
|
||||
// the daemon restart below — config.json is only read at startup.
|
||||
config::apply_daemon_config(router, config)
|
||||
.context("write /etc/tollgate/config.json — tollgate-wrt reads this, not UCI")?;
|
||||
config::apply_payout_identity(router, config.payout_address.as_deref())
|
||||
.context("write /etc/tollgate/identities.json owner payout address")?;
|
||||
// Also must come after provision_ssid: points gatewayinterface at
|
||||
// br-tollgate, which provision_ssid is what creates.
|
||||
nodogsplash::configure(router, config)
|
||||
|
||||
@@ -118,49 +118,6 @@ fn provision_firewall(router: &Router) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fold the upstream `tollgate-module-basic-go` installer's own default
|
||||
/// AP(s) onto the gated `tollgate` network.
|
||||
///
|
||||
/// `install::install_ipk` runs the package's `/etc/uci-defaults/*` first-boot
|
||||
/// scripts itself (no real package manager to trigger them on OpenWrt 25.x —
|
||||
/// see its doc comment). Those upstream scripts rebrand OpenWrt's
|
||||
/// factory-default wifi sections (`wireless.default_radioN`, present on
|
||||
/// every fresh install) to a `TollGate-<serial>` SSID, but only ever touch
|
||||
/// the SSID — they leave `network` at its original `lan` binding. Nothing
|
||||
/// else in this project's own provisioning (`provision_ssid` above) ever
|
||||
/// looks at those sections; it only manages the separate `wireless.tollgate`
|
||||
/// SSID it creates itself. Left alone, the result is two open SSIDs
|
||||
/// broadcasting side by side: ours (gated by NoDogSplash) and upstream's
|
||||
/// (wide open on `lan`, with a direct route to whatever's plugged into the
|
||||
/// wired LAN port).
|
||||
///
|
||||
/// Confirmed live against archy-x250-pa3 2026-09-07: a client joining
|
||||
/// "TollGate-3458" landed on `br-lan` with unrestricted WAN forwarding and
|
||||
/// zero NoDogSplash involvement — free, unmetered internet, no captive
|
||||
/// portal, on the router's own admin network.
|
||||
///
|
||||
/// Must run after `provision_network` (needs the `tollgate` network/bridge
|
||||
/// to already exist) and before the network/wifi restart in
|
||||
/// `restart_services` picks the new binding up.
|
||||
pub fn regate_upstream_default_aps(router: &Router) -> Result<()> {
|
||||
let sections = router.run_ok(
|
||||
"uci show wireless 2>/dev/null | grep -o '^wireless\\.default_radio[0-9]*' | sort -u",
|
||||
)?;
|
||||
for section in sections.lines().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
let network_key = format!("{}.network", section);
|
||||
let current = router.uci_get(&network_key).unwrap_or_default();
|
||||
if current == "lan" {
|
||||
info!(
|
||||
"[{}] Re-gating upstream default AP {} (was network=lan) onto the tollgate network",
|
||||
router.host, section
|
||||
);
|
||||
router.uci_set(&network_key, "tollgate")?;
|
||||
}
|
||||
}
|
||||
router.uci_commit(Some("wireless"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return the first available wireless radio device name (e.g. "radio0").
|
||||
fn detect_radio(router: &Router) -> Result<String> {
|
||||
let out =
|
||||
|
||||
@@ -36,7 +36,6 @@ interface TollGateStatus {
|
||||
min_steps?: number
|
||||
currency?: string
|
||||
mint_url?: string
|
||||
payout_address?: string
|
||||
}
|
||||
|
||||
interface WanStatus {
|
||||
@@ -150,7 +149,6 @@ const editPriceSats = ref(10)
|
||||
const editStepSizeMin = ref(1)
|
||||
const editMinSteps = ref(1)
|
||||
const editMintUrl = ref('')
|
||||
const editPayoutAddress = ref('')
|
||||
const editEnabled = ref(true)
|
||||
|
||||
// WAN setup flow
|
||||
@@ -308,7 +306,6 @@ function startEditTollgate() {
|
||||
editStepSizeMin.value = Math.max(1, Math.round((tg?.step_size_ms ?? 60000) / 60000))
|
||||
editMinSteps.value = tg?.min_steps ?? 1
|
||||
editMintUrl.value = tg?.mint_url ?? ''
|
||||
editPayoutAddress.value = tg?.payout_address ?? ''
|
||||
editEnabled.value = tg?.enabled ?? true
|
||||
updateTollgateError.value = ''
|
||||
editingTollgate.value = true
|
||||
@@ -324,7 +321,6 @@ async function saveTollgateConfig() {
|
||||
step_size_ms: editStepSizeMin.value * 60_000,
|
||||
min_steps: editMinSteps.value,
|
||||
mint_url: editMintUrl.value,
|
||||
payout_address: editPayoutAddress.value,
|
||||
enabled: editEnabled.value,
|
||||
}
|
||||
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
||||
@@ -939,21 +935,6 @@ onMounted(() => {
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs text-white/40 mb-2">Payout Lightning address</label>
|
||||
<input
|
||||
v-model="editPayoutAddress"
|
||||
type="text"
|
||||
placeholder="you@yourwallet.example"
|
||||
class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/30 focus:outline-none focus:border-white/40 transition-colors font-mono text-xs"
|
||||
/>
|
||||
<p class="mt-2 text-xs text-white/40">
|
||||
Where TollGate's built-in auto-payout sends your share once the on-router balance
|
||||
crosses its threshold. Leave blank to keep whatever's already set on the router —
|
||||
on a router not originally provisioned here, that may be an upstream default you
|
||||
don't control.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
:disabled="updatingTollgate"
|
||||
|
||||
Reference in New Issue
Block a user