Swap In & Swap Out

Swap In & Swap Out
pull/1088/head
ShahanaFarooqui 2 years ago
parent 42425ebe26
commit 04bb6eaade

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -13,6 +13,6 @@
<style>@font-face{font-family:Roboto;src:url(Roboto-Thin.f7a95c9c5999532c.woff2) format("woff2"),url(Roboto-Thin.c13c157cb81e8ebb.woff) format("woff");font-weight:100;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-ThinItalic.b0e084abf689f393.woff2) format("woff2"),url(Roboto-ThinItalic.1111028df6cea564.woff) format("woff");font-weight:100;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Light.0e01b6cd13b3857f.woff2) format("woff2"),url(Roboto-Light.603ca9a537b88428.woff) format("woff");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-LightItalic.232ef4b20215f720.woff2) format("woff2"),url(Roboto-LightItalic.1b5e142f787151c8.woff) format("woff");font-weight:300;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Regular.475ba9e4e2d63456.woff2) format("woff2"),url(Roboto-Regular.bcefbfee882bc1cb.woff) format("woff");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-RegularItalic.e3a9ebdaac06bbc4.woff2) format("woff2"),url(Roboto-RegularItalic.0668fae6af0cf8c2.woff) format("woff");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Medium.457532032ceb0168.woff2) format("woff2"),url(Roboto-Medium.6e1ae5f0b324a0aa.woff) format("woff");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-MediumItalic.872f7060602d55d2.woff2) format("woff2"),url(Roboto-MediumItalic.e06fb533801cbb08.woff) format("woff");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Bold.447291a88c067396.woff2) format("woff2"),url(Roboto-Bold.fc482e6133cf5e26.woff) format("woff");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BoldItalic.1b15168ef6fa4e16.woff2) format("woff2"),url(Roboto-BoldItalic.e26ba339b06f09f7.woff) format("woff");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url(Roboto-Black.2eaa390d458c877d.woff2) format("woff2"),url(Roboto-Black.b25f67ad8583da68.woff) format("woff");font-weight:900;font-style:normal}@font-face{font-family:Roboto;src:url(Roboto-BlackItalic.7dc03ee444552bc5.woff2) format("woff2"),url(Roboto-BlackItalic.c8dc642467cb3099.woff) format("woff");font-weight:900;font-style:italic}html{width:100%;height:99%;line-height:1.5;overflow-x:hidden;font-family:Roboto,sans-serif!important;font-size:62.5%}body{box-sizing:border-box;height:100%;margin:0;overflow:hidden}*{margin:0;padding:0}</style><link rel="stylesheet" href="styles.43515fc39338348b.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles.43515fc39338348b.css"></noscript></head>
<body>
<rtl-app></rtl-app>
<script src="runtime.84a51fcf32f6aa74.js" type="module"></script><script src="polyfills.eddc63f1737a019a.js" type="module"></script><script src="main.08b18bef419c8b7e.js" type="module"></script>
<script src="runtime.6449ae8affb0dea2.js" type="module"></script><script src="polyfills.eddc63f1737a019a.js" type="module"></script><script src="main.b852398d8716a19c.js" type="module"></script>
</body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],s=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(s=!1,o<a&&(a=o));if(s){e.splice(i--,1);var l=f();void 0!==l&&(n=l)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{275:"517caefe8eeb635c",508:"06f7dec065381b97",515:"cb8cf5fc374ab82d",924:"1c1eb885f1f101d2"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==o)for(var d=document.getElementsByTagName("script"),l=0;l<d.length;l++){var u=d[l];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==n+o){a=u;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+o),a.src=r.tu(t)),e[t]=[f];var c=(m,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(_=>_(b)),m)return m(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:n=>n},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((u,c)=>i=e[f]=[u,c]);o.push(i[2]=a);var s=r.p+r.u(f),d=new Error;r.l(s,u=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;d.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",d.name="ChunkLoadError",d.type=c,d.request=p,i[1](d)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var d,l,[i,a,s]=o,u=0;if(i.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(s)var c=s(r)}for(f&&f(o);u<i.length;u++)r.o(e,l=i[u])&&e[l]&&e[l][0](),e[l]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();
(()=>{"use strict";var e,v={},g={};function r(e){var n=g[e];if(void 0!==n)return n.exports;var t=g[e]={id:e,loaded:!1,exports:{}};return v[e].call(t.exports,t,t.exports,r),t.loaded=!0,t.exports}r.m=v,e=[],r.O=(n,t,f,o)=>{if(!t){var a=1/0;for(i=0;i<e.length;i++){for(var[t,f,o]=e[i],s=!0,d=0;d<t.length;d++)(!1&o||a>=o)&&Object.keys(r.O).every(b=>r.O[b](t[d]))?t.splice(d--,1):(s=!1,o<a&&(a=o));if(s){e.splice(i--,1);var l=f();void 0!==l&&(n=l)}}return n}o=o||0;for(var i=e.length;i>0&&e[i-1][2]>o;i--)e[i]=e[i-1];e[i]=[t,f,o]},r.n=e=>{var n=e&&e.__esModule?()=>e.default:()=>e;return r.d(n,{a:n}),n},r.d=(e,n)=>{for(var t in n)r.o(n,t)&&!r.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:n[t]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce((n,t)=>(r.f[t](e,n),n),[])),r.u=e=>e+"."+{253:"256a01ccdc95a5d7",508:"06f7dec065381b97",515:"da134be35cc26574",924:"e98936d5bf0dd5da"}[e]+".js",r.miniCssF=e=>{},r.o=(e,n)=>Object.prototype.hasOwnProperty.call(e,n),(()=>{var e={},n="RTLApp:";r.l=(t,f,o,i)=>{if(e[t])e[t].push(f);else{var a,s;if(void 0!==o)for(var d=document.getElementsByTagName("script"),l=0;l<d.length;l++){var u=d[l];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==n+o){a=u;break}}a||(s=!0,(a=document.createElement("script")).type="module",a.charset="utf-8",a.timeout=120,r.nc&&a.setAttribute("nonce",r.nc),a.setAttribute("data-webpack",n+o),a.src=r.tu(t)),e[t]=[f];var c=(m,b)=>{a.onerror=a.onload=null,clearTimeout(p);var h=e[t];if(delete e[t],a.parentNode&&a.parentNode.removeChild(a),h&&h.forEach(_=>_(b)),m)return m(b)},p=setTimeout(c.bind(null,void 0,{type:"timeout",target:a}),12e4);a.onerror=c.bind(null,a.onerror),a.onload=c.bind(null,a.onload),s&&document.head.appendChild(a)}}})(),r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),(()=>{var e;r.tt=()=>(void 0===e&&(e={createScriptURL:n=>n},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(e=trustedTypes.createPolicy("angular#bundler",e))),e)})(),r.tu=e=>r.tt().createScriptURL(e),r.p="",(()=>{var e={666:0};r.f.j=(f,o)=>{var i=r.o(e,f)?e[f]:void 0;if(0!==i)if(i)o.push(i[2]);else if(666!=f){var a=new Promise((u,c)=>i=e[f]=[u,c]);o.push(i[2]=a);var s=r.p+r.u(f),d=new Error;r.l(s,u=>{if(r.o(e,f)&&(0!==(i=e[f])&&(e[f]=void 0),i)){var c=u&&("load"===u.type?"missing":u.type),p=u&&u.target&&u.target.src;d.message="Loading chunk "+f+" failed.\n("+c+": "+p+")",d.name="ChunkLoadError",d.type=c,d.request=p,i[1](d)}},"chunk-"+f,f)}else e[f]=0},r.O.j=f=>0===e[f];var n=(f,o)=>{var d,l,[i,a,s]=o,u=0;if(i.some(p=>0!==e[p])){for(d in a)r.o(a,d)&&(r.m[d]=a[d]);if(s)var c=s(r)}for(f&&f(o);u<i.length;u++)r.o(e,l=i[u])&&e[l]&&e[l][0](),e[l]=0;return r.O(c)},t=self.webpackChunkRTLApp=self.webpackChunkRTLApp||[];t.forEach(n.bind(null,0)),t.push=n.bind(null,t.push.bind(t))})()})();

@ -64,6 +64,8 @@ import { SwapPeersComponent } from './ln-services/peerswap/swap-peers/swap-peers
import { PeerswapsCancelledComponent } from './ln-services/peerswap/swaps-cancelled/swaps-cancelled.component';
import { PeerswapsInComponent } from './ln-services/peerswap/swaps-in/swaps-in.component';
import { PeerswapsOutComponent } from './ln-services/peerswap/swaps-out/swaps-out.component';
import { CLNSwapOutModalComponent } from './ln-services/peerswap/swap-out-modal/swap-out-modal.component';
import { CLNSwapInModalComponent } from './ln-services/peerswap/swap-in-modal/swap-in-modal.component';
import { CLNUnlockedGuard } from '../shared/services/auth.guard';
@ -133,7 +135,9 @@ import { CLNUnlockedGuard } from '../shared/services/auth.guard';
SwapPeersComponent,
PeerswapsCancelledComponent,
PeerswapsInComponent,
PeerswapsOutComponent
PeerswapsOutComponent,
CLNSwapOutModalComponent,
CLNSwapInModalComponent
],
providers: [
CLNUnlockedGuard

@ -12,7 +12,7 @@
<div fxLayout="column" fxLayoutAlign="space-between stretch" fxLayoutAlign.gt-sm="space-between center" fxLayout.gt-sm="row wrap">
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
<input autoFocus matInput [(ngModel)]="requestedAmount" (keyup)="calculateFee()" placeholder="Requested Amount" type="number" [step]="10000" [min]="0" tabindex="1" required name="ramount" #ramount="ngModel">
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="ramount.errors?.required">Requested amount is required.</mat-error>
</mat-form-field>
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
@ -23,7 +23,7 @@
<mat-form-field fxFlex="30" fxLayoutAlign="start end">
<input matInput [(ngModel)]="localAmount" placeholder="Local Amount" type="number" [step]="10000" [min]="20000" [max]="totalBalance" tabindex="3" required name="lamount" #lamount="ngModel">
<mat-hint>Remaining Bal: {{totalBalance - ((localAmount) ? localAmount : 0) | number}}</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="lamount.errors?.required">Local amount is required.</mat-error>
<mat-error *ngIf="lamount.errors?.min">Local amount must be greater than or equal to 20,000 Sats. It's required to cover the channel force close fee, if needed.</mat-error>
<mat-error *ngIf="lamount.errors?.max">Local amount must be less than or equal to {{totalBalance}}.</mat-error>

@ -0,0 +1,36 @@
<div fxLayout="row">
<div fxFlex="100">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<div fxFlex="95" fxLayoutAlign="start start">
<span class="page-title">Initiate a Swapin</span>
</div>
<button tabindex="5" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" [mat-dialog-close]="false" default
mat-button>X</button>
</mat-card-header>
<mat-card-content class="padding-gap-x-large">
<form fxLayout="row wrap" fxLayoutAlign="start space-between" fxFlex="100" #swapInForm="ngForm">
<p fxLayoutAlign="start center" class="pb-2 word-break">Swapin with {{sPeer?.alias}}</p>
<mat-form-field fxFlex="100" fxLayoutAlign="start end">
<input matInput [value]="sPeer.short_channel_id" placeholder="Short Channel ID" tabindex="1" name="shortChanId" disabled>
</mat-form-field>
<mat-form-field fxFlex="100">
<input matInput autoFocus [(ngModel)]="swapAmount" (keyup)="onAmountChange()" placeholder="Amount"
type="number" [step]="100" [min]="1" [max]="sPeer.remote_balance" tabindex="2" name="swapAmt" #swapAmt="ngModel" required>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="swapAmt.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="swapAmt.errors?.max">Amount must be less than or equal to {{sPeer.remote_balance}}.</mat-error>
<mat-hint>Remaining Local: {{sPeer.remote_balance - ((swapAmount) ? swapAmount : 0) | number}}<br>{{swapAmountHint}}</mat-hint>
</mat-form-field>
<div fxFlex="100" class="alert alert-danger mt-2" *ngIf="swapInError !== ''">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
<span *ngIf="swapInError !== ''">{{swapInError}}</span>
</div>
<div fxLayout="row" fxFlex="100" class="mt-1" fxLayoutAlign="end center">
<button class="mr-1" mat-button color="primary" tabindex="3" type="button" (click)="resetData()">Clear
Field</button>
<button mat-button color="primary" (click)="onExecuteSwapin()" tabindex="4">Execute</button>
</div>
</form>
</mat-card-content>
</div>
</div>

@ -0,0 +1,95 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CommonService } from '../../../../shared/services/common.service';
import { DataService } from '../../../../shared/services/data.service';
import { RootReducer } from '../../../../store/rtl.reducers';
import { LNDReducer } from '../../../../lnd/store/lnd.reducers';
import { CLNReducer } from '../../../store/cln.reducers';
import { ECLReducer } from '../../../../eclair/store/ecl.reducers';
import { mockCLEffects, mockDataService, mockECLEffects, mockLNDEffects, mockMatDialogRef, mockRTLEffects } from '../../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../../shared/shared.module';
import { CLNSwapInModalComponent } from './swap-in-modal.component';
describe('CLNSwapInModalComponent', () => {
let component: CLNSwapInModalComponent;
let fixture: ComponentFixture<CLNSwapInModalComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CLNSwapInModalComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }),
EffectsModule.forRoot([mockRTLEffects, mockLNDEffects, mockCLEffects, mockECLEffects])
],
providers: [
CommonService,
{ provide: DataService, useClass: mockDataService },
{ provide: MatDialogRef, useClass: mockMatDialogRef },
{
provide: MAT_DIALOG_DATA, useValue: {
swapPeer: {
nodeid: '02c9fc0cc737abcff2b502076c43f123451e542e96a79ae177059f3bb796add784',
swaps_allowed: true,
supported_assets: [
'btc'
],
channels: [
{
short_channel_id: '104239x1x1',
local_balance: 9589761,
remote_balance: 410239,
local_percentage: 0.9589761,
state: 'CHANNELD_NORMAL'
},
{
short_channel_id: '104770x6x1',
local_balance: 200000,
remote_balance: 0,
local_percentage: 1,
state: 'CHANNELD_NORMAL'
}
],
sent: {
total_swaps_out: 2,
total_swaps_in: 2,
total_sats_swapped_out: 1110154,
total_sats_swapped_in: 600154
},
received: {
total_swaps_out: 1,
total_swaps_in: 0,
total_sats_swapped_out: 100000,
total_sats_swapped_in: 0
},
total_fee_paid: 478,
alias: 'nodeSignet',
short_channel_id: '104239x1x1',
local_balance: 9589761,
remote_balance: 410239,
local_percentage: 0.9589761,
state: 'CHANNELD_NORMAL'
}
}
}
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLNSwapInModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,91 @@
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { CLNSwapInformation } from '../../../../shared/models/alertData';
import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, APICallStatusEnum, CLNActions } from '../../../../shared/services/consts-enums-functions';
import { SwapPeerChannelsFlattened } from '../../../../shared/models/clnModels';
import { CommonService } from '../../../../shared/services/common.service';
import { RTLState } from '../../../../store/rtl.state';
import { SelNodeChild } from '../../../../shared/models/RTLconfig';
import { clnNodeSettings } from '../../../store/cln.selector';
import { swapIn } from '../../../store/cln.actions';
@Component({
selector: 'rtl-swap-in-modal',
templateUrl: './swap-in-modal.component.html',
styleUrls: ['./swap-in-modal.component.scss']
})
export class CLNSwapInModalComponent implements OnInit, OnDestroy {
public faExclamationTriangle = faExclamationTriangle;
public selNode: SelNodeChild | null = {};
public sPeer: SwapPeerChannelsFlattened | null = null;
public swapAmount: number | null;
public swapAmountHint = '';
public swapInError = '';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<CLNSwapInModalComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNSwapInformation, private store: Store<RTLState>, private decimalPipe: DecimalPipe, private commonService: CommonService, private actions: Actions) { }
ngOnInit() {
this.sPeer = this.data.swapPeer;
this.store.select(clnNodeSettings).pipe(takeUntil(this.unSubs[0])).subscribe((nodeSettings: SelNodeChild | null) => {
this.selNode = nodeSettings;
});
this.actions.pipe(
takeUntil(this.unSubs[1]),
filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN)).
subscribe((action: any) => {
if (action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN && action.payload.action === 'PeerswapSwapin') {
if (action.payload.status === APICallStatusEnum.ERROR) {
this.swapInError = action.payload.message;
}
if (action.payload.status === APICallStatusEnum.COMPLETED) {
this.dialogRef.close();
}
}
});
}
onExecuteSwapin(): boolean | void {
this.swapInError = '';
if (!this.swapAmount || !this.sPeer || !this.sPeer.short_channel_id) { return true; }
this.store.dispatch(swapIn({ payload: { amountSats: this.swapAmount, shortChannelId: this.sPeer?.short_channel_id, asset: 'btc' } }));
}
resetData() {
this.swapAmount = null;
this.swapAmountHint = '';
this.swapInError = '';
}
onAmountChange() {
if (this.selNode && this.selNode.fiatConversion && this.swapAmount && this.swapAmount > 99) {
this.swapAmountHint = '';
this.commonService.convertCurrency(this.swapAmount, CurrencyUnitEnum.SATS, CurrencyUnitEnum.OTHER, (this.selNode.currencyUnits && this.selNode.currencyUnits.length > 2 ? this.selNode.currencyUnits[2] : ''), this.selNode.fiatConversion).
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.swapAmountHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.swapAmountHint = 'Conversion Error: ' + err;
}
});
}
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);
completeSub.complete();
});
}
}

@ -0,0 +1,36 @@
<div fxLayout="row">
<div fxFlex="100">
<mat-card-header fxLayout="row" fxLayoutAlign="space-between center" class="modal-info-header">
<div fxFlex="95" fxLayoutAlign="start start">
<span class="page-title">Initiate a Swapout</span>
</div>
<button tabindex="5" fxFlex="5" fxLayoutAlign="center" class="btn-close-x p-0" [mat-dialog-close]="false" default
mat-button>X</button>
</mat-card-header>
<mat-card-content class="padding-gap-x-large">
<form fxLayout="row wrap" fxLayoutAlign="start space-between" fxFlex="100" #swapOutForm="ngForm">
<p fxLayoutAlign="start center" class="pb-2 word-break">Swapout with {{sPeer?.alias}}</p>
<mat-form-field fxFlex="100" fxLayoutAlign="start end">
<input matInput [value]="sPeer.short_channel_id" placeholder="Short Channel ID" tabindex="1" name="shortChanId" disabled>
</mat-form-field>
<mat-form-field fxFlex="100">
<input matInput autoFocus [(ngModel)]="swapAmount" (keyup)="onAmountChange()" placeholder="Amount"
type="number" [step]="100" [min]="1" [max]="sPeer.local_balance" tabindex="2" name="swapAmt" #swapAmt="ngModel" required>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="swapAmt.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="swapAmt.errors?.max">Amount must be less than or equal to {{sPeer.local_balance}}.</mat-error>
<mat-hint>Remaining Local: {{sPeer.local_balance - ((swapAmount) ? swapAmount : 0) | number}}<br>{{swapAmountHint}}</mat-hint>
</mat-form-field>
<div fxFlex="100" class="alert alert-danger mt-2" *ngIf="swapOutError !== ''">
<fa-icon [icon]="faExclamationTriangle" class="mr-1 alert-icon"></fa-icon>
<span *ngIf="swapOutError !== ''">{{swapOutError}}</span>
</div>
<div fxLayout="row" fxFlex="100" class="mt-1" fxLayoutAlign="end center">
<button class="mr-1" mat-button color="primary" tabindex="3" type="button" (click)="resetData()">Clear
Field</button>
<button mat-button color="primary" (click)="onExecuteSwapout()" tabindex="4">Execute</button>
</div>
</form>
</mat-card-content>
</div>
</div>

