Merge pull request #3390 from matiu/feat/trezor48

Feat/trezor48
This commit is contained in:
Gustavo Maximiliano Cortez 2015-11-10 20:23:20 -03:00
commit 8b1130d66b
31 changed files with 958 additions and 979 deletions

View File

@ -37,8 +37,7 @@ module.exports = function(grunt) {
'src/js/routes.js',
'src/js/services/*.js',
'src/js/models/*.js',
'src/js/controllers/*.js',
'src/js/trezor.js'
'src/js/controllers/*.js'
],
tasks: ['concat:js']
}
@ -79,7 +78,8 @@ module.exports = function(grunt) {
'src/js/translations.js',
'src/js/version.js',
'src/js/init.js',
'src/js/trezor.js'
'src/js/trezor-url.js',
'bower_components/trezor-connect/login.js'
],
dest: 'public/js/copay.js'
},

28
baseDerivation.md Normal file
View File

@ -0,0 +1,28 @@
Copay accepts three base derivation paths:
* m/44'
* m/48' (only used for MULTISIGNATURE, HARDWARE Wallets)
* m/45' (deprecated and it is only supported for old wallets)
Both m/44 and m/48 follow the BIP44 standard:
m/XX'/<coin_type>'/<account'>
Supported cointypes are: 0: Livenet, and 1: Testnet
If you need to import a wallet from a mnemonic using an account different
from the default (0), use, for example:
m/44'/0'/11'
to import account 11.
In case you have a multisignature wallet originally created from a hardware device, and you had loose access to the device, you will need to enter the 24 mnemonic backup (from the device) and a path like:
m/48'/0'/8'
for a multisignature wallet, account 8.
Finally, note that TREZOR use 1-based account numbers, so if your are trying for example to recover TREZOR multisig account #8, you should enter `m/48'/0'/7'`.

View File

@ -8,7 +8,7 @@
],
"dependencies": {
"angular": "1.4.6",
"angular-bitcore-wallet-client": "1.1.2",
"angular-bitcore-wallet-client": "1.1.6",
"angular-foundation": "0.7.0",
"angular-gettext": "2.1.0",
"angular-moment": "0.10.1",
@ -21,6 +21,7 @@
"foundation-icon-fonts": "*",
"moment": "2.10.3",
"ng-lodash": "0.2.3",
"qrcode-decoder-js": "*"
"qrcode-decoder-js": "*",
"trezor-connect": "~1.0.1"
}
}

View File

@ -55,6 +55,12 @@ echo $CMD
$CMD
checkOK
cd $BUILDDIR/../..
CMD="rsync -rLRv ./bower_components/trezor-connect/chrome/* $APPDIR"
echo $CMD
$CMD
checkOK
# Zipping chrome-extension
echo "${OpenColor}${Green}* Zipping all chrome-extension files...${CloseColor}"
cd $BUILDDIR

View File

@ -6,7 +6,8 @@
"permissions": [
"storage",
"notifications",
"videoCapture"
"videoCapture",
"webview"
],
"app": {
"background": {

View File

@ -56,21 +56,19 @@
</span>
</div>
<div class="row">
<div class="large-12 columns" ng-hide="create.hideWalletName">
<label><span translate>Wallet name</span>
<div class="input">
<input type="text" placeholder="{{'Family vacation funds'|translate}}" class="form-control" name="walletName" ng-model="walletName" ng-required="true" ng-focus="create.formFocus('wallet-name')" ng-blur="create.formFocus(false)">
</div>
</label>
</div>
<div class="large-12 columns" ng-show="totalCopayers != 1">
<label><span translate>Your nickname</span>
<div class="input">
<input type="text" placeholder="{{'John'|translate}}" class="form-control" name="myName" ng-model="myName" ng-required="totalCopayers != 1" ng-disabled="totalCopayers == 1" ng-focus="create.formFocus('my-name')" ng-blur="create.formFocus(false)">
</div>
</label>
</div>
<div ng-hide="create.hideWalletName">
<label><span translate>Wallet name</span>
<div class="input">
<input type="text" placeholder="{{'Family vacation funds'|translate}}" class="form-control" name="walletName" ng-model="walletName" ng-required="true" ng-focus="create.formFocus('wallet-name')" ng-blur="create.formFocus(false)">
</div>
</label>
</div>
<div ng-show="totalCopayers != 1">
<label><span translate>Your nickname</span>
<div class="input">
<input type="text" placeholder="{{'John'|translate}}" class="form-control" name="myName" ng-model="myName" ng-required="totalCopayers != 1" ng-disabled="totalCopayers == 1" ng-focus="create.formFocus('my-name')" ng-blur="create.formFocus(false)">
</div>
</label>
</div>
<div class="row" ng-show="totalCopayers != 1">
<div class="large-6 medium-6 columns">
@ -98,74 +96,80 @@
<i ng-if="!hideAdv" class="icon-arrow-up4"></i>
</a>
</div>
<div ng-hide="hideAdv" class="row">
<div class="large-12 columns">
<label ng-show="index.isChromeApp && totalCopayers > 1 " for="hw-ledger" class="oh">
<span translate>Use Ledger hardware wallet</span>
<switch id="hw-ledger" name="hwLedger" ng-model="hwLedger" ng-change="isTestnet=false" class="green right m5t m10b"></switch>
</label>
<!-- -->
<label ng-show="!index.isCordova && 0" for="hw-trezor" class="oh">
<span translate>Use TREZOR hardware wallet</span>
<switch id="hw-trezor" name="hwTrezor" ng-model="hwTrezor" class="green right m5t m10b"></switch>
</label>
<!-- TODO account
<div ng-show="hwLedger">
<label class="oh"><span translate>Ledger Slot</span>
<select class="m10t" ng-model="externalIndex" ng-options="externalIndex as externalIndex for externalIndex in create.externalIndexValues">
</select>
<div>
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
<div class="oh text-gray line-b size-12 p10b m20b"><span translate>Ledger supports up to 20 Copay wallets simultaneously. Select which slot should be used to host this wallet</div>
</div>
-->
<label for="network-name" class="oh" ng-show="!hwLedger && !hwTrezor">
<span translate>Testnet</span>
<switch id="network-name" name="isTestnet" ng-model="isTestnet" class="green right m5t m10b"></switch>
</label>
<label ng-show="!hwLedger && !hwTrezor" for="seed" class="oh">
<span translate>Specify your wallet seed</span>
<switch id="seed" name="setSeed" ng-model="setSeed" class="green right m5t m10b"></switch>
</label>
<div>
<label><span translate>Wallet Seed</span>
<select class="m10t" ng-model="seedSource"
ng-options="seed as seed.label for seed in create.seedOptions"
ng-change="create.setSeedSource()">
</select>
</label>
</div>
<label for="createPassphrase" class="oh" ng-hide="setSeed || hwLedger || hwTrezor" ><span translate>Add a Seed Passphrase</span> <small translate>Add an optional passphrase to secure the seed</small>
<div class="input">
<input type="text" class="form-control"
name="createPassphrase" ng-model="createPassphrase">
</div>
</label>
<div ng-show="create.seedSourceId == 'trezor' || create.seedSourceId == 'ledger'">
<label class="oh"><span translate>Account Number</span>
<input type="number" id="account" ng-model="account">
</label>
</div>
<label for="ext-master" class="m10t" ng-show="setSeed">
<span translate>Wallet Seed</span>
<small translate>Enter the seed words (BIP39)</small>
<input id="ext-master"
type="text"
name="privateKey" ng-model="privateKey">
</label>
<label for="passphrase" class="oh" ng-show="setSeed"><span translate>Seed Passphrase</span> <small translate>The seed could require a passphrase to be imported</small>
<div class="input">
<input type="text" class="form-control" name="passphrase" ng-model="passphrase">
</div>
</label>
<div class="box-notification" ng-show="create.seedSourceId=='new' && createPassphrase">
<span class="text-warning size-14">
<i class="fi-alert"></i>
<span translate>
WARNING: Passphrase cannot be recovered. <b>Be sure to write it down</b>. The wallet can not be restored without the passphrase.
</span>
</span>
</div>
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
</div>
<div ng-show="create.seedSourceId=='new' ">
<label for="createPassphrase" ><span translate>Add a Seed Passphrase</span> <small translate>Add an optional passphrase to secure the seed</small>
<div class="input">
<input type="text" class="form-control"
name="createPassphrase" ng-model="createPassphrase">
</div>
</label>
</div>
<div class="box-notification" ng-show="!setSeed && createPassphrase">
<span class="text-warning size-14">
<i class="fi-alert"></i>
<span translate>
WARNING: Passphrase cannot be recovered. <b>Be sure to write it down</b>. The wallet can not be restored without the passphrase.
</span>
</span>
</div>
<div ng-show="create.seedSourceId=='set'">
<label for="ext-master">
<span translate>Wallet Seed</span>
<small translate>Enter the seed words (BIP39)</small>
<input id="ext-master"
type="text"
name="privateKey" ng-model="privateKey">
</label>
</div>
<div ng-show="create.seedSourceId=='set'">
<label for="passphrase"> <span translate>Seed Passphrase</span> <small translate>The seed could require a passphrase to be imported</small>
<div class="input">
<input type="text" class="form-control" name="passphrase" ng-model="passphrase">
</div>
</label>
</div>
<div ng-show="create.seedSourceId == 'set'">
<label class="oh"><span translate>Derivation Path</span> <small translate>BIP32 path for address derivation</small>
<input type="text" class="form-control" name="derivationPath" ng-model="derivationPath">
</label>
</div>
<div class="oh" ng-show="create.seedSourceId == 'new'">
<label for="network-name" >
<span translate>Testnet</span>
<switch id="network-name" name="isTestnet" ng-model="isTestnet" class="green right m5t m10b"></switch>
</label>
</div>
</div> <!-- columns -->
</div> <!-- advanced -->
<button type="submit" class="button round black expand m0" ng-show="totalCopayers != 1" ng-disabled="setupForm.$invalid || create.loading || create.hwWallet">
<span translate>Create {{requiredCopayers}}-of-{{totalCopayers}} wallet</span>
@ -174,8 +178,9 @@
<button type="submit" class="button round black expand m0" ng-show="totalCopayers == 1" ng-disabled="setupForm.$invalid || create.loading || create.hwWallet">
<span translate>Create new wallet</span>
</button>
</div>
</div>
</div> <!-- large-12 columns -->
</div> <!-- row -->
</form>
</div>
<div class="extra-margin-bottom"></div>

View File

@ -45,7 +45,7 @@
</form>
<h4></h4>
<ul class="no-bullet m0" ng-hide="index.isPrivKeyExternal">
<ul class="no-bullet m0" ng-show="index.canSign">
<li>
<switch id="no-sign" name="noSign" ng-model="noSign" class="green right"></switch>
<span translate>Do not include private key</span>
@ -53,7 +53,7 @@
</ul>
<div class="box-notification" ng-show="index.isPrivKeyExternal">
<div class="box-notification" ng-show="!index.canSign">
<span class="text-warning size-14">
<i class="fi-alert"></i>
<span translate>

View File

@ -30,8 +30,6 @@
</div>
</div>
<div class="create-tab pr small-only-text-center" ng-hide="create.hideTabs">
<div class="row">
<div class="tab-container small-4 medium-4 large-4" ng-class="{'selected': type =='12'}">
@ -46,21 +44,21 @@
</div>
</div>
<div class="row" ng-show="type == '12' ">
<div class="large-12 columns">
<form name="importForm12" ng-submit="import.importMnemonic(importForm12)" novalidate>
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<div class="row" ng-show="type == '12' ">
<div class="large-12 columns">
<form name="importForm12" ng-submit="import.importMnemonic(importForm12)" novalidate>
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<div >
<label for="words">
<span translate>Type the Seed Word (usually 12 words)</span>:
</label>
<textarea class="form-control" name="words" ng-model="import.words" rows="2"></textarea>
</div>
<div >
<label for="words">
<span translate>Type the Seed Word (usually 12 words)</span>:
</label>
<textarea class="form-control" name="words" ng-model="import.words" rows="2"></textarea>
</div>
<div class="m10t oh" ng-init="hideAdv=true">
<a class="button outline light-gray expand tiny" ng-click="hideAdv=!hideAdv">
@ -73,18 +71,78 @@
</div>
<div ng-hide="hideAdv" class="row">
<div class="large-12 columns">
<label for="network-name" class=" oh">
<span translate>Testnet</span>
<switch id="network-name" name="isTestnet" ng-model="isTestnet" class="green right m5t m10b"></switch>
</label>
<label for="passphrase" class="oh"><span translate>Passphrase</span> <small translate>Wallet Seed could require a passphrase to be imported</small>
<label for="passphrase" class="oh"><span translate>Passphrase</span> <small translate>Wallet Seed could require a passphrase to be imported</small>
<div class="input">
<input type="password" class="form-control" placeholder="{{'Seed passphrase'|translate}}"
name="passphrase" ng-model="import.passphrase">
</div>
</label>
<div>
<label class="oh"><span translate>Derivation Path</span> <small translate>BIP32 path for address derivation</small>
<input type="text" class="form-control" name="derivationPath" ng-model="derivationPath">
</label>
</div>
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
</div>
<button translate type="submit" class="button round expand black m10t"
ng-disabled="importForm12.$invalid || import.loading">
Import
</button>
</form>
</div>
</div>
<div class="row" ng-show="type == 'file' ">
<div class="large-12 columns">
<form name="importForm" ng-submit="import.importBlob(importForm)" novalidate>
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<div ng-show="!index.isSafari && !index.isCordova" class="line-b m10b">
<label for="backupFile">
<span translate>Choose a backup file from your computer</span> <i class="fi-laptop"></i>
</label>
<input type="file" class="form-control" placeholder="{{'Select a backup file'|translate}}"
name="backupFile" ng-model="import.backupFile" ng-file-select>
</div>
<div ng-show="index.isSafari || index.isCordova">
<label for="backupText">
<span translate>Paste the backup plain text code</span> <i class="fi-clipboard"></i>
</label>
<textarea class="form-control" name="backupText" ng-model="import.backupText" rows="5"></textarea>
</div>
<label for="password"><span translate>Password</span>
</label>
<div class="input">
<input type="password" class="form-control" placeholder="{{'Your backup password'|translate}}"
name="password" ng-model="import.password">
</div>
<div class="m10t oh" ng-init="hideAdv=true">
<a class="button outline light-gray expand tiny" ng-click="hideAdv=!hideAdv">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdv">Show advanced options</span>
<span translate ng-hide="hideAdv">Hide advanced options</span>
<i ng-if="hideAdv" class="icon-arrow-down4"></i>
<i ng-if="!hideAdv" class="icon-arrow-up4"></i>
</a>
</div>
<div ng-hide="hideAdv" class="row">
<div class="large-12 columns">
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
@ -92,45 +150,57 @@
</div>
</div>
<button translate type="submit" class="button round expand black m10t"
ng-disabled="importForm12.$invalid || import.loading">
Import
</button>
</form>
</div>
<button translate type="submit" class="button round expand black"
ng-disabled="importForm.$invalid || !import.password || import.loading">
Import backup
</button>
</form>
<div class="text-center text-gray p20v" ng-click="$root.go('importLegacy')">
<p class="text-gray m5b size-14" translate> Have a Backup from Copay v0.9?</p>
<button class=" outline dark-gray tiny round"> <span translate>Import here</span>
<i class="icon-arrow-right3 size-14"></i>
</button>
</div>
</div>
</div>
<div class="row" ng-show="type == 'hwWallet'">
<div class="large-12 columns">
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<div class="row" ng-show="type == 'file' ">
<div class="large-12 columns">
<form name="importForm" ng-submit="import.importBlob(importForm)" novalidate>
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<div ng-show="!index.isSafari && !index.isCordova" class="line-b m10b">
<label for="backupFile">
<span translate>Choose a backup file from your computer</span> <i class="fi-laptop"></i>
<form name="importForm3" ng-submit="import.importHW(importForm3)" novalidate>
<div class="large-12 columns">
<div ng-show="!import.seedOptions[0]">
<span translate>No harware wallets supported on this device</span>
</div>
<div ng-show="import.seedOptions[0]">
<div>
<label><span translate>Wallet Seed</span>
<select class="m10t" ng-model="seedSource"
ng-options="seed as seed.label for seed in import.seedOptions"
ng-change="import.setSeedSource()">
</select>
</label>
<input type="file" class="form-control" placeholder="{{'Select a backup file'|translate}}"
name="backupFile" ng-model="import.backupFile" ng-file-select>
</div>
<div ng-show="index.isSafari || index.isCordova">
<label for="backupText">
<span translate>Paste the backup plain text code</span> <i class="fi-clipboard"></i>
<div ng-show="import.seedSourceId == 'trezor' || import.seedSourceId == 'ledger'">
<label class="oh"><span translate>Account Number</span>
<input type="number" id="account" ng-model="account">
</label>
<textarea class="form-control" name="backupText" ng-model="import.backupText" rows="5"></textarea>
</div>
<label for="password"><span translate>Password</span>
</label>
<div class="input">
<input type="password" class="form-control" placeholder="{{'Your backup password'|translate}}"
name="password" ng-model="import.password">
<div ng-show="import.seedSourceId == 'trezor'">
<label for="isMultisig" class="oh">
<span translate>Shared Wallet</span>
<switch id="isMultisig" name="isMultisig" ng-model="isMultisig" class="green right m5t m10b"></switch>
</label>
</div>
<div class="m10t oh" ng-init="hideAdv=true">
@ -152,100 +222,16 @@
</div>
</div>
<button translate type="submit" class="button round expand black"
ng-disabled="importForm.$invalid || !import.password || import.loading">
Import backup
</button>
</form>
<div class="text-center text-gray p20v" ng-click="$root.go('importLegacy')">
<p class="text-gray m5b size-14" translate> Have a Backup from Copay v0.9?</p>
<button class=" outline dark-gray tiny round"> <span translate>Import here</span>
<i class="icon-arrow-right3 size-14"></i>
</button>
</div>
</div>
</div>
<div class="row" ng-show="type == 'hwWallet'">
<div class="large-12 columns">
<div class="box-notification" ng-show="import.error">
<span class="text-warning size-14">
{{import.error|translate}}
</span>
</div>
<form name="importForm3" ng-submit="import.importLedger(importForm3)" ng-show="index.isChromeApp" novalidate>
<div class="large-12 columns">
<!-- TODO: account
<label class=" oh">
<span translate>Ledger Slot</span>
<select class="m10t" ng-model="externalIndex" ng-options="externalIndex as externalIndex for externalIndex in import.externalIndexValues">
</select>
</label>
<div class="oh text-gray line-b size-12 p10b m20b"><span translate>Ledger supports up to 20 Copay wallets simultaneously. Select which slot to import</div>
-->
<button translate type="submit" class="button round expand black"
ng-disabled="import.loading || import.ledger">
Import from Ledger
Import
</button>
<div class="m10t oh" ng-init="hideAdvLedger=true">
<a class="button outline light-gray expand tiny" ng-click="hideAdvLedger=!hideAdvLedger">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdvLedger">Show advanced options</span>
<span translate ng-hide="hideAdvLedger">Hide advanced options</span>
<i ng-if="hideAdvLedger" class="icon-arrow-down4"></i>
<i ng-if="!hideAdvLedger" class="icon-arrow-up4"></i>
</a>
</div>
<div ng-hide="hideAdvLedger" class="row">
<div class="large-12 columns">
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
</div>
</div>
</form>
<form name="importForm4" ng-submit="import.importTrezor(importForm4)" novalidate>
<div class="large-12 columns">
<div class="m10t oh" ng-init="hideAdvTrezor=true">
<a class="button outline light-gray expand tiny" ng-click="hideAdvTrezor=!hideAdvTrezor">
<i class="fi-widget m3r"></i>
<span translate ng-hide="!hideAdvTrezor">Show advanced options</span>
<span translate ng-hide="hideAdvTrezor">Hide advanced options</span>
<i ng-if="hideAdvTrezor" class="icon-arrow-down4"></i>
<i ng-if="!hideAdvTrezor" class="icon-arrow-up4"></i>
</a>
</div>
<div ng-hide="hideAdvTrezor" class="row">
<div class="large-12 columns">
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
</div>
<!-- ng-disabled="import.loading || import.ledger" -->
<button translate type="submit" class="button round expand black" ng-disabled="true">
Import from TREZOR
</button>
</div>
</form>
</div>
</div> <!-- seedoptions show -->
</div>
</form>
</div>
</div>
</div>
<div class="extra-margin-bottom"></div>

View File

@ -1,6 +1,16 @@
<span ng-show="index.network != 'livenet'"> Testnet </span>
<span ng-show="!index.canSign && !index.isPrivKeyExternal" translate>No Private key</span>
<div ng-show="index.isPrivKeyExternal" style="text-transform: capitalize">
<span translate>External Private Key:</span>
{{index.externalSource}}
</div>
<span ng-show="index.isShared"><span translate>{{index.m}}-of-{{index.n}}</span></span>
<img style="height:1em" ng-show="index.network != 'livenet'" src="img/icon-testnet.svg">
<img style="height:1em" ng-show="!index.canSign && !index.isPrivKeyExternal" src="img/icon-read-only.svg">
<img style="height:1em" ng-show="index.externalSource == 'trezor'" src="img/icon-trezor.svg">
<img style="height:1em" ng-show="index.externalSource == 'ledger'" src="img/icon-ledger.svg">
<span style="height:1em" ng-show="index.account">#{{index.account || 0}} </span>
<img style="height:1em" ng-show="index.isPrivKeyEncrypted" src="img/icon-lock.svg">
<!-- <img style="height:1em" ng&#45;show="index.preferences.email" src="img/icon&#45;email.svg"> -->
<img style="height:1em" ng-show="index.usingCustomBWS" src="img/icon-bws.svg">
<img style="height:1em" class="animated flash infinite" ng-show="index.updatingTxHistory[index.walletId]" src="img/icon-sync.svg">

View File

@ -32,118 +32,128 @@
</div>
</div>
<div class="row">
<div class="large-12 columns">
<form name="joinForm" ng-submit="join.join(joinForm)" novalidate>
<div class="row">
<div class="large-12 columns">
<div class="box-notification" ng-show="join.error ">
<span class="text-warning size-14">
{{join.error|translate}}
</span>
</div>
<div class="box-notification" ng-show="join.error ">
<span class="text-warning size-14">
{{join.error|translate}}
</span>
</div>
<form name="joinForm" ng-submit="join.join(joinForm)" novalidate>
<div>
<label><span translate>Your nickname</span>
<div class="input">
<input type="text" placeholder="{{'John'|translate}}" class="form-control" name="myName" ng-model="myName" ng-required="true">
</div>
</label>
</div>
<div class="row collapse">
<label for="secret" class="left"><span translate>Wallet Invitation</span>
<small translate ng-show="joinForm.secret.$pristine">Required</small>
</label>
<span class="has-error right size-12" ng-show="joinForm.secret.$invalid
&& !joinForm.secret.$pristine">
<span class="icon-input"><i class="fi-x"></i></span>
<span translate>Wallet Invitation is not valid!</span>
</span>
<small class="icon-input right" ng-show="joinForm.secret.$valid
&& !joinForm.secret.$pristine"><i class="fi-check"></i></small>
<div class="row collapse">
<label for="secret" class="left"><span translate>Wallet Invitation</span>
<small translate ng-show="joinForm.secret.$pristine">Required</small>
</label>
<span class="has-error right size-12" ng-show="joinForm.secret.$invalid
&& !joinForm.secret.$pristine">
<span class="icon-input"><i class="fi-x"></i></span>
<span translate>Wallet Invitation is not valid!</span>
</span>
<small class="icon-input right" ng-show="joinForm.secret.$valid
&& !joinForm.secret.$pristine"><i class="fi-check"></i></small>
</div>
<div class="input">
<input id="secret" type="text" placeholder="{{'Paste invitation here'|translate}}" name="secret" ng-model="secret" wallet-secret required>
<div class="qr-scanner-input">
<qr-scanner on-scan="join.onQrCodeScanned(data)"></qr-scanner>
</div>
</div>
<div class="input">
<input id="secret" type="text" placeholder="{{'Paste invitation here'|translate}}" name="secret" ng-model="secret" wallet-secret required>
<div class="qr-scanner-input">
<qr-scanner on-scan="join.onQrCodeScanned(data)"></qr-scanner>
</div>
</div>
<a class="button outline light-gray tiny expand" ng-click="join.hideAdv=!join.hideAdv">
<div class="m10t oh" ng-init="hideAdv=true">
<a class="button outline light-gray expand tiny" ng-click="hideAdv=!hideAdv">
<i class="fi-widget m3r"></i>
<span translate ng-show="!join.hideAdv">Show advanced options</span>
<span translate ng-show="join.hideAdv">Hide advanced options</span>
<i ng-show="!join.hideAdv" class="icon-arrow-down4"></i>
<i ng-show="join.hideAdv" class="icon-arrow-up4"></i>
<span translate ng-hide="!hideAdv">Show advanced options</span>
<span translate ng-hide="hideAdv">Hide advanced options</span>
<i ng-if="hideAdv" class="icon-arrow-down4"></i>
<i ng-if="!hideAdv" class="icon-arrow-up4"></i>
</a>
<div ng-show="join.hideAdv" class="row">
<div class="large-12 columns">
</div>
<label for="hw-ledger" class="oh" ng-show="index.isChromeApp">
<span translate>Use Ledger hardware wallet</span>
<switch id="hw-ledger" name="hwLedger" ng-model="hwLedger" class="green right m5t m10b"></switch>
</label>
<div ng-hide="hideAdv" class="row">
<div class="large-12 columns">
<div>
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
<label ng-show="!index.isCordova && 0" for="hw-trezor" class="oh">
<span translate>Use TREZOR hardware wallet</span>
<switch id="hw-trezor" name="hwTrezor" ng-model="hwTrezor" class="green right m5t m10b"></switch>
</label>
<div>
<label><span translate>Wallet Seed</span>
<select class="m10t" ng-model="seedSource"
ng-options="seed as seed.label for seed in join.seedOptions"
ng-change="join.setSeedSource()">
</select>
</label>
</div>
<!-- TODO account
<div class="large-12 columns" ng-hide="!hwLedger">
<label class="oh">
<span translate>Ledger Slot</span>
<select class="m10t" ng-model="externalIndex" ng-options="externalIndex as externalIndex for externalIndex in join.externalIndexValues">
<div ng-show="join.seedSourceId == 'trezor' || join.seedSourceId == 'ledger'">
<label class="oh"><span translate>Account</span>
<select class="m10t" ng-model="account" ng-options="externalIndex as externalIndex for externalIndex in join.accountValues">
</select>
</label>
<div class="oh text-gray line-b size-12 p10b m20b"><span translate>Ledger supports up to 20 Copay wallets simultaneously. Select which slot should be used to host this wallet</div>
<div class="oh text-gray line-b size-12 p10b m20b"><span translate>Multiple wallets accounts are supported on the device simultaneously. Select which account should be used for this wallet</div>
</div>
-->
<label ng-show="!hwLedger && !hwTrezor" for="seed" class="oh">
<span translate>Specify your wallet seed</span>
<switch id="seed" name="setSeed" ng-model="setSeed" class="green right m5t m10b"></switch>
</label>
<label for="createPassphrase" class="oh" ng-show="!setSeed && !hwLedger && !hwTrezor" ><span translate>Add a Seed Passphrase</span> <small translate>Add an optional passphrase to secure the seed</small>
<div class="input">
<input type="text" class="form-control"
name="createPassphrase" ng-model="createPassphrase">
</div>
</label>
<div class="box-notification" ng-show="join.seedSourceId=='new' && createPassphrase">
<span class="text-warning size-14">
<i class="fi-alert"></i>
<span translate>
WARNING: Passphrase cannot be recovered. <b>Be sure to write it down</b>. The wallet can not be restored without the passphrase.
</span>
</span>
</div>
<label for="ext-master" class="m10t" ng-show="setSeed">
<span translate>Wallet Seed</span>
<small translate>Enter the seed words (BIP39)</small>
<input id="ext-master"
type="text"
name="privateKey" ng-model="privateKey">
</label>
<label for="passphrase" class="oh" ng-show="setSeed"><span translate>Seed Passphrase</span> <small translate>The seed could require a passphrase to be imported</small>
<div class="input">
<input type="text" class="form-control" name="passphrase" ng-model="passphrase">
</div>
</label>
<div ng-show="join.seedSourceId=='new' ">
<label for="createPassphrase" ><span translate>Add a Seed Passphrase</span> <small translate>Add an optional passphrase to secure the seed</small>
<div class="input">
<input type="text" class="form-control"
name="createPassphrase" ng-model="createPassphrase">
</div>
</label>
</div>
<label for="bws" class="oh">
<span>Wallet Service URL</span>
<input type="text" id="bwsurl" name="bwsurl" ng-model="bwsurl">
</label>
</div>
</div>
<div ng-show="join.seedSourceId=='set'">
<label for="ext-master">
<span translate>Wallet Seed</span>
<small translate>Enter the seed words (BIP39)</small>
<input id="ext-master"
type="text"
name="privateKey" ng-model="privateKey">
</label>
</div>
<div class="box-notification" ng-show="!setSeed && createPassphrase">
<span class="text-warning size-14">
<i class="fi-alert"></i>
<span translate>
WARNING: Passphrase cannot be recovered. <b>Be sure to write it down</b>. The wallet can not be restored without the passphrase.
</span>
</span>
</div>
<div ng-show="join.seedSourceId=='set'">
<label for="passphrase"> <span translate>Seed Passphrase</span> <small translate>The seed could require a passphrase to be imported</small>
<div class="input">
<input type="text" class="form-control" name="passphrase" ng-model="passphrase">
</div>
</label>
</div>
<div ng-show="join.seedSourceId == 'set'">
<label class="oh"><span translate>Derivation Path</span> <small translate>BIP32 path for address derivation</small>
<input type="text" class="form-control" name="derivationPath" ng-model="derivationPath">
</label>
</div>
</div> <!-- columns -->
</div> <!-- advanced -->
<button translate type="submit" class="button expand black m0 round"
ng-disabled="joinForm.$invalid || join.loading">Join</button>
</form>
</div>
</div>
<button translate type="submit" class="button expand black m0 round"
ng-disabled="joinForm.$invalid || join.loading">Join</button>
</div> <!-- large-12 columns -->
</div> <!-- row -->
</form>
</div>
<div class="extra-margin-bottom"></div>

View File

@ -1,28 +0,0 @@
<nav class="tab-bar">
<section class="left-small">
<a ng-click="cancel()" class="p10">
<span class="text-close" translate>Close</span>
</a>
</section>
<section class="middle tab-bar-section">
<h1 class="title ellipsis" ng-style="{'color':color}" translate>
Copayers
</h1>
</section>
</nav>
<div class="modal-content fix-modals-touch">
<ul class="no-bullet">
<li class="line-b p10" ng-repeat="copayer in copayers">
<span class="size-12" ng-show="copayer.id == copayerId">
<i class="icon-contact size-24 m10r"></i> {{copayer.name}} ({{'Me'|translate}}) <i class="fi-check m5 right"></i>
</span>
<span class="size-12 text-gray" ng-show="copayer.id != copayerId">
<i class="icon-contact size-24 m10r"></i> {{copayer.name}}<i class="fi-check m5 right"></i>
</span>
</li>
</ul>
<div class="extra-margin-bottom"></div>
</div>

View File

@ -62,12 +62,12 @@
<div translate>Advanced</div>
</li>
</ul>
<div ng-show="!index.noFocusedWallet && (!index.isPrivKeyExternal || preferences.touchidAvailable)">
<h4 translate>
<div ng-show="!index.noFocusedWallet && index.canSign">
<h4 translate ng-show="index.canSign">
Spending Restrictions
</h4>
<ul class="no-bullet m0 ">
<li ng-show="!index.isPrivKeyExternal">
<ul class="no-bullet m0">
<li>
<switch id="network-name" name="encrypt" ng-model="encrypt" class="green right"></switch>
<div translate>Request Password</div>
</li>

View File

@ -15,6 +15,7 @@
</span>
</li>
<li class="line-b p20 oh">
<span translate>Wallet Id</span>
<span class="right text-gray enable_text_select">
@ -29,6 +30,8 @@
</span>
</li>
<li class="line-b p20 oh">
<span translate>Wallet Network</span>
<span class="right text-gray">
@ -52,6 +55,39 @@
</span>
</li>
<li class="line-b p20 oh" ng-show="index.externalSource">
<span translate>Hardware Wallet</span>
<span class="right text-gray capitalize">
{{index.externalSource}}
</span>
</li>
<li class="line-b p20 oh" ng-show="!index.externalSource && !index.canSign">
<span translate></span>
<span class="right text-gray capitalize">
No private key
</span>
</li>
<li class="line-b p20 oh" ng-show="index.account">
<span translate>Account ({{derivationStrategy}})</span>
<span class="right text-gray">
#{{index.account}}
</span>
</li>
<h4 class="title m0" translate>Copayers</h4>
<li ng-repeat="copayer in index.copayers">
<span class="size-12" ng-show="copayer.id == index.copayerId">
<i class="icon-contact size-24 m10r"></i> {{copayer.name}} ({{'Me'|translate}}) <i class="fi-check m5 right"></i>
</span>
<span class="size-12 text-gray" ng-show="copayer.id != index.copayerId">
<i class="icon-contact size-24 m10r"></i> {{copayer.name}}<i class="fi-check m5 right"></i>
</span>
</li>
<h4 class="title m0" translate>Extended Public Keys</h4>
<li ng-repeat="pk in pubKeys">
<div class="row collapse">

View File

@ -118,13 +118,11 @@
</a>
</div>
<div class="wallet-info">
<div ng-show="index.isShared" ng-click="openCopayersModal(index.copayers, index.copayerId)">
<div ng-show="index.isShared">
<p class="m0">
{{(index.alias || index.walletName)}}
</p>
<div class="size-12 text-gray">
<span translate>Multisignature wallet</span>
(<span translate>{{index.m}}-of-{{index.n}}</span>)
<span ng-include="'views/includes/walletInfo.html'"></span>
</div>
</div>
@ -444,7 +442,7 @@
-->
<div id="history" class="history tab-view">
<div class="row m20t" ng-show="!index.txHistory[0] && !index.updatingTxHistory">
<div class="row m20t" ng-show="!index.txHistory[0] && !index.updatingTxHistory[index.walletId]">
<div class="large-12 columns">
<div class="oh text-center">
<span ng-show="index.txHistoryError && !index.notAuthorized" ng-click='index.updateTxHistory()'>
@ -457,7 +455,7 @@
</div>
</div>
</div>
<div class="row m20t text-center" ng-show="index.updatingTxHistory">
<div class="row m20t text-center" ng-show="index.updatingTxHistory[index.walletId]">
<div class="columns large-12 medium-12 small-12">
<div class="spinner">
<div class="rect1"></div>
@ -467,18 +465,16 @@
<div class="rect5"></div>
</div>
</div>
<p ng-show="index.showWaitingSign" translate class="size-12 text-gray">
<div ng-show="index.txProgress > 6" translate class="size-12 text-gray m20t">
Initial transaction history synchronization can take some minutes for wallets with many transactions.</br>
Please stand by.
</p>
</div>
<div ng-if="!index.isCordova && index.txHistory[0] && !index.updatingTxHistory" class="m20t text-center">
<input id="export_file" type="file" nwsaveas="Copay-{{index.alias || index.walletName}}.csv" accept=".csv" style="display:none">
<a class="text-gray size-12" ng-click="index.csvHistory();">
<i class="fi-page-export-csv"></i>
<span translate>Download CSV file</span>
</a>
</div>
<div ng-show="index.txProgress > 6" translate class="size-14 text-gray m20t">
<b>{{index.txProgress}}</b> Transactions<br>
Downloaded
</div>
</div>
<div ng-show="index.txHistory[0]">
<div ng-repeat="btx in index.txHistory"
ng-click="home.openTxModal(btx)"
@ -522,6 +518,33 @@
</div>
</div>
</div>
<div class="row m20t text-center" ng-show="index.historyRendering">
<div class="columns large-12 medium-12 small-12">
<div class="spinner">
<div class="rect1"></div>
<div class="rect2"></div>
<div class="rect3"></div>
<div class="rect4"></div>
<div class="rect5"></div>
</div>
</div>
</div>
<div class="row m20t">
<div class="large-6 medium-6 small-6 columns">
<button type="submit" class="button black round expand" ng-show="index.historyShowShowAll" ng-click="index.showAllHistory()" ng-style="{'background-color':index.backgroundColor}" translate>
<span translate>Show All</span>
</button>
</div>
<div class="large-6 medium-6 small-6 columns" ng-show="!index.isCordova">
<input id="export_file" type="file" nwsaveas="Copay-{{index.alias || index.walletName}}.csv" accept=".csv" style="display:none">
<a class="button outline dark-gray round" ng-click="index.csvHistory();">
<i class="fi-page-export-csv"></i>
<span translate>Download CSV file</span>
</a>
</div>
</div>
</div>
<div class="extra-margin-bottom"></div>
</div> <!-- END History -->

View File

@ -1,11 +1,12 @@
'use strict';
angular.module('copayApp.controllers').controller('createController',
function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isCordova, gettext, ledger, trezor, isMobile) {
function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isCordova, gettext, ledger, trezor, isMobile, isChromeApp, isDevel, derivationPathHelper) {
var self = this;
var defaults = configService.getDefaults();
this.isWindowsPhoneApp = isMobile.Windows() && isCordova;
$scope.account = 1;
/* For compressed keys, m*73 + n*34 <= 496 */
var COPAYER_PAIR_LIMITS = {
@ -25,6 +26,7 @@ angular.module('copayApp.controllers').controller('createController',
var defaults = configService.getDefaults();
$scope.bwsurl = defaults.bws.url;
$scope.derivationPath = derivationPathHelper.default;
// ng-repeat defined number of times instead of repeating over array?
this.getNumber = function(num) {
@ -38,11 +40,47 @@ angular.module('copayApp.controllers').controller('createController',
$scope.requiredCopayers = Math.min(parseInt(n / 2 + 1), maxReq);
};
var updateSeedSourceSelect = function(n) {
self.seedOptions = [{
id: 'new',
label: gettext('New Random Seed'),
}, {
id: 'set',
label: gettext('Specify Seed...'),
}];
$scope.seedSource = self.seedOptions[0];
if (n > 1 && isChromeApp)
self.seedOptions.push({
id: 'ledger',
label: gettext('Ledger Hardware Wallet'),
});
if (isChromeApp || isDevel) {
self.seedOptions.push({
id: 'trezor',
label: gettext('Trezor Hardware Wallet'),
});
}
};
this.TCValues = lodash.range(2, defaults.limits.totalCopayers + 1);
$scope.totalCopayers = defaults.wallet.totalCopayers;
this.setTotalCopayers = function(tc) {
updateRCSelect(tc);
updateSeedSourceSelect(tc);
self.seedSourceId = $scope.seedSource.id;
};
this.setSeedSource = function(src) {
self.seedSourceId = $scope.seedSource.id;
$timeout(function() {
$rootScope.$apply();
});
};
this.create = function(form) {
@ -50,23 +88,36 @@ angular.module('copayApp.controllers').controller('createController',
this.error = gettext('Please enter the required fields');
return;
}
var opts = {
m: $scope.requiredCopayers,
n: $scope.totalCopayers,
name: form.walletName.$modelValue,
myName: $scope.totalCopayers > 1 ? form.myName.$modelValue : null,
networkName: form.isTestnet.$modelValue ? 'testnet' : 'livenet',
bwsurl: $scope.bwsurl
bwsurl: $scope.bwsurl,
};
var setSeed = form.setSeed.$modelValue;
var setSeed = self.seedSourceId == 'set';
if (setSeed) {
var words = form.privateKey.$modelValue;
var words = form.privateKey.$modelValue || '';
if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) {
opts.extendedPrivateKey = words;
} else {
opts.mnemonic = words;
}
opts.passphrase = form.passphrase.$modelValue;
var pathData = derivationPathHelper.parse($scope.derivationPath);
if (!pathData) {
this.error = gettext('Invalid derivation path');
return;
}
opts.account = pathData.account;
opts.networkName = pathData.networkName;
opts.derivationStrategy = pathData.derivationStrategy;
} else {
opts.passphrase = form.createPassphrase.$modelValue;
}
@ -76,14 +127,21 @@ angular.module('copayApp.controllers').controller('createController',
return;
}
if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) {
self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR';
if (self.seedSourceId == 'ledger' || self.seedSourceId == 'trezor') {
var account = $scope.account;
if (!account || account < 1) {
this.error = gettext('Invalid account number');
return;
}
var src = form.hwLedger.$modelValue ? ledger : trezor;
if ( self.seedSourceId == 'trezor')
account = account - 1;
// TODO : account
var account = 0;
src.getInfoForNewWallet(account, function(err, lopts) {
opts.account = account;
self.hwWallet = self.seedSourceId == 'ledger' ? 'Ledger' : 'Trezor';
var src = self.seedSourceId == 'ledger' ? ledger : trezor;
src.getInfoForNewWallet(opts.n > 1, account, function(err, lopts) {
self.hwWallet = false;
if (err) {
self.error = err;
@ -141,4 +199,7 @@ angular.module('copayApp.controllers').controller('createController',
$scope.$on("$destroy", function() {
$rootScope.hideWalletNavigation = false;
});
updateSeedSourceSelect(1);
self.setSeedSource('new');
});

View File

@ -1,12 +1,14 @@
'use strict';
angular.module('copayApp.controllers').controller('importController',
function($scope, $rootScope, $location, $timeout, $log, profileService, configService, notification, go, sjcl, gettext, lodash, ledger, trezor) {
function($scope, $rootScope, $location, $timeout, $log, profileService, configService, notification, go, sjcl, gettext, lodash, ledger, trezor, isChromeApp, isDevel, derivationPathHelper) {
var self = this;
var reader = new FileReader();
var defaults = configService.getDefaults();
$scope.bwsurl = defaults.bws.url;
$scope.derivationPath = derivationPathHelper.default;
$scope.account = 1;
window.ignoreMobilePause = true;
$scope.$on('$destroy', function() {
@ -15,6 +17,27 @@ angular.module('copayApp.controllers').controller('importController',
}, 100);
});
var updateSeedSourceSelect = function() {
self.seedOptions = [];
if (isChromeApp) {
self.seedOptions.push({
id: 'ledger',
label: gettext('Ledger Hardware Wallet'),
});
}
if (isChromeApp || isDevel) {
self.seedOptions.push({
id: 'trezor',
label: gettext('Trezor Hardware Wallet'),
});
$scope.seedSource = self.seedOptions[0];
}
};
this.setType = function(type) {
$scope.type = type;
this.error = null;
@ -173,23 +196,23 @@ angular.module('copayApp.controllers').controller('importController',
}
opts.passphrase = form.passphrase.$modelValue || null;
opts.networkName = form.isTestnet.$modelValue ? 'testnet' : 'livenet';
var pathData = derivationPathHelper.parse($scope.derivationPath);
if (!pathData) {
this.error = gettext('Invalid derivation path');
return;
}
opts.account = pathData.account;
opts.networkName = pathData.networkName;
opts.derivationStrategy = pathData.derivationStrategy;
_importMnemonic(words, opts);
};
this.importTrezor = function(form) {
this.importTrezor = function(account, isMultisig) {
var self = this;
if (form.$invalid) {
this.error = gettext('There is an error in the form');
$timeout(function() {
$scope.$apply();
});
return;
}
self.hwWallet = 'Trezor';
// TODO account
trezor.getInfoForNewWallet(0, function(err, lopts) {
trezor.getInfoForNewWallet(isMultisig, account, function(err, lopts) {
self.hwWallet = false;
if (err) {
self.error = err;
@ -217,18 +240,53 @@ angular.module('copayApp.controllers').controller('importController',
}, 100);
};
this.importLedger = function(form) {
var self = this;
if (form.$invalid) {
this.importHW = function(form) {
if (form.$invalid || $scope.account < 0 ) {
this.error = gettext('There is an error in the form');
$timeout(function() {
$scope.$apply();
});
return;
}
self.hwWallet = 'Ledger';
// TODO account
ledger.getInfoForNewWallet(0, function(err, lopts) {
this.error = '';
var account = + $scope.account;
if (self.seedSourceId == 'trezor') {
if ( account < 1) {
this.error = gettext('Invalid account number');
return;
}
account = account - 1;
}
var isMultisig = form.isMultisig.$modelValue;
switch (self.seedSourceId) {
case ('ledger'):
self.hwWallet = 'Ledger';
self.importLedger(account);
break;
case ('trezor'):
self.hwWallet = 'Trezor';
self.importTrezor(account, isMultisig);
break;
default:
throw ('Error: bad source id');
};
};
this.setSeedSource = function() {
if (!$scope.seedSource) return;
self.seedSourceId = $scope.seedSource.id;
$timeout(function() {
$rootScope.$apply();
});
};
this.importLedger = function(account) {
var self = this;
ledger.getInfoForNewWallet(true, account, function(err, lopts) {
self.hwWallet = false;
if (err) {
self.error = err;
@ -255,4 +313,6 @@ angular.module('copayApp.controllers').controller('importController',
}, 100);
};
updateSeedSourceSelect();
self.setSeedSource('new');
});

View File

@ -7,7 +7,8 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.isChromeApp = isChromeApp;
self.isSafari = isMobile.Safari();
self.onGoingProcess = {};
self.limitHistory = 6;
self.historyShowLimit = 10;
self.updatingTxHistory = {};
function strip(number) {
return (parseFloat(number.toPrecision(12)));
@ -83,7 +84,9 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.currentFeeLevel = null;
self.notAuthorized = false;
self.txHistory = [];
self.txHistoryUnique = {};
self.completeHistory = [];
self.txProgress = 0;
self.historyShowShowAll = false;
self.balanceByAddress = null;
self.pendingTxProposalsCountForUs = null;
self.setSpendUnconfirmed();
@ -106,7 +109,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.isComplete = fc.isComplete();
self.canSign = fc.canSign();
self.isPrivKeyExternal = fc.isPrivKeyExternal();
self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted();
self.externalSource = fc.getPrivKeyExternalSourceName();
self.account = fc.credentials.account;
if (self.externalSource == 'trezor')
self.account++;
self.txps = [];
self.copayers = [];
self.updateColor();
@ -115,6 +124,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.initGlidera();
self.setCustomBWSFlag();
if (fc.isPrivKeyExternal()) {
self.needsBackup = false;
self.openWallet();
@ -127,6 +137,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r
});
};
self.setCustomBWSFlag = function() {
var defaults = configService.getDefaults();
var config = configService.getSync();
self.usingCustomBWS = config.bwsFor && (config.bwsFor[self.walletId] != defaults.bws.url);
};
self.setTab = function(tab, reset, tries, switchState) {
tries = tries || 0;
@ -485,12 +502,13 @@ angular.module('copayApp.controllers').controller('indexController', function($r
var SAFE_CONFIRMATIONS = 6;
self.setTxHistory = function(txs) {
self.processNewTxs = function(txs) {
var config = configService.getSync().wallet.settings;
var now = Math.floor(Date.now() / 1000);
self.txHistoryUnique = {};
var txHistoryUnique = {};
var ret = [];
self.hasUnsafeConfirmed = false;
lodash.each(txs, function(tx) {
tx = txFormatService.processTx(tx);
@ -505,13 +523,15 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.hasUnsafeConfirmed = true;
}
if (!self.txHistoryUnique[tx.txid]) {
self.txHistory.push(tx);
self.txHistoryUnique[tx.txid] = true;
if (!txHistoryUnique[tx.txid]) {
ret.push(tx);
txHistoryUnique[tx.txid] = true;
} else {
$log.debug('Ignoring duplicate TX in history: ' + tx.txid)
}
});
return ret;
};
self.updateAlias = function() {
@ -735,13 +755,6 @@ angular.module('copayApp.controllers').controller('indexController', function($r
});
};
self.stopSync = function(remoteTx, localTx) {
if (remoteTx.txid == localTx.txid)
return true;
else
return false;
}
self.removeSoftConfirmedTx = function(txs) {
return lodash.map(txs, function(tx) {
if (tx.confirmations >= SOFT_CONFIRMATION_LIMIT)
@ -749,17 +762,14 @@ angular.module('copayApp.controllers').controller('indexController', function($r
});
}
self.getConfirmedTxs = function(cb) {
var fc = profileService.focusedClient;
var c = fc.credentials;
self.getConfirmedTxs = function(walletId, cb) {
storageService.getTxHistory(c.walletId, function(err, txs) {
storageService.getTxHistory(walletId, function(err, txs) {
if (err) return cb(err);
var localTxs = [];
if (!txs) {
self.showWaitingSign = true;
return cb(null, localTxs);
}
@ -772,72 +782,107 @@ angular.module('copayApp.controllers').controller('indexController', function($r
});
}
self.updateLocalTxHistory = function(cb) {
self.getConfirmedTxs(function(err, txsFromLocal) {
self.updateLocalTxHistory = function(client, cb) {
var requestLimit = 6;
var walletId = client.credentials.walletId;
self.getConfirmedTxs(walletId, function(err, txsFromLocal) {
if (err) return cb(err);
var endingTxid = txsFromLocal[0] ? txsFromLocal[0].txid : null;
var fc = profileService.focusedClient;
var c = fc.credentials;
fillTxsObject();
function getNewTxs(newTxs, skip, i_cb) {
function fillTxsObject(txsResult, index) {
txsResult = txsResult || [];
index = index || 0;
self.getTxsFromServer(client, skip, endingTxid, requestLimit, function(err, res, shouldContinue) {
if (err) return i_cb(err);
self.makeTxHistoryRequest(txsResult, index, txsFromLocal[0], function(err, newIndex, exitLoop) {
if (err) return cb(err);
if (exitLoop) {
self.txHistory = [];
self.setTxHistory(lodash.compact(txsResult.concat(txsFromLocal)));
return storageService.setTxHistory(JSON.stringify(self.txHistory), c.walletId, function() {
return cb(null);
});
newTxs = newTxs.concat(lodash.compact(res));
skip = skip + requestLimit;
$log.debug('Syncing TXs. Got:' + newTxs.length + ' Skip:' + skip, ' EndingTxid:', endingTxid, ' Continue:', shouldContinue);
if (!shouldContinue) {
newTxs = self.processNewTxs(newTxs);
$log.debug('Finish Sync: New Txs: ' + newTxs.length);
return i_cb(null, newTxs);
}
fillTxsObject(txsResult, newIndex);
if (walletId == profileService.focusedClient.credentials.walletId)
self.txProgress = newTxs.length;
$timeout(function() {
$rootScope.$apply();
});
getNewTxs(newTxs, skip, i_cb);
});
};
getNewTxs([], 0, function(err, txs) {
if (err) return cb(err);
var newHistory = lodash.compact(txs.concat(txsFromLocal));
$log.debug('Tx History synced. Total Txs: ' + newHistory.length);
if (walletId == profileService.focusedClient.credentials.walletId) {
self.completeHistory = newHistory;
self.txHistory = newHistory.slice(0, self.historyShowLimit);
self.historyShowShowAll = newHistory.length >= self.historyShowLimit;
}
return storageService.setTxHistory(JSON.stringify(newHistory), walletId, function() {
return cb();
});
});
});
}
self.showAllHistory = function() {
self.historyShowShowAll = false;
self.historyRendering = true;
$timeout(function() {
$rootScope.$apply();
$timeout(function() {
self.historyRendering = false;
self.txHistory = self.completeHistory;
}, 100);
}, 100);
};
self.makeTxHistoryRequest = function(txsResult, index, endingTx, cb) {
var fc = profileService.focusedClient;
var c = fc.credentials;
var exitLoop = false;
self.getTxsFromServer = function(client, skip, endingTxid, limit, cb) {
var res = [];
fc.getTxHistory({
skip: index,
limit: self.limitHistory + 1
}, function(err, txsFromBWC) {
client.getTxHistory({
skip: skip,
limit: limit
}, function(err, txsFromServer) {
if (err) return cb(err);
if (!txsFromBWC[0])
exitLoop = true;
if (!txsFromServer.length)
return cb();
lodash.each(txsFromBWC, function(t) {
if (!endingTx) txsResult.push(t);
else {
if (!self.stopSync(t, endingTx) && !exitLoop) {
txsResult.push(t);
} else {
exitLoop = true;
}
}
var res = lodash.takeWhile(txsFromServer, function(tx) {
return tx.txid != endingTxid;
});
index = index + self.limitHistory;
return cb(null, index, exitLoop);
return cb(null, res, res.length == limit);
});
}
};
self.updateHistory = function() {
var fc = profileService.focusedClient;
var walletId = fc.credentials.walletId;
if (!fc.isComplete() || self.updatingTxHistory[walletId]) return;
$log.debug('Updating Transaction History');
self.txHistoryError = false;
self.updatingTxHistory = true;
self.updatingTxHistory[walletId] = true;
$timeout(function() {
self.updateLocalTxHistory(function(err) {
if (err) self.txHistoryError = true;
self.updatingTxHistory = false;
self.showWaitingSign = false;
self.updateLocalTxHistory(fc, function(err) {
self.updatingTxHistory[walletId] = false;
if (err)
self.txHistoryError = true;
$rootScope.$apply();
});
});
@ -1288,4 +1333,12 @@ angular.module('copayApp.controllers').controller('indexController', function($r
self.setFocusedWallet();
});
});
$rootScope.$on('Local/NewEncryptionSetting', function() {
var fc = profileService.focusedClient;
self.isPrivKeyEncrypted = fc.isPrivKeyEncrypted();
$timeout(function() {
$rootScope.$apply();
});
});
});

View File

@ -1,11 +1,12 @@
'use strict';
angular.module('copayApp.controllers').controller('joinController',
function($scope, $rootScope, $timeout, go, notification, profileService, configService, isCordova, storageService, applicationService, $modal, gettext, lodash, ledger, trezor) {
function($scope, $rootScope, $timeout, go, notification, profileService, configService, isCordova, storageService, applicationService, $modal, gettext, lodash, ledger, trezor, isChromeApp, isDevel,derivationPathHelper) {
var self = this;
var defaults = configService.getDefaults();
$scope.bwsurl = defaults.bws.url;
$scope.derivationPath = derivationPathHelper.default;
this.onQrCodeScanned = function(data) {
$scope.secret = data;
@ -13,6 +14,42 @@ angular.module('copayApp.controllers').controller('joinController',
$scope.joinForm.secret.$render();
};
var updateSeedSourceSelect = function() {
self.seedOptions = [{
id: 'new',
label: gettext('New Random Seed'),
}, {
id: 'set',
label: gettext('Specify Seed...'),
}];
$scope.seedSource = self.seedOptions[0];
if (isChromeApp) {
self.seedOptions.push({
id: 'ledger',
label: gettext('Ledger Hardware Wallet'),
});
}
if (isChromeApp || isDevel) {
self.seedOptions.push({
id: 'trezor',
label: gettext('Trezor Hardware Wallet'),
});
}
};
this.setSeedSource = function(src) {
self.seedSourceId = $scope.seedSource.id;
self.accountValues = lodash.range(1, 100);
$timeout(function() {
$rootScope.$apply();
});
};
this.join = function(form) {
if (form && form.$invalid) {
self.error = gettext('Please enter the required fields');
@ -23,10 +60,10 @@ angular.module('copayApp.controllers').controller('joinController',
var opts = {
secret: form.secret.$modelValue,
myName: form.myName.$modelValue,
bwsurl: $scope.bwsurl
bwsurl: $scope.bwsurl,
}
var setSeed = form.setSeed.$modelValue;
var setSeed = self.seedSourceId =='set';
if (setSeed) {
var words = form.privateKey.$modelValue;
if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) {
@ -35,6 +72,15 @@ angular.module('copayApp.controllers').controller('joinController',
opts.mnemonic = words;
}
opts.passphrase = form.passphrase.$modelValue;
var pathData = derivationPathHelper.parse($scope.derivationPath);
if (!pathData) {
this.error = gettext('Invalid derivation path');
return;
}
opts.account = pathData.account;
opts.networkName = pathData.networkName;
opts.derivationStrategy = pathData.derivationStrategy;
} else {
opts.passphrase = form.createPassphrase.$modelValue;
}
@ -44,12 +90,21 @@ angular.module('copayApp.controllers').controller('joinController',
return;
}
if (form.hwLedger.$modelValue || form.hwTrezor.$modelValue) {
self.hwWallet = form.hwLedger.$modelValue ? 'Ledger' : 'TREZOR';
var src = form.hwLedger.$modelValue ? ledger : trezor;
if (self.seedSourceId == 'ledger' || self.seedSourceId == 'trezor') {
var account = $scope.account;
if (!account || account < 1) {
this.error = gettext('Invalid account number');
return;
}
var account = 0;
src.getInfoForNewWallet(account, function(err, lopts) {
if ( self.seedSourceId == 'trezor')
account = account - 1;
opts.account = account;
self.hwWallet = self.seedSourceId == 'ledger' ? 'Ledger' : 'Trezor';
var src = self.seedSourceId == 'ledger' ? ledger : trezor;
src.getInfoForNewWallet(true, account, function(err, lopts) {
self.hwWallet = false;
if (err) {
self.error = err;
@ -82,4 +137,7 @@ angular.module('copayApp.controllers').controller('joinController',
});
}, 100);
};
updateSeedSourceSelect();
self.setSeedSource('new');
});

View File

@ -55,6 +55,7 @@ angular.module('copayApp.controllers').controller('preferencesController',
return;
}
profileService.setPrivateKeyEncryptionFC(password, function() {
$rootScope.$emit('Local/NewEncryptionSetting');
$scope.encrypt = true;
});
});
@ -66,6 +67,7 @@ angular.module('copayApp.controllers').controller('preferencesController',
return;
}
profileService.disablePrivateKeyEncryptionFC(function(err) {
$rootScope.$emit('Local/NewEncryptionSetting');
if (err) {
$scope.encrypt = true;
$log.error(err);

View File

@ -13,7 +13,7 @@ angular.module('copayApp.controllers').controller('preferencesBwsUrlController',
this.bwsurl = (config.bwsFor && config.bwsFor[walletId]) || defaults.bws.url;
this.resetDefaultUrl = function() {
this.bwsurl = 'https://bws.bitpay.com/bws/api';
this.bwsurl = defaults.bws.url;
};
this.save = function() {
@ -50,4 +50,4 @@ angular.module('copayApp.controllers').controller('preferencesBwsUrlController',
});
});
};
});
});

View File

@ -7,7 +7,7 @@ angular.module('copayApp.controllers').controller('preferencesInformation',
var c = fc.credentials;
this.init = function() {
var basePath = profileService.getUtils().getBaseAddressDerivationPath(c.derivationStrategy, c.network, 0);
var basePath = c.getBaseAddressDerivationPath();
$scope.walletName = c.walletName;
$scope.walletId = c.walletId;

View File

@ -106,36 +106,6 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
var cancel_msg = gettextCatalog.getString('Cancel');
var confirm_msg = gettextCatalog.getString('Confirm');
$scope.openCopayersModal = function(copayers, copayerId) {
$rootScope.modalOpened = true;
var fc = profileService.focusedClient;
var ModalInstanceCtrl = function($scope, $modalInstance) {
$scope.copayers = copayers;
$scope.copayerId = copayerId;
$scope.color = fc.backgroundColor;
$scope.cancel = function() {
$modalInstance.dismiss('cancel');
};
};
var modalInstance = $modal.open({
templateUrl: 'views/modals/copayers.html',
windowClass: animationService.modalAnimated.slideUp,
controller: ModalInstanceCtrl,
});
var disableCloseModal = $rootScope.$on('closeModal', function() {
modalInstance.dismiss('cancel');
});
modalInstance.result.finally(function() {
$rootScope.modalOpened = false;
disableCloseModal();
var m = angular.element(document.getElementsByClassName('reveal-modal'));
m.addClass(animationService.modalAnimated.slideOutDown);
});
};
$scope.openDestinationAddressModal = function(wallets, address) {
$rootScope.modalOpened = true;
var fc = profileService.focusedClient;

View File

@ -21,13 +21,14 @@ angular
$logProvider.debugEnabled(true);
$provide.decorator('$log', ['$delegate',
function($delegate) {
function($delegate, isDevel) {
var historicLog = historicLogProvider.$get();
['debug', 'info', 'warn', 'error', 'log'].forEach(function(level) {
if (isDevel && level == 'error') return;
var orig = $delegate[level];
$delegate[level] = function() {
if (level == 'error')
console.log(arguments);

View File

@ -0,0 +1,46 @@
'use strict';
angular.module('copayApp.services').factory('derivationPathHelper', function(lodash) {
var root = {};
root.default = "m/44'/0'/0'"
root.parse = function(str) {
var arr = str.split('/');
var ret = {};
if (arr[0] != 'm')
return false;
switch (arr[1]) {
case "44'":
ret.derivationStrategy = 'BIP44';
break;
case "48'":
ret.derivationStrategy = 'BIP48';
break;
default:
return false;
};
switch (arr[2]) {
case "0'":
ret.networkName = 'livenet';
break;
case "1'":
ret.networkName = 'testnet';
break;
default:
return false;
};
var match = arr[3].match(/(\d+)'/);
if (!match)
return false;
ret.account = + match[1]
return ret;
};
return root;
});

View File

@ -0,0 +1,50 @@
'use strict';
angular.module('copayApp.services')
.factory('hwWallet', function($log, bwcService) {
var root = {};
// Ledger magic number to get xPub without user confirmation
root.ENTROPY_INDEX_PATH = "0xb11e/";
root.UNISIG_ROOTPATH = 44;
root.MULTISIG_ROOTPATH = 48;
root.LIVENET_PATH = 0;
root._err = function(data) {
var msg = 'Hardware Wallet Error: ' + (data.error || data.message || 'unknown');
$log.warn(msg);
return msg;
};
root.getRootPath = function(device, isMultisig, account) {
if (!isMultisig) return root.UNISIG_ROOTPATH;
// Compat
if (device == 'ledger' && account ==0) return root.UNISIG_ROOTPATH;
return root.MULTISIG_ROOTPATH;
};
root.getAddressPath = function(device, isMultisig, account) {
return root.getRootPath(device,isMultisig,account) + "'/" + root.LIVENET_PATH + "'/" + account + "'";
}
root.getEntropyPath = function(device, isMultisig, account) {
var path;
// Old ledger wallet compat
if (device == 'ledger' && account == 0)
return root.ENTROPY_INDEX_PATH + "0'";
return root.ENTROPY_INDEX_PATH + root.getRootPath(device,isMultisig,account) + "'/" + account + "'";
};
root.pubKeyToEntropySource = function(xPubKey) {
var b = bwcService.getBitcore();
var x = b.HDPublicKey(xPubKey);
return x.publicKey.toString();
};
return root;
});

View File

@ -0,0 +1,5 @@
'use strict';
angular.module('copayApp.services').factory('isDevel', function(nodeWebkit, isChromeApp, isMobile) {
return !isMobile.any() && !isChromeApp && !nodeWebkit.isDefined();
});

View File

@ -1,43 +1,27 @@
'use strict';
angular.module('copayApp.services')
.factory('ledger', function($log, bwcService, gettext) {
.factory('ledger', function($log, bwcService, gettext, hwWallet) {
var root = {};
var LEDGER_CHROME_ID = "kkdpmhnladdopljabkgpacgpliggeeaf";
// Ledger magic number to get xPub without user confirmation
root.ENTROPY_INDEX_PATH = "0xb11e/";
root.callbacks = {};
root.hasSession = function() {
root._message({
command: "has_session"
});
}
root.getEntropySource = function(account, callback) {
var path = root.ENTROPY_INDEX_PATH + account + "'";
var xpub = root.getXPubKey(path, function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data);
}
var b = bwcService.getBitcore();
var x = b.HDPublicKey(data.xpubkey);
data.entropySource = x.publicKey.toString();
return callback(data);
root.getEntropySource = function(isMultisig, account, callback) {
root.getXPubKey(hwWallet.getEntropyPath('ledger', isMultisig, account), function(data) {
if (!data.success)
return callback(hwWallet._err(data));
return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey));
});
};
root.getXPubKeyForAddresses = function(account, callback) {
return root.getXPubKey(root._getPath(account), callback);
};
root.getXPubKey = function(path, callback) {
$log.debug('Ledger deriving xPub path:', path);
root.callbacks["get_xpubkey"] = callback;
root._messageAfterSession({
@ -47,35 +31,36 @@ angular.module('copayApp.services')
};
root.getInfoForNewWallet = function(account, callback) {
root.getInfoForNewWallet = function(isMultisig, account, callback) {
var opts = {};
root.getEntropySource(account, function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data.message);
}
opts.entropySource = data.entropySource;
root.getXPubKeyForAddresses(account, function(data) {
root.getEntropySource(isMultisig, account, function(err, entropySource) {
if (err) return callback(err);
opts.entropySource = entropySource;
root.getXPubKey(hwWallet.getAddressPath('ledger', isMultisig, account), function(data) {
if (!data.success) {
$log.warn(data.message);
return callback(data);
}
opts.extendedPublicKey = data.xpubkey;
opts.externalSource = 'ledger';
opts.externalIndex = account;
opts.account = account;
// Old ledger compat
opts.derivationStrategy = account ? 'BIP48' : 'BIP44';
return callback(null, opts);
});
});
};
root._signP2SH = function(txp, account, callback) {
root._signP2SH = function(txp, account, isMultisig, callback) {
root.callbacks["sign_p2sh"] = callback;
var redeemScripts = [];
var paths = [];
var tx = bwcService.getUtils().buildTx(txp);
var tx = bwcService.buildTx(txp);
for (var i = 0; i < tx.inputs.length; i++) {
redeemScripts.push(new ByteString(tx.inputs[i].redeemScript.toBuffer().toString('hex'), GP.HEX).toString());
paths.push(root._getPath(account) + txp.inputs[i].path.substring(1));
paths.push(hwWallet.getAddressPath('ledger', isMultisig, account) + txp.inputs[i].path.substring(1));
}
var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX));
var inputs = [];
@ -98,12 +83,15 @@ angular.module('copayApp.services')
};
root.signTx = function(txp, account, callback) {
// TODO Compat
var isMultisig = true;
if (txp.addressType == 'P2PKH') {
var msg = 'P2PKH wallets are not supported with ledger';
$log.error(msg);
return callback(msg);
} else {
root._signP2SH(txp, account, callback);
root._signP2SH(txp, account, isMultisig, callback);
}
}
@ -154,10 +142,6 @@ angular.module('copayApp.services')
}
}
root._getPath = function(account) {
return "44'/0'/" + account + "'";
}
root._splitTransaction = function(transaction) {
var result = {};
var inputs = [];

View File

@ -46,8 +46,6 @@ angular.module('copayApp.services')
client.setNotificationsInterval(BACKGROUND_UPDATE_PERIOD);
});
root.focusedClient.setNotificationsInterval(FOREGROUND_UPDATE_PERIOD);
console.log('[profileService.js.49] SETTING...'); //TODO
}
return cb();
@ -175,14 +173,17 @@ angular.module('copayApp.services')
var walletClient = bwcService.getClient();
var network = opts.networkName || 'livenet';
if (opts.mnemonic) {
try {
opts.mnemonic = root._normalizeMnemonic(opts.mnemonic);
walletClient.seedFromMnemonic(opts.mnemonic, {
network: network,
passphrase: opts.passphrase,
account: 0,
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
});
} catch (ex) {
$log.info(ex);
return cb(gettext('Could not create: Invalid wallet seed'));
@ -197,7 +198,8 @@ angular.module('copayApp.services')
} else if (opts.extendedPublicKey) {
try {
walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
account: 0
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
});
} catch (ex) {
$log.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
@ -311,7 +313,7 @@ angular.module('copayApp.services')
walletId: walletId
});
delete root.walletClients[walletId];
delete root.walletClients[walletId];
root.focusedClient = null;
storageService.clearLastAddress(walletId, function(err) {
@ -361,7 +363,7 @@ angular.module('copayApp.services')
root.setWalletClients();
root.setAndStoreFocus(walletId, function() {
storageService.storeProfile(root.profile, function(err){
storageService.storeProfile(root.profile, function(err) {
return cb(err, walletId);
});
});
@ -420,7 +422,7 @@ angular.module('copayApp.services')
walletClient.importFromMnemonic(words, {
network: opts.networkName,
passphrase: opts.passphrase,
account: 0,
account: opts.account || 0,
}, function(err) {
if (err)
return bwsError.cb(err, gettext('Could not import'), cb);
@ -437,7 +439,8 @@ angular.module('copayApp.services')
$log.debug('Importing Wallet XPubKey');
walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, {
account: 0
account: opts.account || 0,
derivationStrategy: opts.derivationStrategy || 'BIP44',
}, function(err) {
if (err) {
@ -594,7 +597,7 @@ angular.module('copayApp.services')
var fc = root.focusedClient;
$log.info('Requesting Ledger Chrome app to sign the transaction');
ledger.signTx(txp, 0, function(result) {
ledger.signTx(txp, fc.credentials.account, function(result) {
$log.debug('Ledger response', result);
if (!result.success)
return cb(result.message || result.error);
@ -612,7 +615,7 @@ angular.module('copayApp.services')
$log.info('Requesting Trezor to sign the transaction');
var xPubKeys = lodash.pluck(fc.credentials.publicKeyRing, 'xPubKey');
trezor.signTx(xPubKeys, txp, 0, function(err, result) {
trezor.signTx(xPubKeys, txp, fc.credentials.account, function(err, result) {
if (err) return cb(err);
$log.debug('Trezor response', result);

View File

@ -1,39 +1,21 @@
'use strict';
angular.module('copayApp.services')
.factory('trezor', function($log, $timeout, bwcService, gettext, lodash, bitcore) {
.factory('trezor', function($log, $timeout, gettext, lodash, bitcore, hwWallet) {
var root = {};
var SETTLE_TIME = 3000;
root.ENTROPY_INDEX_PATH = "0xb11e/";
root.callbacks = {};
root._err = function(data) {
var msg = 'TREZOR Error: ' + (data.error || data.message || 'unknown');
$log.warn(msg);
return msg;
};
root.getEntropySource = function(account, callback) {
var path = root.ENTROPY_INDEX_PATH + account + "'";
var xpub = root.getXPubKey(path, function(data) {
if (!data.success) {
return callback(root._err(data));
}
var b = bwcService.getBitcore();
var x = b.HDPublicKey(data.xpubkey);
data.entropySource = x.publicKey.toString();
return callback(null, data);
root.getEntropySource = function(isMultisig, account, callback) {
root.getXPubKey(hwWallet.getEntropyPath('trezor', isMultisig, account), function(data) {
if (!data.success)
return callback(hwWallet._err(data));
return callback(null, hwWallet.pubKeyToEntropySource(data.xpubkey));
});
};
root.getXPubKeyForAddresses = function(account, callback) {
return root.getXPubKey(root._getPath(account), callback);
};
root.getXPubKey = function(path, callback) {
$log.debug('TREZOR deriving xPub path:', path);
@ -41,20 +23,25 @@ angular.module('copayApp.services')
};
root.getInfoForNewWallet = function(account, callback) {
root.getInfoForNewWallet = function(isMultisig, account, callback) {
var opts = {};
root.getEntropySource(account, function(err, data) {
root.getEntropySource(isMultisig, account, function(err, data) {
if (err) return callback(err);
opts.entropySource = data.entropySource;
opts.entropySource = data;
$log.debug('Waiting TREZOR to settle...');
$timeout(function() {
root.getXPubKeyForAddresses(account, function(data) {
if (!data.success)
return callback(root._err(data));
root.getXPubKey(hwWallet.getAddressPath('trezor', isMultisig, account), function(data) {
if (!data.success)
return callback(hwWallet._err(data));
opts.extendedPublicKey = data.xpubkey;
opts.externalSource = 'trezor';
opts.externalIndex = account;
opts.account = account;
if (isMultisig)
opts.derivationStrategy = 'BIP48';
return callback(null, opts);
});
}, SETTLE_TIME);
@ -107,10 +94,13 @@ angular.module('copayApp.services')
if (txp.addressType == 'P2PKH') {
$log.debug("Trezor signing uni-sig p2pkh. Account:", account);
var inAmount = 0;
inputs = lodash.map(txp.inputs, function(i) {
$log.debug("Trezor TX input path:", i.path);
var pathArr = i.path.split('/');
var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var n = [hwWallet.UNISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
inAmount += i.satoshis;
return {
address_n: n,
@ -121,8 +111,9 @@ angular.module('copayApp.services')
var change = inAmount - txp.fee - txp.amount;
if (change > 0) {
$log.debug("Trezor TX change path:", txp.changeAddress.path);
var pathArr = txp.changeAddress.path.split('/');
var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var n = [hwWallet.UNISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
tmpOutputs.push({
address_n: n,
@ -133,8 +124,9 @@ angular.module('copayApp.services')
} else {
// P2SH Wallet
// P2SH Wallet, multisig wallet
var inAmount = 0;
$log.debug("Trezor signing multi-sig p2sh. Account:", account);
var sigs = xPubKeys.map(function(v) {
return '';
@ -142,8 +134,9 @@ angular.module('copayApp.services')
inputs = lodash.map(txp.inputs, function(i) {
$log.debug("Trezor TX input path:", i.path);
var pathArr = i.path.split('/');
var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var n = [hwWallet.MULTISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var np = n.slice(3);
inAmount += i.satoshis;
@ -171,8 +164,9 @@ angular.module('copayApp.services')
var change = inAmount - txp.fee - txp.amount;
if (change > 0) {
$log.debug("Trezor TX change path:", txp.changeAddress.path);
var pathArr = txp.changeAddress.path.split('/');
var n = [44 | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var n = [hwWallet.MULTISIG_ROOTPATH | 0x80000000, 0 | 0x80000000, account | 0x80000000, parseInt(pathArr[1]), parseInt(pathArr[2])];
var np = n.slice(3);
var orderedPubKeys = root._orderPubKeys(xPubKeys, np);
@ -183,18 +177,6 @@ angular.module('copayApp.services')
};
}));
// 6D
// 6C
// Addr: 3HFkHufeSaqJtqby8G9RiajaL6HdQDypRT
//
//
//(sin reverse)
// 6C
// 6D
// Addr: 3KCPRDXpmovs9nFvJHJjjsyoBDXXUZ2Frg
// "asm" : "2 03e53b2f69e1705b253029aae2591fbd0e799ed8071c8588a545b2d472dd12df88 0379797abc21d6f82c7f0aba78fd3888d8ae75ec56a10509b20feedbeac20285d9 2 OP_CHECKMULTISIG",
//
tmpOutputs.push({
address_n: n,
amount: change,
@ -226,17 +208,13 @@ angular.module('copayApp.services')
outputs = JSON.parse(JSON.stringify(outputs));
$log.debug('Signing with TREZOR', inputs, outputs);
TrezorConnect.signTx(inputs, outputs, function(result) {
if (!data.success)
return callback(root._err(data));
TrezorConnect.signTx(inputs, outputs, function(res) {
if (!res.success)
return callback(hwWallet._err(res));
callback(null, result);
callback(null, res);
});
};
root._getPath = function(account) {
return "44'/0'/" + account + "'";
}
return root;
});

2
src/js/trezor-url.js Normal file
View File

@ -0,0 +1,2 @@
window.TREZOR_CHROME_URL = './bower_components/trezor-connect/chrome/wrapper.html';

View File

@ -1,372 +0,0 @@
window.TrezorConnect = (function () {
'use strict';
var CONNECT_ORIGIN = 'https://trezor.github.io';
var CONNECT_PATH = CONNECT_ORIGIN + '/connect';
var CONNECT_POPUP = CONNECT_PATH + '/popup/popup.html';
var ERR_TIMED_OUT = 'Loading timed out';
var ERR_WINDOW_CLOSED = 'Window closed';
var ERR_ALREADY_WAITING = 'Already waiting for a response';
var manager = new PopupManager(
CONNECT_POPUP,
CONNECT_ORIGIN,
'trezor-connect',
function () {
var w = 600;
var h = 500;
var x = (screen.width - w) / 2;
var y = (screen.height - h) / 3;
var params =
'height=' + h +
',width=' + w +
',left=' + x +
',top=' + y +
',menubar=no' +
',toolbar=no' +
',location=no' +
',personalbar=no' +
',status=no';
return params;
}
);
/**
* Public API.
*/
function TrezorConnect() {
/**
* Popup errors.
*/
this.ERR_TIMED_OUT = ERR_TIMED_OUT;
this.ERR_WINDOW_CLOSED = ERR_WINDOW_CLOSED;
this.ERR_ALREADY_WAITING = ERR_ALREADY_WAITING;
/**
* @typedef XPubKeyResult
* @param {boolean} success
* @param {?string} error
* @param {?string} xpubkey serialized extended public key
* @param {?string} path BIP32 serializd path of the key
*/
/**
* Load BIP32 extended public key by path.
*
* Path can be specified either in the string form ("m/44'/1/0") or as
* raw integer array. In case you omit the path, user is asked to select
* a BIP32 account to export, and the result contains m/44'/0'/x' node
* of the account.
*
* @param {?(string|array<number>)} path
* @param {function(XPubKeyResult)} callback
*/
this.getXPubKey = function (path, callback) {
if (typeof path === 'string') {
path = parseHDPath(path);
}
manager.sendWithChannel({
'type': 'xpubkey',
'path': path
}, function (result) {
manager.close();
callback(result);
});
};
/**
* @typedef SignTxResult
* @param {boolean} success
* @param {?string} error
* @param {?string} serialized_tx serialized tx, in hex, including signatures
* @param {?array<string>} signatures array of input signatures, in hex
*/
/**
* Sign a transaction in the device and return both serialized
* transaction and the signatures.
*
* @param {array<TxInputType>} inputs
* @param {array<TxOutputType>} outputs
* @param {function(SignTxResult)} callback
*
* @see https://github.com/trezor/trezor-common/blob/master/protob/types.proto
*/
this.signTx = function (inputs, outputs, callback) {
manager.sendWithChannel({
'type': 'signtx',
'inputs': inputs,
'outputs': outputs
}, function (result) {
manager.close();
callback(result);
});
};
/**
* @typedef RequestLoginResult
* @param {boolean} success
* @param {?string} error
* @param {?string} public_key public key used for signing, in hex
* @param {?string} signature signature, in hex
*/
/**
* Sign a login challenge for active origin.
*
* @param {?string} hosticon
* @param {string} challenge_hidden
* @param {string} challenge_visual
* @param {string|function(RequestLoginResult)} callback
*
* @see https://github.com/trezor/trezor-common/blob/master/protob/messages.proto
*/
this.requestLogin = function (
hosticon,
challenge_hidden,
challenge_visual,
callback
) {
if (typeof callback === 'string') {
// special case for a login through <trezor:login> button.
// `callback` is name of global var
callback = window[callback];
}
if (!callback) {
throw new TypeError('TrezorConnect: login callback not found');
}
manager.sendWithChannel({
'type': 'login',
'icon': hosticon,
'challenge_hidden': challenge_hidden,
'challenge_visual': challenge_visual
}, function (result) {
manager.close();
callback(result);
});
};
var LOGIN_CSS =
'<style>@import url("' + CONNECT_PATH + '/login_buttons.css")</style>';
var LOGIN_ONCLICK =
'TrezorConnect.requestLogin('
+ "'@hosticon@','@challenge_hidden@','@challenge_visual@','@callback@'"
+ ')';
var LOGIN_HTML =
'<div id="trezorconnect-wrapper">'
+ ' <a id="trezorconnect-button" onclick="' + LOGIN_ONCLICK + '">'
+ ' <span id="trezorconnect-icon"></span>'
+ ' <span id="trezorconnect-text">@text@</span>'
+ ' </a>'
+ ' <span id="trezorconnect-info">'
+ ' <a id="trezorconnect-infolink" href="https://www.buytrezor.com/"'
+ ' target="_blank">What is TREZOR?</a>'
+ ' </span>'
+ '</div>';
/**
* Find <trezor:login> elements and replace them with login buttons.
* It's not required to use these special elements, feel free to call
* `TrezorConnect.requestLogin` directly.
*/
this.renderLoginButtons = function () {
var elements = document.getElementsByTagName('trezor:login');
for (var i = 0; i < elements.length; i++) {
var e = elements[i];
var text = e.getAttribute('text') || 'Sign in with TREZOR';
var callback = e.getAttribute('callback') || '';
var hosticon = e.getAttribute('icon') || '';
var challenge_hidden = e.getAttribute('challenge_hidden') || '';
var challenge_visual = e.getAttribute('challenge_visual') || '';
// it's not valid to put markup into attributes, so let users
// supply a raw text and make TREZOR bold
text = text.replace('TREZOR', '<strong>TREZOR</strong>');
e.parentNode.innerHTML =
LOGIN_CSS + LOGIN_HTML
.replace('@text@', text)
.replace('@callback@', callback)
.replace('@hosticon@', hosticon)
.replace('@challenge_hidden@', challenge_hidden)
.replace('@challenge_visual@', challenge_visual);
}
};
}
var exports = new TrezorConnect();
exports.renderLoginButtons();
return exports;
/*
* `getXPubKey()`
*/
function parseHDPath(string) {
return string
.toLowerCase()
.split('/')
.filter(function (p) { return p !== 'm'; })
.map(function (p) {
var n = parseInt(p);
if (p[p.length - 1] === "'") { // hardened index
n = n | 0x80000000;
}
return n;
});
}
/*
* Popup management
*/
function Popup(url, name, params) {
var w = window.open(url, name, params);
var interval;
var iterate = function () {
if (w.closed) {
clearInterval(interval);
if (this.onclose) {
this.onclose();
}
}
}.bind(this);
interval = setInterval(iterate, 100);
this.window = w;
this.onclose = null;
}
function Channel(target, origin, waiting) {
var respond = function (data) {
if (waiting) {
var callback = waiting;
waiting = null;
callback(data);
}
};
var receive = function (event) {
if (event.source === target && event.origin === origin) {
respond(event.data);
}
};
window.addEventListener('message', receive);
this.respond = respond;
this.close = function () {
window.removeEventListener('message', receive);
};
this.send = function (value, callback) {
console.log('[trezor.js.270:value:]',value); //TODO
if (waiting === null) {
waiting = callback;
target.postMessage(value, origin);
} else {
throw new Error(ERR_ALREADY_WAITING);
}
};
}
function ConnectedChannel(url, origin, name, params) {
var ready = function () {
clearTimeout(this.timeout);
this.popup.onclose = null;
this.ready = true;
this.onready();
}.bind(this);
var closed = function () {
clearTimeout(this.timeout);
this.channel.close();
this.onerror(new Error(ERR_WINDOW_CLOSED));
}.bind(this);
var timedout = function () {
this.popup.onclose = null;
this.popup.window.close();
this.channel.close();
this.onerror(new Error(ERR_TIMED_OUT));
}.bind(this);
this.popup = new Popup(url, name, params);
this.channel = new Channel(this.popup.window, origin, ready);
this.timeout = setTimeout(timedout, 5000);
this.popup.onclose = closed;
this.ready = false;
this.onready = null;
this.onerror = null;
}
function PopupManager(url, origin, name, onparams) {
var cc = null;
var closed = function () {
cc.channel.respond(new Error(ERR_WINDOW_CLOSED));
cc.channel.close();
cc = null;
};
var open = function (callback) {
cc = new ConnectedChannel(url, origin, name, onparams());
cc.onready = function () {
cc.popup.onclose = closed;
callback(cc.channel);
};
cc.onerror = function (error) {
cc = null;
callback(error);
};
};
this.close = function () {
if (cc) {
cc.popup.window.close();
}
};
this.waitForChannel = function (callback) {
if (cc) {
if (cc.ready) {
callback(cc.channel);
} else {
callback(new Error(ERR_ALREADY_WAITING));
}
} else {
open(callback);
}
};
this.sendWithChannel = function (message, callback) {
var onresponse = function (response) {
if (response instanceof Error) {
callback({success: false, error: response.message});
} else {
callback(response);
}
};
var onchannel = function (channel) {
if (channel instanceof Error) {
callback({success: false, error: channel.message});
} else {
channel.send(message, onresponse);
}
}
this.waitForChannel(onchannel);
};
}
}());