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-card>
|
||||||
</q-dialog>
|
</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-card class="q-pa-lg q-pt-xl lnbits__dialog-card">
|
||||||
<q-form class="q-gutter-md">
|
<q-form class="q-gutter-md">
|
||||||
<q-input
|
<q-input
|
||||||
|
|
@ -171,10 +210,41 @@
|
||||||
<q-tr :props="props">
|
<q-tr :props="props">
|
||||||
<div class="q-pb-sm">
|
<div class="q-pb-sm">
|
||||||
<div class="row items-center q-gutter-sm">
|
<div class="row items-center q-gutter-sm">
|
||||||
<div
|
<div class="text-subtitle1" v-text="props.row.name"></div>
|
||||||
class="text-subtitle1 col-grow"
|
<div class="text-caption" v-if="props.row.peer_id">
|
||||||
v-text="props.row.name"
|
<span>Peer ID</span>
|
||||||
></div>
|
<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">
|
<div class="text-caption" v-if="props.row.short_id">
|
||||||
<span v-text="props.row.short_id"></span>
|
<span v-text="props.row.short_id"></span>
|
||||||
<q-btn
|
<q-btn
|
||||||
|
|
@ -201,6 +271,7 @@
|
||||||
color="pink"
|
color="pink"
|
||||||
></q-btn>
|
></q-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<lnbits-channel-balance
|
<lnbits-channel-balance
|
||||||
:balance="props.row.balance"
|
:balance="props.row.balance"
|
||||||
:color="props.row.color"
|
:color="props.row.color"
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mixins: [window.windowMixin],
|
mixins: [window.windowMixin],
|
||||||
data: function () {
|
data() {
|
||||||
return {
|
return {
|
||||||
isSuperUser: false,
|
isSuperUser: false,
|
||||||
wallet: {},
|
wallet: {},
|
||||||
|
|
@ -79,6 +79,14 @@
|
||||||
data: {}
|
data: {}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setFeeDialog: {
|
||||||
|
show: false,
|
||||||
|
data: {
|
||||||
|
fee_ppm: 0,
|
||||||
|
fee_base_msat: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
openChannelDialog: {
|
openChannelDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
data: {}
|
data: {}
|
||||||
|
|
@ -118,12 +126,6 @@
|
||||||
name: 'pending',
|
name: 'pending',
|
||||||
label: ''
|
label: ''
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'memo',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('memo'),
|
|
||||||
field: 'memo'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
align: 'left',
|
align: 'left',
|
||||||
|
|
@ -149,6 +151,12 @@
|
||||||
align: 'right',
|
align: 'right',
|
||||||
label: 'Destination',
|
label: 'Destination',
|
||||||
field: 'destination'
|
field: 'destination'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memo',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('memo'),
|
||||||
|
field: 'memo'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|
@ -165,16 +173,10 @@
|
||||||
name: 'pending',
|
name: 'pending',
|
||||||
label: ''
|
label: ''
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'memo',
|
|
||||||
align: 'left',
|
|
||||||
label: this.$t('memo'),
|
|
||||||
field: 'memo'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'paid_at',
|
name: 'paid_at',
|
||||||
field: 'paid_at',
|
field: 'paid_at',
|
||||||
align: 'right',
|
align: 'left',
|
||||||
label: 'Paid at',
|
label: 'Paid at',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
|
|
@ -182,7 +184,7 @@
|
||||||
name: 'expiry',
|
name: 'expiry',
|
||||||
label: this.$t('expiry'),
|
label: this.$t('expiry'),
|
||||||
field: 'expiry',
|
field: 'expiry',
|
||||||
align: 'right',
|
align: 'left',
|
||||||
sortable: true
|
sortable: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -190,6 +192,12 @@
|
||||||
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
label: this.$t('amount') + ' (' + LNBITS_DENOMINATION + ')',
|
||||||
field: row => this.formatMsat(row.amount),
|
field: row => this.formatMsat(row.amount),
|
||||||
sortable: true
|
sortable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'memo',
|
||||||
|
align: 'left',
|
||||||
|
label: this.$t('memo'),
|
||||||
|
field: 'memo'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
pagination: {
|
pagination: {
|
||||||
|
|
@ -201,12 +209,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created: function () {
|
created() {
|
||||||
this.getInfo()
|
this.getInfo()
|
||||||
this.get1MLStats()
|
this.get1MLStats()
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
tab: function (val) {
|
tab(val) {
|
||||||
if (val === 'transactions' && !this.paymentsTable.data.length) {
|
if (val === 'transactions' && !this.paymentsTable.data.length) {
|
||||||
this.getPayments()
|
this.getPayments()
|
||||||
this.getInvoices()
|
this.getInvoices()
|
||||||
|
|
@ -220,14 +228,14 @@
|
||||||
checkChanges() {
|
checkChanges() {
|
||||||
return !_.isEqual(this.settings, this.formData)
|
return !_.isEqual(this.settings, this.formData)
|
||||||
},
|
},
|
||||||
filteredChannels: function () {
|
filteredChannels() {
|
||||||
return this.stateFilters
|
return this.stateFilters
|
||||||
? this.channels.data.filter(channel => {
|
? this.channels.data.filter(channel => {
|
||||||
return this.stateFilters.find(({value}) => value == channel.state)
|
return this.stateFilters.find(({value}) => value == channel.state)
|
||||||
})
|
})
|
||||||
: this.channels.data
|
: this.channels.data
|
||||||
},
|
},
|
||||||
totalBalance: function () {
|
totalBalance() {
|
||||||
return this.filteredChannels.reduce(
|
return this.filteredChannels.reduce(
|
||||||
(balance, channel) => {
|
(balance, channel) => {
|
||||||
balance.local_msat += channel.balance.local_msat
|
balance.local_msat += channel.balance.local_msat
|
||||||
|
|
@ -240,10 +248,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
formatMsat: function (msat) {
|
formatMsat(msat) {
|
||||||
return LNbits.utils.formatMsat(msat)
|
return LNbits.utils.formatMsat(msat)
|
||||||
},
|
},
|
||||||
api: function (method, url, options) {
|
api(method, url, options) {
|
||||||
const params = new URLSearchParams(options?.query)
|
const params = new URLSearchParams(options?.query)
|
||||||
return LNbits.api
|
return LNbits.api
|
||||||
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
|
.request(method, `/node/api/v1${url}?${params}`, {}, options?.data)
|
||||||
|
|
@ -251,23 +259,29 @@
|
||||||
LNbits.utils.notifyApiError(error)
|
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 => {
|
return this.api('GET', '/channels').then(response => {
|
||||||
this.channels.data = response.data
|
this.channels.data = response.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getInfo: function () {
|
getInfo() {
|
||||||
return this.api('GET', '/info').then(response => {
|
return this.api('GET', '/info').then(response => {
|
||||||
this.info = response.data
|
this.info = response.data
|
||||||
this.channel_stats = response.data.channel_stats
|
this.channel_stats = response.data.channel_stats
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
get1MLStats: function () {
|
get1MLStats() {
|
||||||
return this.api('GET', '/rank').then(response => {
|
return this.api('GET', '/rank').then(response => {
|
||||||
this.ranks = response.data
|
this.ranks = response.data
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getPayments: function (props) {
|
getPayments(props) {
|
||||||
if (props) {
|
if (props) {
|
||||||
this.paymentsTable.pagination = props.pagination
|
this.paymentsTable.pagination = props.pagination
|
||||||
}
|
}
|
||||||
|
|
@ -281,7 +295,7 @@
|
||||||
this.paymentsTable.pagination.rowsNumber = response.data.total
|
this.paymentsTable.pagination.rowsNumber = response.data.total
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getInvoices: function (props) {
|
getInvoices(props) {
|
||||||
if (props) {
|
if (props) {
|
||||||
this.invoiceTable.pagination = props.pagination
|
this.invoiceTable.pagination = props.pagination
|
||||||
}
|
}
|
||||||
|
|
@ -295,14 +309,13 @@
|
||||||
this.invoiceTable.pagination.rowsNumber = response.data.total
|
this.invoiceTable.pagination.rowsNumber = response.data.total
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getPeers: function () {
|
getPeers() {
|
||||||
return this.api('GET', '/peers').then(response => {
|
return this.api('GET', '/peers').then(response => {
|
||||||
this.peers.data = response.data
|
this.peers.data = response.data
|
||||||
console.log('peers', this.peers)
|
console.log('peers', this.peers)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
connectPeer: function () {
|
connectPeer() {
|
||||||
console.log('peer', this.connectPeerDialog)
|
|
||||||
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
|
this.api('POST', '/peers', {data: this.connectPeerDialog.data}).then(
|
||||||
() => {
|
() => {
|
||||||
this.connectPeerDialog.show = false
|
this.connectPeerDialog.show = false
|
||||||
|
|
@ -310,7 +323,7 @@
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
disconnectPeer: function (id) {
|
disconnectPeer(id) {
|
||||||
LNbits.utils
|
LNbits.utils
|
||||||
.confirmDialog('Do you really wanna disconnect this peer?')
|
.confirmDialog('Do you really wanna disconnect this peer?')
|
||||||
.onOk(() => {
|
.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})
|
this.api('POST', '/channels', {data: this.openChannelDialog.data})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
this.openChannelDialog.show = false
|
this.openChannelDialog.show = false
|
||||||
|
|
@ -334,7 +357,7 @@
|
||||||
console.log(error)
|
console.log(error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
showCloseChannelDialog: function (channel) {
|
showCloseChannelDialog(channel) {
|
||||||
this.closeChannelDialog.show = true
|
this.closeChannelDialog.show = true
|
||||||
this.closeChannelDialog.data = {
|
this.closeChannelDialog.data = {
|
||||||
force: false,
|
force: false,
|
||||||
|
|
@ -342,7 +365,7 @@
|
||||||
...channel.point
|
...channel.point
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeChannel: function () {
|
closeChannel() {
|
||||||
this.api('DELETE', '/channels', {
|
this.api('DELETE', '/channels', {
|
||||||
query: this.closeChannelDialog.data
|
query: this.closeChannelDialog.data
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
|
@ -350,15 +373,20 @@
|
||||||
this.getChannels()
|
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.show = true
|
||||||
this.openChannelDialog.data = {peer_id, funding_amount: 0}
|
this.openChannelDialog.data = {peer_id, funding_amount: 0}
|
||||||
},
|
},
|
||||||
showNodeInfoDialog: function (node) {
|
showNodeInfoDialog(node) {
|
||||||
this.nodeInfoDialog.show = true
|
this.nodeInfoDialog.show = true
|
||||||
this.nodeInfoDialog.data = node
|
this.nodeInfoDialog.data = node
|
||||||
},
|
},
|
||||||
showTransactionDetailsDialog: function (details) {
|
showTransactionDetailsDialog(details) {
|
||||||
this.transactionDetailsDialog.show = true
|
this.transactionDetailsDialog.show = true
|
||||||
this.transactionDetailsDialog.data = details
|
this.transactionDetailsDialog.data = details
|
||||||
console.log('details', details)
|
console.log('details', details)
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,14 @@ async def api_get_channels(
|
||||||
return await node.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)
|
@super_node_router.post("/channels", response_model=ChannelPoint)
|
||||||
async def api_create_channel(
|
async def api_create_channel(
|
||||||
node: Node = Depends(require_node),
|
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(
|
async def api_get_payments(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
filters: Filters = Depends(parse_filters(NodePaymentsFilters)),
|
||||||
|
|
@ -138,7 +156,7 @@ async def api_get_payments(
|
||||||
return await node.get_payments(filters)
|
return await node.get_payments(filters)
|
||||||
|
|
||||||
|
|
||||||
@node_router.get("/invoices", response_model=Page[NodeInvoice])
|
@node_router.get("/invoices")
|
||||||
async def api_get_invoices(
|
async def api_get_invoices(
|
||||||
node: Node = Depends(require_node),
|
node: Node = Depends(require_node),
|
||||||
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
filters: Filters = Depends(parse_filters(NodeInvoiceFilters)),
|
||||||
|
|
@ -151,7 +169,7 @@ async def api_get_invoices(
|
||||||
return await node.get_invoices(filters)
|
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]:
|
async def api_get_peers(node: Node = Depends(require_node)) -> List[NodePeerInfo]:
|
||||||
return await node.get_peers()
|
return await node.get_peers()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,15 +38,22 @@ class ChannelPoint(BaseModel):
|
||||||
funding_txid: str
|
funding_txid: str
|
||||||
output_index: int
|
output_index: int
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.funding_txid}:{self.output_index}"
|
||||||
|
|
||||||
|
|
||||||
class NodeChannel(BaseModel):
|
class NodeChannel(BaseModel):
|
||||||
short_id: Optional[str] = None
|
|
||||||
point: Optional[ChannelPoint] = None
|
|
||||||
peer_id: str
|
peer_id: str
|
||||||
balance: ChannelBalance
|
balance: ChannelBalance
|
||||||
state: ChannelState
|
state: ChannelState
|
||||||
name: Optional[str]
|
# could be optional for closing/pending channels on lndrest
|
||||||
color: Optional[str]
|
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):
|
class ChannelStats(BaseModel):
|
||||||
|
|
@ -144,7 +151,6 @@ class NodePaymentsFilters(FilterModel):
|
||||||
|
|
||||||
|
|
||||||
class Node(ABC):
|
class Node(ABC):
|
||||||
wallet: Wallet
|
|
||||||
|
|
||||||
def __init__(self, wallet: Wallet):
|
def __init__(self, wallet: Wallet):
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
|
|
@ -161,7 +167,7 @@ class Node(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def _get_id(self) -> str:
|
async def _get_id(self) -> str:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_peers(self) -> list[NodePeerInfo]:
|
async def get_peers(self) -> list[NodePeerInfo]:
|
||||||
peer_ids = await self.get_peer_ids()
|
peer_ids = await self.get_peer_ids()
|
||||||
|
|
@ -169,15 +175,15 @@ class Node(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_peer_ids(self) -> list[str]:
|
async def get_peer_ids(self) -> list[str]:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def connect_peer(self, uri: str):
|
async def connect_peer(self, uri: str):
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def disconnect_peer(self, peer_id: str):
|
async def disconnect_peer(self, peer_id: str):
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
|
async def _get_peer_info(self, peer_id: str) -> NodePeerInfo:
|
||||||
|
|
@ -200,7 +206,7 @@ class Node(ABC):
|
||||||
push_amount: Optional[int] = None,
|
push_amount: Optional[int] = None,
|
||||||
fee_rate: Optional[int] = None,
|
fee_rate: Optional[int] = None,
|
||||||
) -> ChannelPoint:
|
) -> ChannelPoint:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def close_channel(
|
async def close_channel(
|
||||||
|
|
@ -209,15 +215,23 @@ class Node(ABC):
|
||||||
point: Optional[ChannelPoint] = None,
|
point: Optional[ChannelPoint] = None,
|
||||||
force: bool = False,
|
force: bool = False,
|
||||||
):
|
):
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def get_channel(self, channel_id: str) -> Optional[NodeChannel]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_channels(self) -> list[NodeChannel]:
|
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
|
@abstractmethod
|
||||||
async def get_info(self) -> NodeInfoResponse:
|
async def get_info(self) -> NodeInfoResponse:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
async def get_public_info(self) -> PublicNodeInfo:
|
async def get_public_info(self) -> PublicNodeInfo:
|
||||||
info = await self.get_info()
|
info = await self.get_info()
|
||||||
|
|
@ -227,10 +241,10 @@ class Node(ABC):
|
||||||
async def get_payments(
|
async def get_payments(
|
||||||
self, filters: Filters[NodePaymentsFilters]
|
self, filters: Filters[NodePaymentsFilters]
|
||||||
) -> Page[NodePayment]:
|
) -> Page[NodePayment]:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def get_invoices(
|
async def get_invoices(
|
||||||
self, filters: Filters[NodeInvoiceFilters]
|
self, filters: Filters[NodeInvoiceFilters]
|
||||||
) -> Page[NodeInvoice]:
|
) -> Page[NodeInvoice]:
|
||||||
pass
|
raise NotImplementedError
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,28 @@ class CoreLightningNode(Node):
|
||||||
fn = getattr(self.wallet.ln, method)
|
fn = getattr(self.wallet.ln, method)
|
||||||
return await loop.run_in_executor(None, lambda: fn(*args, **kwargs))
|
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
|
@catch_rpc_errors
|
||||||
async def connect_peer(self, uri: str):
|
async def connect_peer(self, uri: str):
|
||||||
# https://docs.corelightning.org/reference/lightning-connect
|
# https://docs.corelightning.org/reference/lightning-connect
|
||||||
|
|
@ -202,48 +224,45 @@ class CoreLightningNode(Node):
|
||||||
else:
|
else:
|
||||||
return NodePeerInfo(id=node["nodeid"])
|
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
|
@catch_rpc_errors
|
||||||
async def get_channels(self) -> list[NodeChannel]:
|
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 = await self.ln_rpc("listnodes")
|
||||||
nodes_by_id = {n["nodeid"]: n for n in nodes["nodes"]}
|
nodes_by_id = {n["nodeid"]: n for n in nodes["nodes"]}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
NodeChannel(
|
NodeChannel(
|
||||||
|
id=ch["channel_id"],
|
||||||
short_id=ch.get("short_channel_id"),
|
short_id=ch.get("short_channel_id"),
|
||||||
point=ChannelPoint(
|
point=ChannelPoint(
|
||||||
funding_txid=ch["funding_txid"],
|
funding_txid=ch["funding_txid"],
|
||||||
output_index=ch["funding_output"],
|
output_index=ch["funding_outnum"],
|
||||||
),
|
),
|
||||||
peer_id=ch["peer_id"],
|
peer_id=ch["peer_id"],
|
||||||
balance=ChannelBalance(
|
balance=ChannelBalance(
|
||||||
local_msat=ch["our_amount_msat"],
|
local_msat=ch["spendable_msat"],
|
||||||
remote_msat=ch["amount_msat"] - ch["our_amount_msat"],
|
remote_msat=ch["receivable_msat"],
|
||||||
total_msat=ch["amount_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"),
|
name=nodes_by_id.get(ch["peer_id"], {}).get("alias"),
|
||||||
color=nodes_by_id.get(ch["peer_id"], {}).get("color"),
|
color=nodes_by_id.get(ch["peer_id"], {}).get("color"),
|
||||||
state=(
|
state=self._parse_state(ch["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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
for ch in funds["channels"]
|
for ch in channels["channels"]
|
||||||
]
|
]
|
||||||
|
|
||||||
@catch_rpc_errors
|
@catch_rpc_errors
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ def _decode_bytes(data: str) -> str:
|
||||||
return base64.b64decode(data).hex()
|
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:
|
def _parse_channel_point(raw: str) -> ChannelPoint:
|
||||||
funding_tx, output_index = raw.split(":")
|
funding_tx, output_index = raw.split(":")
|
||||||
return ChannelPoint(
|
return ChannelPoint(
|
||||||
|
|
@ -129,15 +137,12 @@ class LndRestNode(Node):
|
||||||
response = await self.request(
|
response = await self.request(
|
||||||
"POST",
|
"POST",
|
||||||
"/v1/channels",
|
"/v1/channels",
|
||||||
data=json.dumps(
|
json={
|
||||||
{
|
"node_pubkey": _encode_bytes(peer_id),
|
||||||
# 'node_pubkey': base64.b64encode(peer_id.encode()).decode(),
|
"sat_per_vbyte": fee_rate,
|
||||||
"node_pubkey_string": peer_id,
|
"local_funding_amount": local_amount,
|
||||||
"sat_per_vbyte": fee_rate,
|
"push_sat": push_amount,
|
||||||
"local_funding_amount": local_amount,
|
},
|
||||||
"push_sat": push_amount,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
return ChannelPoint(
|
return ChannelPoint(
|
||||||
# WHY IS THIS REVERSED?!
|
# WHY IS THIS REVERSED?!
|
||||||
|
|
@ -184,6 +189,66 @@ class LndRestNode(Node):
|
||||||
|
|
||||||
asyncio.create_task(self._close_channel(point, force)) # noqa: RUF006
|
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]:
|
async def get_channels(self) -> list[NodeChannel]:
|
||||||
normal, pending, closed = await asyncio.gather(
|
normal, pending, closed = await asyncio.gather(
|
||||||
self.get("/v1/channels"),
|
self.get("/v1/channels"),
|
||||||
|
|
@ -203,6 +268,7 @@ class LndRestNode(Node):
|
||||||
state=state,
|
state=state,
|
||||||
name=info.alias,
|
name=info.alias,
|
||||||
color=info.color,
|
color=info.color,
|
||||||
|
id=channel.get("chan_id", "node is for pending channels"),
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
point=_parse_channel_point(channel["channel_point"]),
|
||||||
balance=ChannelBalance(
|
balance=ChannelBalance(
|
||||||
local_msat=msat(channel["local_balance"]),
|
local_msat=msat(channel["local_balance"]),
|
||||||
|
|
@ -222,6 +288,7 @@ class LndRestNode(Node):
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
info = await self.get_peer_info(channel["remote_pubkey"])
|
||||||
channels.append(
|
channels.append(
|
||||||
NodeChannel(
|
NodeChannel(
|
||||||
|
id=channel.get("chan_id", "node is for closing channels"),
|
||||||
peer_id=info.id,
|
peer_id=info.id,
|
||||||
state=ChannelState.CLOSED,
|
state=ChannelState.CLOSED,
|
||||||
name=info.alias,
|
name=info.alias,
|
||||||
|
|
@ -239,6 +306,7 @@ class LndRestNode(Node):
|
||||||
info = await self.get_peer_info(channel["remote_pubkey"])
|
info = await self.get_peer_info(channel["remote_pubkey"])
|
||||||
channels.append(
|
channels.append(
|
||||||
NodeChannel(
|
NodeChannel(
|
||||||
|
id=channel["chan_id"],
|
||||||
short_id=channel["chan_id"],
|
short_id=channel["chan_id"],
|
||||||
point=_parse_channel_point(channel["channel_point"]),
|
point=_parse_channel_point(channel["channel_point"]),
|
||||||
peer_id=channel["remote_pubkey"],
|
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
|
@pytest.mark.anyio
|
||||||
async def test_channel_management(node_client):
|
async def test_channel_management(node_client):
|
||||||
async def get_channels():
|
async def get_channels():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue