eth-lattice-keyring
Advanced tools
Comparing version 0.4.9 to 0.5.0
194
index.js
@@ -48,2 +48,4 @@ const crypto = require('crypto'); | ||
this.page = opts.page; | ||
if (opts.sdkState) | ||
this.sdkState = opts.sdkState; | ||
return Promise.resolve() | ||
@@ -57,3 +59,2 @@ } | ||
serialize() { | ||
this.triedConnection = false; | ||
return Promise.resolve({ | ||
@@ -70,2 +71,5 @@ creds: this.creds, | ||
hdPath: this.hdPath, | ||
sdkState: this.sdkSession ? | ||
this.sdkSession.getStateData() : | ||
null | ||
}) | ||
@@ -77,7 +81,13 @@ } | ||
isUnlocked () { | ||
return !!this._getCurrentWalletUID() | ||
return !!this._getCurrentWalletUID() && !!this.sdkSession; | ||
} | ||
// Initialize a session with the Lattice1 device using the GridPlus SDK | ||
unlock() { | ||
// NOTE: `bypassOnStateData=true` allows us to rehydrate a new SDK session without | ||
// reconnecting to the target Lattice. This is only currently used for signing | ||
// because it eliminates the need for 2 connection requests and shaves off ~4-6sec. | ||
// We avoid passing `bypassOnStateData=true` for other calls on `unlock` to avoid | ||
// possible edge cases related to this new functionality (it's probably fine - just | ||
// being cautious). In the future we may remove `bypassOnStateData` entirely. | ||
unlock(bypassOnStateData=false) { | ||
return new Promise((resolve, reject) => { | ||
@@ -96,6 +106,6 @@ // Force compatability. `this.accountOpts` were added after other | ||
if (this.isUnlocked()) { | ||
if (this.isUnlocked() && !this.forceReconnect) { | ||
return resolve('Unlocked'); | ||
} | ||
this._getCreds() | ||
@@ -110,3 +120,8 @@ .then((creds) => { | ||
}) | ||
.then(() => { | ||
.then((includedStateData) => { | ||
// If state data was provided and if we are authorized to | ||
// bypass reconnecting, we can exit here. | ||
if (includedStateData && bypassOnStateData) { | ||
return resolve('Unlocked'); | ||
} | ||
return this._connect(); | ||
@@ -171,35 +186,3 @@ }) | ||
getAccounts() { | ||
return new Promise((resolve, reject) => { | ||
const accounts = this.accounts ? this.accounts.slice() : [].slice(); | ||
// Exit without connecting? | ||
// * If we have no credentials, just return the accounts. It shouldn't be | ||
// possible to have addresses without credentials, i.e. it should be an | ||
// empty array. | ||
// * If we have already tried to connect, we don't need to do it again -- | ||
// just return the accounts. Note that this proceeds regardless of the | ||
// connection result. If the user's device is offline then the extension | ||
// will still unlock but the device won't be reachable until it tries to | ||
// connect again. | ||
if (!this._hasCreds() || this.triedConnection) { | ||
return resolve(accounts); | ||
} | ||
// Since this is called when the extension unlocks, we should make sure | ||
// we have a connection with the user's Lattice. If no connection is found | ||
// this will attempt to create one. The error will not be handled in this | ||
// function because the user could have their Lattice unplugged and may | ||
// want to use a different account on MetaMask. | ||
this._ensureCurrentWalletUID() | ||
.then(() => { | ||
this.triedConnection = true; | ||
return resolve(accounts); | ||
}) | ||
.catch((err) => { | ||
// Rather than throw an error here, we should still unlock the | ||
// extension and return whatever accounts we currently have. | ||
// If we threw an error, it would lock people out of the extension | ||
// if they could not talk to their Lattice. | ||
this.triedConnection = true; | ||
return resolve(accounts); | ||
}) | ||
}) | ||
return Promise.resolve(this.accounts ? this.accounts.slice() : [].slice()); | ||
} | ||
@@ -301,3 +284,3 @@ | ||
}) | ||
return resolve(validatingTx) | ||
return resolve(validatingTx); | ||
}) | ||
@@ -340,14 +323,18 @@ .catch((err) => { | ||
} | ||
if (!this.isUnlocked()) | ||
return reject(new Error('No connection to Lattice. Could not send request.')); | ||
this.sdkSession.sign(req, (err, res) => { | ||
if (err) | ||
if (err) { | ||
return reject(new Error(err)); | ||
if (!res.sig) | ||
} | ||
if (!this._syncCurrentWalletUID()) { | ||
return reject('No active wallet.'); | ||
} | ||
if (!res.sig) { | ||
return reject(new Error('No signature returned')); | ||
} | ||
// Convert the `v` to a number. It should convert to 0 or 1 | ||
try { | ||
let v = res.sig.v.toString('hex'); | ||
if (v.length < 2) | ||
if (v.length < 2) { | ||
v = `0${v}`; | ||
} | ||
return resolve(`0x${res.sig.r}${res.sig.s}${v}`); | ||
@@ -381,2 +368,9 @@ } catch (err) { | ||
getFirstPage() { | ||
// This function gets called after the user has connected to the Lattice. | ||
// Update a state variable to force opening of the Lattice manager window. | ||
// If we don't do this, MetaMask will automatically start requesting addresses, | ||
// even if the device is not reachable. | ||
// This way the user can close the window and connect accounts from other | ||
// wallets instead of being forced into selecting Lattice accounts | ||
this.forceReconnect = true; | ||
this.page = 0; | ||
@@ -409,6 +403,11 @@ return this._getPage(0); | ||
return new Promise((resolve, reject) => { | ||
this._ensureCurrentWalletUID() | ||
// Unlock and get the wallet UID. We will bypass the reconnection | ||
// step if we are able to rehydrate an SDK session with state data. | ||
this.unlock(true) | ||
.then(() => { | ||
return this.getAccounts() | ||
return this._ensureCurrentWalletUID(); | ||
}) | ||
.then(() => { | ||
return this.getAccounts(); | ||
}) | ||
.then((addrs) => { | ||
@@ -422,8 +421,4 @@ // Find the signer in our current set of accounts | ||
}) | ||
if (accountIdx === null) | ||
if (accountIdx === null) { | ||
return reject('Signer not present'); | ||
// Make sure the account is associated with the current wallet | ||
if (this.accountOpts[accountIdx].walletUID !== this._getCurrentWalletUID()) { | ||
return reject(new Error('Account on a different wallet. ' + | ||
'Please switch to the correct wallet on your Lattice.')); | ||
} | ||
@@ -534,5 +529,6 @@ return resolve(accountIdx); | ||
// We only need to setup if we don't have a deviceID | ||
if (this._hasCreds()) | ||
if (this._hasCreds() && !this.forceReconnect) | ||
return resolve(); | ||
// Cancel the force reconnect, if applicable | ||
this.forceReconnect = false; | ||
// If we are not aware of what Lattice we should be talking to, | ||
@@ -631,14 +627,8 @@ // we need to open a window that lets the user go through the | ||
this.sdkSession.timeout = SDK_TIMEOUT; | ||
if (err) | ||
if (err) { | ||
return reject(err); | ||
// Save the current wallet UID | ||
const activeWallet = this.sdkSession.getActiveWallet(); | ||
if (!activeWallet || !activeWallet.uid) | ||
return reject(new Error('No active wallet')); | ||
const newUID = activeWallet.uid.toString('hex'); | ||
// If we fetched a walletUID that does not match our current one, | ||
// reset accounts and update the known UID | ||
if (newUID != this.walletUID) { | ||
this.walletUID = newUID; | ||
} | ||
if (!this._syncCurrentWalletUID()) { | ||
return reject('No active wallet.'); | ||
} | ||
return resolve(); | ||
@@ -658,12 +648,23 @@ }); | ||
url = this.creds.endpoint | ||
const setupData = { | ||
name: this.appName, | ||
baseUrl: url, | ||
crypto, | ||
timeout: SDK_TIMEOUT, | ||
privKey: this._genSessionKey(), | ||
network: this.network | ||
let setupData; | ||
if (this.sdkState) { | ||
// If we have state data we can fully rehydrate the session. | ||
setupData = { | ||
stateData: this.sdkState | ||
} | ||
} else { | ||
// If we have no state data, we need to create a session. | ||
// Its state will be saved once the connection is established. | ||
setupData = { | ||
name: this.appName, | ||
baseUrl: url, | ||
timeout: SDK_TIMEOUT, | ||
privKey: this._genSessionKey(), | ||
network: this.network, | ||
} | ||
} | ||
this.sdkSession = new SDK.Client(setupData); | ||
return resolve(); | ||
// Return a boolean indicating whether we provided state data. | ||
// If we have, we can skip `connect`. | ||
return resolve(!!setupData.stateData); | ||
} catch (err) { | ||
@@ -677,10 +678,10 @@ return reject(err); | ||
return new Promise((resolve, reject) => { | ||
if (!this.isUnlocked()) | ||
if (!this.isUnlocked()) { | ||
return reject('No connection to Lattice. Cannot fetch addresses.') | ||
} | ||
this.__fetchAddresses(n, i, (err, addrs) => { | ||
if (err) | ||
if (err) { | ||
return reject(err); | ||
else | ||
return resolve(addrs); | ||
} | ||
return resolve(addrs); | ||
}) | ||
@@ -708,5 +709,9 @@ }) | ||
return cb(err); | ||
if (!this._syncCurrentWalletUID()) { | ||
return cb(new Error('No active wallet.')); | ||
} | ||
// Sanity check -- if this returned 0 addresses, handle the error | ||
if (addrs.length < 1) | ||
if (addrs.length < 1) { | ||
return cb(new Error('No addresses returned')); | ||
} | ||
// Return the addresses we fetched *without* updating state | ||
@@ -723,9 +728,12 @@ if (shouldRecurse) { | ||
return new Promise((resolve, reject) => { | ||
if (!this.isUnlocked()) | ||
return reject(new Error('No connection to Lattice. Cannot sign transaction.')); | ||
this.sdkSession.sign({ currency: 'ETH', data: txData }, (err, res) => { | ||
if (err) | ||
if (err) { | ||
return reject(err); | ||
if (!res.tx) | ||
} | ||
if (!this._syncCurrentWalletUID()) { | ||
return reject('No active wallet.'); | ||
} | ||
if (!res.tx) { | ||
return reject(new Error('No transaction payload returned.')); | ||
} | ||
// Here we catch an edge case where the requester is asking for an EIP1559 | ||
@@ -817,8 +825,26 @@ // transaction but firmware is not updated to support it. We fallback to legacy. | ||
_getCurrentWalletUID() { | ||
return this.walletUID || null; | ||
} | ||
// The SDK has an auto-retry mechanism for all requests if a "wrong wallet" | ||
// error gets thrown. In such a case, the SDK will re-connect to the device to | ||
// find the new wallet UID and will then save that UID to memory and will retry | ||
// the original request with that new wallet UID. | ||
// Therefore, we should sync the walletUID with `this.walletUID` after each | ||
// SDK request. This is a synchronous and fast operation. | ||
_syncCurrentWalletUID() { | ||
if (!this.sdkSession) { | ||
return null; | ||
} else if (!this.sdkSession.getActiveWallet()) { | ||
} | ||
const activeWallet = this.sdkSession.getActiveWallet(); | ||
if (!activeWallet || !activeWallet.uid) { | ||
return null; | ||
} | ||
return this.sdkSession.getActiveWallet().uid.toString('hex'); | ||
const newUID = activeWallet.uid.toString('hex'); | ||
// If we fetched a walletUID that does not match our current one, | ||
// reset accounts and update the known UID | ||
if (newUID != this.walletUID) { | ||
this.walletUID = newUID; | ||
} | ||
return this.walletUID; | ||
} | ||
@@ -825,0 +851,0 @@ |
{ | ||
"name": "eth-lattice-keyring", | ||
"version": "0.4.9", | ||
"version": "0.5.0", | ||
"description": "Keyring for connecting to the Lattice1 hardware wallet", | ||
@@ -24,4 +24,4 @@ "main": "index.js", | ||
"ethereumjs-util": "^7.0.10", | ||
"gridplus-sdk": "^0.9.7" | ||
"gridplus-sdk": "^1.0.0" | ||
} | ||
} |
32094
807
+ Added@ethereumjs/common@2.4.0(transitive)
+ Added@ethereumjs/tx@3.3.0(transitive)
+ Addedgridplus-sdk@1.3.5(transitive)
+ Addedrlp@3.0.0(transitive)
- Removedgridplus-sdk@0.9.10(transitive)
- Removedrlp-browser@1.0.1(transitive)
Updatedgridplus-sdk@^1.0.0