commit
37aac02773
34 changed files with 2659 additions and 568 deletions
74
backupRecovery.md
Normal file
74
backupRecovery.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
|
||||
|
||||
# Copay Backup and Restore Notes
|
||||
|
||||
## Description
|
||||
|
||||
Copay is a Multisig HD Wallet. Copay app holds the extended private keys for the wallet. The private key never leaves the device, so for accessing a wallet funds it is necesary to have the device or a backup of the wallet.
|
||||
|
||||
## Definitions
|
||||
|
||||
### Backup Formats:
|
||||
* Wallet Seed (WS): 12 words mnemonic backup (available from Copay v1.2+). The 12 words are used as wallet seed, following BIP39 specification. The wallet seed may require a passphrase to recreate the wallet (if one was specified at creation time).
|
||||
* Wallet Backup (WB): Exported data from Copay, containing an AES encrypted JSON with many wallet parameters (like extended private key, wallet name, extended public keys of copayers, etc. See #export-format). This data can be created from Copay v1.2+ (Settings -> Export) and it was the default backup format on previous Copay versions. WB can be a file (standard format for Copay desktop versions) or a text (standard for Copay mobile versions).
|
||||
|
||||
### Backup recovery cases
|
||||
* Case 1: Loss of device holding the wallet
|
||||
* Case 2: Change to a new Bitcore Wallet Service (BWS)
|
||||
* Case 3: Lost device + new Bitcore Wallet Service
|
||||
|
||||
### Wallet Recovery Scope
|
||||
* Basic Recovery: Wallet access is restored. It is possible to see wallet balance and past transactions. It is possible to send and receive payments.
|
||||
* Full Recovery: All the features of Partial Recovery + wallet name, copayer names are recovered, past payment proposal metadata (who signed, and notes) are recoved.
|
||||
|
||||
## Wallet Restore Scenarios
|
||||
|
||||
### Non-multisig wallets
|
||||
|
||||
Case 1: From both WS and WB, full recovery is possible.
|
||||
- Enter the WS or the WB at 'Import wallet' in a new device.
|
||||
- Wallet access should be restored.
|
||||
|
||||
Case 2: Basic recovery is possible using the device where the wallet is installed, pointing to the new server (Recreate wallet feature).
|
||||
- Point to the new server (Settings -> Bitcore Wallet Service).
|
||||
- If the wallet is not registered at the new Wallet service, a 'Recreate' button will appear at wallet's home. Click it to recreate the wallet.
|
||||
- Wallet should be recreated and access to funds should be restored.
|
||||
- If the wallet existed, it may be necessary to rescan Wallet's addresses for funds (from Settings -> Advanced -> Scan Addresses for Funds)
|
||||
|
||||
Case 3: From both Backup Words and Backup file, basic recovery is possible.
|
||||
(Using WS)
|
||||
- Enter the WS at 'Import Wallet'
|
||||
If the error "This wallet is not registered at the wallet service" appears:
|
||||
- Go to 'Create Wallet', and enter the WS at 'Advanced Options'. Select a new name for the restored wallet. Total and required number of copayers should be set to 1.
|
||||
- Wallet should be recreated and access to funds should be restored.
|
||||
|
||||
(Using WD)
|
||||
- Enter the WD at 'Import Wallet'
|
||||
- Wallet should be recreated and access to funds should be restored.
|
||||
|
||||
|
||||
### Multisig wallets
|
||||
|
||||
Case 1: From both WS and WD, full recovery is possible.
|
||||
- Enter WS or WD at 'Import wallet' in a new device.
|
||||
- Wallet access should be restored.
|
||||
|
||||
Case 2: Basic recovery is possible using the device where the wallet is installed, pointing the the new server (Recreate Wallet feature).
|
||||
- Point to the new server (Settings -> Bitcore Wallet Service).
|
||||
- If the wallet is not registered at the new Wallet service, a 'Recreate' button will appear at wallet's home. Click it to recreate the wallet.
|
||||
- Wallet should be recreated and access to funds should be restored.
|
||||
- If the wallet existed, it may be necessary to rescan Wallet's addresses for funds (from Settings -> Advanced -> Scan Addresses for Funds)
|
||||
|
||||
Case 3: Basic recovery is possible using:
|
||||
|
||||
A) WS of all copayers in the wallet
|
||||
- Enter one WS at Create (at the Advanced option section). Note that the wallet configuration (M-of-N and network paramenters) needs to match the parameters that where entered when the wallet was first created. Wallet name and copayer nicknames need to be entered also, but there is no need for them to match the original wallet setup.
|
||||
- Ask other copayers to join the wallet using the given invitation code. All copayers need to enter their WS at Join (at -> Advanced Options -> Wallet Seed).
|
||||
- Wallet should be recreated and access to funds should be restored.
|
||||
|
||||
B) One WD and a quorum of WS of the other members.
|
||||
- Using the WD, import the wallet.
|
||||
- Ask other copayers to import the wallet using their WS.
|
||||
- Wallet should be recreated and funds should be accesable
|
||||
|
||||
In this case, Copayers will not be able to decrypt the 'notes' field on the new Spend Proposals, because the shared secret stored at the WD is not longer known by other copayers.
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
"ng-lodash": "~0.2.0",
|
||||
"angular-moment": "0.10.1",
|
||||
"moment": "2.10.3",
|
||||
"angular-bitcore-wallet-client": "0.1.2",
|
||||
"angular-bitcore-wallet-client": "0.3.0",
|
||||
"angular-ui-router": "~0.2.13",
|
||||
"qrcode-decoder-js": "*",
|
||||
"fastclick": "*",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,100 +6,111 @@
|
|||
|
||||
|
||||
|
||||
<div class="content p20v" ng-controller="backupController as backup">
|
||||
<div class="row">
|
||||
<div class="columns" ng-show="!backup.backupWalletPlainText">
|
||||
<div class="text-warning size-14 m20b" ng-show="backup.error">
|
||||
<i class="fi-alert size-12"></i>
|
||||
<span translate> Failed to create backup </span>
|
||||
<div class="content p20v" ng-controller="wordsController as wordsC">
|
||||
|
||||
<div ng-show="wordsC.mnemonicWords">
|
||||
<div class="row" ng-show="index.n==1">
|
||||
<div class="m10t columns size-14 text-gray">
|
||||
<span translate>
|
||||
You need the wallet seed to restore this personal wallet.
|
||||
</span>
|
||||
<span translate class="text-bold">
|
||||
Write it down and keep them somewhere safe.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="text-warning size-14 m20b" ng-show="backup.isEncrypted">
|
||||
<i class="fi-alert size-12"></i>
|
||||
<span translate> The private key for this wallet is encrypted. Exporting a backup will keep the private key encrypted in the backup archive.</span>
|
||||
|
||||
</div>
|
||||
<div class="row" ng-show="(index.n>1 && index.m != index.n )">
|
||||
<div class="m10t columns size-14 text-gray">
|
||||
<span translate>
|
||||
To restore this {{index.m}}-{{index.n}} shared wallet you will need
|
||||
</span>:
|
||||
<ol class="m10t columns size-14 text-gray">
|
||||
<li translate>Your wallet seed and access to the wallet service where your wallet is registered</li>
|
||||
<li translate><b>OR</b> the wallet seed of <b>all</b> copayers in the wallet</li>
|
||||
<li translate><b>OR</b> 1 wallet export file and the remaining quorum of wallet seeds (e.g. in a 3-5 wallet: 1 wallet export file + 2 wallet seeds of any of the other copayers).</li>
|
||||
</ol>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label for="password" translate>Set up a Password for your backup</label>
|
||||
<div class="input">
|
||||
<input type="password" class="form-control"
|
||||
placeholder="{{'Your backup password'|translate}}"
|
||||
name="password" ng-model="backup.password">
|
||||
</div>
|
||||
<div class="row" ng-show="(index.n>1 && index.m == index.n )">
|
||||
<div class="m10t columns size-14 text-gray">
|
||||
<span translate>
|
||||
To restore this {{index.m}}-{{index.n}} <b>shared</b> wallet you will need
|
||||
</span>:
|
||||
<ol class="m10t columns size-14 text-gray">
|
||||
<li translate> Your wallet seed and access to the wallet service where wallet is registered</li>
|
||||
<li translate><b>OR</b> the wallet seeds of <b>all</b> copayers in the wallet</li>
|
||||
</ol>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m20t" ng-show="!wordsC.mnemonicWords">
|
||||
<div class="columns size-14 text-gray text-center" translate>
|
||||
Wallet seed not available. You can still export it from Advanced > Export.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="wordsC.mnemonicWords">
|
||||
<div class="row">
|
||||
<div class="m10t oh" ng-init="show=false">
|
||||
<a class="button outline light-gray expand tiny" ng-click="show=!show">
|
||||
<i class="fi-widget m3r"></i>
|
||||
<span translate ng-hide="show" ng-click="wordsC.done()">Show Wallet Seed</span>
|
||||
<span translate ng-hide="!show">Hide Wallet Seed</span>
|
||||
<i ng-if="!show" class="icon-arrow-down4"></i>
|
||||
<i ng-if="show" class="icon-arrow-up4"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="password" translate>Repeat password</label>
|
||||
<div class="input">
|
||||
<input type="password" class="form-control"
|
||||
placeholder="{{'Repeat password'|translate}}"
|
||||
name="password" ng-model="backup.repeatpassword">
|
||||
</div>
|
||||
<div class="row enable_text_select" ng-show="show">
|
||||
|
||||
|
||||
<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>
|
||||
<div class="small-centered p10t p10b large-centered medium-centered large-8 medium-8 small-10 columns enable_text_select" style="border:1px solid gray; background:#eee;
|
||||
">
|
||||
<span class="m10r" ng-repeat="word in wordsC.mnemonicWords">{{word}} </span>
|
||||
</div>
|
||||
|
||||
<div class="box-notification large-centered medium-centered small-centered large-8 medium-8 small-10 columns m10t" ng-show="wordsC.mnemonicHasPassphrase">
|
||||
<span class="text-warning size-14">
|
||||
<i class="fi-alert"></i>
|
||||
<span translate>
|
||||
WARNING: This seed was created with a passphrase. To recover this wallet both the mnemonic and passphrase are needed.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="m10 text-center columns">
|
||||
<div class="m10 size-14 text-gray">
|
||||
<span translate>
|
||||
Once you have copy your wallet seed, it is recommended to delete it from this device.
|
||||
</span>
|
||||
</div>
|
||||
<button class="button outline round dark-gray tiny" ng-click="wordsC.delete()">
|
||||
<i class="fi-trash"></i>
|
||||
<span translate>
|
||||
DELETE WORDS
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- hide this in multisig just to show less text -->
|
||||
<div class="row m20t" ng-show="index.n==1">
|
||||
<div class="columns size-14 text-gray">
|
||||
<span translate>You can safely install your wallet on another device and use it from multiple devices at the same time.</span>
|
||||
<a href="#" ng-click="$root.openExternalLink('https://github.com/bitpay/copay#backups')" translate>
|
||||
Learn more about Copay backups
|
||||
</a>
|
||||
</div>
|
||||
<div ng-hide="hideAdv" class="row">
|
||||
<div class="large-12 columns">
|
||||
<label for="no-sign" class="line-b oh">
|
||||
<span translate>Do not include private key in backup</span>
|
||||
<switch id="no-sign" name="noSign" ng-model="noSign" class="green right m5t m10b"></switch>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="m10 size-14 text-gray" translate>
|
||||
A backup without its private key will allow the user to see the wallet balance, transactions, and create spend proposals. However, it will not be able to approve (sign) proposals.
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="black round expand m0" ng-click="backup.downloadWalletBackup()"
|
||||
ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-show="!backup.isSafari && !backup.isCordova"><i class="fi-download"></i>
|
||||
<span translate>Download backup</span></button>
|
||||
<button class="black round expand m0" ng-click="backup.viewWalletBackup()"
|
||||
ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-show="backup.isSafari && !backup.isCordova"><i class="fi-eye"></i>
|
||||
<span translate>View backup</span></button>
|
||||
<div ng-show="backup.isCordova">
|
||||
<h4 translate>Backup options</h4>
|
||||
<button class="black round expand" ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-click="backup.copyWalletBackup()"><i class="fi-clipboard-pencil"></i>
|
||||
<span translate>Copy to clipboard</span></button>
|
||||
<button class="black round expand m0" ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-click="backup.sendWalletBackup()"><i class="fi-mail"></i>
|
||||
<span translate>Send by email</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backup.backupWalletPlainText">
|
||||
<div class="large-12 columns">
|
||||
<h3 translate>Copy backup to a safe place</h3>
|
||||
<div class="input">
|
||||
<textarea rows="12">{{backup.backupWalletPlainText}}</textarea>
|
||||
</div>
|
||||
<div class="size-12 text-gray text-right">
|
||||
<i class="icon-compose"></i>
|
||||
<span translate>Copy this text as it is to a safe place (notepad or email)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row m10t">
|
||||
<div class="columns size-14 text-gray" translate>
|
||||
* You can safely install your backup on another device and use your wallet from multiple devices at the same time.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<div class="content p20v" ng-controller="createController as create" ng-init="create.setTotalCopayers(1)">
|
||||
|
||||
<div class="onGoingProcess" ng-show="create.loading">
|
||||
<div class="onGoingProcess" ng-show="create.loading && !create.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
|
|
@ -21,6 +21,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onGoingProcess" ng-show="create.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
<span translate>Connecting to Ledger Wallet...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="create-tab small-only-text-center" ng-hide="create.hideTabs">
|
||||
<div class="row">
|
||||
<div class="tab-container small-6 medium-3 large-2">
|
||||
|
|
@ -63,7 +76,7 @@
|
|||
</div>
|
||||
<div class="row" ng-show="totalCopayers != 1">
|
||||
<div class="large-6 medium-6 columns">
|
||||
<label><span translate>Select total number of copayers</span>
|
||||
<label><span translate>Total number of copayers</span>
|
||||
<select class="m10t" ng-model="totalCopayers"
|
||||
ng-options="totalCopayers as totalCopayers for totalCopayers in create.TCValues"
|
||||
ng-change="create.setTotalCopayers(totalCopayers)">
|
||||
|
|
@ -71,7 +84,7 @@
|
|||
</label>
|
||||
</div>
|
||||
<div class="large-6 medium-6 columns">
|
||||
<label><span translate>Select required number of signatures</span>
|
||||
<label><span translate>Required number of signatures</span>
|
||||
<select class="m10t" ng-model="requiredCopayers" ng-options="requiredCopayers as requiredCopayers for requiredCopayers in create.RCValues" ng-disabled="totalCopayers == 1">
|
||||
</select>
|
||||
</label>
|
||||
|
|
@ -89,27 +102,67 @@
|
|||
</div>
|
||||
<div ng-hide="hideAdv" class="row">
|
||||
<div class="large-12 columns">
|
||||
<label for="network-name" class="line-b oh">
|
||||
<label ng-show="create.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>
|
||||
<!-- 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>
|
||||
</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">
|
||||
<span translate>Testnet</span>
|
||||
<switch id="network-name" name="isTestnet" ng-model="isTestnet" class="green right m5t m10b"></switch>
|
||||
</label>
|
||||
<label for="ext-master" class="m10t">
|
||||
<span translate>Master extended private key</span>
|
||||
<small translate>If not given, a secure key will be generated</small>
|
||||
|
||||
<label ng-show="!hwLedger" 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="line-b oh" ng-show="!setSeed && !hwLedger" ><span translate>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>
|
||||
|
||||
<label for="ext-master" class="m10t" ng-show="setSeed && !hwLedger">
|
||||
<span translate>Wallet Seed</span>
|
||||
<small translate>Enter the seed words (BIP39)</small>
|
||||
<input id="ext-master"
|
||||
type="text"
|
||||
placeholder="{{'BIP32 master extended private key'|translate}}"
|
||||
name="privateKey" ng-model="privateKey">
|
||||
</label>
|
||||
<label for="passphrase" class="line-b oh" ng-show="setSeed && !hwLedger"><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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<button type="submit" class="button round black expand m0" ng-show="totalCopayers != 1" ng-disabled="setupForm.$invalid || create.loading">
|
||||
<button type="submit" class="button round black expand m0" ng-show="totalCopayers != 1" ng-disabled="setupForm.$invalid || create.loading || create.ledger">
|
||||
<span translate>Create {{requiredCopayers}}-of-{{totalCopayers}} wallet</span>
|
||||
</button>
|
||||
|
||||
<button type="submit" class="button round black expand m0" ng-show="totalCopayers == 1" ng-disabled="setupForm.$invalid || create.loading">
|
||||
<button type="submit" class="button round black expand m0" ng-show="totalCopayers == 1" ng-disabled="setupForm.$invalid || create.loading || create.ledger">
|
||||
<span translate>Create new wallet</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
ng-if="agreed && index.hasProfile"
|
||||
class="topbar-container"
|
||||
ng-include="'views/includes/topbar.html'"
|
||||
ng-init="titleSection='Terms of Use'; goBackToState = 'about'; noColor = true">
|
||||
ng-init="titleSection='Terms of Use'; goBackToState = 'about'; noColor = true"
|
||||
>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="content p20b" ng-class="{'disclaimer':!index.hasProfile}">
|
||||
<div class="content p20b" ng-class="{'disclaimer':!index.hasProfile}" ng-controller="disclaimerController">
|
||||
<h4 class="title m0" ng-show="!index.hasProfile">
|
||||
<span translate>Terms of Use</span>
|
||||
<logo class="right" width="40"></logo>
|
||||
|
|
|
|||
106
public/views/export.html
Normal file
106
public/views/export.html
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<div
|
||||
class="topbar-container"
|
||||
ng-include="'views/includes/topbar.html'"
|
||||
ng-init="titleSection='Export'; goBackToState = 'preferencesAdvanced'">
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="content p20v" ng-controller="backupController as backup">
|
||||
<div class="row">
|
||||
<div class="columns" ng-show="!backup.backupWalletPlainText">
|
||||
<div class="text-warning size-14 m20b" ng-show="backup.error">
|
||||
<i class="fi-alert size-12"></i>
|
||||
<span translate> Failed to export </span>
|
||||
</div>
|
||||
|
||||
<div class="text-warning size-14 m20b" ng-show="backup.isEncrypted">
|
||||
<i class="fi-alert size-12"></i>
|
||||
<span translate> The private key for this wallet is encrypted. Exporting keep the private key encrypted in the export archive.</span>
|
||||
|
||||
</div>
|
||||
|
||||
<label for="password" translate>Set up a Export Password </label>
|
||||
<div class="input">
|
||||
<input type="password" class="form-control"
|
||||
placeholder="{{'Your export password'|translate}}"
|
||||
name="password" ng-model="backup.password">
|
||||
</div>
|
||||
|
||||
<label for="password" translate>Repeat password</label>
|
||||
<div class="input">
|
||||
<input type="password" class="form-control"
|
||||
placeholder="{{'Repeat password'|translate}}"
|
||||
name="password" ng-model="backup.repeatpassword">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row" ng-hide="index.isPrivKeyExternal">
|
||||
<div class="large-12 columns">
|
||||
<label for="no-sign" class="line-b oh">
|
||||
<span translate>Do not include private key</span>
|
||||
<switch id="no-sign" name="noSign" ng-model="noSign" class="green right m5t m10b"></switch>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="box-notification" ng-show="index.isPrivKeyExternal">
|
||||
<span class="text-warning size-14">
|
||||
<i class="fi-alert"></i>
|
||||
<span translate>
|
||||
WARNING: The private key of this wallet is not available. The export allows to check the wallet balance, transaction history, and create spend proposals from the export. However, does not allow to approve (sign) proposals, so <b>funds will not be accessible from the export</b>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="box-notification" ng-show="noSign">
|
||||
<span class="text-warning size-14">
|
||||
<i class="fi-alert"></i>
|
||||
<span translate>
|
||||
WARNING: Not including the private key allows to check the wallet balance, transaction history, and create spend proposals from the export. However, does not allow to approve (sign) proposals, so <b>funds will not be accessible from the export</b>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<button class="black round expand m0" ng-click="backup.downloadWalletBackup()"
|
||||
ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-show="!backup.isSafari && !backup.isCordova"><i class="fi-download"></i>
|
||||
<span translate>Download backup</span></button>
|
||||
<button class="black round expand m0" ng-click="backup.viewWalletBackup()"
|
||||
ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-show="backup.isSafari && !backup.isCordova"><i class="fi-eye"></i>
|
||||
<span translate>View backup</span></button>
|
||||
<div ng-show="backup.isCordova">
|
||||
<h4 translate>Backup options</h4>
|
||||
<button class="black round expand" ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-click="backup.copyWalletBackup()"><i class="fi-clipboard-pencil"></i>
|
||||
<span translate>Copy to clipboard</span></button>
|
||||
<button class="black round expand m0" ng-disabled="(!backup.password || backup.password != backup.repeatpassword)"
|
||||
ng-style="{'background-color':index.backgroundColor}"
|
||||
ng-click="backup.sendWalletBackup()"><i class="fi-mail"></i>
|
||||
<span translate>Send by email</span></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="backup.backupWalletPlainText">
|
||||
<div class="large-12 columns">
|
||||
<h3 translate>Copy backup to a safe place</h3>
|
||||
<div class="input">
|
||||
<textarea rows="12">{{backup.backupWalletPlainText}}</textarea>
|
||||
</div>
|
||||
<div class="size-12 text-gray text-right">
|
||||
<i class="icon-compose"></i>
|
||||
<span translate>Copy this text as it is to a safe place (notepad or email)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="extra-margin-bottom"></div>
|
||||
|
|
@ -4,8 +4,8 @@
|
|||
ng-init="titleSection='Import wallet'; goBackToState = 'add'; noColor = true">
|
||||
</div>
|
||||
|
||||
<div class="content p20v" ng-controller="importController as import">
|
||||
<div class="onGoingProcess" ng-show="import.loading">
|
||||
<div class="content p20v" ng-controller="importController as import" ng-init="type='12'">
|
||||
<div class="onGoingProcess" ng-show="import.loading && !import.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
|
|
@ -17,11 +17,112 @@
|
|||
<span translate>Importing wallet...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="onGoingProcess" ng-show="import.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
<span translate>Connecting to Ledger Wallet...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="create-tab small-only-text-center" ng-hide="create.hideTabs">
|
||||
<div class="row" ng-show="!index.isChromeApp">
|
||||
<div class="tab-container small-6 medium-6 large-6">
|
||||
<a href
|
||||
ng-class="{'selected': type =='12'}"
|
||||
ng-click="import.setType('12')" translate>Walled Seed</a>
|
||||
</div>
|
||||
<div class="tab-container small-6 medium-6 large-6">
|
||||
<a href
|
||||
ng-class="{'selected': type=='file'}"
|
||||
ng-click="import.setType('file')" translate>File/Text Backup</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" ng-show="index.isChromeApp">
|
||||
<div class="tab-container small-4 medium-4 large-4">
|
||||
<a href
|
||||
ng-class="{'selected': type =='12'}"
|
||||
ng-click="import.setType('12')" translate>Walled Seed</a>
|
||||
</div>
|
||||
<div class="tab-container small-4 medium-4 large-4">
|
||||
<a href
|
||||
ng-class="{'selected': type=='file'}"
|
||||
ng-click="import.setType('file')" translate>File/Text Backup</a>
|
||||
</div>
|
||||
<div class="tab-container small-4 medium-4 large-4">
|
||||
<a href
|
||||
ng-class="{'selected': type=='ledger'}"
|
||||
ng-click="import.setType('ledger')" translate>Ledger</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="row" ng-show="type == '12' ">
|
||||
<div class="large-12 columns">
|
||||
<form name="importForm" ng-submit="import.import(importForm)" novalidate>
|
||||
<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 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="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 line-b"><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>
|
||||
</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}}
|
||||
|
|
@ -43,7 +144,7 @@
|
|||
<textarea class="form-control" name="backupText" ng-model="import.backupText" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<label for="password"><span translate>Password</span> <small translate>Required</small>
|
||||
<label for="password"><span translate>Password</span>
|
||||
</label>
|
||||
<div class="input">
|
||||
<input type="password" class="form-control" placeholder="{{'Your backup password'|translate}}"
|
||||
|
|
@ -64,6 +165,35 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-show="type == 'ledger'">
|
||||
<div class="large-12 columns">
|
||||
<form name="importForm3" ng-submit="import.importLedger(importForm3)" novalidate>
|
||||
<div class="box-notification" ng-show="import.error">
|
||||
<span class="text-warning size-14">
|
||||
{{import.error|translate}}
|
||||
</span>
|
||||
</div>
|
||||
<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 backup
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="extra-margin-bottom"></div>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
<p class="text-warning size-12 columns m20t text-center" ng-show="index.askPassword.isSetup">
|
||||
<i class="fi-alert"></i>
|
||||
<span translate ng-show="!pass.error">Password cannot be recovered. Be sure to write it down</span>
|
||||
<span translate ng-show="!pass.error"> Your wallet key will be encrypted. Password cannot be recovered. Be sure to write it down</span>
|
||||
|
||||
<span ng-show="pass.error">{{pass.error|translate}}</span>
|
||||
</p>
|
||||
|
|
|
|||
6
public/views/includes/walletInfo.html
Normal file
6
public/views/includes/walletInfo.html
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<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>
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
|
||||
<div class="content p20v" ng-controller="joinController as join">
|
||||
<div class="onGoingProcess" ng-show="join.loading">
|
||||
<div class="onGoingProcess" ng-show="join.loading && !join.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
|
|
@ -19,6 +19,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="onGoingProcess" ng-show="join.ledger">
|
||||
<div class="onGoingProcess-content" ng-style="{'background-color':'#222'}">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
<span translate>Connecting to Ledger Wallet...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="large-12 columns">
|
||||
|
||||
|
|
@ -72,16 +85,60 @@
|
|||
<i ng-show="join.hideAdv" class="icon-arrow-up4"></i>
|
||||
</a>
|
||||
<div ng-show="join.hideAdv" class="row">
|
||||
<div class="large-12 columns">
|
||||
<label for="ext-master">{{'Master extended private key'|translate}}
|
||||
<small translate>If not given, a secure key will be generated</small>
|
||||
<input id="ext-master"
|
||||
type="text"
|
||||
placeholder="{{'BIP32 master extended private key'|translate}}"
|
||||
name="privateKey" ng-model="privateKey">
|
||||
<div class="large-12 columns" ng-show="join.isChromeApp()">
|
||||
<label for="hw-ledger" class="oh">
|
||||
<span translate>Use Ledger hardware wallet</span>
|
||||
<switch id="hw-ledger" name="hwLedger" ng-model="hwLedger" class="green right m5t m10b"></switch>
|
||||
</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">
|
||||
</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>
|
||||
-->
|
||||
<div class="large-12 columns">
|
||||
|
||||
<label ng-show="!hwLedger" 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="line-b oh" ng-show="!setSeed && !hwLedger" ><span translate>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>
|
||||
|
||||
<label for="ext-master" class="m10t" ng-show="setSeed && !hwLedger">
|
||||
<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="line-b oh" ng-show="setSeed && !hwLedger"><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>
|
||||
|
||||
<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>
|
||||
|
||||
<button translate type="submit" class="button expand black m0 round"
|
||||
ng-disabled="joinForm.$invalid || join.loading">Join</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -36,16 +36,31 @@
|
|||
<span ng-style="{'color':index.backgroundColor}">█</span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="line-b p20">
|
||||
<span translate>Encrypt Private Key</span>
|
||||
<li class="line-b p20" ng-hide="index.isPrivKeyExternal">
|
||||
<span translate>Request Password for Spending Funds</span>
|
||||
<switch id="network-name" name="encrypt" ng-model="encrypt" class="green right"></switch>
|
||||
</li>
|
||||
|
||||
<h4 class="title m0"> </h4>
|
||||
<li class="line-b p20" ng-click="$root.go('backup')">
|
||||
</li>
|
||||
<li class="line-b p20" ng-show="index.isPrivKeyExternal">
|
||||
<span translate>Hardware wallet</span>
|
||||
<span class="right text-gray">
|
||||
{{preferences.externalSource}}
|
||||
<!-- (Accont {{preferences.externalAccount}}) -->
|
||||
</span>
|
||||
</li>
|
||||
<li class="line-b p20" ng-click="$root.go('backup')" ng-hide="index.isPrivKeyExternal">
|
||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||
<span class="text-warning right" ng-show="index.needsBackup">
|
||||
<i class="fi-alert"></i> <span translate> Still not done</span>
|
||||
</span>
|
||||
<span translate>Backup</span>
|
||||
</li>
|
||||
|
||||
<h4 class="title m0"> </h4>
|
||||
<li class="line-b p20" ng-click="$root.go('export')" ng-hide="preferences.externalIndex >= 0">
|
||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||
<span translate>Export</span>
|
||||
</li>
|
||||
|
||||
<li class="line-b p20" ng-click="$root.go('preferencesAdvanced')">
|
||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||
<span translate>Advanced</span>
|
||||
|
|
@ -59,7 +74,7 @@
|
|||
<span translate>Language</span>
|
||||
<span class="right text-gray">
|
||||
<i class="icon-arrow-right3 size-24 right"></i>
|
||||
{{index.defaultLanguageName|translate}}
|
||||
{{preferences.currentLanguageName|translate}}
|
||||
</span>
|
||||
</li>
|
||||
<li class="line-b p20" ng-show="!index.noFocusedWallet" ng-click="$root.go('preferencesUnit')">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@
|
|||
<span translate>Scan addresses for funds</span>
|
||||
</li>
|
||||
|
||||
<li class="line-b p20" ng-click="$root.go('export')">
|
||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||
<span translate>Export</span>
|
||||
</li>
|
||||
|
||||
<li class="line-b p20" ng-click="$root.go('delete')">
|
||||
<i class="icon-arrow-right3 size-24 right text-gray"></i>
|
||||
<span translate>Delete Wallet</span>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<div class="content preferences" ng-controller="preferencesLanguageController as prefLang">
|
||||
<div class="animated infinite flash text-center m20t text-gray" ng-show="prefLang.loading" translate>Applying changes</div>
|
||||
<div ng-show="!prefLang.loading"
|
||||
ng-repeat="lang in index.availableLanguages"
|
||||
ng-repeat="lang in prefLang.availableLanguages"
|
||||
ng-click="prefLang.save(lang.isoCode)" class="line-b p20 size-14">
|
||||
<span>{{lang.name}}</span>
|
||||
<i class="fi-check size-16 right" ng-show="index.defaultLanguageIsoCode == lang.isoCode"></i>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="splash content text-center" ng-if="!index.hasProfile">
|
||||
<div class="splash content text-center" ng-if="!index.hasProfile" ng-controller="splashController">
|
||||
<div class="row">
|
||||
<div class="medium-centered small-centered large-centered columns">
|
||||
<div class="m20t">
|
||||
|
|
|
|||
|
|
@ -123,19 +123,17 @@
|
|||
{{(index.alias || index.walletName)}}
|
||||
</p>
|
||||
<div class="size-12 text-gray">
|
||||
<span translate>Multisignature wallet</span>
|
||||
<span translate>Multisignature wallet</span>
|
||||
(<span translate>{{index.m}}-of-{{index.n}}</span>)
|
||||
<span ng-if="index.network != 'livenet'">- Testnet</span>
|
||||
<span ng-if="!index.canSign"> - <span translate>No Private key</span></span>
|
||||
<span ng-include="'views/includes/walletInfo.html'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="!index.isShared">
|
||||
<p class="m0">
|
||||
{{(index.alias || index.walletName)}}
|
||||
</p>
|
||||
<div class="size-12 text-gray" ng-if="index.network != 'livenet'">
|
||||
Testnet
|
||||
<span ng-if="!index.canSign"> - <span translate>No Private key</span></span>
|
||||
<div class="size-12 text-gray">
|
||||
<span ng-include="'views/includes/walletInfo.html'"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -215,16 +213,14 @@
|
|||
<!-- Address-->
|
||||
<div class="large-12 columns">
|
||||
<h2 class="text-center m10t" translate>My Bitcoin address</h2>
|
||||
<div >
|
||||
<div>
|
||||
<div class="box-notification" ng-show="home.addrError">
|
||||
<span class="text-warning">
|
||||
{{home.addrError|translate}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="box-notification" ng-show="home.addrError">
|
||||
<span class="text-warning">
|
||||
{{home.addrError|translate}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="text-center" ng-click="home.copyAddress(home.addr[index.walletId])">
|
||||
<div class="text-center" ng-click="home.copyAddress(home.addr[index.walletId])" ng-show="home.addr[index.walletId] || home.generatingAddress">
|
||||
<qrcode size="220" data="bitcoin:{{home.addr[index.walletId]}}"></qrcode>
|
||||
<div ng-show="home.generatingAddress" style="position:relative; top:-226px; height:0px">
|
||||
<div style="height:220px; width:220px; margin:auto; background: url(img/qr.png) white">
|
||||
|
|
@ -246,15 +242,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="m10t text-center" ng-show="index.isCordova">
|
||||
<span class="button outline dark-gray tiny round"
|
||||
ng-click="home.shareAddress(home.addr[index.walletId])">
|
||||
<i class="fi-share"></i>
|
||||
<span translate>Share address</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="line-t size-12" translate>
|
||||
Share this wallet address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.
|
||||
<div ng-show="home.addr[index.walletId]">
|
||||
<div class="m10t text-center" ng-show="index.isCordova">
|
||||
<span class="button outline dark-gray tiny round"
|
||||
ng-click="home.shareAddress(home.addr[index.walletId])">
|
||||
<i class="fi-share"></i>
|
||||
<span translate>Share address</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="line-t size-12" translate>
|
||||
Share this wallet address to receive payments. To protect your privacy, new addresses are generated automatically once you use them.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -375,7 +373,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row" ng-hide="home.hideNote">
|
||||
<div class="large-12 columns">
|
||||
<label for="comment"><span translate>Note</span>
|
||||
<small translate ng-hide="!sendForm.comment.$pristine">optional</small>
|
||||
|
|
|
|||
|
|
@ -573,6 +573,13 @@ button.radius, .button.radius {
|
|||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
label small {
|
||||
font-size: 10px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
label small.has-error {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,86 +1,33 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('backupController',
|
||||
function($rootScope, $scope, $timeout, backupService, profileService, isMobile, isCordova, notification, go, gettext, gettextCatalog) {
|
||||
this.isSafari = isMobile.Safari();
|
||||
this.isCordova = isCordova;
|
||||
this.error = null;
|
||||
this.success = null;
|
||||
angular.module('copayApp.controllers').controller('wordsController',
|
||||
function($rootScope, $scope, $timeout, profileService, go, gettext, confirmDialog, notification) {
|
||||
|
||||
var fc = profileService.focusedClient;
|
||||
this.isEncrypted = fc.isPrivKeyEncrypted();
|
||||
var msg = gettext('Are you sure you want to delete the backup words?');
|
||||
var successMsg = gettext('Backup words deleted');
|
||||
|
||||
this.downloadWalletBackup = function() {
|
||||
var self = this;
|
||||
var opts = {
|
||||
noSign: $scope.noSign,
|
||||
};
|
||||
backupService.walletDownload(this.password, opts, function(err) {
|
||||
if (err) {
|
||||
self.error = true;
|
||||
return ;
|
||||
}
|
||||
this.done = function() {
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
notification.success(gettext('Backup created'), gettext('Encrypted backup file saved'));
|
||||
go.walletHome();
|
||||
};
|
||||
|
||||
this.delete = function() {
|
||||
var fc = profileService.focusedClient;
|
||||
confirmDialog.show(msg,function(ok){
|
||||
if (ok) {
|
||||
fc.clearMnemonic();
|
||||
profileService.updateCredentialsFC(function() {
|
||||
notification.success(successMsg);
|
||||
go.walletHome();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.getBackup = function() {
|
||||
var opts = {
|
||||
noSign: $scope.noSign,
|
||||
};
|
||||
var fc = profileService.focusedClient;
|
||||
var words = fc.getMnemonic();
|
||||
|
||||
var ew = backupService.walletExport(this.password, opts);
|
||||
if (!ew) {
|
||||
this.error = true;
|
||||
} else {
|
||||
this.error = false;
|
||||
}
|
||||
return ew;
|
||||
};
|
||||
|
||||
this.viewWalletBackup = function() {
|
||||
var self = this;
|
||||
$timeout(function() {
|
||||
var ew = self.getBackup();
|
||||
if (!ew) return;
|
||||
self.backupWalletPlainText = ew;
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.copyWalletBackup = function() {
|
||||
var ew = this.getBackup();
|
||||
if (!ew) return;
|
||||
window.cordova.plugins.clipboard.copy(ew);
|
||||
window.plugins.toast.showShortCenter(gettextCatalog.getString('Copied to clipboard'));
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
};
|
||||
|
||||
this.sendWalletBackup = function() {
|
||||
var fc = profileService.focusedClient;
|
||||
if (isMobile.Android() || isMobile.Windows()) {
|
||||
window.ignoreMobilePause = true;
|
||||
}
|
||||
window.plugins.toast.showShortCenter(gettextCatalog.getString('Preparing backup...'));
|
||||
var name = (fc.credentials.walletName || fc.credentials.walletId);
|
||||
if (fc.alias) {
|
||||
name = fc.alias + ' [' + name + ']';
|
||||
}
|
||||
var ew = this.getBackup();
|
||||
if (!ew) return;
|
||||
|
||||
if( $scope.noSign)
|
||||
name = name + '(No Private Key)';
|
||||
|
||||
var properties = {
|
||||
subject: 'Copay Wallet Backup: ' + name,
|
||||
body: 'Here is the encrypted backup of the wallet ' + name + ': \n\n' + ew + '\n\n To import this backup, copy all text between {...}, including the symbols {}',
|
||||
isHtml: false
|
||||
};
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
window.plugin.email.open(properties);
|
||||
};
|
||||
if (words)
|
||||
this.mnemonicWords = words.split(' ');
|
||||
|
||||
this.mnemonicHasPassphrase = fc.mnemonicHasPassphrase();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('createController',
|
||||
function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isMobile, isCordova, gettext) {
|
||||
function($scope, $rootScope, $location, $timeout, $log, lodash, go, profileService, configService, isMobile, isCordova, gettext, isChromeApp, ledger) {
|
||||
|
||||
var self = this;
|
||||
var defaults = configService.getDefaults();
|
||||
|
|
@ -42,6 +42,10 @@ angular.module('copayApp.controllers').controller('createController',
|
|||
updateRCSelect(tc);
|
||||
};
|
||||
|
||||
this.isChromeApp = function() {
|
||||
return isChromeApp;
|
||||
};
|
||||
|
||||
this.create = function(form) {
|
||||
if (form && form.$invalid) {
|
||||
this.error = gettext('Please enter the required fields');
|
||||
|
|
@ -51,40 +55,77 @@ angular.module('copayApp.controllers').controller('createController',
|
|||
m: $scope.requiredCopayers,
|
||||
n: $scope.totalCopayers,
|
||||
name: form.walletName.$modelValue,
|
||||
extendedPrivateKey: form.privateKey.$modelValue,
|
||||
myName: $scope.totalCopayers > 1 ? form.myName.$modelValue : null,
|
||||
networkName: form.isTestnet.$modelValue ? 'testnet' : 'livenet'
|
||||
networkName: form.isTestnet.$modelValue ? 'testnet' : 'livenet',
|
||||
};
|
||||
self.loading = true;
|
||||
var setSeed = form.setSeed.$modelValue;
|
||||
if (setSeed) {
|
||||
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;
|
||||
} else {
|
||||
opts.passphrase = form.createPassphrase.$modelValue;
|
||||
}
|
||||
|
||||
if (setSeed && !opts.mnemonic && !opts.extendedPrivateKey) {
|
||||
this.error = gettext('Please enter the wallet seed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.hwLedger.$modelValue) {
|
||||
self.ledger = true;
|
||||
// TODO : account
|
||||
ledger.getInfoForNewWallet(0, function(err, lopts) {
|
||||
self.ledger = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
$scope.$apply();
|
||||
return;
|
||||
}
|
||||
opts = lodash.assign(lopts, opts);
|
||||
self._create(opts);
|
||||
});
|
||||
} else {
|
||||
self._create(opts);
|
||||
}
|
||||
};
|
||||
|
||||
this._create = function(opts) {
|
||||
self.loading = true;
|
||||
$timeout(function() {
|
||||
profileService.createWallet(opts, function(err, secret) {
|
||||
profileService.createWallet(opts, function(err, secret, walletId) {
|
||||
self.loading = false;
|
||||
if (err) {
|
||||
$log.debug(err);
|
||||
$log.warn(err);
|
||||
self.error = err;
|
||||
$timeout(function() {
|
||||
$rootScope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (opts.mnemonic || opts.externalSource || opts.extendedPrivateKey) {
|
||||
if (opts.n == 1) {
|
||||
$rootScope.$emit('Local/WalletImported', walletId);
|
||||
}
|
||||
}
|
||||
else {
|
||||
go.walletHome();
|
||||
}
|
||||
go.walletHome();
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
this.formFocus = function(what) {
|
||||
if (!this.isWindowsPhoneApp) return
|
||||
|
||||
if (what && what == 'my-name') {
|
||||
this.hideWalletName = true;
|
||||
this.hideTabs = true;
|
||||
}
|
||||
else if (what && what == 'wallet-name'){
|
||||
this.hideTabs = true;
|
||||
}
|
||||
else {
|
||||
} else if (what && what == 'wallet-name') {
|
||||
this.hideTabs = true;
|
||||
} else {
|
||||
this.hideWalletName = false;
|
||||
this.hideTabs = false;
|
||||
}
|
||||
|
|
|
|||
28
src/js/controllers/disclaimer.js
Normal file
28
src/js/controllers/disclaimer.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('disclaimerController',
|
||||
function($scope, $timeout, storageService, applicationService, go, gettextCatalog, isCordova) {
|
||||
storageService.getCopayDisclaimerFlag(function(err, val) {
|
||||
$scope.agreed = val;
|
||||
$timeout(function() {
|
||||
$scope.$digest();
|
||||
}, 1);
|
||||
});
|
||||
|
||||
$scope.agree = function() {
|
||||
if (isCordova) {
|
||||
window.plugins.spinnerDialog.show(null, gettextCatalog.getString('Loading...'), true);
|
||||
}
|
||||
$scope.loading = true;
|
||||
$timeout(function() {
|
||||
storageService.setCopayDisclaimerFlag(function(err) {
|
||||
$timeout(function() {
|
||||
if (isCordova) {
|
||||
window.plugins.spinnerDialog.hide();
|
||||
}
|
||||
applicationService.restart();
|
||||
}, 1000);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
});
|
||||
86
src/js/controllers/export.js
Normal file
86
src/js/controllers/export.js
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('backupController',
|
||||
function($rootScope, $scope, $timeout, backupService, profileService, isMobile, isCordova, notification, go, gettext, gettextCatalog) {
|
||||
this.isSafari = isMobile.Safari();
|
||||
this.isCordova = isCordova;
|
||||
this.error = null;
|
||||
this.success = null;
|
||||
|
||||
var fc = profileService.focusedClient;
|
||||
this.isEncrypted = fc.isPrivKeyEncrypted();
|
||||
|
||||
this.downloadWalletBackup = function() {
|
||||
var self = this;
|
||||
var opts = {
|
||||
noSign: $scope.noSign,
|
||||
};
|
||||
backupService.walletDownload(this.password, opts, function(err) {
|
||||
if (err) {
|
||||
self.error = true;
|
||||
return ;
|
||||
}
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
notification.success(gettext('Success'), gettext('Encrypted export file saved'));
|
||||
go.walletHome();
|
||||
});
|
||||
};
|
||||
|
||||
this.getBackup = function() {
|
||||
var opts = {
|
||||
noSign: $scope.noSign,
|
||||
};
|
||||
|
||||
var ew = backupService.walletExport(this.password, opts);
|
||||
if (!ew) {
|
||||
this.error = true;
|
||||
} else {
|
||||
this.error = false;
|
||||
}
|
||||
return ew;
|
||||
};
|
||||
|
||||
this.viewWalletBackup = function() {
|
||||
var self = this;
|
||||
$timeout(function() {
|
||||
var ew = self.getBackup();
|
||||
if (!ew) return;
|
||||
self.backupWalletPlainText = ew;
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
}, 100);
|
||||
};
|
||||
|
||||
this.copyWalletBackup = function() {
|
||||
var ew = this.getBackup();
|
||||
if (!ew) return;
|
||||
window.cordova.plugins.clipboard.copy(ew);
|
||||
window.plugins.toast.showShortCenter(gettextCatalog.getString('Copied to clipboard'));
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
};
|
||||
|
||||
this.sendWalletBackup = function() {
|
||||
var fc = profileService.focusedClient;
|
||||
if (isMobile.Android() || isMobile.Windows()) {
|
||||
window.ignoreMobilePause = true;
|
||||
}
|
||||
window.plugins.toast.showShortCenter(gettextCatalog.getString('Preparing backup...'));
|
||||
var name = (fc.credentials.walletName || fc.credentials.walletId);
|
||||
if (fc.alias) {
|
||||
name = fc.alias + ' [' + name + ']';
|
||||
}
|
||||
var ew = this.getBackup();
|
||||
if (!ew) return;
|
||||
|
||||
if( $scope.noSign)
|
||||
name = name + '(No Private Key)';
|
||||
|
||||
var properties = {
|
||||
subject: 'Copay Wallet Backup: ' + name,
|
||||
body: 'Here is the encrypted backup of the wallet ' + name + ': \n\n' + ew + '\n\n To import this backup, copy all text between {...}, including the symbols {}',
|
||||
isHtml: false
|
||||
};
|
||||
$rootScope.$emit('Local/BackupDone');
|
||||
window.plugin.email.open(properties);
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('importController',
|
||||
function($scope, $rootScope, $location, $timeout, $log, profileService, notification, go, isMobile, isCordova, sjcl, gettext) {
|
||||
function($scope, $rootScope, $location, $timeout, $log, profileService, notification, go, isMobile, isCordova, sjcl, gettext, lodash, ledger) {
|
||||
|
||||
var self = this;
|
||||
|
||||
|
|
@ -11,15 +11,23 @@ angular.module('copayApp.controllers').controller('importController',
|
|||
|
||||
window.ignoreMobilePause = true;
|
||||
$scope.$on('$destroy', function() {
|
||||
$timeout(function(){
|
||||
$timeout(function() {
|
||||
window.ignoreMobilePause = false;
|
||||
}, 100);
|
||||
});
|
||||
|
||||
var _import = function(str, opts) {
|
||||
this.setType = function(type) {
|
||||
$scope.type = type;
|
||||
this.error = null;
|
||||
$timeout(function() {
|
||||
$rootScope.$apply();
|
||||
});
|
||||
};
|
||||
|
||||
var _importBlob = function(str, opts) {
|
||||
var str2, err;
|
||||
try {
|
||||
str2 = sjcl.decrypt(self.password, str);
|
||||
str2 = sjcl.decrypt(self.password, str);
|
||||
} catch (e) {
|
||||
err = gettext('Could not decrypt file, check your password');
|
||||
$log.warn(e);
|
||||
|
|
@ -27,7 +35,9 @@ angular.module('copayApp.controllers').controller('importController',
|
|||
|
||||
if (err) {
|
||||
self.error = err;
|
||||
$rootScope.$apply();
|
||||
$timeout(function() {
|
||||
$rootScope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -41,8 +51,7 @@ angular.module('copayApp.controllers').controller('importController',
|
|||
self.loading = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$rootScope.$emit('Local/WalletImported', walletId);
|
||||
go.walletHome();
|
||||
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
|
||||
|
|
@ -51,19 +60,63 @@ angular.module('copayApp.controllers').controller('importController',
|
|||
}, 100);
|
||||
};
|
||||
|
||||
|
||||
var _importExtendedPrivateKey = function(xPrivKey) {
|
||||
self.loading = true;
|
||||
|
||||
$timeout(function() {
|
||||
profileService.importExtendedPrivateKey(xPrivKey, function(err, walletId) {
|
||||
self.loading = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
return $timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
$rootScope.$emit('Local/WalletImported', walletId);
|
||||
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
|
||||
go.walletHome();
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
|
||||
|
||||
var _importMnemonic = function(words, opts) {
|
||||
self.loading = true;
|
||||
|
||||
$timeout(function() {
|
||||
profileService.importMnemonic(words, opts, function(err, walletId) {
|
||||
self.loading = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
return $timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
$rootScope.$emit('Local/WalletImported', walletId);
|
||||
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
|
||||
go.walletHome();
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
$scope.getFile = function() {
|
||||
// If we use onloadend, we need to check the readyState.
|
||||
reader.onloadend = function(evt) {
|
||||
if (evt.target.readyState == FileReader.DONE) { // DONE == 2
|
||||
_import(evt.target.result);
|
||||
_importBlob(evt.target.result);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.import = function(form) {
|
||||
this.importBlob = function(form) {
|
||||
if (form.$invalid) {
|
||||
this.error = gettext('There is an error in the form');
|
||||
$scope.$apply();
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -73,14 +126,102 @@ angular.module('copayApp.controllers').controller('importController',
|
|||
|
||||
if (!backupFile && !backupText) {
|
||||
this.error = gettext('Please, select your backup file');
|
||||
$scope.$apply();
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupFile) {
|
||||
reader.readAsBinaryString(backupFile);
|
||||
} else {
|
||||
_import(backupText);
|
||||
_importBlob(backupText);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.importMnemonic = function(form) {
|
||||
if (form.$invalid) {
|
||||
this.error = gettext('There is an error in the form');
|
||||
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var opts = {};
|
||||
|
||||
var passphrase = form.passphrase.$modelValue;
|
||||
var words = form.words.$modelValue;
|
||||
this.error = null;
|
||||
|
||||
if (!words) {
|
||||
this.error = gettext('Please enter the seed words');
|
||||
} else if (words.indexOf(' ') == -1 && words.indexOf('prv') == 1 && words.length > 108) {
|
||||
return _importExtendedPrivateKey(words)
|
||||
} else {
|
||||
var wordList = words.split(/ /).filter(function(v) {
|
||||
return v.length > 0;
|
||||
});
|
||||
|
||||
// m/ allows to enter a custom derivation
|
||||
if ((wordList.length % 3) != 0 && wordList[0].indexOf('m/') != 0)
|
||||
this.error = gettext('Wrong number of seed words:') + wordList.length;
|
||||
else
|
||||
words = wordList.join(' ');
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
opts.passphrase = form.passphrase.$modelValue || null;
|
||||
opts.networkName = form.isTestnet.$modelValue ? 'testnet' : 'livenet';
|
||||
|
||||
_importMnemonic(words, opts);
|
||||
};
|
||||
|
||||
this.importLedger = function(form) {
|
||||
var self = this;
|
||||
if (form.$invalid) {
|
||||
this.error = gettext('There is an error in the form');
|
||||
$timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
return;
|
||||
}
|
||||
self.ledger = true;
|
||||
// TODO account
|
||||
ledger.getInfoForNewWallet(0, function(err, lopts) {
|
||||
self.ledger = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
$scope.$apply();
|
||||
return;
|
||||
}
|
||||
lopts.externalIndex = $scope.externalIndex;
|
||||
lopts.externalSource = 'ledger';
|
||||
self.loading = true;
|
||||
$log.debug('Import opts', lopts);
|
||||
profileService.importExtendedPublicKey(lopts, function(err, walletId) {
|
||||
self.loading = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
return $timeout(function() {
|
||||
$scope.$apply();
|
||||
});
|
||||
}
|
||||
$rootScope.$emit('Local/WalletImported', walletId);
|
||||
notification.success(gettext('Success'), gettext('Your wallet has been imported correctly'));
|
||||
go.walletHome();
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettextCatalog, gettext, amMoment, nodeWebkit, addonManager, feeService, isChromeApp, bwsError, txFormatService, glideraService, $state) {
|
||||
angular.module('copayApp.controllers').controller('indexController', function($rootScope, $scope, $log, $filter, $timeout, lodash, go, profileService, configService, isCordova, rateService, storageService, addressService, gettext, amMoment, nodeWebkit, addonManager, feeService, isChromeApp, bwsError, txFormatService, uxLanguage, $state, glideraService) {
|
||||
var self = this;
|
||||
self.isCordova = isCordova;
|
||||
self.isChromeApp = isChromeApp;
|
||||
self.onGoingProcess = {};
|
||||
self.limitHistory = 5;
|
||||
|
||||
|
|
@ -39,35 +40,6 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
|
||||
self.tab = 'walletHome';
|
||||
|
||||
self.availableLanguages = [{
|
||||
name: 'English',
|
||||
isoCode: 'en',
|
||||
}, {
|
||||
name: 'Français',
|
||||
isoCode: 'fr',
|
||||
}, {
|
||||
name: 'Italiano',
|
||||
isoCode: 'it',
|
||||
}, {
|
||||
name: 'Deutsch',
|
||||
isoCode: 'de',
|
||||
}, {
|
||||
name: 'Español',
|
||||
isoCode: 'es',
|
||||
}, {
|
||||
name: 'Português',
|
||||
isoCode: 'pt',
|
||||
}, {
|
||||
name: 'Ελληνικά',
|
||||
isoCode: 'el',
|
||||
}, {
|
||||
name: '日本語',
|
||||
isoCode: 'ja',
|
||||
}, {
|
||||
name: 'Pусский',
|
||||
isoCode: 'ru',
|
||||
}];
|
||||
|
||||
self.feeOpts = feeService.feeOpts;
|
||||
|
||||
self.setOngoingProcess = function(processName, isOn) {
|
||||
|
|
@ -130,16 +102,23 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
self.walletId = fc.credentials.walletId;
|
||||
self.isComplete = fc.isComplete();
|
||||
self.canSign = fc.canSign();
|
||||
self.isPrivKeyExternal = fc.isPrivKeyExternal();
|
||||
self.externalSource = fc.getPrivKeyExternalSourceName();
|
||||
self.txps = [];
|
||||
self.copayers = [];
|
||||
self.updateColor();
|
||||
self.updateAlias();
|
||||
self.initGlidera();
|
||||
|
||||
storageService.getBackupFlag(self.walletId, function(err, val) {
|
||||
self.needsBackup = self.network == 'testnet' ? false : !val;
|
||||
if (fc.isPrivKeyExternal()) {
|
||||
self.needsBackup = false;
|
||||
self.openWallet();
|
||||
});
|
||||
} else {
|
||||
storageService.getBackupFlag(self.walletId, function(err, val) {
|
||||
self.needsBackup = self.network == 'testnet' ? false : !val;
|
||||
self.openWallet();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -273,7 +252,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
return cb(null, opts.walletStatus);
|
||||
else {
|
||||
self.updateError = false;
|
||||
return fc.getStatus(function(err, ret) {
|
||||
return fc.getStatus({}, function(err, ret) {
|
||||
if (err) {
|
||||
self.updateError = bwsError.msg(err, gettext('Could not update Wallet'));
|
||||
} else {
|
||||
|
|
@ -293,7 +272,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
if (!opts.quiet)
|
||||
self.setOngoingProcess('updatingStatus', true);
|
||||
|
||||
$log.debug('Updating Status:', fc, tries);
|
||||
$log.debug('Updating Status:', fc.credentials.walletName, tries);
|
||||
get(function(err, walletStatus) {
|
||||
var currentStatusHash = _walletStatusHash(walletStatus);
|
||||
$log.debug('Status update. hash:' + currentStatusHash + ' Try:' + tries);
|
||||
|
|
@ -423,7 +402,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
|
||||
self.updateTxHistory = function(skip) {
|
||||
var fc = profileService.focusedClient;
|
||||
if (!fc.isComplete()) return;
|
||||
if (!fc || !fc.isComplete()) return;
|
||||
if (!skip) {
|
||||
self.txHistory = [];
|
||||
}
|
||||
|
|
@ -680,16 +659,18 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
return str;
|
||||
}
|
||||
|
||||
var step = 6;
|
||||
|
||||
function getHistory(skip, cb) {
|
||||
skip = skip || 0;
|
||||
fc.getTxHistory({
|
||||
skip: skip,
|
||||
limit: 100
|
||||
limit: step,
|
||||
}, function(err, txs) {
|
||||
if (err) return cb(err);
|
||||
if (txs && txs.length > 0) {
|
||||
allTxs.push(txs);
|
||||
return getHistory(skip + 100, cb);
|
||||
return getHistory(skip + step, cb);
|
||||
} else {
|
||||
return cb(null, lodash.flatten(allTxs));
|
||||
}
|
||||
|
|
@ -761,7 +742,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
});
|
||||
};
|
||||
|
||||
self.showErrorPopup = function (msg, cb) {
|
||||
self.showErrorPopup = function(msg, cb) {
|
||||
$log.warn('Showing err popup:' + msg);
|
||||
self.showAlert = {
|
||||
msg: msg,
|
||||
|
|
@ -773,7 +754,6 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
$timeout(function() {
|
||||
$rootScope.$apply();
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
self.recreate = function(cb) {
|
||||
|
|
@ -811,6 +791,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
|
||||
self.startScan = function(walletId) {
|
||||
var c = profileService.walletClients[walletId];
|
||||
if (!c.isComplete()) return;
|
||||
|
||||
if (self.walletId == walletId)
|
||||
self.setOngoingProcess('scanning', true);
|
||||
|
|
@ -827,29 +808,9 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
};
|
||||
|
||||
self.setUxLanguage = function() {
|
||||
var userLang = configService.getSync().wallet.settings.defaultLanguage;
|
||||
if (!userLang) {
|
||||
// Auto-detect browser language
|
||||
var androidLang;
|
||||
|
||||
if (navigator && navigator.userAgent && (androidLang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) {
|
||||
userLang = androidLang[1];
|
||||
} else {
|
||||
// works for iOS and Android 4.x
|
||||
userLang = navigator.userLanguage || navigator.language;
|
||||
}
|
||||
userLang = userLang ? (userLang.split('-', 1)[0] || 'en') : 'en';
|
||||
}
|
||||
if (userLang != gettextCatalog.getCurrentLanguage()) {
|
||||
$log.debug('Setting default language: ' + userLang);
|
||||
gettextCatalog.setCurrentLanguage(userLang);
|
||||
amMoment.changeLocale(userLang);
|
||||
}
|
||||
|
||||
var userLang = uxLanguage.update();
|
||||
self.defaultLanguageIsoCode = userLang;
|
||||
self.defaultLanguageName = lodash.result(lodash.find(self.availableLanguages, {
|
||||
'isoCode': self.defaultLanguageIsoCode
|
||||
}), 'name');
|
||||
self.defaultLanguageName = uxLanguage.getName(userLang);
|
||||
};
|
||||
|
||||
self.initGlidera = function(accessToken) {
|
||||
|
|
@ -1059,7 +1020,9 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
self.needsBackup = false;
|
||||
storageService.setBackupFlag(walletId, function() {
|
||||
addressService.expireAddress(walletId, function(err) {
|
||||
self.startScan(walletId);
|
||||
$timeout(function() {
|
||||
self.startScan(walletId);
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1152,7 +1115,7 @@ angular.module('copayApp.controllers').controller('indexController', function($r
|
|||
});
|
||||
|
||||
$rootScope.$on('Local/ShowAlert', function(event, msg, cb) {
|
||||
self.showErrorPopup(msg,cb);
|
||||
self.showErrorPopup(msg, cb);
|
||||
});
|
||||
|
||||
$rootScope.$on('Local/NeedsPassword', function(event, isSetup, cb) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('joinController',
|
||||
function($scope, $rootScope, $timeout, go, isMobile, notification, profileService, isCordova, $modal, gettext) {
|
||||
function($scope, $rootScope, $timeout, go, isMobile, notification, profileService, isCordova, isChromeApp, $modal, gettext, lodash, ledger) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.isChromeApp = function() {
|
||||
return isChromeApp;
|
||||
};
|
||||
|
||||
//TODO : make one function - this was copied from topbar.js
|
||||
var cordovaOpenScanner = function() {
|
||||
window.ignoreMobilePause = true;
|
||||
|
|
@ -145,12 +149,50 @@ angular.module('copayApp.controllers').controller('joinController',
|
|||
}
|
||||
self.loading = true;
|
||||
|
||||
var opts = {
|
||||
secret: form.secret.$modelValue,
|
||||
myName: form.myName.$modelValue,
|
||||
}
|
||||
|
||||
var setSeed = form.setSeed.$modelValue;
|
||||
if (setSeed) {
|
||||
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;
|
||||
} else {
|
||||
opts.passphrase = form.createPassphrase.$modelValue;
|
||||
}
|
||||
|
||||
if (setSeed && !opts.mnemonic && !opts.extendedPrivateKey) {
|
||||
this.error = gettext('Please enter the wallet seed');
|
||||
return;
|
||||
}
|
||||
|
||||
if (form.hwLedger.$modelValue) {
|
||||
self.ledger = true;
|
||||
// TODO account
|
||||
ledger.getInfoForNewWallet(0, function(err, lopts) {
|
||||
self.ledger = false;
|
||||
if (err) {
|
||||
self.error = err;
|
||||
$scope.$apply();
|
||||
return;
|
||||
}
|
||||
opts = lodash.assign(lopts, opts);
|
||||
self._join(opts);
|
||||
});
|
||||
} else {
|
||||
self._join(opts);
|
||||
}
|
||||
};
|
||||
|
||||
this._join = function(opts) {
|
||||
$timeout(function() {
|
||||
profileService.joinWallet({
|
||||
secret: form.secret.$modelValue,
|
||||
extendedPrivateKey: form.privateKey.$modelValue,
|
||||
myName: form.myName.$modelValue
|
||||
}, function(err) {
|
||||
profileService.joinWallet(opts, function(err) {
|
||||
if (err) {
|
||||
self.loading = false;
|
||||
self.error = err;
|
||||
|
|
@ -158,9 +200,14 @@ angular.module('copayApp.controllers').controller('joinController',
|
|||
return
|
||||
}
|
||||
$timeout(function() {
|
||||
go.walletHome();
|
||||
var fc = profileService.focusedClient;
|
||||
if ( fc.isComplete() && (opts.mnemonic || opts.externalSource || opts.extendedPrivateKey)) {
|
||||
$rootScope.$emit('Local/WalletImported', fc.credentials.walletId);
|
||||
} else {
|
||||
go.walletHome();
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('preferencesController',
|
||||
function($scope, $rootScope, $filter, $timeout, $modal, $log, lodash, configService, profileService) {
|
||||
function($scope, $rootScope, $filter, $timeout, $modal, $log, lodash, configService, profileService, uxLanguage) {
|
||||
var config = configService.getSync();
|
||||
this.unitName = config.wallet.settings.unitName;
|
||||
this.bwsurl = config.bws.url;
|
||||
this.currentLanguageName = uxLanguage.getCurrentLanguageName();
|
||||
this.selectedAlternative = {
|
||||
name: config.wallet.settings.alternativeName,
|
||||
isoCode: config.wallet.settings.alternativeIsoCode
|
||||
|
|
@ -13,8 +14,12 @@ angular.module('copayApp.controllers').controller('preferencesController',
|
|||
$scope.glideraEnabled = config.glidera.enabled;
|
||||
$scope.glideraTestnet = config.glidera.testnet;
|
||||
var fc = profileService.focusedClient;
|
||||
if (fc)
|
||||
if (fc) {
|
||||
$scope.encrypt = fc.hasPrivKeyEncrypted();
|
||||
this.externalSource = fc.getPrivKeyExternalSourceName() == 'ledger' ? "Ledger" : null;
|
||||
// TODO externalAccount
|
||||
//this.externalIndex = fc.getExternalIndex();
|
||||
}
|
||||
|
||||
var unwatchSpendUnconfirmed = $scope.$watch('spendUnconfirmed', function(newVal, oldVal) {
|
||||
if (newVal == oldVal) return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('preferencesLanguageController',
|
||||
function($scope, $log, $timeout, configService, go) {
|
||||
function($scope, $log, $timeout, configService, go, uxLanguage) {
|
||||
|
||||
this.availableLanguages = uxLanguage.getLanguages();
|
||||
|
||||
console.log('[preferencesLanguage.js.7]', this.availableLanguages); //TODO
|
||||
this.save = function(newLang) {
|
||||
|
||||
var opts = {
|
||||
|
|
|
|||
32
src/js/controllers/splash.js
Normal file
32
src/js/controllers/splash.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('splashController',
|
||||
function($scope, $timeout, $log, profileService, storageService, go, bwcService) {
|
||||
storageService.getCopayDisclaimerFlag(function(err, val) {
|
||||
if (!val) go.path('disclaimer');
|
||||
|
||||
if (profileService.profile) {
|
||||
go.walletHome();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.create = function(noWallet) {
|
||||
$scope.creatingProfile = true;
|
||||
|
||||
$timeout(function() {
|
||||
profileService.create({
|
||||
noWallet: noWallet
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
$scope.creatingProfile = false;
|
||||
$log.warn(err);
|
||||
$scope.error = err;
|
||||
$scope.$apply();
|
||||
$timeout(function() {
|
||||
$scope.create(noWallet);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.controllers').controller('walletHomeController', function($scope, $rootScope, $timeout, $filter, $modal, $log, notification, txStatus, isCordova, profileService, lodash, configService, rateService, storageService, bitcore, isChromeApp, gettext, gettextCatalog, nodeWebkit, addressService, feeService, bwsError, txFormatService) {
|
||||
angular.module('copayApp.controllers').controller('walletHomeController', function($scope, $rootScope, $timeout, $filter, $modal, $log, notification, txStatus, isCordova, profileService, lodash, configService, rateService, storageService, bitcore, isChromeApp, gettext, gettextCatalog, nodeWebkit, addressService, ledger, feeService, bwsError, confirmDialog, txFormatService) {
|
||||
|
||||
var self = this;
|
||||
$rootScope.hideMenuBar = false;
|
||||
|
|
@ -173,7 +173,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
});
|
||||
};
|
||||
|
||||
var GLIDERA_LOCK_TIME = 6 * 60 * 60 ;
|
||||
var GLIDERA_LOCK_TIME = 6 * 60 * 60;
|
||||
// isGlidera flag is a security mesure so glidera status is not
|
||||
// only determined by the tx.message
|
||||
this.openTxpModal = function(tx, copayers, isGlidera) {
|
||||
|
|
@ -184,7 +184,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
$scope.error = null;
|
||||
$scope.copayers = copayers
|
||||
$scope.copayerId = fc.credentials.copayerId;
|
||||
$scope.canSign = fc.canSign();
|
||||
$scope.canSign = fc.canSign() || fc.isPrivKeyExternal();
|
||||
$scope.loading = null;
|
||||
$scope.color = fc.backgroundColor;
|
||||
|
||||
|
|
@ -192,7 +192,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
if (tx.message === 'Glidera transaction' && isGlidera) {
|
||||
tx.isGlidera = true;
|
||||
if (tx.canBeRemoved) {
|
||||
tx.canBeRemoved = (Date.now()/1000 - (tx.ts || tx.createdOn)) > GLIDERA_LOCK_TIME;
|
||||
tx.canBeRemoved = (Date.now() / 1000 - (tx.ts || tx.createdOn)) > GLIDERA_LOCK_TIME;
|
||||
}
|
||||
}
|
||||
$scope.tx = tx;
|
||||
|
|
@ -244,7 +244,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
$scope.sign = function(txp) {
|
||||
var fc = profileService.focusedClient;
|
||||
|
||||
if (!fc.canSign())
|
||||
if (!fc.canSign() && !fc.isPrivKeyExternal())
|
||||
return;
|
||||
|
||||
if (fc.isPrivKeyEncrypted()) {
|
||||
|
|
@ -257,13 +257,12 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
});
|
||||
return;
|
||||
};
|
||||
self._setOngoingForSigning();
|
||||
|
||||
self.setOngoingProcess(gettext('Signing payment'));
|
||||
$scope.loading = true;
|
||||
$scope.error = null;
|
||||
$timeout(function() {
|
||||
fc.signTxProposal(txp, function(err, txpsi) {
|
||||
profileService.lockFC();
|
||||
profileService.signTxProposal(txp, function(err, txpsi) {
|
||||
self.setOngoingProcess();
|
||||
if (err) {
|
||||
$scope.$emit('UpdateTx');
|
||||
|
|
@ -680,6 +679,10 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
var fc = profileService.focusedClient;
|
||||
// ToDo: use a credential's (or fc's) function for this
|
||||
this.hideNote = !fc.credentials.sharedEncryptingKey;
|
||||
};
|
||||
|
||||
this.setSendError = function(err) {
|
||||
|
|
@ -741,9 +744,17 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
return;
|
||||
};
|
||||
|
||||
var comment = form.comment.$modelValue;
|
||||
|
||||
// ToDo: use a credential's (or fc's) function for this
|
||||
if (comment && !fc.credentials.sharedEncryptingKey) {
|
||||
var msg = 'Could not add message to imported wallet without shared encrypting key';
|
||||
$log.warn(msg);
|
||||
return self.setSendError(gettext(msg));
|
||||
}
|
||||
|
||||
self.setOngoingProcess(gettext('Creating transaction'));
|
||||
$timeout(function() {
|
||||
var comment = form.comment.$modelValue;
|
||||
var paypro = self._paypro;
|
||||
var address, amount;
|
||||
|
||||
|
|
@ -774,7 +785,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
return self.setSendError(err);
|
||||
}
|
||||
|
||||
if (!fc.canSign()) {
|
||||
if (!fc.canSign() && !fc.isPrivKeyExternal()) {
|
||||
$log.info('No signing proposal: No private key')
|
||||
self.setOngoingProcess();
|
||||
self.resetForm();
|
||||
|
|
@ -786,7 +797,6 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
|
||||
self.signAndBroadcast(txp, function(err) {
|
||||
self.setOngoingProcess();
|
||||
profileService.lockFC();
|
||||
self.resetForm();
|
||||
if (err) {
|
||||
self.error = err.message ? err.message : gettext('The payment was created but could not be completed. Please try again from home screen');
|
||||
|
|
@ -801,12 +811,21 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
}, 100);
|
||||
};
|
||||
|
||||
this._setOngoingForSigning = function() {
|
||||
var fc = profileService.focusedClient;
|
||||
|
||||
if (fc.isPrivKeyExternal() && fc.getPrivKeyExternalSourceName() == 'ledger') {
|
||||
self.setOngoingProcess(gettext('Requesting Ledger Wallet to sign'));
|
||||
} else {
|
||||
self.setOngoingProcess(gettext('Signing payment'));
|
||||
}
|
||||
};
|
||||
|
||||
this.signAndBroadcast = function(txp, cb) {
|
||||
var fc = profileService.focusedClient;
|
||||
self.setOngoingProcess(gettext('Signing transaction'));
|
||||
fc.signTxProposal(txp, function(err, signedTx) {
|
||||
profileService.lockFC();
|
||||
|
||||
this._setOngoingForSigning();
|
||||
profileService.signTxProposal(txp, function(err, signedTx) {
|
||||
self.setOngoingProcess();
|
||||
if (err) {
|
||||
err.message = bwsError.msg(err, gettextCatalog.getString('The payment was created but could not be signed. Please try again from home screen'));
|
||||
|
|
@ -1087,6 +1106,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
this.setForm(null, amount, null, feeRate);
|
||||
};
|
||||
|
||||
// TODO: showPopup alike
|
||||
this.confirmDialog = function(msg, cb) {
|
||||
if (isCordova) {
|
||||
navigator.notification.confirm(
|
||||
|
|
@ -1112,7 +1132,7 @@ angular.module('copayApp.controllers').controller('walletHomeController', functi
|
|||
|
||||
this.sendAll = function(amount, feeStr, feeRate) {
|
||||
var self = this;
|
||||
var msg = gettextCatalog.getString("{{fee}} will be discounted for bitcoin networking fees", {
|
||||
var msg = gettextCatalog.getString("{{fee}} will be deducted for bitcoin networking fees", {
|
||||
fee: feeStr
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -81,35 +81,6 @@ angular
|
|||
views: {
|
||||
'main': {
|
||||
templateUrl: 'views/splash.html',
|
||||
controller: function($scope, $timeout, $log, profileService, storageService, go) {
|
||||
storageService.getCopayDisclaimerFlag(function(err, val) {
|
||||
if (!val) go.path('disclaimer');
|
||||
|
||||
if (profileService.profile) {
|
||||
go.walletHome();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.create = function(noWallet) {
|
||||
$scope.creatingProfile = true;
|
||||
|
||||
$timeout(function() {
|
||||
profileService.create({
|
||||
noWallet: noWallet
|
||||
}, function(err) {
|
||||
if (err) {
|
||||
$scope.creatingProfile = false;
|
||||
$log.warn(err);
|
||||
$scope.error = err;
|
||||
$scope.$apply();
|
||||
$timeout(function() {
|
||||
$scope.create(noWallet);
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -131,31 +102,6 @@ angular
|
|||
views: {
|
||||
'main': {
|
||||
templateUrl: 'views/disclaimer.html',
|
||||
controller: function($scope, $timeout, storageService, applicationService, go, gettextCatalog, isCordova) {
|
||||
storageService.getCopayDisclaimerFlag(function(err, val) {
|
||||
$scope.agreed = val;
|
||||
$timeout(function() {
|
||||
$scope.$digest();
|
||||
}, 1);
|
||||
});
|
||||
|
||||
$scope.agree = function() {
|
||||
if (isCordova) {
|
||||
window.plugins.spinnerDialog.show(null, gettextCatalog.getString('Loading...'), true);
|
||||
}
|
||||
$scope.loading = true;
|
||||
$timeout(function() {
|
||||
storageService.setCopayDisclaimerFlag(function(err) {
|
||||
$timeout(function() {
|
||||
if (isCordova) {
|
||||
window.plugins.spinnerDialog.hide();
|
||||
}
|
||||
applicationService.restart();
|
||||
}, 1000);
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -453,6 +399,17 @@ angular
|
|||
},
|
||||
}
|
||||
})
|
||||
.state('export', {
|
||||
url: '/export',
|
||||
templateUrl: 'views/export.html',
|
||||
walletShouldBeComplete: true,
|
||||
needProfile: true,
|
||||
views: {
|
||||
'main': {
|
||||
templateUrl: 'views/export.html'
|
||||
},
|
||||
}
|
||||
})
|
||||
.state('backup', {
|
||||
url: '/backup',
|
||||
templateUrl: 'views/backup.html',
|
||||
|
|
@ -506,22 +463,10 @@ angular
|
|||
needProfile: false
|
||||
});
|
||||
})
|
||||
.run(function($rootScope, $state, $log, gettextCatalog, uriHandler, isCordova, amMoment, profileService, $timeout, nodeWebkit) {
|
||||
.run(function($rootScope, $state, $log, uriHandler, isCordova, profileService, $timeout, nodeWebkit, uxLanguage) {
|
||||
FastClick.attach(document.body);
|
||||
|
||||
// Auto-detect browser language
|
||||
var userLang, androidLang;
|
||||
|
||||
if (navigator && navigator.userAgent && (androidLang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) {
|
||||
userLang = androidLang[1];
|
||||
} else {
|
||||
// works for iOS and Android 4.x
|
||||
userLang = navigator.userLanguage || navigator.language;
|
||||
}
|
||||
|
||||
userLang = userLang ? (userLang.split('-', 1)[0] || 'en') : 'en';
|
||||
gettextCatalog.setCurrentLanguage(userLang);
|
||||
amMoment.changeLocale(userLang);
|
||||
uxLanguage.init();
|
||||
|
||||
// Register URI handler, not for mobileApp
|
||||
if (!isCordova) {
|
||||
|
|
@ -566,6 +511,7 @@ angular
|
|||
preferencesBwsUrl: 12,
|
||||
preferencesAlias: 12,
|
||||
preferencesEmail: 12,
|
||||
export: 13,
|
||||
logs: 13,
|
||||
information: 13,
|
||||
translators: 13,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ angular.module('copayApp.services')
|
|||
body = gettextCatalog.getString('Copayer already in this wallet');
|
||||
break;
|
||||
case 'COPAYER_REGISTERED':
|
||||
body = gettextCatalog.getString('Copayer already registered');
|
||||
body = gettextCatalog.getString('Wallet already registered');
|
||||
break;
|
||||
case 'COPAYER_VOTED':
|
||||
body = gettextCatalog.getString('Copayer already voted on this spend proposal');
|
||||
|
|
@ -84,7 +84,27 @@ angular.module('copayApp.services')
|
|||
case 'WALLET_NOT_FOUND':
|
||||
body = gettextCatalog.getString('Wallet not found');
|
||||
break;
|
||||
case 'SERVER_COMPROMISED':
|
||||
body = gettextCatalog.getString('Server response could not be verified');
|
||||
break;
|
||||
case 'WALLET_DOES_NOT_EXIST':
|
||||
body = gettextCatalog.getString('Wallet not registed at the Wallet Service. Recreate it from "Create Wallet" using "Advanced Options" to set your seed');
|
||||
break;
|
||||
case 'INVALID_BACKUP':
|
||||
body = gettextCatalog.getString('Wallet seed is invalid');
|
||||
break;
|
||||
|
||||
case 'ERROR':
|
||||
body = (err.message || err.error);
|
||||
break;
|
||||
|
||||
default:
|
||||
$log.warn('Unknown error type:', err.code);
|
||||
body = err.message || err.code;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
body = gettextCatalog.getString(err);
|
||||
}
|
||||
|
||||
var msg = prefix + ( body ? ': ' + body : '');
|
||||
|
|
|
|||
37
src/js/services/confirmDialog.js
Normal file
37
src/js/services/confirmDialog.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
angular.module('copayApp.services').factory('confirmDialog', function($log, profileService, configService, gettextCatalog, isCordova, isChromeApp) {
|
||||
var root = {};
|
||||
|
||||
|
||||
var acceptMsg = gettextCatalog.getString('Accept');
|
||||
var cancelMsg = gettextCatalog.getString('Cancel');
|
||||
var confirmMsg = gettextCatalog.getString('Confirm');
|
||||
|
||||
root.show = function(msg, cb) {
|
||||
if (isCordova) {
|
||||
navigator.notification.confirm(
|
||||
msg,
|
||||
function(buttonIndex) {
|
||||
if (buttonIndex == 1) {
|
||||
$timeout(function() {
|
||||
return cb(true);
|
||||
}, 1);
|
||||
} else {
|
||||
return cb(false);
|
||||
}
|
||||
},
|
||||
confirmMsg, [acceptMsg, cancelMsg]
|
||||
);
|
||||
} else if (isChromeApp) {
|
||||
// No feedback, alert/confirm not supported.
|
||||
return cb(true);
|
||||
} else {
|
||||
return cb(confirm(msg));
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
});
|
||||
|
||||
545
src/js/services/ledger.js
Normal file
545
src/js/services/ledger.js
Normal file
|
|
@ -0,0 +1,545 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('copayApp.services')
|
||||
.factory('ledger', function($log, bwcService, gettext) {
|
||||
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.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({
|
||||
command: "get_xpubkey",
|
||||
path: path
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
root.getInfoForNewWallet = function(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) {
|
||||
if (!data.success) {
|
||||
$log.warn(data.message);
|
||||
return callback(data);
|
||||
}
|
||||
opts.extendedPublicKey = data.xpubkey;
|
||||
opts.externalSource = 'ledger';
|
||||
opts.externalIndex = account;
|
||||
return callback(null, opts);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
root._signP2SH = function(txp, account, callback) {
|
||||
root.callbacks["sign_p2sh"] = callback;
|
||||
var redeemScripts = [];
|
||||
var paths = [];
|
||||
var tx = bwcService.getUtils().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));
|
||||
}
|
||||
var splitTransaction = root._splitTransaction(new ByteString(tx.toString(), GP.HEX));
|
||||
var inputs = [];
|
||||
for (var i = 0; i < splitTransaction.inputs.length; i++) {
|
||||
var input = splitTransaction.inputs[i];
|
||||
inputs.push([
|
||||
root._reverseBytestring(input.prevout.bytes(0, 32)).toString(),
|
||||
root._reverseBytestring(input.prevout.bytes(32)).toString()
|
||||
]);
|
||||
}
|
||||
$log.debug('Ledger signing paths:', paths);
|
||||
root._messageAfterSession({
|
||||
command: "sign_p2sh",
|
||||
inputs: inputs,
|
||||
scripts: redeemScripts,
|
||||
outputs_number: splitTransaction.outputs.length,
|
||||
outputs_script: splitTransaction.outputScript.toString(),
|
||||
paths: paths
|
||||
});
|
||||
};
|
||||
|
||||
root.signTx = function(txp, account, callback) {
|
||||
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._message = function(data) {
|
||||
chrome.runtime.sendMessage(
|
||||
LEDGER_CHROME_ID, {
|
||||
request: data
|
||||
},
|
||||
function(response) {
|
||||
root._callback(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
root._messageAfterSession = function(data) {
|
||||
root._after_session = data;
|
||||
root._message({
|
||||
command: "launch"
|
||||
});
|
||||
root._should_poll_session = true;
|
||||
root._do_poll_session();
|
||||
}
|
||||
|
||||
root._do_poll_session = function() {
|
||||
root.hasSession();
|
||||
if (root._should_poll_session) {
|
||||
setTimeout(root._do_poll_session, 500);
|
||||
}
|
||||
}
|
||||
|
||||
root._callback = function(data) {
|
||||
if (typeof data == "object") {
|
||||
if (data.command == "has_session" && data.success) {
|
||||
root._message(root._after_session);
|
||||
root._after_session = null;
|
||||
root._should_poll_session = false;
|
||||
} else if (typeof root.callbacks[data.command] == "function") {
|
||||
root.callbacks[data.command](data);
|
||||
}
|
||||
} else {
|
||||
root._should_poll_session = false;
|
||||
Object.keys(root.callbacks).forEach(function(key) {
|
||||
root.callbacks[key]({
|
||||
success: false,
|
||||
message: gettext("The Ledger Chrome application is not installed"),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
root._getPath = function(account) {
|
||||
return "44'/0'/" + account + "'";
|
||||
}
|
||||
|
||||
root._splitTransaction = function(transaction) {
|
||||
var result = {};
|
||||
var inputs = [];
|
||||
var outputs = [];
|
||||
var offset = 0;
|
||||
var version = transaction.bytes(offset, 4);
|
||||
offset += 4;
|
||||
var varint = root._getVarint(transaction, offset);
|
||||
var numberInputs = varint[0];
|
||||
offset += varint[1];
|
||||
for (var i = 0; i < numberInputs; i++) {
|
||||
var input = {};
|
||||
input['prevout'] = transaction.bytes(offset, 36);
|
||||
offset += 36;
|
||||
varint = root._getVarint(transaction, offset);
|
||||
offset += varint[1];
|
||||
input['script'] = transaction.bytes(offset, varint[0]);
|
||||
offset += varint[0];
|
||||
input['sequence'] = transaction.bytes(offset, 4);
|
||||
offset += 4;
|
||||
inputs.push(input);
|
||||
}
|
||||
varint = root._getVarint(transaction, offset);
|
||||
var numberOutputs = varint[0];
|
||||
offset += varint[1];
|
||||
var outputStartOffset = offset;
|
||||
for (var i = 0; i < numberOutputs; i++) {
|
||||
var output = {};
|
||||
output['amount'] = transaction.bytes(offset, 8);
|
||||
offset += 8;
|
||||
varint = root._getVarint(transaction, offset);
|
||||
offset += varint[1];
|
||||
output['script'] = transaction.bytes(offset, varint[0]);
|
||||
offset += varint[0];
|
||||
outputs.push(output);
|
||||
}
|
||||
var locktime = transaction.bytes(offset, 4);
|
||||
result['version'] = version;
|
||||
result['inputs'] = inputs;
|
||||
result['outputs'] = outputs;
|
||||
result['locktime'] = locktime;
|
||||
result['outputScript'] = transaction.bytes(outputStartOffset, offset - outputStartOffset);
|
||||
return result;
|
||||
}
|
||||
|
||||
root._getVarint = function(data, offset) {
|
||||
if (data.byteAt(offset) < 0xfd) {
|
||||
return [data.byteAt(offset), 1];
|
||||
}
|
||||
if (data.byteAt(offset) == 0xfd) {
|
||||
return [((data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 3];
|
||||
}
|
||||
if (data.byteAt(offset) == 0xfe) {
|
||||
return [((data.byteAt(offset + 4) << 24) + (data.byteAt(offset + 3) << 16) +
|
||||
(data.byteAt(offset + 2) << 8) + data.byteAt(offset + 1)), 5];
|
||||
}
|
||||
}
|
||||
|
||||
root._reverseBytestring = function(x) {
|
||||
var res = "";
|
||||
for (var i = x.length - 1; i >= 0; i--) {
|
||||
res += Convert.toHexByte(x.byteAt(i));
|
||||
}
|
||||
return new ByteString(res, GP.HEX);
|
||||
}
|
||||
|
||||
return root;
|
||||
});
|
||||
|
||||
var Convert = {};
|
||||
|
||||
/**
|
||||
* Convert a binary string to his hexadecimal representation
|
||||
* @param {String} src binary string
|
||||
* @static
|
||||
* @returns {String} hexadecimal representation
|
||||
*/
|
||||
Convert.stringToHex = function(src) {
|
||||
var r = "";
|
||||
var hexes = new Array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f");
|
||||
for (var i = 0; i < src.length; i++) {
|
||||
r += hexes[src.charCodeAt(i) >> 4] + hexes[src.charCodeAt(i) & 0xf];
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an hexadecimal string to its binary representation
|
||||
* @param {String} src hexadecimal string
|
||||
* @static
|
||||
* @return {Array} byte array
|
||||
* @throws {InvalidString} if the string isn't properly formatted
|
||||
*/
|
||||
Convert.hexToBin = function(src) {
|
||||
var result = "";
|
||||
var digits = "0123456789ABCDEF";
|
||||
if ((src.length % 2) != 0) {
|
||||
throw "Invalid string";
|
||||
}
|
||||
src = src.toUpperCase();
|
||||
for (var i = 0; i < src.length; i += 2) {
|
||||
var x1 = digits.indexOf(src.charAt(i));
|
||||
if (x1 < 0) {
|
||||
return "";
|
||||
}
|
||||
var x2 = digits.indexOf(src.charAt(i + 1));
|
||||
if (x2 < 0) {
|
||||
return "";
|
||||
}
|
||||
result += String.fromCharCode((x1 << 4) + x2);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a double digit hexadecimal number to an integer
|
||||
* @static
|
||||
* @param {String} data buffer containing the digit to parse
|
||||
* @param {Number} offset offset to the digit (default is 0)
|
||||
* @returns {Number} converted digit
|
||||
*/
|
||||
Convert.readHexDigit = function(data, offset) {
|
||||
var digits = '0123456789ABCDEF';
|
||||
if (typeof offset == "undefined") {
|
||||
offset = 0;
|
||||
}
|
||||
return (digits.indexOf(data.substring(offset, offset + 1).toUpperCase()) << 4) + (digits.indexOf(data.substring(offset + 1, offset + 2).toUpperCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number to a two digits hexadecimal string (deprecated)
|
||||
* @static
|
||||
* @param {Number} number number to convert
|
||||
* @returns {String} converted number
|
||||
*/
|
||||
Convert.toHexDigit = function(number) {
|
||||
var digits = '0123456789abcdef';
|
||||
return digits.charAt(number >> 4) + digits.charAt(number & 0x0F);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number to a two digits hexadecimal string (similar to toHexDigit)
|
||||
* @static
|
||||
* @param {Number} number number to convert
|
||||
* @returns {String} converted number
|
||||
*/
|
||||
Convert.toHexByte = function(number) {
|
||||
return Convert.toHexDigit(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a BCD number to a two digits hexadecimal string
|
||||
* @static
|
||||
* @param {Number} number number to convert
|
||||
* @returns {String} converted number
|
||||
*/
|
||||
Convert.toHexByteBCD = function(numberBCD) {
|
||||
var number = ((numberBCD / 10) * 16) + (numberBCD % 10);
|
||||
return Convert.toHexDigit(number);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a number to an hexadecimal short number
|
||||
* @static
|
||||
* @param {Number} number number to convert
|
||||
* @returns {String} converted number
|
||||
*/
|
||||
Convert.toHexShort = function(number) {
|
||||
return Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number to an hexadecimal int number
|
||||
* @static
|
||||
* @param {Number} number number to convert
|
||||
* @returns {String} converted number
|
||||
*/
|
||||
Convert.toHexInt = function(number) {
|
||||
return Convert.toHexDigit((number >> 24) & 0xff) + Convert.toHexDigit((number >> 16) & 0xff) +
|
||||
Convert.toHexDigit((number >> 8) & 0xff) + Convert.toHexDigit(number & 0xff);
|
||||
}
|
||||
|
||||
|
||||
var GP = {};
|
||||
GP.ASCII = 1;
|
||||
GP.HEX = 5;
|
||||
|
||||
/**
|
||||
* @class GPScript ByteString implementation
|
||||
* @param {String} value initial value
|
||||
* @param {HEX|ASCII} encoding encoding to use
|
||||
* @property {Number} length length of the ByteString
|
||||
* @constructs
|
||||
*/
|
||||
var ByteString = function(value, encoding) {
|
||||
this.encoding = encoding;
|
||||
this.hasBuffer = (typeof Buffer != 'undefined');
|
||||
if (this.hasBuffer && (value instanceof Buffer)) {
|
||||
this.value = value;
|
||||
this.encoding = GP.HEX;
|
||||
} else {
|
||||
switch (encoding) {
|
||||
case GP.HEX:
|
||||
if (!this.hasBuffer) {
|
||||
this.value = Convert.hexToBin(value);
|
||||
} else {
|
||||
this.value = new Buffer(value, 'hex');
|
||||
}
|
||||
break;
|
||||
|
||||
case GP.ASCII:
|
||||
if (!this.hasBuffer) {
|
||||
this.value = value;
|
||||
} else {
|
||||
this.value = new Buffer(value, 'ascii');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw "Invalid arguments";
|
||||
}
|
||||
}
|
||||
this.length = this.value.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the byte value at the given index
|
||||
* @param {Number} index index
|
||||
* @returns {Number} byte value
|
||||
*/
|
||||
ByteString.prototype.byteAt = function(index) {
|
||||
if (arguments.length < 1) {
|
||||
throw "Argument missing";
|
||||
}
|
||||
if (typeof index != "number") {
|
||||
throw "Invalid index";
|
||||
}
|
||||
if ((index < 0) || (index >= this.value.length)) {
|
||||
throw "Invalid index offset";
|
||||
}
|
||||
if (!this.hasBuffer) {
|
||||
return Convert.readHexDigit(Convert.stringToHex(this.value.substring(index, index + 1)));
|
||||
} else {
|
||||
return this.value[index];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a subset of the ByteString
|
||||
* @param {Number} offset offset to start at
|
||||
* @param {Number} [count] size of the target ByteString (default : use the remaining length)
|
||||
* @returns {ByteString} subset of the original ByteString
|
||||
*/
|
||||
ByteString.prototype.bytes = function(offset, count) {
|
||||
var result;
|
||||
if (arguments.length < 1) {
|
||||
throw "Argument missing";
|
||||
}
|
||||
if (typeof offset != "number") {
|
||||
throw "Invalid offset";
|
||||
}
|
||||
//if ((offset < 0) || (offset >= this.value.length)) {
|
||||
if (offset < 0) {
|
||||
throw "Invalid offset";
|
||||
}
|
||||
if (typeof count == "number") {
|
||||
if (count < 0) {
|
||||
throw "Invalid count";
|
||||
}
|
||||
if (!this.hasBuffer) {
|
||||
result = new ByteString(this.value.substring(offset, offset + count), GP.ASCII);
|
||||
} else {
|
||||
result = new Buffer(count);
|
||||
this.value.copy(result, 0, offset, offset + count);
|
||||
}
|
||||
} else
|
||||
if (typeof count == "undefined") {
|
||||
if (!this.hasBuffer) {
|
||||
result = new ByteString(this.value.substring(offset), GP.ASCII);
|
||||
} else {
|
||||
result = new Buffer(this.value.length - offset);
|
||||
this.value.copy(result, 0, offset, this.value.length);
|
||||
}
|
||||
} else {
|
||||
throw "Invalid count";
|
||||
}
|
||||
if (!this.hasBuffer) {
|
||||
result.encoding = this.encoding;
|
||||
return result;
|
||||
} else {
|
||||
return new ByteString(result, GP.HEX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends two ByteString
|
||||
* @param {ByteString} target ByteString to append
|
||||
* @returns {ByteString} result of the concatenation
|
||||
*/
|
||||
ByteString.prototype.concat = function(target) {
|
||||
if (arguments.length < 1) {
|
||||
throw "Not enough arguments";
|
||||
}
|
||||
if (!(target instanceof ByteString)) {
|
||||
throw "Invalid argument";
|
||||
}
|
||||
if (!this.hasBuffer) {
|
||||
var result = this.value + target.value;
|
||||
var x = new ByteString(result, GP.ASCII);
|
||||
x.encoding = this.encoding;
|
||||
return x;
|
||||
} else {
|
||||
var result = Buffer.concat([this.value, target.value]);
|
||||
return new ByteString(result, GP.HEX);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two ByteString are equal
|
||||
* @param {ByteString} target ByteString to check against
|
||||
* @returns {Boolean} true if the two ByteString are equal
|
||||
*/
|
||||
ByteString.prototype.equals = function(target) {
|
||||
if (arguments.length < 1) {
|
||||
throw "Not enough arguments";
|
||||
}
|
||||
if (!(target instanceof ByteString)) {
|
||||
throw "Invalid argument";
|
||||
}
|
||||
if (!this.hasBuffer) {
|
||||
return (this.value == target.value);
|
||||
} else {
|
||||
return Buffer.equals(this.value, target.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert the ByteString to a String using the given encoding
|
||||
* @param {HEX|ASCII|UTF8|BASE64|CN} encoding encoding to use
|
||||
* @return {String} converted content
|
||||
*/
|
||||
ByteString.prototype.toString = function(encoding) {
|
||||
var targetEncoding = this.encoding;
|
||||
if (arguments.length >= 1) {
|
||||
if (typeof encoding != "number") {
|
||||
throw "Invalid encoding";
|
||||
}
|
||||
switch (encoding) {
|
||||
case GP.HEX:
|
||||
case GP.ASCII:
|
||||
targetEncoding = encoding;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw "Unsupported arguments";
|
||||
}
|
||||
targetEncoding = encoding;
|
||||
}
|
||||
switch (targetEncoding) {
|
||||
case GP.HEX:
|
||||
if (!this.hasBuffer) {
|
||||
return Convert.stringToHex(this.value);
|
||||
} else {
|
||||
return this.value.toString('hex');
|
||||
}
|
||||
case GP.ASCII:
|
||||
if (!this.hasBuffer) {
|
||||
return this.value;
|
||||
} else {
|
||||
return this.value.toString();
|
||||
}
|
||||
default:
|
||||
throw "Unsupported";
|
||||
}
|
||||
}
|
||||
|
||||
ByteString.prototype.toStringIE = function(encoding) {
|
||||
return this.toString(encoding);
|
||||
}
|
||||
|
||||
ByteString.prototype.toBuffer = function() {
|
||||
return this.value;
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services')
|
||||
.factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, storageService, bwcService, configService, notificationService, isChromeApp, isCordova, gettext, nodeWebkit, bwsError) {
|
||||
.factory('profileService', function profileServiceFactory($rootScope, $location, $timeout, $filter, $log, lodash, storageService, bwcService, configService, notificationService, isChromeApp, isCordova, gettext, nodeWebkit, bwsError, uxLanguage, ledger, bitcore) {
|
||||
|
||||
var root = {};
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ angular.module('copayApp.services')
|
|||
// Set local object
|
||||
if (walletId)
|
||||
root.focusedClient = root.walletClients[walletId];
|
||||
else
|
||||
else
|
||||
root.focusedClient = [];
|
||||
|
||||
if (lodash.isEmpty(root.focusedClient)) {
|
||||
|
|
@ -146,7 +146,7 @@ angular.module('copayApp.services')
|
|||
return cb(err);
|
||||
}
|
||||
if (!profile) {
|
||||
// Migration??
|
||||
// Migration??
|
||||
storageService.tryToMigrate(function(err, migratedProfile) {
|
||||
if (err) return cb(err);
|
||||
if (!migratedProfile)
|
||||
|
|
@ -165,62 +165,102 @@ angular.module('copayApp.services')
|
|||
});
|
||||
};
|
||||
|
||||
root._seedWallet = function(opts, cb) {
|
||||
opts = opts || {};
|
||||
var walletClient = bwcService.getClient();
|
||||
var network = opts.networkName || 'livenet';
|
||||
|
||||
if (opts.mnemonic && opts.mnemonic.indexOf('m/') == 0) {
|
||||
var xPrivKey = root._preDerivation(opts.mnemonic, network);
|
||||
if (!xPrivKey)
|
||||
return bwsError.cb('Bad derivation', gettext('Could not import'), cb);
|
||||
opts.mnemonic = null;
|
||||
opts.extendedPrivateKey = xPrivKey;
|
||||
}
|
||||
if (opts.mnemonic) {
|
||||
try {
|
||||
walletClient.seedFromMnemonic(opts.mnemonic, opts.passphrase, network);
|
||||
} catch (ex) {
|
||||
$log.info(ex);
|
||||
return cb(gettext('Could not create: Invalid wallet seed'));
|
||||
}
|
||||
} else if (opts.extendedPrivateKey) {
|
||||
try {
|
||||
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
|
||||
} catch (ex) {
|
||||
$log.warn(ex);
|
||||
return cb(gettext('Could not create using the specified extended private key'));
|
||||
}
|
||||
} else if (opts.extendedPublicKey) {
|
||||
try {
|
||||
walletClient.seedFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource);
|
||||
} catch (ex) {
|
||||
$log.warn("Creating wallet from Extended Public Key Arg:", ex, opts);
|
||||
return cb(gettext('Could not create using the specified extended public key'));
|
||||
}
|
||||
} else {
|
||||
var lang = uxLanguage.getCurrentLanguage();
|
||||
try {
|
||||
walletClient.seedFromRandomWithMnemonic(network, opts.passphrase, lang);
|
||||
} catch (e) {
|
||||
$log.info('Error creating seed: ' + e.message);
|
||||
if (e.message.indexOf('language') > 0) {
|
||||
$log.info('Using default language for mnemonic');
|
||||
walletClient.seedFromRandomWithMnemonic(network, opts.passphrase);
|
||||
} else {
|
||||
return cb(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cb(null, walletClient);
|
||||
};
|
||||
|
||||
root._createNewProfile = function(opts, cb) {
|
||||
|
||||
if (opts.noWallet) {
|
||||
return cb(null, Profile.create());
|
||||
}
|
||||
|
||||
var walletClient = bwcService.getClient();
|
||||
walletClient.createWallet('Personal Wallet', 'me', 1, 1, {
|
||||
network: 'livenet'
|
||||
}, function(err) {
|
||||
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
|
||||
var p = Profile.create({
|
||||
credentials: [JSON.parse(walletClient.export())],
|
||||
root._seedWallet({}, function(err, walletClient) {
|
||||
if (err) return cb(err);
|
||||
|
||||
walletClient.createWallet('Personal Wallet', 'me', 1, 1, {
|
||||
network: 'livenet'
|
||||
}, function(err) {
|
||||
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
|
||||
var p = Profile.create({
|
||||
credentials: [JSON.parse(walletClient.export())],
|
||||
});
|
||||
return cb(null, p);
|
||||
});
|
||||
return cb(null, p);
|
||||
})
|
||||
};
|
||||
|
||||
root.createWallet = function(opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Creating Wallet:', opts);
|
||||
root._seedWallet(opts, function(err, walletClient) {
|
||||
if (err) return cb(err);
|
||||
|
||||
if (opts.extendedPrivateKey) {
|
||||
try {
|
||||
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
|
||||
} catch (ex) {
|
||||
return cb(gettext('Could not create using the specified extended private key'));
|
||||
}
|
||||
}
|
||||
walletClient.createWallet(opts.name, opts.myName || 'me', opts.m, opts.n, {
|
||||
network: opts.networkName
|
||||
}, function(err, secret) {
|
||||
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
|
||||
walletClient.createWallet(opts.name, opts.myName || 'me', opts.m, opts.n, {
|
||||
network: opts.networkName
|
||||
}, function(err, secret) {
|
||||
if (err) return bwsError.cb(err, gettext('Error creating wallet'), cb);
|
||||
|
||||
root.profile.credentials.push(JSON.parse(walletClient.export()));
|
||||
root.setWalletClients();
|
||||
root.profile.credentials.push(JSON.parse(walletClient.export()));
|
||||
root.setWalletClients();
|
||||
|
||||
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
|
||||
storageService.storeProfile(root.profile, function(err) {
|
||||
return cb(null, secret);
|
||||
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
|
||||
storageService.storeProfile(root.profile, function(err) {
|
||||
return cb(null, secret, walletClient.credentials.walletId);
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
root.joinWallet = function(opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Joining Wallet:', opts);
|
||||
if (opts.extendedPrivateKey) {
|
||||
try {
|
||||
walletClient.seedFromExtendedPrivateKey(opts.extendedPrivateKey);
|
||||
} catch (ex) {
|
||||
return cb(gettext('Could not join using the specified extended private key'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
var walletData = this.getUtils().fromSecret(opts.secret);
|
||||
|
|
@ -229,23 +269,29 @@ angular.module('copayApp.services')
|
|||
if (lodash.find(root.profile.credentials, {
|
||||
'walletId': walletData.walletId
|
||||
})) {
|
||||
return cb(gettext('Cannot join the same wallet more that once'));
|
||||
return cb(gettext('Cannot join the same wallet more that once'));
|
||||
}
|
||||
} catch (ex) {
|
||||
return cb(gettext('Bad wallet invitation'));
|
||||
}
|
||||
opts.networkName = walletData.network;
|
||||
$log.debug('Joining Wallet:', opts);
|
||||
|
||||
walletClient.joinWallet(opts.secret, opts.myName || 'me', function(err) {
|
||||
if (err) return bwsError.cb(err, gettext('Could not join wallet'), cb);
|
||||
root._seedWallet(opts, function(err, walletClient) {
|
||||
if (err) return cb(err);
|
||||
|
||||
root.profile.credentials.push(JSON.parse(walletClient.export()));
|
||||
root.setWalletClients();
|
||||
walletClient.joinWallet(opts.secret, opts.myName || 'me', function(err) {
|
||||
if (err) return bwsError.cb(err, gettext('Could not join wallet'), cb);
|
||||
|
||||
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
|
||||
storageService.storeProfile(root.profile, function(err) {
|
||||
return cb(null, opts.secret);
|
||||
root.profile.credentials.push(JSON.parse(walletClient.export()));
|
||||
root.setWalletClients();
|
||||
|
||||
root.setAndStoreFocus(walletClient.credentials.walletId, function() {
|
||||
storageService.storeProfile(root.profile, function(err) {
|
||||
return cb();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
|
|
@ -277,18 +323,7 @@ angular.module('copayApp.services')
|
|||
});
|
||||
};
|
||||
|
||||
root.importWallet = function(str, opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Importing Wallet:', opts);
|
||||
try {
|
||||
walletClient.import(str, {
|
||||
compressed: opts.compressed,
|
||||
password: opts.password
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(gettext('Could not import. Check input file and password'));
|
||||
}
|
||||
|
||||
root._addWalletClient = function(walletClient, cb) {
|
||||
var walletId = walletClient.credentials.walletId;
|
||||
|
||||
// check if exist
|
||||
|
|
@ -306,8 +341,92 @@ angular.module('copayApp.services')
|
|||
return cb(null, walletId);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
root.importWallet = function(str, opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Importing Wallet:', opts);
|
||||
try {
|
||||
walletClient.import(str, {
|
||||
compressed: opts.compressed,
|
||||
password: opts.password
|
||||
});
|
||||
} catch (err) {
|
||||
return cb(gettext('Could not import. Check input file and password'));
|
||||
}
|
||||
root._addWalletClient(walletClient, cb);
|
||||
};
|
||||
|
||||
root.importExtendedPrivateKey = function(xPrivKey, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Importing Wallet xPrivKey');
|
||||
|
||||
walletClient.importFromExtendedPrivateKey(xPrivKey, function(err) {
|
||||
if (err)
|
||||
return bwsError.cb(err, gettext('Could not import'), cb);
|
||||
|
||||
root._addWalletClient(walletClient, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
root._preDerivation = function(words, network) {
|
||||
var wordList = words.split(/ /).filter(function(v) {
|
||||
return v.length > 0;
|
||||
});
|
||||
var path = wordList.shift();
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.info('preDerivation:', path);
|
||||
walletClient.seedFromMnemonic(wordList.join(' '), null, network);
|
||||
var k = new bitcore.HDPrivateKey(walletClient.credentials.xPrivKey);
|
||||
var k2 = k.derive(path);
|
||||
return k2.toString();
|
||||
};
|
||||
|
||||
root.importMnemonic = function(words, opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
|
||||
if (words.indexOf('m/') == 0) {
|
||||
var xPrivKey = root._preDerivation(words, opts.networkName);
|
||||
if (!xPrivKey)
|
||||
return bwsError.cb('Bad derivation', gettext('Could not import'), cb);
|
||||
return root.importExtendedPrivateKey(xPrivKey, cb);
|
||||
}
|
||||
|
||||
$log.debug('Importing Wallet Mnemonic');
|
||||
|
||||
walletClient.importFromMnemonic(words, {
|
||||
network: opts.networkName,
|
||||
passphrase: opts.passphrase,
|
||||
}, function(err) {
|
||||
if (err)
|
||||
return bwsError.cb(err, gettext('Could not import'), cb);
|
||||
|
||||
root._addWalletClient(walletClient, cb);
|
||||
});
|
||||
};
|
||||
|
||||
root.importExtendedPublicKey = function(opts, cb) {
|
||||
var walletClient = bwcService.getClient();
|
||||
$log.debug('Importing Wallet XPubKey');
|
||||
|
||||
walletClient.importFromExtendedPublicKey(opts.extendedPublicKey, opts.externalSource, opts.entropySource, function(err) {
|
||||
if (err) {
|
||||
|
||||
// in HW wallets, req key is always the same. They can't addAccess.
|
||||
if (err.code == 'NOT_AUTHORIZED')
|
||||
err.code = 'WALLET_DOES_NOT_EXIST';
|
||||
|
||||
return bwsError.cb(err, gettext('Could not import'), cb);
|
||||
}
|
||||
|
||||
root._addWalletClient(walletClient, cb);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
root.create = function(opts, cb) {
|
||||
$log.info('Creating profile');
|
||||
configService.get(function(err) {
|
||||
|
|
@ -364,7 +483,7 @@ angular.module('copayApp.services')
|
|||
$log.debug('Encrypting private key for', fc.credentials.walletName);
|
||||
|
||||
fc.setPrivateKeyEncryption(password);
|
||||
fc.lock();
|
||||
root.lockFC();
|
||||
root.updateCredentialsFC(function() {
|
||||
$log.debug('Wallet encrypted');
|
||||
return cb();
|
||||
|
|
@ -399,13 +518,17 @@ angular.module('copayApp.services')
|
|||
$log.debug('Wallet is encrypted');
|
||||
$rootScope.$emit('Local/NeedsPassword', false, function(err2, password) {
|
||||
if (err2 || !password) {
|
||||
return cb({message: (err2 || gettext('Password needed'))});
|
||||
return cb({
|
||||
message: (err2 || gettext('Password needed'))
|
||||
});
|
||||
}
|
||||
try {
|
||||
fc.unlock(password);
|
||||
} catch (e) {
|
||||
$log.debug(e);
|
||||
return cb({message: gettext('Wrong password')});
|
||||
return cb({
|
||||
message: gettext('Wrong password')
|
||||
});
|
||||
}
|
||||
$timeout(function() {
|
||||
if (fc.isPrivKeyEncrypted()) {
|
||||
|
|
@ -439,7 +562,38 @@ angular.module('copayApp.services')
|
|||
return lodash.sortBy(ret, 'name');
|
||||
};
|
||||
|
||||
root._signWithLedger = function(txp, cb) {
|
||||
var fc = root.focusedClient;
|
||||
$log.info('Requesting Ledger Chrome app to sign the transaction');
|
||||
|
||||
ledger.signTx(txp, 0, function(result) {
|
||||
if (!result.success)
|
||||
return cb(result);
|
||||
|
||||
txp.signatures = lodash.map(result.signatures, function(s) {
|
||||
return s.substring(0, s.length - 2);
|
||||
});
|
||||
return fc.signTxProposal(txp, cb);
|
||||
});
|
||||
};
|
||||
|
||||
root.signTxProposal = function(txp, cb) {
|
||||
var fc = root.focusedClient;
|
||||
|
||||
if (fc.isPrivKeyExternal()) {
|
||||
if (fc.getPrivKeyExternalSourceName() != 'ledger') {
|
||||
var msg = 'Unsupported External Key:' + fc.getPrivKeyExternalSourceName();
|
||||
$log.error(msg);
|
||||
return cb(msg);
|
||||
}
|
||||
return root._signWithLedger(txp, cb);
|
||||
} else {
|
||||
return fc.signTxProposal(txp, function(err, signedTxp) {
|
||||
root.lockFC();
|
||||
return cb(err, signedTxp);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return root;
|
||||
});
|
||||
|
|
|
|||
95
src/js/services/uxLanguage.js
Normal file
95
src/js/services/uxLanguage.js
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
'use strict';
|
||||
angular.module('copayApp.services')
|
||||
.factory('uxLanguage', function languageService($log, lodash, gettextCatalog, amMoment, configService) {
|
||||
var root = {};
|
||||
|
||||
root.availableLanguages = [{
|
||||
name: 'English',
|
||||
isoCode: 'en',
|
||||
}, {
|
||||
name: 'Français',
|
||||
isoCode: 'fr',
|
||||
}, {
|
||||
name: 'Italiano',
|
||||
isoCode: 'it',
|
||||
}, {
|
||||
name: 'Deutsch',
|
||||
isoCode: 'de',
|
||||
}, {
|
||||
name: 'Español',
|
||||
isoCode: 'es',
|
||||
}, {
|
||||
name: 'Português',
|
||||
isoCode: 'pt',
|
||||
}, {
|
||||
name: 'Ελληνικά',
|
||||
isoCode: 'el',
|
||||
}, {
|
||||
name: '日本語',
|
||||
isoCode: 'ja',
|
||||
}, {
|
||||
name: 'Pусский',
|
||||
isoCode: 'ru',
|
||||
}];
|
||||
|
||||
root.currentLanguage = null;
|
||||
|
||||
root._detect = function() {
|
||||
// Auto-detect browser language
|
||||
var userLang, androidLang;
|
||||
|
||||
if (navigator && navigator.userAgent && (androidLang = navigator.userAgent.match(/android.*\W(\w\w)-(\w\w)\W/i))) {
|
||||
userLang = androidLang[1];
|
||||
} else {
|
||||
// works for iOS and Android 4.x
|
||||
userLang = navigator.userLanguage || navigator.language;
|
||||
}
|
||||
userLang = userLang ? (userLang.split('-', 1)[0] || 'en') : 'en';
|
||||
|
||||
return userLang;
|
||||
};
|
||||
|
||||
root._set = function(lang) {
|
||||
$log.debug('Setting default language: ' + lang);
|
||||
gettextCatalog.setCurrentLanguage(lang);
|
||||
amMoment.changeLocale(lang);
|
||||
root.currentLanguage = lang;
|
||||
};
|
||||
|
||||
root.getCurrentLanguage = function() {
|
||||
return root.currentLanguage;
|
||||
};
|
||||
|
||||
root.getCurrentLanguageName = function() {
|
||||
return root.getName(root.currentLanguage);
|
||||
};
|
||||
|
||||
root.getLanguages = function() {
|
||||
return root.availableLanguages;
|
||||
};
|
||||
|
||||
root.init = function() {
|
||||
root._set(root._detect());
|
||||
};
|
||||
|
||||
root.update = function() {
|
||||
var userLang = configService.getSync().wallet.settings.defaultLanguage;
|
||||
|
||||
if (!userLang) {
|
||||
userLang = root._detect();
|
||||
}
|
||||
|
||||
if (userLang != gettextCatalog.getCurrentLanguage()) {
|
||||
root._set(userLang);
|
||||
}
|
||||
return userLang;
|
||||
};
|
||||
|
||||
root.getName = function(lang) {
|
||||
return lodash.result(lodash.find(root.availableLanguages, {
|
||||
'isoCode': lang
|
||||
}), 'name');
|
||||
};
|
||||
|
||||
return root;
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue