Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 77 additions & 65 deletions trezor-wallet-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ const Web3 = require("web3");
const HookedWalletSubprovider = require("@trufflesuite/web3-provider-engine/subproviders/hooked-wallet.js");
// there is a bug in web3's BN that is not calculating hex correctly, so we use this instead
const numberToBN = require("number-to-bn");
const tmp = require("tmp");

const { toChecksumAddress } = Web3.utils;
const fs = require("fs");

const ETHEREUM_PATH = "m/44'/60'/0'/0";
const fs = require("fs").promises;
const os = require("os");
const path = require("path");

function trezorCtl(args) {
let stdout = execSync(`trezorctl ${args}`, {
Expand Down Expand Up @@ -64,6 +64,41 @@ class Trezor {
return this.cachedAccounts;
}

async signTypedMessage({ from, data }, cb) {
from = toChecksumAddress(from);

const defaultDerivationPath = this.validateFromAndGetDerivationPath(from);
const path = this.opts.derivationPath || defaultDerivationPath;

let response;

try {
await withTempFile(async (file) => {
await fs.writeFile(file, data);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is received

The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object

data filed here is of the form object. It can be converted via JSON.stringify(data)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably we want to add a type guard first before we stringify it.maybe lodash's isPlainObject https://lodash.com/docs/4.17.15#isPlainObject

response = trezorCtl(
`ethereum sign-typed-data --address "${path}" ${file}`
);
const match = response.match(/signature: (0x[0-9A-Fa-f]+)/);
if (!match || !match[1]) {
cb(`Could not find signature in trezor response: ${response}`);
}
cb(null, match[1]);
});
} catch (e) {
cb(e.message);
}
}

validateFromAndGetDerivationPath(from) {
let addresses = this.getAccountsSync();
let idx = addresses.findIndex((i) => i === from);
if (idx === -1) {
throw new Error(
`tried to sign transaction with a 'from' address that is not an address on this trezor ${from}. Possible addresses are ${addresses.join()}. Use 'numberOfAccounts' constructor option to increase available addresses.`
);
}
}

signTransaction(txn, cb) {
let {
to = "", // according to trezorctl docs, empty string designates contract creation
Expand All @@ -79,72 +114,36 @@ class Trezor {
gasPrice = numberToBN(gasPrice).toString();
value = numberToBN(value).toString();
nonce = numberToBN(nonce).toString();
let addresses = this.getAccountsSync();
let idx = addresses.findIndex((i) => i === from);
if (idx === -1) {
cb(
`tried to sign transaction with a 'from' address that is not an address on this trezor ${from}. Possible addresses are ${addresses.join()}. Use 'numberOfAccounts' constructor option to increase available addresses.`

let defaultDerivationPath;
try {
defaultDerivationPath = this.validateFromAndGetDerivationPath(from);
} catch (e) {
cb(e.message);
}

const path = this.opts.derivationPath || defaultDerivationPath;
let response;
try {
// --gas-limit is an integer
// --gas-price is a string integer (wei)
// --nonce is an integer
// --data is a string hex: "0x1234"
// --chain-id is an integer
// "to" is a string hex: "0x1234" (or empty string for contract creation)
// "value" is a string integer (wei)
console.log("\u0007");
response = trezorCtl(
`ethereum sign-tx --chain-id ${this.opts.chainId} --address "${path}" --nonce ${nonce} --gas-limit ${gasLimit} --gas-price "${gasPrice}" --data "${txn.data}" "${to}" "${value}"`
);
} catch (e) {
cb(e.message);
}
if (idx > -1) {
const path = this.opts.derivationPath
? this.opts.derivationPath
: `${this.opts.derivationPathPrefix}/${idx}`;
let response;
try {
// --gas-limit is an integer
// --gas-price is a string integer (wei)
// --nonce is an integer
// --data is a string hex: "0x1234"
// --chain-id is an integer
// "to" is a string hex: "0x1234" (or empty string for contract creation)
// "value" is a string integer (wei)
response = trezorCtl(
`ethereum sign-tx --chain-id ${this.opts.chainId} --address "${path}" --nonce ${nonce} --gas-limit ${gasLimit} --gas-price "${gasPrice}" --data "${txn.data}" "${to}" "${value}"`
);
} catch (e) {
cb(e.message);
}
if (response) {
let signedTxn = response.slice(response.indexOf("0x")).trim();
cb(null, signedTxn);
}
if (response) {
let signedTxn = response.slice(response.indexOf("0x")).trim();
cb(null, signedTxn);
}
}

signTypedMessage(txn, cb) {
let { from, data } = txn;
let addresses = this.getAccountsSync();
let idx = addresses.findIndex((i) => i === from);
const path = this.opts.derivationPath
? this.opts.derivationPath
: `${this.opts.derivationPathPrefix}/${idx}`;
// tmp file created because `ethereum sign-typed-data` takes file path as input.
tmp.file(
{ postfix: ".json" },
function _tempFileCreated(err, filePath, fd, cleanupCallback) {
if (err) throw err;
let response;
fs.writeFileSync(filePath, JSON.stringify(data), function (err) {
if (err) throw err;
});
try {
let command = `ethereum sign-typed-data --address "${path}" ${filePath}`;
response = trezorCtl(command);
} catch (e) {
console.log(e);
}
cleanupCallback();
if (response) {
let keyword = "signature: 0x";
let signedTxn = response.slice(
response.indexOf(keyword) + keyword.length
);
cb(null, signedTxn);
}
}
);
}
}

module.exports = class TrezorWalletProvider extends HookedWalletSubprovider {
Expand All @@ -157,3 +156,16 @@ module.exports = class TrezorWalletProvider extends HookedWalletSubprovider {
});
}
};

// https://advancedweb.hu/secure-tempfiles-in-nodejs-without-dependencies/

const withTempFile = (fn) => withTempDir((dir) => fn(path.join(dir, "file")));

const withTempDir = async (fn) => {
const dir = await fs.mkdtemp((await fs.realpath(os.tmpdir())) + path.sep);
try {
return await fn(dir);
} finally {
fs.rmdir(dir, { recursive: true });
}
};