From 22987046875e2a7e40ef3c1d3cba89879b437581 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Thu, 10 Sep 2026 15:19:45 +0000 Subject: [PATCH] core: ethereumPath takes the address index The BIP-44 path is the same for every address under an account, so the caller that wants one names it, rather than rebuilding the path around the one this hard-coded. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX --- src/Simplex/Messaging/Eth/Address.hs | 8 +++++--- tests/CoreTests/EthCryptoTests.hs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Simplex/Messaging/Eth/Address.hs b/src/Simplex/Messaging/Eth/Address.hs index 005d5f8f3..8acb5f9e1 100644 --- a/src/Simplex/Messaging/Eth/Address.hs +++ b/src/Simplex/Messaging/Eth/Address.hs @@ -108,9 +108,11 @@ parseAddress s letters = filter (not . isDigit) bodyC mixedCase = any isUpper letters && any isLower letters --- | BIP-44 path for Ethereum account @i@: @m\/44'\/60'\/i'\/0\/0@. -ethereumPath :: Word32 -> [Word32] -ethereumPath account = [hardened 44, hardened 60, hardened account, 0, 0] +-- | BIP-44 path for Ethereum account @i@, address @k@: @m\/44'\/60'\/i'\/0\/k@. +-- The account must be below 'hardenedOffset', as 'hardened' returns anything +-- at or above it unchanged and the path would be another account's. +ethereumPath :: Word32 -> Word32 -> [Word32] +ethereumPath account address = [hardened 44, hardened 60, hardened account, 0, address] -- Hex via memory's Base16, which this package already depends on and which -- Crypto.Secp256k1 already uses. Base16 emits lowercase, which is what EIP-55 diff --git a/tests/CoreTests/EthCryptoTests.hs b/tests/CoreTests/EthCryptoTests.hs index 47f53e7a4..0f83b9e8b 100644 --- a/tests/CoreTests/EthCryptoTests.hs +++ b/tests/CoreTests/EthCryptoTests.hs @@ -179,7 +179,7 @@ bip32Tests = do it "renders a path" $ B32.renderPath [hardened' 44, hardened' 60, hardened' 0, 0, 0] `shouldBe` "m/44'/60'/0'/0/0" it "round-trips render and parse" $ - B32.parsePath (B32.renderPath (ethereumPath 7)) `shouldBe` Right (ethereumPath 7) + B32.parsePath (B32.renderPath (ethereumPath 7 3)) `shouldBe` Right (ethereumPath 7 3) it "rejects a non-numeric component" $ B32.parsePath "m/44x/60" `shouldSatisfy` isLeft it "rejects an index at the hardened boundary" $ @@ -239,7 +239,7 @@ derivationTests = do m = right $ B39.parseMnemonic canonicalPhrase seed = B39.mnemonicToSeed m "" master = right $ B32.masterKey seed - addrAt i = addressFromPrivateKey . B32.xkKey . right $ B32.derivePath master (ethereumPath i) + addrAt i = addressFromPrivateKey . B32.xkKey . right $ B32.derivePath master (ethereumPath i 0) dedup a as = if a `elem` as then as else a : as eip55Tests :: Spec