ethers.js icon indicating copy to clipboard operation
ethers.js copied to clipboard

Trezor and Ledger Nano S Hardware wallets

Open fjrojasgarcia opened this issue 7 years ago • 31 comments

I'm curious about the possibilities of those Hardware wallets integration with ethers.js. Can someone point me to the right documentation to implement them?

fjrojasgarcia avatar Feb 17 '18 06:02 fjrojasgarcia

You need to get the ledger nano s (or trezor) sdk and the device of course. Then you just hook in to the signing method. I build something similar for web3.

florianlenz avatar Feb 17 '18 12:02 florianlenz

Thanks a lot @florianlenz!

fjrojasgarcia avatar Feb 18 '18 05:02 fjrojasgarcia

I am curious what type of support you are looking for. I hadn’t thought of it, but maybe it makes sense to create a LedgerSigner, for example.

I am going to try getting a ledger Nano and see what type of support is possible.

@florianlenz can you send me a link to your Web3 thing?

ricmoo avatar Feb 19 '18 21:02 ricmoo

@ricmoo I think something like that would be very helpful for the developers. I come from Mastering Ethereum, by Andreas M. Antonopoulos and Gavin Wood, where I'm contribuing.

I am trying to promote in the book the disclosure of the best Ethereum JavaScript APIs currently available.

I see that many developers are unaware of ethers.js and I think it is not fair that in a book like Mastering Ethereum there is no room for an API of such high quality and level of finishing.

I was looking for values ​​in ethers.js that would allow me to enter it in the book as a true competitor of web3.js

I thought that perhaps an advantage would be that ether.js offered a certain level of integration with the APIs of the Hardware Wallets currently predominant.

I am fully aware that this would probably mean hard work for the developers of ethers.js, but every day more and more users enter the crypto space with the help of a hardware device that provides real security to their funds.

For me, the challenge would be to offer a unique integration interface for most existing hardware wallets, although I know that adds potential complexity to that task.

Personally I could not say to what extent my collaboration could be useful in this task, especially considering my Pythonist profile. Still today I keep discovering and marveling with nodejs.

In any case, I take this opportunity to congratulate the ether.js team. Personally I use it and I recommend it above all for its high degree of abstraction when connecting with an Ethereum node.

fjrojasgarcia avatar Feb 21 '18 05:02 fjrojasgarcia

btw... I have a Ledger S in the mail so I can start experimenting with this.

It will likely be a non-base library, along the lines of ethers-ens (for extended ENS functionality) and ethers-meow (for extended CryptoKitties functionality). An ethers-ledger, which will expose a LedgerSigner object that can be interchangeably used in place of any existing Signer for the ethers.js library.

This will also be included in https://ethers.io for hardware wallet support in our dApp Browser.

ricmoo avatar Mar 04 '18 00:03 ricmoo

Feel free to make use of our subproviders or rip any code from there if it helps. They primarily are to be plugged into web3-provider-engine but also expose public interfaces to sign.

Here's our ledger one in particular.

dekz avatar Apr 18 '18 10:04 dekz

As a quick note, here is the preliminary LedgerSigner using the v4 API of ethers. It needs some TLC for handling errors, but it works in both the browser and in nodejs, like any other Signer.

https://github.com/ethers-io/ethers-ledger

ricmoo avatar Jul 18 '18 19:07 ricmoo

Hey guys, can someone give me a hint how to integrate Trezor using Ethers. Thanks in advance.

kraikov avatar Nov 17 '18 17:11 kraikov

I haven’t written a trezor Signer for ethers yet. I should order one from Amazon so I can...

If you have one though, you can look at the ledger signer for some guidance.

ricmoo avatar Nov 17 '18 17:11 ricmoo

I haven’t written a trezor Signer for ethers yet. I should order one from Amazon so I can...

If you have one though, you can look at the ledger signer for some guidance.

Thanks, I will try to create the same for Trezor and I'll send that to you so you can take a look.

kraikov avatar Nov 17 '18 17:11 kraikov

I have implemented a preliminary Trezor signer using trezor-connect for one of my projects and would be happy to share it, as soon as it's a bit more mature.

At the moment I managed to sign transactions, but only with the help of the ethereumjs-tx package for serializing the resulting transaction (instead of ethers.utils.serializeTransaction - which is used for Ledger). I have to admit that I don't fully understand all the details yet, but my best guess is that it's connected to the calculation of the recoveryParameter (here) and EIP-155. It seems wrong when using a custom chain id. But that would be a more general bug and I'd be surprised if I were the first to trigger it (and transactions with regular wallets work fine on my test net). Any ideas @ricmoo ?

svenstucki avatar Nov 29 '18 01:11 svenstucki

@svenstucki: can you please share the function sendTransaction? I extended ethers.Signer and can implement 3 functions: provider, getAddress, signMessage but I can't figure out sendTransaction function (because Trezor API' signTransaction has very different parameters compares to the input transaction of ethers.js's sendTransaction

piavgh avatar Dec 03 '18 00:12 piavgh

Have you looked into the ledger signer?

https://github.com/ethers-io/ethers-ledger/blob/master/src.ts/index.ts#L67

ricmoo avatar Dec 03 '18 00:12 ricmoo

@ricmoo : Thanks, I checked it. But the input parameters for Ledger's signTransaction function is totally different from the input parameters of Trezor's signTransaction.

  1. Ledger: this._eth.signTransaction(this.path, unsignedTx)

unsignedTx here is a string

  1. Trezor: https://github.com/trezor/connect/blob/develop/docs/methods/signTransaction.md#example

While in this case the input is something different.

The problem is I don't know how to construct the input parameters for Trezor's signTransaction.

piavgh avatar Dec 03 '18 03:12 piavgh

You are looking at the btc docs. This is the right one for ethereum: https://github.com/trezor/connect/blob/develop/docs/methods/ethereumSignTransaction.md

alcuadrado avatar Dec 03 '18 03:12 alcuadrado

@alcuadrado : thanks, now I can implement it :+1:

But there is another problem that I encountered:

After getting the signature from function TrezorConnect.ethereumSignTransaction(...), I tried to use

utils.serializeTransaction(
    tx,
    sig
);

The sig value is:

{
    v: "0x1c", 
    r: "0x86b2ea859bb0fc086090e53a631cdfdcbf406a0ef042ea8f0bdf7c749cf821c5", 
    s: "0x3e52fabe81fbef8530a5b5d4bd2d0f797a9051387908096334afd5f3a556a180"
}

But now I got the error:

Uncaught (in promise) Error: invalid arrayify value (arg="value", value={}, type="object", version=4.0.15)

This is error of function utils.serializeTransaction

Do you know what's wrong with that?

piavgh avatar Dec 03 '18 04:12 piavgh

What is the value of tx? You may have to convert the 0x1c to 28? I can experiment with it once I’m back at my computer. :)

ricmoo avatar Dec 03 '18 04:12 ricmoo

@ricmoo : I changed 0x1c to 28 and now I don't have that error anymore.

But I have another one "Error: insufficient funds (version=4.0.15)"

I tried to send 100 ethers and I'm sure my test account have more than that.

Let me attach here the code of my TrezorSigner, if you have time please take a look:

import { Signer, providers, utils } from "ethers";
import TrezorConnect from "trezor-connect";

import { NETWORK_URL } from "../../../config/url";
import { addMethodsToSigner } from "./index";

const defaultDPath = "m/44'/60'/0'/0";

export class TrezorSigner extends Signer {
    constructor() {
        super();
        const networkId = 8888;
        this.provider = new providers.JsonRpcProvider(NETWORK_URL, {
            chainId: networkId,
            name: undefined
        });
        window.signer = { instance: this, type: "hardwareWallet" };
        addMethodsToSigner(this);
    }

