What changed, and why it matters
This commit adds two read-only statistics commands to Electrum's swapserver plugin, allowing swap providers to view their swap history and summary. It does not change how swaps are executed, does not move funds, and does not introduce obvious security bugs. The main consideration is that these commands expose swap-related financial data through existing command interfaces, but they appear to use the same access controls as other wallet-lightning ('wl') commands.
No immediate security action required. Treat as a normal feature addition. If reviewing further, verify that the 'wl' command category enforces appropriate wallet access controls and that the missing wallet argument in get_summary's call to get_history is intentional or fixed before release.
Security signals we found
New read-only plugin commands added to swapserver plugin
Commands use existing @plugin_command('wl', ...) registration, inheriting existing authorization model
No input parsing from untrusted network sources
No state mutation, fund movement, or cryptographic changes
Potential functional issue: get_summary calls get_history(self) without forwarding wallet argument
Evidence from the diff
The patch registers two new plugin commands, get_history and get_summary, under the ‘wl’ category in electrum/plugins/swapserver/init.py. Both are decorated with @plugin_command(‘wl’, plugin_name) and accept a wallet argument. They read from wallet.get_full_history() and wallet.lnworker.swap_manager.get_groups_for_onchain_history(), then return aggregated swap statistics. No network input is parsed, no subprocesses are spawned, no file writes occur, and no cryptographic operations are altered. The commands are read-only with respect to on-chain/lightning state. A minor code-quality note: get_summary calls get_history(self) without passing the wallet argument, which may cause issues if the command is invoked in contexts where a wallet is not automatically supplied; however, this is a functional bug rather than a clear security vulnerability.
Changed components
electrum/plugins/swapserver/__init__.pyElectrum swapserver plugin CLI/RPC command surfaceInspect captured patch +85 / −0
diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py
index 69c216a..d8b1934 100644
--- a/electrum/plugins/swapserver/__init__.py
+++ b/electrum/plugins/swapserver/__init__.py
@@ -1,5 +1,90 @@
+from typing import TYPE_CHECKING, List
+
from electrum.simple_config import ConfigVar, SimpleConfig
+from electrum.commands import plugin_command
+
+if TYPE_CHECKING:
+ from electrum.commands import Commands
+ from electrum.wallet import Abstract_Wallet
+
+
+plugin_name = "swapserver"
+
SimpleConfig.SWAPSERVER_PORT = ConfigVar('plugins.swapserver.port', default=None, type_=int, plugin=__name__)
SimpleConfig.SWAPSERVER_FEE_MILLIONTHS = ConfigVar('plugins.swapserver.fee_millionths', default=5000, type_=int, plugin=__name__)
SimpleConfig.SWAPSERVER_ANN_POW_NONCE = ConfigVar('plugins.swapserver.ann_pow_nonce', default=0, type_=int, plugin=__name__)
+
+
+@plugin_command('wl', plugin_name)
+async def get_history(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> List[dict]:
+ """
+ Get a list of all swaps provided by this swapserver.
+ Single elements can potentially cover multiple swaps if transactions have been batched.
+
+ Example result:
+
+ [
+ {
+ "date": "2025-09-04",
+ "label": "Forward swap 0.2018 mBTC",
+ "timestamp": 1756982141, # unix timestamp
+ "return_sat": -205 # value in satoshi that has been earned or lost with this swap
+ },
+ {
+ "date": "2025-09-04",
+ "label": "Reverse swap 0.30406 mBTC",
+ "timestamp": 1756983236,
+ "return_sat": 64
+ }
+ ]
+ """
+ assert wallet.lnworker, "lightning not available"
+ assert wallet.lnworker.swap_manager, "swap manager not available"
+
+ full_history = wallet.get_full_history()
+ swap_group_ids = set(
+ x['group_id'] for x in wallet.lnworker.swap_manager.get_groups_for_onchain_history().values()
+ )
+
+ swap_history_items = []
+ for swap_group_id in swap_group_ids:
+ if swap_history_item := full_history.get('group:' + swap_group_id):
+ swap_history_items.append(swap_history_item)
+
+ result = []
+ for swap in swap_history_items:
+ result.append({
+ 'label': swap['label'],
+ 'return_sat': int(swap['value'].value),
+ 'date': swap['date'].strftime("%Y-%m-%d"),
+ 'timestamp': swap['timestamp']
+ })
+ result = sorted(result, key=lambda x: x['timestamp'])
+ return result
+
+
+@plugin_command('wl', plugin_name)
+async def get_summary(self: 'Commands', wallet: 'Abstract_Wallet' = None, plugin = None) -> dict:
+ """Get a summary of all swaps provided by this swapserver.
+ Can become incorrect if closed lightning channels have been deleted in this wallet.
+
+ Example result:
+ {
+ "num_swaps": 160,
+ "overall_return_sat": 159052, # value earned or lost in satoshi
+ "swaps_per_day": 0.78 # between first swap and last swap
+ }
+ """
+ swap_history = await get_history(self)
+ profit_loss_sum = sum(swap['return_sat'] for swap in swap_history) if swap_history else 0
+ first_swap = min(swap['timestamp'] for swap in swap_history) if swap_history else 0
+ last_swap = max(swap['timestamp'] for swap in swap_history) if swap_history else 0
+ days_in_operation = (last_swap - first_swap) // 86400
+ swaps_per_day = (len(swap_history) / days_in_operation) if days_in_operation > 0 else 0
+
+ return {
+ 'num_swaps': len(swap_history),
+ 'overall_return_sat': profit_loss_sum,
+ 'swaps_per_day': round(swaps_per_day, 2),
+ }
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.