From 1e0c3503e09f1430bf97b85acebfc4a17a06d6d9 Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Thu, 10 Sep 2026 13:06:45 +0000 Subject: [PATCH] core: create binds no profile either Create and import differed only in whether they bound a profile, which made the convenient one inconsistent. Neither binds now, so the store keeps one function for both, and a profile reaches an account only through bind. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX --- src/Simplex/Chat/Library/Commands.hs | 6 +++--- src/Simplex/Chat/Store/Wallets.hs | 28 +++------------------------- tests/WalletTests.hs | 13 ++++++++----- 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index ac18508ea2..29d4585689 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -58,7 +58,7 @@ import qualified Data.UUID.V4 as V4 import Simplex.Chat.Library.Subscriber import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), badgeServerCredential, maxXFTPFileSize, mkBadgeStatus, verifyCredential) import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim) -import Simplex.Chat.Store.Wallets (bindAccountIndex, createSeed, deleteSeed, getAccountIndex, getDeviceSeed, getSeedProfiles, importSeed) +import Simplex.Chat.Store.Wallets (bindAccountIndex, createSeed, deleteSeed, getAccountIndex, getDeviceSeed, getSeedProfiles) import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), accountAddress, accountSecret, deriveNameKey, importRecoveryKey, newSeed, recoveryKeyPhrase, renderNameKeyPath) import Simplex.Chat.Call import Simplex.Chat.Controller @@ -1515,8 +1515,8 @@ processChatCommand cxt nm = \case processChatCommand cxt nm APIWallet APIWalletImport phrase -> withUser $ \_ -> do entropy <- either (const $ throwCmdError "bad recovery phrase") pure $ importRecoveryKey (encodeUtf8 phrase) - imported <- withFastStore' $ \db -> importSeed db entropy - unless imported $ throwCmdError "this device already has a wallet key" + created <- withFastStore' $ \db -> createSeed db entropy + unless created $ throwCmdError "this device already has a wallet key" processChatCommand cxt nm APIWallet APIWalletExportSeedMnemonic -> withUser $ \user -> do seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure diff --git a/src/Simplex/Chat/Store/Wallets.hs b/src/Simplex/Chat/Store/Wallets.hs index 84ab251bba..e85363110e 100644 --- a/src/Simplex/Chat/Store/Wallets.hs +++ b/src/Simplex/Chat/Store/Wallets.hs @@ -9,17 +9,14 @@ module Simplex.Chat.Store.Wallets getAccountIndex, getSeedProfiles, createSeed, - importSeed, bindAccountIndex, deleteSeed, ) where -import Control.Monad (forM_) import Data.ByteString (ByteString) import Data.Int (Int64) import Data.Text (Text) -import Simplex.Chat.Store.Shared (insertedRowId) import Simplex.Chat.Types (User (..)) import Simplex.Chat.Wallet (AccountIndex, SeedId (..), WalletSeed (..)) import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow) @@ -70,26 +67,13 @@ bindUser db uId (SeedId sId) acct = "UPDATE users SET wallet_seed_id = ?, wallet_account_index = ? WHERE user_id = ?" (sId, acct, uId) --- | False if the device already has a key. Every profile is bound, as a new --- seed has no account that already owns a name. +-- | False if the device already has a key. No profile is bound, as which +-- account a profile takes is said with bind. createSeed :: DB.Connection -> ByteString -> IO Bool createSeed db entropy = getDeviceSeed db >>= \case Just _ -> pure False - Nothing -> do - s <- createWalletSeed db entropy - uIds <- map fromOnly <$> DB.query_ db "SELECT user_id FROM users ORDER BY user_id" - forM_ (zip uIds [0 ..]) $ \(uId, acct) -> bindUser db uId (wsId s) acct - setNextAccountIndex db (wsId s) (fromIntegral $ length uIds) - pure True - --- | False if the device already has a key. No profile is bound: which account --- a profile had is what the import is recovering, and the seed does not say. -importSeed :: DB.Connection -> ByteString -> IO Bool -importSeed db entropy = - getDeviceSeed db >>= \case - Just _ -> pure False - Nothing -> True <$ createWalletSeed db entropy + Nothing -> True <$ DB.execute db "INSERT INTO wallet_seeds (seed) VALUES (?)" (Only entropy) -- | Without an account the next free one is taken. False if another profile -- holds the account asked for. @@ -111,12 +95,6 @@ bindAccountIndex db User {userId} sId@(SeedId sId') = \case setNextAccountIndex db sId (fromIntegral acct + 1) pure True -createWalletSeed :: DB.Connection -> ByteString -> IO WalletSeed -createWalletSeed db entropy = do - DB.execute db "INSERT INTO wallet_seeds (seed) VALUES (?)" (Only entropy) - sId <- insertedRowId db - pure WalletSeed {wsId = SeedId sId, wsEntropy = entropy} - -- | Incremented in SQL, so two profiles cannot be handed the same account. takeAccountIndex :: DB.Connection -> SeedId -> IO Int64 takeAccountIndex db (SeedId sId) = do diff --git a/tests/WalletTests.hs b/tests/WalletTests.hs index a6eabf4b19..6aed5410a6 100644 --- a/tests/WalletTests.hs +++ b/tests/WalletTests.hs @@ -70,19 +70,20 @@ testWalletCreate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice <## "no wallet key" alice ##> "/_wallet export" alice <## "bad chat command: no wallet key on this device" - alice ##> "/create user alisa" - showActiveUser alice "alisa" - -- a new seed has no account that owns a name, so every profile is bound + -- creating the seed binds no profile to an account alice ##> "/_wallet create" + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind" rows <- nameRows alice - alice <## "also on same seed: alice" - map fst rows `shouldBe` ["m/44'/60'/1'/0/0", "m/44'/60'/1'/0/1"] + map fst rows `shouldBe` ["m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1"] length (nub $ map snd rows) `shouldBe` 2 testWalletPersists :: HasCallStack => TestParams -> IO () testWalletPersists ps = do rows <- withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/_wallet create" + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind" nameRows alice -- same database, new session: a name bought at that address must stay reachable withTestChat ps "alice" $ \alice -> do @@ -93,6 +94,8 @@ testWalletPersists ps = do testWalletSecondProfile :: HasCallStack => TestParams -> IO () testWalletSecondProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/_wallet create" + alice <## "this profile has no wallet key" + alice ##> "/_wallet bind" rows <- nameRows alice alice ##> "/_wallet export" phrase <- getTermLine alice