    getPublicKey = (path = defaultDPath) => {
        return new Promise(async (resolve, reject) => {
            let result = await TrezorConnect.getPublicKey({
                path
            });
            if (result.success) {
                resolve(result.payload);
            } else {
                console.log(result);
                reject(result.payload.error);
            }
        });
    };

    getAddress = (path = defaultDPath) => {
        return new Promise(async (resolve, reject) => {
            let result = await TrezorConnect.getAddress({
                path,
                coin: "eth"
            });
            if (result.success) {
                resolve(result.payload.address);
            } else {
                console.log(result);
                reject(result.payload.error);
            }
        });
    };

    signMessage = async message => {
        return new Promise(async (resolve, reject) => {
            let result = await TrezorConnect.signMessage({
                path: defaultDPath,
                message
            });
            console.log(result);
            if (result.success) {
                resolve(result.payload.signature);
            } else {
                console.error("Error:", result.payload.error); // error message
                reject(result.payload.error);
            }
        });
    };

    sign = async transaction => {
        try {
            let tx = {
                to: transaction.to,
                gasLimit: utils.hexlify(transaction.gasLimit),
                gasPrice: utils.hexlify(transaction.gasPrice),
                value: utils.hexlify(transaction.value),
                nonce: transaction.nonce
                    ? utils.hexlify(transaction.nonce)
                    : utils.hexlify(0)
            };

            let result = await TrezorConnect.ethereumSignTransaction({
                path: defaultDPath,
                transaction: tx
            });

            if (result.success) {
                let sig = {
                    v: parseInt(result.payload.v, 10),
                    r: result.payload.r,
                    s: result.payload.s
                };

                let serializedTransaction = await utils.serializeTransaction(
                    tx,
                    sig
                );

                return serializedTransaction;
            }

            throw new Error(result.payload.error);
        } catch (err) {
            console.log(err);
        }
    };

    sendTransaction = async transaction => {
        let signedTx = await this.sign(transaction);
        return this.provider.sendTransaction(signedTx);
    };
}

Not sure if this part is correct:

this.provider = new providers.JsonRpcProvider(NETWORK_URL, {
            chainId: networkId,
            name: undefined
        });

piavgh avatar Dec 03 '18 07:12 piavgh

@piavgh This is the same issue I had. To debug this, you can decode the serialized transaction and then compare the result to tx and sig. In my case the from address and v parameter did not match my transaction. This is why you get the insufficient funds error.

Since the from address is most likely ecrecover'd from the signature, I looked for the parts of serializeTransaction that touch the signature. This led to the code I mentioned above.

I've attached my code below, it's quite dirty still - I didn't have the idea to use utils.hexlify. I've mainly used this for singing transactions to smart contracts, in my case this always required the populateTransaction step.

I'm quite sure that your code will work as well, if you use ethereumjs-tx to serialze the tx.


    public sign(transaction: ethers.providers.TransactionRequest): Promise<string> {
      if (!transaction.value) {
        transaction.value = '0x0';
      }

      let resolvedTx: any = {};

      // populate TX object with nonce, chainId, gasLimit, etc.
      return ethers.utils.populateTransaction(transaction, this.provider, this.getAddress())
      .then((tx: any) => {
        resolvedTx = tx;
        console.log('resolved tx is:', resolvedTx);

        // use strings only (handle BigNumber and number)
        if (typeof tx.value === 'object') tx.value = '0x' + tx.value.toString(16);
        if (typeof tx.gasPrice === 'object') tx.gasPrice = '0x' + tx.gasPrice.toString(16);
        if (typeof tx.gasLimit === 'object') tx.gasLimit = '0x' + tx.gasLimit.toString(16);
        if (typeof tx.nonce === 'number') tx.nonce = '0x' + tx.nonce.toString(16);

        return TrezorConnect.ethereumSignTransaction({
          path: this.options.path,
          transaction: tx,
        });
      })
      .then((result: any) => {
        console.log('signature is:', result);
        if (!result.success) {
          throw result.payload.error;
        }
        console.log('resolved tx is now:', resolvedTx);

        /*
        if (typeof result.payload.v === 'string' && result.payload.v.startsWith('0x')) {
          // remove leading 0x as the v parameter is handled differently by ethers
          result.payload.v = parseInt(result.payload.v.substring(2), 16);
        }

        return ethers.utils.serializeTransaction(resolvedTx, result.payload);
        */

        // TODO: extremely hacky - somehow TX serializer of ethers library returns invalid result
        const EthereumTx = require('ethereumjs-tx');
        const etx = new EthereumTx({ ...resolvedTx, ...result.payload });
        return etx.serialize();
      });
    }

