Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support bip 44 + tests #3

Merged
merged 2 commits into from
Aug 14, 2018
Merged
Show file tree
Hide file tree
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
157 changes: 118 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ const type = 'Ledger Hardware'
const BRIDGE_URL = 'https://metamask.github.io/eth-ledger-bridge-keyring'
const pathBase = 'm'
const MAX_INDEX = 1000
const NETWORK_API_URLS = {
ropsten: 'http://api-ropsten.etherscan.io',
kovan: 'http://api-kovan.etherscan.io',
rinkeby: 'https://api-rinkeby.etherscan.io',
mainnet: 'https://api.etherscan.io'
}

class LedgerBridgeKeyring extends EventEmitter {
constructor (opts = {}) {
Expand All @@ -21,6 +27,7 @@ class LedgerBridgeKeyring extends EventEmitter {
this.hdk = new HDKey()
this.paths = {}
this.iframe = null
this.network = 'mainnet'
this.deserialize(opts)
this._setupIframe()
}
Expand All @@ -32,35 +39,33 @@ class LedgerBridgeKeyring extends EventEmitter {
deserialize (opts = {}) {
this.hdPath = opts.hdPath || hdPathString
this.bridgeUrl = opts.bridgeUrl || BRIDGE_URL
this.unlocked = opts.unlocked || false
this.accounts = opts.accounts || []
return Promise.resolve()
}

isUnlocked () {
return this.unlocked
return this.hdk && this.hdk.publicKey ? true : false
}

setAccountToUnlock (index) {
this.unlockedAccount = parseInt(index, 10)
}

unlock () {

if (this.isUnlocked()) return Promise.resolve('already unlocked')
unlock (hdPath) {
if (this.isUnlocked() && !hdPath) return Promise.resolve('already unlocked')

return new Promise((resolve, reject) => {
this._sendMessage({
action: 'ledger-unlock',
params: {
hdPath: this.hdPath,
hdPath: this._toLedgerPath(hdPath ? hdPath : this.hdPath),
},
},
({success, payload}) => {
if (success) {
this.hdk.publicKey = new Buffer(payload.publicKey, 'hex')
this.hdk.chainCode = new Buffer(payload.chainCode, 'hex')
resolve('just unlocked')
resolve(payload.address)
} else {
reject(payload.error || 'Unknown error')
}
Expand All @@ -72,13 +77,18 @@ class LedgerBridgeKeyring extends EventEmitter {

return new Promise((resolve, reject) => {
this.unlock()
.then(_ => {
.then(async _ => {
const from = this.unlockedAccount
const to = from + n
this.accounts = []

for (let i = from; i < to; i++) {
const address = this._addressFromIndex(pathBase, i)
let address
if(this._isBIP44()){
const path = this._getPathForIndex(i)
address = await this.unlock(path)
}else{
address = this._addressFromIndex(pathBase, i)
}
this.accounts.push(address)
this.page = 0
}
Expand Down Expand Up @@ -114,9 +124,8 @@ class LedgerBridgeKeyring extends EventEmitter {
this.accounts = this.accounts.filter(a => a.toLowerCase() !== address.toLowerCase())
}


// tx is an instance of the ethereumjs-transaction class.
async signTransaction (address, tx) {
signTransaction (address, tx) {
return new Promise((resolve, reject) => {
this.unlock()
.then(_ => {
Expand All @@ -135,11 +144,18 @@ class LedgerBridgeKeyring extends EventEmitter {
s: '0x00',
})

let hdPath
if(this._isBIP44()){
hdPath = this._getPathForIndex(this.unlockedAccount)
}else{
hdPath = this._toLedgerPath(this._pathFromAddress(address))
}

this._sendMessage({
action: 'ledger-sign-transaction',
params: {
tx: newTx.serialize().toString('hex'),
hdPath: this._pathFromAddress(address),
hdPath,
},
},
({success, payload}) => {
Expand All @@ -163,21 +179,28 @@ class LedgerBridgeKeyring extends EventEmitter {
})
}

async signMessage (withAccount, data) {
signMessage (withAccount, data) {
throw new Error('Not supported on this device')
}

// For personal_sign, we need to prefix the message:
async signPersonalMessage (withAccount, message) {
signPersonalMessage (withAccount, message) {
const humanReadableMsg = this._toAscii(message)
const bufferMsg = Buffer.from(humanReadableMsg).toString('hex')
return new Promise((resolve, reject) => {
this.unlock()
.then(_ => {
let hdPath
if(this._isBIP44()){
hdPath = this._getPathForIndex(this.unlockedAccount)
}else{
hdPath = this._toLedgerPath(this._pathFromAddress(withAccount))
}

this._sendMessage({
action: 'ledger-sign-personal-message',
params: {
hdPath: this._pathFromAddress(withAccount),
hdPath,
message: bufferMsg,
},
},
Expand All @@ -202,20 +225,20 @@ class LedgerBridgeKeyring extends EventEmitter {
})
}

async signTypedData (withAccount, typedData) {
signTypedData (withAccount, typedData) {
throw new Error('Not supported on this device')
}

async exportAccount (address) {
exportAccount (address) {
throw new Error('Not supported on this device')
}

forgetDevice () {
this.accounts = []
this.unlocked = false
this.page = 0
this.unlockedAccount = 0
this.paths = {}
this.hdk = null
}

/* PRIVATE METHODS */
Expand Down Expand Up @@ -247,25 +270,17 @@ class LedgerBridgeKeyring extends EventEmitter {
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

const from = (this.page - 1) * this.perPage
const to = from + this.perPage

return new Promise((resolve, reject) => {
this.unlock(from)
.then( async _ => {
let accounts
if(this._isBIP44()){
accounts = await this._getAccountsBIP44(from, to)
}else{
accounts = this._getAccountsLegacy(from, to)
}
resolve(accounts)
})
Expand All @@ -275,6 +290,43 @@ class LedgerBridgeKeyring extends EventEmitter {
})
}

async _getAccountsBIP44(from, to) {
const accounts = []

for (let i = from; i < to; i++) {
const path = this._getPathForIndex(i)
const address = await this.unlock(path)
const valid = await this._hasPreviousTransactions(address)
accounts.push({
address: address,
balance: null,
index: i,
})
// PER BIP44
// "Software should prevent a creation of an account if
// a previous account does not have a transaction history
// (meaning none of its addresses have been used before)."
if(!valid){
break
}
}
return accounts
}

_getAccountsLegacy(from, to){
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
}
return accounts
}

_padLeftEven (hex) {
return hex.length % 2 !== 0 ? `0${hex}` : hex
Expand Down Expand Up @@ -307,7 +359,7 @@ class LedgerBridgeKeyring extends EventEmitter {
if (typeof index === 'undefined') {
throw new Error('Unknown address')
}
return `${this.hdPath}/${index}`
return this._getPathForIndex(index)
}

_toAscii (hex) {
Expand All @@ -324,6 +376,33 @@ class LedgerBridgeKeyring extends EventEmitter {
return str
}

_getPathForIndex(index){
// Check if the path is BIP 44 (Ledger Live)
return this._isBIP44() ? `m/44'/60'/${index}'/0/0` : `${this.hdPath}/${index}`
}

_isBIP44(){
return this.hdPath === `m/44'/60'/0'/0/0` ? true : false
}

_toLedgerPath(path){
return path.toString().replace('m/','')
}

async _hasPreviousTransactions(address) {
const apiUrl = this._getApiUrl()
const response = await fetch(`${apiUrl}/api?module=account&action=txlist&address=${address}&tag=latest&page=1&offset=1`)
const parsedResponse = await response.json()
if (parsedResponse.status!=='0' && parsedResponse.result.length > 0) {
return true
}
return false
}

_getApiUrl(){
return NETWORK_API_URLS[this.network] ? NETWORK_API_URLS[this.network] : NETWORK_API_URLS['mainnet']
}

}

LedgerBridgeKeyring.type = type
Expand Down
27 changes: 27 additions & 0 deletions test/document.shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
try {
module.exports = document || {
head: {
appendChild: _ => false,
},
createElement: _ => ({
src: false,
contentWindow: {
postMessage: _ => false,
},
}),
addEventListener: _ => false,
}
} catch (e) {
module.exports = {
head: {
appendChild: _ => false,
},
createElement: _ => ({
src: false,
contentWindow: {
postMessage: _ => false,
},
}),
addEventListener: _ => false,
}
}
Loading