@metamask/eth-ledger-bridge-keyring 0.14.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +7 -14
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/ledger-bridge.d.ts +34 -0
- package/dist/ledger-bridge.js +3 -0
- package/dist/ledger-bridge.js.map +1 -0
- package/dist/ledger-iframe-bridge.d.ts +85 -0
- package/dist/ledger-iframe-bridge.js +150 -0
- package/dist/ledger-iframe-bridge.js.map +1 -0
- package/dist/ledger-keyring.d.ts +91 -0
- package/dist/ledger-keyring.js +531 -0
- package/dist/ledger-keyring.js.map +1 -0
- package/package.json +68 -36
- package/index.js +0 -651
package/index.js
DELETED
|
@@ -1,651 +0,0 @@
|
|
|
1
|
-
const { EventEmitter } = require('events')
|
|
2
|
-
const HDKey = require('hdkey')
|
|
3
|
-
const ethUtil = require('ethereumjs-util')
|
|
4
|
-
const sigUtil = require('eth-sig-util')
|
|
5
|
-
const { TransactionFactory } = require('@ethereumjs/tx')
|
|
6
|
-
|
|
7
|
-
const pathBase = 'm'
|
|
8
|
-
const hdPathString = `${pathBase}/44'/60'/0'`
|
|
9
|
-
const type = 'Ledger Hardware'
|
|
10
|
-
|
|
11
|
-
const BRIDGE_URL = 'https://metamask.github.io/eth-ledger-bridge-keyring'
|
|
12
|
-
|
|
13
|
-
const MAX_INDEX = 1000
|
|
14
|
-
const NETWORK_API_URLS = {
|
|
15
|
-
ropsten: 'http://api-ropsten.etherscan.io',
|
|
16
|
-
kovan: 'http://api-kovan.etherscan.io',
|
|
17
|
-
rinkeby: 'https://api-rinkeby.etherscan.io',
|
|
18
|
-
mainnet: 'https://api.etherscan.io',
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const CONNECTION_EVENT = 'ledger-connection-change'
|
|
22
|
-
|
|
23
|
-
class LedgerBridgeKeyring extends EventEmitter {
|
|
24
|
-
constructor (opts = {}) {
|
|
25
|
-
super()
|
|
26
|
-
this.accountDetails = {}
|
|
27
|
-
this.bridgeUrl = null
|
|
28
|
-
this.type = type
|
|
29
|
-
this.page = 0
|
|
30
|
-
this.perPage = 5
|
|
31
|
-
this.unlockedAccount = 0
|
|
32
|
-
this.hdk = new HDKey()
|
|
33
|
-
this.paths = {}
|
|
34
|
-
this.iframe = null
|
|
35
|
-
this.network = 'mainnet'
|
|
36
|
-
this.implementFullBIP44 = false
|
|
37
|
-
this.deserialize(opts)
|
|
38
|
-
|
|
39
|
-
this.iframeLoaded = false
|
|
40
|
-
this._setupIframe()
|
|
41
|
-
|
|
42
|
-
this.currentMessageId = 0
|
|
43
|
-
this.messageCallbacks = {}
|
|
44
|
-
this._setupListener()
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
serialize () {
|
|
48
|
-
return Promise.resolve({
|
|
49
|
-
hdPath: this.hdPath,
|
|
50
|
-
accounts: this.accounts,
|
|
51
|
-
accountDetails: this.accountDetails,
|
|
52
|
-
bridgeUrl: this.bridgeUrl,
|
|
53
|
-
implementFullBIP44: false,
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
deserialize (opts = {}) {
|
|
58
|
-
this.hdPath = opts.hdPath || hdPathString
|
|
59
|
-
this.bridgeUrl = opts.bridgeUrl || BRIDGE_URL
|
|
60
|
-
this.accounts = opts.accounts || []
|
|
61
|
-
this.accountDetails = opts.accountDetails || {}
|
|
62
|
-
if (!opts.accountDetails) {
|
|
63
|
-
this._migrateAccountDetails(opts)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
this.implementFullBIP44 = opts.implementFullBIP44 || false
|
|
67
|
-
|
|
68
|
-
// Remove accounts that don't have corresponding account details
|
|
69
|
-
this.accounts = this.accounts
|
|
70
|
-
.filter((account) => Object.keys(this.accountDetails).includes(ethUtil.toChecksumAddress(account)))
|
|
71
|
-
|
|
72
|
-
return Promise.resolve()
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
_migrateAccountDetails (opts) {
|
|
76
|
-
if (this._isLedgerLiveHdPath() && opts.accountIndexes) {
|
|
77
|
-
for (const account of Object.keys(opts.accountIndexes)) {
|
|
78
|
-
this.accountDetails[account] = {
|
|
79
|
-
bip44: true,
|
|
80
|
-
hdPath: this._getPathForIndex(opts.accountIndexes[account]),
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// try to migrate non-LedgerLive accounts too
|
|
86
|
-
if (!this._isLedgerLiveHdPath()) {
|
|
87
|
-
this.accounts
|
|
88
|
-
.filter((account) => !Object.keys(this.accountDetails).includes(ethUtil.toChecksumAddress(account)))
|
|
89
|
-
.forEach((account) => {
|
|
90
|
-
try {
|
|
91
|
-
this.accountDetails[ethUtil.toChecksumAddress(account)] = {
|
|
92
|
-
bip44: false,
|
|
93
|
-
hdPath: this._pathFromAddress(account),
|
|
94
|
-
}
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.log(`failed to migrate account ${account}`)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
isUnlocked () {
|
|
103
|
-
return Boolean(this.hdk && this.hdk.publicKey)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
isConnected () {
|
|
107
|
-
return this.isDeviceConnected
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
setAccountToUnlock (index) {
|
|
111
|
-
this.unlockedAccount = parseInt(index, 10)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
setHdPath (hdPath) {
|
|
115
|
-
// Reset HDKey if the path changes
|
|
116
|
-
if (this.hdPath !== hdPath) {
|
|
117
|
-
this.hdk = new HDKey()
|
|
118
|
-
}
|
|
119
|
-
this.hdPath = hdPath
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
unlock (hdPath, updateHdk = true) {
|
|
123
|
-
if (this.isUnlocked() && !hdPath) {
|
|
124
|
-
return Promise.resolve('already unlocked')
|
|
125
|
-
}
|
|
126
|
-
const path = hdPath ? this._toLedgerPath(hdPath) : this.hdPath
|
|
127
|
-
return new Promise((resolve, reject) => {
|
|
128
|
-
this._sendMessage({
|
|
129
|
-
action: 'ledger-unlock',
|
|
130
|
-
params: {
|
|
131
|
-
hdPath: path,
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
({ success, payload }) => {
|
|
135
|
-
if (success) {
|
|
136
|
-
if (updateHdk) {
|
|
137
|
-
this.hdk.publicKey = Buffer.from(payload.publicKey, 'hex')
|
|
138
|
-
this.hdk.chainCode = Buffer.from(payload.chainCode, 'hex')
|
|
139
|
-
}
|
|
140
|
-
resolve(payload.address)
|
|
141
|
-
} else {
|
|
142
|
-
reject(payload.error || new Error('Unknown error'))
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
addAccounts (n = 1) {
|
|
149
|
-
|
|
150
|
-
return new Promise((resolve, reject) => {
|
|
151
|
-
this.unlock()
|
|
152
|
-
.then(async (_) => {
|
|
153
|
-
const from = this.unlockedAccount
|
|
154
|
-
const to = from + n
|
|
155
|
-
for (let i = from; i < to; i++) {
|
|
156
|
-
const path = this._getPathForIndex(i)
|
|
157
|
-
let address
|
|
158
|
-
if (this._isLedgerLiveHdPath()) {
|
|
159
|
-
address = await this.unlock(path)
|
|
160
|
-
} else {
|
|
161
|
-
address = this._addressFromIndex(pathBase, i)
|
|
162
|
-
}
|
|
163
|
-
this.accountDetails[ethUtil.toChecksumAddress(address)] = {
|
|
164
|
-
// TODO: consider renaming this property, as the current name is misleading
|
|
165
|
-
// It's currently used to represent whether an account uses the Ledger Live path.
|
|
166
|
-
bip44: this._isLedgerLiveHdPath(),
|
|
167
|
-
hdPath: path,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (!this.accounts.includes(address)) {
|
|
171
|
-
this.accounts.push(address)
|
|
172
|
-
}
|
|
173
|
-
this.page = 0
|
|
174
|
-
}
|
|
175
|
-
resolve(this.accounts)
|
|
176
|
-
})
|
|
177
|
-
.catch(reject)
|
|
178
|
-
})
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
getFirstPage () {
|
|
182
|
-
this.page = 0
|
|
183
|
-
return this.__getPage(1)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
getNextPage () {
|
|
187
|
-
return this.__getPage(1)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
getPreviousPage () {
|
|
191
|
-
return this.__getPage(-1)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
getAccounts () {
|
|
195
|
-
return Promise.resolve(this.accounts.slice())
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
removeAccount (address) {
|
|
199
|
-
if (!this.accounts.map((a) => a.toLowerCase()).includes(address.toLowerCase())) {
|
|
200
|
-
throw new Error(`Address ${address} not found in this keyring`)
|
|
201
|
-
}
|
|
202
|
-
this.accounts = this.accounts.filter((a) => a.toLowerCase() !== address.toLowerCase())
|
|
203
|
-
delete this.accountDetails[ethUtil.toChecksumAddress(address)]
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
attemptMakeApp () {
|
|
207
|
-
return new Promise((resolve, reject) => {
|
|
208
|
-
this._sendMessage({
|
|
209
|
-
action: 'ledger-make-app',
|
|
210
|
-
}, ({ success, error }) => {
|
|
211
|
-
if (success) {
|
|
212
|
-
resolve(true)
|
|
213
|
-
} else {
|
|
214
|
-
reject(error)
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
updateTransportMethod (transportType) {
|
|
221
|
-
return new Promise((resolve, reject) => {
|
|
222
|
-
// If the iframe isn't loaded yet, let's store the desired transportType value and
|
|
223
|
-
// optimistically return a successful promise
|
|
224
|
-
if (!this.iframeLoaded) {
|
|
225
|
-
this.delayedPromise = {
|
|
226
|
-
resolve,
|
|
227
|
-
reject,
|
|
228
|
-
transportType,
|
|
229
|
-
}
|
|
230
|
-
return
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
this._sendMessage({
|
|
234
|
-
action: 'ledger-update-transport',
|
|
235
|
-
params: { transportType },
|
|
236
|
-
}, ({ success }) => {
|
|
237
|
-
if (success) {
|
|
238
|
-
resolve(true)
|
|
239
|
-
} else {
|
|
240
|
-
reject(new Error('Ledger transport could not be updated'))
|
|
241
|
-
}
|
|
242
|
-
})
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// tx is an instance of the ethereumjs-transaction class.
|
|
247
|
-
signTransaction (address, tx) {
|
|
248
|
-
let rawTxHex
|
|
249
|
-
// transactions built with older versions of ethereumjs-tx have a
|
|
250
|
-
// getChainId method that newer versions do not. Older versions are mutable
|
|
251
|
-
// while newer versions default to being immutable. Expected shape and type
|
|
252
|
-
// of data for v, r and s differ (Buffer (old) vs BN (new))
|
|
253
|
-
if (typeof tx.getChainId === 'function') {
|
|
254
|
-
// In this version of ethereumjs-tx we must add the chainId in hex format
|
|
255
|
-
// to the initial v value. The chainId must be included in the serialized
|
|
256
|
-
// transaction which is only communicated to ethereumjs-tx in this
|
|
257
|
-
// value. In newer versions the chainId is communicated via the 'Common'
|
|
258
|
-
// object.
|
|
259
|
-
tx.v = ethUtil.bufferToHex(tx.getChainId())
|
|
260
|
-
tx.r = '0x00'
|
|
261
|
-
tx.s = '0x00'
|
|
262
|
-
|
|
263
|
-
rawTxHex = tx.serialize().toString('hex')
|
|
264
|
-
|
|
265
|
-
return this._signTransaction(address, rawTxHex, (payload) => {
|
|
266
|
-
tx.v = Buffer.from(payload.v, 'hex')
|
|
267
|
-
tx.r = Buffer.from(payload.r, 'hex')
|
|
268
|
-
tx.s = Buffer.from(payload.s, 'hex')
|
|
269
|
-
return tx
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// The below `encode` call is only necessary for legacy transactions, as `getMessageToSign`
|
|
274
|
-
// calls `rlp.encode` internally for non-legacy transactions. As per the "Transaction Execution"
|
|
275
|
-
// section of the ethereum yellow paper, transactions need to be "well-formed RLP, with no additional
|
|
276
|
-
// trailing bytes".
|
|
277
|
-
|
|
278
|
-
// Note also that `getMessageToSign` will return valid RLP for all transaction types, whereas the
|
|
279
|
-
// `serialize` method will not for any transaction type except legacy. This is because `serialize` includes
|
|
280
|
-
// empty r, s and v values in the encoded rlp. This is why we use `getMessageToSign` here instead of `serialize`.
|
|
281
|
-
const messageToSign = tx.getMessageToSign(false)
|
|
282
|
-
|
|
283
|
-
rawTxHex = Buffer.isBuffer(messageToSign)
|
|
284
|
-
? messageToSign.toString('hex')
|
|
285
|
-
: ethUtil.rlp.encode(messageToSign).toString('hex')
|
|
286
|
-
|
|
287
|
-
return this._signTransaction(address, rawTxHex, (payload) => {
|
|
288
|
-
// Because tx will be immutable, first get a plain javascript object that
|
|
289
|
-
// represents the transaction. Using txData here as it aligns with the
|
|
290
|
-
// nomenclature of ethereumjs/tx.
|
|
291
|
-
const txData = tx.toJSON()
|
|
292
|
-
// The fromTxData utility expects a type to support transactions with a type other than 0
|
|
293
|
-
txData.type = tx.type
|
|
294
|
-
// The fromTxData utility expects v,r and s to be hex prefixed
|
|
295
|
-
txData.v = ethUtil.addHexPrefix(payload.v)
|
|
296
|
-
txData.r = ethUtil.addHexPrefix(payload.r)
|
|
297
|
-
txData.s = ethUtil.addHexPrefix(payload.s)
|
|
298
|
-
// Adopt the 'common' option from the original transaction and set the
|
|
299
|
-
// returned object to be frozen if the original is frozen.
|
|
300
|
-
return TransactionFactory.fromTxData(txData, { common: tx.common, freeze: Object.isFrozen(tx) })
|
|
301
|
-
})
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
_signTransaction (address, rawTxHex, handleSigning) {
|
|
305
|
-
return new Promise((resolve, reject) => {
|
|
306
|
-
this.unlockAccountByAddress(address)
|
|
307
|
-
.then((hdPath) => {
|
|
308
|
-
this._sendMessage({
|
|
309
|
-
action: 'ledger-sign-transaction',
|
|
310
|
-
params: {
|
|
311
|
-
tx: rawTxHex,
|
|
312
|
-
hdPath,
|
|
313
|
-
},
|
|
314
|
-
},
|
|
315
|
-
({ success, payload }) => {
|
|
316
|
-
if (success) {
|
|
317
|
-
|
|
318
|
-
const newOrMutatedTx = handleSigning(payload)
|
|
319
|
-
const valid = newOrMutatedTx.verifySignature()
|
|
320
|
-
if (valid) {
|
|
321
|
-
resolve(newOrMutatedTx)
|
|
322
|
-
} else {
|
|
323
|
-
reject(new Error('Ledger: The transaction signature is not valid'))
|
|
324
|
-
}
|
|
325
|
-
} else {
|
|
326
|
-
reject(payload.error || new Error('Ledger: Unknown error while signing transaction'))
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
})
|
|
330
|
-
.catch(reject)
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
signMessage (withAccount, data) {
|
|
335
|
-
return this.signPersonalMessage(withAccount, data)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// For personal_sign, we need to prefix the message:
|
|
339
|
-
signPersonalMessage (withAccount, message) {
|
|
340
|
-
return new Promise((resolve, reject) => {
|
|
341
|
-
this.unlockAccountByAddress(withAccount)
|
|
342
|
-
.then((hdPath) => {
|
|
343
|
-
this._sendMessage({
|
|
344
|
-
action: 'ledger-sign-personal-message',
|
|
345
|
-
params: {
|
|
346
|
-
hdPath,
|
|
347
|
-
message: ethUtil.stripHexPrefix(message),
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
({ success, payload }) => {
|
|
351
|
-
if (success) {
|
|
352
|
-
let v = parseInt(payload.v, 10)
|
|
353
|
-
v = v.toString(16)
|
|
354
|
-
if (v.length < 2) {
|
|
355
|
-
v = `0${v}`
|
|
356
|
-
}
|
|
357
|
-
const signature = `0x${payload.r}${payload.s}${v}`
|
|
358
|
-
const addressSignedWith = sigUtil.recoverPersonalSignature({ data: message, sig: signature })
|
|
359
|
-
if (ethUtil.toChecksumAddress(addressSignedWith) !== ethUtil.toChecksumAddress(withAccount)) {
|
|
360
|
-
reject(new Error('Ledger: The signature doesnt match the right address'))
|
|
361
|
-
}
|
|
362
|
-
resolve(signature)
|
|
363
|
-
} else {
|
|
364
|
-
reject(payload.error || new Error('Ledger: Unknown error while signing message'))
|
|
365
|
-
}
|
|
366
|
-
})
|
|
367
|
-
})
|
|
368
|
-
.catch(reject)
|
|
369
|
-
})
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
async unlockAccountByAddress (address) {
|
|
373
|
-
const checksummedAddress = ethUtil.toChecksumAddress(address)
|
|
374
|
-
if (!Object.keys(this.accountDetails).includes(checksummedAddress)) {
|
|
375
|
-
throw new Error(`Ledger: Account for address '${checksummedAddress}' not found`)
|
|
376
|
-
}
|
|
377
|
-
const { hdPath } = this.accountDetails[checksummedAddress]
|
|
378
|
-
const unlockedAddress = await this.unlock(hdPath, false)
|
|
379
|
-
|
|
380
|
-
// unlock resolves to the address for the given hdPath as reported by the ledger device
|
|
381
|
-
// if that address is not the requested address, then this account belongs to a different device or seed
|
|
382
|
-
if (unlockedAddress.toLowerCase() !== address.toLowerCase()) {
|
|
383
|
-
throw new Error(`Ledger: Account ${address} does not belong to the connected device`)
|
|
384
|
-
}
|
|
385
|
-
return hdPath
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async signTypedData (withAccount, data, options = {}) {
|
|
389
|
-
const isV4 = options.version === 'V4'
|
|
390
|
-
if (!isV4) {
|
|
391
|
-
throw new Error('Ledger: Only version 4 of typed data signing is supported')
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const {
|
|
395
|
-
domain,
|
|
396
|
-
types,
|
|
397
|
-
primaryType,
|
|
398
|
-
message,
|
|
399
|
-
} = sigUtil.TypedDataUtils.sanitizeData(data)
|
|
400
|
-
const domainSeparatorHex = sigUtil.TypedDataUtils.hashStruct('EIP712Domain', domain, types, isV4).toString('hex')
|
|
401
|
-
const hashStructMessageHex = sigUtil.TypedDataUtils.hashStruct(primaryType, message, types, isV4).toString('hex')
|
|
402
|
-
|
|
403
|
-
const hdPath = await this.unlockAccountByAddress(withAccount)
|
|
404
|
-
const { success, payload } = await new Promise((resolve) => {
|
|
405
|
-
this._sendMessage({
|
|
406
|
-
action: 'ledger-sign-typed-data',
|
|
407
|
-
params: {
|
|
408
|
-
hdPath,
|
|
409
|
-
domainSeparatorHex,
|
|
410
|
-
hashStructMessageHex,
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
(result) => resolve(result))
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
if (success) {
|
|
417
|
-
let v = parseInt(payload.v, 10)
|
|
418
|
-
v = v.toString(16)
|
|
419
|
-
if (v.length < 2) {
|
|
420
|
-
v = `0${v}`
|
|
421
|
-
}
|
|
422
|
-
const signature = `0x${payload.r}${payload.s}${v}`
|
|
423
|
-
const addressSignedWith = sigUtil.recoverTypedSignature_v4({
|
|
424
|
-
data,
|
|
425
|
-
sig: signature,
|
|
426
|
-
})
|
|
427
|
-
if (ethUtil.toChecksumAddress(addressSignedWith) !== ethUtil.toChecksumAddress(withAccount)) {
|
|
428
|
-
throw new Error('Ledger: The signature doesnt match the right address')
|
|
429
|
-
}
|
|
430
|
-
return signature
|
|
431
|
-
}
|
|
432
|
-
throw payload.error || new Error('Ledger: Unknown error while signing message')
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
exportAccount () {
|
|
436
|
-
throw new Error('Not supported on this device')
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
forgetDevice () {
|
|
440
|
-
this.accounts = []
|
|
441
|
-
this.page = 0
|
|
442
|
-
this.unlockedAccount = 0
|
|
443
|
-
this.paths = {}
|
|
444
|
-
this.accountDetails = {}
|
|
445
|
-
this.hdk = new HDKey()
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/* PRIVATE METHODS */
|
|
449
|
-
|
|
450
|
-
_setupIframe () {
|
|
451
|
-
this.iframe = document.createElement('iframe')
|
|
452
|
-
this.iframe.src = this.bridgeUrl
|
|
453
|
-
this.iframe.allow = `hid 'src'`
|
|
454
|
-
this.iframe.onload = async () => {
|
|
455
|
-
// If the ledger live preference was set before the iframe is loaded,
|
|
456
|
-
// set it after the iframe has loaded
|
|
457
|
-
this.iframeLoaded = true
|
|
458
|
-
if (this.delayedPromise) {
|
|
459
|
-
try {
|
|
460
|
-
const result = await this.updateTransportMethod(
|
|
461
|
-
this.delayedPromise.transportType,
|
|
462
|
-
)
|
|
463
|
-
this.delayedPromise.resolve(result)
|
|
464
|
-
} catch (e) {
|
|
465
|
-
this.delayedPromise.reject(e)
|
|
466
|
-
} finally {
|
|
467
|
-
delete this.delayedPromise
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
document.head.appendChild(this.iframe)
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
_getOrigin () {
|
|
475
|
-
const tmp = this.bridgeUrl.split('/')
|
|
476
|
-
tmp.splice(-1, 1)
|
|
477
|
-
return tmp.join('/')
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
_sendMessage (msg, cb) {
|
|
481
|
-
msg.target = 'LEDGER-IFRAME'
|
|
482
|
-
|
|
483
|
-
this.currentMessageId += 1
|
|
484
|
-
msg.messageId = this.currentMessageId
|
|
485
|
-
|
|
486
|
-
this.messageCallbacks[this.currentMessageId] = cb
|
|
487
|
-
this.iframe.contentWindow.postMessage(msg, '*')
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
_setupListener () {
|
|
491
|
-
this._eventListener = ({ origin, data }) => {
|
|
492
|
-
if (origin !== this._getOrigin()) {
|
|
493
|
-
return false
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (data) {
|
|
497
|
-
if (this.messageCallbacks[data.messageId]) {
|
|
498
|
-
this.messageCallbacks[data.messageId](data)
|
|
499
|
-
} else if (data.action === CONNECTION_EVENT) {
|
|
500
|
-
this.isDeviceConnected = data.payload.connected
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return undefined
|
|
505
|
-
}
|
|
506
|
-
window.addEventListener('message', this._eventListener)
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
destroy () {
|
|
510
|
-
window.removeEventListener('message', this._eventListener)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async __getPage (increment) {
|
|
514
|
-
|
|
515
|
-
this.page += increment
|
|
516
|
-
|
|
517
|
-
if (this.page <= 0) {
|
|
518
|
-
this.page = 1
|
|
519
|
-
}
|
|
520
|
-
const from = (this.page - 1) * this.perPage
|
|
521
|
-
const to = from + this.perPage
|
|
522
|
-
|
|
523
|
-
await this.unlock()
|
|
524
|
-
let accounts
|
|
525
|
-
if (this._isLedgerLiveHdPath()) {
|
|
526
|
-
accounts = await this._getAccountsBIP44(from, to)
|
|
527
|
-
} else {
|
|
528
|
-
accounts = this._getAccountsLegacy(from, to)
|
|
529
|
-
}
|
|
530
|
-
return accounts
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
async _getAccountsBIP44 (from, to) {
|
|
534
|
-
const accounts = []
|
|
535
|
-
|
|
536
|
-
for (let i = from; i < to; i++) {
|
|
537
|
-
const path = this._getPathForIndex(i)
|
|
538
|
-
const address = await this.unlock(path)
|
|
539
|
-
const valid = this.implementFullBIP44 ? await this._hasPreviousTransactions(address) : true
|
|
540
|
-
accounts.push({
|
|
541
|
-
address,
|
|
542
|
-
balance: null,
|
|
543
|
-
index: i,
|
|
544
|
-
})
|
|
545
|
-
// PER BIP44
|
|
546
|
-
// "Software should prevent a creation of an account if
|
|
547
|
-
// a previous account does not have a transaction history
|
|
548
|
-
// (meaning none of its addresses have been used before)."
|
|
549
|
-
if (!valid) {
|
|
550
|
-
break
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
return accounts
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
_getAccountsLegacy (from, to) {
|
|
557
|
-
const accounts = []
|
|
558
|
-
|
|
559
|
-
for (let i = from; i < to; i++) {
|
|
560
|
-
const address = this._addressFromIndex(pathBase, i)
|
|
561
|
-
accounts.push({
|
|
562
|
-
address,
|
|
563
|
-
balance: null,
|
|
564
|
-
index: i,
|
|
565
|
-
})
|
|
566
|
-
this.paths[ethUtil.toChecksumAddress(address)] = i
|
|
567
|
-
}
|
|
568
|
-
return accounts
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
_padLeftEven (hex) {
|
|
572
|
-
return hex.length % 2 === 0 ? hex : `0${hex}`
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
_normalize (buf) {
|
|
576
|
-
return this._padLeftEven(ethUtil.bufferToHex(buf).toLowerCase())
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// eslint-disable-next-line no-shadow
|
|
580
|
-
_addressFromIndex (pathBase, i) {
|
|
581
|
-
const dkey = this.hdk.derive(`${pathBase}/${i}`)
|
|
582
|
-
const address = ethUtil
|
|
583
|
-
.publicToAddress(dkey.publicKey, true)
|
|
584
|
-
.toString('hex')
|
|
585
|
-
return ethUtil.toChecksumAddress(`0x${address}`)
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
_pathFromAddress (address) {
|
|
589
|
-
const checksummedAddress = ethUtil.toChecksumAddress(address)
|
|
590
|
-
let index = this.paths[checksummedAddress]
|
|
591
|
-
if (typeof index === 'undefined') {
|
|
592
|
-
for (let i = 0; i < MAX_INDEX; i++) {
|
|
593
|
-
if (checksummedAddress === this._addressFromIndex(pathBase, i)) {
|
|
594
|
-
index = i
|
|
595
|
-
break
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
if (typeof index === 'undefined') {
|
|
601
|
-
throw new Error('Unknown address')
|
|
602
|
-
}
|
|
603
|
-
return this._getPathForIndex(index)
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
_toAscii (hex) {
|
|
607
|
-
let str = ''
|
|
608
|
-
let i = 0
|
|
609
|
-
const l = hex.length
|
|
610
|
-
if (hex.substring(0, 2) === '0x') {
|
|
611
|
-
i = 2
|
|
612
|
-
}
|
|
613
|
-
for (; i < l; i += 2) {
|
|
614
|
-
const code = parseInt(hex.substr(i, 2), 16)
|
|
615
|
-
str += String.fromCharCode(code)
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
return str
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
_getPathForIndex (index) {
|
|
622
|
-
// Check if the path is BIP 44 (Ledger Live)
|
|
623
|
-
return this._isLedgerLiveHdPath() ? `m/44'/60'/${index}'/0/0` : `${this.hdPath}/${index}`
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
_isLedgerLiveHdPath () {
|
|
627
|
-
return this.hdPath === `m/44'/60'/0'/0/0`
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
_toLedgerPath (path) {
|
|
631
|
-
return path.toString().replace('m/', '')
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
async _hasPreviousTransactions (address) {
|
|
635
|
-
const apiUrl = this._getApiUrl()
|
|
636
|
-
const response = await window.fetch(`${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1&offset=1`)
|
|
637
|
-
const parsedResponse = await response.json()
|
|
638
|
-
if (parsedResponse.status !== '0' && parsedResponse.result.length > 0) {
|
|
639
|
-
return true
|
|
640
|
-
}
|
|
641
|
-
return false
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
_getApiUrl () {
|
|
645
|
-
return NETWORK_API_URLS[this.network] || NETWORK_API_URLS.mainnet
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
LedgerBridgeKeyring.type = type
|
|
651
|
-
module.exports = LedgerBridgeKeyring
|