Skip to content
Closed
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
6 changes: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ dependencies {
// Isolated Projects forbids. The module lists come from `settings.gradle.kts`; the
// task depends on each module's test task by *path* — a lazy, cross-project-safe
// dependency that never configures the other project at configuration time.
// Android modules expose `testDebugUnitTest`; pure-JVM modules expose `test`.
// Android modules expose `testDebugUnitTest`; KMP modules expose `testAndroidHostTest`;
// pure-JVM modules expose `test`.
@Suppress("UNCHECKED_CAST")
val androidUnitTestModules = rootProject.extra["flipcash.androidUnitTestModules"] as List<String>
@Suppress("UNCHECKED_CAST")
val jvmUnitTestModules = rootProject.extra["flipcash.jvmUnitTestModules"] as List<String>
@Suppress("UNCHECKED_CAST")
val kmpUnitTestModules = rootProject.extra["flipcash.kmpUnitTestModules"] as List<String>

tasks.register("flipcashTestDebug") {
description = "Run testDebug for all Flipcash modules"
dependsOn(androidUnitTestModules.map { "$it:testDebugUnitTest" })
dependsOn(jvmUnitTestModules.map { "$it:test" })
dependsOn(kmpUnitTestModules.map { "$it:testAndroidHostTest" })
}
2 changes: 2 additions & 0 deletions kmp/shared-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ kotlin {
baseName = "SharedCore"
isStatic = true
export(project(":libs:encryption:base58"))
export(project(":libs:encryption:derivepath"))
}
}

sourceSets {
commonMain {
dependencies {
api(project(":libs:encryption:base58"))
api(project(":libs:encryption:derivepath"))
}
}
}
Expand Down
28 changes: 28 additions & 0 deletions libs/encryption/derivepath/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
}

