Skip to content
This repository has been archived by the owner on Nov 30, 2020. It is now read-only.

Add Trezor support #75

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions controllers/login.controller.es6
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Keypair} from 'stellar-sdk';
import {Alert, AlertGroup} from 'interstellar-ui-messages';
import LedgerTransport from '@ledgerhq/hw-transport-u2f';
import LedgerStr from '@ledgerhq/hw-app-str';
import TrezorConnect from 'trezor-connect';

@Controller("LoginController")
@Inject("$scope", "interstellar-core.Config", "interstellar-core.IntentBroadcast", "interstellar-sessions.Sessions", "interstellar-ui-messages.Alerts")
Expand All @@ -30,6 +31,11 @@ export default class LoginController {
this.bip32Path = "44'/148'/0'";
this.connectLedger();

this.trezorAlertGroup = new AlertGroup();
this.trezorAlertGroup.registerUpdateListener(alerts => {
this.trezorAlerts = alerts;
});

this.infoImage = require('../images/info.png');
this.showInfo = false;

Expand Down Expand Up @@ -92,6 +98,35 @@ export default class LoginController {
}
}

proceedWithTrezor() {
let params = {
// Trezor requires "m/" prefix
path: 'm/' + this.bip32Path,
showOnTrezor: false // don't bother with the "confirm sharing address" prompt on the Trezor device
};

TrezorConnect.stellarGetAddress(params).then((result) => {
if (!result.success) {
let alert = new Alert({
title: 'Could not use Trezor',
text: result.payload.error,
type: Alert.TYPES.ERROR,
dismissible: true
});
this.trezorAlertGroup.show(alert);
// Necessary to get the alert to be rendered
this.$scope.$applyAsync();
return;
}

let permanent = this.Config.get("permanentSession");
let data = { useTrezor: true, bip32Path: this.bip32Path };
let address = result.payload.address;
this.Sessions.createDefault({address, data, permanent})
.then(() => this.broadcastShowDashboardIntent());
});
}

generate() {
let keypair = Keypair.random();
this.newKeypair = {
Expand Down
105 changes: 103 additions & 2 deletions controllers/send-widget.controller.es6
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import BasicClientError from '../errors';
import knownAccounts from '../known_accounts';
import LedgerTransport from '@ledgerhq/hw-transport-u2f';
import LedgerStr from '@ledgerhq/hw-app-str';
import {Network} from "stellar-base";
import TrezorConnect from 'trezor-connect';

const BASE_RESERVE = 0.5;

Expand Down Expand Up @@ -80,7 +82,11 @@ export default class SendWidgetController {
});
Alerts.registerGroup(this.memoWarningAlertGroup);

// Used to display additional details on the "outcome" page describing why signing failed
this.signingErrorMessage = null;

this.useLedger = this.session.data && this.session.data['useLedger'];
this.useTrezor = this.session.data && this.session.data['useTrezor'];
this.bip32Path = this.session.data && this.session.data['bip32Path'];
}

Expand Down Expand Up @@ -201,6 +207,7 @@ export default class SendWidgetController {

this.sending = true;
this.ledgerError = false;
this.signingErrorMessage = null;

this.addressAlertGroup.clear();
this.amountAlertGroup.clear();
Expand Down Expand Up @@ -345,7 +352,9 @@ export default class SendWidgetController {
asset: Asset.native(),
amount: this.amount
});
return this._submitTransaction(operation);
return this._submitTransaction(operation)
.catch(this._preSubmitOnFailure.bind(this))
.finally(this._submitFinally.bind(this));
})
.catch(err => {
if (err.name === 'NotFoundError') {
Expand All @@ -363,6 +372,7 @@ export default class SendWidgetController {

resubmitTransaction() {
if (this.lastTransactionXDR === null) {
this.showView(null, 'sendSetup');
return;
}

Expand Down Expand Up @@ -419,7 +429,71 @@ export default class SendWidgetController {
throw e;
});
})
} else {
}
else if (this.useTrezor) {
/*
'memo' must always be present in the transaction parameters so default it
to type 0 (no memo)
*/
let trezorMemoParams = { type: 0 };

// If there is a memo, convert it to the correct value for the Trezor
if (this.memo) {
switch (this.memoType) {
case 'MEMO_TEXT':
trezorMemoParams = { type: 1, text: this.memoValue };
break;
case 'MEMO_ID':
trezorMemoParams = { type: 2, id: this.memoValue };
break;
case 'MEMO_HASH':
trezorMemoParams = { type: 3, hash: this.memoValue };
break;
case 'MEMO_RETURN':
trezorMemoParams = { type: 4, hash: this.memoValue };
break;
}
}

// Object sent to the Trezor API
// Documentation at: https://github.com/trezor/connect/blob/develop/docs/methods/stellarSignTransaction.md
let trezorTxParams = {
path: 'm/' + this.bip32Path,
networkPassphrase: Network.current().networkPassphrase(),
transaction: {
source: this.session.address,
fee: transaction.fee,
sequence: transaction.sequence,
memo: trezorMemoParams,
operations: [
{
type: 'payment',
destination: transaction.operations[0].destination,
// Trezor expects this in stroops
amount: transaction.operations[0].amount * 10000000
}
]
}
};

return TrezorConnect.stellarSignTransaction(trezorTxParams)
.then((result) => {
if (!result.success) {
this.signingErrorMessage = "Trezor: " + result.payload.error;
throw new BasicClientError("TrezorError", { message: result.payload.error });
}

// Convert hex string returned from Trezor into binary
let signature = this._hexToByteArray(result.payload.signature);
let keyPair = Keypair.fromAccountId(this.session.address);
let hint = keyPair.signatureHint();
let decorated = new xdr.DecoratedSignature({hint, signature});
transaction.signatures.push(decorated);

return transaction;
});
}
else {
transaction.sign(Keypair.fromSeed(this.session.getSecret()));
return transaction;
}
Expand Down Expand Up @@ -450,6 +524,11 @@ export default class SendWidgetController {
this.$rootScope.$broadcast('account-viewer.transaction-success');
}

_preSubmitOnFailure(e) {
this.success = false;
this.needsResubmit = true;
}

_submitOnFailure(e) {
this.success = false;
this.needsResubmit = false;
Expand Down Expand Up @@ -480,4 +559,26 @@ export default class SendWidgetController {
let accountSignerWeight = account.signers.find(signer => signer.key === account.id).weight;
return accountSignerWeight < account.thresholds.med_threshold;
}

_byteArrayToBase64(buffer) {
buffer = buffer.slice();
let binary = '';
let bytes = new Uint8Array( buffer );
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa( binary );
}

_hexToByteArray(str) {
let result = [];
while (str.length >= 2) {
result.push(parseInt(str.substring(0, 2), 16));

str = str.substring(2, str.length);
}

return new Uint8Array(result);
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"interstellar-ui-messages": "~0.0.3",
"lodash": "^3.10.1",
"stellar-base": "~0.6.0",
"stellar-sdk": "~0.6.2"
"stellar-sdk": "~0.6.2",
"trezor-connect": "^5.0.32"
},
"devDependencies": {
"gulp": "^3.9.0"
Expand Down
22 changes: 17 additions & 5 deletions templates/login.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,35 @@ <h2 class="welcomeBox__title">Stellar Account Viewer</h2>
<button type="button" class="s-button loginBox__submit" ng-click="login.generate()">Generate</button>
</div>
<div class="middleBox">
Connect Ledger Hardware Wallet <img ng-src="{{login.infoImage}}" ng-mousedown="login.toggleInfo()" style="cursor: pointer;" title="Click me" />
Use Hardware Wallet
<br/>
<div class="loginBox__info" ng-show="login.showInfo">
Available on Chrome and Opera. Install the Stellar app from Ledger and enable browser support in the app settings.
</div>
<div class="loginBox__ledgerAccount">
<input id="ledgerDefaultAccountCheckbox" type="checkbox" ng-model="hideBip32Input" ng-init="hideBip32Input=true" />
<label for="ledgerDefaultAccountCheckbox" ng-if="hideBip32Input">Use default account</label>
<input type="text" ng-model="login.bip32Path" class="loginBox__input" placeholder="BIP32 path: 44'/148'/n'" ng-if="!hideBip32Input" />
</div>

<button type="button" class="s-button loginBox__submit" ng-click="login.proceedWithLedger()" ng-disabled="login.ledgerStatus != 'Connected'">Sign in with Ledger</button>
<div class="loginBox__info">
Available on Chrome and Opera. Install the Stellar app from Ledger and enable browser support in the app settings.
</div>
<div class="s-alert s-alert--alert" ng-if="login.ledgerAlerts.length > 0">
<span ng-repeat="alert in login.ledgerAlerts">
{{alert.title}}<br /><br />
{{alert.text}}
</span>
</div>
<button type="button" class="s-button loginBox__submit" ng-click="login.proceedWithLedger()" ng-disabled="login.ledgerStatus != 'Connected'">Sign in with Ledger</button>

<button type="button" class="s-button loginBox__submit" ng-click="login.proceedWithTrezor()">Sign in with Trezor</button>
<div class="loginBox__info">
Requires Trezor Bridge and Trezor One (with firmware 1.7.0+) or Trezor Model T (with firmware 2.0.8+).
</div>
<div class="s-alert s-alert--alert" ng-if="login.trezorAlerts.length > 0">
<span ng-repeat="alert in login.trezorAlerts">
{{alert.title}}<br /><br />
{{alert.text}}
</span>
</div>
</div>
<div class="middleBox">
Looking for a more advanced wallet? <a href="https://www.stellar.org/lumens/wallets/">Proceed here &raquo;</a>
Expand Down
4 changes: 4 additions & 0 deletions templates/send-widget.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ <h2 class="sendPane__title">Send lumens</h2>
<div ng-if="!widget.success">
<p class="sendOutcome__label">Transaction failed</p>

<p class="" ng-if="widget.signingErrorMessage">
<strong>Could not sign transaction: </strong> {{widget.signingErrorMessage}}
</p>

<p class="" ng-if="widget.needsResubmit">
<strong>Warning</strong> We submitted your transaction to the network but because of the network conditions we are
not certain about it's status: it could either succeed or fail. Instead of recreating the transaction you should use
Expand Down
Loading