svenstucki avatar Dec 03 '18 08:12 svenstucki

@svenstucki : Thank you. Can you please provide the whole class includes the constructor? I'm afraid that I'm using the wrong provider.

piavgh avatar Dec 03 '18 09:12 piavgh

@svenstucki : I used parseTransaction to decode the serialized transaction and the "from" parameter is very weird, I can't recognize it in any account that I used.

I used ethereumjs-tx and the result is the same.

piavgh avatar Dec 03 '18 09:12 piavgh

Hey @piavgh the provider is just a regular JsonRpcProvider from ethers.js that I pass in.

So are you saying you get the same wrong transaction from ethereumjs-tx? In that case it might make sense to do the populateTransaction first (although TrezorConnect is very stringent in what it accepts - if the fields were missing you'd get an error, but maybe the data type / format is different). I'm using the code I posted above and it works for me, the resulting TX has the right from address and is accepted by my private network.

svenstucki avatar Dec 03 '18 12:12 svenstucki

@svenstucki : I found the reason for this, created a new issue in trezor-connect repository: https://github.com/trezor/connect/issues/277

I'm using trezor-connect v6, not sure if v4 or v5 (that you used) has this problem.

Now I have to use path m/44'/60'/0'/0/0 to get the correct address of path m/44'/60'/0'/0

Even in their repository https://github.com/trezor/trezor-wallet, they have to use path m/44'/60'/0'/0/0 in order to get the correct address of path m/44'/60'/0'/0.

piavgh avatar Dec 04 '18 07:12 piavgh

As a side-note, I've just added node.js support for Ledger as the @ethersproject/hardware-wallets package. A little work is required to get browser support, but if anyone wants to try it out, let me know if it works for you.

ricmoo avatar Jan 10 '20 08:01 ricmoo

is there trezor support in @ethersproject/hardware-wallets on the way?

PipsqueakH avatar Mar 14 '21 14:03 PipsqueakH

As a side-note, I've just added node.js support for Ledger as the @ethersproject/hardware-wallets package. A little work is required to get browser support, but if anyone wants to try it out, let me know if it works for you.

I noticed that this package is lacking some tests, thought I might share this repo which can help writing tests using a ledger emulator. https://github.com/Zondax/zemu

asafsilman avatar Jun 28 '21 03:06 asafsilman

Conceptually, how does the Ledger signer in ethers work? Do I need to have it plugged into my local machine?

EvilJordan avatar Jul 23 '21 17:07 EvilJordan

Yes. It definitely must be connected. That is part of the value of a Hardware Wallet. :)

Right now there are some issues with the hardware wallet signer with its upstream dependencies that I’m working on resolving.

ricmoo avatar Jul 23 '21 20:07 ricmoo

Hi @ricmoo ! Is the ledge signer safe to use in the browser at the moment (upstream dependencies fixed?) Is there an experimental Trezor signer I can build upon to then PR?

pde-rent avatar May 07 '22 07:05 pde-rent

Can anyone point me to examples of using TrezorConnect with ether.js library that is somewhat recent? I am having trouble getting past the insufficient funds error with plenty of funds.

Adamj1232 avatar Sep 01 '22 03:09 Adamj1232