broken wallet but ok

This commit is contained in:
peter
2019-08-17 14:16:00 +02:00
parent 92666ba0c0
commit 8a46c56211
2 changed files with 301 additions and 188 deletions
@@ -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 { PluginsComponent } from './btc.component';
import { ClrDatagridStringFilterInterface, ClrDatagridNumericFilterInterface, ClrDropdownModule } from '@clr/angular'; import { ClrDatagridStringFilterInterface, ClrDatagridNumericFilterInterface, ClrDropdownModule } from '@clr/angular';
declare const fb declare const fb
@@ -36,13 +36,13 @@ declare const fb
<clr-dg-row *clrDgItems="let line of regularAddresses"> <clr-dg-row *clrDgItems="let line of regularAddresses">
<clr-dg-cell class="monospace" style="word-break: break-all">{{line.address}}</clr-dg-cell> <clr-dg-cell class="monospace" style="word-break: break-all">{{line.address}}</clr-dg-cell>
<clr-dg-cell class="monospace">{{line.confirmed.balance | number: '1.8-18'}}</clr-dg-cell> <clr-dg-cell class="">{{line.confirmed.balance | number: '1.8-18'}}</clr-dg-cell>
<clr-dg-cell class="monospace"> <clr-dg-cell class="">
<span *ngIf="line.unconfirmed.balance != 0"> <span *ngIf="line.unconfirmed.balance != 0">
{{line.unconfirmed.balance | number: '1.8-18'}} {{line.unconfirmed.balance | number: '1.8-18'}}
</span> </span>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell class="monospace">{{line.total.balance | number: '1.8-18'}}</clr-dg-cell> <clr-dg-cell class="">{{line.total.balance | number: '1.8-18'}}</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded > <clr-dg-row-detail *clrIfExpanded >
<div class="clr-row"> <div class="clr-row">
@@ -101,13 +101,13 @@ declare const fb
<clr-dg-row *clrDgItems="let line of changeAddresses"> <clr-dg-row *clrDgItems="let line of changeAddresses">
<clr-dg-cell class="monospace" style="word-break: break-all">{{line.address}}</clr-dg-cell> <clr-dg-cell class="monospace" style="word-break: break-all">{{line.address}}</clr-dg-cell>
<clr-dg-cell class="monospace">{{line.confirmed.balance | number: '1.8-18'}}</clr-dg-cell> <clr-dg-cell class="">{{line.confirmed.balance | number: '1.8-18'}}</clr-dg-cell>
<clr-dg-cell class="monospace"> <clr-dg-cell class="">
<span *ngIf="line.unconfirmed.balance != 0"> <span *ngIf="line.unconfirmed.balance != 0">
{{line.unconfirmed.balance | number: '1.8-18'}} {{line.unconfirmed.balance | number: '1.8-18'}}
</span> </span>
</clr-dg-cell> </clr-dg-cell>
<clr-dg-cell class="monospace">{{line.total.balance | number: '1.8-18'}}</clr-dg-cell> <clr-dg-cell class="">{{line.total.balance | number: '1.8-18'}}</clr-dg-cell>
<clr-dg-row-detail *clrIfExpanded > <clr-dg-row-detail *clrIfExpanded >
<div class="clr-row"> <div class="clr-row">
@@ -141,7 +141,8 @@ declare const fb
</div> </div>
` `
}) })
export class AddressViewerComponent implements OnInit{ export class AddressViewerComponent implements OnInit, OnDestroy{
balanceFilter = new BalanceFilter() balanceFilter = new BalanceFilter()
otherBalanceFilter = new BalanceFilter() otherBalanceFilter = new BalanceFilter()
regularAddresses = [] regularAddresses = []
@@ -161,6 +162,10 @@ export class AddressViewerComponent implements OnInit{
setChangeAddresses(addresses){ setChangeAddresses(addresses){
this.changeAddresses = addresses this.changeAddresses = addresses
} }
ngOnDestroy(): void {
}
} }
class BalanceFilter implements ClrDatagridNumericFilterInterface<any>{ class BalanceFilter implements ClrDatagridNumericFilterInterface<any>{
@@ -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 { WalletGeneratorComponent } from './wallet-generator.component';
import { WalletPickerComponent } from './wallet-picker.component'; import { WalletPickerComponent } from './wallet-picker.component';
import { HDKey } from "btc-hdkey" import { HDKey } from "btc-hdkey"
import { AddressViewerComponent } from './address-viewer.component'; import { AddressViewerComponent } from './address-viewer.component';
const fetch = require("node-fetch") const fetch = require("node-fetch")
const coinselect = require('coinselect/accumulative') const coinselect = require('coinselect')
import { TransactionBuilder, Signer } from 'bitcoinjs-lib' import { TransactionBuilder, Signer } from 'bitcoinjs-lib'
import { networks } from 'bitcoinjs-lib' import { networks } from 'bitcoinjs-lib'
import { SubscriptionResponse, ErrorResponse } from 'frontblock-generic/Types';
let feeRate = 55 // satoshis per byte let feeRate = 55 // satoshis per byte
const hdkey = new HDKey() const hdkey = new HDKey()
@@ -31,7 +32,7 @@ declare const fb
</clr-modal> </clr-modal>
<!-- / loading --> <!-- / loading -->
<!-- row 0 --> <!-- faucets -->
<div *ngIf="mode.net == 'testnet'" class="clr-all-12"> <div *ngIf="mode.net == 'testnet'" class="clr-all-12">
<div class="card"> <div class="card">
@@ -51,6 +52,7 @@ declare const fb
</div> </div>
</div> </div>
<!-- /faucets -->
<!-- row 1 --> <!-- row 1 -->
<div class="clr-all-12"> <div class="clr-all-12">
@@ -137,9 +139,14 @@ declare const fb
</p> </p>
</div> </div>
<div class="card-footer" > <div class="card-footer clr-row" style="margin-left: 2px" >
<button class="btn btn-icon" (click)="reload()"> <clr-icon shape="refresh"></clr-icon> Reload </button> <button *ngIf="!autorefresh" class="btn btn-icon clr-col-4" (click)="reload()"> <clr-icon shape="refresh"></clr-icon> Reload </button>
<clr-toggle-wrapper class="clr-col-auto clr-align-self-center">
<input type="checkbox" clrToggle [(ngModel)]="autorefresh" />
<label>Autorefresh</label>
</clr-toggle-wrapper>
</div> </div>
</div> </div>
<!-- Balance box--> <!-- Balance box-->
@@ -149,59 +156,111 @@ declare const fb
</div> </div>
<!-- /row wallet picker + send box + balance box --> <!-- /row wallet picker + send box + balance box -->
<div class="clr-all-12" style="padding-left:0px; padding-right: 0px">
<address-viewer></address-viewer>
<!-- live-tx -->
<div class="clr-row" *ngIf="apiClientPlugin && transactions.length != 0" >
<div class="clr-col-12">
<div class="clr-all-12" style="padding-left:0px; padding-right: 0px">
<div class="card">
<div class="card-header">
Live Transactions
</div>
<div class="card-block" >
<table class="table table-compact">
<thead>
<tr>
<th>Hash</th>
<th>Amount</th>
<th>Status</th>
<th>To</th>
<th>From</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let line of transactions">
<td style="word-break:break-all">{{line.hash}}</td>
<td>{{line.amount}}</td>
<td>{{line.status}}</td>
<td style="word-break:break-all">{{line.to}}</td>
<td style="word-break:break-all">{{line.from.join(', ')}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</div>
<!-- /live-tx -->
<!-- address-viewer -->
<div class="clr-all-12" style="padding-left:0px; padding-right: 0px">
<address-viewer></address-viewer>
</div>
<!-- address-viewer -->
</div> </div>
<!-- /row 1 --> <!-- /row 1 -->
` `
}) })
export class PluginsComponent implements AfterViewInit { export class PluginsComponent implements AfterViewInit, OnDestroy {
errors:string[] = [] timerRefresh: NodeJS.Timeout
successes:string[] = [] errors: string[] = []
wallets:WalletStore = {} successes: string[] = []
txToSend:any wallets: WalletStore = {}
rootkey:bip32Keypair = {publicExtendedKey: "",privateExtendedKey: ""} rootkey: bip32Keypair = { publicExtendedKey: "", privateExtendedKey: "" }
keys:KeyStore = {} keys: KeyStore = {}
regaddresses:AddressLine[] = [] regaddresses: AddressLine[] = []
changeaddresses:AddressLine[] = [] changeaddresses: AddressLine[] = []
balanceDisplay = { balanceDisplay = {
confirmed: 0, confirmed: 0,
total: 0, total: 0,
unconfirmed: 0 unconfirmed: 0
} }
mode : { mode: {
type:keyType, type: keyType,
net:networkType net: networkType
} = { } = {
net: "testnet", net: "testnet",
type: "public" type: "public"
} }
sendTo:string sendTo: string
sendAmount:number sendAmount: number
loading = false loading = false
loadingN = 1 loadingN = 1
autorefresh = false
refreshing = false
apiClientPlugin = false
subscriptions = []
transactions = []
@ViewChild(AddressViewerComponent, {static: false})
AddressViewerComponent:AddressViewerComponent
@ViewChild(WalletGeneratorComponent, {static: false}) @ViewChild(AddressViewerComponent, { static: false })
WalletGeneratorComponent:WalletGeneratorComponent AddressViewerComponent: AddressViewerComponent
@ViewChild(WalletPickerComponent, {static: false}) @ViewChild(WalletGeneratorComponent, { static: false })
WalletPickerComponent:WalletPickerComponent WalletGeneratorComponent: WalletGeneratorComponent
@ViewChild(WalletPickerComponent, { static: false })
WalletPickerComponent: WalletPickerComponent
ngAfterViewInit(): void { ngAfterViewInit(): void {
this.WalletGeneratorComponent.setParent(this) this.WalletGeneratorComponent.setParent(this)
this.WalletPickerComponent.setParent(this) this.WalletPickerComponent.setParent(this)
this.AddressViewerComponent.setParent(this) this.AddressViewerComponent.setParent(this)
this.apiClientPlugin = fb.ApiClient != null
window['btc'] = this window['btc'] = this
this.timerRefresh = setInterval(async () => { if (this.autorefresh && !this.refreshing && !this.loading) this.refresh() }, 30000)
this.loadKey("tprv8cXm1S7PGS2fHuLjZajuDZZsvxerzv4zZJJAhkLAWZzwz5rE7efHm31xsqpzfoGMHBNC8bkRcsjiMdmkSuNn5C1qNqk7rrk1pCuhk4HfVak") this.loadKey("tprv8cXm1S7PGS2fHuLjZajuDZZsvxerzv4zZJJAhkLAWZzwz5rE7efHm31xsqpzfoGMHBNC8bkRcsjiMdmkSuNn5C1qNqk7rrk1pCuhk4HfVak")
} }
addWallet(wallet){ ngOnDestroy(): void {
clearInterval(this.timerRefresh)
}
addWallet(wallet) {
/* store somewhere */ /* store somewhere */
this.wallets[wallet.name] = wallet this.wallets[wallet.name] = wallet
@@ -209,19 +268,16 @@ export class PluginsComponent implements AfterViewInit {
} }
showWizard(){ showWizard() {
this.WalletGeneratorComponent.open() this.WalletGeneratorComponent.open()
} }
loadKey(key:string){ loadKey(key: string) {
this.wallets = {} this.wallets = {}
this.txToSend = null
this.sendTo = ""
this.regaddresses = [] this.regaddresses = []
const t = hdkey.checkHDKey(key) const t = hdkey.checkHDKey(key)
if(t){ if (t) {
console.log(t)
this.mode = t this.mode = t
} }
@@ -229,42 +285,65 @@ export class PluginsComponent implements AfterViewInit {
this.reload() this.reload()
} }
async reload():Promise<void>{ async reload(): Promise<void> {
if (this.loading || this.refreshing)
return
this.loading = true this.loading = true
this.loadingN = 1 this.loadingN = 1
this.keys = {} this.keys = {}
this.balanceDisplay.total = 0
this.balanceDisplay.unconfirmed = 0
this.balanceDisplay.confirmed = 0
await this.refresh() 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 this.loading = false
} }
async refresh():Promise<void>{ async refresh(): Promise<void> {
if (this.refreshing)
return
this.refreshing = true
let p1 = this.updateChange() let p1 = this.updateChange()
let p2 = this.updateBalance() let p2 = this.updateBalance()
let balances:GrandTotal[] = await Promise.all([p1,p2]) let balances: GrandTotal[] = await Promise.all([p1, p2])
balances.forEach(balance => {
this.balanceDisplay.confirmed += balance.confirmed this.balanceDisplay = balances.reduce((a, o) => {
this.balanceDisplay.total += balance.total a.confirmed += o.confirmed
this.balanceDisplay.unconfirmed += balance.unconfirmed a.total += o.total
}) a.unconfirmed += o.unconfirmed
return a
}, {
confirmed: 0,
unconfirmed: 0,
total: 0
})
this.AddressViewerComponent.setChangeAddresses(this.changeaddresses) this.AddressViewerComponent.setChangeAddresses(this.changeaddresses)
this.AddressViewerComponent.setRegularAddresses(this.regaddresses) 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[] = [] let addresses: string[] = []
for(let i = skip; i < skip+n; i++){ for (let i = skip; i < skip + n; i++) {
let addr let addr
if(changeAddresses){ if (changeAddresses) {
addr = hdkey.deriveAddress(rootAddr, i, 'm/1/') addr = hdkey.deriveAddress(rootAddr, i, 'm/1/')
}else{ } else {
addr = hdkey.deriveAddress(rootAddr, i) addr = hdkey.deriveAddress(rootAddr, i)
} }
if(!addr){ if (!addr) {
return return
} }
addresses.push(addr) addresses.push(addr)
@@ -272,28 +351,28 @@ export class PluginsComponent implements AfterViewInit {
return addresses 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(',') let lookupstr = addresses.join(',')
try{ try {
let r = await fetch(api[network]+'/address/'+lookupstr, { let r = await fetch(api[network] + '/address/' + lookupstr, {
'method':'GET', 'method': 'GET',
'mode':'cors', 'mode': 'cors',
'data': {limit: 200} 'data': { limit: 200 }
}) })
if(r.status !== 200){ if (r.status !== 200) {
return {success: false} return { success: false }
} }
let json = (await r).json() let json = (await r).json()
return json return json
}catch(e){ } catch (e) {
return {success: false} return { success: false }
} }
} }
async getAddressInfos(rootAddr: string, n = 10, skip = 0, changeAddresses = false): Promise<any>{ async getAddressInfos(rootAddr: string, n = 10, skip = 0, changeAddresses = false): Promise<any> {
const t = hdkey.checkHDKey(rootAddr) const t = hdkey.checkHDKey(rootAddr)
if(!t){ if (!t) {
return false return false
} }
const addresses = this.getAddresses(rootAddr, n, skip, changeAddresses) const addresses = this.getAddresses(rootAddr, n, skip, changeAddresses)
@@ -301,88 +380,104 @@ export class PluginsComponent implements AfterViewInit {
} }
async getTxs(address: string, type:networkType){ /*
try{ async getTxs(address: string, type:networkType){
let r = await fetch(api[type]+'/address/'+address, { try{
'method':'GET', let r = await fetch(api[type]+'/address/'+address, {
'mode':'cors' 'method':'GET',
}) 'mode':'cors'
if(r.status !== 200){ })
return {success: false} 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<getUnspentTxsApiResponse> {
async getUnspentTxs(addresses:string[], type:networkType):Promise<any>{
let lookupstr = addresses.join(',') let lookupstr = addresses.join(',')
let url = api[type] + '/address/' + lookupstr + '/unspent'
let txs:getUnspentTxsApiResponse['unspent'] = []
while (true) {
try {
try{ let resp = await fetch(url, {
let r = await fetch(api[type]+'/address/'+lookupstr+'/unspent', { 'method': 'GET',
'method':'GET', 'mode': 'cors'
'mode':'cors' })
}) if (resp.status !== 200) {
if(r.status !== 200){ return { success: false }
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 (await r).json() return {
}catch(e){ success: true,
console.warn ("getUnspentTxs", e) unspent: txs
return {success: false}
} }
} }
getKeyForAddress(keyName){ getKeyForAddress(keyName) {
return this.keys[keyName] return this.keys[keyName]
} }
calculateTotal(addresses:AddressLine[]):GrandTotal{ calculateTotal(addresses: AddressLine[]): GrandTotal {
const resMap: any = addresses.reduce((o1, o2)=>{o1[o2.address] = o2; return o1}, {}) const resMap: any = addresses.reduce((o1, o2) => { o1[o2.address] = o2; return o1 }, {})
const total: any = Object.values(resMap).reduce((accum:GrandTotal, obj:AddressLine) => { const total: any = Object.values(resMap).reduce((accum: GrandTotal, obj: AddressLine) => {
accum.confirmed += parseFloat(<any>obj.confirmed.balance) accum.confirmed += parseFloat(<any>obj.confirmed.balance)
accum.unconfirmed += parseFloat(<any>obj.unconfirmed.balance) accum.unconfirmed += parseFloat(<any>obj.unconfirmed.balance)
accum.total += parseFloat(<any>obj.confirmed.balance) + parseFloat(<any>obj.unconfirmed.balance) accum.total += parseFloat(<any>obj.confirmed.balance) + parseFloat(<any>obj.unconfirmed.balance)
return accum return accum
},{ }, {
confirmed: 0, confirmed: 0,
unconfirmed: 0, unconfirmed: 0,
total: 0 total: 0
}) })
return total return total
} }
async updateBalance():Promise<GrandTotal>{ async updateBalance(): Promise<GrandTotal> {
let n = 1 let n = 1
let res: { success: boolean; addresses: any; address: any; } let res: { success: boolean; addresses: any; address: any; }
this.regaddresses = [] this.regaddresses = []
while(true){ while (true) {
this.loadingN = n this.loadingN = n
try{ try {
res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115*n, 115*(n-1)) res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115 * n, 115 * (n - 1))
}catch(e){ } catch (e) {
break break
} }
if(res.success === true){ if (res.success === true) {
if('addresses' in res){ // multiple if ('addresses' in res) { // multiple
this.regaddresses = [...this.regaddresses, ...res.addresses] this.regaddresses = [...this.regaddresses, ...res.addresses]
} }
if('address' in res){ if ('address' in res) {
this.regaddresses.push(res.address) this.regaddresses.push(res.address)
} }
n++ n++
}else{ } else {
console.warn(`This error is a result of a failed fetch request. 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) 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`) They are completely safe and mean no more addresses were found`)
console.log('No more balance found after attempting ', 115*n, 'addresses') console.log('No more balance found after attempting ', 115 * n, 'addresses')
if(this.mode.type == "private"){ if (this.mode.type == "private") {
for(let i = 0; i < 115*n+1; i++){ for (let i = 0; i < 115 * n + 1; i++) {
const keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i) const keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i)
const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net) const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net)
keyN.derivation = i 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) const extraAddr = hdkey.deriveAddress(this.rootkey.publicExtendedKey, this.regaddresses.length)
this.regaddresses.push({ this.regaddresses.push({
address: <string>extraAddr, address: <string>extraAddr,
confirmed: {balance: 0}, confirmed: { balance: 0 },
unconfirmed: {balance: 0}, unconfirmed: { balance: 0 },
total: {balance: 0}, total: { balance: 0 },
}) })
return this.calculateTotal(this.regaddresses) return this.calculateTotal(this.regaddresses)
} }
async updateChange():Promise<GrandTotal>{ async updateChange(): Promise<GrandTotal> {
let n = 1 let n = 1
this.changeaddresses = [] this.changeaddresses = []
while(true){ while (true) {
let res let res
try{ try {
res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115*n, 115*(n-1), true) res = await this.getAddressInfos(this.rootkey.publicExtendedKey, 115 * n, 115 * (n - 1), true)
}catch(e){ } catch (e) {
break break
} }
if(res.success === true){ if (res.success === true) {
if('addresses' in res){ // multiple if ('addresses' in res) { // multiple
this.changeaddresses = [...this.changeaddresses, ...res.addresses] this.changeaddresses = [...this.changeaddresses, ...res.addresses]
} }
if('address' in res){ if ('address' in res) {
this.changeaddresses.push(res.address) this.changeaddresses.push(res.address)
} }
n++ n++
}else{ } else {
console.warn(`This error is a result of a failed fetch request. 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) 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`) They are completely safe and mean no more addresses were found`)
console.log('No more change balance found after attempting ', 115*n, 'addresses') console.log('No more change balance found after attempting ', 115 * n, 'addresses')
if(this.mode.type == "private"){ if (this.mode.type == "private") {
//make keypairs for addresses //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 keyN = hdkey.deriveKey(this.rootkey.privateExtendedKey, i, 'm/1/')
const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net) const addrN = hdkey.addressFromHDKey(keyN._publicKey, this.mode.net)
keyN.derivation = i keyN.derivation = i
@@ -444,14 +539,13 @@ 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){ async createAndSendTx(target: string, value: number) {
if (target == null || typeof target !== 'string' || target === '' || value == null || value <= 0) {
if(target == null || typeof target !== 'string' || target === '' || value == null || value <= 0 ){
this.errors.push('Bad inputs. Expected bitcoin address and number') this.errors.push('Bad inputs. Expected bitcoin address and number')
return return
} }
if(value > this.balanceDisplay.confirmed){ if (value > this.balanceDisplay.confirmed) {
this.errors.push('Insufficient funds') this.errors.push('Insufficient funds')
return 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)] let addresses: string[] = [...this.regaddresses.map(a => a.address), ...this.changeaddresses.map(a => a.address)]
const txs = await this.getUnspentTxs(addresses, this.mode.net) 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') this.errors.push('No UTXOs found')
return return
} }
@@ -469,14 +563,13 @@ They are completely safe and mean no more addresses were found`)
address: t.addresses[0], address: t.addresses[0],
txId: t.txid, txId: t.txid,
vout: t.n, vout: t.n,
value: t.value * 100000000 value: parseFloat(t.value) * 100000000
} }
}) })
let { inputs, outputs, fee } = coinselect(utxos, [{ address: target, value: parseFloat(<any>value) * 100000000 }], feeRate)
let { inputs, outputs, fee } = coinselect(utxos, [{address: target, value: value*100000000}], feeRate)
// .inputs and .outputs will be undefined if no solution was found // .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') this.errors.push('Could not build a valid transaction')
return return
} }
@@ -490,7 +583,7 @@ They are completely safe and mean no more addresses were found`)
}) })
outputs.forEach((output) => { outputs.forEach((output) => {
if (!output.address) { 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) txb.addOutput(output.address, output.value)
}) })
@@ -498,59 +591,58 @@ They are completely safe and mean no more addresses were found`)
txb.sign(i, <Signer>this.keys[input.address]) txb.sign(i, <Signer>this.keys[input.address])
}) })
this.txToSend = txb.build() await this.sendTx(txb.build())
this.sendTx()
} }
private async sendTx(){ private async sendTx(tx) {
if(this.txToSend == null){ if (tx == null) {
this.errors.push('No transaction to send!') this.errors.push('No transaction to send!')
return return
} }
let url = api[this.mode.net]+"/pushtx" let url = api[this.mode.net] + "/pushtx"
let r let r
try{ try {
r = await fetch(url, { r = await fetch(url, {
'method':'POST', 'method': 'POST',
'body': JSON.stringify({'hex': this.txToSend.toHex()}) 'body': JSON.stringify({ 'hex': tx.toHex() })
}) })
}catch(e){ } catch (e) {
console.warn("sendTx", e) console.warn("sendTx", e)
return return
} }
this.txToSend = null if (r.status === 200) {
if(r.status === 200){
this.errors = [] this.errors = []
this.successes.push("Sent "+this.sendAmount+" to "+this.sendTo) this.successes.push("Sent " + this.sendAmount + " to " + this.sendTo)
this.reload() this.sendTo = ""
}else{ this.sendAmount = 0
this.errors.push('The API '+url+' returned a non-200 error code '+r.status+' '+r.statusText) //this.reload()
} else {
this.errors.push('The API ' + url + ' returned a non-200 error code ' + r.status + ' ' + r.statusText)
} }
} }
addrExplorer(address){ addrExplorer(address) {
const url = this.mode.net === 'testnet'?'https://live.blockcypher.com/btc-testnet/address/':'https://live.blockcypher.com/btc/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') window.open(url + address, '_blank')
} }
} }
export type KeyStore = {[keyname in string]: bip32Keypair & Signer} export type KeyStore = { [keyname in string]: bip32Keypair & Signer }
export type AddressLine = { export type AddressLine = {
address: string, address: string,
confirmed: {balance: number}, confirmed: { balance: number },
unconfirmed: {balance: number}, unconfirmed: { balance: number },
total: {balance: number}, total: { balance: number },
} }
type networkType = 'testnet' | 'mainnet' type networkType = 'testnet' | 'mainnet'
type keyType = 'public' | 'private' type keyType = 'public' | 'private'
type WalletStore = { type WalletStore = {
[address in string]:{ [address in string]: {
key:string key: string
name:string name: string
} }
} }
type GrandTotal = { type GrandTotal = {
@@ -559,7 +651,23 @@ type GrandTotal = {
total: number total: number
} }
type bip32Keypair = { publicExtendedKey: string, privateExtendedKey: string } 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 = { const api = {
testnet: 'https://testnet-api.smartbit.com.au/v1/blockchain', testnet: 'https://testnet-api.smartbit.com.au/v1/blockchain',
mainnet: 'https://api.smartbit.com.au/v1/blockchain' mainnet: 'https://api.smartbit.com.au/v1/blockchain'