wallet
This commit is contained in:
+2
-1
@@ -11,7 +11,8 @@
|
||||
"build-dashboard": "cd src/frontend/dashboard; npm run build; cp -r dist/dashboard/* ../../../static",
|
||||
"build-widget": "rollup -c src/frontend/widget/rollup.config.js; mkdir -p plugins/PluginManager; cp FrontendPlugin.js plugins/PluginManager",
|
||||
"clean": "rm -rf lib static plugins data conf dist widget .rpt2_cache *.js *.ts src/frontend/dashboard/dist",
|
||||
"update-frontblock": "rm -rf node_modules/frontblock*; npm install"
|
||||
"update-frontblock": "rm -rf node_modules/frontblock*; npm install",
|
||||
"webpack": "webpack --entry ./lib/FrontblockLib.js --output ./FrontblockLib.min.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Generated
+16
@@ -2588,6 +2588,17 @@
|
||||
"bsert": "~0.0.10"
|
||||
}
|
||||
},
|
||||
"btc-hdkey": {
|
||||
"version": "0.0.17",
|
||||
"resolved": "https://registry.npmjs.org/btc-hdkey/-/btc-hdkey-0.0.17.tgz",
|
||||
"integrity": "sha512-1hzd8DeK/cjrBY6nJ+LenBTGL5rQ+dFBaDEoDSmQ/7zvV3EoV9rsa/aYLLke0Q5XPfbxC1lV/Ro5KpNULA7EYQ==",
|
||||
"requires": {
|
||||
"bip39": "^3.0.2",
|
||||
"bitcoinjs-lib": "^5.1.3",
|
||||
"bsert": "0.0.10",
|
||||
"hdkey": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||
@@ -2973,6 +2984,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"coinselect": {
|
||||
"version": "3.1.11",
|
||||
"resolved": "https://registry.npmjs.org/coinselect/-/coinselect-3.1.11.tgz",
|
||||
"integrity": "sha1-4fBjvRpYgvZzXuBRm52LWsSpMJk="
|
||||
},
|
||||
"coinstring": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/coinstring/-/coinstring-2.3.0.tgz",
|
||||
|
||||
@@ -24,12 +24,16 @@
|
||||
"@clr/icons": "^2.1.1",
|
||||
"@clr/ui": "^2.1.1",
|
||||
"@webcomponents/custom-elements": "^1.0.0",
|
||||
"btc-hdkey": "0.0.17",
|
||||
"coinselect": "^3.1.11",
|
||||
"frontblock": "latest",
|
||||
"frontblock-generic": "^0.22.0",
|
||||
"key-file-storage": "^2.2.4",
|
||||
"node-fetch": "^2.6.0",
|
||||
"rxjs": "~6.5.2",
|
||||
"systemjs": "^0.21.3",
|
||||
"tslib": "^1.9.0",
|
||||
"uuid": "^3.3.2",
|
||||
"zone.js": "~0.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,40 +1,16 @@
|
||||
import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
import { SubscriptionResponse, parseResponse, SuccessResponse } from 'frontblock-generic/Types.js';
|
||||
import { SubscriptionResponse, parseResponse, SuccessResponse, Coin } from 'frontblock-generic/Types.js';
|
||||
import { default as uuid } from 'uuid/v4'
|
||||
|
||||
declare const fb
|
||||
|
||||
const fb = !isDevMode() ? window["fb"] : {
|
||||
ApiClient: {
|
||||
setConfig: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve('Promise A win!');
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
getConsumptions: async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve([new SubscriptionResponse("654", "321"), new SubscriptionResponse("456", "123")]);
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
getSubscriptions: async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve([new SubscriptionResponse("321"), new SubscriptionResponse("123")]);
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
unsubscribe: (uid) => new SuccessResponse(),
|
||||
quit: (uid) => new SuccessResponse()
|
||||
}
|
||||
}
|
||||
@Component({
|
||||
selector: 'consumptions',
|
||||
selector: 'consumers',
|
||||
template: `
|
||||
<div class="clr-row">
|
||||
<div class="card clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12">
|
||||
<div class="card clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-auto clr-col-xl-auto">
|
||||
<div class="card-header">
|
||||
Consumptions
|
||||
Consumers
|
||||
</div>
|
||||
<clr-alert *ngFor="let entry of alerts" [clrAlertType]="entry.severity">
|
||||
<clr-alert-item>
|
||||
@@ -47,16 +23,18 @@ const fb = !isDevMode() ? window["fb"] : {
|
||||
<div *ngIf="loading" class="card-block">
|
||||
<span class="spinner spinner-inline"></span>
|
||||
</div>
|
||||
<div *ngIf="entries.length !== 0 && !loading" class="card-block">
|
||||
<div class="card-text">
|
||||
<div *ngIf="entries.length !== 0 && !loading" class="card-block limit-height">
|
||||
<div class="card-text limit-height">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>UUID</clr-dg-column>
|
||||
<clr-dg-column>Subscription UUID</clr-dg-column>
|
||||
<clr-dg-column>Expiry</clr-dg-column>
|
||||
<clr-dg-column>Creation</clr-dg-column>
|
||||
|
||||
<clr-dg-row *ngFor="let line of entries">
|
||||
<clr-dg-row *clrDgItems="let line of entries">
|
||||
<clr-dg-cell>{{line.uid}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.expiry}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.message}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.expiry | date}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.created | date}}</clr-dg-cell>
|
||||
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
@@ -64,7 +42,11 @@ const fb = !isDevMode() ? window["fb"] : {
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{entries.length}} entries
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">Users per page</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}} users
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
@@ -76,9 +58,9 @@ const fb = !isDevMode() ? window["fb"] : {
|
||||
`
|
||||
})
|
||||
export class ApiclientConsumptionComponent implements OnInit {
|
||||
loading:boolean = false
|
||||
actionName:string = "load"
|
||||
entries:SubscriptionResponse[] = []
|
||||
loading: boolean = false
|
||||
actionName: string = "load"
|
||||
entries: SubscriptionResponse[] = []
|
||||
alerts: {
|
||||
severity: "danger" | "warning" | "success"
|
||||
message: string
|
||||
@@ -88,24 +70,24 @@ export class ApiclientConsumptionComponent implements OnInit {
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
async load(){
|
||||
async load() {
|
||||
this.loading = true
|
||||
this.entries = await fb.ApiClient.getConsumptions()
|
||||
if(this.entries.length === 0){
|
||||
this.alerts.push({ severity: "warning", message: "0 results were returned"})
|
||||
if (this.entries.length === 0) {
|
||||
this.alerts.push({ severity: "warning", message: "0 results were returned" })
|
||||
}
|
||||
this.loading = false
|
||||
this.actionName = "refresh"
|
||||
}
|
||||
|
||||
async quit(uid:string){
|
||||
async quit(uid: string) {
|
||||
const r = await fb.ApiClient.quit(uid)
|
||||
const res = parseResponse(r)
|
||||
if(res instanceof SuccessResponse){
|
||||
this.alerts.push({ severity: "success", message: "Quit consuming "+uid})
|
||||
if (res instanceof SuccessResponse) {
|
||||
this.alerts.push({ severity: "success", message: "Quit consuming " + uid })
|
||||
this.load()
|
||||
}else{
|
||||
this.alerts.push({ severity: "danger", message: "Error "+res.message})
|
||||
} else {
|
||||
this.alerts.push({ severity: "danger", message: "Error " + res.message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { SidebarEntry } from 'frontblock-generic/Plugin';
|
||||
import { environment } from "../../environments/environment"
|
||||
|
||||
@Component({
|
||||
selector: 'apiclient', //!!!!
|
||||
template: `
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-lg-auto">
|
||||
<settings></settings>
|
||||
</div>
|
||||
<div class="clr-col-12 clr-col-lg-12 clr-col-xl-12 clr-col-sm-12">
|
||||
<consumptions></consumptions>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<subscriptions></subscriptions>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ApiclientDevComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
|
||||
export const sidebarEntry:SidebarEntry = {
|
||||
icon: "wrench",
|
||||
route: "dev/apiclient",
|
||||
text: "DEV ApiClient",
|
||||
}
|
||||
@@ -2,17 +2,7 @@ import { Component, OnInit } from '@angular/core';
|
||||
import { FrontblockApiConf } from 'frontblock';
|
||||
import { isDevMode } from '@angular/core';
|
||||
|
||||
const fb = !isDevMode ? window["fb"] : {
|
||||
ApiClient: {
|
||||
setConfig: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve('Promise A win!');
|
||||
}, 500)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
declare const fb
|
||||
|
||||
@Component({
|
||||
selector: 'settings',
|
||||
|
||||
@@ -1,33 +1,7 @@
|
||||
import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
import { SubscriptionResponse, SuccessResponse, parseResponse } from 'frontblock-generic/Types.js';
|
||||
|
||||
const fb = !isDevMode() ? window["fb"] : {
|
||||
ApiClient: {
|
||||
setConfig: async () => {
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve('Promise A win!');
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
getConsumptions: async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve([new SubscriptionResponse("654", "321"), new SubscriptionResponse("456", "123")]);
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
getSubscriptions: async () => {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
resolve([new SubscriptionResponse("321"), new SubscriptionResponse("123")]);
|
||||
}, 500)
|
||||
})
|
||||
},
|
||||
unsubscribe: (uid) => new SuccessResponse(),
|
||||
quit: (uid) => new SuccessResponse()
|
||||
}
|
||||
}
|
||||
import { SubscriptionResponse, SuccessResponse, parseResponse, Coin } from 'frontblock-generic/Types.js';
|
||||
import { default as uuid } from 'uuid/v4'
|
||||
declare const fb
|
||||
|
||||
@Component({
|
||||
selector: 'subscriptions',
|
||||
@@ -52,20 +26,34 @@ const fb = !isDevMode() ? window["fb"] : {
|
||||
<div class="card-text">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>UUID</clr-dg-column>
|
||||
<!--
|
||||
<clr-dg-column>Currency</clr-dg-column>
|
||||
<clr-dg-column>Address</clr-dg-column>
|
||||
<clr-dg-column>Memo</clr-dg-column>
|
||||
<clr-dg-column>Expiry</clr-dg-column>
|
||||
<clr-dg-column>Creation</clr-dg-column>
|
||||
-->
|
||||
|
||||
<clr-dg-row *ngFor="let line of entries">
|
||||
<clr-dg-row *clrDgItems="let line of entries">
|
||||
<clr-dg-cell>{{line.uid}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.expiry}}</clr-dg-cell>
|
||||
<!--
|
||||
<clr-dg-cell>{{line.currency}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.address}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.memo}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.expiry | date}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.created | date}}</clr-dg-cell>
|
||||
-->
|
||||
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
<button class="btn btn-icon btn-danger-outline" (click)="quit(line.uid)"><clr-icon shape="times"></clr-icon> Unsubscribe</button>
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
{{entries.length}} entries
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">Users per page</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}} users
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
@@ -81,7 +69,7 @@ const fb = !isDevMode() ? window["fb"] : {
|
||||
export class ApiclientSubscriptionComponent implements OnInit {
|
||||
loading: boolean = false
|
||||
actionName: string = "load"
|
||||
entries: SubscriptionResponse[] = []
|
||||
entries: (SubscriptionResponse )[] = []
|
||||
alerts: {
|
||||
severity: "danger" | "warning" | "success"
|
||||
message: string
|
||||
|
||||
@@ -2,7 +2,6 @@ import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { ApiclientDevComponent as ApiclientComponent } from './apiclient-dev.component';
|
||||
import { ApiclientFormComponent } from './apiclient-settings-form.component';
|
||||
import { ApiclientConsumptionComponent } from './apiclient-consumptions.component';
|
||||
import { ApiclientSubscriptionComponent } from './apiclient-subscriptions.component';
|
||||
@@ -21,12 +20,11 @@ import { FormsModule } from '@angular/forms';
|
||||
RouterModule.forChild([
|
||||
{path: "settings", component: ApiclientFormComponent},
|
||||
{path: "subscriptions", component: ApiclientSubscriptionComponent},
|
||||
{path: "consumptions", component: ApiclientConsumptionComponent},
|
||||
{path: "consumers", component: ApiclientConsumptionComponent},
|
||||
]),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
declarations: [
|
||||
ApiclientComponent,
|
||||
ApiclientFormComponent,
|
||||
ApiclientConsumptionComponent,
|
||||
ApiclientSubscriptionComponent
|
||||
@@ -42,8 +40,8 @@ export class PluginModule implements FrontendPlugin{
|
||||
route: "settings",
|
||||
text: "Settings"
|
||||
},{
|
||||
route: "consumptions",
|
||||
text: "Consumptions"
|
||||
route: "consumers",
|
||||
text: "Consumers"
|
||||
},{
|
||||
route: "subscriptions",
|
||||
text: "Subscriptions"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'plugins', //!!!!
|
||||
@@ -15,32 +15,61 @@ declare const fb
|
||||
</clr-modal>
|
||||
|
||||
<div class="clr-row">
|
||||
<div class="clr-col">
|
||||
<div class="card">
|
||||
<div class="card clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12">
|
||||
<div class="card-header">
|
||||
Plugins
|
||||
</div>
|
||||
<div class="card-block">
|
||||
<clr-alert *ngFor="let entry of alerts" [clrAlertType]="line.severity">
|
||||
<clr-alert-item>
|
||||
<span class="alert-text">
|
||||
{{line.message}}
|
||||
</span>
|
||||
</clr-alert-item>
|
||||
</clr-alert>
|
||||
|
||||
<div *ngIf="loading" class="card-block">
|
||||
<span class="spinner spinner-inline"></span>
|
||||
</div>
|
||||
<div *ngIf="plugins.length !== 0 && !loading" class="card-block">
|
||||
<div class="card-text">
|
||||
<clr-accordion>
|
||||
<clr-accordion-panel *ngFor="let entry of plugins">
|
||||
<clr-accordion-title>{{entry.name}}</clr-accordion-title>
|
||||
<clr-accordion-content *clrIfExpanded>
|
||||
<button *ngIf="entry.status === 'Stopped'" class="btn btn-icon btn-success-outline" (click)="start(entry.name)"><clr-icon shape="play"></clr-icon></button>
|
||||
<button *ngIf="entry.status === 'Running'" class="btn btn-icon btn-warning-outline" (click)="stop(entry.name)"><clr-icon shape="stop"></clr-icon></button>
|
||||
<button *ngIf="entry.status === 'Stopped'" class="btn btn-icon btn-danger-outline" (click)="delete(entry.name)"><clr-icon shape="trash"></clr-icon></button>
|
||||
<button *ngIf="entry.status === 'Available'" class="btn btn-icon btn-info-outline" (click)="showConfirmation(entry.name)"><clr-icon shape="download"></clr-icon></button>
|
||||
<button *ngIf="entry.available !== entry.installed && entry.status === 'Stopped'" class="btn btn-icon btn-outline " (click)="showConfirmation(entry.name)">
|
||||
<clr-datagrid>
|
||||
<clr-dg-column>Name</clr-dg-column>
|
||||
<clr-dg-column>Available</clr-dg-column>
|
||||
<clr-dg-column>Installed</clr-dg-column>
|
||||
<clr-dg-column>Status</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let line of plugins">
|
||||
<clr-dg-cell>{{line.name}}</clr-dg-cell>
|
||||
<clr-dg-cell>
|
||||
<span *ngIf="line.installed != line.available" class="label label-light-blue">{{line.available}}</span>
|
||||
<span *ngIf="line.installed == line.available">{{line.available}}</span>
|
||||
</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.installed}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.status}}</clr-dg-cell>
|
||||
|
||||
<clr-dg-row-detail *clrIfExpanded>
|
||||
<button *ngIf="line.status === 'Stopped'" class="btn btn-icon btn-success-outline" (click)="start(line.name)"><clr-icon shape="play"></clr-icon></button>
|
||||
<button *ngIf="line.status === 'Running'" class="btn btn-icon btn-warning-outline" (click)="stop(line.name)"><clr-icon shape="pause"></clr-icon></button>
|
||||
<button *ngIf="line.status === 'Stopped'" class="btn btn-icon btn-danger-outline" (click)="delete(line.name)"><clr-icon shape="trash"></clr-icon></button>
|
||||
<button *ngIf="line.status === 'Available'" class="btn btn-icon btn-info-outline" (click)="showConfirmation(line.name)"><clr-icon shape="download"></clr-icon></button>
|
||||
<button *ngIf="line.available !== line.installed && line.status === 'Stopped'" class="btn btn-icon btn-outline " (click)="showConfirmation(line.name)">
|
||||
<clr-icon shape="sync"></clr-icon>
|
||||
</button>
|
||||
</clr-accordion-content>
|
||||
</clr-accordion-panel>
|
||||
</clr-accordion>
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="10">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">per page</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<!-- <button class="btn btn-success" onclick="setup()">HELLO</button> -->
|
||||
</div>
|
||||
<button *ngIf="!reloading" class="btn btn-sm btn-link" (click)="reload()">Check</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,14 +85,20 @@ export class PluginsComponent implements OnInit {
|
||||
name: "test II: Return of test",
|
||||
available: "0.0.1",
|
||||
installed: "0.0.1",
|
||||
status: "Available"
|
||||
status: "Stopped"
|
||||
}, {
|
||||
name: "test 3",
|
||||
available: "0.0.2",
|
||||
installed: "0.0.1",
|
||||
installed: "",
|
||||
status: "Available"
|
||||
}, {
|
||||
name: "test IV: minor bump",
|
||||
available: "0.9.27",
|
||||
installed: "0.0.11",
|
||||
status: "Stopped"
|
||||
},]
|
||||
|
||||
reloading = false
|
||||
updatepending = false
|
||||
updateCandidate = ""
|
||||
|
||||
@@ -78,7 +113,9 @@ export class PluginsComponent implements OnInit {
|
||||
}
|
||||
|
||||
delete(name) {
|
||||
this.plugins.find((el) => el.name === name).status = "Available"
|
||||
let plugin = this.plugins.find((el) => el.name === name)
|
||||
plugin.status = "Available"
|
||||
plugin.installed = ""
|
||||
}
|
||||
|
||||
showConfirmation(name) {
|
||||
@@ -98,6 +135,21 @@ export class PluginsComponent implements OnInit {
|
||||
this.updatepending = !this.updatepending
|
||||
}
|
||||
|
||||
async reload(){
|
||||
this.reloading = true
|
||||
if(isDevMode()){
|
||||
await new Promise((resolve, reject) => {
|
||||
setTimeout(resolve, 1000)
|
||||
})
|
||||
this.plugins = this.plugins.map(plugin => {
|
||||
const parts = plugin.available.split('.')
|
||||
plugin.available = parts[0]+"."+parts[1]+"."+(parseInt(parts[2])+1)
|
||||
return plugin
|
||||
})
|
||||
this.reloading = false
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
|
||||
import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
import { PluginsComponent } from './btc.component';
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'address-viewer', //!!!!
|
||||
template: `
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Addresses
|
||||
</div>
|
||||
<clr-alert *ngFor="let entry of alerts" [clrAlertType]="line.severity">
|
||||
<clr-alert-item>
|
||||
<span class="alert-text">
|
||||
{{line.message}}
|
||||
</span>
|
||||
</clr-alert-item>
|
||||
</clr-alert>
|
||||
|
||||
<clr-datagrid class="datagrid-compact">
|
||||
<clr-dg-column>Address</clr-dg-column>
|
||||
<clr-dg-column>Balance</clr-dg-column>
|
||||
<clr-dg-column>Pending</clr-dg-column>
|
||||
<clr-dg-column>Estimate total</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let line of regularAddresses">
|
||||
<clr-dg-cell style="word-break: break-all">{{line.address}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.confirmed.balance}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.unconfirmed.balance}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.total.balance}}</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded >
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-3">
|
||||
<button class="btn btn-info-outline">Explorer</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="parent.keys[line.address] != null" class="clr-col-9" style="word-break:break-all">
|
||||
<h6 style="margin-top: 0"> Public key </h6>
|
||||
{{parent.keys[line.address].publicExtendedKey}}
|
||||
<h6> Private key </h6>
|
||||
{{parent.keys[line.address].privateExtendedKey}}
|
||||
</div>
|
||||
</div>
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="5">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[5,10,20,50,100]">per page</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
|
||||
<clr-datagrid class="datagrid-compact">
|
||||
<clr-dg-column>Address</clr-dg-column>
|
||||
<clr-dg-column>Balance</clr-dg-column>
|
||||
<clr-dg-column>Pending</clr-dg-column>
|
||||
<clr-dg-column>Estimate total</clr-dg-column>
|
||||
|
||||
<clr-dg-row *clrDgItems="let line of changeAddresses">
|
||||
<clr-dg-cell style="word-break: break-all">{{line.address}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.confirmed.balance}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.unconfirmed.balance}}</clr-dg-cell>
|
||||
<clr-dg-cell>{{line.total.balance}}</clr-dg-cell>
|
||||
<clr-dg-row-detail *clrIfExpanded >
|
||||
<div class="clr-row">
|
||||
<div class="clr-col-3">
|
||||
<button class="btn btn-info-outline">Explorer</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="parent.keys[line.address] != null" class="clr-col-9" style="word-break:break-all">
|
||||
<h6 style="margin-top: 0"> Public key </h6>
|
||||
{{parent.keys[line.address].publicExtendedKey}}
|
||||
<h6> Private key </h6>
|
||||
{{parent.keys[line.address].privateExtendedKey}}
|
||||
</div>
|
||||
</div>
|
||||
</clr-dg-row-detail>
|
||||
</clr-dg-row>
|
||||
|
||||
<clr-dg-footer>
|
||||
<clr-dg-pagination #pagination [clrDgPageSize]="5">
|
||||
<clr-dg-page-size [clrPageSizeOptions]="[5,10,20,50,100]">per page</clr-dg-page-size>
|
||||
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
|
||||
of {{pagination.totalItems}}
|
||||
</clr-dg-pagination>
|
||||
</clr-dg-footer>
|
||||
</clr-datagrid>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class AddressViewerComponent implements OnInit {
|
||||
|
||||
regularAddresses = []
|
||||
changeAddresses = []
|
||||
keys = {}
|
||||
parent:PluginsComponent
|
||||
ngOnInit() { }
|
||||
|
||||
setParent(parent:PluginsComponent){
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
setRegularAddresses(addresses){
|
||||
this.regularAddresses = addresses
|
||||
}
|
||||
|
||||
setChangeAddresses(addresses){
|
||||
this.changeAddresses = addresses
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,508 @@
|
||||
import { Component, OnInit, isDevMode, ViewChild, AfterViewInit } from '@angular/core';
|
||||
import { WalletGeneratorComponent } from './wallet-generator.component';
|
||||
import { WalletPickerComponent } from './wallet-picker.component';
|
||||
import { HDKey } from "btc-hdkey"
|
||||
import { AddressViewerComponent } from './address-viewer.component';
|
||||
const fetch = require("node-fetch")
|
||||
const coinselect = require('coinselect/accumulative')
|
||||
import { TransactionBuilder } from 'bitcoinjs-lib'
|
||||
import { networks } from 'bitcoinjs-lib'
|
||||
let feeRate = 55 // satoshis per byte
|
||||
|
||||
const hdkey = new HDKey()
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'plugins', //!!!!
|
||||
template: `
|
||||
|
||||
<wallet-generator></wallet-generator>
|
||||
|
||||
<!-- row 1 -->
|
||||
<div class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12">
|
||||
|
||||
<!-- wallet picker + send box + balance box -->
|
||||
<div class="clr-row">
|
||||
|
||||
<!-- left column -->
|
||||
<div class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-5 clr-col-xl-5">
|
||||
|
||||
<!-- Wallet picker -->
|
||||
<div class="clr-row">
|
||||
<walletpicker class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12"></walletpicker>
|
||||
</div>
|
||||
<!-- /Wallet picker -->
|
||||
|
||||
<!-- Send box -->
|
||||
<div class="clr-row">
|
||||
|
||||
<div class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Send
|
||||
</div>
|
||||
|
||||
<div class="card-block" >
|
||||
<input [(ngModel)]="sendTo" type="text" placeholder="To address" clrInput />
|
||||
<input [(ngModel)]="sendAmount" type="number" min="0" placeholder="Amount" clrInput />
|
||||
|
||||
</div>
|
||||
<div class="card-footer" >
|
||||
<button class="btn btn-success-outline" (click)="createTx(sendTo, sendAmount)">send</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- /Send Box -->
|
||||
|
||||
</div>
|
||||
<!-- /left column -->
|
||||
|
||||
<!-- right column -->
|
||||
<div class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-7 clr-col-xl-7" >
|
||||
|
||||
<!-- Balance box -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="word-break: break-all">
|
||||
{{rootkey.publicExtendedKey}}
|
||||
</div>
|
||||
<clr-alert *ngFor="let entry of alerts" [clrAlertType]="line.severity">
|
||||
<clr-alert-item>
|
||||
<span class="alert-text">
|
||||
{{line.message}}
|
||||
</span>
|
||||
</clr-alert-item>
|
||||
</clr-alert>
|
||||
|
||||
<div class="card-block" >
|
||||
<h3 style="margin-top:12px">Balance: {{balance.confirmed}}</h3>
|
||||
<p>
|
||||
Unconfirmed: {{balance.unconfirmed}}<br />
|
||||
Estimated total: {{balance.total}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Balance box-->
|
||||
|
||||
</div>
|
||||
<!-- right column -->
|
||||
|
||||
</div>
|
||||
<!-- /row wallet picker + send box + balance box -->
|
||||
|
||||
<div class="clr-col-12 clr-col-sm-12 clr-col-md-12 clr-col-lg-12 clr-col-xl-12" style="padding-left:0px; padding-right: 0px">
|
||||
<address-viewer></address-viewer>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /row 1 -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
`
|
||||
})
|
||||
export class PluginsComponent implements AfterViewInit {
|
||||
|
||||
wallets:{
|
||||
[someLabel in string]:{
|
||||
key:string
|
||||
name:string
|
||||
}
|
||||
} = {}
|
||||
txToSend:any
|
||||
rootkey = {
|
||||
publicExtendedKey: "",
|
||||
privateExtendedKey: ""
|
||||
}
|
||||
keys = {}
|
||||
regaddresses = []
|
||||
changeaddresses = []
|
||||
balance = {
|
||||
confirmed: 0,
|
||||
total: 0,
|
||||
unconfirmed: 0
|
||||
}
|
||||
mode : {
|
||||
net:network
|
||||
} = {
|
||||
net: "testnet"
|
||||
}
|
||||
sendto:string
|
||||
amount:number
|
||||
|
||||
constructor(){
|
||||
window["btc"] = () => this
|
||||
}
|
||||
@ViewChild(AddressViewerComponent, {static: false})
|
||||
AddressViewerComponent:AddressViewerComponent
|
||||
|
||||
@ViewChild(WalletGeneratorComponent, {static: false})
|
||||
WalletGeneratorComponent:WalletGeneratorComponent
|
||||
|
||||
@ViewChild(WalletPickerComponent, {static: false})
|
||||
WalletPickerComponent:WalletPickerComponent
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.WalletGeneratorComponent.setParent(this)
|
||||
this.WalletPickerComponent.setParent(this)
|
||||
this.AddressViewerComponent.setParent(this)
|
||||
|
||||
//tprv8cXm1S7PGS2fHuLjZajuDZZsvxerzv4zZJJAhkLAWZzwz5rE7efHm31xsqpzfoGMHBNC8bkRcsjiMdmkSuNn5C1qNqk7rrk1pCuhk4HfVak
|
||||
//tpubD9Do9r9dQoiLBNNXTEQVcyDzVzAoAFFu8btwzGNTvqoLpa6zk3UswXdq3zeArrakUu8huCtG1L5pTqNYNftanuutf57fn1C74rBzFybH1xe
|
||||
this.loadKey("tprv8cXm1S7PGS2fHuLjZajuDZZsvxerzv4zZJJAhkLAWZzwz5rE7efHm31xsqpzfoGMHBNC8bkRcsjiMdmkSuNn5C1qNqk7rrk1pCuhk4HfVak")
|
||||
}
|
||||
|
||||
addWallet(wallet){
|
||||
/* store somewhere */
|
||||
|
||||
this.wallets[wallet.name] = wallet
|
||||
this.WalletPickerComponent.setWallets(this.wallets)
|
||||
|
||||
}
|
||||
|
||||
showWizard(){
|
||||
this.WalletGeneratorComponent.open()
|
||||
}
|
||||
|
||||
loadKey(key:string){
|
||||
this.wallets = {}
|
||||
this.txToSend = null
|
||||
this.sendto = ""
|
||||
this.regaddresses = []
|
||||
|
||||
const t = hdkey.checkHDKey(key)
|
||||
if(t){
|
||||
this.mode = t
|
||||
}
|
||||
|
||||
this.rootkey = hdkey.HDKeyFromExtendedKey(key)
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
async refresh(){
|
||||
this.keys = {}
|
||||
this.changeaddresses = []
|
||||
this.regaddresses = []
|
||||
this.AddressViewerComponent.setChangeAddresses([])
|
||||
this.AddressViewerComponent.setRegularAddresses([])
|
||||
this.balance.total = 0
|
||||
this.balance.unconfirmed = 0
|
||||
this.balance.confirmed = 0
|
||||
|
||||
let p1 = this.updateChange()
|
||||
let p2 = this.update()
|
||||
|
||||
let balances:any[] = await Promise.all([p1,p2])
|
||||
balances.forEach(balance => {
|
||||
this.balance.confirmed += balance.confirmed
|
||||
this.balance.total += balance.total
|
||||
this.balance.unconfirmed += balance.unconfirmed
|
||||
})
|
||||
|
||||
console.log(this.changeaddresses)
|
||||
|
||||
this.AddressViewerComponent.setChangeAddresses(this.changeaddresses)
|
||||
this.AddressViewerComponent.setRegularAddresses(this.regaddresses)
|
||||
}
|
||||
|
||||
|
||||
async getInfo(rootAddr: string, n = 10, skip = 0, changeAddresses = false): Promise<any>{
|
||||
const t = hdkey.checkHDKey(rootAddr)
|
||||
if(!t){
|
||||
return false
|
||||
}
|
||||
let addresses: string[] = []
|
||||
for(let i = skip; i < skip+n; i++){
|
||||
let addr
|
||||
if(changeAddresses){
|
||||
addr = hdkey.deriveAddress(rootAddr, i, 'm/1/')
|
||||
}else{
|
||||
addr = hdkey.deriveAddress(rootAddr, i)
|
||||
}
|
||||
if(!addr){
|
||||
return false
|
||||
}
|
||||
addresses.push(addr)
|
||||
}
|
||||
let lookupstr = addresses.join(',')
|
||||
try{
|
||||
let r = await fetch(api[t.net]+'/address/'+lookupstr, {
|
||||
'method':'GET',
|
||||
'mode':'cors',
|
||||
'data': {limit: 200}
|
||||
})
|
||||
if(r.status !== 200){
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
let json = (await r).json()
|
||||
return json
|
||||
}catch(e){
|
||||
return {success: false}
|
||||
}
|
||||
}
|
||||
|
||||
async getTxs(address: string, type:network){
|
||||
let r = await fetch(api[type]+'/address/'+address, {
|
||||
'method':'GET',
|
||||
'mode':'cors'
|
||||
})
|
||||
if(r.status !== 200){
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
return (await r).json()
|
||||
}
|
||||
|
||||
async getUnspentTxs(addresses:string[], type:network):Promise<any>{
|
||||
let lookupstr = addresses.join(',')
|
||||
try{
|
||||
let r = await fetch(api[type]+'/address/'+lookupstr+'/unspent', {
|
||||
'method':'GET',
|
||||
'mode':'cors'
|
||||
})
|
||||
if(r.status !== 200){
|
||||
return {success: false}
|
||||
}
|
||||
|
||||
return (await r).json()
|
||||
}catch(e){
|
||||
return {success: false}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getKeyForAddress(keyName){
|
||||
return this.keys[keyName]
|
||||
}
|
||||
|
||||
async update(){
|
||||
let n = 1
|
||||
let regAddresses
|
||||
|
||||
while(true){
|
||||
try{
|
||||
regAddresses = await this.getInfo(this.rootkey.publicExtendedKey, 115*n, 115*(n-1))
|
||||
}catch(e){
|
||||
break
|
||||
}
|
||||
if(regAddresses.success === true){
|
||||
if('addresses' in regAddresses){ // multiple
|
||||
this.regaddresses = [...this.regaddresses, ...regAddresses.addresses]
|
||||
}
|
||||
if('address' in regAddresses){
|
||||
this.regaddresses.push(regAddresses.address)
|
||||
}
|
||||
n++
|
||||
}else{
|
||||
console.log('No more balance found after attempting ', 115*n, 'addresses')
|
||||
if(this.rootkey.privateExtendedKey != null){
|
||||
for(let i = 0; i < 115*n; i++){
|
||||
const keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i)
|
||||
const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net)
|
||||
this.keys[addrN] = keyN
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if(this.regaddresses.length === 0){
|
||||
const addr = hdkey.deriveAddress(this.rootkey.publicExtendedKey, 0)
|
||||
this.regaddresses.push({
|
||||
address: addr,
|
||||
confirmed: {balance: 0},
|
||||
unconfirmed: {balance: 0},
|
||||
total: {balance: 0},
|
||||
})
|
||||
return {
|
||||
total: 0,
|
||||
unconfirmed: 0,
|
||||
confirmed: 0
|
||||
}
|
||||
}
|
||||
const resMap: any = {}
|
||||
this.regaddresses.forEach(a => {
|
||||
resMap[a.address] = a
|
||||
})
|
||||
|
||||
const confirmed = <number>Object.values(resMap).reduce((accum:any, obj:any) => {
|
||||
return accum + parseFloat(obj.confirmed.balance)
|
||||
}, 0)
|
||||
const unconfirmed = <number>Object.values(resMap).reduce((accum:any, obj:any) => {
|
||||
return accum + parseFloat(obj.unconfirmed.balance)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
total: confirmed+unconfirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
confirmed: confirmed
|
||||
}
|
||||
}
|
||||
|
||||
async updateChange(){
|
||||
let n = 1
|
||||
let res
|
||||
|
||||
while(true){
|
||||
try{
|
||||
res = await this.getInfo(this.rootkey.publicExtendedKey, 115*n, 115*(n-1), true)
|
||||
}catch(e){
|
||||
break
|
||||
}
|
||||
if(res.success === true){
|
||||
if('addresses' in res){ // multiple
|
||||
this.changeaddresses = [...this.changeaddresses, ...res.addresses]
|
||||
}
|
||||
if('address' in res){
|
||||
this.changeaddresses.push(res.address)
|
||||
}
|
||||
n++
|
||||
}else{
|
||||
console.log('No more balance found after attempting ', 115*n, 'addresses')
|
||||
if(this.rootkey.privateExtendedKey != null){
|
||||
for(let i = 0; i < 115*n; i++){
|
||||
const keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i, 'm/1/')
|
||||
const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net)
|
||||
this.keys[addrN] = keyN
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if(this.changeaddresses.length === 0){
|
||||
const addr = hdkey.deriveAddress(this.rootkey.publicExtendedKey, 0, 'm/1/')
|
||||
this.changeaddresses.push({
|
||||
address: addr,
|
||||
confirmed: {balance: 0},
|
||||
unconfirmed: {balance: 0},
|
||||
total: {balance: 0},
|
||||
})
|
||||
return {
|
||||
total: 0,
|
||||
unconfirmed: 0,
|
||||
confirmed: 0
|
||||
}
|
||||
}
|
||||
|
||||
const resMap: any = {}
|
||||
this.changeaddresses.forEach(a => {
|
||||
resMap[a.address] = a
|
||||
})
|
||||
|
||||
const confirmed = <number>Object.values(resMap).reduce((accum:any, obj:any) => {
|
||||
return accum + parseFloat(obj.confirmed.balance)
|
||||
}, 0)
|
||||
const unconfirmed = <number>Object.values(resMap).reduce((accum:any, obj:any) => {
|
||||
return accum + parseFloat(obj.unconfirmed.balance)
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
confirmed: confirmed,
|
||||
unconfirmed: unconfirmed,
|
||||
total: unconfirmed + confirmed
|
||||
}
|
||||
}
|
||||
|
||||
async createTx(target:string, value:number){
|
||||
|
||||
console.log(target, value)
|
||||
|
||||
if(target == null || typeof target !== 'string' || target === '' || value == null || value <= 0 ){
|
||||
//this.error = 'Bad inputs. Expected bitcoin address and number'
|
||||
return
|
||||
}
|
||||
|
||||
if(value > this.balance.confirmed){
|
||||
//this.error = 'Insufficient funds'
|
||||
return
|
||||
}
|
||||
|
||||
let addresses: string[] = this.regaddresses.map(a => a.address)
|
||||
addresses = [...addresses, ...this.changeaddresses.map(a => a.address)]
|
||||
const txs = await this.getUnspentTxs(addresses, this.mode.net)
|
||||
|
||||
if(txs == null || txs.success !== true || txs.unspent.length === 0){
|
||||
//this.error = 'No UTXOs found'
|
||||
return
|
||||
}
|
||||
|
||||
const utxos = txs.unspent.map(t => {
|
||||
return {
|
||||
address: t.addresses[0],
|
||||
txId: t.txid,
|
||||
vout: t.n,
|
||||
value: t.value * 100000000
|
||||
}
|
||||
})
|
||||
|
||||
let { inputs, outputs, fee } = coinselect(utxos, [{address: target, value: value*100000000}], feeRate)
|
||||
|
||||
// .inputs and .outputs will be undefined if no solution was found
|
||||
if (!inputs || !outputs){
|
||||
//this.error = 'Could not build a valid transaction'
|
||||
return
|
||||
}
|
||||
|
||||
const txb = new TransactionBuilder(networks[this.mode.net])
|
||||
|
||||
|
||||
txb.setVersion(1)
|
||||
inputs.forEach((input) => {
|
||||
txb.addInput(input.txId, input.vout)
|
||||
})
|
||||
outputs.forEach((output) => {
|
||||
if (!output.address) {
|
||||
output.address = hdkey.deriveAddress(this.rootkey.publicExtendedKey, this.changeaddresses.length, 'm/1/')
|
||||
}
|
||||
txb.addOutput(output.address, output.value)
|
||||
})
|
||||
inputs.forEach((input, i) => {
|
||||
txb.sign(i, this.keys[input.address])
|
||||
})
|
||||
|
||||
this.txToSend = txb.build()
|
||||
|
||||
this.sendTx()
|
||||
}
|
||||
|
||||
async sendTx(){
|
||||
if(this.txToSend == null){
|
||||
//this.error = 'No transaction to send!'
|
||||
return
|
||||
}
|
||||
|
||||
let url
|
||||
if(this.mode.net === 'mainnet'){
|
||||
url = 'https://api.smartbit.com.au/v1/blockchain/pushtx'
|
||||
}else{
|
||||
url = 'https://testnet-api.smartbit.com.au/v1/blockchain/pushtx'
|
||||
}
|
||||
let r = await fetch(url, {
|
||||
'method':'POST',
|
||||
'body': JSON.stringify({'hex': this.txToSend.toHex()})
|
||||
})
|
||||
this.txToSend = null
|
||||
if(r.status === 200){
|
||||
console.log(r)
|
||||
this.refresh()
|
||||
}else{
|
||||
//this.error = 'The API '+url+' returned a non-200 error code '+r.status+' '+r.statusText
|
||||
console.log(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
type network = 'testnet' | 'mainnet'
|
||||
type keyType = 'public' | 'private'
|
||||
|
||||
type bip32Keypair = { publicExtendedKey: string, privateExtendedKey: string }
|
||||
|
||||
const api = {
|
||||
testnet: 'https://testnet-api.smartbit.com.au/v1/blockchain',
|
||||
mainnet: 'https://api.smartbit.com.au/v1/blockchain'
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { Component, OnInit, isDevMode, ViewChild } from '@angular/core';
|
||||
import { ClrWizard } from '@clr/angular';
|
||||
import { HDKey } from "btc-hdkey"
|
||||
import { PluginsComponent } from './btc.component';
|
||||
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'wallet-generator', //!!!!
|
||||
template: `
|
||||
<clr-wizard #wizard [(clrWizardOpen)]="mdOpen" [clrWizardForceForwardNavigation]="true" clrWizardSize="lg">
|
||||
<clr-wizard-title>Bitcoin wallet generator and importer</clr-wizard-title>
|
||||
|
||||
<clr-wizard-button [type]="'cancel'">Cancel</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'previous'">Back</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'next'">Next</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'finish'">Finish</clr-wizard-button>
|
||||
|
||||
<clr-wizard-page (clrWizardPageCustomButton)="doCustomClick($event)">
|
||||
<ng-template clrPageTitle>Menemonic/BIP39 (optional)</ng-template>
|
||||
<ng-template clrPageNavTitle>
|
||||
Menemonic/BIP39
|
||||
</ng-template>
|
||||
<p>
|
||||
A menemonic is a collection of words that is supposed to be easier to remember than a complex seed. It may be used to reconstruct the private key.
|
||||
</p>
|
||||
<p>
|
||||
Enter your menemonic below or generate a new one.
|
||||
</p>
|
||||
<textarea [(ngModel)]="menemonic" class="clr-textarea" style="margin-top: 24px; width: 100%; height: 200px">{{menemonic}}</textarea>
|
||||
<div class="clr-row">
|
||||
<clr-checkbox-wrapper class="clr-col">
|
||||
<input type="checkbox" clrCheckbox value="testnet" name="testnet1" [(ngModel)]="testnet" />
|
||||
<label>testnet</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<button class="btn btn-outline-success clr-col" (click)="generateMenemonic()">{{"Generate"}}</button>
|
||||
</div>
|
||||
<ng-template clrPageButtons>
|
||||
<clr-wizard-button [type]="'cancel'">Cancel</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'menemonic-next'">{{menemonic.length==0?"Skip":"Next"}}</clr-wizard-button>
|
||||
</ng-template>
|
||||
</clr-wizard-page>
|
||||
|
||||
<clr-wizard-page (clrWizardPageCustomButton)="doCustomClick($event)">
|
||||
<ng-template clrPageTitle>Seed phrase (optional)</ng-template>
|
||||
<ng-template clrPageNavTitle>
|
||||
Seed phrase
|
||||
</ng-template>
|
||||
<p>
|
||||
If you entered a menemonic in the previous step, a seed was generated from it.
|
||||
</p>
|
||||
<p>
|
||||
If you would like to use your own seed enter it below. Use something sufficiently hard to guess.
|
||||
</p>
|
||||
<textarea class="clr-textarea" (ngModelChange)="updateSeed()" [(ngModel)]="seed" style="margin-top: 24px; width: 100%; height: 200px">{{seed}}</textarea>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox value="testnet" name="testnet2" [(ngModel)]="testnet" />
|
||||
<label>testnet</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<ng-template clrPageButtons>
|
||||
<clr-wizard-button [type]="'cancel'">Cancel</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'back'">Back</clr-wizard-button>
|
||||
<clr-wizard-button [type]="'seed-next'">{{seed.length==0?"Skip":"Next"}}</clr-wizard-button>
|
||||
</ng-template>
|
||||
</clr-wizard-page>
|
||||
|
||||
<clr-wizard-page [clrWizardPageNextDisabled]="keyInvalid" (clrWizardPageCustomButton)="doCustomClick($event)">
|
||||
<ng-template clrPageTitle>Extended Key/BIP32</ng-template>
|
||||
<p>
|
||||
Accepts keys in the xpub and xprv (BIP32) format
|
||||
</p>
|
||||
<textarea [(ngModel)]="key" (ngModelChange)="updateKey()"class="clr-textarea" style="margin-top: 24px; width: 100%; height: 200px"></textarea>
|
||||
</clr-wizard-page>
|
||||
|
||||
<clr-wizard-page class="break-word" (clrWizardPageCustomButton)="doCustomClick($event)">
|
||||
<ng-template clrPageTitle>Confirm</ng-template>
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
Do not give any of this information to other people! Even your public key holds the power to correlate all your addresses back to you!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="keypair.privateExtendedKey != null && keypair.privateExtendedKey.length != 0" class="alert alert-warning" role="alert">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
WARNING: A private key is not required for operating frontblock
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="keypair.privateExtendedKey != null && keypair.privateExtendedKey.length != 0" class="alert alert-info" role="alert">
|
||||
<div class="alert-items">
|
||||
<div class="alert-item static">
|
||||
<div class="alert-icon-wrapper">
|
||||
<clr-icon class="alert-icon" shape="exclamation-circle"></clr-icon>
|
||||
</div>
|
||||
<span class="alert-text">
|
||||
HINT: You can exclusively store the public key by going back one step and importing the xpub you just created
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="left">Type</th>
|
||||
<th class="left">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngIf="menemonic.length != 0">
|
||||
<td class="left"> menemonic</td>
|
||||
<td class="left">{{menemonic}}</td>
|
||||
</tr>
|
||||
<tr *ngIf="seed.length != 0">
|
||||
<td class="left"> seed</td>
|
||||
<textarea rows="3" class="clr-textarea left" [ngModel]="seed" style="width: 100%"></textarea>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left"> xpub</td>
|
||||
<textarea rows="3" class="clr-textarea left" [ngModel]="keypair.publicExtendedKey" style="width: 100%"></textarea>
|
||||
</tr>
|
||||
<tr *ngIf="keypair.privateExtendedKey != null && keypair.privateExtendedKey.length != 0">
|
||||
<td class="left"> xprv</td>
|
||||
<textarea rows="3" class="clr-textarea left" [ngModel]="keypair.privateExtendedKey" style="width: 100%"></textarea>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<clr-checkbox-wrapper>
|
||||
<input type="checkbox" clrCheckbox value="option1" name="options" />
|
||||
<label>I understand this is important</label>
|
||||
</clr-checkbox-wrapper>
|
||||
<ng-template clrPageButtons>
|
||||
<clr-wizard-button [type]="'cancel'">Cancel</clr-wizard-button>
|
||||
<clr-wizard-button class="btn btn-success" [type]="'myfinish'">Finish</clr-wizard-button>
|
||||
</ng-template>
|
||||
</clr-wizard-page>
|
||||
</clr-wizard>
|
||||
|
||||
`
|
||||
})
|
||||
export class WalletGeneratorComponent implements OnInit {
|
||||
@ViewChild("wizard", {static: false}) wizard: ClrWizard;
|
||||
key:string = ""
|
||||
keyInvalid = true
|
||||
accept = false
|
||||
seed:string = ""
|
||||
mdOpen: boolean = false
|
||||
checkKey: boolean = true
|
||||
menemonic:string = ""
|
||||
keypair: any = {}
|
||||
testnet: boolean = false
|
||||
profileName
|
||||
private hdKey = new HDKey()
|
||||
private parent:PluginsComponent
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
open(){
|
||||
this.key = ""
|
||||
this.keyInvalid = true
|
||||
this.seed = ""
|
||||
this.checkKey = true
|
||||
this.menemonic = ""
|
||||
this.keypair = {}
|
||||
this.testnet = false
|
||||
this.wizard.reset()
|
||||
this.mdOpen = true
|
||||
}
|
||||
|
||||
setParent(parent:PluginsComponent){
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
async generateMenemonic(){
|
||||
this.menemonic = this.hdKey.randomMenemonic()
|
||||
}
|
||||
|
||||
async seedFromMenemonic(){
|
||||
try{
|
||||
this.seed = this.hdKey.seedFromMenemonic(this.menemonic)
|
||||
}catch(e){
|
||||
/* invalid menemonic */
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
async accountFromSeed(){
|
||||
this.keypair = this.hdKey.deriveAccount(this.seed, this.testnet?"testnet":"mainnet", 0)
|
||||
console.log(this.keypair)
|
||||
this.key = this.keypair.privateExtendedKey != null?this.keypair.privateExtendedKey:this.keypair.publicExtendedKey
|
||||
this.keyInvalid = false
|
||||
}
|
||||
|
||||
async importKey(){
|
||||
this.hdKey.HDKeyFromExtendedKey(this.key)
|
||||
}
|
||||
|
||||
checkBip32(){
|
||||
return this.hdKey.checkHDKey(this.keypair.privateExtendedKey != null?this.keypair.privateExtendedKey:this.keypair.publicExtendedKey)
|
||||
}
|
||||
|
||||
finish(){
|
||||
this.parent.addWallet({
|
||||
name: this.key.substr(4,15),
|
||||
key: this.key
|
||||
})
|
||||
}
|
||||
|
||||
public doCustomClick(buttonType: string): void {
|
||||
if ("menemonic-next" === buttonType) {
|
||||
if(this.menemonic.length != 0){
|
||||
this.seedFromMenemonic()
|
||||
}
|
||||
this.wizard.next();
|
||||
}
|
||||
|
||||
if ("myfinish" === buttonType) {
|
||||
this.finish()
|
||||
this.wizard.close()
|
||||
}
|
||||
|
||||
if ("seed-next" === buttonType) {
|
||||
if(this.seed.length != 0){
|
||||
this.accountFromSeed()
|
||||
}
|
||||
this.wizard.next();
|
||||
}
|
||||
|
||||
if ("back" === buttonType) {
|
||||
this.wizard.previous();
|
||||
}
|
||||
}
|
||||
|
||||
updateSeed(){
|
||||
this.menemonic = ""
|
||||
}
|
||||
|
||||
updateKey(){
|
||||
this.keyInvalid = true
|
||||
this.menemonic = ""
|
||||
this.seed = ""
|
||||
let key
|
||||
try{
|
||||
key = this.hdKey.HDKeyFromExtendedKey(this.key)
|
||||
}catch(e){
|
||||
/* invalid key */
|
||||
console.log("bad key")
|
||||
}
|
||||
if(key){
|
||||
this.keypair = key
|
||||
this.keyInvalid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
|
||||
import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
import { PluginsComponent } from './btc.component';
|
||||
declare const fb
|
||||
@Component({
|
||||
selector: 'walletpicker', //!!!!
|
||||
|
||||
template: `
|
||||
<div class="card" style="overflow:hidden">
|
||||
<div class="card-header">
|
||||
Wallets
|
||||
</div>
|
||||
<clr-alert *ngFor="let entry of alerts" [clrAlertType]="line.severity">
|
||||
<clr-alert-item>
|
||||
<span class="alert-text">
|
||||
{{line.message}}
|
||||
</span>
|
||||
</clr-alert-item>
|
||||
</clr-alert>
|
||||
|
||||
<clr-tree-node style="margin-top:12px; margin-bottom:12px">
|
||||
<a style="cursor:pointer" (click)="parent.showWizard()"><clr-icon shape="download" class="is-solid"></clr-icon> New / Import</a>
|
||||
</clr-tree-node>
|
||||
<clr-tree-node *ngFor="let line of wallets">
|
||||
<a style="cursor:pointer" (click)="parent.loadKey(line.key)">
|
||||
<clr-icon *ngIf="line.key.startsWith('xpub') || line.key.startsWith('tpub')" shape="lock" class="is-solid"></clr-icon>
|
||||
<clr-icon *ngIf="line.key.startsWith('xprv') || line.key.startsWith('tprv')" shape="key" class="is-solid"></clr-icon>
|
||||
{{line.name}}
|
||||
</a>
|
||||
</clr-tree-node>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class WalletPickerComponent implements OnInit {
|
||||
private parent:PluginsComponent
|
||||
wallets = []
|
||||
|
||||
setWallets(wallets){
|
||||
this.wallets = Object.values(wallets)
|
||||
}
|
||||
|
||||
setParent(parent:PluginsComponent){
|
||||
this.parent = parent
|
||||
}
|
||||
|
||||
ngOnInit() { }
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { PluginsComponent } from './btc/btc.component';
|
||||
import { WalletPickerComponent } from './btc/wallet-picker.component';
|
||||
import { AddressViewerComponent } from './btc/address-viewer.component';
|
||||
|
||||
import { FrontendPlugin, SidebarEntries } from 'frontblock-generic/Plugin';
|
||||
import { ClarityModule } from '@clr/angular';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { WalletGeneratorComponent } from './btc/wallet-generator.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
ClarityModule,
|
||||
|
||||
CommonModule,
|
||||
RouterModule.forChild([
|
||||
{path: "btc", component: PluginsComponent},
|
||||
]),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
declarations: [
|
||||
PluginsComponent,
|
||||
WalletPickerComponent,
|
||||
AddressViewerComponent,
|
||||
WalletGeneratorComponent
|
||||
]
|
||||
})
|
||||
export class PluginModule implements FrontendPlugin{
|
||||
getSidebarEntry(): SidebarEntries {
|
||||
return {
|
||||
icon: "wallet",
|
||||
text: "Wallets",
|
||||
parentRoute: "wallet",
|
||||
links: [{
|
||||
route: "btc",
|
||||
text: "BTC Wallet"
|
||||
},{
|
||||
route: "ltc",
|
||||
text: "LTC Wallet"
|
||||
},{
|
||||
route: "eth",
|
||||
text: "ETH Wallet"
|
||||
},{
|
||||
route: "xrp",
|
||||
text: "XRP Wallet"
|
||||
},{
|
||||
route: "xlm",
|
||||
text: "XLM Wallet"
|
||||
},{
|
||||
route: "settings",
|
||||
text: "Settings"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
.heightmax{
|
||||
height: 100%
|
||||
}
|
||||
.break-word {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
font-size: 90%
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Component, ChangeDetectorRef, NgZone, ViewChild, OnInit, AfterContentInit } from '@angular/core';
|
||||
import { Component, ChangeDetectorRef, NgZone, ViewChild, OnInit, AfterContentInit, isDevMode } from '@angular/core';
|
||||
import { SidebarComponent } from './sidebar/sidebar.component';
|
||||
import { SidebarEntryService } from './sidebar-entry-service.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
@@ -15,6 +14,9 @@ export class AppComponent implements AfterContentInit {
|
||||
|
||||
constructor(private zone:NgZone, private sidebarService: SidebarEntryService){
|
||||
window["refresh"] = setInterval(() => zone.run(()=>{ /*this triggers an angular reload*/}), 200)
|
||||
if(isDevMode()){
|
||||
require("../assets/FrontblockLib")
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentInit(){
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<div class="content-area clr-col-12 clr-col-sm-12 clr-col-lg-12 clr-col-xl-12">
|
||||
<div style="overflow:hidden" class="content-area clr-col-12 clr-col-sm-12 clr-col-lg-12 clr-col-xl-12">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
|
||||
@@ -39,12 +39,11 @@ import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||
import { SidebarComponent } from '../sidebar/sidebar.component';
|
||||
import { Router } from '@angular/router';
|
||||
import { FrontendPlugin, SidebarEntries, SidebarEntry } from "frontblock-generic/Plugin"
|
||||
import { SidebarEntryService } from '../sidebar-entry-service.service';
|
||||
import { environment } from "../../environments/environment"
|
||||
|
||||
const fb = environment.production ? window["fb"] : {
|
||||
PluginManager: {
|
||||
getLoadedPluginNames: () => ["ApiClient", "PluginManager"]
|
||||
getLoadedPluginNames: () => ["ApiClient", "PluginManager", "Wallet"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Component, OnInit, isDevMode } from '@angular/core';
|
||||
styleUrls: ['./header-bar.component.scss']
|
||||
})
|
||||
export class HeaderBarComponent implements OnInit {
|
||||
devmode = isDevMode()?"{ DEV }":""
|
||||
devmode = isDevMode()?"{ ng-DEV }":""
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
./FrontblockLib.js
|
||||
File diff suppressed because one or more lines are too long
@@ -61,3 +61,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
*/
|
||||
// Add global to window, assigning the value of window itself.
|
||||
(window as any).global = window;
|
||||
global.Buffer = global.Buffer || require('buffer').Buffer;
|
||||
global.process = global.process || require('process')
|
||||
|
||||
Reference in New Issue
Block a user