@ -0,0 +1,95 @@
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CommonService } from '../../../../shared/services/common.service';
import { DataService } from '../../../../shared/services/data.service';
import { RootReducer } from '../../../../store/rtl.reducers';
import { LNDReducer } from '../../../../lnd/store/lnd.reducers';
import { CLNReducer } from '../../../../cln/store/cln.reducers';
import { ECLReducer } from '../../../../eclair/store/ecl.reducers';
import { mockCLEffects, mockDataService, mockECLEffects, mockLNDEffects, mockMatDialogRef, mockRTLEffects } from '../../../../shared/test-helpers/mock-services';
import { SharedModule } from '../../../../shared/shared.module';
import { CLNSwapOutModalComponent } from './swap-out-modal.component';
describe('CLNSwapOutModalComponent', () => {
let component: CLNSwapOutModalComponent;
let fixture: ComponentFixture<CLNSwapOutModalComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [CLNSwapOutModalComponent],
imports: [
BrowserAnimationsModule,
SharedModule,
StoreModule.forRoot({ root: RootReducer, lnd: LNDReducer, cln: CLNReducer, ecl: ECLReducer }),
EffectsModule.forRoot([mockRTLEffects, mockLNDEffects, mockCLEffects, mockECLEffects])
],
providers: [
CommonService,
{ provide: DataService, useClass: mockDataService },
{ provide: MatDialogRef, useClass: mockMatDialogRef },
{
provide: MAT_DIALOG_DATA, useValue: {
swapPeer: {
nodeid: '02c9fc0cc737abcff2b502076c43f123451e542e96a79ae177059f3bb796add784',
swaps_allowed: true,
supported_assets: [
'btc'
],
channels: [
{
short_channel_id: '104239x1x1',
local_balance: 9589761,
remote_balance: 410239,
local_percentage: 0.9589761,
state: 'CHANNELD_NORMAL'
},
{
short_channel_id: '104770x6x1',
local_balance: 200000,
remote_balance: 0,
local_percentage: 1,
state: 'CHANNELD_NORMAL'
}
],
sent: {
total_swaps_out: 2,
total_swaps_in: 2,
total_sats_swapped_out: 1110154,
total_sats_swapped_in: 600154
},
received: {
total_swaps_out: 1,
total_swaps_in: 0,
total_sats_swapped_out: 100000,
total_sats_swapped_in: 0
},
total_fee_paid: 478,
alias: 'nodeSignet',
short_channel_id: '104239x1x1',
local_balance: 9589761,
remote_balance: 410239,
local_percentage: 0.9589761,
state: 'CHANNELD_NORMAL'
}
}
}
]
}).
compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CLNSwapOutModalComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,91 @@
import { Component, OnInit, OnDestroy, Inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { Store } from '@ngrx/store';
import { Actions } from '@ngrx/effects';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { CLNSwapInformation } from '../../../../shared/models/alertData';
import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, APICallStatusEnum, CLNActions } from '../../../../shared/services/consts-enums-functions';
import { SwapPeerChannelsFlattened } from '../../../../shared/models/clnModels';
import { CommonService } from '../../../../shared/services/common.service';
import { RTLState } from '../../../../store/rtl.state';
import { SelNodeChild } from '../../../../shared/models/RTLconfig';
import { clnNodeSettings } from '../../../store/cln.selector';
import { swapOut } from '../../../store/cln.actions';
@Component({
selector: 'rtl-swap-out-modal',
templateUrl: './swap-out-modal.component.html',
styleUrls: ['./swap-out-modal.component.scss']
})
export class CLNSwapOutModalComponent implements OnInit, OnDestroy {
public faExclamationTriangle = faExclamationTriangle;
public selNode: SelNodeChild | null = {};
public sPeer: SwapPeerChannelsFlattened | null = null;
public swapAmount: number | null;
public swapAmountHint = '';
public swapOutError = '';
private unSubs: Array<Subject<void>> = [new Subject(), new Subject(), new Subject(), new Subject(), new Subject()];
constructor(public dialogRef: MatDialogRef<CLNSwapOutModalComponent>, @Inject(MAT_DIALOG_DATA) public data: CLNSwapInformation, private store: Store<RTLState>, private decimalPipe: DecimalPipe, private commonService: CommonService, private actions: Actions) { }
ngOnInit() {
this.sPeer = this.data.swapPeer;
this.store.select(clnNodeSettings).pipe(takeUntil(this.unSubs[0])).subscribe((nodeSettings: SelNodeChild | null) => {
this.selNode = nodeSettings;
});
this.actions.pipe(
takeUntil(this.unSubs[1]),
filter((action) => action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN)).
subscribe((action: any) => {
if (action.type === CLNActions.UPDATE_API_CALL_STATUS_CLN && action.payload.action === 'PeerswapSwapout') {
if (action.payload.status === APICallStatusEnum.ERROR) {
this.swapOutError = action.payload.message;
}
if (action.payload.status === APICallStatusEnum.COMPLETED) {
this.dialogRef.close();
}
}
});
}
onExecuteSwapout(): boolean | void {
this.swapOutError = '';
if (!this.swapAmount || !this.sPeer || !this.sPeer.short_channel_id) { return true; }
this.store.dispatch(swapOut({ payload: { amountSats: this.swapAmount, shortChannelId: this.sPeer?.short_channel_id, asset: 'btc' } }));
}
resetData() {
this.swapAmount = null;
this.swapAmountHint = '';
this.swapOutError = '';
}
onAmountChange() {
if (this.selNode && this.selNode.fiatConversion && this.swapAmount && this.swapAmount > 99) {
this.swapAmountHint = '';
this.commonService.convertCurrency(this.swapAmount, CurrencyUnitEnum.SATS, CurrencyUnitEnum.OTHER, (this.selNode.currencyUnits && this.selNode.currencyUnits.length > 2 ? this.selNode.currencyUnits[2] : ''), this.selNode.fiatConversion).
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.swapAmountHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.swapAmountHint = 'Conversion Error: ' + err;
}
});
}
}
ngOnDestroy() {
this.unSubs.forEach((completeSub) => {
completeSub.next(<any>null);
completeSub.complete();
});
}
}

@ -1,16 +1,19 @@
<div fxLayout="column" fxLayoutAlign="start stretch" class="padding-gap-x">
<div class="p-2 error-border my-2" *ngIf="errorMessage !== ''">{{errorMessage}}</div>
<div *ngIf="errorMessage === ''" fxLayout.gt-xs="column" fxLayout="row" fxLayoutAlign="start center" fxLayoutAlign.gt-xs="start stretch" class="page-sub-title-container">
<div fxFlex="100">
<div fxLayout="row wrap" fxLayoutAlign="start start" fxLayout.gt-sm="column" fxFlex="100" fxLayoutAlign.gt-sm="start stretch">
<div fxFlex="100" class="page-sub-title-container padding-gap-large pb-0">
<fa-icon [icon]="faPeopleGroup" class="page-title-img mr-1"></fa-icon>
<span class="page-title">Number of peers enabled with Peerswap: {{totalSwapPeers}}</span>
</div>
<div fxLayout="column" fxLayout.gt-xs="row wrap" fxLayoutAlign.gt-xs="start center" fxLayoutAlign="start stretch" class="page-sub-title-container">
<div fxFlex="70"></div>
<mat-form-field fxFlex="30">
<input matInput (keyup)="applyFilter()" [(ngModel)]="selFilter" placeholder="Filter">
</mat-form-field>
</div>
</div>
<div *ngIf="errorMessage === ''" [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<div [perfectScrollbar] fxLayout="column" fxLayoutAlign="start center" fxFlex="100" class="table-container">
<mat-progress-bar *ngIf="apiCallStatus?.status === apiCallStatusEnum.INITIATED" mode="indeterminate"></mat-progress-bar>
<table mat-table #table [dataSource]="swapPeers" fxFlex="100" matSort class="overflow-auto">
<table mat-table #table [dataSource]="swapPeers" fxFlex="100" matSort [ngClass]="{'error-border': errorMessage !== ''}" class="overflow-auto">
<ng-container matColumnDef="short_channel_id">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Short Channel ID</th>
<td mat-cell *matCellDef="let sPeer">{{sPeer?.short_channel_id}}</td>

@ -7,9 +7,10 @@ import { Store } from '@ngrx/store';
import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { faPeopleGroup } from '@fortawesome/free-solid-svg-icons';
import { SwapPeerChannelsFlattened } from '../../../../shared/models/clnModels';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, APICallStatusEnum } from '../../../../shared/services/consts-enums-functions';
import { PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, APICallStatusEnum, DataTypeEnum, AlertTypeEnum } from '../../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../../shared/models/apiCallsPayload';
import { LoggerService } from '../../../../shared/services/logger.service';
import { CommonService } from '../../../../shared/services/common.service';
@ -18,6 +19,8 @@ import { RTLState } from '../../../../store/rtl.state';
import { openAlert } from '../../../../store/rtl.actions';
import { fetchSwapPeers } from '../../../store/cln.actions';
import { swapPeers } from '../../../store/cln.selector';
import { CLNSwapOutModalComponent } from '../swap-out-modal/swap-out-modal.component';
import { CLNSwapInModalComponent } from '../swap-in-modal/swap-in-modal.component';
@Component({
selector: 'rtl-peerswap-peers',
@ -31,7 +34,9 @@ export class SwapPeersComponent implements OnInit, OnDestroy {
@ViewChild(MatSort, { static: false }) sort: MatSort | undefined;
@ViewChild(MatPaginator, { static: false }) paginator: MatPaginator | undefined;
public faPeopleGroup = faPeopleGroup;
public displayedColumns: any[] = [];
public totalSwapPeers = 0;
public peersData: SwapPeerChannelsFlattened[] = [];
public swapPeers: any;
public flgSticky = false;
@ -67,12 +72,13 @@ export class SwapPeersComponent implements OnInit, OnDestroy {
this.router.onSameUrlNavigation = 'reload';
this.store.dispatch(fetchSwapPeers());
this.store.select(swapPeers).pipe(takeUntil(this.unSubs[0])).
subscribe((spSeletor: { swapPeers: SwapPeerChannelsFlattened[], apiCallStatus: ApiCallStatusPayload }) => {
subscribe((spSeletor: { totalSwapPeers: number, swapPeers: SwapPeerChannelsFlattened[], apiCallStatus: ApiCallStatusPayload }) => {
this.errorMessage = '';
this.apiCallStatus = spSeletor.apiCallStatus;
if (this.apiCallStatus.status === APICallStatusEnum.ERROR) {
this.errorMessage = !this.apiCallStatus.message ? '' : (typeof (this.apiCallStatus.message) === 'object') ? JSON.stringify(this.apiCallStatus.message) : this.apiCallStatus.message;
}
this.totalSwapPeers = spSeletor.totalSwapPeers;
this.peersData = spSeletor.swapPeers || [];
if (this.peersData.length > 0 && this.sort && this.paginator) {
this.loadSwapPeersTable(this.peersData);
@ -83,27 +89,55 @@ export class SwapPeersComponent implements OnInit, OnDestroy {
onSwapPeerClick(selSPeer: SwapPeerChannelsFlattened) {
this.logger.warn(selSPeer);
// nodeid, alias, swaps_allowed, supported_assets[], total_fee_paid
// channels: {short_channel_id, local_balance, remote_balance, local_percentage, state}[]
// sent: {total_swaps_out, total_swaps_in, total_sats_swapped_out, total_sats_swapped_in}
// received: {total_swaps_out, total_swaps_in, total_sats_swapped_out, total_sats_swapped_in}
const reorderedSPeer = [
[{ key: 'nodeid', value: selSPeer.nodeid, title: 'Node Id', width: 100, type: DataTypeEnum.STRING }],
[{ key: 'alias', value: selSPeer.alias, title: 'Alias', width: 50, type: DataTypeEnum.STRING },
{ key: 'short_channel_id', value: selSPeer.short_channel_id, title: 'Short Channel ID', width: 50, type: DataTypeEnum.STRING }],
[{ key: 'local_balance', value: selSPeer.local_balance, title: 'Local Balance (Sats)', width: 50, type: DataTypeEnum.NUMBER },
{ key: 'remote_balance', value: selSPeer.remote_balance, title: 'Remote Balance (Sats)', width: 50, type: DataTypeEnum.NUMBER }],
[{ key: 'total_fee_paid', value: selSPeer.total_fee_paid, title: 'Total Fee Paid (Sats)', width: 40, type: DataTypeEnum.NUMBER },
{ key: 'swaps_allowed', value: selSPeer.swaps_allowed ? 'Allowed' : 'Denied', title: 'Swaps Allowed', width: 30, type: DataTypeEnum.STRING },
{ key: 'total_channels', value: selSPeer.channels?.length, title: 'Channels Opened', width: 30, type: DataTypeEnum.NUMBER }],
[{ key: 'sent_total_swaps_out', value: selSPeer.sent?.total_swaps_out, title: 'Swap Out Sent', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'sent_total_swaps_in', value: selSPeer.sent?.total_swaps_in, title: 'Swap In Sent', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'sent_total_sats_swapped_out', value: selSPeer.sent?.total_sats_swapped_out, title: 'Swapped Out Sent (Sats)', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'sent_total_sats_swapped_in', value: selSPeer.sent?.total_sats_swapped_in, title: 'Swapped In Sent (Sats)', width: 25, type: DataTypeEnum.NUMBER }],
[{ key: 'received_total_swaps_out', value: selSPeer.received?.total_swaps_out, title: 'Swap Out Received', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'received_total_swaps_in', value: selSPeer.received?.total_swaps_in, title: 'Swap In Received', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'received_total_sats_swapped_out', value: selSPeer.received?.total_sats_swapped_out, title: 'Swapped Out Received(Sats)', width: 25, type: DataTypeEnum.NUMBER },
{ key: 'received_total_sats_swapped_in', value: selSPeer.received?.total_sats_swapped_in, title: 'Swapped In Received (Sats)', width: 25, type: DataTypeEnum.NUMBER }]
];
this.store.dispatch(openAlert({
payload: {
data: {
// invoice: reCreatedInvoice,
// newlyAdded: false,
// component: CLNInvoiceInformationComponent
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Swap Peer Information',
message: reorderedSPeer
}
}
}));
}
onSwapOut(sPeer) {
this.logger.warn('Swap Out');
onSwapOut(sPeer: SwapPeerChannelsFlattened) {
this.store.dispatch(openAlert({
payload: {
data: {
swapPeer: sPeer,
component: CLNSwapOutModalComponent
}
}
}));
}
onSwapIn(sPeer) {
this.logger.warn('Swap In');
onSwapIn(sPeer: SwapPeerChannelsFlattened) {
this.store.dispatch(openAlert({
payload: {
data: {
swapPeer: sPeer,
component: CLNSwapInModalComponent
}
}
}));
}
loadSwapPeersTable(swapPeers: SwapPeerChannelsFlattened[]) {

@ -24,7 +24,7 @@
<mat-form-field fxFlex="70" fxLayoutAlign="start end">
<input matInput [(ngModel)]="fundingAmount" placeholder="Amount" type="number" [step]="1000" [min]="1" [max]="totalBalance" tabindex="1" required name="amount" #amount="ngModel" [disabled]="flgUseAllBalance">
<mat-hint>Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0) | number}}{{flgUseAllBalance ? '. Amount replaced by UTXO balance' : ''}}</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="amount.errors?.required || !fundingAmount">Amount is required.</mat-error>
<mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>
</mat-form-field>

@ -33,7 +33,7 @@
<mat-form-field fxFlex="60" fxLayoutAlign="start end">
<input matInput autoFocus formControlName="fundingAmount" placeholder="Amount" type="number" [step]="1000" tabindex="1" required>
<mat-hint>Remaining Bal: {{totalBalance - ((channelFormGroup.controls.fundingAmount.value) ? channelFormGroup.controls.fundingAmount.value : 0)}}</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.min">Amount must be a positive number.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>

@ -142,3 +142,11 @@ export const setSwapPeers = createAction(CLNActions.SET_SWAP_PEERS_CLN, props<{
export const fetchSwapRequests = createAction(CLNActions.FETCH_SWAP_REQUESTS_CLN);
export const setSwapRequests = createAction(CLNActions.SET_SWAP_REQUESTS_CLN, props<{ payload: SwapRequest[] }>());
export const swapOut = createAction(CLNActions.SWAPOUT_CLN, props<{ payload: { amountSats: number, shortChannelId: string, asset: string } }>());
export const addSwapout = createAction(CLNActions.ADD_SWAPOUT_CLN, props<{ payload: Swap }>());
export const swapIn = createAction(CLNActions.SWAPIN_CLN, props<{ payload: { amountSats: number, shortChannelId: string, asset: string } }>());
export const addSwapin = createAction(CLNActions.ADD_SWAPIN_CLN, props<{ payload: Swap }>());

@ -14,8 +14,8 @@ import { SessionService } from '../../shared/services/session.service';
import { WebSocketClientService } from '../../shared/services/web-socket.service';
import { ErrorMessageComponent } from '../../shared/components/data-modal/error-message/error-message.component';
import { CLNInvoiceInformationComponent } from '../transactions/invoices/invoice-information-modal/invoice-information.component';
import { GetInfo, Fees, Balance, LocalRemoteBalance, Payment, FeeRates, ListInvoices, Invoice, Peer, OnChain, QueryRoutes, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, SendPayment, GetQueryRoutes, ChannelLookup, FetchInvoices, Channel, OfferInvoice, Offer, ListForwards, FetchListForwards, ForwardingEvent, LocalFailedEvent } from '../../shared/models/clnModels';
import { AlertTypeEnum, APICallStatusEnum, UI_MESSAGES, CLNWSEventTypeEnum, CLNActions, RTLActions, CLNForwardingEventsStatusEnum } from '../../shared/services/consts-enums-functions';
import { GetInfo, Fees, Balance, LocalRemoteBalance, Payment, FeeRates, ListInvoices, Invoice, Peer, OnChain, QueryRoutes, SaveChannel, GetNewAddress, DetachPeer, UpdateChannel, CloseChannel, SendPayment, GetQueryRoutes, ChannelLookup, FetchInvoices, Channel, OfferInvoice, Offer, ListForwards, FetchListForwards, ForwardingEvent, LocalFailedEvent, Swap } from '../../shared/models/clnModels';
import { AlertTypeEnum, APICallStatusEnum, UI_MESSAGES, CLNWSEventTypeEnum, CLNActions, RTLActions, CLNForwardingEventsStatusEnum, DataTypeEnum } from '../../shared/services/consts-enums-functions';
import { closeAllDialogs, closeSpinner, logout, openAlert, openSnackBar, openSpinner, setApiUrl, setNodeData } from '../../store/rtl.actions';
import { RTLState } from '../../store/rtl.state';
@ -966,7 +966,7 @@ export class CLNEffects implements OnDestroy {
payload: res || []
};
}), catchError((err: any) => {
this.handleErrorWithoutAlert('FetchSwaps', UI_MESSAGES.NO_SPINNER, 'Fetching Swap Peers Failed.', err);
this.handleErrorWithoutAlert('FetchSwapPeers', UI_MESSAGES.NO_SPINNER, 'Fetching Swap Peers Failed.', err);
return of({ type: RTLActions.VOID });
}));
})
@ -991,6 +991,74 @@ export class CLNEffects implements OnDestroy {
})
));
peerswapOutCL = createEffect(() => this.actions.pipe(
ofType(CLNActions.SWAPOUT_CLN),
mergeMap((action: { type: string, payload: { amountSats: number, shortChannelId: string, asset: string } }) => {
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.PEERSWAP_SWAPOUT }));
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'PeerswapSwapout', status: APICallStatusEnum.INITIATED } }));
return this.httpClient.post(this.CHILD_API_URL + environment.PEERSWAP_API + '/swapOut', {
amountSats: action.payload.amountSats, shortChannelId: action.payload.shortChannelId, asset: action.payload.asset
}).pipe(map((postRes: Swap) => {
this.logger.info(postRes);
this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.PEERSWAP_SWAPOUT }));
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'PeerswapSwapout', status: APICallStatusEnum.COMPLETED } }));
setTimeout(() => {
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Swapout Initiated',
message: postRes.id
}
}
}));
}, 100);
return {
type: CLNActions.ADD_SWAPOUT_CLN,
message: [{ key: 'id', value: postRes.id, title: 'Swap Id', width: 100, type: DataTypeEnum.STRING }]
};
}), catchError((err: any) => {
this.handleErrorWithoutAlert('PeerswapSwapout', UI_MESSAGES.PEERSWAP_SWAPOUT, 'Swapout Failed.', err);
return of({ type: RTLActions.VOID });
})
);
})
));
peerswapInCL = createEffect(() => this.actions.pipe(
ofType(CLNActions.SWAPIN_CLN),
mergeMap((action: { type: string, payload: { amountSats: number, shortChannelId: string, asset: string } }) => {
this.store.dispatch(openSpinner({ payload: UI_MESSAGES.PEERSWAP_SWAPIN }));
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'PeerswapSwapin', status: APICallStatusEnum.INITIATED } }));
return this.httpClient.post(this.CHILD_API_URL + environment.PEERSWAP_API + '/swapIn', {
amountSats: action.payload.amountSats, shortChannelId: action.payload.shortChannelId, asset: action.payload.asset
}).pipe(map((postRes: Swap) => {
this.logger.info(postRes);
this.store.dispatch(closeSpinner({ payload: UI_MESSAGES.PEERSWAP_SWAPIN }));
this.store.dispatch(updateCLAPICallStatus({ payload: { action: 'PeerswapSwapin', status: APICallStatusEnum.COMPLETED } }));
setTimeout(() => {
this.store.dispatch(openAlert({
payload: {
data: {
type: AlertTypeEnum.INFORMATION,
alertTitle: 'Swapin Initiated',
message: [{ key: 'id', value: postRes.id, title: 'Swap Id', width: 100, type: DataTypeEnum.STRING }]
}
}
}));
}, 100);
return {
type: CLNActions.ADD_SWAPIN_CLN,
payload: postRes
};
}), catchError((err: any) => {
this.handleErrorWithoutAlert('PeerswapSwapin', UI_MESSAGES.PEERSWAP_SWAPIN, 'Swapin Failed.', err);
return of({ type: RTLActions.VOID });
})
);
})
));
initializeRemainingData(info: any, landingPage: string) {
this.sessionService.setItem('clUnlocked', 'true');
const node_data = {

@ -5,10 +5,10 @@ import {
setChildNodeSettingsCL, setFeeRates, setFees, setForwardingHistory,
setInfo, setInvoices, setLocalRemoteBalance, setOffers, addOffer, setPayments, setPeers, setUTXOs,
updateCLAPICallStatus, updateInvoice, updateOffer, setOfferBookmarks, addUpdateOfferBookmark, removeOfferBookmark,
setSwaps, setSwapPeers, setSwapRequests
setSwaps, setSwapPeers, setSwapRequests, addSwapout, addSwapin
} from './cln.actions';
import { Channel, OfferBookmark, SwapPeerChannelsFlattened } from '../../shared/models/clnModels';
import { CLNForwardingEventsStatusEnum } from '../../shared/services/consts-enums-functions';
import { Channel, OfferBookmark, Swap } from '../../shared/models/clnModels';
import { CLNForwardingEventsStatusEnum, PeerswapTypes } from '../../shared/services/consts-enums-functions';
export const CLNReducer = createReducer(initCLNState,
on(updateCLAPICallStatus, (state, { payload }) => {
@ -218,10 +218,22 @@ export const CLNReducer = createReducer(initCLNState,
offersBookmarks: modifiedOfferBookmarks
};
}),
on(setSwaps, (state, { payload }) => ({
...state,
swaps: payload
})),
on(setSwaps, (state, { payload }) => {
const swapOutArr: Swap[] = [];
const swapInArr: Swap[] = [];
payload.forEach((swap) => {
if (swap.type === PeerswapTypes.SWAP_OUT) {
swapOutArr.push(swap);
} else {
swapInArr.push(swap);
}
});
return {
...state,
swapOuts: swapOutArr,
swapIns: swapInArr
};
}),
on(setSwapRequests, (state, { payload }) => ({
...state,
swapRequests: payload
@ -235,8 +247,25 @@ export const CLNReducer = createReducer(initCLNState,
}, <any[]>[]);
return {
...state,
totalSwapPeers: payload.length || 0,
swapPeers: flattenedSwapPeers
};
}),
on(addSwapout, (state, { payload }) => {
const newSwapOuts = state.swapOuts;
newSwapOuts?.unshift(payload);
return {
...state,
swapOuts: newSwapOuts
};
}),
on(addSwapin, (state, { payload }) => {
const newSwapIns = state.swapIns;
newSwapIns?.unshift(payload);
return {
...state,
swapIns: newSwapIns
};
})
);

@ -27,6 +27,7 @@ export const nodeInfoAndNodeSettingsAndAPIsStatus = createSelector(clnState, (st
export const offers = createSelector(clnState, (state: CLNState) => ({ offers: state.offers, apiCallStatus: state.apisCallStatus.FetchOffers }));
export const offerBookmarks = createSelector(clnState, (state: CLNState) => ({ offersBookmarks: state.offersBookmarks, apiCallStatus: state.apisCallStatus.FetchOfferBookmarks }));
export const getoffer = (bolt12Str) => createSelector(clnState, (state: CLNState) => (state.offers.find((offer: Offer) => offer.bolt12 === bolt12Str)));
export const swaps = createSelector(clnState, (state: CLNState) => ({ swaps: state.swaps, apiCallStatus: state.apisCallStatus.FetchSwaps }));
export const swapPeers = createSelector(clnState, (state: CLNState) => ({ swapPeers: state.swapPeers, apiCallStatus: state.apisCallStatus.FetchSwapPeers }));
export const swapOuts = createSelector(clnState, (state: CLNState) => ({ swapOuts: state.swapOuts, apiCallStatus: state.apisCallStatus.FetchSwaps }));
export const swapIns = createSelector(clnState, (state: CLNState) => ({ swapIns: state.swapIns, apiCallStatus: state.apisCallStatus.FetchSwaps }));
export const swapPeers = createSelector(clnState, (state: CLNState) => ({ totalSwapPeers: state.totalSwapPeers, swapPeers: state.swapPeers, apiCallStatus: state.apisCallStatus.FetchSwapPeers }));
export const swapRequests = createSelector(clnState, (state: CLNState) => ({ swapRequests: state.swapRequests, apiCallStatus: state.apisCallStatus.FetchSwapRequests }));

@ -24,8 +24,10 @@ export interface CLNState {
utxos: UTXO[];
offers: Offer[];
offersBookmarks: OfferBookmark[];
totalSwapPeers: number;
swapPeers: SwapPeerChannelsFlattened[];
swaps: Swap[];
swapOuts: Swap[];
swapIns: Swap[];
swapRequests: SwapRequest[];
}
@ -70,7 +72,9 @@ export const initCLNState: CLNState = {
utxos: [],
offers: [],
offersBookmarks: [],
totalSwapPeers: 0,
swapPeers: [],
swaps: [],
swapOuts: [],
swapIns: [],
swapRequests: []
};

@ -100,7 +100,7 @@ export class CLNCreateInvoiceComponent implements OnInit, OnDestroy {
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -9,7 +9,7 @@ import { MatPaginator, MatPaginatorIntl } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, APICallStatusEnum, UI_MESSAGES, CLNActions } from '../../../../shared/services/consts-enums-functions';
import { CurrencyUnitEnum, CURRENCY_UNIT_FORMATS, PAGE_SIZE, PAGE_SIZE_OPTIONS, getPaginatorLabel, ScreenSizeEnum, APICallStatusEnum, CLNActions } from '../../../../shared/services/consts-enums-functions';
import { ApiCallStatusPayload } from '../../../../shared/models/apiCallsPayload';
import { SelNodeChild } from '../../../../shared/models/RTLconfig';
import { GetInfo, Invoice, ListInvoices } from '../../../../shared/models/clnModels';
@ -201,7 +201,7 @@ export class CLNLightningInvoicesTableComponent implements OnInit, AfterViewInit
pipe(takeUntil(this.unSubs[5])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -83,7 +83,7 @@ export class CLNCreateOfferComponent implements OnInit, OnDestroy {
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.offerValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.offerValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.offerValueHint = 'Conversion Error: ' + err;
}

@ -33,7 +33,7 @@
<mat-form-field fxFlex="60" fxLayoutAlign="start end">
<input matInput autoFocus formControlName="fundingAmount" placeholder="Amount" type="number" [step]="1000" tabindex="1" required>
<mat-hint>Remaining Bal: {{totalBalance - ((channelFormGroup.controls.fundingAmount.value) ? channelFormGroup.controls.fundingAmount.value : 0)}}</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.min">Amount must be a positive number.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>

@ -104,7 +104,7 @@ export class ECLCreateInvoiceComponent implements OnInit, OnDestroy {
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -195,7 +195,7 @@ export class ECLLightningInvoicesComponent implements OnInit, AfterViewInit, OnD
pipe(takeUntil(this.unSubs[4])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -24,7 +24,7 @@
<mat-form-field fxFlex="60" fxLayoutAlign="start end">
<input matInput [(ngModel)]="fundingAmount" placeholder="Amount" type="number" [step]="1000" [min]="1" [max]="totalBalance" tabindex="1" required name="amnt" #amt="ngModel">
<mat-hint>(Remaining Bal: {{totalBalance - ((fundingAmount) ? fundingAmount : 0) | number}})</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="amount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="amount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>
</mat-form-field>

@ -33,7 +33,7 @@
<mat-form-field fxFlex="60" fxLayoutAlign="start end">
<input matInput formControlName="fundingAmount" placeholder="Amount" type="number" [step]="1000" tabindex="1" required>
<mat-hint>Remaining Bal: {{totalBalance - ((channelFormGroup.controls.fundingAmount.value) ? channelFormGroup.controls.fundingAmount.value : 0)}}</mat-hint>
<span matSuffix> Sats </span>
<span matSuffix class="ml-1"> Sats </span>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.required">Amount is required.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.min">Amount must be a positive number.</mat-error>
<mat-error *ngIf="channelFormGroup.controls.fundingAmount.errors?.max">Amount must be less than or equal to {{totalBalance}}.</mat-error>

@ -99,7 +99,7 @@ export class CreateInvoiceComponent implements OnInit, OnDestroy {
pipe(takeUntil(this.unSubs[3])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -205,7 +205,7 @@ export class LightningInvoicesComponent implements OnInit, AfterViewInit, OnDest
pipe(takeUntil(this.unSubs[4])).
subscribe({
next: (data) => {
this.invoiceValueHint = '= ' + data.symbol + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.unit;
this.invoiceValueHint = '= ' + this.decimalPipe.transform(data.OTHER, CURRENCY_UNIT_FORMATS.OTHER) + ' ' + data.symbol;
}, error: (err) => {
this.invoiceValueHint = 'Conversion Error: ' + err;
}

@ -1,7 +1,7 @@
import { DataTypeEnum, LoopTypeEnum, PaymentTypes, SwapTypeEnum } from '../services/consts-enums-functions';
import { GetInfoRoot, RTLConfiguration } from './RTLconfig';
import { GetInfo, Invoice, Channel, Peer, PendingOpenChannel, UTXO } from './lndModels';
import { Invoice as InvoiceCLN, GetInfo as GetInfoCLN, Peer as PeerCLN, Channel as ChannelCLN, UTXO as UTXOCLN, Offer as OfferCLN, LookupNode as LookupNodeCLN } from './clnModels';
import { Invoice as InvoiceCLN, GetInfo as GetInfoCLN, Peer as PeerCLN, Channel as ChannelCLN, UTXO as UTXOCLN, Offer as OfferCLN, LookupNode as LookupNodeCLN, SwapPeerChannelsFlattened as SwapPeerChannelsFlattenedCLN } from './clnModels';
import { GetInfo as GetInfoECL, Peer as PeerECL, Channel as ChannelECL, Invoice as InvoiceECL, PaymentSent as PaymentSentECL } from './eclModels';
import { LoopQuote } from './loopModels';
import { ServiceInfo } from './boltzModels';
@ -117,6 +117,11 @@ export interface CLNOfferInformation {
component?: any;
}
export interface CLNSwapInformation {
swapPeer: SwapPeerChannelsFlattenedCLN;
component?: any;
}
export interface ECLInvoiceInformation {
invoice: InvoiceECL;
newlyAdded?: boolean;
@ -229,7 +234,7 @@ export interface DialogConfig {
maxWidth?: string;
minHeight?: string;
data: AlertData | ConfirmationData | ErrorData | ChannelRebalanceAlert | OpenChannelAlert | CLNOpenChannelAlert | InvoiceInformation |
CLNPaymentInformation | CLNInvoiceInformation | CLNOfferInformation | ECLInvoiceInformation | ECLPaymentInformation | ChannelInformation | CLNChannelInformation |
PendingOpenChannelInformation | OnChainAddressInformation | ShowPubkeyData | LoopAlert | SwapAlert | AuthConfig |
OnChainLabelUTXO | OnChainSendFunds | CLNOnChainSendFunds | ECLChannelInformation | ECLOpenChannelAlert;
CLNPaymentInformation | CLNInvoiceInformation | CLNOfferInformation | CLNSwapInformation | ECLInvoiceInformation | ECLPaymentInformation |
ChannelInformation | CLNChannelInformation | PendingOpenChannelInformation | OnChainAddressInformation | ShowPubkeyData | LoopAlert |
SwapAlert | AuthConfig | OnChainLabelUTXO | OnChainSendFunds | CLNOnChainSendFunds | ECLChannelInformation | ECLOpenChannelAlert;
}

@ -590,6 +590,7 @@ export interface Swap {
short_channel_id?: string;
opening_tx_id?: string;
claim_tx_id?: string;
cancel_message?: string;
}
export interface SwapChannel {
@ -626,6 +627,7 @@ export interface SwapPeerChannelsFlattened {
sent?: SwapPeerSentReceived;
received?: SwapPeerSentReceived;
total_fee_paid?: number;
channels?: SwapChannel[];
// Channels array flattened
short_channel_id?: string;
local_balance?: number;

@ -322,6 +322,8 @@ export const UI_MESSAGES = {
GET_FUNDER_POLICY: 'Getting Or Updating Funder Policy...',
GET_LIST_CONFIGS: 'Getting Configurations List...',
LIST_NETWORK_NODES: 'Getting Network Nodes List...',
PEERSWAP_SWAPOUT: 'Initializing Swapout...',
PEERSWAP_SWAPIN: 'Initializing Swapin...',
LOG_OUT: 'Logging Out...'
};
@ -331,6 +333,11 @@ export enum PaymentTypes {
KEYSEND = 'KEYSEND'
}
export enum PeerswapTypes {
SWAP_OUT = 'swap-out',
SWAP_IN = 'swap-in'
}
export enum ReportBy {
FEES = 'FEES',
EVENTS = 'EVENTS'
@ -525,6 +532,10 @@ export enum CLNActions {
SET_SWAP_PEERS_CLN = 'SET_SWAP_PEERS_CLN',
FETCH_SWAP_REQUESTS_CLN = 'FETCH_SWAP_REQUESTS_CLN',
SET_SWAP_REQUESTS_CLN = 'SET_SWAP_REQUESTS_CLN',
SWAPOUT_CLN = 'SWAPOUT_CLN',
ADD_SWAPOUT_CLN = 'ADD_SWAPOUT_CLN',
SWAPIN_CLN = 'SWAPIN_CLN',
ADD_SWAPIN_CLN = 'ADD_SWAPIN_CLN',
};
export enum ECLActions {

Loading…
Cancel
Save