feat: nodemanager, view and edit channels fees (#2818)
This commit is contained in:
parent
0c5b909c7a
commit
3900d2871d
7 changed files with 360 additions and 93 deletions
|
|
@ -29,7 +29,46 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="openChannelDialog.show" position="top">
|
||||
<q-dialog v-model="setFeeDialog.show">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<label class="text-h6">Set Channel Fee</label>
|
||||
<p class="text-caption" v-text="setFeeDialog.channel_id"></p>
|
||||
<q-separator></q-separator>
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="setFeeDialog.data.fee_ppm"
|
||||
label="Fee Rate PPM"
|
||||
></q-input>
|
||||
<q-input
|
||||
dense
|
||||
type="number"
|
||||
filled
|
||||
v-model.number="setFeeDialog.data.fee_base_msat"
|
||||
label="Fee Base msat"
|
||||
></q-input>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
:label="$t('set')"
|
||||
color="primary"
|
||||
@click="setChannelFee(setFeeDialog.channel_id)"
|
||||
></q-btn>
|
||||
<q-btn
|
||||
v-close-popup
|
||||
flat
|
||||
color="grey"
|
||||
class="q-ml-auto"
|
||||
:label="$t('cancel')"
|
||||
></q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<q-dialog v-model="openChannelDialog.show">
|
||||
<q-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||
<q-form class="q-gutter-md">
|
||||
<q-input
|
||||
|
|
@ -171,10 +210,41 @@
|
|||
<q-tr :props="props">
|
||||
<div class="q-pb-sm">
|
||||
<div class="row items-center q-gutter-sm">
|
||||
<div
|
||||
class="text-subtitle1 col-grow"
|
||||
v-text="props.row.name"
|
||||
></div>
|
||||
<div class="text-subtitle1" v-text="props.row.name"></div>
|
||||
<div class="text-caption" v-if="props.row.peer_id">
|
||||
<span>Peer ID</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.peer_id)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="text-caption col-grow">
|
||||
<span>Fees</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="settings"
|
||||
@click="showSetFeeDialog(props.row.id)"
|
||||
></q-btn>
|
||||
<span v-if="props.row.fee_ppm">
|
||||
<span v-text="props.row.fee_ppm"></span> ppm,
|
||||
<span v-text="props.row.fee_base_msat"></span> msat
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-caption" v-if="props.row.id">
|
||||
<span>Channel ID</span>
|
||||
<q-btn
|
||||
size="xs"
|
||||
flat
|
||||
dense
|
||||
icon="content_paste"
|
||||
@click="copyText(props.row.id)"
|
||||
></q-btn>
|
||||
</div>
|
||||
<div class="text-caption" v-if="props.row.short_id">
|
||||
<span v-text="props.row.short_id"></span>
|
||||
<q-btn
|
||||
|
|
@ -201,6 +271,7 @@
|
|||
color="pink"
|
||||
></q-btn>
|
||||
</div>
|
||||
|
||||
<lnbits-channel-balance
|
||||
:balance="props.row.balance"
|
||||
:color="props.row.color"
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
}
|
||||
},
|
||||
mixins: [window.windowMixin],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
isSuperUser: false,
|
||||
wallet: {},
|
||||
|
|
@ -79,6 +79,14 @@
|
|||
data: {}
|
||||
},
|
||||
|
||||
setFeeDialog: {
|
||||
show: false,
|
||||
data: {
|
||||
fee_ppm: 0,
|
||||
fee_base_msat: 0
|
||||
}
|
||||
},
|
||||
|
||||
openChannelDialog: {
|
||||
show: false,
|
||||
data: {}
|
||||
|
|
@ -118,12 +126,6 @@
|
|||
name: 'pending',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'memo'
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
align: 'left',
|
||||
|
|
@ -149,6 +151,12 @@
|
|||
align: 'right',
|
||||
label: 'Destination',
|
||||
field: 'destination'
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'memo'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
|
|
@ -165,16 +173,10 @@
|
|||
name: 'pending',
|
||||
label: ''
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'memo'
|
||||
},
|
||||
{
|
||||
name: 'paid_at',
|
||||
field: 'paid_at',
|
||||
align: 'right',
|
||||
align: 'left',
|
||||
label: 'Paid at',
|
||||
sortable: true
|
||||
},
|
||||
|
|
@ -182,7 +184,7 @@
|
|||
name: 'expiry',
|
||||
label: this.$t('expiry'),
|
||||
field: 'expiry',
|
||||
align: 'right',
|
||||
align: 'left',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
|
|
@ -190,6 +192,12 @@
|
|||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||
field: row => this.formatMsat(row.amount),
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
name: 'memo',
|
||||
align: 'left',
|
||||
label: this.$t('memo'),
|
||||
field: 'memo'
|
||||
}
|
||||
],
|
||||
pagination: {
|
||||
|
|
@ -201,12 +209,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
created() {
|
||||
this.getInfo()
|
||||
this.get1MLStats()
|
||||
},
|
||||
watch: {
|
||||
tab: function (val) {
|
||||
tab(val) {
|
||||
if (val === 'transactions' && !this.paymentsTable.data.length) {
|
||||
this.getPayments()
|
||||
this.getInvoices()
|
||||
|
|
@ -220,14 +228,14 @@
|
|||
checkChanges() {
|
||||
return !_.isEqual(this.settings, this.formData)
|
||||
},
|
||||
filteredChannels: function () {
|
||||
filteredChannels() {
|
||||
return this.stateFilters
|
||||
? this.channels.data.filter(channel => {
|
||||
return this.stateFilters.find(({value}) => value == channel.state)
|
||||
})
|
||||
: this.channels.data
|
||||
},
|
||||
totalBalance: function () {
|
||||
totalBalance() {
|
||||
return this.filteredChannels.reduce(
|
||||
(balance, channel) => {
|
||||
balance.local_msat += channel.balance.local_msat
|
||||
|
|
@ -240,10 +248,10 @@
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
formatMsat: function (msat) {
|
||||
formatMsat(msat) {
|
||||
return LNbits.utils.formatMsat(msat)
|
||||
},
|
||||
api: function (method, url, options) {
|
||||
api(method, url, options) {
|
||||
const params = new URLSearchParams(options?.query)
|
||||
return LNbits.api
|
||||
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
|
||||
|
|
@ -251,23 +259,29 @@
|
|||
LNbits.utils.notifyApiError(error)
|
||||
})
|
||||
},
|
||||
getChannels: function () {
|
||||
getChannel(channel_id) {
|
||||
return this.api('GET', `/channels/${channel_id}`).then(response => {
|
||||
this.setFeeDialog.data.fee_ppm = response.data.fee_ppm
|
||||
this.setFeeDialog.data.fee_base_msat = response.data.fee_base_msat
|
||||
})
|
||||
},
|
||||
getChannels() {
|
||||
return this.api('GET', '/channels').then(response => {
|
||||
this.channels.data = response.data
|
||||
})
|
||||
},
|
||||
getInfo: function () {
|
||||
getInfo() {
|
||||
return this.api('GET', '/info').then(response => {
|
||||
this.info = response.data
|
||||
this.channel_stats = response.data.channel_stats
|
||||
})
|
||||
},
|
||||
get1MLStats: function () {
|
||||
get1MLStats() {
|
||||
return this.api('GET', '/rank').then(response => {
|
||||
this.ranks = response.data
|
||||
})
|
||||
},
|
||||
getPayments: function (props) {
|
||||
getPayments(props) {
|
||||
if (props) {
|
||||
this.paymentsTable.pagination = props.pagination
|
||||
}
|
||||
|
|
@ -281,7 +295,7 @@
|
|||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||
})
|
||||
},
|
||||
getInvoices: function (props) {
|
||||
getInvoices(props) {
|
||||
if (props) {
|
||||
this.invoiceTable.pagination = props.pagination
|
||||
}
|
||||
|
|
@ -295,14 +309,13 @@
|
|||
this.invoiceTable.pagination.rowsNumber = response.data.total
|
||||
})
|
||||
},
|
||||
getPeers: function () {
|
||||
getPeers() {
|
||||
return this.api('GET', '/peers').then(response => {
|
||||
this.peers.data = response.data
|
||||
console.log('peers', this.peers)
|
||||
})
|
||||
},
|
||||
connectPeer: function () {
|
||||
console.log('peer', this.connectPeerDialog)
|
||||
connectPeer() {
|
||||
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
|
||||
() => {
|
||||
this.connectPeerDialog.show = false
|
||||
|
|
@ -310,7 +323,7 @@
|
|||
}
|
||||
)
|
||||
},
|
||||
disconnectPeer: function (id) {
|
||||
disconnectPeer(id) {
|
||||
LNbits.utils
|
||||
.confirmDialog('Do you really wanna disconnect this peer?')
|
||||
.onOk(() => {
|
||||
|
|
@ -324,7 +337,17 @@
|
|||
})
|
||||
})
|
||||
},
|
||||
openChannel: function () {
|
||||
setChannelFee(channel_id) {
|
||||
this.api('PUT', `/channels/${channel_id}`, {
|
||||
data: this.setFeeDialog.data
|
||||
})
|
||||
.then(response => {
|
||||
this.setFeeDialog.show = false
|
||||
this.getChannels()
|
||||
})
|
||||
.catch(LNbits.utils.notifyApiError)
|
||||
},
|
||||
openChannel() {
|
||||
this.api('POST', '/channels', {data: this.openChannelDialog.data})
|
||||
.then(response => {
|
||||
this.openChannelDialog.show = false
|
||||
|
|
@ -334,7 +357,7 @@
|
|||
console.log(error)
|
||||
})
|
||||
},
|
||||
showCloseChannelDialog: function (channel) {
|
||||
showCloseChannelDialog(channel) {
|
||||
this.closeChannelDialog.show = true
|
||||
this.closeChannelDialog.data = {
|
||||
force: false,
|
||||
|
|
@ -342,7 +365,7 @@
|
|||
...channel.point
|
||||
}
|
||||
},
|
||||
closeChannel: function () {
|
||||
closeChannel() {
|
||||
this.api('DELETE', '/channels', {
|
||||
query: this.closeChannelDialog.data
|
||||
}).then(response => {
|
||||
|
|
@ -350,15 +373,20 @@
|
|||
this.getChannels()
|
||||
})
|
||||
},
|
||||
showOpenChannelDialog: function (peer_id) {
|
||||
showSetFeeDialog(channel_id) {
|
||||
this.setFeeDialog.show = true
|
||||
this.setFeeDialog.channel_id = channel_id
|
||||
this.getChannel(channel_id)
|
||||
},
|
||||
showOpenChannelDialog(peer_id) {
|
||||
this.openChannelDialog.show = true
|
||||
this.openChannelDialog.data = {peer_id, funding_amount: 0}
|
||||
},
|
||||
showNodeInfoDialog: function (node) {
|
||||
showNodeInfoDialog(node) {
|
||||
this.nodeInfoDialog.show = true
|
||||
this.nodeInfoDialog.data = node
|
||||
},
|
||||
showTransactionDetailsDialog: function (details) {
|
||||
showTransactionDetailsDialog(details) {
|
||||
this.transactionDetailsDialog.show = true
|
||||
this.transactionDetailsDialog.data = details
|
||||
console.log('details', details)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,14 @@ async def api_get_channels(
|
|||
return await node.get_channels()
|
||||
|
||||
|
||||
@node_router.get("/channels/{channel_id}")
|
||||
async def api_get_channel(
|
||||
channel_id: str,
|
||||
node: Node = Depends(require_node),
|
||||
) -> Optional[NodeChannel]:
|
||||
return await node.get_channel(channel_id)
|
||||
|
||||
|
||||
@super_node_router.post("/channels", response_model=ChannelPoint)
|
||||
async def api_create_channel(
|
||||
node: Node = Depends(require_node),
|
||||
|
|
@ -125,7 +133,17 @@ async def api_delete_channel(
|
|||
)
|
||||
|
||||
|
||||
@node_router.get("/payments", response_model=Page[NodePayment])
|
||||
@super_node_router.put("/channels/{channel_id}")
|
||||
async def api_set_channel_fees(
|
||||
channel_id: str,
|
||||
node: Node = Depends(require_node),
|
||||
fee_ppm: int = Body(None),
|
||||
fee_base_msat: int = Body(None),
|
||||
):
|
||||
await node.set_channel_fee(channel_id, fee_base_msat, fee_ppm)
|
||||
|
||||
|
||||
@node_router.get("/payments")
|
||||
async def api_get_payments(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||
|
|
@ -138,7 +156,7 @@ async def api_get_payments(
|
|||
return await node.get_payments(filters)
|
||||
|
||||
|
||||
@node_router.get("/invoices", response_model=Page[NodeInvoice])
|
||||
@node_router.get("/invoices")
|
||||
async def api_get_invoices(
|
||||
node: Node = Depends(require_node),
|
||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||
|
|
@ -151,7 +169,7 @@ async def api_get_invoices(
|
|||
return await node.get_invoices(filters)
|
||||
|
||||
|
||||
@node_router.get("/peers", response_model=List[NodePeerInfo])
|
||||
@node_router.get("/peers")
|
||||
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
|
||||
return await node.get_peers()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -99,6 +99,55 @@ async def test_node_payments(node_client, real_invoice, adminkey_headers_from):
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_get_channel(node_client):
|
||||
# lndrest is slow / async with channel commands
|
||||
await asyncio.sleep(3)
|
||||
response = await node_client.get("/node/api/v1/channels")
|
||||
assert response.status_code == 200
|
||||
channels = parse_obj_as(list[NodeChannel], response.json())
|
||||
|
||||
ch = random.choice(
|
||||
[channel for channel in channels if channel.state == ChannelState.ACTIVE]
|
||||
)
|
||||
assert ch, "No active channel found"
|
||||
assert ch.point, "No channel point found"
|
||||
|
||||
await asyncio.sleep(3)
|
||||
response = await node_client.get(f"/node/api/v1/channels/{ch.id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
channel = parse_obj_as(NodeChannel, response.json())
|
||||
assert channel.id == ch.id
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_set_channel_fees(node_client):
|
||||
# lndrest is slow / async with channel commands
|
||||
await asyncio.sleep(3)
|
||||
response = await node_client.get("/node/api/v1/channels")
|
||||
assert response.status_code == 200
|
||||
channels = parse_obj_as(list[NodeChannel], response.json())
|
||||
|
||||
ch = random.choice(
|
||||
[channel for channel in channels if channel.state == ChannelState.ACTIVE]
|
||||
)
|
||||
assert ch, "No active channel found"
|
||||
assert ch.point, "No channel point found"
|
||||
|
||||
response = await node_client.put(
|
||||
f"/node/api/v1/channels/{ch.id}", json={"fee_base_msat": 42, "fee_ppm": 69}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
await asyncio.sleep(3)
|
||||
response = await node_client.get(f"/node/api/v1/channels/{ch.id}")
|
||||
assert response.status_code == 200
|
||||
channel = parse_obj_as(NodeChannel, response.json())
|
||||
assert channel.fee_ppm == 69
|
||||
assert channel.fee_base_msat == 42
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_channel_management(node_client):
|
||||
async def get_channels():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue