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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX
This commit is contained in:
Alain Brenzikofer
2026-09-10 13:06:45 +00:00
co-authored by Claude Opus 5
parent c5b44fe848
commit 1e0c3503e0
3 changed files with 14 additions and 33 deletions
+3 -3
View File
@@ -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
+3 -25
View File
@@ -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
+8 -5
View File
@@ -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