From 519c18bafcd27348d15a207da8868e9003728450 Mon Sep 17 00:00:00 2001 From: Shahana Farooqui Date: Fri, 13 May 2022 17:24:37 -0400 Subject: [PATCH] Funder Policy Update Funder Policy Update --- backend/controllers/cln/channels.js | 19 +++++ backend/routes/cln/channels.js | 5 +- server/controllers/cln/channels.ts | 18 +++++ server/routes/cln/channels.ts | 6 +- src/app/cln/store/cln.effects.ts | 2 +- .../experimental-settings.component.html | 49 +++++++++++- .../experimental-settings.component.ts | 75 +++++++++++++++++-- src/app/shared/models/clModels.ts | 19 +++++ .../shared/services/consts-enums-functions.ts | 7 ++ src/app/shared/services/data.service.ts | 15 ++++ 10 files changed, 204 insertions(+), 11 deletions(-) diff --git a/backend/controllers/cln/channels.js b/backend/controllers/cln/channels.js index 7176b806..bacfdf6d 100644 --- a/backend/controllers/cln/channels.js +++ b/backend/controllers/cln/channels.js @@ -120,3 +120,22 @@ export const listForwards = (req, res, next) => { return res.status(err.statusCode).json({ message: err.message, error: err.error }); }); }; +export const funderUpdatePolicy = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting or Updating Funder Policy..' }); + options = common.getOptions(req); + if (options.error) { + return res.status(options.statusCode).json({ message: options.message, error: options.error }); + } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/funderUpdate'; + if (req.body && req.body.policy) { + options.body = req.body; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Funder Update Body', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Funder Policy Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Funder Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/backend/routes/cln/channels.js b/backend/routes/cln/channels.js index 34968fa8..68110542 100644 --- a/backend/routes/cln/channels.js +++ b/backend/routes/cln/channels.js @@ -1,12 +1,13 @@ import exprs from 'express'; const { Router } = exprs; import { isAuthenticated } from '../../utils/authCheck.js'; -import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards } from '../../controllers/cln/channels.js'; +import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy } from '../../controllers/cln/channels.js'; const router = Router(); router.get('/listChannels', isAuthenticated, listChannels); router.post('/', isAuthenticated, openChannel); router.post('/setChannelFee', isAuthenticated, setChannelFee); router.delete('/:channelId', isAuthenticated, closeChannel); -router.get('/localremotebalance', isAuthenticated, getLocalRemoteBalance); +router.get('/localRemoteBalance', isAuthenticated, getLocalRemoteBalance); router.get('/listForwards', isAuthenticated, listForwards); +router.post('/funderUpdate', isAuthenticated, funderUpdatePolicy); export default router; diff --git a/server/controllers/cln/channels.ts b/server/controllers/cln/channels.ts index 4f433742..3abe95b4 100644 --- a/server/controllers/cln/channels.ts +++ b/server/controllers/cln/channels.ts @@ -106,3 +106,21 @@ export const listForwards = (req, res, next) => { return res.status(err.statusCode).json({ message: err.message, error: err.error }); }); }; + +export const funderUpdatePolicy = (req, res, next) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Getting or Updating Funder Policy..' }); + options = common.getOptions(req); + if (options.error) { return res.status(options.statusCode).json({ message: options.message, error: options.error }); } + options.url = req.session.selectedNode.ln_server_url + '/v1/channel/funderUpdate'; + if (req.body && req.body.policy) { + options.body = req.body; + } + logger.log({ selectedNode: req.session.selectedNode, level: 'DEBUG', fileName: 'Channels', msg: 'Funder Update Body', data: options.body }); + request.post(options).then((body) => { + logger.log({ selectedNode: req.session.selectedNode, level: 'INFO', fileName: 'Channels', msg: 'Funder Policy Received', data: body }); + res.status(200).json(body); + }).catch((errRes) => { + const err = common.handleError(errRes, 'Channels', 'Funder Policy Error', req.session.selectedNode); + return res.status(err.statusCode).json({ message: err.message, error: err.error }); + }); +}; diff --git a/server/routes/cln/channels.ts b/server/routes/cln/channels.ts index 67fc2e28..de1a07bd 100644 --- a/server/routes/cln/channels.ts +++ b/server/routes/cln/channels.ts @@ -1,7 +1,7 @@ import exprs from 'express'; const { Router } = exprs; import { isAuthenticated } from '../../utils/authCheck.js'; -import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards } from '../../controllers/cln/channels.js'; +import { listChannels, openChannel, setChannelFee, closeChannel, getLocalRemoteBalance, listForwards, funderUpdatePolicy } from '../../controllers/cln/channels.js'; const router = Router(); @@ -10,7 +10,9 @@ router.post('/', isAuthenticated, openChannel); router.post('/setChannelFee', isAuthenticated, setChannelFee); router.delete('/:channelId', isAuthenticated, closeChannel); -router.get('/localremotebalance', isAuthenticated, getLocalRemoteBalance); +router.get('/localRemoteBalance', isAuthenticated, getLocalRemoteBalance); router.get('/listForwards', isAuthenticated, listForwards); +router.post('/funderUpdate', isAuthenticated, funderUpdatePolicy); + export default router; diff --git a/src/app/cln/store/cln.effects.ts b/src/app/cln/store/cln.effects.ts index a26a0f91..e417cd62 100644 --- a/src/app/cln/store/cln.effects.ts +++ b/src/app/cln/store/cln.effects.ts @@ -198,7 +198,7 @@ export class CLNEffects implements OnDestroy { ofType(CLNActions.FETCH_LOCAL_REMOTE_BALANCE_CLN), mergeMap(() => { this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'FetchLocalRemoteBalance', status: APICallStatusEnum.INITIATED } })); - return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/localremotebalance'); + return this.httpClient.get(this.CHILD_API_URL + environment.CHANNELS_API + '/localRemoteBalance'); }), map((lrBalance) => { this.logger.info(lrBalance); diff --git a/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.html b/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.html index 9cb84212..8189a9eb 100644 --- a/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.html +++ b/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.html @@ -9,7 +9,7 @@ Features - +

