Использование Trezor (аппаратного кошелька) с Web3js на Truffle или Ropsten

Мы пытаемся интегрировать web3js с Trezor в сеть разработки truffle или с помощью тестовой сети ropsten .

Идея состоит в том, чтобы подписывать транзакции с помощью аппаратного кошелька, а затем отправлять необработанные транзакции с помощью web3js.

Мы получаем, что у нас нет баланса для совершения транзакции, вероятно, потому, что web3js не использует одну из 10 учетных записей трюфелей и использует адрес трезора, которого нет в моей локальной сети .

На ropsten у меня есть несколько эфиров, и я получаю «неверный адрес»

Есть ли способ отправить подписанные транзакции (с помощью trezor) с помощью web3js в сеть разработки трюфелей? я имею в виду, есть ли способ включить адрес trezor в сеть truffle?

Ситуация с трюфелем более подробно описана здесь, но вопрос можно обобщить так: « Есть ли способ включить аппаратные кошельки в сеть разработки трюфелей? »: https://github.com/trufflesuite/truffle/issues/973 .

Используя ropsten, нам удалось отправить транзакцию и получить хэш транзакции в обратном вызове, но если мы запросим эту транзакцию, мы получим, что транзакция не существует... так что... как это возможно?

Мы также пытались развернуть контракт в Ropsten, и теперь мы получаем «Неверный адрес» при вызове функции смарт-контракта. Может быть, функция подписи неверна? кто-нибудь может интегрировать подписание транзакций Trezor с web3js?

Ребята, вы видите что-то неправильное в процессе подписания и отправки, которому мы следовали? Возможно, что-то не так с обработкой параметров R, V и S.

Еще одна важная вещь заключается в том, что мы используем https://github.com/ethereumjs/ethereumjs-tx для создания необработанных транзакций.

Проблемы, опубликованные в web3js, truffle и trezzor, связаны с дополнительной информацией:

с уважением

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


    }

Функция getTrezorConnect просто получает window.trezorConnect асинхронно, потому что объект вводится как скрипт

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

Ответы (2)

У вас много вопросов в списке. Лучше публиковать по одному вопросу за раз, чтобы увеличить ваши шансы получить на них ответ.

Позвольте мне коснуться самых важных из них .

Q1. Есть ли способ включить аппаратные кошельки в сеть разработки трюфелей?

Да, используя truffle consoleи настроив его для подключения к testrpc. С testrpc, вы можете иметь любую учетную запись, которую вы хотите финансировать (отредактируйте: это неправда - учетные записи на самом деле являются закрытыми ключами, которые недоступны при использовании кошелька HW), запустив ее следующим образом:

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

Где адреса — это ваши трезор-адреса.

Или вы можете запустить его с помощью сида вашего трезора (я не рекомендую этого, если вы точно не знаете, что трезор используется в живой сети):

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

Q2: Ребята, вы видите что-то неправильное в процессе подписания и отправки, которому мы следовали?

Мне удалось программно подписать транзакции eth с помощью аппаратного кошелька Ledger Nano S. Библиотека ledgerco js, ​​которую я использовал для подписи транзакций, также возвращает параметры V, R и S. Я предполагаю, что они в том же формате, что и файлы, возвращаемые библиотекой Трезора, но я не могу быть в этом уверен. Во всяком случае, вот как я использую V, R, S для создания действительных транзакций:

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');

Вот и все.

Как вы можете запустить testrpc с учетной записью Trezzor, если закрытый ключ Trezzor недоступен? Кроме того, мнемоника состоит из 24 слов.. так что я застрял там.. когда я пробую ваш код против ropsten, я получаю хэш транзакции, как будто транзакция была отправлена, но затем, если я зарегистрируюсь в ropsten.etherscan для этих транзакций контракта, я вижу только конструкцию сделки, есть идеи по этому поводу? Спасибо
Ну, проблема была связана с тем, как мы управляли значениями r, s, v, не было необходимости использовать буфер. Я собираюсь принять этот ответ как правильный.
testrpc не хочет, чтобы закрытый ключ мог кредитовать адрес в эфирах. Вы только говорите ему, какой адрес кредитовать сколько Wei. Неважно, есть ли у вас закрытый ключ или нет.
Для параметра --account требуются закрытые ключи и Wei. Если я использую адрес устройства Trezor, testrpc сообщает мне «RangeError: длина закрытого ключа недействительна».
действительно, вы правы, ему нужны закрытые ключи. Похоже, ваше единственное решение — использовать мнемонику Trezor при запуске testrpc.
Да, единственное, чего я не понимаю, это то, что когда я запускаю testrpc с мнемоникой Trezor, я получаю 10 учетных записей, и любой из адресов совпадает с адресом моего устройства Trezor.

Что ж, после долгих попыток нам удалось отправить необработанную транзакцию, подписанную с помощью Trezor, в Ropsten, основываясь на помощи Tudor Constantin:

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

Это то, что я изменил, чтобы сделать возможной отправку подписанных транзакций в тестовую сеть Ropsten.

Это предполагает, что ваш контракт развернут в Ropsten и у вас есть адрес контракта.

1) Получите адрес вашей учетной записи 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) Поместите адрес trezor в fromполе вашей необработанной транзакции, получите nonceтранзакцию, получив количество транзакций для этого адреса. Важно: используйте необязательный параметр «ожидание» в getTransactionCount, чтобы получить все транзакции учетной записи, иначе вы будете перезаписывать ожидающие транзакции.

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) Параметры r, s, v были неверными, правильный способ справиться с ними — взять эти значения для ответа трезора и просто преобразовать их в шестнадцатеричный формат:

// 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);

Важно: время майнинга в ropsten будет от 15 до 30 секунд, поэтому, если в someCallbackFunction вы проверите получение транзакции, используя хэш, вы получите результат null, потому что транзакция находится в состоянии ожидания.

4) Для проверки на ropsten используем Infura, поэтому меняем провайдера 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

РЕДАКТИРОВАТЬ :

Это также работает на Truffle ! проверьте последние комментарии к этой проблеме https://github.com/trufflesuite/truffle/issues/973