kotlin {
android {
namespace = "com.getcode.encryption.derivepath"
compileSdk = 37
minSdk = 29
withHostTest {}
}

iosArm64()
iosSimulatorArm64()
iosX64()

sourceSets {
commonMain {
// Pure Kotlin — no external dependencies needed.
}
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.getcode.crypt

/// Represents a BIP-44/SLIP-10 hierarchical derivation path (e.g. `m/44'/501'/0'/0'`).
class DerivePath(val indexes: List<Index>, val password: String? = null) {
/// Returns the canonical string form of the path (e.g. `m/44'/501'/0'/0'`).
fun stringRepresentation(): String {
val components = indexes.joinToString(separator) { it.stringRepresentation() }
return "$identifier$separator$components"
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DerivePath) return false
if (indexes != other.indexes) return false
return true
}

override fun hashCode(): Int {
return indexes.hashCode()
}

/// A single index component of a derivation path, optionally hardened.
data class Index(val value: Int, val hardened: Boolean) {
/// Returns the string form of this index (e.g. `0'` for a hardened index, `0` otherwise).
fun stringRepresentation(): String =
value.toString().let { if (hardened) "$it$hardener" else it }
}

companion object {
/// Parses a derivation path string (e.g. `m/44'/501'/0'/0'`) into a `DerivePath`, or returns `null` if invalid.
fun newInstance(string: String, password: String? = null): DerivePath? {
val strings = string.split(separator)
if (strings.firstOrNull() != identifier) return null
val indexStrings = strings.drop(1)

val indexes: List<Index> = indexStrings.map { s ->
val hardened = s.contains(hardener)
val value = s.replace(hardener, "").toIntOrNull() ?: return@map null
Index(value, hardened)
}.filterNotNull()

if (indexes.size != indexStrings.count()) return null

return DerivePath(indexes, password)
}

// Primary - m/44'/501'/0'/0'
//
// Incoming - m/44'/501'/0'/0'/i'/2
// Outgoing - m/44'/501'/0'/0'/i'/3
//
// Relationship - m/44'/501'/0'/0'/0'/0
// Swap - m/44'/501'/0'/0'/1'/0
//
// Bucket1 - m/44'/501'/0'/0'/0'/1
// Bucket10 - m/44'/501'/0'/0'/0'/10
// Bucket100 - m/44'/501'/0'/0'/0'/100
// Bucket1k - m/44'/501'/0'/0'/0'/1000
// Bucket10k - m/44'/501'/0'/0'/0'/10000
// Bucket100k - m/44'/501'/0'/0'/0'/100000
// Bucket1m - m/44'/501'/0'/0'/0'/1000000

val bucket1 = newInstance("m/44'/501'/0'/0'/0'/1")!!
val bucket10 = newInstance("m/44'/501'/0'/0'/0'/10")!!
val bucket100 = newInstance("m/44'/501'/0'/0'/0'/100")!!
val bucket1k = newInstance("m/44'/501'/0'/0'/0'/1000")!!
val bucket10k = newInstance("m/44'/501'/0'/0'/0'/10000")!!
val bucket100k = newInstance("m/44'/501'/0'/0'/0'/100000")!!
val bucket1m = newInstance("m/44'/501'/0'/0'/0'/1000000")!!
val primary = newInstance("m/44'/501'/0'/0'")!!
val swap = newInstance("m/44'/501'/0'/0'/1'/0")!!

/// Returns the derivation path for an incoming bucket at the given index.
fun getBucketIncoming(index: Int): DerivePath =
newInstance("m/44'/501'/0'/0'/$index'/2")!!

/// Returns the derivation path for an outgoing bucket at the given index.
fun getBucketOutgoing(index: Int): DerivePath =
newInstance("m/44'/501'/0'/0'/$index'/3")!!

/// Returns the derivation path for a pool at the given index.
fun getPool(index: Long): DerivePath =
newInstance("m/44'/501'/0'/0'/7665'/$index'")!!

/// Returns the derivation path for a pool rendezvous key at the given index.
fun getPoolRendezvous(index: Long): DerivePath =
newInstance("m/44'/501'/0'/0'/2335'/$index'")!!

/// Returns the relationship derivation path using the given domain host string as the password.
fun relationship(host: String): DerivePath =
newInstance("m/44'/501'/0'/0'/0'/0", password = host)!!

private const val identifier = "m"
private const val separator = "/"
private const val hardener = "'"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,5 +132,18 @@ class DerivePathTest {
assertEquals(a, b)
}

@Test
fun relationshipUsesHostAsPassword() {
val path = DerivePath.relationship("example.com")
assertNotNull(path)
assertEquals("m/44'/501'/0'/0'/0'/0", path.stringRepresentation())
assertEquals("example.com", path.password)
}

@Test
fun primaryPathString() {
assertEquals("m/44'/501'/0'/0'", DerivePath.primary.stringRepresentation())
}

private fun assertFalse(value: Boolean) = kotlin.test.assertFalse(value)
}
1 change: 1 addition & 0 deletions libs/encryption/mnemonic/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ android {

dependencies {
implementation(project(":libs:encryption:base58"))
api(project(":libs:encryption:derivepath"))
implementation(project(":libs:encryption:ed25519"))
implementation(project(":libs:encryption:hmac"))
implementation(project(":libs:encryption:sha256"))
Expand Down

This file was deleted.

This file was deleted.

49 changes: 38 additions & 11 deletions libs/encryption/utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
plugins {
alias(libs.plugins.flipcash.android.library)
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
}

android {
namespace = "${Gradle.codeNamespace}.encryption.utils"
}
kotlin {
android {
namespace = "com.getcode.encryption.utils"
compileSdk = 37
minSdk = 29
withHostTest {}
}

dependencies {
implementation(project(":libs:encryption:base58"))
implementation(libs.protobuf.kotlin.lite)
implementation(project(":libs:encryption:ed25519"))
implementation(libs.bundles.kotlinx.serialization)
iosArm64()
iosSimulatorArm64()
iosX64()

testImplementation(kotlin("test"))
testImplementation(libs.robolectric)
sourceSets {
commonMain {
dependencies {
implementation(project(":libs:encryption:base58"))
}
}
androidMain {
dependencies {
implementation(project(":libs:encryption:ed25519"))
implementation(project(":libs:logging"))
implementation(libs.protobuf.kotlin.lite)
implementation(libs.bundles.kotlinx.serialization)
}
}
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
getByName("androidHostTest") {
dependencies {
implementation(kotlin("test"))
implementation(libs.robolectric)
}
}
}
}
Loading
Loading