-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtrezor-wallet-provider.js
164 lines (151 loc) · 4.92 KB
/
trezor-wallet-provider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
const { execSync } = require("child_process");
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";
function trezorCtl(args) {
let stdout = execSync(`trezorctl ${args}`, {
// this allows the CLI to prompt for a PIN if needed, but also for us to
// capture the result of the trezor command in stdout
stdio: ["inherit", "pipe", "inherit"],
});
return String(stdout).trim();
}
class Trezor {
constructor(opts = {}) {
this.opts = {
chainId: 1,
derivationPathPrefix: ETHEREUM_PATH,
numberOfAccounts: 1,
...opts,
};
}
getAccounts(cb) {
let accounts;
try {
accounts = this.getAccountsSync();
} catch (e) {
cb(e.message);
}
if (accounts) {
cb(null, accounts);
}
}
getAccountsSync() {
if (this.cachedAccounts && this.cachedAccounts.length) {
return this.cachedAccounts;
}
this.cachedAccounts = [];
if (this.opts.derivationPath) {
this.cachedAccounts.push(
trezorCtl(`ethereum get-address -n "${this.opts.derivationPath}"`)
);
return this.cachedAccounts;
}
for (let i = 0; i < this.opts.numberOfAccounts; i++) {
this.cachedAccounts.push(
trezorCtl(
`ethereum get-address -n "${this.opts.derivationPathPrefix}/${i}"`
)
);
}
return this.cachedAccounts;
}
signTransaction(txn, cb) {
let {
to = "", // according to trezorctl docs, empty string designates contract creation
from,
gas: gasLimit,
gasPrice,
nonce,
value = "0x0",
} = txn;
from = toChecksumAddress(from);
to = to !== "" ? toChecksumAddress(to) : to;
gasLimit = numberToBN(gasLimit).toString();
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.`
);
}
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);
}
}
}
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;
if (typeof data !== "string") {
data = JSON.stringify(data);
}
fs.writeFileSync(filePath, 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 {
constructor(opts = {}) {
let trezor = new Trezor(opts);
super({
getAccounts: trezor.getAccounts.bind(trezor),
signTransaction: trezor.signTransaction.bind(trezor),
signTypedMessage: trezor.signTypedMessage.bind(trezor),
});
}
};