feat(openwrt): make TollGate payout Lightning address configurable
Archipelago never touched /etc/tollgate/identities.json — the "owner" payout identity was whatever the router's TollGate install happened to default to. Confirmed live against archy-x250-pa3: an unmodified upstream placeholder (tollgate@minibits.cash), meaning 79% of every customer payment would auto-payout to an address the operator never chose and doesn't control. Adds TollGateConfig.payout_address (opt-in — None leaves the router untouched), config::apply_payout_identity() to merge it into the "owner" entry of identities.json without disturbing the merchant keypair or the other profit-share identities, an RPC param on openwrt.provision-tollgate, and a status field + reconfigure-form input in the OpenWrt Gateway panel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KTdMfVJChCwCCYF1ZTRQLc
This commit is contained in:
committed by
archipelago
co-authored by
Claude Sonnet 5
parent
87a5025341
commit
f9af30b08a
@@ -150,6 +150,7 @@ impl RpcHandler {
|
|||||||
"min_steps": router.uci_get("tollgate.main.min_steps").ok().and_then(|v| v.parse::<u32>().ok()).unwrap_or(1),
|
"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(),
|
"currency": router.uci_get("tollgate.main.currency").unwrap_or_default(),
|
||||||
"mint_url": router.uci_get("tollgate.main.mint_url").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 {
|
} else {
|
||||||
serde_json::json!({ "installed": false })
|
serde_json::json!({ "installed": false })
|
||||||
@@ -199,10 +200,15 @@ impl RpcHandler {
|
|||||||
///
|
///
|
||||||
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
/// Params: `{ "host": "192.168.1.1", "ssh_user": "root", "ssh_password": "",
|
||||||
/// "price_sats": 10, "step_size_ms": 60000, "min_steps": 1,
|
/// "price_sats": 10, "step_size_ms": 60000, "min_steps": 1,
|
||||||
/// "mint_url": "<optional override>" }`
|
/// "mint_url": "<optional override>",
|
||||||
|
/// "payout_address": "<optional Lightning address>" }`
|
||||||
///
|
///
|
||||||
/// `mint_url` defaults to `http://<this node's IP>:3338` — the local Cashu
|
/// `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.
|
/// 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(
|
pub(super) async fn handle_openwrt_provision_tollgate(
|
||||||
&self,
|
&self,
|
||||||
params: Option<serde_json::Value>,
|
params: Option<serde_json::Value>,
|
||||||
@@ -254,6 +260,17 @@ impl RpcHandler {
|
|||||||
.trim_end_matches('/')
|
.trim_end_matches('/')
|
||||||
.to_string();
|
.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 {
|
let config = TollGateConfig {
|
||||||
ssid: "archipelago".to_string(),
|
ssid: "archipelago".to_string(),
|
||||||
mint_url,
|
mint_url,
|
||||||
@@ -264,6 +281,7 @@ impl RpcHandler {
|
|||||||
.unwrap_or(60_000),
|
.unwrap_or(60_000),
|
||||||
min_steps: p.get("min_steps").and_then(|v| v.as_u64()).unwrap_or(1) as u32,
|
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),
|
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 —
|
// Blocking SSH session, and provision runs `opkg install` over it —
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ pub struct TollGateConfig {
|
|||||||
pub min_steps: u32,
|
pub min_steps: u32,
|
||||||
/// Whether the TollGate service should be running and enabled at boot.
|
/// Whether the TollGate service should be running and enabled at boot.
|
||||||
pub enabled: bool,
|
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 {
|
impl Default for TollGateConfig {
|
||||||
@@ -34,6 +42,7 @@ impl Default for TollGateConfig {
|
|||||||
step_size_ms: 60_000,
|
step_size_ms: 60_000,
|
||||||
min_steps: 1,
|
min_steps: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
payout_address: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,19 +55,27 @@ impl Default for TollGateConfig {
|
|||||||
/// tollgate.main.enabled` etc.); changing pricing or the mint here has no
|
/// tollgate.main.enabled` etc.); changing pricing or the mint here has no
|
||||||
/// effect on what the daemon advertises or accepts.
|
/// effect on what the daemon advertises or accepts.
|
||||||
pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> {
|
||||||
router.uci_apply(
|
let step_size = cfg.step_size_ms.to_string();
|
||||||
"tollgate",
|
let min_steps = cfg.min_steps.to_string();
|
||||||
&[
|
let price_sats = cfg.price_sats.to_string();
|
||||||
("tollgate.main", "tollgate"),
|
|
||||||
("tollgate.main.enabled", if cfg.enabled { "1" } else { "0" }),
|
let mut pairs = vec![
|
||||||
("tollgate.main.metric", "milliseconds"),
|
("tollgate.main", "tollgate"),
|
||||||
("tollgate.main.step_size", &cfg.step_size_ms.to_string()),
|
("tollgate.main.enabled", if cfg.enabled { "1" } else { "0" }),
|
||||||
("tollgate.main.min_steps", &cfg.min_steps.to_string()),
|
("tollgate.main.metric", "milliseconds"),
|
||||||
("tollgate.main.price_per_step", &cfg.price_sats.to_string()),
|
("tollgate.main.step_size", step_size.as_str()),
|
||||||
("tollgate.main.currency", "sat"),
|
("tollgate.main.min_steps", min_steps.as_str()),
|
||||||
("tollgate.main.mint_url", &cfg.mint_url),
|
("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)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,3 +114,61 @@ pub fn apply_daemon_config(router: &Router, cfg: &TollGateConfig) -> Result<()>
|
|||||||
.context("upload /etc/tollgate/config.json")?;
|
.context("upload /etc/tollgate/config.json")?;
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> {
|
|||||||
// the daemon restart below — config.json is only read at startup.
|
// the daemon restart below — config.json is only read at startup.
|
||||||
config::apply_daemon_config(router, config)
|
config::apply_daemon_config(router, config)
|
||||||
.context("write /etc/tollgate/config.json — tollgate-wrt reads this, not UCI")?;
|
.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
|
// Also must come after provision_ssid: points gatewayinterface at
|
||||||
// br-tollgate, which provision_ssid is what creates.
|
// br-tollgate, which provision_ssid is what creates.
|
||||||
nodogsplash::configure(router, config)
|
nodogsplash::configure(router, config)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface TollGateStatus {
|
|||||||
min_steps?: number
|
min_steps?: number
|
||||||
currency?: string
|
currency?: string
|
||||||
mint_url?: string
|
mint_url?: string
|
||||||
|
payout_address?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WanStatus {
|
interface WanStatus {
|
||||||
@@ -149,6 +150,7 @@ const editPriceSats = ref(10)
|
|||||||
const editStepSizeMin = ref(1)
|
const editStepSizeMin = ref(1)
|
||||||
const editMinSteps = ref(1)
|
const editMinSteps = ref(1)
|
||||||
const editMintUrl = ref('')
|
const editMintUrl = ref('')
|
||||||
|
const editPayoutAddress = ref('')
|
||||||
const editEnabled = ref(true)
|
const editEnabled = ref(true)
|
||||||
|
|
||||||
// WAN setup flow
|
// WAN setup flow
|
||||||
@@ -306,6 +308,7 @@ function startEditTollgate() {
|
|||||||
editStepSizeMin.value = Math.max(1, Math.round((tg?.step_size_ms ?? 60000) / 60000))
|
editStepSizeMin.value = Math.max(1, Math.round((tg?.step_size_ms ?? 60000) / 60000))
|
||||||
editMinSteps.value = tg?.min_steps ?? 1
|
editMinSteps.value = tg?.min_steps ?? 1
|
||||||
editMintUrl.value = tg?.mint_url ?? ''
|
editMintUrl.value = tg?.mint_url ?? ''
|
||||||
|
editPayoutAddress.value = tg?.payout_address ?? ''
|
||||||
editEnabled.value = tg?.enabled ?? true
|
editEnabled.value = tg?.enabled ?? true
|
||||||
updateTollgateError.value = ''
|
updateTollgateError.value = ''
|
||||||
editingTollgate.value = true
|
editingTollgate.value = true
|
||||||
@@ -321,6 +324,7 @@ async function saveTollgateConfig() {
|
|||||||
step_size_ms: editStepSizeMin.value * 60_000,
|
step_size_ms: editStepSizeMin.value * 60_000,
|
||||||
min_steps: editMinSteps.value,
|
min_steps: editMinSteps.value,
|
||||||
mint_url: editMintUrl.value,
|
mint_url: editMintUrl.value,
|
||||||
|
payout_address: editPayoutAddress.value,
|
||||||
enabled: editEnabled.value,
|
enabled: editEnabled.value,
|
||||||
}
|
}
|
||||||
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
await rpcClient.call({ method: 'openwrt.provision-tollgate', params, timeout: 300000 })
|
||||||
@@ -935,6 +939,21 @@ 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"
|
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>
|
||||||
|
<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">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
:disabled="updatingTollgate"
|
:disabled="updatingTollgate"
|
||||||
|
|||||||
Reference in New Issue
Block a user