aboutsummaryrefslogtreecommitdiffstats
path: root/packages/subproviders/src/subproviders/ledger.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/subproviders/src/subproviders/ledger.ts')
-rw-r--r--packages/subproviders/src/subproviders/ledger.ts198
1 files changed, 129 insertions, 69 deletions
diff --git a/packages/subproviders/src/subproviders/ledger.ts b/packages/subproviders/src/subproviders/ledger.ts
index b67b49bee..8bc70d8b6 100644
--- a/packages/subproviders/src/subproviders/ledger.ts
+++ b/packages/subproviders/src/subproviders/ledger.ts
@@ -8,6 +8,7 @@ import { Lock } from 'semaphore-async-await';
import * as Web3 from 'web3';
import {
+ Callback,
LedgerEthereumClient,
LedgerEthereumClientFactoryAsync,
LedgerSubproviderConfigs,
@@ -23,6 +24,11 @@ const DEFAULT_NUM_ADDRESSES_TO_FETCH = 10;
const ASK_FOR_ON_DEVICE_CONFIRMATION = false;
const SHOULD_GET_CHAIN_CODE = true;
+/**
+ * Subprovider for interfacing with a user's [Ledger Nano S](https://www.ledgerwallet.com/products/ledger-nano-s).
+ * This subprovider intercepts all account related RPC requests (e.g message/transaction signing, etc...) and
+ * re-routes them to a Ledger device plugged into the users computer.
+ */
export class LedgerSubprovider extends Subprovider {
private _nonceLock = new Lock();
private _connectionLock = new Lock();
@@ -37,6 +43,12 @@ export class LedgerSubprovider extends Subprovider {
throw new Error(LedgerSubproviderErrors.SenderInvalidOrNotSupplied);
}
}
+ /**
+ * Instantiates a LedgerSubprovider. Defaults to derivationPath set to `44'/60'/0'`.
+ * TestRPC/Ganache defaults to `m/44'/60'/0'/0`, so set this in the configs if desired.
+ * @param config Several available configurations
+ * @return LedgerSubprovider instance
+ */
constructor(config: LedgerSubproviderConfigs) {
super();
this._networkId = config.networkId;
@@ -49,84 +61,38 @@ export class LedgerSubprovider extends Subprovider {
: ASK_FOR_ON_DEVICE_CONFIRMATION;
this._derivationPathIndex = 0;
}
+ /**
+ * Retrieve the set derivation path
+ * @returns derivation path
+ */
public getPath(): string {
return this._derivationPath;
}
+ /**
+ * Set a desired derivation path when computing the available user addresses
+ * @param derivationPath The desired derivation path (e.g `44'/60'/0'`)
+ */
public setPath(derivationPath: string) {
this._derivationPath = derivationPath;
}
+ /**
+ * Set the final derivation path index. If a user wishes to sign a message with the
+ * 6th address in a derivation path, before calling `signPersonalMessageAsync`, you must
+ * call this method with pathIndex `6`.
+ * @param pathIndex Desired derivation path index
+ */
public setPathIndex(pathIndex: number) {
this._derivationPathIndex = pathIndex;
}
- // Required to implement this public interface which doesn't conform to our linting rule.
- // tslint:disable-next-line:async-suffix
- public async handleRequest(
- payload: Web3.JSONRPCRequestPayload,
- next: () => void,
- end: (err: Error | null, result?: any) => void,
- ) {
- let accounts;
- let txParams;
- switch (payload.method) {
- case 'eth_coinbase':
- try {
- accounts = await this.getAccountsAsync();
- end(null, accounts[0]);
- } catch (err) {
- end(err);
- }
- return;
-
- case 'eth_accounts':
- try {
- accounts = await this.getAccountsAsync();
- end(null, accounts);
- } catch (err) {
- end(err);
- }
- return;
-
- case 'eth_sendTransaction':
- txParams = payload.params[0];
- try {
- LedgerSubprovider._validateSender(txParams.from);
- const result = await this._sendTransactionAsync(txParams);
- end(null, result);
- } catch (err) {
- end(err);
- }
- return;
-
- case 'eth_signTransaction':
- txParams = payload.params[0];
- try {
- const result = await this._signTransactionWithoutSendingAsync(txParams);
- end(null, result);
- } catch (err) {
- end(err);
- }
- return;
-
- case 'eth_sign':
- case 'personal_sign':
- const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
- try {
- if (_.isUndefined(data)) {
- throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
- }
- assert.isHexString('data', data);
- const ecSignatureHex = await this.signPersonalMessageAsync(data);
- end(null, ecSignatureHex);
- } catch (err) {
- end(err);
- }
- return;
-
- default:
- next();
- return;
- }
- }
+ /**
+ * Retrieve a users Ledger accounts. The accounts are derived from the derivationPath,
+ * master public key and chainCode. Because of this, you can request as many accounts
+ * as you wish and it only requires a single request to the Ledger device. This method
+ * is automatically called when issuing a `eth_accounts` JSON RPC request via your providerEngine
+ * instance.
+ * @param numberOfAccounts Number of accounts to retrieve (default: 10)
+ * @return An array of accounts
+ */
public async getAccountsAsync(numberOfAccounts: number = DEFAULT_NUM_ADDRESSES_TO_FETCH): Promise<string[]> {
this._ledgerClientIfExists = await this._createLedgerClientAsync();
@@ -158,6 +124,14 @@ export class LedgerSubprovider extends Subprovider {
}
return accounts;
}
+ /**
+ * Sign a transaction with the Ledger. If you've added the LedgerSubprovider to your
+ * app's provider, you can simply send an `eth_sendTransaction` JSON RPC request, and
+ * this method will be called auto-magically. If you are not using this via a ProviderEngine
+ * instance, you can call it directly.
+ * @param txParams Parameters of the transaction to sign
+ * @return Signed transaction hex string
+ */
public async signTransactionAsync(txParams: PartialTxParams): Promise<string> {
this._ledgerClientIfExists = await this._createLedgerClientAsync();
@@ -193,6 +167,16 @@ export class LedgerSubprovider extends Subprovider {
throw err;
}
}
+ /**
+ * Sign a personal Ethereum signed message. The signing address will be to one
+ * retrieved given a derivationPath and pathIndex set on the subprovider.
+ * The Ledger adds the Ethereum signed message prefix on-device. If you've added
+ * the LedgerSubprovider to your app's provider, you can simply send an `eth_sign`
+ * or `personal_sign` JSON RPC request, and this method will be called auto-magically.
+ * If you are not using this via a ProviderEngine instance, you can call it directly.
+ * @param data Message to sign
+ * @return Signature hex string (order: rsv)
+ */
public async signPersonalMessageAsync(data: string): Promise<string> {
this._ledgerClientIfExists = await this._createLedgerClientAsync();
try {
@@ -214,6 +198,82 @@ export class LedgerSubprovider extends Subprovider {
throw err;
}
}
+ /**
+ * This method conforms to the web3-provider-engine interface.
+ * It is called internally by the ProviderEngine when it is this subproviders
+ * turn to handle a JSON RPC request.
+ * @param payload JSON RPC payload
+ * @param next Callback to call if this subprovider decides not to handle the request
+ * @param end Callback to call if subprovider handled the request and wants to pass back the request.
+ */
+ // tslint:disable-next-line:async-suffix
+ public async handleRequest(
+ payload: Web3.JSONRPCRequestPayload,
+ next: Callback,
+ end: (err: Error | null, result?: any) => void,
+ ) {
+ let accounts;
+ let txParams;
+ switch (payload.method) {
+ case 'eth_coinbase':
+ try {
+ accounts = await this.getAccountsAsync();
+ end(null, accounts[0]);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_accounts':
+ try {
+ accounts = await this.getAccountsAsync();
+ end(null, accounts);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_sendTransaction':
+ txParams = payload.params[0];
+ try {
+ LedgerSubprovider._validateSender(txParams.from);
+ const result = await this._sendTransactionAsync(txParams);
+ end(null, result);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_signTransaction':
+ txParams = payload.params[0];
+ try {
+ const result = await this._signTransactionWithoutSendingAsync(txParams);
+ end(null, result);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ case 'eth_sign':
+ case 'personal_sign':
+ const data = payload.method === 'eth_sign' ? payload.params[1] : payload.params[0];
+ try {
+ if (_.isUndefined(data)) {
+ throw new Error(LedgerSubproviderErrors.DataMissingForSignPersonalMessage);
+ }
+ assert.isHexString('data', data);
+ const ecSignatureHex = await this.signPersonalMessageAsync(data);
+ end(null, ecSignatureHex);
+ } catch (err) {
+ end(err);
+ }
+ return;
+
+ default:
+ next();
+ return;
+ }
+ }
private _getDerivationPath() {
const derivationPath = `${this.getPath()}/${this._derivationPathIndex}`;
return derivationPath;