list all names owned

This commit is contained in:
Alain Brenzikofer committed 2026-09-23 15:46:01 +02:00
1 parent 2f92c6b7ae
commit 6de3e28949
8 files changed
+57 -8

No files matched your search

+1
View File
@@ -370,6 +370,7 @@ undocumentedCommands =
"APICreateWallet",
"APIDeleteChatTag",
"APIDeleteWallet",
"APIGetWalletNames",
"APIScanWallet",
"APIDeleteMemberSupportChat",
"APIDeleteReceivedReports",
+1
View File
@@ -219,5 +219,6 @@ undocumentedResponses =
"CRWalletAccountSecret",
"CRWalletAddress",
"CRWalletMnemonic",
"CRWalletNames",
"CRWelcome"
]
+3 -1
View File
@@ -68,7 +68,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Wallet (AccountIndex, WalletAddress, WalletError)
import Simplex.Chat.Wallet (AccountIndex, WalletAddress, WalletError, WalletName)
import Simplex.Chat.Util (liftIOEither)
import Simplex.FileTransfer.Description (FileDescriptionURI)
import Simplex.Messaging.Server.Information (ServerPublicInfo)
@@ -444,6 +444,7 @@ data ChatCommand
| APIExportWalletMnemonic
| APIExportWalletAccount {accountIndex :: AccountIndex}
| APIScanWallet
| APIGetWalletNames
| APIDeleteWallet
| APISendCallInvitation ContactId CallType
| SendCallInvitation ContactName CallType
@@ -883,6 +884,7 @@ data ChatResponse
| CRWallet {user :: User, accountIndexes_ :: Maybe [AccountIndex]}
| CRWalletMnemonic {user :: User, mnemonic :: Text}
| CRWalletAddress {user :: User, walletAddress :: WalletAddress}
| CRWalletNames {user :: User, walletNames :: [WalletName]}
| CRWalletAccountSecret {user :: User, walletAddress :: WalletAddress, secret :: Text}
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool, msgSigned :: Bool}
+4 -1
View File
@@ -65,7 +65,7 @@ import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind (..), BadgeSta
import Simplex.Chat.Badges.Code (badgeCodeText, parseBadgeCode)
import Simplex.Chat.Badges.Service (BadgeBalance (..), BadgeServiceCommand (..), BadgeServiceErrorCode (..), BadgeServiceRequest (..), BadgeServiceResponse (..), BadgeStatement (..), StatementDebitType (..), StatementEntry (..), StatementEntryType (..), currentBadgeServiceVersion)
import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim)
import Simplex.Chat.Store.Wallets (WalletSeed (..), accountHeldByOther, bindAccount, createWalletSeed, deleteWalletSeed, getUserAccounts, getWalletSeed, recordScan, resolveAccount)
import Simplex.Chat.Store.Wallets (WalletSeed (..), accountHeldByOther, bindAccount, createWalletSeed, deleteWalletSeed, getUserAccounts, getUserNames, getWalletSeed, recordScan, resolveAccount)
import Simplex.Chat.Wallet (AccountIndex, AccountKey, WalletAddress (..), WalletError (..), accountSecret, deriveAccountKey, entropyFromMnemonic, newSeedEntropy, renderAccountPath, scanGapLimit, scanMaxAccounts, seedMaster, seedMnemonic)
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
import Simplex.Messaging.Names.Record (NameResponse, OwnedNames (..))
@@ -1527,6 +1527,8 @@ processChatCommand cxt nm = \case
when heldByOther $ throwWalletError WEAccountBound
k <- accountKey seed n
pure $ CRWalletAccountSecret user (accountAddress n k) (accountSecret k)
APIGetWalletNames -> withUser $ \user@User {userId} ->
CRWalletNames user <$> withFastStore' (\db -> getWalletSeed db >>= maybe (pure []) (\WalletSeed {wsId} -> getUserNames db wsId userId))
APIScanWallet -> withUser $ \user@User {userId, viewPwdHash} -> do
when (isJust viewPwdHash) $ throwWalletError WEHiddenProfile
seed <- walletSeed
@@ -6119,6 +6121,7 @@ chatCommandP =
"/_wallet address" $> APIGetWalletAddress Nothing,
"/_wallet export master" $> APIExportWalletMnemonic,
"/_wallet export account " *> (APIExportWalletAccount <$> accountIndexP),
"/_wallet names" $> APIGetWalletNames,
"/_wallet scan" $> APIScanWallet,
"/_wallet delete" $> APIDeleteWallet,
"/_wallet" $> APIGetWallet,
+18 -1
View File
@@ -14,6 +14,7 @@ module Simplex.Chat.Store.Wallets
deleteWalletSeed,
resolveAccount,
getUserAccounts,
getUserNames,
accountHeldByOther,
bindAccount,
recordScan,
@@ -26,7 +27,7 @@ import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import Data.Int (Int64)
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), checkAccountIndex)
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), WalletName (..), checkAccountIndex)
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Names.Record (NameRecord (..), NameRegistration (..), NameResponse (..))
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
@@ -107,6 +108,22 @@ getUserAccounts db sId userId =
|]
(sId, userId)
-- | The names the profile's accounts hold, as the last scan saw them.
getUserNames :: DB.Connection -> SeedId -> UserId -> IO [WalletName]
getUserNames db sId userId =
map toName
<$> DB.query
db
[sql|
SELECT n.account_index, n.name FROM wallet_owned_names n
JOIN wallet_accounts a ON a.wallet_seed_id = n.wallet_seed_id AND a.account_index = n.account_index
WHERE n.wallet_seed_id = ? AND a.user_id = ?
ORDER BY n.name
|]
(sId, userId)
where
toName (n, name) = WalletName {accountIndex = fromIntegral @Int64 n, name}
-- | Which profile holds an account: 'Nothing' when it is unknown, @Just Nothing@ when no profile holds it.
accountUser :: DB.Connection -> SeedId -> AccountIndex -> IO (Maybe (Maybe Int64))
accountUser db sId n =
+5 -1
View File
@@ -60,7 +60,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Wallet (WalletAddress (..), WalletError (..))
import Simplex.Chat.Wallet (WalletAddress (..), WalletError (..), WalletName (..))
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent (DatabaseDiff (..))
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
@@ -202,6 +202,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
Just accounts -> [plain $ "accounts: " <> T.intercalate ", " (map tshow accounts)]
CRWalletMnemonic u mnemonic -> ttyUser u [plain mnemonic]
CRWalletAddress u a -> ttyUser u [walletAddressRow a]
CRWalletNames u names -> ttyUser u $ if null names then ["wallet, no names for this profile"] else map walletNameRow names
CRWalletAccountSecret u a secret -> ttyUser u [walletAddressRow a <> " " <> plain secret]
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
CRPublicGroupCreated u g _groupLink _relays -> ttyUser u $ viewGroupCreated g testView
@@ -1114,6 +1115,9 @@ walletAddressRow :: WalletAddress -> StyledString
walletAddressRow WalletAddress {accountIndex, keyPath, address} =
plain $ tshow accountIndex <> " " <> keyPath <> " " <> address
walletNameRow :: WalletName -> StyledString
walletNameRow WalletName {accountIndex, name} = plain $ tshow accountIndex <> " " <> name
walletErrorText :: WalletError -> Text
walletErrorText = \case
WENoMaster -> "this device has no wallet"
+11
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
@@ -6,6 +7,7 @@ module Simplex.Chat.Wallet
( AccountIndex,
AccountKey,
WalletAddress (..),
WalletName (..),
WalletError (..),
newSeedEntropy,
entropyFromMnemonic,
@@ -48,6 +50,13 @@ data WalletAddress = WalletAddress
}
deriving (Show)
-- | A name one of the seed's accounts holds, as the last scan saw it.
data WalletName = WalletName
{ accountIndex :: AccountIndex,
name :: Text
}
deriving (Show)
data WalletError
= WENoMaster -- the device has no master entropy
| WEMasterExists -- create, when it already has one
@@ -117,4 +126,6 @@ bipError = either (Left . WEDerivation) Right
$(JQ.deriveJSON defaultJSON ''WalletAddress)
$(JQ.deriveJSON defaultJSON ''WalletName)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "WE") ''WalletError)
+14 -4
View File
@@ -7,13 +7,13 @@ module WalletTests where
import ChatClient
import ChatTests.DBUtils
import ChatTests.Utils
import Database.SQLite.Simple (Only (..))
import qualified Data.ByteArray as BA
import qualified Data.ByteArray.Encoding as BAE
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as B
import Data.Char (toUpper)
import Data.Either (isRight)
import Data.Int (Int64)
import Data.List (nub)
import Data.Text (Text)
import qualified Data.Text as T
@@ -111,6 +111,7 @@ walletScanTests = do
it "a scan of a recovered phrase finds the accounts in use" testWalletScan
it "the counter a scan sets clears the accounts already bound" testWalletScanCounterPastBound
it "a hidden profile cannot scan" testWalletScanHiddenProfile
it "a profile with no names says so" testWalletNamesEmpty
testWalletScan :: HasCallStack => TestParams -> IO ()
testWalletScan ps = withSmpServerAndNames $ \reg -> withNewTestChat ps "alice" aliceProfile $ \alice -> do
@@ -121,9 +122,10 @@ testWalletScan ps = withSmpServerAndNames $ \reg -> withNewTestChat ps "alice" a
alice ##> "/_wallet scan"
alice <## "accounts: 1"
-- the names the scan saw are recorded against the account that holds them
names <- withCCTransaction alice $ \db -> DB.query_ db "SELECT account_index, name, name_response FROM wallet_owned_names"
map (\(n, nm, _) -> (n, nm)) names `shouldBe` [(1 :: Int64, "alice.simplex" :: Text)]
map (\(_, _, r) -> registeredName <$> decodeJSON r) names `shouldBe` [Just (Just "alice.simplex")]
alice ##> "/_wallet names"
alice <## "1 alice.simplex"
names <- withCCTransaction alice $ \db -> DB.query_ db "SELECT name_response FROM wallet_owned_names"
map (\(Only r) -> registeredName <$> decodeJSON r) names `shouldBe` [Just (Just "alice.simplex")]
-- the scan gives the imported phrase the counter it had none of
alice ##> "/_wallet bind"
alice <## "accounts: 1, 2"
@@ -294,6 +296,14 @@ testWalletScanCounterPastBound ps = withSmpServerAndNames $ \_reg -> withNewTest
alice ##> "/_wallet bind"
alice <## "accounts: 5, 6"
-- | Names come from a scan, so a profile that has not scanned has none to list.
testWalletNamesEmpty :: HasCallStack => TestParams -> IO ()
testWalletNamesEmpty ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
alice ##> "/_wallet create new"
alice <## "wallet, no accounts for this profile"
alice ##> "/_wallet names"
alice <## "wallet, no names for this profile"
-- | A scan binds what it finds, so it is refused where a bind is.
testWalletScanHiddenProfile :: HasCallStack => TestParams -> IO ()
testWalletScanHiddenProfile ps = withSmpServerAndNames $ \_reg -> withNewTestChat ps "alice" aliceProfile $ \alice -> do