Uso de Trezor (cartera de hardware) con Web3js en Truffle o Ropsten

Estamos tratando de integrar web3js con Trezor en una red de desarrollo de trufas o usando la red de prueba de ropsten .

La idea es firmar las transacciones usando la billetera de hardware y luego enviar una transacción sin procesar usando web3js

Estamos recibiendo que no tenemos saldo para realizar la transacción, probablemente porque web3js no está tomando una de las 10 cuentas de trufas y está usando la dirección trezor que no está en mi red local .

En ropsten tengo algunos éteres y obtengo "dirección no válida"

¿Hay alguna forma de enviar transacciones firmadas (con trezor) usando web3js a una red de desarrollo de trufas? Quiero decir, ¿hay alguna forma de incluir la dirección de Trezor en la red de trufas?

La situación en truffle se explica con más detalle aquí, pero la pregunta podría generalizarse a " ¿hay alguna manera de incluir carteras de hardware en la red de desarrollo de truffle? ": https://github.com/trufflesuite/truffle/issues/973

Usando ropsten, hemos logrado enviar una transacción y recibir un hash de transacción en la devolución de llamada, pero si consultamos esa transacción, obtenemos que la transacción no existe ... entonces ... ¿cómo es eso posible?

También intentamos implementar un contrato en Ropsten y ahora obtenemos "Dirección no válida" al invocar una función de contrato inteligente. ¿Quizás la función de firma está mal? ¿Alguien podría integrar la firma de transacciones de Trezor con web3js?

¿Ustedes ven algo malo en el proceso de firma y envío que hemos seguido? Tal vez hay algo mal en el manejo de los parámetros R, V y S...

Otra cosa importante es que estamos usando https://github.com/ethereumjs/ethereumjs-tx para crear transacciones sin procesar .

Números publicados en web3js, truffle y trezzor conectan con más información:

Saludos cordiales

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

        // site icon, optional. at least 48x48px
        var hosticon = 'https://doc.satoshilabs.com/trezor-apps/_images/copay_logo.png';
        // server-side generated and randomized challenges
        var challenge_hidden = '';
        var 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
        var address_n = "m/44'/60'/0'/0/0"
        // var address_n = [44 | 0x80000000,
        //                  60 | 0x80000000,
        //                  0  | 0x80000000 ,
        //                  0 ]; // same, in raw form
        var nonce = transaction.nonce.substring(2); // note - it is hex, not number!!!
        var gas_price = transaction.gasPrice.substring(2);
        var gas_limit = transaction.gasLimit.substring(2);
        var to = transaction.to.substring(2);
        // var value = '01'; // in hexadecimal, in wei - this is 1 wei
        var value = transaction.value.substring(2); // in hexadecimal, in wei - this is about 18 ETC
        var data = transaction.data.substring(2); // some contract data
        // var data = null  // for no data
        var 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
        var 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;

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

        var 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")
        }


    }

La función getTrezorConnect solo obtiene window.trezorConnect de forma asincrónica porque el objeto se inyecta como secuencia de comandos

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

Respuestas (2)

Tienes muchas preguntas en la lista. Es mejor publicar una pregunta a la vez, para aumentar sus posibilidades de que las respondan.

Permítanme abordar los más importantes .

Q1. ¿Hay alguna manera de incluir billeteras de hardware en la red de desarrollo de trufas?

Sí, al usarlo truffle consoley configurarlo para conectarse a testrpc. Con testrpc, puede tener cualquier cuenta que desee financiar (editar: esto no es cierto, las cuentas son en realidad claves privadas, que no están disponibles usando una billetera HW), al iniciarlo como:

testrpc --account="0x8414315fe005b8f294020dfc61cfd13749fbc045b0c6abc31fbd1ee3f4ff3b41, 10000000000000000000"         --account="0x566a9022cd3f0dfcc3dff657a6c578897d4b0300e335fa569a082b637e6bb273, 70000000000000000000"         --account="0x90b4e47ca43b66fab5dbebfee464087b51923f73f649701ca485da313574fd5b, 80000000000000000000"         --account="0x5d47b245c405d706fecbc5eb213819d20a2168ad696b352644ad0ffc87aef18e, 90000000000000000000"

Donde las direcciones son sus direcciones trezor.

O puede comenzar con la semilla de su trezor (no lo recomiendo, a menos que esté seguro de que el trezor se usa en la red en vivo):

testrpc -m 'candy maple cake sugar pudding cream honey rich smooth crumble sweet treat'

P2: ¿Ustedes ven algo malo en el proceso de firma y envío que hemos seguido?

Me las arreglé para firmar transacciones eth mediante programación usando la billetera de hardware Ledger Nano S. La biblioteca ledgerco js que utilicé para firmar las transacciones también devuelve los parámetros V, R y S. Supongo que están en el mismo formato que los devueltos por la biblioteca de Trezor, pero no puedo estar seguro de eso. De todos modos, así es como uso V, R, S para crear transacciones válidas:

console.log('Please sign transaction on device...');
  //the signature is an object with keys v,r and s
  const signature = await eth_utils.ledger.signTransaction_async(argv['derivation_path'], tx.serialize().toString('hex'));

  //"hexify" the keys
  Object.keys(signature).map( (key, index) => {
    signature[key] = '0x'+signature[key];
  });
  //tx_raw is a js object that contains all the tx params, the one that was signed on the hw device
  //(equivalent of your tx from your sendTransaction() function)
  const tx_obj = { ...tx_raw, ...signature};

  //re-create the Transaction using ethereumjs-tx
  const signed_tx = new Transaction( tx_obj );

  //signed_tx_hex needs to be broadcasted
  const signed_tx_hex = '0x'+signed_tx.serialize().toString('hex');

Y eso es.

¿Cómo puede iniciar testrpc con la cuenta de Trezzor cuando la clave privada de Trezzor no está disponible? también el mnemotécnico tiene una longitud de 24 palabras ... así que estoy atrapado allí ... cuando pruebo su código contra ropsten, obtengo un hash de transacción como si se enviara la transacción, pero luego si reviso ropsten.etherscan para ese contrato transacciones solo veo la construcción de la transacción, alguna idea al respecto? Gracias
Bueno, el problema estaba relacionado con como manejamos los valores r,s,v, no era necesario usar el Buffer. Voy a aceptar esta respuesta como correcta.
testrpc no quiere que la clave privada pueda acreditar una dirección con ethers. Solo le dices qué dirección acreditar con cuántos Wei. No importa si tienes la clave privada o no.
La opción --account requiere las claves privadas y Wei. Si uso la dirección del dispositivo Trezor, testrpc me dice "RangeError: la longitud de la clave privada no es válida"
de hecho, tienes razón, necesita las claves privadas. Parece que su única solución es usar el mnemotécnico de Trezor cuando inicia testrpc
Sí, lo único que no entiendo es que cuando ejecuto testrpc con el mnemotécnico de Trezor obtengo 10 cuentas y cualquiera de las direcciones coincide con la dirección de mi dispositivo Trezor.

Bueno, después de mucho intentarlo, hemos logrado enviar una transacción sin formato firmada con Trezor a Ropsten, basada en la ayuda de Tudor Constantin:

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

Estas son las cosas que había cambiado para que fuera posible enviar transacciones firmadas a la red de prueba de Ropsten.

Esto supone que tiene su contrato implementado en Ropsten y tiene la dirección del contrato.

1) Obtenga la dirección de su cuenta de Trezor

  getTrezorAddress = async() => {
        let trezor=  await this.getTrezor();
        // spend one change output
        var 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) Ponga la dirección de trezor en el fromcampo de su transacción sin procesar, obtenga el noncenúmero de transacción obteniendo el recuento de transacciones para esa dirección. Importante: use el parámetro opcional "pendiente" en getTransactionCount para obtener todas las transacciones de la cuenta; de lo contrario, sobrescribirá las transacciones pendientes.

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;

       });

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

3) Los parámetros r, s, v eran incorrectos, la forma correcta de manejarlos es tomar esos valores para la respuesta trezor y simplemente convertirlos a 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);

Importante: el tiempo de minería en ropsten será de entre 15 y 30 segundos, por lo que si en su someCallbackFunction verifica el recibo de la transacción, utilizando el hash, obtendrá un resultado nulo, porque la transacción está en un estado pendiente.

4) Para probarlo en ropsten usamos Infura, por lo que cambiamos de proveedor web3:

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() {
        var results
        var 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.
            // var provider = new Web3.providers.HttpProvider("https://ropsten.infura.io/your_infura_api_key")

            var mnemonic = "infura mnemonic"
            var 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

EDITAR :

¡ Esto también funciona en Trufa ! consulte los últimos comentarios de este número https://github.com/trufflesuite/truffle/issues/973