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 @@
+
+
+