Skip to content
This repository has been archived by the owner on Dec 1, 2021. It is now read-only.

Added Bluetooth Ledger support #510

Merged
merged 12 commits into from
Apr 30, 2021
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## [UNRELEASED]
* [#387] Added Bluetooth Ledger support

## v0.41.x-11
* Replaced Random Validators and Chart components with Latest Blocks and Latest Transactions components on homepage
* Update meta data to align with setting values
Expand Down
3 changes: 2 additions & 1 deletion both/i18n/en-us.i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,10 @@ accounts:
signInText: 'You are signed in as '
toLoginAs: 'To log in as'
signInWithLedger: 'Sign In With Ledger'
signInWarning: 'Please make sure your Ledger device is connected and <strong class="text-primary">{$network} App {$version} or above</strong> is opened.'
signInWarning: 'Please make sure your Ledger device is turned on and <strong class="text-primary">{$network} App {$version} or above</strong> is opened.'
pleaseAccept: 'please accept in your Ledger device.'
noRewards: 'No Rewards'
BLESupport: 'Bluetooth connection is currently only supported on Google Chrome Browser.'
activities:
single: 'A'
happened: 'happened.'
Expand Down
1 change: 1 addition & 0 deletions client/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { render } from 'react-dom';

CURRENTUSERADDR = 'ledgerUserAddress';
CURRENTUSERPUBKEY = 'ledgerUserPubKey';
BLELEDGERCONNECTION = 'ledgerBLEConnection'

// import { onPageLoad } from 'meteor/server-render';

Expand Down
2 changes: 1 addition & 1 deletion imports/api/ledger/server/methods.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Meteor.methods({
"chain_id": Meteor.settings.public.chainId,
"gas_adjustment": adjustment,
"account_number": accountNumber,
"sequence": sequence,
"sequence": sequence.toString(),
"simulate": true
}
};
Expand Down
8 changes: 5 additions & 3 deletions imports/ui/ledger/LedgerActions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class LedgerButton extends Component {
errorMessage: '',
user: localStorage.getItem(CURRENTUSERADDR),
pubKey: localStorage.getItem(CURRENTUSERPUBKEY),
transportBLE: localStorage.getItem(BLELEDGERCONNECTION),
memo: DEFAULT_MEMO
};
this.ledger = new Ledger({testModeAllowed: false});
Expand Down Expand Up @@ -194,7 +195,8 @@ class LedgerButton extends Component {
if (state.user !== localStorage.getItem(CURRENTUSERADDR)) {
return {
user: localStorage.getItem(CURRENTUSERADDR),
pubKey: localStorage.getItem(CURRENTUSERPUBKEY)
pubKey: localStorage.getItem(CURRENTUSERPUBKEY),
transportBLE: localStorage.getItem(BLELEDGERCONNECTION)
};
}
return null;
Expand Down Expand Up @@ -306,7 +308,7 @@ class LedgerButton extends Component {
}

tryConnect = () => {
this.ledger.getCosmosAddress().then((res) => {
this.ledger.getCosmosAddress(this.state.transportBLE).then((res) => {
if (res.address == this.state.user)
this.setState({
success: true,
Expand Down Expand Up @@ -437,7 +439,7 @@ class LedgerButton extends Component {
let txMsg = this.state.txMsg;
const txContext = this.getTxContext();
const bytesToSign = Ledger.getBytesToSign(txMsg, txContext);
this.ledger.sign(bytesToSign).then((sig) => {
this.ledger.sign(bytesToSign, this.state.transportBLE).then((sig) => {
try {
Ledger.applySignature(txMsg, txContext, sig);
Meteor.call('transaction.submit', txMsg, (err, res) => {
Expand Down
37 changes: 30 additions & 7 deletions imports/ui/ledger/LedgerModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ class LedgerModal extends React.Component {
super(props);
this.state = {
loading: false,
activeTab: '1'
activeTab: '1',
transportBLE: localStorage.getItem(BLELEDGERCONNECTION) ?? false
};
this.ledger = new Ledger({testModeAllowed: false});
}

autoOpenModal = () => {
if (!this.props.isOpen && this.props.handleLoginConfirmed) {
this.tryConnect(5000);
// this.tryConnect(5000);
this.props.toggle(true);
}
}
Expand All @@ -28,15 +29,30 @@ class LedgerModal extends React.Component {

componentDidUpdate(prevProps, prevState) {
this.autoOpenModal();
if (this.props.isOpen && !prevProps.isOpen) {
let bleTransport = this.state.transportBLE
if (bleTransport != prevState.transportBLE) {
this.tryConnect();
}
}

connectionSelection = async (e) => {
e.persist();
if(e?.currentTarget?.value === "usb"){
await this.setState({ transportBLE: false })
this.tryConnect()

}
if (e?.currentTarget?.value === "bluetooth") {
await this.setState({ transportBLE: true })
this.tryConnect()

}
}

tryConnect = (timeout=undefined) => {
if (this.state.loading) return
this.setState({ loading: true, errorMessage: '' })
this.ledger.getCosmosAddress(timeout).then((res) => {
this.ledger.getCosmosAddress(this.state.transportBLE).then((res) => {
let currentUser = localStorage.getItem(CURRENTUSERADDR);
if (this.props.handleLoginConfirmed && res.address === currentUser) {
this.closeModal(true)
Expand All @@ -59,11 +75,15 @@ class LedgerModal extends React.Component {
});
}




trySignIn = () => {
this.setState({ loading: true, errorMessage: '' })
this.ledger.confirmLedgerAddress().then((res) => {
this.ledger.confirmLedgerAddress(this.state.transportBLE).then((res) => {
localStorage.setItem(CURRENTUSERADDR, this.state.address);
localStorage.setItem(CURRENTUSERPUBKEY, this.state.pubKey);
localStorage.setItem(BLELEDGERCONNECTION, this.state.transportBLE);
this.props.refreshApp();
this.closeModal(true);
}, (err) => {
Expand All @@ -74,8 +94,6 @@ class LedgerModal extends React.Component {
}

getActionButton() {
if (this.state.activeTab === '1' && !this.state.loading)
return <Button color="primary" onClick={this.tryConnect}><T>common.retry</T></Button>
if (this.state.activeTab === '2' && this.state.errorMessage !== '')
return <Button color="primary" onClick={this.trySignIn}><T>common.retry</T></Button>
}
Expand All @@ -102,6 +120,11 @@ class LedgerModal extends React.Component {
<TabContent activeTab={this.state.activeTab}>
<TabPane tabId="1">
<T _purify={false} network={Meteor.settings.public.ledger.appName} version={Meteor.settings.public.ledger.appVersion}>accounts.signInWarning</T>
<div className="d-flex justify-content-center">
<Button color="secondary" value="usb" onClick={this.connectionSelection} className="mt-3 mr-4"><span><img src="/img/usb.svg" alt="USB" style={{height: "25px"}}/><T>USB</T></span></Button>
<Button color="secondary" value="bluetooth" onClick={this.connectionSelection} className="mt-3 "><span><img src="/img/bluetooth.svg" alt="Bluetooth" style={{ height: "25px" }} /><T>Bluetooth</T></span></Button>
</div>
<h6 className="error-message text-center mt-3"><T>accounts.BLESupport</T></h6>
</TabPane>
<TabPane tabId="2">
{this.state.currentUser?<span>You are currently logged in as <strong className="text-primary d-block">{this.state.currentUser}.</strong></span>:null}
Expand Down
66 changes: 46 additions & 20 deletions imports/ui/ledger/ledger.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// https://github.com/cosmos/ledger-cosmos-js/blob/master/src/index.js
import 'babel-polyfill';
import TransportWebUSB from "@ledgerhq/hw-transport-webusb";
import BluetoothTransport from "@ledgerhq/hw-transport-web-ble";
import CosmosApp from "ledger-cosmos-js"
import { signatureImport } from "secp256k1"
import semver from "semver"
Expand Down Expand Up @@ -54,7 +55,7 @@ export class Ledger {
async testDevice() {
// poll device with low timeout to check if the device is connected
const secondsTimeout = 3 // a lower value always timeouts
await this.connect(secondsTimeout)
await this.connect(secondsTimeout, false)
}
async isSendingData() {
// check if the device is connected or on screensaver mode
Expand All @@ -63,33 +64,56 @@ export class Ledger {
timeoutMessag: "Could not find a connected and unlocked Ledger device"
})
}
async isReady() {
async isReady(transportBLE) {
// check if the version is supported
const version = await this.getCosmosAppVersion()
const version = await this.getCosmosAppVersion(transportBLE)

if (!semver.gte(version, REQUIRED_COSMOS_APP_VERSION)) {
const msg = `Outdated version: Please update Ledger Cosmos App to the latest version.`
throw new Error(msg)
}

// throws if not open
await this.isCosmosAppOpen()
await this.isCosmosAppOpen(transportBLE)
}
// connects to the device and checks for compatibility
async connect(timeout = INTERACTION_TIMEOUT) {
async connect(timeout = INTERACTION_TIMEOUT, transportBLE) {
// assume well connection if connected once
if (this.cosmosApp) return

const transport = await TransportWebUSB.create(timeout)
let transport;
if(transportBLE === true || transportBLE === 'true'){
transport = await BluetoothTransport.create(timeout)
}
else{
transport= await TransportWebUSB.create(timeout)
}
const cosmosLedgerApp = new CosmosApp(transport)

this.cosmosApp = cosmosLedgerApp

await this.isSendingData()
await this.isReady()
await this.isReady(transportBLE)
}

async getDevice(){
return new Promise((resolve, reject) => {
const subscription = BluetoothTransport.listen({
next(event) {
if (event.type === 'add') {
subscription.unsubscribe();
resolve(event.descriptor);
}
},
error(error) {
reject(error);
},
complete() {
}
});
});
}
async getCosmosAppVersion() {
await this.connect()
async getCosmosAppVersion(transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)

const response = await this.cosmosApp.getVersion()
this.checkLedgerErrors(response)
Expand All @@ -99,8 +123,8 @@ export class Ledger {

return version
}
async isCosmosAppOpen() {
await this.connect()
async isCosmosAppOpen(transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)

const response = await this.cosmosApp.appInfo()
this.checkLedgerErrors(response)
Expand All @@ -110,21 +134,21 @@ export class Ledger {
throw new Error(`Close ${appName} and open the ${Meteor.settings.public.ledger.appName} app`)
}
}
async getPubKey() {
await this.connect()
async getPubKey(transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)

const response = await this.cosmosApp.publicKey(HDPATH)
this.checkLedgerErrors(response)
return response.compressed_pk
}
async getCosmosAddress() {
await this.connect()
async getCosmosAddress(transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)

const pubKey = await this.getPubKey(this.cosmosApp)
return {pubKey, address:createCosmosAddress(pubKey)}
}
async confirmLedgerAddress() {
await this.connect()
async confirmLedgerAddress(transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)
const cosmosAppVersion = await this.getCosmosAppVersion()

if (semver.lt(cosmosAppVersion, REQUIRED_COSMOS_APP_VERSION)) {
Expand All @@ -141,8 +165,8 @@ export class Ledger {
})
}

async sign(signMessage) {
await this.connect()
async sign(signMessage, transportBLE) {
await this.connect(INTERACTION_TIMEOUT, transportBLE)

const response = await this.cosmosApp.sign(HDPATH, signMessage)
this.checkLedgerErrors(response)
Expand Down Expand Up @@ -183,6 +207,8 @@ export class Ledger {
`Your ${Meteor.settings.public.ledger.appName} Ledger App is not up to date. ` +
`Please update to version ${REQUIRED_COSMOS_APP_VERSION}.`
)
case `Web Bluetooth API globally disabled`:
throw new Error(`Bluetooth not supported. Please use the latest version of Chrome browser.`)
case `No errors`:
// do nothing
break
Expand Down
66 changes: 66 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@babel/runtime": "^7.13.17",
"@ledgerhq/hw-transport-web-ble": "^5.50.0",
"@ledgerhq/hw-transport-webusb": "^5.49.0",
"@types/meteor-universe-i18n": "^1.14.5",
"babel-polyfill": "^6.26.0",
Expand Down
Loading