{{feature.name}}

@@ -37,6 +37,53 @@ Enable Offers {{enableOffers ? '(You can find Offers under Lightning -> Transactions -> Offers)' : ''}} +
+
+ + These config changes should be configured permanently via the config file on your CLN node otherwise the policy would need to be configured again, if your node restarts. +
+
+ + + + {{policyType.id | titlecase}} + + + + + + {{selPolicyType.placeholder}} should be between {{selPolicyType.min}} and {{selPolicyType.max}} + {{selPolicyType.placeholder}} is required. + {{selPolicyType.placeholder}} must be greater than or equal to {{selPolicyType.min}}. + {{selPolicyType.placeholder}} must be less than or equal to {{selPolicyType.max}}. + +
+
+ + + Lease base fee is required. + + + + Lease base basis is required. + +
+
+ + + Max channel routing base fee is required. + + + + Max channel routing fee rate is required. + +
+

{{(updateMsg.error && updateMsg.error !== '') ? (('Error: ' + updateMsg.error) || 'Unknown Error') : ((updateMsg) || 'Successfully Updated the Funding Policy!')}}

+
+ + +
+
diff --git a/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.ts b/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.ts index 7e143a61..3ac9b09a 100644 --- a/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.ts +++ b/src/app/shared/components/node-config/experimental-settings/experimental-settings.component.ts @@ -12,7 +12,11 @@ import { updateServiceSettings } from '../../../../store/rtl.actions'; import { setChildNodeSettingsLND } from '../../../../lnd/store/lnd.actions'; import { setChildNodeSettingsCL } from '../../../../cln/store/cln.actions'; import { setChildNodeSettingsECL } from '../../../../eclair/store/ecl.actions'; -import { ServicesEnum, UI_MESSAGES } from '../../../services/consts-enums-functions'; +import { DataService } from '../../../services/data.service'; +import { ServicesEnum, UI_MESSAGES, LADS_POLICY } from '../../../services/consts-enums-functions'; +import { balance } from '../../../../cln/store/cln.selector'; +import { Balance, FunderPolicy } from '../../../models/clModels'; +import { ApiCallStatusPayload } from '../../../models/apiCallsPayload'; @Component({ selector: 'rtl-experimental-settings', @@ -24,21 +28,36 @@ export class ExperimentalSettingsComponent implements OnInit, OnDestroy { public faInfoCircle = faInfoCircle; public faExclamationTriangle = faExclamationTriangle; public faCode = faCode; - public features = [{ name: 'Offers', enabled: false }]; + public features = [{ name: 'Offers', enabled: false }, { name: 'Channel Funding Policy', enabled: false }]; public enableOffers = false; public selNode: ConfigSettingsNode; - private unSubs: Array> = [new Subject(), new Subject(), new Subject()]; + public fundingPolicy: FunderPolicy = null; + public policyTypes = LADS_POLICY; + public selPolicyType = LADS_POLICY[0]; + public policyMod: number; + public leaseFeeBaseSat: number; + public leaseFeeBasis: number; + public channelFeeMaxBaseSat: number; + public channelFeeMaxProportional: number; + public flgUpdateCalled = false; + public updateMsg = ''; + private unSubs: Array> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()]; - constructor(private logger: LoggerService, private store: Store) { } + + constructor(private logger: LoggerService, private store: Store, private dataService: DataService) { } ngOnInit() { - this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[0])). + this.store.select(rootSelectedNode).pipe(takeUntil(this.unSubs[1])). subscribe((selNode) => { this.selNode = selNode; this.enableOffers = this.selNode.settings.enableOffers; this.features[0].enabled = this.enableOffers; this.logger.info(this.selNode); }); + this.store.select(balance).pipe(takeUntil(this.unSubs[2])). + subscribe((balanceSeletor: { balance: Balance, apiCallStatus: ApiCallStatusPayload }) => { + this.policyTypes[2].max = balanceSeletor.balance.totalBalance || 1000; + }); } onUpdateFeature(): boolean | void { @@ -66,6 +85,52 @@ export class ExperimentalSettingsComponent implements OnInit, OnDestroy { })); } + onPanelOpen(i) { + if (i === 1 && !this.fundingPolicy) { + this.dataService.getOrUpdateFunderPolicy().pipe(takeUntil(this.unSubs[0])).subscribe((res: any) => { + this.logger.info('Received Funder Update Policy: ' + JSON.stringify(res)); + this.fundingPolicy = res; + if (this.fundingPolicy.policy) { + this.selPolicyType = LADS_POLICY.find((policy) => policy.id === this.fundingPolicy.policy); + } + this.policyMod = this.fundingPolicy.policy_mod || this.fundingPolicy.policy_mod === 0 ? this.fundingPolicy.policy_mod : null; + this.leaseFeeBaseSat = this.fundingPolicy.lease_fee_base_msat ? this.fundingPolicy.lease_fee_base_msat / 1000 : this.fundingPolicy.lease_fee_base_msat === 0 ? 0 : null; + this.leaseFeeBasis = this.fundingPolicy.lease_fee_basis || this.fundingPolicy.lease_fee_basis === 0 ? this.fundingPolicy.lease_fee_basis : null; + this.channelFeeMaxBaseSat = this.fundingPolicy.channel_fee_max_base_msat ? this.fundingPolicy.channel_fee_max_base_msat / 1000 : this.fundingPolicy.channel_fee_max_base_msat === 0 ? 0 : null; + this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths || this.fundingPolicy.channel_fee_max_proportional_thousandths === 0 ? this.fundingPolicy.channel_fee_max_proportional_thousandths : null; + }); + } + } + + onUpdateFundingPolicy() { + this.flgUpdateCalled = false; + this.updateMsg = ''; + this.dataService.getOrUpdateFunderPolicy(this.selPolicyType.id, this.policyMod, this.leaseFeeBaseSat, this.leaseFeeBasis, this.channelFeeMaxBaseSat * 1000, this.channelFeeMaxProportional). + pipe(takeUntil(this.unSubs[3])). + subscribe({ + next: (updatePolicyRes: any) => { + this.logger.info(updatePolicyRes); + this.fundingPolicy = updatePolicyRes; + this.onResetPolicy(); + this.updateMsg = 'Compact Lease: ' + updatePolicyRes.compact_lease; + }, error: (err) => { + this.logger.error(err); + this.updateMsg = JSON.stringify(err.error.error.message); + } + }); + } + + onResetPolicy() { + this.flgUpdateCalled = false; + this.updateMsg = ''; + this.selPolicyType = LADS_POLICY[0]; + this.policyMod = this.fundingPolicy.policy_mod !== 0 && !this.fundingPolicy.policy_mod ? null : this.fundingPolicy.policy_mod; + this.leaseFeeBaseSat = this.fundingPolicy.lease_fee_base_msat !== 0 && !this.fundingPolicy.lease_fee_base_msat ? null : this.fundingPolicy.lease_fee_base_msat / 1000; + this.leaseFeeBasis = this.fundingPolicy.lease_fee_basis !== 0 && !this.fundingPolicy.lease_fee_basis ? null : this.fundingPolicy.lease_fee_basis; + this.channelFeeMaxBaseSat = this.fundingPolicy.channel_fee_max_base_msat !== 0 && !this.fundingPolicy.channel_fee_max_base_msat ? null : this.fundingPolicy.channel_fee_max_base_msat / 1000; + this.channelFeeMaxProportional = this.fundingPolicy.channel_fee_max_proportional_thousandths !== 0 && !this.fundingPolicy.channel_fee_max_proportional_thousandths ? null : this.fundingPolicy.channel_fee_max_proportional_thousandths; + } + ngOnDestroy() { this.unSubs.forEach((completeSub) => { completeSub.next(null); diff --git a/src/app/shared/models/clModels.ts b/src/app/shared/models/clModels.ts index ce8955f1..61ef2b51 100644 --- a/src/app/shared/models/clModels.ts +++ b/src/app/shared/models/clModels.ts @@ -454,3 +454,22 @@ export interface FetchInvoices { index_offset?: number; reversed?: boolean; } + +export interface FunderPolicy { + summary?: string; + policy?: string; + policy_mod?: number; + leases_only?: boolean; + min_their_funding_msat?: string; + max_their_funding_msat?: string; + per_channel_min_msat?: string; + per_channel_max_msat?: string; + reserve_tank_msat?: string; + fuzz_percent?: number; + fund_probability?: number; + lease_fee_base_msat?: number; + lease_fee_basis?: number; + funding_weight?: number; + channel_fee_max_base_msat?: number; + channel_fee_max_proportional_thousandths?: number; +} diff --git a/src/app/shared/services/consts-enums-functions.ts b/src/app/shared/services/consts-enums-functions.ts index 12ea7aa0..c7949f21 100644 --- a/src/app/shared/services/consts-enums-functions.ts +++ b/src/app/shared/services/consts-enums-functions.ts @@ -317,6 +317,7 @@ export const UI_MESSAGES = { DISABLE_OFFER: 'Disabling Offer...', CREATE_OFFER: 'Creating Offer...', DELETE_OFFER_BOOKMARK: 'Deleting Bookmark...', + GET_FUNDER_POLICY: 'Getting Or Updating Funder Policy...', LOG_OUT: 'Logging Out...' }; @@ -615,3 +616,9 @@ export enum NodeFeaturesLND { 'anchors-zero-fee-htlc-tx' = 'Anchor commitment type with zero fee HTLC transactions', 'amp' = 'AMP' }; + +export const LADS_POLICY = [ + { id: 'match', placeholder: 'Policy Match (%age)', min: 0, max: 200 }, + { id: 'available', placeholder: 'Policy Available (%age)', min: 0, max: 100 }, + { id: 'fixed', placeholder: 'Fixed Policy (Sats)', min: 0, max: 100 } +]; diff --git a/src/app/shared/services/data.service.ts b/src/app/shared/services/data.service.ts index d6b44851..68d06be3 100644 --- a/src/app/shared/services/data.service.ts +++ b/src/app/shared/services/data.service.ts @@ -247,6 +247,21 @@ export class DataService implements OnDestroy { })); } + getOrUpdateFunderPolicy(policy?: any, policyMod?: any, leaseFeeBaseMsat?: any, leaseFeeBasis?: any, channelFeeMaxBaseMsat?: any, channelFeeMaxProportional?: any) { + const postParams = policy ? { policy: policy, policyMod: policyMod, leaseFeeBaseMsat: leaseFeeBaseMsat, leaseFeeBasis: leaseFeeBasis, channelFeeMaxBaseMsat: channelFeeMaxBaseMsat, channelFeeMaxProportional: channelFeeMaxProportional } : null; + this.store.dispatch(openSpinner({ payload: UI_MESSAGES.GET_FUNDER_POLICY })); + return this.httpClient.post(this.childAPIUrl + environment.CHANNELS_API + '/funderUpdate', postParams).pipe( + takeUntil(this.unSubs[8]), + map((res) => { + this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.GET_FUNDER_POLICY })); + return res; + }), catchError((err) => { + this.handleErrorWithoutAlert('Funder Policy', UI_MESSAGES.GET_FUNDER_POLICY, err); + return throwError(() => new Error(this.extractErrorMessage(err))); + }) + ); + } + extractErrorMessage(err: any, genericErrorMessage: string = 'Unknown Error.') { return this.titleCasePipe.transform((err.error && err.error.error && err.error.error.error && err.error.error.error.error && err.error.error.error.error.error && typeof err.error.error.error.error.error === 'string') ? err.error.error.error.error.error : (err.error && err.error.error && err.error.error.error && err.error.error.error.error && typeof err.error.error.error.error === 'string') ? err.error.error.error.error :