mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-24 11:01:41 +01:00
330 lines
8.7 KiB
JavaScript
330 lines
8.7 KiB
JavaScript
const {EventEmitter} = require('events')
|
|
const HDKey = require('hdkey')
|
|
const ethUtil = require('ethereumjs-util')
|
|
const sigUtil = require('eth-sig-util')
|
|
const Transaction = require('ethereumjs-tx')
|
|
|
|
|
|
// HD path differs from eth-hd-keyring - MEW, Parity, Geth and Official Ledger clients use same unusual derivation for Ledger
|
|
const hdPathString = `44'/60'/0'`
|
|
const type = 'Ledger Hardware'
|
|
const ORIGIN = 'https://localhost:3000'
|
|
const pathBase = 'm'
|
|
const MAX_INDEX = 1000
|
|
|
|
class LedgerKeyring extends EventEmitter {
|
|
constructor (opts = {}) {
|
|
super()
|
|
this.type = type
|
|
this.page = 0
|
|
this.perPage = 5
|
|
this.unlockedAccount = 0
|
|
this.hdk = new HDKey()
|
|
this.paths = {}
|
|
this.iframe = null
|
|
this.setupIframe()
|
|
this.deserialize(opts)
|
|
}
|
|
|
|
setupIframe () {
|
|
this.iframe = document.createElement('iframe')
|
|
this.iframe.src = ORIGIN
|
|
console.log('Injecting ledger iframe')
|
|
document.head.appendChild(this.iframe)
|
|
}
|
|
|
|
sendMessage (msg, cb) {
|
|
console.log('[LEDGER]: SENDING MESSAGE TO IFRAME', msg)
|
|
this.iframe.contentWindow.postMessage({...msg, target: 'LEDGER-IFRAME'}, '*')
|
|
window.addEventListener('message', ({ origin, data }) => {
|
|
if (origin !== ORIGIN) return false
|
|
if (data && data.action && data.action === `${msg.action}-reply`) {
|
|
console.log('[LEDGER]: GOT MESAGE FROM IFRAME', data)
|
|
cb(data)
|
|
}
|
|
})
|
|
}
|
|
|
|
serialize () {
|
|
return Promise.resolve({hdPath: this.hdPath, accounts: this.accounts})
|
|
}
|
|
|
|
deserialize (opts = {}) {
|
|
this.hdPath = opts.hdPath || hdPathString
|
|
this.unlocked = opts.unlocked || false
|
|
this.accounts = opts.accounts || []
|
|
return Promise.resolve()
|
|
}
|
|
|
|
isUnlocked () {
|
|
return this.unlocked
|
|
}
|
|
|
|
setAccountToUnlock (index) {
|
|
this.unlockedAccount = parseInt(index, 10)
|
|
}
|
|
|
|
unlock () {
|
|
|
|
if (this.isUnlocked()) return Promise.resolve('already unlocked')
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.sendMessage({
|
|
action: 'ledger-unlock',
|
|
params: {
|
|
hdPath: this.hdPath,
|
|
},
|
|
},
|
|
({action, success, payload}) => {
|
|
if (success) {
|
|
this.hdk.publicKey = new Buffer(payload.publicKey, 'hex')
|
|
this.hdk.chainCode = new Buffer(payload.chainCode, 'hex')
|
|
resolve('just unlocked')
|
|
} else {
|
|
reject(payload.error || 'Unknown error')
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
setAccountToUnlock (index) {
|
|
this.unlockedAccount = parseInt(index, 10)
|
|
}
|
|
|
|
addAccounts (n = 1) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.unlock()
|
|
.then(_ => {
|
|
const from = this.unlockedAccount
|
|
const to = from + n
|
|
this.accounts = []
|
|
|
|
for (let i = from; i < to; i++) {
|
|
const address = this._addressFromIndex(pathBase, i)
|
|
this.accounts.push(address)
|
|
this.page = 0
|
|
}
|
|
resolve(this.accounts)
|
|
})
|
|
.catch(e => {
|
|
reject(e)
|
|
})
|
|
})
|
|
}
|
|
|
|
getFirstPage () {
|
|
this.page = 0
|
|
return this.__getPage(1)
|
|
}
|
|
|
|
getNextPage () {
|
|
return this.__getPage(1)
|
|
}
|
|
|
|
getPreviousPage () {
|
|
return this.__getPage(-1)
|
|
}
|
|
|
|
__getPage (increment) {
|
|
|
|
this.page += increment
|
|
|
|
if (this.page <= 0) { this.page = 1 }
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.unlock()
|
|
.then(_ => {
|
|
|
|
const from = (this.page - 1) * this.perPage
|
|
const to = from + this.perPage
|
|
|
|
const accounts = []
|
|
|
|
for (let i = from; i < to; i++) {
|
|
const address = this._addressFromIndex(pathBase, i)
|
|
accounts.push({
|
|
address: address,
|
|
balance: null,
|
|
index: i,
|
|
})
|
|
this.paths[ethUtil.toChecksumAddress(address)] = i
|
|
|
|
}
|
|
resolve(accounts)
|
|
})
|
|
.catch(e => {
|
|
reject(e)
|
|
})
|
|
})
|
|
}
|
|
|
|
getAccounts () {
|
|
return Promise.resolve(this.accounts.slice())
|
|
}
|
|
|
|
removeAccount (address) {
|
|
if (!this.accounts.map(a => a.toLowerCase()).includes(address.toLowerCase())) {
|
|
throw new Error(`Address ${address} not found in this keyring`)
|
|
}
|
|
this.accounts = this.accounts.filter(a => a.toLowerCase() !== address.toLowerCase())
|
|
}
|
|
|
|
|
|
// tx is an instance of the ethereumjs-transaction class.
|
|
async signTransaction (address, tx) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.unlock()
|
|
.then(_ => {
|
|
this.sendMessage({
|
|
action: 'ledger-sign-transaction',
|
|
params: {
|
|
tx: {
|
|
from: this._normalize(address),
|
|
to: this._normalize(tx.to),
|
|
value: this._normalize(tx.value),
|
|
data: this._normalize(tx.data),
|
|
chainId: tx._chainId,
|
|
nonce: this._fixNonce(this._normalize(tx.nonce)),
|
|
gasLimit: this._normalize(tx.gasLimit),
|
|
gasPrice: this._normalize(tx.gasPrice),
|
|
},
|
|
path: this._pathFromAddress(address),
|
|
},
|
|
},
|
|
({action, success, payload}) => {
|
|
if (success) {
|
|
const signedTx = new Transaction(payload.txData)
|
|
// Validate that the signature matches the right address
|
|
const addressSignedWith = ethUtil.toChecksumAddress(`0x${signedTx.from.toString('hex')}`)
|
|
const correctAddress = ethUtil.toChecksumAddress(address)
|
|
if (addressSignedWith !== correctAddress) {
|
|
reject('signature doesnt match the right address')
|
|
}
|
|
resolve(signedTx)
|
|
} else {
|
|
reject(payload)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
async signMessage (withAccount, data) {
|
|
throw new Error('Not supported on this device')
|
|
}
|
|
|
|
// For personal_sign, we need to prefix the message:
|
|
async signPersonalMessage (withAccount, message) {
|
|
const humanReadableMsg = this._toAscii(message)
|
|
const bufferMsg = Buffer.from(humanReadableMsg).toString('hex')
|
|
return new Promise((resolve, reject) => {
|
|
this.unlock()
|
|
.then(_ => {
|
|
this.sendMessage({
|
|
action: 'ledger-sign-personal-message',
|
|
params: {
|
|
path: this._pathFromAddress(withAccount ),
|
|
message: bufferMsg,
|
|
},
|
|
},
|
|
({action, success, payload}) => {
|
|
if (success) {
|
|
const { result } = payload
|
|
let v = result['v'] - 27
|
|
v = v.toString(16)
|
|
if (v.length < 2) {
|
|
v = `0${v}`
|
|
}
|
|
const signature = `0x${result['r']}${result['s']}${v}`
|
|
const addressSignedWith = sigUtil.recoverPersonalSignature({data: message, sig: signature})
|
|
if (ethUtil.toChecksumAddress(addressSignedWith) !== ethUtil.toChecksumAddress(withAccount)) {
|
|
reject('signature doesnt match the right address')
|
|
}
|
|
resolve(signature)
|
|
} else {
|
|
reject(payload)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
async signTypedData (withAccount, typedData) {
|
|
throw new Error('Not supported on this device')
|
|
}
|
|
|
|
async exportAccount (address) {
|
|
throw new Error('Not supported on this device')
|
|
}
|
|
|
|
forgetDevice () {
|
|
this.accounts = []
|
|
this.unlocked = false
|
|
this.page = 0
|
|
this.unlockedAccount = 0
|
|
this.paths = {}
|
|
}
|
|
|
|
/* PRIVATE METHODS */
|
|
|
|
_padLeftEven (hex) {
|
|
return hex.length % 2 !== 0 ? `0${hex}` : hex
|
|
}
|
|
|
|
_normalize (buf) {
|
|
return this._padLeftEven(ethUtil.bufferToHex(buf).toLowerCase())
|
|
}
|
|
|
|
_addressFromIndex (pathBase, i) {
|
|
const dkey = this.hdk.derive(`${pathBase}/${i}`)
|
|
const address = ethUtil
|
|
.publicToAddress(dkey.publicKey, true)
|
|
.toString('hex')
|
|
return ethUtil.toChecksumAddress(address)
|
|
}
|
|
|
|
_pathFromAddress (address) {
|
|
const checksummedAddress = ethUtil.toChecksumAddress(address)
|
|
let index = this.paths[checksummedAddress]
|
|
if (typeof index === 'undefined') {
|
|
for (let i = 0; i < MAX_INDEX; i++) {
|
|
if (checksummedAddress === this._addressFromIndex(pathBase, i)) {
|
|
index = i
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (typeof index === 'undefined') {
|
|
throw new Error('Unknown address')
|
|
}
|
|
return `${this.hdPath}/${index}`
|
|
}
|
|
|
|
_toAscii (hex) {
|
|
let str = ''
|
|
let i = 0; const l = hex.length
|
|
if (hex.substring(0, 2) === '0x') {
|
|
i = 2
|
|
}
|
|
for (; i < l; i += 2) {
|
|
const code = parseInt(hex.substr(i, 2), 16)
|
|
str += String.fromCharCode(code)
|
|
}
|
|
|
|
return str
|
|
}
|
|
|
|
_fixNonce (nonce) {
|
|
if (nonce === '0x') {
|
|
return `${nonce}0`
|
|
}
|
|
return nonce
|
|
}
|
|
}
|
|
|
|
LedgerKeyring.type = type
|
|
module.exports = LedgerKeyring
|