diff --git a/CHANGELOG.md b/CHANGELOG.md index 959f3340a..6cc80b723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/both/i18n/en-us.i18n.yml b/both/i18n/en-us.i18n.yml index b09a24509..876a5b078 100644 --- a/both/i18n/en-us.i18n.yml +++ b/both/i18n/en-us.i18n.yml @@ -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 {$network} App {$version} or above is opened.' + signInWarning: 'Please make sure your Ledger device is turned on and {$network} App {$version} or above 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.' diff --git a/client/main.js b/client/main.js index 7b645e5d8..7741e1b95 100644 --- a/client/main.js +++ b/client/main.js @@ -13,6 +13,7 @@ import { render } from 'react-dom'; CURRENTUSERADDR = 'ledgerUserAddress'; CURRENTUSERPUBKEY = 'ledgerUserPubKey'; +BLELEDGERCONNECTION = 'ledgerBLEConnection' // import { onPageLoad } from 'meteor/server-render'; diff --git a/imports/api/ledger/server/methods.js b/imports/api/ledger/server/methods.js index 1f3c40b63..0626aa4fe 100644 --- a/imports/api/ledger/server/methods.js +++ b/imports/api/ledger/server/methods.js @@ -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 } }; diff --git a/imports/ui/ledger/LedgerActions.jsx b/imports/ui/ledger/LedgerActions.jsx index 4639afe43..721d1c442 100644 --- a/imports/ui/ledger/LedgerActions.jsx +++ b/imports/ui/ledger/LedgerActions.jsx @@ -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}); @@ -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; @@ -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, @@ -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) => { diff --git a/imports/ui/ledger/LedgerModal.jsx b/imports/ui/ledger/LedgerModal.jsx index 1f2c2eecf..2e605cfbc 100644 --- a/imports/ui/ledger/LedgerModal.jsx +++ b/imports/ui/ledger/LedgerModal.jsx @@ -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); } } @@ -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) @@ -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) => { @@ -74,8 +94,6 @@ class LedgerModal extends React.Component { } getActionButton() { - if (this.state.activeTab === '1' && !this.state.loading) - return if (this.state.activeTab === '2' && this.state.errorMessage !== '') return } @@ -102,6 +120,11 @@ class LedgerModal extends React.Component { accounts.signInWarning +
+ + +
+
accounts.BLESupport
{this.state.currentUser?You are currently logged in as {this.state.currentUser}.:null} diff --git a/imports/ui/ledger/ledger.js b/imports/ui/ledger/ledger.js index 94f2e56c9..4e339ea32 100644 --- a/imports/ui/ledger/ledger.js +++ b/imports/ui/ledger/ledger.js @@ -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" @@ -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 @@ -63,9 +64,9 @@ 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.` @@ -73,23 +74,46 @@ export class Ledger { } // 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) @@ -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) @@ -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)) { @@ -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) @@ -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 diff --git a/package-lock.json b/package-lock.json index d0e320a6e..396b867ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -240,6 +240,72 @@ "events": "^3.3.0" } }, + "@ledgerhq/hw-transport-web-ble": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-web-ble/-/hw-transport-web-ble-5.50.0.tgz", + "integrity": "sha512-tI9XKxF1w/DdQXcPRuY7JdGJr8Ghq013HLOQ63r6B4vy1DYPnH4niIyXXZqrP9HozZlmXZ/SnRSZ1uy9GlRvAg==", + "requires": { + "@ledgerhq/devices": "^5.50.0", + "@ledgerhq/errors": "^5.50.0", + "@ledgerhq/hw-transport": "^5.50.0", + "@ledgerhq/logs": "^5.50.0", + "rxjs": "^6.6.7" + }, + "dependencies": { + "@ledgerhq/devices": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-5.50.0.tgz", + "integrity": "sha512-VU3i48egHwUSHqNPKa8dNXLxD6gvmPKbKkp2pGL8tNNGXT44iHWplEAQy7et7+Fa48Sh7G2WPBvCbQg9K/SeCw==", + "requires": { + "@ledgerhq/errors": "^5.50.0", + "@ledgerhq/logs": "^5.50.0", + "rxjs": "^6.6.7", + "semver": "^7.3.5" + } + }, + "@ledgerhq/errors": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-5.50.0.tgz", + "integrity": "sha512-gu6aJ/BHuRlpU7kgVpy2vcYk6atjB4iauP2ymF7Gk0ez0Y/6VSMVSJvubeEQN+IV60+OBK0JgeIZG7OiHaw8ow==" + }, + "@ledgerhq/hw-transport": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-5.50.0.tgz", + "integrity": "sha512-VlcVGgp+Ae4hrUFzSroPJS4i7iBWEYVat91pCl8LyrN+xD8sjamHje69JCdDYY+Cb5++0pbSZt3FGiV0ml3xGA==", + "requires": { + "@ledgerhq/devices": "^5.50.0", + "@ledgerhq/errors": "^5.50.0", + "events": "^3.3.0" + } + }, + "@ledgerhq/logs": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-5.50.0.tgz", + "integrity": "sha512-swKHYCOZUGyVt4ge0u8a7AwNcA//h4nx5wIi0sruGye1IJ5Cva0GyK9L2/WdX+kWVTKp92ZiEo1df31lrWGPgA==" + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, "@ledgerhq/hw-transport-webusb": { "version": "5.49.0", "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-5.49.0.tgz", diff --git a/package.json b/package.json index 87e45b90a..5d571732b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/img/bluetooth.svg b/public/img/bluetooth.svg new file mode 100644 index 000000000..a35cc077b --- /dev/null +++ b/public/img/bluetooth.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/usb.svg b/public/img/usb.svg new file mode 100644 index 000000000..3ad83489c --- /dev/null +++ b/public/img/usb.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +