feat: nodemanager, view and edit channels fees (#2818)

This commit is contained in:
dni ⚡
2024-12-20 09:48:09 +01:00
committed by GitHub
parent 0c5b909c7a
commit 3900d2871d
7 changed files with 360 additions and 93 deletions
+29 -15
View File
@@ -38,15 +38,22 @@ class ChannelPoint(BaseModel):
funding_txid: str
output_index: int
def __str__(self):
return f"{self.funding_txid}:{self.output_index}"
class NodeChannel(BaseModel):
short_id: Optional[str] = None
point: Optional[ChannelPoint] = None
peer_id: str
balance: ChannelBalance
state: ChannelState
name: Optional[str]
color: Optional[str]
# could be optional for closing/pending channels on lndrest
id: Optional[str] = None
short_id: Optional[str] = None
point: Optional[ChannelPoint] = None
name: Optional[str] = None
color: Optional[str] = None
fee_ppm: Optional[int] = None
fee_base_msat: Optional[int] = None
class ChannelStats(BaseModel):
@@ -144,7 +151,6 @@ class NodePaymentsFilters(FilterModel):
class Node(ABC):
wallet: Wallet
def __init__(self, wallet: Wallet):
self.wallet = wallet
@@ -161,7 +167,7 @@ class Node(ABC):
@abstractmethod
async def _get_id(self) -> str:
pass
raise NotImplementedError
async def get_peers(self) -> list[NodePeerInfo]:
peer_ids = await self.get_peer_ids()
@@ -169,15 +175,15 @@ class Node(ABC):
@abstractmethod
async def get_peer_ids(self) -> list[str]:
pass
raise NotImplementedError
@abstractmethod
async def connect_peer(self, uri: str):
pass
raise NotImplementedError
@abstractmethod
async def disconnect_peer(self, peer_id: str):
pass
raise NotImplementedError
@abstractmethod
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
@@ -200,7 +206,7 @@ class Node(ABC):
push_amount: Optional[int] = None,
fee_rate: Optional[int] = None,
) -> ChannelPoint:
pass
raise NotImplementedError
@abstractmethod
async def close_channel(
@@ -209,15 +215,23 @@ class Node(ABC):
point: Optional[ChannelPoint] = None,
force: bool = False,
):
pass
raise NotImplementedError
@abstractmethod
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
raise NotImplementedError
@abstractmethod
async def get_channels(self) -> list[NodeChannel]:
pass
raise NotImplementedError
@abstractmethod
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
raise NotImplementedError
@abstractmethod
async def get_info(self) -> NodeInfoResponse:
pass
raise NotImplementedError
async def get_public_info(self) -> PublicNodeInfo:
info = await self.get_info()
@@ -227,10 +241,10 @@ class Node(ABC):
async def get_payments(
self, filters: Filters[NodePaymentsFilters]
) -> Page[NodePayment]:
pass
raise NotImplementedError
@abstractmethod
async def get_invoices(
self, filters: Filters[NodeInvoiceFilters]
) -> Page[NodeInvoice]:
pass
raise NotImplementedError
+44 -25
View File
@@ -66,6 +66,28 @@ class CoreLightningNode(Node):
fn = getattr(self.wallet.ln, method)
return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
def _parse_state(self, state: str) -> ChannelState:
if state == "CHANNELD_NORMAL":
return ChannelState.ACTIVE
if state in (
# wait for force close
"AWAITING_UNILATERAL",
# waiting for close
"CHANNELD_SHUTTING_DOWN",
# waiting for open
"CHANNELD_AWAITING_LOCKIN",
"OPENINGD",
):
return ChannelState.PENDING
if state in (
"CHANNELD_CLOSING",
"CLOSINGD_COMPLETE",
"CLOSINGD_SIGEXCHANGE",
"ONCHAIN",
):
return ChannelState.CLOSED
return ChannelState.INACTIVE
@catch_rpc_errors
async def connect_peer(self, uri: str):
# https://docs.corelightning.org/reference/lightning-connect
@@ -202,48 +224,45 @@ class CoreLightningNode(Node):
else:
return NodePeerInfo(id=node["nodeid"])
@catch_rpc_errors
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
await self.ln_rpc("setchannel", channel_id, feebase=base_msat, feeppm=ppm)
@catch_rpc_errors
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
channels = await self.get_channels()
for channel in channels:
if channel.id == channel_id:
return channel
return None
@catch_rpc_errors
async def get_channels(self) -> list[NodeChannel]:
funds = await self.ln_rpc("listfunds")
channels = await self.ln_rpc("listpeerchannels")
nodes = await self.ln_rpc("listnodes")
nodes_by_id = {n["nodeid"]: n for n in nodes["nodes"]}
return [
NodeChannel(
id=ch["channel_id"],
short_id=ch.get("short_channel_id"),
point=ChannelPoint(
funding_txid=ch["funding_txid"],
output_index=ch["funding_output"],
output_index=ch["funding_outnum"],
),
peer_id=ch["peer_id"],
balance=ChannelBalance(
local_msat=ch["our_amount_msat"],
remote_msat=ch["amount_msat"] - ch["our_amount_msat"],
total_msat=ch["amount_msat"],
local_msat=ch["spendable_msat"],
remote_msat=ch["receivable_msat"],
total_msat=ch["total_msat"],
),
fee_ppm=ch["fee_proportional_millionths"],
fee_base_msat=ch["fee_base_msat"],
name=nodes_by_id.get(ch["peer_id"], {}).get("alias"),
color=nodes_by_id.get(ch["peer_id"], {}).get("color"),
state=(
ChannelState.ACTIVE
if ch["state"] == "CHANNELD_NORMAL"
else (
ChannelState.PENDING
if ch["state"] in ("CHANNELD_AWAITING_LOCKIN", "OPENINGD")
else (
ChannelState.CLOSED
if ch["state"]
in (
"CHANNELD_CLOSING",
"CLOSINGD_COMPLETE",
"CLOSINGD_SIGEXCHANGE",
"ONCHAIN",
)
else ChannelState.INACTIVE
)
)
),
state=self._parse_state(ch["state"]),
)
for ch in funds["channels"]
for ch in channels["channels"]
]
@catch_rpc_errors
+77 -9
View File
@@ -41,6 +41,14 @@ def _decode_bytes(data: str) -> str:
return base64.b64decode(data).hex()
def _encode_bytes(data: str) -> str:
return base64.b64encode(bytes.fromhex(data)).decode()
def _encode_urlsafe_bytes(data: str) -> str:
return base64.urlsafe_b64encode(bytes.fromhex(data)).decode()
def _parse_channel_point(raw: str) -> ChannelPoint:
funding_tx, output_index = raw.split(":")
return ChannelPoint(
@@ -129,15 +137,12 @@ class LndRestNode(Node):
response = await self.request(
"POST",
"/v1/channels",
data=json.dumps(
{
# 'node_pubkey': base64.b64encode(peer_id.encode()).decode(),
"node_pubkey_string": peer_id,
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
}
),
json={
"node_pubkey": _encode_bytes(peer_id),
"sat_per_vbyte": fee_rate,
"local_funding_amount": local_amount,
"push_sat": push_amount,
},
)
return ChannelPoint(
# WHY IS THIS REVERSED?!
@@ -184,6 +189,66 @@ class LndRestNode(Node):
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
async def set_channel_fee(self, channel_id: str, base_msat: int, ppm: int):
# https://lightning.engineering/api-docs/api/lnd/lightning/update-channel-policy/
channel = await self.get_channel(channel_id)
if not channel:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Channel not found"
)
if not channel.point:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="Channel point required"
)
await self.request(
"POST",
"/v1/chanpolicy",
json={
"base_fee_msat": base_msat,
"fee_rate_ppm": ppm,
"chan_point": {
"funding_txid_str": channel.point.funding_txid,
"output_index": channel.point.output_index,
},
# https://docs.lightning.engineering/lightning-network-tools/lnd/optimal-configuration-of-a-routing-node#channel-defaults
"time_lock_delta": 80,
# 'max_htlc_msat': <uint64>,
# 'min_htlc_msat': <uint64>,
# 'inbound_fee': <InboundFee>,
},
)
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
channel_info = await self.get(f"/v1/graph/edge/{channel_id}")
peer_id = channel_info["node2_pub"]
peer_b64 = _encode_urlsafe_bytes(peer_id)
channels = await self.get(f"/v1/channels?peer={peer_b64}")
if "error" in channel_info and "error" in channels:
return None
for channel in channels["channels"]:
if channel["chan_id"] == channel_id:
peer_info = await self.get_peer_info(peer_id)
return NodeChannel(
id=channel.get("chan_id"),
peer_id=peer_info.id,
name=peer_info.alias,
color=peer_info.color,
state=(
ChannelState.ACTIVE
if channel["active"]
else ChannelState.INACTIVE
),
fee_ppm=channel_info["node1_policy"]["fee_rate_milli_msat"],
fee_base_msat=channel_info["node1_policy"]["fee_base_msat"],
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
remote_msat=msat(channel["remote_balance"]),
total_msat=msat(channel["capacity"]),
),
)
return None
async def get_channels(self) -> list[NodeChannel]:
normal, pending, closed = await asyncio.gather(
self.get("/v1/channels"),
@@ -203,6 +268,7 @@ class LndRestNode(Node):
state=state,
name=info.alias,
color=info.color,
id=channel.get("chan_id", "node is for pending channels"),
point=_parse_channel_point(channel["channel_point"]),
balance=ChannelBalance(
local_msat=msat(channel["local_balance"]),
@@ -222,6 +288,7 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel.get("chan_id", "node is for closing channels"),
peer_id=info.id,
state=ChannelState.CLOSED,
name=info.alias,
@@ -239,6 +306,7 @@ class LndRestNode(Node):
info = await self.get_peer_info(channel["remote_pubkey"])
channels.append(
NodeChannel(
id=channel["chan_id"],
short_id=channel["chan_id"],
point=_parse_channel_point(channel["channel_point"]),
peer_id=channel["remote_pubkey"],