New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

eth-lattice-keyring

Package Overview
Dependencies
Maintainers
1
Versions
76
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

eth-lattice-keyring - npm Package Compare versions

Comparing version 0.5.0 to 0.6.0

945

index.js
const crypto = require('crypto');
const EventEmitter = require('events').EventEmitter;
const BN = require('bignumber.js');
const BN = require('bn.js');
const SDK = require('gridplus-sdk');

@@ -8,2 +8,3 @@ const EthTx = require('@ethereumjs/tx');

const Util = require('ethereumjs-util');
const secp256k1 = require('secp256k1');
const keyringType = 'Lattice Hardware';

@@ -28,3 +29,3 @@ const HARDENED_OFFSET = 0x80000000;

//-------------------------------------------------------------------
deserialize (opts = {}) {
async deserialize (opts = {}) {
if (opts.hdPath)

@@ -52,3 +53,3 @@ this.hdPath = opts.hdPath;

this.sdkState = opts.sdkState;
return Promise.resolve()
return;
}

@@ -60,4 +61,4 @@

serialize() {
return Promise.resolve({
async serialize() {
return {
creds: this.creds,

@@ -76,3 +77,3 @@ accounts: this.accounts,

null
})
};
}

@@ -93,259 +94,249 @@

// being cautious). In the future we may remove `bypassOnStateData` entirely.
unlock(bypassOnStateData=false) {
return new Promise((resolve, reject) => {
// Force compatability. `this.accountOpts` were added after other
// state params and must be synced in order for this keyring to function.
if ((!this.accountOpts) ||
(this.accounts.length > 0 && this.accountOpts.length != this.accounts.length))
{
this.forgetDevice();
return reject(new Error(
'You can now add multiple Lattice and SafeCard accounts at the same time! ' +
'Your accounts have been cleared. Please press Continue to add them back in.'
));
}
if (this.isUnlocked() && !this.forceReconnect) {
return resolve('Unlocked');
}
this._getCreds()
.then((creds) => {
if (creds) {
this.creds.deviceID = creds.deviceID;
this.creds.password = creds.password;
this.creds.endpoint = creds.endpoint || null;
}
return this._initSession();
})
.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();
})
.then(() => {
return resolve('Unlocked');
})
.catch((err) => {
return reject(new Error(err));
})
})
async unlock (bypassOnStateData = false) {
// Force compatability. `this.accountOpts` were added after other
// state params and must be synced in order for this keyring to function.
if (
!this.accountOpts ||
(this.accounts.length > 0 &&
this.accountOpts.length != this.accounts.length)
) {
this.forgetDevice();
throw new Error(
"You can now add multiple Lattice and SafeCard accounts at the same time! " +
"Your accounts have been cleared. Please press Continue to add them back in."
);
}
if (this.isUnlocked()) {
return "Unlocked";
}
const creds = await this._getCreds();
if (creds) {
this.creds.deviceID = creds.deviceID;
this.creds.password = creds.password;
this.creds.endpoint = creds.endpoint || null;
}
const includedStateData = await this._initSession();
// If state data was provided and if we are authorized to
// bypass reconnecting, we can exit here.
if (includedStateData && bypassOnStateData) {
return "Unlocked";
}
await this._connect();
return "Unlocked";
}
// Add addresses to the local store and return the full result
addAccounts(n=1) {
return new Promise((resolve, reject) => {
if (n === CLOSE_CODE) {
// Special case: use a code to forget the device.
// (This function is overloaded due to constraints upstream)
this.forgetDevice();
return resolve([]);
} else if (n <= 0) {
// Avoid non-positive numbers.
return reject('Number of accounts to add must be a positive number.');
} else {
// Normal behavior: establish the connection and fetch addresses.
this.unlock()
.then(() => {
return this._fetchAddresses(n, this.unlockedAccount)
async addAccounts(n=1) {
if (n === CLOSE_CODE) {
// Special case: use a code to forget the device.
// (This function is overloaded due to constraints upstream)
this.forgetDevice();
return [];
} else if (n <= 0) {
// Avoid non-positive numbers.
throw new Error(
'Number of accounts to add must be a positive number.'
);
}
// Normal behavior: establish the connection and fetch addresses.
await this.unlock()
const addrs = await this._fetchAddresses(n, this.unlockedAccount);
const walletUID = this._getCurrentWalletUID();
if (!walletUID) {
// We should not add accounts that do not have wallet UIDs.
// Something went wrong and needs to be retried.
await this._connect();
throw new Error('No active wallet found in Lattice. Please retry.');
}
// Add these indices
addrs.forEach((addr, i) => {
let alreadySaved = false;
for (let j = 0; j < this.accounts.length; j++) {
if ((this.accounts[j] === addr) &&
(this.accountOpts[j].walletUID === walletUID) &&
(this.accountOpts[j].hdPath === this.hdPath))
alreadySaved = true;
}
if (!alreadySaved) {
this.accounts.push(addr);
this.accountIndices.push(this.unlockedAccount+i);
this.accountOpts.push({
walletUID,
hdPath: this.hdPath,
})
.then((addrs) => {
const walletUID = this._getCurrentWalletUID();
// Add these indices
addrs.forEach((addr, i) => {
let alreadySaved = false;
for (let j = 0; j < this.accounts.length; j++) {
if ((this.accounts[j] === addr) &&
(this.accountOpts[j].walletUID === walletUID) &&
(this.accountOpts[j].hdPath === this.hdPath))
alreadySaved = true;
}
if (!alreadySaved) {
this.accounts.push(addr);
this.accountIndices.push(this.unlockedAccount+i);
this.accountOpts.push({
walletUID,
hdPath: this.hdPath,
})
}
})
return resolve(this.accounts);
})
.catch((err) => {
return reject(new Error(err));
})
}
})
return this.accounts;
}
// Return the local store of addresses. This gets called when the extension unlocks.
getAccounts() {
return Promise.resolve(this.accounts ? this.accounts.slice() : [].slice());
async getAccounts() {
return this.accounts ? [...this.accounts] : [];
}
signTransaction (address, tx) {
return new Promise((resolve, reject) => {
this._findSignerIdx(address)
.then((accountIdx) => {
try {
// Build the Lattice request data and make request
// We expect `tx` to be an `ethereumjs-tx` object, meaning all fields are bufferized
// To ensure everything plays nicely with gridplus-sdk, we convert everything to hex strings
const addressIdx = this.accountIndices[accountIdx];
const addressParentPath = this.accountOpts[accountIdx].hdPath;
const txData = {
chainId: `0x${this._getEthereumJsChainId(tx).toString('hex')}` || 1,
nonce: `0x${tx.nonce.toString('hex')}` || 0,
gasLimit: `0x${tx.gasLimit.toString('hex')}`,
to: !!tx.to ? tx.to.toString('hex') : null, // null for contract deployments
value: `0x${tx.value.toString('hex')}`,
data: tx.data.length === 0 ? null : `0x${tx.data.toString('hex')}`,
signerPath: this._getHDPathIndices(addressParentPath, addressIdx),
}
switch (tx._type) {
case 2: // eip1559
if ((tx.maxPriorityFeePerGas === null || tx.maxFeePerGas === null) ||
(tx.maxPriorityFeePerGas === undefined || tx.maxFeePerGas === undefined))
throw new Error('`maxPriorityFeePerGas` and `maxFeePerGas` must be included for EIP1559 transactions.');
txData.maxPriorityFeePerGas = `0x${tx.maxPriorityFeePerGas.toString('hex')}`;
txData.maxFeePerGas = `0x${tx.maxFeePerGas.toString('hex')}`;
txData.accessList = tx.accessList || [];
txData.type = 2;
break;
case 1: // eip2930
txData.accessList = tx.accessList || [];
txData.gasPrice = `0x${tx.gasPrice.toString('hex')}`;
txData.type = 1;
break;
default: // legacy
txData.gasPrice = `0x${tx.gasPrice.toString('hex')}`;
txData.type = null;
break;
}
// Lattice firmware v0.11.0 implemented EIP1559 and EIP2930 so for previous verisons
// we need to overwrite relevant params and revert to legacy type.
// Note: `this.sdkSession.fwVersion is of format [fix, minor, major, reserved]
const forceLegacyTx = this.sdkSession.fwVersion[2] < 1 &&
this.sdkSession.fwVersion[1] < 11;
if (forceLegacyTx && txData.type === 2) {
txData.gasPrice = txData.maxFeePerGas;
txData.revertToLegacy = true;
delete txData.type;
delete txData.maxFeePerGas;
delete txData.maxPriorityFeePerGas;
delete txData.accessList;
} else if (forceLegacyTx && txData.type === 1) {
txData.revertToLegacy = true;
delete txData.type;
delete txData.accessList;
}
// Get the signature
return this._signTxData(txData)
} catch (err) {
throw new Error(`Failed to build transaction.`)
async signTransaction (address, tx) {
let signedTx, v;
// We will be adding a signature to hydration data for a new
// transaction object since the sig data is not mutable.
// Setup `txToReturn` data and start adding to it.
const txToReturn = tx.toJSON();
txToReturn.type = tx._type || null;
// Setup info related to signer account
const accountIdx = await this._findSignerIdx(address);
const chainId = getTxChainId(tx).toNumber();
const fwVersion = this.sdkSession.getFwVersion();
const addressIdx = this.accountIndices[accountIdx];
const { hdPath } = this.accountOpts[accountIdx];
// Build the signing request
if (fwVersion.major > 0 || fwVersion.minor >= 15) {
// Newer firmware versions support an easier pathway
const data = {
// Legacy transactions return tx params. Newer transactions
// return the raw, serialized transaction
payload: tx._type ?
tx.getMessageToSign(false) :
rlp.encode(tx.getMessageToSign(false)),
curveType: SDK.Constants.SIGNING.CURVES.SECP256K1,
hashType: SDK.Constants.SIGNING.HASHES.KECCAK256,
encodingType: SDK.Constants.SIGNING.ENCODINGS.EVM,
signerPath: this._getHDPathIndices(hdPath, addressIdx),
};
signedTx = await this.sdkSession.sign({ data });
v = getV(tx, signedTx);
} else {
// Older firmware versions (<0.15.0) use the legacy signing pathway.
let txData;
try {
txData = {
chainId,
nonce: `0x${tx.nonce.toString('hex')}` || 0,
gasLimit: `0x${tx.gasLimit.toString('hex')}`,
to: !!tx.to ? tx.to.toString('hex') : null, // null for contract deployments
value: `0x${tx.value.toString('hex')}`,
data: tx.data.length === 0 ? null : `0x${tx.data.toString('hex')}`,
signerPath: this._getHDPathIndices(hdPath, addressIdx),
}
})
.then((signedTx) => {
// Add the sig params. `signedTx = { sig: { v, r, s }, tx, txHash}`
if (!signedTx.sig || !signedTx.sig.v || !signedTx.sig.r || !signedTx.sig.s)
return reject(new Error('No signature returned.'));
const txToReturn = tx.toJSON();
const v = signedTx.sig.v.length === 0 ? '0' : signedTx.sig.v.toString('hex')
txToReturn.r = Util.addHexPrefix(signedTx.sig.r.toString('hex'));
txToReturn.s = Util.addHexPrefix(signedTx.sig.s.toString('hex'));
txToReturn.v = Util.addHexPrefix(v);
if (signedTx.revertToLegacy === true) {
// If firmware does not support an EIP1559/2930 transaction we revert to legacy
txToReturn.type = 0;
txToReturn.gasPrice = signedTx.gasPrice;
} else {
// Otherwise relay the tx type
txToReturn.type = signedTx.type;
switch (tx._type) {
case 2: // eip1559
if ((tx.maxPriorityFeePerGas === null || tx.maxFeePerGas === null) ||
(tx.maxPriorityFeePerGas === undefined || tx.maxFeePerGas === undefined))
throw new Error('`maxPriorityFeePerGas` and `maxFeePerGas` must be included for EIP1559 transactions.');
txData.maxPriorityFeePerGas = `0x${tx.maxPriorityFeePerGas.toString('hex')}`;
txData.maxFeePerGas = `0x${tx.maxFeePerGas.toString('hex')}`;
txData.accessList = tx.accessList || [];
txData.type = 2;
break;
case 1: // eip2930
txData.accessList = tx.accessList || [];
txData.gasPrice = `0x${tx.gasPrice.toString('hex')}`;
txData.type = 1;
break;
default: // legacy
txData.gasPrice = `0x${tx.gasPrice.toString('hex')}`;
txData.type = null;
break;
}
// Lattice firmware v0.11.0 implemented EIP1559 and EIP2930
// We should throw an error if we cannot support this.
if (fwVersion.major === 0 && fwVersion.minor <= 11 && txData.type) {
throw new Error('Please update Lattice firmware.');
}
} catch (err) {
throw new Error(`Failed to build transaction.`)
}
// Get the signature
signedTx = await this.sdkSession.sign({ currency: 'ETH', data: txData });
// Build the tx for export
let validatingTx;
const _chainId = `0x${this._getEthereumJsChainId(tx).toString('hex')}`;
const chainId = new BN(_chainId).toNumber();
const customNetwork = Common.forCustomChain('mainnet', {
name: 'notMainnet',
networkId: chainId,
chainId: chainId,
}, 'london')
// Add the sig params. `signedTx = { sig: { v, r, s }, tx, txHash}`
if (!signedTx.sig || !signedTx.sig.r || !signedTx.sig.s) {
throw new Error('No signature returned.');
}
if (signedTx.sig.v === undefined) {
// V2 signature needs `v` calculated
v = getV(tx, signedTx);
} else {
// Legacy signatures have `v` in the response
v = signedTx.sig.v.length === 0 ? '0' : signedTx.sig.v.toString('hex')
}
}
validatingTx = EthTx.TransactionFactory.fromTxData(txToReturn, {
common: customNetwork, freeze: Object.isFrozen(tx)
})
return resolve(validatingTx);
})
.catch((err) => {
return reject(new Error(err));
})
// Pack the signature into the return object
txToReturn.r = Util.addHexPrefix(signedTx.sig.r.toString('hex'));
txToReturn.s = Util.addHexPrefix(signedTx.sig.s.toString('hex'));
txToReturn.v = Util.addHexPrefix(v);
// Make sure the active wallet is correct to avoid returning
// a signature from an unexpected signer.
const foundIdx = await this._accountIdxInCurrentWallet(address);
if (foundIdx === null) {
throw new Error(
'Wrong account. Please change your Lattice wallet or ' +
'switch to an account on your current active wallet.'
);
}
return EthTx.TransactionFactory.fromTxData(txToReturn, {
common: tx.common, freeze: Object.isFrozen(tx)
})
}
signPersonalMessage(address, msg) {
async signPersonalMessage(address, msg) {
return this.signMessage(address, { payload: msg, protocol: 'signPersonal' });
}
signTypedData(address, msg, opts) {
if (opts.version && (opts.version !== 'V4' && opts.version !== 'V3'))
throw new Error(`Only signTypedData V3 and V4 messages (EIP712) are supported. Got version ${opts.version}`);
async signTypedData(address, msg, opts) {
if (opts.version && (opts.version !== 'V4' && opts.version !== 'V3')) {
throw new Error(
`Only signTypedData V3 and V4 messages (EIP712) are supported. Got version ${opts.version}`
);
}
return this.signMessage(address, { payload: msg, protocol: 'eip712' })
}
signMessage(address, msg) {
return new Promise((resolve, reject) => {
this._findSignerIdx(address)
.then((accountIdx) => {
let { payload, protocol } = msg;
// If the message is not an object we assume it is a legacy signPersonal request
if (!payload || !protocol) {
payload = msg;
protocol = 'signPersonal';
}
const addressIdx = this.accountIndices[accountIdx];
const addressParentPath = this.accountOpts[accountIdx].hdPath;
const req = {
currency: 'ETH_MSG',
data: {
protocol,
payload,
signerPath: this._getHDPathIndices(addressParentPath, addressIdx),
}
}
this.sdkSession.sign(req, (err, res) => {
if (err) {
return reject(new Error(err));
}
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) {
v = `0${v}`;
}
return resolve(`0x${res.sig.r}${res.sig.s}${v}`);
} catch (err) {
return reject(new Error('Invalid signature format returned.'))
}
})
})
.catch((err) => {
return reject(new Error(err));
})
})
async signMessage (address, msg) {
const accountIdx = await this._findSignerIdx(address);
let { payload, protocol } = msg;
// If the message is not an object we assume it is a legacy signPersonal request
if (!payload || !protocol) {
payload = msg;
protocol = "signPersonal";
}
const addressIdx = this.accountIndices[accountIdx];
const addressParentPath = this.accountOpts[accountIdx].hdPath;
const req = {
currency: "ETH_MSG",
data: {
protocol,
payload,
signerPath: this._getHDPathIndices(addressParentPath, addressIdx),
},
};
const res = await this.sdkSession.sign(req);
if (!res.sig) {
throw new Error("No signature returned");
}
// Convert the `v` to a number. It should convert to 0 or 1
let v;
try {
v = res.sig.v.toString("hex");
if (v.length < 2) {
v = `0${v}`;
}
} catch (err) {
throw new Error("Invalid signature format returned.");
}
// Make sure the active wallet is correct to avoid returning
// a signature from an unexpected signer.
const foundIdx = await this._accountIdxInCurrentWallet(address);
if (foundIdx === null) {
throw new Error(
'Wrong account. Please change your Lattice wallet or ' +
'switch to an account on your current active wallet.'
);
}
// Return the sig string
return `0x${res.sig.r}${res.sig.s}${v}`;
}
exportAccount(address) {
return Promise.reject(Error('exportAccount not supported by this device'))
async exportAccount(address) {
throw new Error('exportAccount not supported by this device');
}

@@ -364,10 +355,3 @@

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;
async getFirstPage() {
this.page = 0;

@@ -377,7 +361,7 @@ return this._getPage(0);

getNextPage () {
async getNextPage () {
return this._getPage(1);
}
getPreviousPage () {
async getPreviousPage () {
return this._getPage(-1);

@@ -399,30 +383,58 @@ }

// Note that this is the BIP39 path index, not the index in the address cache.
_findSignerIdx(address) {
return new Promise((resolve, reject) => {
// 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._ensureCurrentWalletUID();
})
.then(() => {
return this.getAccounts();
})
.then((addrs) => {
// Find the signer in our current set of accounts
// If we can't find it, return an error
let accountIdx = null;
addrs.forEach((addr, i) => {
if (address.toLowerCase() === addr.toLowerCase())
accountIdx = i;
})
if (accountIdx === null) {
return reject('Signer not present');
}
return resolve(accountIdx);
})
.catch((err) => {
return reject(err);
})
async _findSignerIdx (address) {
// Take note if this was already unlocked
const wasUnlocked = this.isUnlocked();
// Unlock and get the wallet UID. We will bypass the reconnection
// step if we are able to rehydrate an SDK session with state data.
await this.unlock(true);
let accountIdx = await this._accountIdxInCurrentWallet(address);
if (accountIdx !== null) {
return accountIdx;
}
// If this was unlocked already, the `this.unlock` call did not sync
// data with the Lattice. We should force a sync by reconnecting.
if (wasUnlocked) {
await this._connect();
// Check the new wallet and see if there is a match
accountIdx = await this._accountIdxInCurrentWallet(address);
if (accountIdx !== null) {
return accountIdx;
}
}
// If we could not find a match, exit here
throw new Error(
"Account not found in active Lattice wallet. Please switch."
);
}
async _accountIdxInCurrentWallet(address) {
// Get the wallet UID associated with the signer and make sure
// the Lattice has that as its active wallet before continuing.
const accountIdx = await this._findAccountByAddress(address);
const { walletUID } = this.accountOpts[accountIdx];
// Get the last updated SDK wallet UID
const activeWallet = this.sdkSession.getActiveWallet();
if (!activeWallet) {
this._connect();
throw new Error("No active wallet in Lattice.");
}
const activeUID = activeWallet.uid.toString("hex");
// If this is already the active wallet we don't need to make a request
if (walletUID.toString("hex") === activeUID) {
return accountIdx;
}
return null;
}
async _findAccountByAddress(address) {
const addrs = await this.getAccounts();
let accountIdx = -1;
addrs.forEach((addr, i) => {
if (address.toLowerCase() === addr.toLowerCase())
accountIdx = i;
})
if (accountIdx < 0) {
throw new Error('Signer not present');
}
return accountIdx;
}

@@ -480,4 +492,4 @@

_openConnectorTab(url) {
return new Promise((resolve, reject) => {
async _openConnectorTab(url) {
try {
const browserTab = window.open(url);

@@ -487,3 +499,3 @@ // Preferred option for Chromium browsers. This extension runs in a window

if (browserTab) {
return resolve({ chromium: browserTab });
return { chromium: browserTab };
} else if (browser && browser.tabs && browser.tabs.create) {

@@ -494,31 +506,20 @@ // FireFox extensions do not run in windows, so it will return `null` from

// will indicate that the user has logged in.
browser.tabs.create({url})
.then((tab) => {
return resolve({ firefox: tab });
})
.catch((err) => {
return reject(new Error('Failed to open Lattice connector.'))
})
const tab = await browser.tabs.create({url})
return { firefox: tab };
} else {
return reject(new Error('Unknown browser context. Cannot open Lattice connector.'))
throw new Error('Unknown browser context. Cannot open Lattice connector.');
}
})
} catch (err) {
throw new Error('Failed to open Lattice connector.');
}
}
_findTabById(id) {
return new Promise((resolve, reject) => {
browser.tabs.query({})
.then((tabs) => {
tabs.forEach((tab) => {
if (tab.id === id) {
return resolve(tab);
}
})
return resolve(null);
})
.catch((err) => {
return reject(err);
})
async _findTabById(id) {
const tabs = browser.tabs.query({});
tabs.forEach((tab) => {
if (tab.id === id) {
return tab;
}
})
return null;
}

@@ -529,6 +530,4 @@

// We only need to setup if we don't have a deviceID
if (this._hasCreds() && !this.forceReconnect)
if (this._hasCreds())
return resolve();
// Cancel the force reconnect, if applicable
this.forceReconnect = false;
// If we are not aware of what Lattice we should be talking to,

@@ -619,4 +618,4 @@ // we need to open a window that lets the user go through the

// This will handle SafeCard insertion/removal events.
_connect() {
return new Promise((resolve, reject) => {
async _connect () {
try {
// Attempt to connect with a Lattice using a shorter timeout. If

@@ -626,126 +625,78 @@ // the device is unplugged it will time out and we don't need to wait

this.sdkSession.timeout = CONNECT_TIMEOUT;
this.sdkSession.connect(this.creds.deviceID, (err) => {
this.sdkSession.timeout = SDK_TIMEOUT;
if (err) {
return reject(err);
}
if (!this._syncCurrentWalletUID()) {
return reject('No active wallet.');
}
return resolve();
});
})
await this.sdkSession.connect(this.creds.deviceID)
} finally {
// Reset to normal timeout no matter what
this.sdkSession.timeout = SDK_TIMEOUT;
}
}
_initSession() {
return new Promise((resolve, reject) => {
if (this.isUnlocked()) {
return resolve();
async _initSession() {
if (this.isUnlocked()) {
return;
}
let url = 'https://signing.gridpl.us';
if (this.creds.endpoint)
url = this.creds.endpoint
let setupData = {
name: this.appName,
baseUrl: url,
timeout: SDK_TIMEOUT,
privKey: this._genSessionKey(),
network: this.network,
skipRetryOnWrongWallet: true,
};
/*
NOTE: We need state to actually be synced by MetaMask or we can't
use this. See: https://github.com/MetaMask/KeyringController/issues/130
if (this.sdkState) {
// If we have state data we can fully rehydrate the session.
setupData = {
stateData: this.sdkState,
skipRetryOnWrongWallet: true,
}
try {
let url = 'https://signing.gridpl.us';
if (this.creds.endpoint)
url = this.creds.endpoint
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 a boolean indicating whether we provided state data.
// If we have, we can skip `connect`.
return resolve(!!setupData.stateData);
} catch (err) {
return reject(err);
}
})
}
*/
this.sdkSession = new SDK.Client(setupData);
// Return a boolean indicating whether we provided state data.
// If we have, we can skip `connect`.
return !!setupData.stateData;
}
_fetchAddresses(n=1, i=0, recursedAddrs=[]) {
return new Promise((resolve, reject) => {
if (!this.isUnlocked()) {
return reject('No connection to Lattice. Cannot fetch addresses.')
}
this.__fetchAddresses(n, i, (err, addrs) => {
if (err) {
return reject(err);
}
return resolve(addrs);
})
})
async _fetchAddresses(n=1, i=0, recursedAddrs=[]) {
if (!this.isUnlocked()) {
throw new Error('No connection to Lattice. Cannot fetch addresses.')
}
return this.__fetchAddresses(n, i);
}
__fetchAddresses(n=1, i=0, cb, recursedAddrs=[]) {
// Determine if we need to do a recursive call here. We prefer not to
// because they will be much slower, but Ledger paths require it since
// they are non-standard.
if (n === 0)
return cb(null, recursedAddrs);
const shouldRecurse = this._hdPathHasInternalVarIdx();
async __fetchAddresses(n=1, i=0, recursedAddrs=[]) {
// Determine if we need to do a recursive call here. We prefer not to
// because they will be much slower, but Ledger paths require it since
// they are non-standard.
if (n === 0) {
return recursedAddrs;
}
const shouldRecurse = this._hdPathHasInternalVarIdx();
// Make the request to get the requested address
const addrData = {
currency: 'ETH',
startPath: this._getHDPathIndices(this.hdPath, i),
n: shouldRecurse ? 1 : n,
skipCache: true,
};
this.sdkSession.getAddresses(addrData, (err, addrs) => {
if (err)
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) {
return cb(new Error('No addresses returned'));
}
// Return the addresses we fetched *without* updating state
if (shouldRecurse) {
return this.__fetchAddresses(n-1, i+1, cb, recursedAddrs.concat(addrs));
} else {
return cb(null, addrs);
}
})
// Make the request to get the requested address
const addrData = {
currency: 'ETH',
startPath: this._getHDPathIndices(this.hdPath, i),
n: shouldRecurse ? 1 : n,
};
const addrs = await this.sdkSession.getAddresses(addrData);
// Sanity check -- if this returned 0 addresses, handle the error
if (addrs.length < 1) {
throw new Error('No addresses returned');
}
// Return the addresses we fetched *without* updating state
if (shouldRecurse) {
return await this.__fetchAddresses(n-1, i+1, recursedAddrs.concat(addrs));
}
return addrs;
}
_signTxData(txData) {
return new Promise((resolve, reject) => {
this.sdkSession.sign({ currency: 'ETH', data: txData }, (err, res) => {
if (err) {
return reject(err);
}
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
// transaction but firmware is not updated to support it. We fallback to legacy.
res.type = txData.type;
if (txData.revertToLegacy) {
res.revertToLegacy = true;
res.gasPrice = txData.gasPrice;
}
// Return the signed tx
return resolve(res)
})
})
}
_getPage(increment=0) {
return new Promise((resolve, reject) => {
async _getPage(increment=0) {
try {
this.page += increment;

@@ -756,21 +707,30 @@ if (this.page < 0)

// Otherwise unlock the device and fetch more addresses
this.unlock()
.then(() => {
return this._fetchAddresses(PER_PAGE, start)
})
.then((addrs) => {
const accounts = []
addrs.forEach((address, i) => {
accounts.push({
address,
balance: null,
index: start + i,
})
})
return resolve(accounts)
})
.catch((err) => {
return reject(err);
})
})
await this.unlock()
const addrs = await this._fetchAddresses(PER_PAGE, start)
const accounts = addrs.map((address, i) => {
return {
address,
balance: null,
index: start + i,
};
});
return accounts;
} catch (err) {
// This will get hit for a few reasons. Here are two possibilities:
// 1. The user has a SafeCard inserted, but not unlocked
// 2. The user fetched a page for a different wallet, then switched
// interface on the device
// In either event we should try to resync the wallet and if that
// fails throw an error
try {
await this._connect();
const accounts = await this._getPage(0);
return accounts;
} catch (err) {
throw new Error(
'Failed to get accounts. Please forget the device and try again. ' +
'Make sure you do not have a locked SafeCard inserted.'
);
}
}
}

@@ -808,27 +768,3 @@

// Get the chainId for whatever object this is.
// Returns a hex string without the 0x prefix
_getEthereumJsChainId(tx) {
if (typeof tx.getChainId === 'function')
return tx.getChainId();
else if (tx.common && typeof tx.common.chainIdBN === 'function')
return tx.common.chainIdBN().toString(16);
else if (typeof tx.chainId === 'number')
return tx.chainId.toString(16);
else if (typeof tx.chainId === 'string')
return tx.chainId;
return '1';
}
_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) {

@@ -841,32 +777,63 @@ return null;

}
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;
return activeWallet.uid.toString('hex');
}
}
// Make sure we have an SDK connection and, therefore, an active wallet UID.
// If we do not, force an unlock, which adds ~5 seconds to the request.
_ensureCurrentWalletUID() {
return new Promise((resolve, reject) => {
if (!!this._getCurrentWalletUID()) {
return resolve();
}
this.unlock()
.then(() => {
if (!!this._getCurrentWalletUID()) {
return resolve()
} else {
return reject('Could not access Lattice wallet. Please re-connect.')
}
})
.catch((err) => {
return reject(err);
})
})
// Get the `v` component of the signature. This is calculating using
// secp256k1's ecdsa recover. The format of `v` returned depends
// on the type of transaction (restrictions come from the
// @ethereumjs/tx object).
// * Type 1 and 2 transactions both only require a [0,1] `v` value
// because EIP155 is no longer needed for those protocols (since
// chainId is a parameter in the request).
// See: https://github.com/ethereumjs/ethereumjs-monorepo/
// blob/7330c4ace495903748c606a47a8b993812504db8/packages/
// tx/src/eip1559Transaction.ts#L226
// * Type 0 transactions (legacy) require conversion to EIP155-style `v`
// See: https://github.com/ethereumjs/ethereumjs-monorepo/
// blob/v4.0.0/packages/tx/src/legacyTransaction.ts#L133
// * If the chain does not support EIP155, we simply add 27 to recovery.
// In practice I don't think we will ever see this type of tx.
function getV (tx, resp) {
const hash = tx.getMessageToSign(true);
const rs = new Uint8Array(Buffer.concat([ resp.sig.r, resp.sig.s ]))
const pubkey = new Uint8Array(resp.pubkey);
const recovery0 = secp256k1.ecdsaRecover(rs, 0, hash, false);
const recovery1 = secp256k1.ecdsaRecover(rs, 1, hash, false);
const pubkeyStr = Buffer.from(pubkey).toString('hex');
const recovery0Str = Buffer.from(recovery0).toString('hex');
const recovery1Str = Buffer.from(recovery1).toString('hex');
let recovery;
if (pubkeyStr === recovery0Str) {
recovery = 0;
} else if (pubkeyStr === recovery1Str) {
recovery = 1;
} else {
return null;
}
// Newer transaction types can include the recovery directly
if (tx._type) {
return recovery.toString(16)
}
// Check for EIP155 support. In practice, virtually every transaction
// should have EIP155 support since that hardfork happened in 2016...
const chainId = getTxChainId(tx);
if (
!tx.supports(EthTx.Capability.EIP155ReplayProtection) &&
(!tx.common || !tx.common.gteHardfork('spuriousDragon'))
) {
return new BN(recovery).addn(27).toString('hex');
}
// EIP155 replay protection is included in the `v` param
// and uses the chainId value.
return chainId.muln(2).addn(35).addn(recovery).toString('hex');
}
function getTxChainId (tx) {
if (tx && tx.common && typeof tx.common.chainIdBN === 'function') {
return tx.common.chainIdBN();
} else if (tx && tx.chainId) {
return new BN(tx.chainId);
}
return new BN(1);
}

@@ -873,0 +840,0 @@

{
"name": "eth-lattice-keyring",
"version": "0.5.0",
"version": "0.6.0",
"description": "Keyring for connecting to the Lattice1 hardware wallet",

@@ -20,8 +20,10 @@ "main": "index.js",

"dependencies": {
"@ethereumjs/common": "^2.4.0",
"@ethereumjs/tx": "^3.1.1",
"bignumber.js": "^9.0.1",
"@ethereumjs/common": "^2.6.2",
"@ethereumjs/tx": "^3.5.0",
"bn.js": "^5.2.0",
"ethereumjs-util": "^7.0.10",
"gridplus-sdk": "^1.0.0"
"gridplus-sdk": "^1.1.5",
"rlp": "^3.0.0",
"secp256k1": "4.0.2"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc