Commit 750b2f19 authored by Pieterjan Vanhoof's avatar Pieterjan Vanhoof Committed by GitHub

Merge pull request #233 from stratisproject/ui

Add Stratis
parents dfc76ea9 5d2e3ac1
...@@ -61,22 +61,10 @@ function createWindow() { ...@@ -61,22 +61,10 @@ function createWindow() {
// Emitted when the window is going to close. // Emitted when the window is going to close.
mainWindow.on('close', function () { mainWindow.on('close', function () {
if (process.platform !== 'darwin' && !serve) { closeBitcoinApi(),
var http = require('http'); closeStratisApi();
const options = { })
hostname: 'localhost', };
port: 5000,
path: '/api/node/shutdown',
method: 'POST'
};
const req = http.request(options, (res) => {});
req.write('');
req.end();
}
}
);
}
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
...@@ -86,7 +74,8 @@ app.on('ready', function () { ...@@ -86,7 +74,8 @@ app.on('ready', function () {
console.log("Breeze UI was started in development mode. This requires the user to be running the Breeze Daemon himself.") console.log("Breeze UI was started in development mode. This requires the user to be running the Breeze Daemon himself.")
} }
else { else {
startApi(); startBitcoinApi();
startStratisApi();
} }
createTray(); createTray();
createWindow(); createWindow();
...@@ -113,17 +102,73 @@ app.on('activate', function () { ...@@ -113,17 +102,73 @@ app.on('activate', function () {
} }
}); });
function startApi() { function closeBitcoinApi() {
var apiProcess; if (process.platform !== 'darwin' && !serve) {
const exec = require('child_process').exec; var http1 = require('http');
const options1 = {
hostname: 'localhost',
port: 5000,
path: '/api/node/shutdown',
method: 'POST'
};
const req = http1.request(options1, (res) => {});
req.write('');
req.end();
}
};
function closeStratisApi() {
if (process.platform !== 'darwin' && !serve) {
var http2 = require('http');
const options2 = {
hostname: 'localhost',
port: 5105,
path: '/api/node/shutdown',
method: 'POST'
};
const req = http2.request(options2, (res) => {});
req.write('');
req.end();
}
};
function startBitcoinApi() {
var bitcoinProcess;
const execBitcoin = require('child_process').exec;
//Start Breeze Bitcoin Daemon
let apiPath = path.join(__dirname, '".//assets//daemon//Breeze.Daemon"');
if (os.platform() === 'win32') {
apiPath = path.join(__dirname, '".\\assets\\daemon\\Breeze.Daemon.exe"');
}
bitcoinProcess = execBitcoin('"' + apiPath + '" light -testnet', {
detached: true
}, (error, stdout, stderr) => {
if (error) {
writeLogError(`exec error: ${error}`);
return;
}
if (serve) {
writeLog(`stdout: ${stdout}`);
writeLog(`stderr: ${stderr}`);
}
});
}
function startStratisApi() {
var stratisProcess;
const execStratis = require('child_process').exec;
//Start Breeze Daemon //Start Breeze Stratis Daemon
let apiPath = path.join(__dirname, '".//assets//daemon//Breeze.Daemon"'); let apiPath = path.join(__dirname, '".//assets//daemon//Breeze.Daemon"');
if (os.platform() === 'win32') { if (os.platform() === 'win32') {
apiPath = path.join(__dirname, '".\\assets\\daemon\\Breeze.Daemon.exe"'); apiPath = path.join(__dirname, '".\\assets\\daemon\\Breeze.Daemon.exe"');
} }
apiProcess = exec('"' + apiPath + '" light -testnet', { stratisProcess = execStratis('"' + apiPath + '" stratis light -testnet', {
detached: true detached: true
}, (error, stdout, stderr) => { }, (error, stdout, stderr) => {
if (error) { if (error) {
......
...@@ -20,6 +20,7 @@ export class LoginComponent implements OnInit { ...@@ -20,6 +20,7 @@ export class LoginComponent implements OnInit {
private openWalletForm: FormGroup; private openWalletForm: FormGroup;
private hasWallet: boolean = false; private hasWallet: boolean = false;
private wallets: [string]; private wallets: [string];
private isDecrypting = false;
ngOnInit() { ngOnInit() {
this.getWalletFiles(); this.getWalletFiles();
...@@ -62,34 +63,6 @@ export class LoginComponent implements OnInit { ...@@ -62,34 +63,6 @@ export class LoginComponent implements OnInit {
} }
}; };
private updateWalletFileDisplay(walletName: string) {
this.openWalletForm.patchValue({selectWallet: walletName})
}
private onDecryptClicked() {
this.setGlobalWalletName(this.openWalletForm.get("selectWallet").value);
let walletLoad = new WalletLoad(
this.openWalletForm.get("password").value,
this.globalService.getWalletPath(),
this.openWalletForm.get("selectWallet").value
);
this.loadWallet(walletLoad);
}
private onCreateClicked() {
this.router.navigate(['/setup']);
}
private onEnter() {
if (this.openWalletForm.valid) {
this.onDecryptClicked();
}
}
private setGlobalWalletName(walletName: string) {
this.globalService.setWalletName(walletName);
}
private getWalletFiles() { private getWalletFiles() {
this.apiService.getWalletFiles() this.apiService.getWalletFiles()
.subscribe( .subscribe(
...@@ -125,17 +98,71 @@ export class LoginComponent implements OnInit { ...@@ -125,17 +98,71 @@ export class LoginComponent implements OnInit {
; ;
} }
private loadWallet(walletLoad: WalletLoad) { private updateWalletFileDisplay(walletName: string) {
this.apiService.loadWallet(walletLoad) this.openWalletForm.patchValue({selectWallet: walletName})
}
private onCreateClicked() {
this.router.navigate(['/setup']);
}
private onEnter() {
if (this.openWalletForm.valid) {
this.onDecryptClicked();
}
}
private onDecryptClicked() {
this.isDecrypting = true;
this.globalService.setWalletName(this.openWalletForm.get("selectWallet").value);
let walletLoad = new WalletLoad(
this.openWalletForm.get("selectWallet").value,
this.openWalletForm.get("password").value
);
this.loadWallets(walletLoad);
}
private loadWallets(walletLoad: WalletLoad) {
this.apiService.loadBitcoinWallet(walletLoad)
.subscribe( .subscribe(
response => { response => {
if (response.status >= 200 && response.status < 400) { if (response.status >= 200 && response.status < 400) {
this.globalService.setWalletName(walletLoad.name) // Set Bitcoin as the default wallet
this.globalService.setCoinName("TestBitcoin");
this.globalService.setCoinUnit("TBTC");
this.globalService.setWalletName(walletLoad.name);
this.globalService.setCoinType(1); this.globalService.setCoinType(1);
}
},
error => {
this.isDecrypting = false;
if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) {
if (!error.json().errors[0]) {
console.log(error);
}
else {
alert(error.json().errors[0].message);
}
}
},
() => this.loadStratisWallet(walletLoad)
)
;
}
private loadStratisWallet(walletLoad: WalletLoad) {
this.apiService.loadStratisWallet(walletLoad)
.subscribe(
response => {
if (response.status >= 200 && response.status < 400) {
// Navigate to the wallet section
this.router.navigate(['/wallet']); this.router.navigate(['/wallet']);
} }
}, },
error => { error => {
this.isDecrypting = false;
if (error.status === 0) { if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application."); alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) { } else if (error.status >= 400) {
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
</div> </div>
<!-- /row--> <!-- /row-->
<div class="row d-flex justify-content-center"> <div class="row d-flex justify-content-center">
<button type="button" class="btn btn-darkgray btn-lg" [disabled]="!createWalletForm.valid" (click)="onCreateClicked()">Create</button> <button type="button" class="btn btn-darkgray btn-lg" [disabled]="!createWalletForm.valid || isCreating" (click)="onCreateClicked()">Create</button>
</div> </div>
<!-- /row--> <!-- /row-->
</div> </div>
......
import { Component, Injectable } from '@angular/core'; import { Component, Injectable, OnInit } from '@angular/core';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
...@@ -16,7 +16,7 @@ import { Mnemonic } from '../../shared/classes/mnemonic'; ...@@ -16,7 +16,7 @@ import { Mnemonic } from '../../shared/classes/mnemonic';
styleUrls: ['./create.component.css'], styleUrls: ['./create.component.css'],
}) })
export class CreateComponent { export class CreateComponent implements OnInit {
constructor(private globalService: GlobalService, private apiService: ApiService, private router: Router, private fb: FormBuilder) { constructor(private globalService: GlobalService, private apiService: ApiService, private router: Router, private fb: FormBuilder) {
this.buildCreateForm(); this.buildCreateForm();
} }
...@@ -24,18 +24,23 @@ export class CreateComponent { ...@@ -24,18 +24,23 @@ export class CreateComponent {
private createWalletForm: FormGroup; private createWalletForm: FormGroup;
private newWallet: WalletCreation; private newWallet: WalletCreation;
private mnemonic: string; private mnemonic: string;
private isCreating: Boolean = false;
ngOnInit() {
this.getNewMnemonic();
}
private buildCreateForm(): void { private buildCreateForm(): void {
this.createWalletForm = this.fb.group({ this.createWalletForm = this.fb.group({
"walletName": ["", "walletName": ["",
Validators.compose([ Validators.compose([
Validators.required, Validators.required,
Validators.minLength(3), Validators.minLength(1),
Validators.maxLength(24), Validators.maxLength(24),
Validators.pattern(/^[a-zA-Z0-9]*$/) Validators.pattern(/^[a-zA-Z0-9]*$/)
]) ])
], ],
"walletPassword": ["", "walletPassword": ["",
Validators.compose([ Validators.compose([
Validators.required, Validators.required,
Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{10,})/)]) Validators.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{10,})/)])
...@@ -75,10 +80,10 @@ export class CreateComponent { ...@@ -75,10 +80,10 @@ export class CreateComponent {
validationMessages = { validationMessages = {
'walletName': { 'walletName': {
'required': 'Name is required.', 'required': 'A wallet name is required.',
'minlength': 'Name must be at least 3 characters long.', 'minlength': 'A wallet name must be at least one character long.',
'maxlength': 'Name cannot be more than 24 characters long.', 'maxlength': 'A wallet name cannot be more than 24 characters long.',
'pattern': 'Enter a valid wallet name. [a-Z] and [0-9] are the only characters allowed.' 'pattern': 'Please enter a valid wallet name. [a-Z] and [0-9] are the only characters allowed.'
}, },
'walletPassword': { 'walletPassword': {
'required': 'A password is required.', 'required': 'A password is required.',
...@@ -95,27 +100,84 @@ export class CreateComponent { ...@@ -95,27 +100,84 @@ export class CreateComponent {
} }
private onCreateClicked() { private onCreateClicked() {
this.newWallet = new WalletCreation( this.isCreating = true;
this.createWalletForm.get("walletPassword").value, if (this.mnemonic) {
this.createWalletForm.get("selectNetwork").value, this.newWallet = new WalletCreation(
this.globalService.getWalletPath(), this.createWalletForm.get("walletName").value,
this.createWalletForm.get("walletName").value this.mnemonic,
this.createWalletForm.get("walletPassword").value,
this.createWalletForm.get("selectNetwork").value
); );
this.createWallet(this.newWallet); this.createWallets(this.newWallet);
}
} }
private createWallet(wallet: WalletCreation) { private getNewMnemonic() {
this.apiService this.apiService
.createWallet(wallet) .getNewMnemonic()
.subscribe( .subscribe(
response => { response => {
if (response.status >= 200 && response.status < 400){ if (response.status >= 200 && response.status < 400){
this.mnemonic = response.json(); this.mnemonic = response.json();
}
},
error => {
console.log(error);
if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) {
if (!error.json().errors[0]) {
console.log(error);
}
else {
alert(error.json().errors[0].message);
}
}
}
)
;
}
private createWallets(wallet: WalletCreation) {
this.apiService
.createBitcoinWallet(wallet)
.subscribe(
response => {
if (response.status >= 200 && response.status < 400){
// Bitcoin wallet created
}
},
error => {
console.log(error);
this.isCreating = false;
if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) {
if (!error.json().errors[0]) {
console.log(error);
}
else {
alert(error.json().errors[0].message);
}
}
},
() => this.createStratisWallet(wallet)
)
;
}
private createStratisWallet(wallet: WalletCreation) {
this.apiService
.createStratisWallet(wallet)
.subscribe(
response => {
if (response.status >= 200 && response.status < 400){
alert("Your wallet has been created.\n\nPlease write down your 12 word passphrase: \n" + this.mnemonic + "\n\nYou can recover your wallet on any computer with:\n- your passphrase AND\n- your password AND\n- the wallet creation time\n\nUnlike most other wallets if an attacker acquires your passphrase, it will not be able to hack your wallet without knowing your password. On the contrary, unlike other wallets, you will not be able to recover your wallet only with your passphrase if you lose your password."); alert("Your wallet has been created.\n\nPlease write down your 12 word passphrase: \n" + this.mnemonic + "\n\nYou can recover your wallet on any computer with:\n- your passphrase AND\n- your password AND\n- the wallet creation time\n\nUnlike most other wallets if an attacker acquires your passphrase, it will not be able to hack your wallet without knowing your password. On the contrary, unlike other wallets, you will not be able to recover your wallet only with your passphrase if you lose your password.");
this.router.navigate(['']); this.router.navigate(['']);
} }
}, },
error => { error => {
this.isCreating = false;
console.log(error); console.log(error);
if (error.status === 0) { if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application."); alert("Something went wrong while connecting to the API. Please restart the application.");
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
</div> </div>
<!-- /row--> <!-- /row-->
<div class="row d-flex justify-content-center"> <div class="row d-flex justify-content-center">
<button type="button" class="btn btn-darkgray btn-lg" [disabled]="!recoverWalletForm.valid" (click)="onRecoverClicked()">Recover</button> <button type="button" class="btn btn-darkgray btn-lg" [disabled]="!recoverWalletForm.valid || isRecovering" (click)="onRecoverClicked()">Recover</button>
</div> </div>
<!-- /row--> <!-- /row-->
</div> </div>
......
...@@ -22,6 +22,7 @@ export class RecoverComponent implements OnInit { ...@@ -22,6 +22,7 @@ export class RecoverComponent implements OnInit {
private recoverWalletForm: FormGroup; private recoverWalletForm: FormGroup;
private creationDate: Date; private creationDate: Date;
private walletRecovery: WalletRecovery; private walletRecovery: WalletRecovery;
private isRecovering: Boolean = false;
private responseMessage: string; private responseMessage: string;
private errorMessage: string; private errorMessage: string;
...@@ -35,8 +36,9 @@ export class RecoverComponent implements OnInit { ...@@ -35,8 +36,9 @@ export class RecoverComponent implements OnInit {
"walletPassword": ["", Validators.required], "walletPassword": ["", Validators.required],
"walletName": ["", [ "walletName": ["", [
Validators.required, Validators.required,
Validators.minLength(3), Validators.minLength(1),
Validators.maxLength(24) Validators.maxLength(24),
Validators.pattern(/^[a-zA-Z0-9]*$/)
] ]
], ],
"selectNetwork": ["test", Validators.required] "selectNetwork": ["test", Validators.required]
...@@ -77,10 +79,11 @@ export class RecoverComponent implements OnInit { ...@@ -77,10 +79,11 @@ export class RecoverComponent implements OnInit {
'required': 'A password is required.' 'required': 'A password is required.'
}, },
'walletName': { 'walletName': {
'required': 'Name is required.', 'required': 'A wallet name is required.',
'minlength': 'Name must be at least 3 characters long.', 'minlength': 'A wallet name must be at least one character long.',
'maxlength': 'Name cannot be more than 24 characters long.' 'maxlength': 'A wallet name cannot be more than 24 characters long.',
} 'pattern': 'Please enter a valid wallet name. [a-Z] and [0-9] are the only characters allowed.'
},
}; };
private onBackClicked() { private onBackClicked() {
...@@ -88,21 +91,48 @@ export class RecoverComponent implements OnInit { ...@@ -88,21 +91,48 @@ export class RecoverComponent implements OnInit {
} }
private onRecoverClicked(){ private onRecoverClicked(){
this.isRecovering = true;
this.walletRecovery = new WalletRecovery( this.walletRecovery = new WalletRecovery(
this.recoverWalletForm.get("walletName").value,
this.recoverWalletForm.get("walletMnemonic").value, this.recoverWalletForm.get("walletMnemonic").value,
this.recoverWalletForm.get("walletPassword").value, this.recoverWalletForm.get("walletPassword").value,
this.recoverWalletForm.get("selectNetwork").value, this.recoverWalletForm.get("selectNetwork").value,
this.globalService.getWalletPath(),
this.recoverWalletForm.get("walletName").value,
this.creationDate this.creationDate
); );
this.recoverWallet(this.walletRecovery); this.recoverWallets(this.walletRecovery);
} }
private recoverWallet(recoverWallet: WalletRecovery) { private recoverWallets(recoverWallet: WalletRecovery) {
this.apiService
.recoverBitcoinWallet(recoverWallet)
.subscribe(
response => {
if (response.status >= 200 && response.status < 400) {
//Bitcoin Wallet Recovered
}
},
error => {
this.isRecovering = false;
console.log(error);
if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) {
if (!error.json().errors[0]) {
console.log(error);
}
else {
alert(error.json().errors[0].message);
}
}
},
() => this.recoverStratisWallet(recoverWallet)
)
;
}
private recoverStratisWallet(recoverWallet: WalletRecovery){
this.apiService this.apiService
.recoverWallet(recoverWallet) .recoverStratisWallet(recoverWallet)
.subscribe( .subscribe(
response => { response => {
if (response.status >= 200 && response.status < 400) { if (response.status >= 200 && response.status < 400) {
...@@ -112,6 +142,7 @@ export class RecoverComponent implements OnInit { ...@@ -112,6 +142,7 @@ export class RecoverComponent implements OnInit {
} }
}, },
error => { error => {
this.isRecovering = false;
console.log(error); console.log(error);
if (error.status === 0) { if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application."); alert("Something went wrong while connecting to the API. Please restart the application.");
...@@ -124,6 +155,7 @@ export class RecoverComponent implements OnInit { ...@@ -124,6 +155,7 @@ export class RecoverComponent implements OnInit {
} }
} }
} }
); )
;
} }
} }
export class WalletCreation { export class WalletCreation {
constructor(password: string, network:string, folderPath: string, name: string) { constructor(name: string, mnemonic: string, password: string, network:string, folderPath: string = null ) {
this.name = name;
this.mnemonic = mnemonic;
this.password = password; this.password = password;
this.network = network; this.network = network;
this.folderPath = folderPath; this.folderPath = folderPath;
this.name = name;
} }
name: string;
mnemonic: string;
password: string; password: string;
network: string; network: string;
folderPath: string; folderPath?: string;
name: string;
} }
export class WalletLoad { export class WalletLoad {
constructor(password: string, folderPath: string, name: string) { constructor(name: string, password: string, folderPath: string = null ) {
this.name = name;
this.password = password; this.password = password;
this.folderPath = folderPath; this.folderPath = folderPath;
this.name = name;
} }
public password: string;
public folderPath: string;
public name: string; public name: string;
public password: string;
public folderPath?: string;
} }
export class WalletRecovery { export class WalletRecovery {
constructor(mnemonic: string, password: string, network:string, folderPath: string, walletName: string, creationDate: Date) { constructor(walletName: string, mnemonic: string, password: string, network:string, creationDate: Date, folderPath: string = null) {
this.name = walletName;
this.mnemonic = mnemonic; this.mnemonic = mnemonic;
this.password = password; this.password = password;
this.network = network; this.network = network;
this.folderPath = folderPath;
this.name = walletName;
this.creationDate = creationDate; this.creationDate = creationDate;
this.folderPath = folderPath;
} }
mnemonic: string; mnemonic: string;
password: string; password: string;
folderPath: string;
name: string; name: string;
network: string; network: string;
creationDate: Date; creationDate: Date;
folderPath?: string;
} }
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { GlobalService } from '../services/global.service';
@Pipe({ @Pipe({
name: 'coinNotation' name: 'coinNotation'
}) })
export class CoinNotationPipe implements PipeTransform { export class CoinNotationPipe implements PipeTransform {
constructor (private globalService: GlobalService) {
this.setCoinUnit();
}
private coinUnit = "BTC"; private coinUnit: string;
private coinNotation: number; private coinNotation: number;
private decimalLimit = 8; private decimalLimit = 8;
...@@ -15,13 +19,40 @@ export class CoinNotationPipe implements PipeTransform { ...@@ -15,13 +19,40 @@ export class CoinNotationPipe implements PipeTransform {
switch (this.getCoinUnit()) { switch (this.getCoinUnit()) {
case "BTC": case "BTC":
temp = value / 100000000; temp = value / 100000000;
return temp.toFixed(this.decimalLimit) + " TBTC"; return temp.toFixed(this.decimalLimit) + " BTC";
case "mBTC": case "mBTC":
temp = value / 100000; temp = value / 100000;
return temp.toFixed(this.decimalLimit) + " TmBTC"; return temp.toFixed(this.decimalLimit) + " mBTC";
case "uBTC": case "uBTC":
temp = value / 100;
return temp.toFixed(this.decimalLimit) + " uBTC";
case "TBTC":
temp = value / 100000000;
return temp.toFixed(this.decimalLimit) + " TBTC";
case "TmBTC":
temp = value / 100000;
return temp.toFixed(this.decimalLimit) + " TmBTC";
case "TuBTC":
temp = value / 100; temp = value / 100;
return temp.toFixed(this.decimalLimit) + " TuBTC"; return temp.toFixed(this.decimalLimit) + " TuBTC";
case "STRAT":
temp = value / 100000000;
return temp.toFixed(this.decimalLimit) + " STRAT";
case "mSTRAT":
temp = value / 100000;
return temp.toFixed(this.decimalLimit) + " mSTRAT";
case "uSTRAT":
temp = value / 100;
return temp.toFixed(this.decimalLimit) + " uSTRAT";
case "TSTRAT":
temp = value / 100000000;
return temp.toFixed(this.decimalLimit) + " TSTRAT";
case "TmSTRAT":
temp = value / 100000;
return temp.toFixed(this.decimalLimit) + " TmSTRAT";
case "TuSTRAT":
temp = value / 100;
return temp.toFixed(this.decimalLimit) + " TuSTRAT";
} }
} }
} }
...@@ -29,6 +60,10 @@ export class CoinNotationPipe implements PipeTransform { ...@@ -29,6 +60,10 @@ export class CoinNotationPipe implements PipeTransform {
getCoinUnit() { getCoinUnit() {
return this.coinUnit; return this.coinUnit;
} }
setCoinUnit() {
this.coinUnit = this.globalService.getCoinUnit();
};
} }
...@@ -7,6 +7,8 @@ import 'rxjs/add/operator/catch'; ...@@ -7,6 +7,8 @@ import 'rxjs/add/operator/catch';
import "rxjs/add/observable/interval"; import "rxjs/add/observable/interval";
import 'rxjs/add/operator/startWith'; import 'rxjs/add/operator/startWith';
import { GlobalService } from './global.service';
import { WalletCreation } from '../classes/wallet-creation'; import { WalletCreation } from '../classes/wallet-creation';
import { WalletRecovery } from '../classes/wallet-recovery'; import { WalletRecovery } from '../classes/wallet-recovery';
import { WalletLoad } from '../classes/wallet-load'; import { WalletLoad } from '../classes/wallet-load';
...@@ -20,46 +22,96 @@ import { TransactionSending } from '../classes/transaction-sending'; ...@@ -20,46 +22,96 @@ import { TransactionSending } from '../classes/transaction-sending';
*/ */
@Injectable() @Injectable()
export class ApiService { export class ApiService {
constructor(private http: Http) {}; constructor(private http: Http, private globalService: GlobalService) {};
private mockApiUrl = 'http://localhost:3000/api';
private webApiUrl = 'http://localhost:5000/api';
private headers = new Headers({'Content-Type': 'application/json'}); private headers = new Headers({'Content-Type': 'application/json'});
private pollingInterval = 3000; private pollingInterval = 3000;
private bitcoinApiUrl = 'http://localhost:5000/api';
private stratisApiUrl = 'http://localhost:5105/api';
private currentApiUrl = 'http://localhost:5000/api';
private getCurrentCoin() {
let currentCoin = this.globalService.getCoinName();
if (currentCoin === "Bitcoin" || currentCoin === "TestBitcoin") {
this.currentApiUrl = this.bitcoinApiUrl;
} else if (currentCoin === "Stratis" || currentCoin === "TestStratis") {
this.currentApiUrl = this.stratisApiUrl;
}
}
/** /**
* Gets available wallets at the default path * Gets available wallets at the default path
*/ */
getWalletFiles(): Observable<any> { getWalletFiles(): Observable<any> {
return this.http return this.http
.get(this.webApiUrl + '/wallet/files') .get(this.bitcoinApiUrl + '/wallet/files')
.map((response: Response) => response); .map((response: Response) => response);
} }
/**
* Get a new mnemonic
*/
getNewMnemonic(): Observable<any> {
let params: URLSearchParams = new URLSearchParams();
params.set('language', 'English');
params.set('wordCount', '12');
return this.http
.get(this.bitcoinApiUrl + '/wallet/mnemonic', new RequestOptions({headers: this.headers, search: params}))
.map((response: Response) => response);
}
/** /**
* Create a new wallet. * Create a new Bitcoin wallet.
*/ */
createWallet(data: WalletCreation): Observable<any> { createBitcoinWallet(data: WalletCreation): Observable<any> {
return this.http return this.http
.post(this.webApiUrl + '/wallet/create/', JSON.stringify(data), {headers: this.headers}) .post(this.bitcoinApiUrl + '/wallet/create/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response); .map((response: Response) => response);
} }
/** /**
* Recover a wallet. * Create a new Stratis wallet.
*/ */
recoverWallet(data: WalletRecovery): Observable<any> { createStratisWallet(data: WalletCreation): Observable<any> {
return this.http return this.http
.post(this.webApiUrl + '/wallet/recover/', JSON.stringify(data), {headers: this.headers}) .post(this.stratisApiUrl + '/wallet/create/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response); .map((response: Response) => response);
} }
/** /**
* Load a wallet * Recover a Bitcoin wallet.
*/ */
loadWallet(data: WalletLoad): Observable<any> { recoverBitcoinWallet(data: WalletRecovery): Observable<any> {
return this.http return this.http
.post(this.webApiUrl + '/wallet/load/', JSON.stringify(data), {headers: this.headers}) .post(this.bitcoinApiUrl + '/wallet/recover/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response);
}
/**
* Recover a Stratis wallet.
*/
recoverStratisWallet(data: WalletRecovery): Observable<any> {
return this.http
.post(this.stratisApiUrl + '/wallet/recover/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response);
}
/**
* Load a Bitcoin wallet
*/
loadBitcoinWallet(data: WalletLoad): Observable<any> {
return this.http
.post(this.bitcoinApiUrl + '/wallet/load/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response);
}
/**
* Load a Stratis wallet
*/
loadStratisWallet(data: WalletLoad): Observable<any> {
return this.http
.post(this.stratisApiUrl + '/wallet/load/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response); .map((response: Response) => response);
} }
...@@ -67,8 +119,10 @@ export class ApiService { ...@@ -67,8 +119,10 @@ export class ApiService {
* Get wallet status info from the API. * Get wallet status info from the API.
*/ */
getWalletStatus(): Observable<any> { getWalletStatus(): Observable<any> {
this.getCurrentCoin();
return this.http return this.http
.get(this.webApiUrl + '/wallet/status') .get(this.currentApiUrl + '/wallet/status')
.map((response: Response) => response); .map((response: Response) => response);
} }
...@@ -76,13 +130,15 @@ export class ApiService { ...@@ -76,13 +130,15 @@ export class ApiService {
* Get general wallet info from the API. * Get general wallet info from the API.
*/ */
getGeneralInfo(data: WalletInfo): Observable<any> { getGeneralInfo(data: WalletInfo): Observable<any> {
this.getCurrentCoin();
let params: URLSearchParams = new URLSearchParams(); let params: URLSearchParams = new URLSearchParams();
params.set('Name', data.walletName); params.set('Name', data.walletName);
return Observable return Observable
.interval(this.pollingInterval) .interval(this.pollingInterval)
.startWith(0) .startWith(0)
.switchMap(() => this.http.get(this.webApiUrl + '/wallet/general-info', new RequestOptions({headers: this.headers, search: params}))) .switchMap(() => this.http.get(this.currentApiUrl + '/wallet/general-info', new RequestOptions({headers: this.headers, search: params})))
.map((response: Response) => response); .map((response: Response) => response);
} }
...@@ -90,66 +146,79 @@ export class ApiService { ...@@ -90,66 +146,79 @@ export class ApiService {
* Get wallet balance info from the API. * Get wallet balance info from the API.
*/ */
getWalletBalance(data: WalletInfo): Observable<any> { getWalletBalance(data: WalletInfo): Observable<any> {
this.getCurrentCoin();
let params: URLSearchParams = new URLSearchParams(); let params: URLSearchParams = new URLSearchParams();
params.set('walletName', data.walletName); params.set('walletName', data.walletName);
return Observable return Observable
.interval(this.pollingInterval) .interval(this.pollingInterval)
.startWith(0) .startWith(0)
.switchMap(() => this.http.get(this.webApiUrl + '/wallet/balance', new RequestOptions({headers: this.headers, search: params}))) .switchMap(() => this.http.get(this.currentApiUrl + '/wallet/balance', new RequestOptions({headers: this.headers, search: params})))
.map((response: Response) => response); .map((response: Response) => response);
// return this.http
// .get(this.webApiUrl + '/wallet/balance', new RequestOptions({headers: this.headers, search: params}))
// .map((response: Response) => response);
} }
/** /**
* Get a wallets transaction history info from the API. * Get a wallets transaction history info from the API.
*/ */
getWalletHistory(data: WalletInfo): Observable<any> { getWalletHistory(data: WalletInfo): Observable<any> {
this.getCurrentCoin();
let params: URLSearchParams = new URLSearchParams(); let params: URLSearchParams = new URLSearchParams();
params.set('walletName', data.walletName); params.set('walletName', data.walletName);
return Observable return Observable
.interval(this.pollingInterval) .interval(this.pollingInterval)
.startWith(0) .startWith(0)
.switchMap(() => this.http.get(this.webApiUrl + '/wallet/history', new RequestOptions({headers: this.headers, search: params}))) .switchMap(() => this.http.get(this.currentApiUrl + '/wallet/history', new RequestOptions({headers: this.headers, search: params})))
.map((response: Response) => response); .map((response: Response) => response);
// return this.http
// .get(this.webApiUrl + '/wallet/history', new RequestOptions({headers: this.headers, search: params}))
// .map((response: Response) => response);
} }
/** /**
* Get unused receive addresses for a certain wallet from the API. * Get unused receive addresses for a certain wallet from the API.
*/ */
getUnusedReceiveAddress(data: WalletInfo): Observable<any> { getUnusedReceiveAddress(data: WalletInfo): Observable<any> {
this.getCurrentCoin();
let params: URLSearchParams = new URLSearchParams(); let params: URLSearchParams = new URLSearchParams();
params.set('walletName', data.walletName); params.set('walletName', data.walletName);
params.set('accountName', "account 0"); //temporary params.set('accountName', "account 0"); //temporary
return this.http return this.http
.get(this.webApiUrl + '/wallet/address', new RequestOptions({headers: this.headers, search: params})) .get(this.currentApiUrl + '/wallet/address', new RequestOptions({headers: this.headers, search: params}))
.map((response: Response) => response); .map((response: Response) => response);
} }
/**
* Build a transaction
*/
buildTransaction(data: TransactionBuilding): Observable<any> { buildTransaction(data: TransactionBuilding): Observable<any> {
this.getCurrentCoin();
return this.http return this.http
.post(this.webApiUrl + '/wallet/build-transaction/', JSON.stringify(data), {headers: this.headers}) .post(this.currentApiUrl + '/wallet/build-transaction/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response); .map((response: Response) => response);
} }
/**
* Send transaction
*/
sendTransaction(data: TransactionSending): Observable<any> { sendTransaction(data: TransactionSending): Observable<any> {
this.getCurrentCoin();
return this.http return this.http
.post(this.webApiUrl + '/wallet/send-transaction/', JSON.stringify(data), {headers: this.headers}) .post(this.currentApiUrl + '/wallet/send-transaction/', JSON.stringify(data), {headers: this.headers})
.map((response: Response) => response); .map((response: Response) => response);
} }
/**
* Send shutdown signal to the daemon
*/
shutdownNode(): Observable<any> { shutdownNode(): Observable<any> {
this.getCurrentCoin();
return this.http return this.http
.post(this.webApiUrl + '/node/shutdown', '') .post(this.currentApiUrl + '/node/shutdown', '')
.map((response: Response) => response); .map((response: Response) => response);
} }
} }
...@@ -6,18 +6,25 @@ export class GlobalService { ...@@ -6,18 +6,25 @@ export class GlobalService {
private walletPath: string; private walletPath: string;
private currentWalletName: string; private currentWalletName: string;
private coinType: number; private coinType: number = 0;
private coinName: string = "TestBitcoin";
private coinUnit: string = "TBTC";
private network: string = "TestNet";
getWalletPath() { getWalletPath() {
return this.walletPath; return this.walletPath;
} }
setWalletPath(walletPath: string) {
this.walletPath = walletPath;
}
getNetwork() { getNetwork() {
return "TestNet"; return this.network;
} }
setWalletPath(walletPath: string) { setNetwork(network: string) {
this.walletPath = walletPath; this.network = network;
} }
getWalletName() { getWalletName() {
...@@ -28,11 +35,27 @@ export class GlobalService { ...@@ -28,11 +35,27 @@ export class GlobalService {
this.currentWalletName = currentWalletName; this.currentWalletName = currentWalletName;
} }
getCoinType () { getCoinType() {
return this.coinType; return this.coinType;
} }
setCoinType (coinType: number) { setCoinType (coinType: number) {
this.coinType = coinType; this.coinType = coinType;
} }
getCoinName() {
return this.coinName;
}
setCoinName(coinName: string) {
this.coinName = coinName;
}
getCoinUnit() {
return this.coinUnit;
}
setCoinUnit(coinUnit: string) {
this.coinUnit = coinUnit;
}
} }
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
<!-- /JUMBOTRON--> <!-- /JUMBOTRON-->
<!-- TRANSACTIONS --> <!-- TRANSACTIONS -->
<section id="transaction" class="container mt-4"> <section id="transaction" class="container">
<h5 class="pt-4">Transactions</h5> <h5>Transactions</h5>
<div *ngIf="transactions; else noTransactions"> <div *ngIf="transactions; else noTransactions">
<div *ngFor="let transaction of transactions"> <div *ngFor="let transaction of transactions">
<div class="card" (click)="openTransactionDetailDialog(transaction)"> <div class="card" (click)="openTransactionDetailDialog(transaction)">
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<span class="float-right"><a ngxClipboard [cbContent]="address" (click)="onCopiedClick()">copy</a></span> <span class="float-right"><a ngxClipboard [cbContent]="address" (click)="onCopiedClick()">copy</a></span>
</div> </div>
<div class="myAddress"><code>{{ address }}</code></div> <div class="myAddress"><code>{{ address }}</code></div>
<div class="text-center row" *ngIf="copied"> <div class="text-center" *ngIf="copied">
<span class="badge badge-success col">Your address has been copied to your clipboard.</span> <span class="badge badge-success col">Your address has been copied to your clipboard.</span>
</div> </div>
</form> </form>
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
</ul> </ul>
<ul class="list-inline row"> <ul class="list-inline row">
<li class="list-inline-item col blockLabel">Amount</li> <li class="list-inline-item col blockLabel">Amount</li>
<li class="list-inline-item col-9 blockText"><strong class="text-danger">-{{ transaction.amount }}</strong> <small class="text-uppercase ml-2">BTC</small></li> <li class="list-inline-item col-9 blockText text-danger">-{{ transaction.amount | number:'1.8-8' }} {{ coinUnit }}</li>
</ul> </ul>
<ul class="list-inline row"> <ul class="list-inline row">
<li class="list-inline-item col blockLabel">Destination</li> <li class="list-inline-item col blockLabel">Destination</li>
......
...@@ -2,6 +2,8 @@ import { Component, OnInit, Input } from '@angular/core'; ...@@ -2,6 +2,8 @@ import { Component, OnInit, Input } from '@angular/core';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { GlobalService } from '../../../shared/services/global.service';
@Component({ @Component({
selector: 'app-send-confirmation', selector: 'app-send-confirmation',
templateUrl: './send-confirmation.component.html', templateUrl: './send-confirmation.component.html',
...@@ -10,12 +12,13 @@ import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; ...@@ -10,12 +12,13 @@ import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
export class SendConfirmationComponent implements OnInit { export class SendConfirmationComponent implements OnInit {
@Input() transaction: any; @Input() transaction: any;
constructor(public activeModal: NgbActiveModal) { } constructor(private globalService: GlobalService, public activeModal: NgbActiveModal) { }
private showDetails: boolean = false; private showDetails: boolean = false;
private coinUnit: string;
ngOnInit() { ngOnInit() {
console.log(this.transaction); this.coinUnit = this.globalService.getCoinUnit();
} }
toggleDetails() { toggleDetails() {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="form-group clearfix"> <div class="form-group clearfix">
<label class="float-left" for="yourAddress">Amount</label> <label class="float-left" for="yourAddress">Amount</label>
<!--<span class="float-right"><a href="#">max</a></span>--> <!--<span class="float-right"><a href="#">max</a></span>-->
<input type="text" class="form-control form-control-danger" formControlName="amount" id="Amount" placeholder="0.00 BTC"> <input type="text" class="form-control form-control-danger" formControlName="amount" id="Amount" placeholder="0.00 {{ coinUnit }}">
<div *ngIf="formErrors.amount" class="form-control-feedback">{{ formErrors.amount }}</div> <div *ngIf="formErrors.amount" class="form-control-feedback">{{ formErrors.amount }}</div>
</div> </div>
<!--<div class="form-group has-success">--> <!--<div class="form-group has-success">-->
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
<a><button type="button" class="btn btn-link col-12" (click)="activeModal.close('Close click')">Cancel</button></a> <a><button type="button" class="btn btn-link col-12" (click)="activeModal.close('Close click')">Cancel</button></a>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="submit" class="btn btn-lg btn-primary" [disabled]="!sendForm.valid" (click)="send()">Send</button> <button type="submit" class="btn btn-lg btn-primary" [disabled]="!sendForm.valid || isSending" (click)="send()">Send</button>
</div> </div>
</div> </div>
</div> </div>
......
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ApiService } from '../../shared/services/api.service'; import { ApiService } from '../../shared/services/api.service';
import { GlobalService } from '../../shared/services/global.service'; import { GlobalService } from '../../shared/services/global.service';
import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms'; import { FormGroup, FormControl, Validators, FormBuilder } from '@angular/forms';
...@@ -16,7 +16,7 @@ import { SendConfirmationComponent } from './send-confirmation/send-confirmation ...@@ -16,7 +16,7 @@ import { SendConfirmationComponent } from './send-confirmation/send-confirmation
styleUrls: ['./send.component.css'], styleUrls: ['./send.component.css'],
}) })
export class SendComponent { export class SendComponent implements OnInit {
constructor(private apiService: ApiService, private globalService: GlobalService, private modalService: NgbModal, public activeModal: NgbActiveModal, private fb: FormBuilder) { constructor(private apiService: ApiService, private globalService: GlobalService, private modalService: NgbModal, public activeModal: NgbActiveModal, private fb: FormBuilder) {
this.buildSendForm(); this.buildSendForm();
} }
...@@ -24,7 +24,13 @@ export class SendComponent { ...@@ -24,7 +24,13 @@ export class SendComponent {
private sendForm: FormGroup; private sendForm: FormGroup;
private responseMessage: any; private responseMessage: any;
private errorMessage: string; private errorMessage: string;
private coinUnit: string;
private transaction: TransactionBuilding; private transaction: TransactionBuilding;
private isSending: Boolean = false;
ngOnInit() {
this.coinUnit = this.globalService.getCoinUnit();
}
private buildSendForm(): void { private buildSendForm(): void {
this.sendForm = this.fb.group({ this.sendForm = this.fb.group({
...@@ -79,6 +85,8 @@ export class SendComponent { ...@@ -79,6 +85,8 @@ export class SendComponent {
}; };
private send() { private send() {
this.isSending = true;
this.transaction = new TransactionBuilding( this.transaction = new TransactionBuilding(
this.globalService.getWalletName(), this.globalService.getWalletName(),
this.globalService.getCoinType(), this.globalService.getCoinType(),
...@@ -96,11 +104,11 @@ export class SendComponent { ...@@ -96,11 +104,11 @@ export class SendComponent {
response => { response => {
if (response.status >= 200 && response.status < 400){ if (response.status >= 200 && response.status < 400){
this.responseMessage = response.json(); this.responseMessage = response.json();
console.log(this.responseMessage);
} }
}, },
error => { error => {
console.log(error); console.log(error);
this.isSending = false;
if (error.status === 0) { if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application."); alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) { } else if (error.status >= 400) {
...@@ -142,6 +150,7 @@ export class SendComponent { ...@@ -142,6 +150,7 @@ export class SendComponent {
}, },
error => { error => {
console.log(error); console.log(error);
this.isSending = false;
if (error.status === 0) { if (error.status === 0) {
alert("Something went wrong while connecting to the API. Please restart the application."); alert("Something went wrong while connecting to the API. Please restart the application.");
} else if (error.status >= 400) { } else if (error.status >= 400) {
......
<!-- SIDEBAR --> <!-- sidebar -->
<aside id="sidebar"> <aside id="sidebar">
<!-- menu-->
<ul class="list-unstyled menu"> <ul class="list-unstyled menu">
<li class="active"> <li (click)="loadBitcoinWallet()" [class.active]="bitcoinActive">
<!--<a href="#">--><img src="../../../assets/images/ico_bitcoin.svg" alt="Bitcoin"><!--</a>--> <img src="../../../assets/images/ico_bitcoin.svg" alt="Bitcoin">
<span class="bar"></span> <span class="bar"></span>
</li> </li>
<!--<li> <li (click)="loadStratisWallet()" [class.active]="!bitcoinActive">
<a href="#"><img src="../../../assets/images/ico_stratis.svg" alt="Stratis"></a> <img src="../../../assets/images/ico_stratis.svg" alt="Stratis">
<span class="bar"></span> <span class="bar"></span>
</li>--> </li>
</ul> </ul>
<!-- /menu--> <!-- /menu-->
<ul class="list-unstyled second"> <ul class="list-unstyled second">
......
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { LogoutConfirmationComponent } from '../logout-confirmation/logout-confirmation.component'; import { LogoutConfirmationComponent } from '../logout-confirmation/logout-confirmation.component';
import { Router } from '@angular/router';
import { GlobalService } from '../../shared/services/global.service';
@Component({ @Component({
selector: 'sidebar', selector: 'sidebar',
...@@ -9,13 +12,36 @@ import { LogoutConfirmationComponent } from '../logout-confirmation/logout-confi ...@@ -9,13 +12,36 @@ import { LogoutConfirmationComponent } from '../logout-confirmation/logout-confi
}) })
export class SidebarComponent implements OnInit { export class SidebarComponent implements OnInit {
constructor(private modalService: NgbModal) { } constructor(private globalService: GlobalService, private router: Router, private modalService: NgbModal) { }
private bitcoinActive: Boolean;
ngOnInit() { ngOnInit() {
if (this.globalService.getCoinName() === "Bitcoin" || this.globalService.getCoinName() === "TestBitcoin") {
this.bitcoinActive = true;
} else if (this.globalService.getCoinName() === "Stratis" || this.globalService.getCoinName() === "TestStratis") {
this.bitcoinActive = false;
}
}
private loadBitcoinWallet() {
this.toggleClass();
this.globalService.setCoinName("TestBitcoin");
this.globalService.setCoinUnit("TBTC");
this.router.navigate(['/wallet']);
}
private loadStratisWallet() {
this.toggleClass();
this.globalService.setCoinName("TestStratis");
this.globalService.setCoinUnit("TSTRAT");
this.router.navigate(['/wallet/stratis-wallet']);
}
private toggleClass(){
this.bitcoinActive = !this.bitcoinActive;
} }
private logOut() { private logOut() {
const modalRef = this.modalService.open(LogoutConfirmationComponent); const modalRef = this.modalService.open(LogoutConfirmationComponent);
} }
} }
...@@ -8,14 +8,20 @@ import { HistoryComponent } from './history/history.component'; ...@@ -8,14 +8,20 @@ import { HistoryComponent } from './history/history.component';
import { DashboardComponent } from './dashboard/dashboard.component'; import { DashboardComponent } from './dashboard/dashboard.component';
const routes: Routes = [ const routes: Routes = [
{ path: '', redirectTo: 'wallet', pathMatch: 'full' }, { path: '', component: WalletComponent,
{ path: 'wallet', component: WalletComponent,
children: [ children: [
{ path: '', redirectTo:'dashboard', pathMatch:'full' }, { path: '', redirectTo:'dashboard', pathMatch:'full' },
{ path: 'dashboard', component: DashboardComponent}, { path: 'dashboard', component: DashboardComponent},
{ path: 'history', component: HistoryComponent} { path: 'history', component: HistoryComponent}
] ]
}, },
{ path: 'stratis-wallet', component: WalletComponent,
children: [
{ path: '', redirectTo:'dashboard', pathMatch:'full' },
{ path: 'dashboard', component: DashboardComponent},
{ path: 'history', component: HistoryComponent}
]
}
]; ];
@NgModule({ @NgModule({
......
...@@ -139,10 +139,12 @@ em { ...@@ -139,10 +139,12 @@ em {
font-size: .85em; font-size: .85em;
color: $gray-dark; color: $gray-dark;
} }
p {margin-bottom: 0 !important;}
.lead { .lead {
color: $black; color: $black;
font-size: 1.85em; font-size: 1.85em;
font-weight: 500; font-weight: 500;
margin-bottom: 0;
.h2 { .h2 {
font-size: 2em; font-size: 2em;
font-weight: 600; font-weight: 600;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment