Skip to content

Commit

Permalink
Add module and tests for BIP44 wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
ptzianos committed Oct 16, 2019
1 parent d6a1dba commit 1e5e622
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
105 changes: 105 additions & 0 deletions android-client/app/src/main/java/org/hermes/hd/BIP44.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.hermes.hd

import org.hermes.utils.toTritArray
import java.lang.Exception

class BIP44Wallet(seed: ByteArray, keyRegistry: KeyRegistry = InMemoryKeyRegistry()) {

class InvalidCoinType(): Exception()

/**
* ExtendedWallet is a class that extends the standard BIP32 compliant wallet so
* that it becomes compliant with BIP44.
*
* Specifically, it injects different keys in some paths according to the specification
* in SLIP44.
* The ExtendedWallet is seeded once and can then be re-used for multiple different
* currencies from the same application.
*/
class ExtendedWallet(seed: ByteArray, private val keyRegistry: KeyRegistry = InMemoryKeyRegistry()):
Wallet(seed, keyRegistry)
{

companion object {
val BIP44Path = BIP32.normalizeToStr("m/44'") // "m/2147483692" // m/44'
}

/**
* The get method will intercept all calls and will make sure to inject the correct keys
* wherever necessary.
*
* The implementation is lazy, meaning that the keys will be injected only when requested.
* It does not force the user to use just the BIP44 related paths.
*/
override operator fun get(path: String): BIP32Key {
BIP32.verify(path)
val normalizedPath = BIP32.normalize(path).drop(1)
val normalizedPathStr = BIP32.normalizeToStr(path)
if (normalizedPathStr.startsWith(BIP44Path) and (normalizedPath.size > 1)) {
val coinIndex = normalizedPath[1]
val coinEntry = SLIP44.byWalletIndex[coinIndex] ?: throw InvalidCoinType()
val childPath = BIP32.normalizeToStr("$BIP44Path/${coinEntry.walletIndex}")
// If the special paths have not been set, then put them in place
if ((keyRegistry.get(BIP44Path) == null) or (keyRegistry.get(childPath) == null)) {
val bip44Key = super.get(BIP44Path)
val childKey = if (coinEntry.symbol == "IOTA") {
// Special keys for IOTA
val (chainCode, key) = IOTAExPrivKey.CKDpriv(
coinEntry.walletIndex,
(bip44Key as ExPrivKey).value.toByteArray(),
bip44Key.public.encoded,
bip44Key.chainCode)
IOTAExPrivKey(childPath, bip44Key, chainCode, key.toTritArray().toTryteArray())
}
else {
// Standard ExPrivKeys
val encoder = when (coinEntry.symbol) {
"ETH" -> ETHKeyEncoder
else -> BTCKeyEncoder
}
val (chainCode, key) = ExPrivKey.CKDPriv(
coinEntry.walletIndex,
(bip44Key as ExPrivKey).value.toByteArray(),
(bip44Key as ExPrivKey).public.encoded,
bip44Key.chainCode)
ExPrivKey(childPath, bip44Key, chainCode, key, encoder)
}
keyRegistry.put(childPath, childKey)
}
}
// One the keys have been put in place, proceed as normal
return super.get(path)
}
}

class Context(val coinType: String, val account: Int, val change: Int = 0, private val wallet: Wallet) {

private val coinPath = BIP32.normalizeToStr("m/44'/${SLIP44.bySymbol[coinType]!!.walletIndex}")
private val accountPath = "$account'/$change"
private val internalPath = "$coinPath/$accountPath"

operator fun get(index: Long): BIP32Key {
if (index >= BIP32.HARDENED_KEY_OFFSET)
throw BIP32Key.InvalidIndex()
return wallet["$internalPath/$index"]
}

operator fun get(path: String): BIP32Key {
return wallet["$internalPath/$path"]
}

init {
if (!SLIP44.bySymbol.containsKey(coinType))
throw InvalidCoinType()
}
}

// Be careful accessing the wallet this way because you might cause some paths to be initialised.
val masterWallet: Wallet = ExtendedWallet(seed, keyRegistry = keyRegistry)

fun getWalletForCoin(coinType: String, account: Int, change: Int = 0): Context =
Context(coinType, account, change, masterWallet).apply {
// Use this to initialize the path
get(0L)
}
}
26 changes: 26 additions & 0 deletions android-client/app/src/test/java/org/hermes/hd/BIP44WalletTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.hermes.hd

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*

internal class BIP44WalletTest {

@Test
fun getWallet() {
val words = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split(' ')
val wallet = BIP44Wallet(seed = Mnemonic.fromWordList(words.toTypedArray()).seed)
val btcWallet = wallet.getWalletForCoin("BTC", 0, 0)
val ethWallet = wallet.getWalletForCoin("ETH", 0, 0)
val iotaWallet = wallet.getWalletForCoin("IOTA", 0, 0)
assertTrue(wallet.masterWallet["m"] is ExPrivKey)
assertTrue(wallet.masterWallet["m/44'"] is ExPrivKey)
assertTrue(wallet.masterWallet["m/44'/${SLIP44.bySymbol["BTC"]!!.walletIndex}/0'/0"] is ExPrivKey)
assertTrue(wallet.masterWallet["m/44'/${SLIP44.bySymbol["ETH"]!!.walletIndex}/0'/0"] is ExPrivKey)
assertTrue(wallet.masterWallet["m/44'/${SLIP44.bySymbol["IOTA"]!!.walletIndex}/0'/0"] is IOTAExPrivKey)
assertTrue(iotaWallet[0] is IOTAExPrivKey)
assertArrayEquals(
(iotaWallet[0] as IOTAExPrivKey).value.toByteArray(),
(wallet.masterWallet["m/44'/${SLIP44.bySymbol["IOTA"]!!.walletIndex}/0'/0/0"] as IOTAExPrivKey).value.toByteArray()
)
}
}

0 comments on commit 1e5e622

Please sign in to comment.