diff --git a/src/frontend/dashboard/src/app/Wallet/btc/address-viewer.component.ts b/src/frontend/dashboard/src/app/Wallet/btc/address-viewer.component.ts index cd96b87..3e9eef2 100644 --- a/src/frontend/dashboard/src/app/Wallet/btc/address-viewer.component.ts +++ b/src/frontend/dashboard/src/app/Wallet/btc/address-viewer.component.ts @@ -1,6 +1,6 @@ -import { Component, OnInit, isDevMode } from '@angular/core'; +import { Component, OnInit, isDevMode, OnDestroy } from '@angular/core'; import { PluginsComponent } from './btc.component'; import { ClrDatagridStringFilterInterface, ClrDatagridNumericFilterInterface, ClrDropdownModule } from '@clr/angular'; declare const fb @@ -36,13 +36,13 @@ declare const fb {{line.address}} - {{line.confirmed.balance | number: '1.8-18'}} - + {{line.confirmed.balance | number: '1.8-18'}} + {{line.unconfirmed.balance | number: '1.8-18'}} - {{line.total.balance | number: '1.8-18'}} + {{line.total.balance | number: '1.8-18'}}
@@ -101,13 +101,13 @@ declare const fb {{line.address}} - {{line.confirmed.balance | number: '1.8-18'}} - + {{line.confirmed.balance | number: '1.8-18'}} + {{line.unconfirmed.balance | number: '1.8-18'}} - {{line.total.balance | number: '1.8-18'}} + {{line.total.balance | number: '1.8-18'}}
@@ -141,7 +141,8 @@ declare const fb
` }) -export class AddressViewerComponent implements OnInit{ +export class AddressViewerComponent implements OnInit, OnDestroy{ + balanceFilter = new BalanceFilter() otherBalanceFilter = new BalanceFilter() regularAddresses = [] @@ -160,7 +161,11 @@ export class AddressViewerComponent implements OnInit{ setChangeAddresses(addresses){ this.changeAddresses = addresses - } + } + + ngOnDestroy(): void { + + } } class BalanceFilter implements ClrDatagridNumericFilterInterface{ diff --git a/src/frontend/dashboard/src/app/Wallet/btc/btc.component.ts b/src/frontend/dashboard/src/app/Wallet/btc/btc.component.ts index dd89fcb..054e228 100644 --- a/src/frontend/dashboard/src/app/Wallet/btc/btc.component.ts +++ b/src/frontend/dashboard/src/app/Wallet/btc/btc.component.ts @@ -1,12 +1,13 @@ -import { Component, OnInit, isDevMode, ViewChild, AfterViewInit } from '@angular/core'; +import { Component, OnInit, isDevMode, ViewChild, AfterViewInit, OnDestroy } 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') +const coinselect = require('coinselect') import { TransactionBuilder, Signer } from 'bitcoinjs-lib' import { networks } from 'bitcoinjs-lib' +import { SubscriptionResponse, ErrorResponse } from 'frontblock-generic/Types'; let feeRate = 55 // satoshis per byte const hdkey = new HDKey() @@ -31,7 +32,7 @@ declare const fb - +
@@ -51,6 +52,7 @@ declare const fb
+
@@ -136,10 +138,15 @@ declare const fb Private key available

- - @@ -149,79 +156,128 @@ declare const fb
-
- + + + +
+
+
+
+ +
+ Live Transactions +
+
+ + + + + + + + + + + + + + + + + + + +
HashAmountStatusToFrom
{{line.hash}}{{line.amount}}{{line.status}}{{line.to}}{{line.from.join(', ')}}
+
+
+
+
+ + + +
+ +
+
` }) -export class PluginsComponent implements AfterViewInit { - errors:string[] = [] - successes:string[] = [] - wallets:WalletStore = {} - txToSend:any - rootkey:bip32Keypair = {publicExtendedKey: "",privateExtendedKey: ""} - keys:KeyStore = {} - regaddresses:AddressLine[] = [] - changeaddresses:AddressLine[] = [] +export class PluginsComponent implements AfterViewInit, OnDestroy { + timerRefresh: NodeJS.Timeout + errors: string[] = [] + successes: string[] = [] + wallets: WalletStore = {} + rootkey: bip32Keypair = { publicExtendedKey: "", privateExtendedKey: "" } + keys: KeyStore = {} + regaddresses: AddressLine[] = [] + changeaddresses: AddressLine[] = [] balanceDisplay = { confirmed: 0, total: 0, unconfirmed: 0 } - mode : { - type:keyType, - net:networkType + mode: { + type: keyType, + net: networkType } = { - net: "testnet", - type: "public" - } - sendTo:string - sendAmount:number + net: "testnet", + type: "public" + } + sendTo: string + sendAmount: number loading = false loadingN = 1 + autorefresh = false + refreshing = false + apiClientPlugin = false + subscriptions = [] + transactions = [] - @ViewChild(AddressViewerComponent, {static: false}) - AddressViewerComponent:AddressViewerComponent - - @ViewChild(WalletGeneratorComponent, {static: false}) - WalletGeneratorComponent:WalletGeneratorComponent - @ViewChild(WalletPickerComponent, {static: false}) - WalletPickerComponent:WalletPickerComponent + @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) - + this.apiClientPlugin = fb.ApiClient != null window['btc'] = this + this.timerRefresh = setInterval(async () => { if (this.autorefresh && !this.refreshing && !this.loading) this.refresh() }, 30000) this.loadKey("tprv8cXm1S7PGS2fHuLjZajuDZZsvxerzv4zZJJAhkLAWZzwz5rE7efHm31xsqpzfoGMHBNC8bkRcsjiMdmkSuNn5C1qNqk7rrk1pCuhk4HfVak") } - addWallet(wallet){ + ngOnDestroy(): void { + clearInterval(this.timerRefresh) + } + + addWallet(wallet) { /* store somewhere */ this.wallets[wallet.name] = wallet this.WalletPickerComponent.setWallets(this.wallets) - + } - showWizard(){ + showWizard() { this.WalletGeneratorComponent.open() } - loadKey(key:string){ + loadKey(key: string) { this.wallets = {} - this.txToSend = null - this.sendTo = "" this.regaddresses = [] const t = hdkey.checkHDKey(key) - if(t){ - console.log(t) + if (t) { this.mode = t } @@ -229,42 +285,65 @@ export class PluginsComponent implements AfterViewInit { this.reload() } - async reload():Promise{ + async reload(): Promise { + if (this.loading || this.refreshing) + return this.loading = true this.loadingN = 1 this.keys = {} - this.balanceDisplay.total = 0 - this.balanceDisplay.unconfirmed = 0 - this.balanceDisplay.confirmed = 0 + await this.refresh() + if (fb.ApiClient) { + const res: (SubscriptionResponse | ErrorResponse)[] = await Promise.all([...this.regaddresses, ...this.changeaddresses].map(async a => await fb.ApiClient.subsume("BTC", { address: a.address }, tx => { console.log(tx); this.transactions.unshift(tx) }))) + this.subscriptions.push(res.map(r => { + if (r.result = "Success") { + return r.message + } else { + return + } + })) + } this.loading = false } - async refresh():Promise{ + async refresh(): Promise { + if (this.refreshing) + return + + this.refreshing = true let p1 = this.updateChange() let p2 = this.updateBalance() - let balances:GrandTotal[] = await Promise.all([p1,p2]) - balances.forEach(balance => { - this.balanceDisplay.confirmed += balance.confirmed - this.balanceDisplay.total += balance.total - this.balanceDisplay.unconfirmed += balance.unconfirmed - }) + let balances: GrandTotal[] = await Promise.all([p1, p2]) + + this.balanceDisplay = balances.reduce((a, o) => { + a.confirmed += o.confirmed + a.total += o.total + a.unconfirmed += o.unconfirmed + + return a + }, { + confirmed: 0, + unconfirmed: 0, + total: 0 + }) this.AddressViewerComponent.setChangeAddresses(this.changeaddresses) this.AddressViewerComponent.setRegularAddresses(this.regaddresses) + this.refreshing = false + } - getAddresses(rootAddr: string, n = 10, skip = 0, changeAddresses = false): string[] | null{ + getAddresses(rootAddr: string, n = 10, skip = 0, changeAddresses = false): string[] | null { let addresses: string[] = [] - for(let i = skip; i < skip+n; i++){ + for (let i = skip; i < skip + n; i++) { let addr - if(changeAddresses){ + if (changeAddresses) { addr = hdkey.deriveAddress(rootAddr, i, 'm/1/') - }else{ + } else { addr = hdkey.deriveAddress(rootAddr, i) } - if(!addr){ + if (!addr) { return } addresses.push(addr) @@ -272,117 +351,133 @@ export class PluginsComponent implements AfterViewInit { return addresses } - async fetchAddresses(addresses:string[], network:networkType): Promise<{success:boolean, address?:any, addresses?:any}>{ + async fetchAddresses(addresses: string[], network: networkType): Promise<{ success: boolean, address?: any, addresses?: any }> { let lookupstr = addresses.join(',') - try{ - let r = await fetch(api[network]+'/address/'+lookupstr, { - 'method':'GET', - 'mode':'cors', - 'data': {limit: 200} + try { + let r = await fetch(api[network] + '/address/' + lookupstr, { + 'method': 'GET', + 'mode': 'cors', + 'data': { limit: 200 } }) - if(r.status !== 200){ - return {success: false} + if (r.status !== 200) { + return { success: false } } let json = (await r).json() return json - }catch(e){ - return {success: false} + } catch (e) { + return { success: false } } } - async getAddressInfos(rootAddr: string, n = 10, skip = 0, changeAddresses = false): Promise{ + async getAddressInfos(rootAddr: string, n = 10, skip = 0, changeAddresses = false): Promise { const t = hdkey.checkHDKey(rootAddr) - if(!t){ + if (!t) { return false } const addresses = this.getAddresses(rootAddr, n, skip, changeAddresses) return await this.fetchAddresses(addresses, t.net) - + } - async getTxs(address: string, type:networkType){ - try{ - let r = await fetch(api[type]+'/address/'+address, { - 'method':'GET', - 'mode':'cors' - }) - if(r.status !== 200){ - return {success: false} + /* + async getTxs(address: string, type:networkType){ + try{ + let r = await fetch(api[type]+'/address/'+address, { + 'method':'GET', + 'mode':'cors' + }) + if(r.status !== 200){ + return {success: false} + } + + return (await r).json() + }catch(e){ + console.warn ("getTxs", e) } - - return (await r).json() - }catch(e){ - console.warn ("getTxs", e) } - } - - async getUnspentTxs(addresses:string[], type:networkType):Promise{ + */ + async getUnspentTxs(addresses: string[], type: networkType): Promise { 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} - } + let url = api[type] + '/address/' + lookupstr + '/unspent' + let txs:getUnspentTxsApiResponse['unspent'] = [] + while (true) { + try { - return (await r).json() - }catch(e){ - console.warn ("getUnspentTxs", e) - return {success: false} + let resp = await fetch(url, { + 'method': 'GET', + 'mode': 'cors' + }) + if (resp.status !== 200) { + return { success: false } + } + const json: getUnspentTxsApiResponse = await resp.json() + if (!json.success) + break; + + txs = [...txs, ...(json.unspent?json.unspent:[])] + if(!json.paging.next_link) + break; + + url = json.paging.next_link + } catch (e) { + console.warn("getUnspentTxs", e) + return { success: false } + } + } + return { + success: true, + unspent: txs } } - getKeyForAddress(keyName){ + getKeyForAddress(keyName) { return this.keys[keyName] } - calculateTotal(addresses:AddressLine[]):GrandTotal{ - const resMap: any = addresses.reduce((o1, o2)=>{o1[o2.address] = o2; return o1}, {}) - const total: any = Object.values(resMap).reduce((accum:GrandTotal, obj:AddressLine) => { + calculateTotal(addresses: AddressLine[]): GrandTotal { + const resMap: any = addresses.reduce((o1, o2) => { o1[o2.address] = o2; return o1 }, {}) + const total: any = Object.values(resMap).reduce((accum: GrandTotal, obj: AddressLine) => { accum.confirmed += parseFloat(obj.confirmed.balance) accum.unconfirmed += parseFloat(obj.unconfirmed.balance) accum.total += parseFloat(obj.confirmed.balance) + parseFloat(obj.unconfirmed.balance) return accum - },{ - confirmed: 0, - unconfirmed: 0, - total: 0 - }) - + }, { + confirmed: 0, + unconfirmed: 0, + total: 0 + }) + return total } - async updateBalance():Promise{ + async updateBalance(): Promise { let n = 1 let res: { success: boolean; addresses: any; address: any; } this.regaddresses = [] - while(true){ + while (true) { this.loadingN = n - try{ - res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115*n, 115*(n-1)) - }catch(e){ + try { + res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115 * n, 115 * (n - 1)) + } catch (e) { break } - if(res.success === true){ - if('addresses' in res){ // multiple + if (res.success === true) { + if ('addresses' in res) { // multiple this.regaddresses = [...this.regaddresses, ...res.addresses] } - if('address' in res){ + if ('address' in res) { this.regaddresses.push(res.address) } n++ - }else{ + } else { console.warn(`This error is a result of a failed fetch request. These are always shown by the browser and cannot be hidden (a design decision) They are completely safe and mean no more addresses were found`) - console.log('No more balance found after attempting ', 115*n, 'addresses') - if(this.mode.type == "private"){ - for(let i = 0; i < 115*n+1; i++){ + console.log('No more balance found after attempting ', 115 * n, 'addresses') + if (this.mode.type == "private") { + for (let i = 0; i < 115 * n + 1; i++) { const keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i) const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net) keyN.derivation = i @@ -397,40 +492,40 @@ They are completely safe and mean no more addresses were found`) const extraAddr = hdkey.deriveAddress(this.rootkey.publicExtendedKey, this.regaddresses.length) this.regaddresses.push({ address: extraAddr, - confirmed: {balance: 0}, - unconfirmed: {balance: 0}, - total: {balance: 0}, + confirmed: { balance: 0 }, + unconfirmed: { balance: 0 }, + total: { balance: 0 }, }) - - return this.calculateTotal(this.regaddresses) + + return this.calculateTotal(this.regaddresses) } - async updateChange():Promise{ + async updateChange(): Promise { let n = 1 this.changeaddresses = [] - while(true){ + while (true) { let res - try{ - res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115*n, 115*(n-1), true) - }catch(e){ + try { + res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115 * n, 115 * (n - 1), true) + } catch (e) { break } - if(res.success === true){ - if('addresses' in res){ // multiple + if (res.success === true) { + if ('addresses' in res) { // multiple this.changeaddresses = [...this.changeaddresses, ...res.addresses] } - if('address' in res){ + if ('address' in res) { this.changeaddresses.push(res.address) } n++ - }else{ + } else { console.warn(`This error is a result of a failed fetch request. These are always shown by the browser and cannot be hidden (a design decision) They are completely safe and mean no more addresses were found`) - console.log('No more change balance found after attempting ', 115*n, 'addresses') - if(this.mode.type == "private"){ + console.log('No more change balance found after attempting ', 115 * n, 'addresses') + if (this.mode.type == "private") { //make keypairs for addresses - for(let i = 0; i < 115*n; i++){ + 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) keyN.derivation = i @@ -441,17 +536,16 @@ They are completely safe and mean no more addresses were found`) } } - return this.calculateTotal(this.changeaddresses) + return this.calculateTotal(this.changeaddresses) } - async createAndSendTx(target:string, value:number){ - - if(target == null || typeof target !== 'string' || target === '' || value == null || value <= 0 ){ + async createAndSendTx(target: string, value: number) { + if (target == null || typeof target !== 'string' || target === '' || value == null || value <= 0) { this.errors.push('Bad inputs. Expected bitcoin address and number') return } - if(value > this.balanceDisplay.confirmed){ + if (value > this.balanceDisplay.confirmed) { this.errors.push('Insufficient funds') return } @@ -459,7 +553,7 @@ They are completely safe and mean no more addresses were found`) let addresses: string[] = [...this.regaddresses.map(a => a.address), ...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){ + if (txs == null || txs.success !== true || txs.unspent.length === 0) { this.errors.push('No UTXOs found') return } @@ -469,14 +563,13 @@ They are completely safe and mean no more addresses were found`) address: t.addresses[0], txId: t.txid, vout: t.n, - value: t.value * 100000000 + value: parseFloat(t.value) * 100000000 } }) - - let { inputs, outputs, fee } = coinselect(utxos, [{address: target, value: value*100000000}], feeRate) + let { inputs, outputs, fee } = coinselect(utxos, [{ address: target, value: parseFloat(value) * 100000000 }], feeRate) // .inputs and .outputs will be undefined if no solution was found - if (!inputs || !outputs){ + if (!inputs || !outputs) { this.errors.push('Could not build a valid transaction') return } @@ -490,68 +583,67 @@ They are completely safe and mean no more addresses were found`) }) outputs.forEach((output) => { if (!output.address) { - output.address = hdkey.deriveAddress(this.rootkey.publicExtendedKey, this.changeaddresses.length, 'm/1/') + 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]) + txb.sign(i, this.keys[input.address]) }) - this.txToSend = txb.build() - - this.sendTx() + await this.sendTx(txb.build()) } - private async sendTx(){ - if(this.txToSend == null){ + private async sendTx(tx) { + if (tx == null) { this.errors.push('No transaction to send!') return } - let url = api[this.mode.net]+"/pushtx" + let url = api[this.mode.net] + "/pushtx" let r - try{ + try { r = await fetch(url, { - 'method':'POST', - 'body': JSON.stringify({'hex': this.txToSend.toHex()}) + 'method': 'POST', + 'body': JSON.stringify({ 'hex': tx.toHex() }) }) - }catch(e){ - console.warn("sendTx", e) - return + } catch (e) { + console.warn("sendTx", e) + return } - this.txToSend = null - if(r.status === 200){ + if (r.status === 200) { this.errors = [] - this.successes.push("Sent "+this.sendAmount+" to "+this.sendTo) - this.reload() - }else{ - this.errors.push('The API '+url+' returned a non-200 error code '+r.status+' '+r.statusText) + this.successes.push("Sent " + this.sendAmount + " to " + this.sendTo) + this.sendTo = "" + this.sendAmount = 0 + //this.reload() + } else { + this.errors.push('The API ' + url + ' returned a non-200 error code ' + r.status + ' ' + r.statusText) } } - addrExplorer(address){ - const url = this.mode.net === 'testnet'?'https://live.blockcypher.com/btc-testnet/address/':'https://live.blockcypher.com/btc/address/' - window.open(url+address, '_blank') + addrExplorer(address) { + const url = this.mode.net === 'testnet' ? 'https://live.blockcypher.com/btc-testnet/address/' : 'https://live.blockcypher.com/btc/address/' + window.open(url + address, '_blank') } } -export type KeyStore = {[keyname in string]: bip32Keypair & Signer} +export type KeyStore = { [keyname in string]: bip32Keypair & Signer } export type AddressLine = { address: string, - confirmed: {balance: number}, - unconfirmed: {balance: number}, - total: {balance: number}, + confirmed: { balance: number }, + unconfirmed: { balance: number }, + total: { balance: number }, } type networkType = 'testnet' | 'mainnet' type keyType = 'public' | 'private' type WalletStore = { - [address in string]:{ - key:string - name:string - } + [address in string]: { + key: string + name: string + } } type GrandTotal = { confirmed: number, @@ -559,7 +651,23 @@ type GrandTotal = { total: number } type bip32Keypair = { publicExtendedKey: string, privateExtendedKey: string } - +type getUnspentTxsApiResponse = { + success: boolean, + paging?: { + next_link: string | null + } + unspent?: { + addresses: string[], + confirmations: number, + id: number, n: number, + req_sigs: number, + script_pub_key: any, + txid: string, + type: string, + value: string, + value_int: number + }[] +} const api = { testnet: 'https://testnet-api.smartbit.com.au/v1/blockchain', mainnet: 'https://api.smartbit.com.au/v1/blockchain'