Swap In & Swap Out

Swap In & Swap Out
This commit is contained in:
ShahanaFarooqui 2022-08-29 01:29:54 -07:00
parent 42425ebe26
commit 04bb6eaade
43 changed files with 671 additions and 61 deletions

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

View file

@ -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

View file

@ -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))})()})();

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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();
});
}
}

View file

@ -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>

View file

@ -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();
});
});

View file

@ -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();
});
}
}

View file

@ -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>

View file

@ -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[]) {

View file

@ -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>

View file

@ -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>

View file

@ -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 }>());

View file

@ -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 = {

View file

@ -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
};
})
);

View file

@ -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 }));

View file

@ -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: []
};

View file

@ -1,3 +0,0 @@
.mat-column-actions {
min-height: 4.8rem;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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>

View file

@ -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>

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {