Skip to content

Commit

Permalink
Merge 6ffcc78 into b3f0db2
Browse files Browse the repository at this point in the history
  • Loading branch information
pbca26 authored Dec 22, 2020
2 parents b3f0db2 + 6ffcc78 commit c295b0c
Show file tree
Hide file tree
Showing 8 changed files with 292 additions and 52 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ jobs:
needs: [job-macos-build, job-linux-build, job-win-build]

steps:
- name: ACTIONS_ALLOW_UNSECURE_COMMANDS
id: ACTIONS_ALLOW_UNSECURE_COMMANDS
run: echo 'ACTIONS_ALLOW_UNSECURE_COMMANDS=true' >> $GITHUB_ENV
- name: Download Artifacts Portable (Linux)
env:
BASE_BRANCH: ${{ github.base_ref }}
Expand Down
2 changes: 2 additions & 0 deletions config-overrides.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = {
path.resolve(paths.appNodeModules, 'bip32'),
path.resolve(paths.appNodeModules, 'typeforce'),
path.resolve(paths.appNodeModules, 'semver'),
path.resolve(paths.appNodeModules, 'lru-cache'),
path.resolve(paths.appNodeModules, 'yallist'),
path.resolve(paths.appNodeModules, '@ledgerhq/hw-app-btc'),
path.resolve(paths.appNodeModules, '@ledgerhq/hw-transport/lib-es'),
path.resolve(paths.appNodeModules, '@ledgerhq/hw-transport-webusb/lib-es'),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hw-kmd-wallet",
"version": "0.2.0",
"version": "0.2.1",
"author": "Luke Childs <lukechilds123@gmail.com> (http://lukechilds.co.uk), Komodo Platform (https://komodoplatform.com)",
"repository": "pbca26/hw-kmd-wallet",
"homepage": "https://pbca26.github.io/hw-kmd-wallet",
Expand Down
22 changes: 16 additions & 6 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {isEqual} from 'lodash';
import Header from './Header';
import BetaWarning from './BetaWarning';
import CheckBalanceButton from './CheckBalanceButton';
import CheckAllBalancesButton from './CheckAllBalancesButton';
import Accounts from './Accounts';
import WarnU2fCompatibility from './WarnU2fCompatibility';
import WarnBrowser from './WarnBrowser';
Expand Down Expand Up @@ -367,12 +368,21 @@ class App extends React.Component {
</select>
{(this.state.vendor === 'trezor' || (this.state.vendor === 'ledger' && this.state.ledgerDeviceType)) &&
this.state.explorerEndpoint &&
<CheckBalanceButton
handleRewardData={this.handleRewardData}
checkTipTime={this.checkTipTime}
vendor={this.state.vendor}>
<strong>Check Balance</strong>
</CheckBalanceButton>
<React.Fragment>
<CheckBalanceButton
handleRewardData={this.handleRewardData}
checkTipTime={this.checkTipTime}
vendor={this.state.vendor}>
<strong>Check Balance</strong>
</CheckBalanceButton>
<CheckAllBalancesButton
handleRewardData={this.handleRewardData}
checkTipTime={this.checkTipTime}
vendor={this.state.vendor}
explorerEndpoint={this.state.explorerEndpoint}>
<strong>Check All</strong>
</CheckAllBalancesButton>
</React.Fragment>
}
<button
className="button is-light"
Expand Down
248 changes: 248 additions & 0 deletions src/CheckAllBalancesButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
import React from 'react';
import getKomodoRewards from './lib/get-komodo-rewards';
import hw from './lib/hw';
import accountDiscovery, {clearPubkeysCache} from './lib/account-discovery';
import blockchain, {setExplorerUrl, getInfo} from './lib/blockchain';
import updateActionState from './lib/update-action-state';
import {TX_FEE, VENDOR} from './constants';
import ActionListModal from './ActionListModal';
import asyncForEach from './lib/async';
import coins from './lib/coins';
import humanReadableSatoshis from './lib/human-readable-satoshis';
import {
isElectron,
appData,
} from './Electron';

const headings = [
'Coin',
'Balance',
];
const coinsToCheckDev = [
'RICK',
'MORTY',
'KMD',
];

let cancel = false;

class CheckAllBalancesButton extends React.Component {
state = this.initialState;

get initialState() {
return {
isCheckingRewards: false,
error: false,
coin: '',
balances: [],
progress: '',
emptyBalances: false,
actions: {
connect: {
icon: 'fab fa-usb',
description: this.props.vendor === 'ledger' ? <div>Connect and unlock your Ledger, then open the Komodo app on your device.</div> : <div>Connect and unlock your Trezor.</div>,
state: null
},
approve: {
icon: 'fas fa-microchip',
description: <div>Approve all public key export requests on your device. <strong>There will be multiple requests</strong>.</div>,
state: null
},
finished: {
icon: 'fas fa-check',
description: <div>All coin balances are checked.</div>,
state: null
},
},
isDebug: isElectron ? appData.isDev : window.location.href.indexOf('devmode') > -1,
};
}

resetState = () => {
cancel = true;
this.setState(this.initialState);
}

calculateRewardData = ({accounts, tiptime}) => accounts.map(account => {
account.balance = account.utxos.reduce((balance, utxo) => balance + utxo.satoshis, 0);
account.rewards = account.utxos.reduce((rewards, utxo) => rewards + getKomodoRewards({tiptime, ...utxo}), 0);
account.claimableAmount = account.rewards - TX_FEE * 2;

return account;
});

scanAddresses = async () => {
const coinTickers = Object.keys(coins);
let balances = [];
cancel = false;

await asyncForEach(coinTickers, async (coin, index) => {
if (!cancel && (!this.state.isDebug || (this.state.isDebug && coinsToCheckDev.indexOf(coin) > -1))) {
const getInfoRes = await Promise.all(coins[coin].api.map((value, index) => {
return getInfo(value);
}));
let isExplorerEndpointSet = false;

console.warn('checkExplorerEndpoints', getInfoRes);

for (let i = 0; i < coins[coin].api.length; i++) {
if (getInfoRes[i] &&
getInfoRes[i].hasOwnProperty('info') &&
getInfoRes[i].info.hasOwnProperty('version')) {
console.warn(`${coin} set api endpoint to ${coins[coin].api[i]}`);
setExplorerUrl(coins[coin].api[i]);
isExplorerEndpointSet = true;

break;
}
}

if (isExplorerEndpointSet) {
this.setState({
...this.initialState,
isCheckingRewards: true,
coin,
progress: ` (${index + 1}/${coinTickers.length})`,
balances,
});

let currentAction;
try {
currentAction = 'connect';
updateActionState(this, currentAction, 'loading');
const hwIsAvailable = await hw[this.props.vendor].isAvailable();
if (!hwIsAvailable) {
throw new Error(`${VENDOR[this.props.vendor]} device is unavailable!`);
}
updateActionState(this, currentAction, true);

currentAction = 'approve';
updateActionState(this, currentAction, 'loading');
let [accounts, tiptime] = await Promise.all([
accountDiscovery(this.props.vendor),
blockchain.getTipTime()
]);

tiptime = this.props.checkTipTime(tiptime);

accounts = this.calculateRewardData({accounts, tiptime});
updateActionState(this, currentAction, true);

let balanceSum = 0;
let rewardsSum = 0;

for (let i = 0; i < accounts.length; i++) {
balanceSum += accounts[i].balance;
rewardsSum += accounts[i].rewards;
}

if (balanceSum) {
balances.push({
coin,
balance: balanceSum,
rewards: rewardsSum,
});
}

this.setState({
balances,
});
//this.setState({...this.initialState});
} catch (error) {
console.warn(error);
updateActionState(this, currentAction, false);
this.setState({error: error.message});
}
}
}
});

if (!cancel) {
if (!this.state.error || (this.state.error && this.state.error.indexOf('Failed to fetch') > -1)) {
updateActionState(this, 'approve', true);
updateActionState(this, 'finished', true);
}

clearPubkeysCache();

this.setState({
error: false,
progress: '',
coin: '',
isCheckingRewards: true,
emptyBalances: !this.state.balances.length,
});
}

setExplorerUrl(this.props.explorerEndpoint);
};

renderCoinBalances() {
const balances = this.state.balances;

return (
<table className="table is-striped">
<thead>
<tr>
{headings.map(heading => <th key={heading}>{heading}</th>)}
</tr>
</thead>
{balances.length > 10 &&
<tfoot>
<tr>
{headings.map(heading => <th key={heading}>{heading}</th>)}
</tr>
</tfoot>
}
<tbody>
{balances.map(item => (
<tr key={item.coin}>
<th>{item.coin}</th>
<td>{humanReadableSatoshis(item.balance)}{item.rewards ? ` (${humanReadableSatoshis(item.rewards)})` : ''}</td>
</tr>
))}
</tbody>
</table>
);
}

render() {
const {
isCheckingRewards,
actions,
error,
} = this.state;

return (
<React.Fragment>
<button
className="button is-primary"
onClick={this.scanAddresses}>
{this.props.children}
</button>
<ActionListModal
title={`Scanning Blockchain ${this.state.coin}${this.state.progress}`}
isCloseable={true}
actions={actions}
error={error}
handleClose={this.resetState}
show={isCheckingRewards}>
<p>
Exporting public keys from your {VENDOR[this.props.vendor]} device, scanning the blockchain for funds, and calculating any claimable rewards. Please approve any public key export requests on your device.
</p>
{this.state.balances &&
this.state.balances.length > 0 &&
<React.Fragment>{this.renderCoinBalances()}</React.Fragment>
}
{this.state.emptyBalances &&
<p>
<strong>No active balances are found</strong>
</p>
}
</ActionListModal>
</React.Fragment>
);
}
}

export default CheckAllBalancesButton;
22 changes: 14 additions & 8 deletions src/lib/account-discovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@ import blockchain from './blockchain';
import getAddress from './get-address';
import bitcoin from 'bitcoinjs-lib';
import parseHistory from './history-parser';
import asyncForEach from './async';

let pubKeysCache = {};

async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

const walkDerivationPath = async node => {
const addressConcurrency = 10;
const gapLimit = 20;
const addresses = [];
let addressConcurrency = 10;
let gapLimit = 20;
let consecutiveUnusedAddresses = 0;
let addressIndex = 0;

if (window.location.href.indexOf('extgap=s') > -1) gapLimit = 30;
if (window.location.href.indexOf('extgap=m') > -1) gapLimit = 40;
if (window.location.href.indexOf('extgap=l') > -1) gapLimit = 50;

if (window.location.href.indexOf('timeout=s') > -1) addressConcurrency = 2;
if (window.location.href.indexOf('timeout=m') > -1) addressConcurrency = 5;

while (consecutiveUnusedAddresses < gapLimit) {
const addressApiRequests = [];

Expand Down Expand Up @@ -211,4 +213,8 @@ const accountDiscovery = async vendor => {
return accounts;
};

export const clearPubkeysCache = () => {
pubKeysCache = {};
};

export default accountDiscovery;
7 changes: 7 additions & 0 deletions src/lib/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

export default asyncForEach;
Loading

0 comments on commit c295b0c

Please sign in to comment.