16
votes

Im trying to integrate web3js with Trezor in a truffle dev network or using ropsten test network.

The idea is to sign the transactions using the hardware wallet and then send a raw transaction using web3js

Im getting that we dont have balance to make the transaction, probably because web3js isnt taking one of the 10 truffle accounts and is using the trezor address that isnt in my local network..

On ropsten i have some ethers and i get "invalid address"

Is there a way to send a signed transactions (with trezor) using web3js into a truffle develop network? i mean, is there a way to include the trezor address into the truffle network?

The situation in truffle is explained more in details here, but the question could be generalized to "is there a way to include hardware wallets into truffle development network?" : https://github.com/trufflesuite/truffle/issues/973

Using ropsten I have managed to send a transaction and receive a transaction hash in the callback, but if we query for that transaction we get that the transaction doesnt exists.. so.. how is that possible?

I tried deploying a contract into Ropsten too and now im getting "Invalid address" when invoking a smart contract function. Maybe the signing function is wrong? anyone could integrate Trezor transaction signining with web3js?

Do you guys see anything wrong in the signing and sending process that we have followed? Maybe is there something wrong on the R, V and S parameters handling ..

Another important thing is that i am using https://github.com/ethereumjs/ethereumjs-tx for creating the raw transactions

Issues published in web3js, truffle and trezzor connect with more information:

kind regards

 trezorLogin = async()=> {
        let trezor=  await this.getTrezor();

        // site icon, optional. at least 48x48px
        let hosticon = 'https://doc.satoshilabs.com/trezor-apps/_images/copay_logo.png';
        // server-side generated and randomized challenges
        let challenge_hidden = '';
        let challenge_visual = '';
        //use anonimous functions on callback otherwise returns cross origin errors
        trezor.requestLogin(hosticon, challenge_hidden, challenge_visual, function (result){
            if (result.success) {
                console.log('Public key:', result.public_key); // pubkey in hex
                console.log('Signature:', result.signature); // signature in hex
                console.log('Version 2:', result.version === 2); // version field
                console.log(result);
            }else {
                console.error('Error:', result.error);
            }
        });}


    trezorSignTx= async(transaction)=> {
        let trezor=  await this.getTrezor();
        // spend one change output
        let address_n = "m/44'/60'/0'/0/0"
        // let address_n = [44 | 0x80000000,
        //                  60 | 0x80000000,
        //                  0  | 0x80000000 ,
        //                  0 ]; // same, in raw form
        let nonce = transaction.nonce.substring(2); // note - it is hex, not number!!!
        let gas_price = transaction.gasPrice.substring(2);
        let gas_limit = transaction.gasLimit.substring(2);
        let to = transaction.to.substring(2);
        // let value = '01'; // in hexadecimal, in wei - this is 1 wei
        let value = transaction.value.substring(2); // in hexadecimal, in wei - this is about 18 ETC
        let data = transaction.data.substring(2); // some contract data
        // let data = null  // for no data
        let chain_id = 5777; // 1 for ETH, 61 for ETC
        return new Promise (function (resolve,reject) {
            trezor.ethereumSignTx(
                address_n,
                nonce,
                gas_price,
                gas_limit,
                to,
                value,
                data,
                chain_id,
                function (response) {
                    if (response.success) {

                        console.log('Signature V (recovery parameter):', response.v); // number
                        console.log('Signature R component:', response.r); // bytes
                        console.log('Signature S component:', response.s); // bytes
                        resolve(response);

                    } else {
                        console.error('Error:', response.error); // error message
                        resolve(null);
                    }

                });
        })
    }

    getTrezorAddress = async() => {
        let trezor=  await this.getTrezor();
        // spend one change output
        let address_n = "m/44'/60'/0'/0/0";
        trezor.ethereumGetAddress(address_n, function (result) {
            if (result.success) { // success
                console.log('Address: ', result.address);
            } else {
                console.error('Error:', result.error); // error message
            }
        });
    }


    getTrezor = async() => {
        let trezorC;
        await getTrezorConnect
            .then(trezorConnect => {
                trezorC= trezorConnect;
            })
            .catch((error) => {
                console.log(error)
            })
        return trezorC;

    }

 sendTransaction= async(address, amount, id)=>{
        let tokenInstance = this.props.smartContractInstance;

        let getData = tokenInstance.mint.getData(address, amount);

        let tx = {
            nonce: '0x00',
            gasPrice: '0x09184e72a000',
            gasLimit: '0x2710',
            to: CONTRACT_ADDRESS,
            value: '0x00',
            from:CONTRACT_OWNER_ADDRESS,
            data: getData
        };
        let response = await this.trezorSignTx(tx);

        let web3;
        let _this = this;
        if (response!=null){
            getWeb3
                .then(results => {
                    web3= results.web3;
                    let v = response.v.toString();
                    if (v.length % 2 != 0){
                        v="0"+v;
                    }
                    tx.r=Buffer.from(response.r,'hex');
                    tx.v=Buffer.from(v,'hex');
                    tx.s=Buffer.from(response.s,'hex');
                    let ethtx = new ethereumjs(tx);
                    console.dir(ethtx.getSenderAddress().toString('hex'), );
                    const serializedTx = ethtx.serialize();
                    const rawTx = '0x' + serializedTx.toString('hex');
                    console.log(rawTx);
                    //finally pass this data parameter to send Transaction
                    web3.eth.sendRawTransaction(rawTx, function (error, result) {
                        if(!error){
                            _this.props.addTokens(id)
                                .then(()=>{
                                        _this.setState({modalOpen: true});
                                        _this.props.getAllTransactions();
                                    }
                                );
                        }else{
                            alert(error)
                        }
                    });
                })
                .catch((error) => {
                    console.log(error)
                })
        }else{
            alert("There was an error signing with trezor hardware wallet")
        }


    }

The getTrezorConnect function is just get window.trezorConnect asynchronously because the object is injected as script

<script src="https://connect.trezor.io/4/connect.js"></script>


let getTrezorConnect = new Promise(function(resolve, reject) {
    // Wait for loading completion
    window.addEventListener('load', function() {

        let trezorConnect = window.TrezorConnect

            return resolve(trezorConnect)


})});

export default getTrezorConnect
2

2 Answers

2
votes

Well, after a lot of trying we have managed to send a raw transaction signed with Trezor to Ropsten, Truffle (see the edit on the bottom of the answer) and also to a local private Geth network, so, the code is ok and there is no problem with Trezor integration on those environments

https://ropsten.etherscan.io/address/0x89e2c46b22881f747797cf67310aad1a831d50b7

This are the things that i had changed in order to make it possible to send signed transactions to the Ropsten testnet.

This assumes that you have your contract deployed into Ropsten and you have the contract address.

1) Get the address of your Trezor account

  getTrezorAddress = async() => {
        let trezor=  await this.getTrezor();
        // spend one change output
        let address_n = "m/44'/1'/0'/0/0";
        trezor.ethereumGetAddress(address_n, function (result) {
            if (result.success) { // success
                console.log('Address: ', result.address);
            } else {
                console.error('Error:', result.error); // error message
            }
        });
    }

2) Put the trezor address into the from field of your raw transaction, get the nonce of the transaction by getting the transaction count for that address. Important: use the "pending" optional parameter on getTransactionCount to get all the transactions of the account, otherwise you will be overriting pending transactions.

getNonce = async(address) => {

        let web3 = await this.getWeb3();
        return new Promise (function (resolve,reject) {
            web3.eth.getTransactionCount(address, "pending", function (error,result){
                console.log("Nonce "+result);
                resolve(result);


            });
        });

    }

let count = null;
        await this.getNonce("0xedff546ac229317df81ef9e6cb3b67c0e6425fa7").then(result => {
            if(result.length % 2 !==0){
                result = "0"+result;
            }
            count = "0x"+result;

       });

let tx = {
            nonce: count ,
            gasPrice: web3.toHex(gasPriceGwei*1e9),
            gasLimit: web3.toHex(gasLimit),
            to: CONTRACT_ADDRESS,
            value: '0x00',
            data: getData,
            chainId:chainId,
            from:"yourTrezzorAddress"
        };

3) The r, s, v parameters were incorrect, the right way to handle them is take that values for the trezor response and just convert it to hexa:

// response is the Trezor sign response
tx.v= response.v;
tx.r="0x"+response.r;
tx.s="0x"+response.s;
let ethtx = new ethereumjs(tx);.
const serializedTx = ethtx.serialize();
const rawTx = '0x' + serializedTx.toString('hex');
 //finally pass this data parameter to send Transaction
web3.eth.sendRawTransaction(rawTx, someCallbackFunction);

Important: the mining time in ropsten will be between 15 and 30 secs so if in your someCallbackFunction you check for the transaction receipt, using the hash, you will get null as result, because the transaction is in a pending state.

4) To test it at ropsten we use Infura, so we change the web3 provider:

import Web3 from 'web3'
import HDWalletProvider from "truffle-hdwallet-provider";

let getWeb3 = new Promise(function(resolve, reject) {
    // Wait for loading completion to avoid race conditions with web3 injection timing.
    window.addEventListener('load', function() {
        let results
        let web3 = window.web3

        // Checking if Web3 has been injected by the browser (Mist/MetaMask)
        if (typeof web3 !== 'undefined') {
            // Use Mist/MetaMask's provider.
            web3 = new Web3(web3.currentProvider)

            results = {
                web3: web3
            }

            console.log('Injected web3 detected.');

            return resolve(results)
        } else {
            // Fallback to localhost if no web3 injection. We've configured this to
            // use the development console's port by default.
            // let provider = new Web3.providers.HttpProvider("https://ropsten.infura.io/your_infura_api_key")

            let mnemonic = "infura mnemonic"
            let provider = new HDWalletProvider(mnemonic, "https://ropsten.infura.io/your_infura_api_key")
            web3 = new Web3(provider)

            results = {
                web3: web3
            }

            console.log('No web3 instance injected, using Local web3.');

            return resolve(results)
        }
    })
})

export default getWeb3

EDIT:

This also works on Truffle! check the last comments of this issue https://github.com/trufflesuite/truffle/issues/973

0
votes

We developed a small library to use Truffle with Trezor hardware wallets: https://github.com/rariblecom/trezor-provider

It can be used like this:

const { createProvider } = require('@rarible/trezor-provider')

module.exports = {
    networks: {
        ropsten: {
            provider: function() {
                //websocket and http urls are supported
                return createProvider({ url: "{infura or other}", path: "m/44'/60'/0'/0/0", chainId: 3 }) 
            },
            network_id: 3
        }
    }
};