diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index f80b1a23ca..08aca5dbf3 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 (deleteSeed, getBoundAccount, getDeviceSeed, getOrCreateAccountRef, getSeedAccounts, importSeed) +import Simplex.Chat.Store.Wallets (deleteSeed, getDeviceSeed, getOrCreateAccountRef, getSeedAccounts, importSeed) import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), accountAddress, deriveNameKey, importRecoveryKey, newSeed, recoveryKeyPhrase, renderNameKeyPath) import Simplex.Chat.Call import Simplex.Chat.Controller @@ -1496,7 +1496,8 @@ processChatCommand cxt nm = \case accs <- case seed_ of Nothing -> pure [] Just seed -> do - -- hidden profiles are left out, as they are by /users + -- hidden profiles are left out, as they are by /users, but the gap in + -- account indexes still shows that one exists as <- filter (\(_, _, active, hidden) -> active || not hidden) <$> withFastStore' (\db -> getSeedAccounts db (wsId seed)) forM as $ \(n, acct, active, _) -> do keys <- forM [0 .. walletNamesShown - 1] $ \k -> do @@ -1515,17 +1516,17 @@ processChatCommand cxt nm = \case when (isNothing r) $ throwCmdError "this device already has a wallet key" processChatCommand cxt nm APIWallet APIWalletExport -> withUser $ \user -> do - (seed, _) <- withFastStore' (\db -> getBoundAccount db user) >>= maybe (throwCmdError noKeyError) pure + seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure phrase <- either (throwCmdError . ("wallet: " <>)) pure $ recoveryKeyPhrase seed pure $ CRWalletPhrase user (safeDecodeUtf8 phrase) APIWalletDelete confirmWord -> withUser $ \user -> do seed <- withFastStore' getDeviceSeed >>= maybe (throwCmdError noKeyError) pure phrase <- either (throwCmdError . ("wallet: " <>)) pure $ recoveryKeyPhrase seed case reverse . T.words $ safeDecodeUtf8 phrase of - w : _ | w == confirmWord -> do + w : _ | w == T.toLower confirmWord -> do withFastStore' $ \db -> deleteSeed db (wsId seed) processChatCommand cxt nm APIWallet - _ -> throwCmdError "to confirm, pass the last word of the recovery phrase" + _ -> throwCmdError "this deletes the wallet key for all profiles on this device, to confirm pass the last word of the recovery phrase" APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call ct <- withFastStore $ \db -> getContact db cxt user contactId @@ -5467,7 +5468,7 @@ walletNamesShown :: NameIndex walletNamesShown = 2 noKeyError :: String -noKeyError = "no wallet key for this profile" +noKeyError = "no wallet key on this device" chatCommandP :: Parser ChatCommand chatCommandP = diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs index fcd14ee593..4e7235ff40 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20260908_wallet_seeds.hs @@ -12,7 +12,8 @@ CREATE TABLE wallet_seeds ( wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT, seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes -- known issue: after import this starts at 0, so a recovered device can - -- re-issue an account that already owns names + -- re-issue an account that already owns names, and profiles are bound in + -- the order they ask for a key, not the order they had next_account_index INTEGER NOT NULL DEFAULT 0, -- one key per device for now single_seed INTEGER NOT NULL DEFAULT 1 UNIQUE diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index aa8adc9714..7a3b18af4d 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -858,7 +858,8 @@ CREATE TABLE wallet_seeds( wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT, seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes -- known issue: after import this starts at 0, so a recovered device can - -- re-issue an account that already owns names + -- re-issue an account that already owns names, and profiles are bound in + -- the order they ask for a key, not the order they had next_account_index INTEGER NOT NULL DEFAULT 0, -- one key per device for now single_seed INTEGER NOT NULL DEFAULT 1 UNIQUE diff --git a/src/Simplex/Chat/Store/Wallets.hs b/src/Simplex/Chat/Store/Wallets.hs index 90ddf96653..b7a41a1e79 100644 --- a/src/Simplex/Chat/Store/Wallets.hs +++ b/src/Simplex/Chat/Store/Wallets.hs @@ -6,7 +6,6 @@ module Simplex.Chat.Store.Wallets ( getDeviceSeed, - getBoundAccount, getSeedAccounts, getOrCreateAccountRef, importSeed, diff --git a/tests/WalletTests.hs b/tests/WalletTests.hs index 644b3cea3e..3f90ea8852 100644 --- a/tests/WalletTests.hs +++ b/tests/WalletTests.hs @@ -65,7 +65,7 @@ testWalletCreate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/_wallet" alice <## "no wallet key" alice ##> "/_wallet export" - alice <## "bad chat command: no wallet key for this profile" + alice <## "bad chat command: no wallet key on this device" alice ##> "/_wallet create" rows <- accountRows alice "alice, active" 0 map fst rows `shouldBe` ["m/44'/60'/0'/0/0", "m/44'/60'/0'/0/1"] @@ -86,11 +86,16 @@ testWalletSecondProfile :: HasCallStack => TestParams -> IO () testWalletSecondProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> "/_wallet create" rows <- accountRows alice "alice, active" 0 + alice ##> "/_wallet export" + phrase <- getTermLine alice alice ##> "/create user alisa" showActiveUser alice "alisa" alice ##> "/_wallet" _ <- accountRows alice "alice" 0 alice <## "this profile has no wallet key" + -- the key belongs to the device, so a profile without an account exports it too + alice ##> "/_wallet export" + alice <## phrase alice ##> "/_wallet create" _ <- accountRows alice "alice" 0 rows' <- accountRows alice "alisa, active" 1 @@ -115,8 +120,8 @@ testWalletDelete ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do alice ##> ("/_wallet import " <> B.unpack testPhrase) _ <- accountRows alice "alice, active" 0 alice ##> "/_wallet delete abandon" - alice <## "bad chat command: to confirm, pass the last word of the recovery phrase" - alice ##> "/_wallet delete about" + alice <## "bad chat command: this deletes the wallet key for all profiles on this device, to confirm pass the last word of the recovery phrase" + alice ##> "/_wallet delete About" alice <## "no wallet key" -- deleting unbinds the profile, so a key can be imported again alice ##> ("/_wallet import " <> B.unpack testPhrase)