feat: nodemanager, view and edit channels fees (#2818)
This commit is contained in:
+29
-15
@@ -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
@@ -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
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user