Files
flamenet-messenger-android/engine/src/test/kotlin/io/flamenet/e2e/HdAccountTest.kt
T
Paul HittandClaude Fable 5.1 2fa430f21c API-style comments and ktlint formatting
Every comment is reduced to its first sentence plus @param, @return
and @throws tags. Comment lines: 20040 to 8526. ktlint 1.8 applied
with its default (official Kotlin) style across all modules; 262 files
reformatted. The 264 remaining ktlint findings are naming and filename
rules, which are code changes, and are left alone.

Comments and formatting only. 1208 tests pass, counted from the XML.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 20:02:07 -04:00

176 lines
6.2 KiB
Kotlin

package io.flamenet.e2e
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
import kotlin.test.assertTrue
/**
* From a recovery phrase to an address somebody can be paid at.
*/
class HdAccountTest {
private val words by lazy { Bip39.englishWordlist() }
private val phrase by lazy { Bip39.phrase(ByteArray(16), words) }
private val seed by lazy { Bip39.seed(phrase) }
private fun account() = HdAccount(seed)
// ---- The path --------------------------------------------------------.
/**
* BIP-44 with every level hardened, which is what this curve requires and
* what every other Solana wallet uses.
*/
@Test
fun `the derivation path is the standard Solana shape`() {
assertEquals("m/44H/501H/0H/0H/0H", account().path(0))
assertEquals("m/44H/501H/0H/0H/7H", account().path(7))
assertEquals("m/44H/501H/3H/0H/0H", HdAccount(seed, account = 3).path(0))
}
// ---- The property that guards the money ------------------------------.
/**
* The signing key owns the address that was displayed.
*/
@Test
fun `the signing key owns the address that was shown`() {
val account = account()
for (index in 0L..4L) {
val shown = account.receive(index)
val signing = account.signingSeed(index)
val fromSigningKey = Base58.encode(Ed25519.fromSeed(signing).publicKey)
assertEquals(
shown.address,
fromSigningKey,
"index $index would sign with a key that does not own its address",
)
}
}
/** And that key can actually sign, the engine carries RFC 8032 itself. */
@Test
fun `a derived key can sign and the signature verifies under its address`() {
val account = account()
val shown = account.receive(0)
val seedAt0 = account.signingSeed(0)
val message = "a transaction".toByteArray()
val signature = Ed25519.sign(seedAt0, message)
val publicKey = Base58.decode(shown.address)!!
// `(publicKey, signature, message)`.
assertTrue(
Ed25519.verify(publicKey, signature, message),
"the signature does not verify under the address it was made for",
)
}
// ---- Fresh addresses -------------------------------------------------.
/**
* A new address per receive is the point.
*/
@Test
fun `every index gives a different address`() {
val account = account()
val addresses = (0L..9L).map { account.receive(it).address }
assertEquals(10, addresses.toSet().size, "two indices produced one address")
}
@Test
fun `next advances the counter and never repeats`() {
var account = account()
val seen = mutableListOf<String>()
repeat(5) {
val (receive, advanced) = account.next()
seen += receive.address
account = advanced
}
assertEquals(5, seen.toSet().size)
assertEquals(5L, account.nextIndex)
}
/** Different accounts under one phrase are different wallets. */
@Test
fun `account numbers separate`() {
assertNotEquals(
HdAccount(seed, account = 0).receive(0).address,
HdAccount(seed, account = 1).receive(0).address,
)
}
// ---- Restoring -------------------------------------------------------.
/**
* The whole promise of a recovery phrase: the same words give back the same
* addresses, on any device and any implementation.
*/
@Test
fun `the same phrase restores the same addresses`() {
val restored = HdAccount(Bip39.seed(Bip39.phrase(ByteArray(16), words)))
assertEquals(account().receive(0).address, restored.receive(0).address)
assertEquals(account().receive(3).address, restored.receive(3).address)
}
/** A different phrase is a different wallet, not a near-miss. */
@Test
fun `a different phrase gives entirely different addresses`() {
val other = HdAccount(Bip39.seed(Bip39.phrase(ByteArray(16) { 1 }, words)))
assertNotEquals(account().receive(0).address, other.receive(0).address)
}
// ---- What an address is, and is not ----------------------------------.
/** An address is Base58 of 32 bytes, so it decodes back to the key. */
@Test
fun `an address decodes back to a 32-byte key`() {
val shown = account().receive(0)
val decoded = Base58.decode(shown.address)
assertContentEquals(
Ed25519.fromSeed(account().signingSeed(0)).publicKey,
decoded,
)
assertEquals(32, decoded!!.size)
}
@Test
fun `a well-formed address is recognised`() {
assertTrue(HdAccount.looksLikeAddress(account().receive(0).address))
}
@Test
fun `something that is not an address is rejected`() {
assertFalse(HdAccount.looksLikeAddress(""))
assertFalse(HdAccount.looksLikeAddress("not an address"))
// Right alphabet, wrong length.
assertFalse(HdAccount.looksLikeAddress("abc"))
// Characters the alphabet excludes so a hand-copied address cannot
// become a different valid one.
assertFalse(HdAccount.looksLikeAddress("0OIl0OIl0OIl0OIl0OIl0OIl0OIl0OIl"))
}
/**
* The limit, asserted as a passing test so nobody reads `looksLikeAddress`
* as stronger than it is.
*/
@Test
fun `a corrupted address can still look valid, and that is not fixable here`() {
val real = account().receive(0).address
// Walk the alphabet at one position until the result still decodes to
// 32 bytes.
val nearMiss =
Base58.ALPHABET
.asSequence()
.map { real.take(3) + it + real.drop(4) }
.firstOrNull { it != real && HdAccount.looksLikeAddress(it) }
assertTrue(
nearMiss != null,
"no single-character change produced a still-valid address, if this " +
"ever becomes true the format has gained a checksum, and the " +
"warnings written around typed addresses can be revisited",
)
assertNotEquals(real, nearMiss)
}
}