mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-29 11:09:19 +00:00
core: names are the device's, so drop the per-profile account
A name's profile is the record it resolves to, not the key that owns it, so the account level was carrying a mapping nothing needs. It was also the only source of the profile to account ambiguity after a restore, of /_wallet bind, and of the index gap and the refusal that told a visible profile an account was held by one it cannot see. All of it goes. Names sit in account 0 from index 1. Index 0 is left unused so neither the names nor the profile accounts, which start at 1, claim the origin of both dimensions. users gains no columns at all now, so the migration is one CREATE TABLE. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX
This commit is contained in:
co-authored by
Claude Opus 5
parent
05508c0edd
commit
309358a162
@@ -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, NameIndex)
|
||||
import Simplex.Chat.Wallet (NameIndex)
|
||||
import Simplex.Chat.Util (liftIOEither)
|
||||
import Simplex.FileTransfer.Description (FileDescriptionURI)
|
||||
import Simplex.Messaging.Server.Information (ServerPublicInfo)
|
||||
@@ -418,11 +418,10 @@ data ChatCommand
|
||||
| APISendServiceRequest {userId :: UserId, sendTarget :: ConnectTarget 'CMContact, requestTimeout :: Maybe NominalDiffTime, signKey :: Maybe (C.StoredPrivateKey 'C.Ed25519), request :: J.Object}
|
||||
| APISendServiceResponse {userId :: UserId, requestId :: AgentInvId, responseData :: J.Object}
|
||||
| APIWallet
|
||||
| APIWalletBind {boundAccountIndex :: Maybe AccountIndex}
|
||||
| APIWalletCreate
|
||||
| APIWalletImport {recoveryPhrase :: Text}
|
||||
| APIWalletExportSeedMnemonic
|
||||
| APIWalletExportDerivedSecret {accountIndex :: AccountIndex, nameIndex :: NameIndex}
|
||||
| APIWalletExportDerivedSecret {nameIndex :: NameIndex}
|
||||
| APIWalletDelete
|
||||
| APISendCallInvitation ContactId CallType
|
||||
| SendCallInvitation ContactName CallType
|
||||
@@ -750,7 +749,6 @@ allowRemoteCommand = \case
|
||||
ExecChatStoreSQL _ -> False
|
||||
ExecAgentStoreSQL _ -> False
|
||||
APIWallet -> False
|
||||
APIWalletBind {} -> False
|
||||
APIWalletCreate -> False
|
||||
APIWalletImport _ -> False
|
||||
APIWalletExportSeedMnemonic -> False
|
||||
@@ -857,7 +855,7 @@ data ChatResponse
|
||||
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact}
|
||||
| CRServiceResponse {user :: User, responseData :: J.Object}
|
||||
| CRServiceReplyAccepted {user :: User, connectionId :: AgentConnId}
|
||||
| CRWallet {user :: User, walletKeyExists :: Bool, walletKeyPaths :: [(Text, Text)], walletProfiles :: [Text]}
|
||||
| CRWallet {user :: User, walletKeyExists :: Bool, walletKeyPaths :: [(Text, Text)]}
|
||||
| CRWalletSeedMnemonic {user :: User, recoveryPhrase :: Text}
|
||||
| CRWalletDerivedSecret {user :: User, keyPath :: Text, address :: Text, derivedSecret :: Text}
|
||||
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
|
||||
|
||||
@@ -58,8 +58,8 @@ 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)
|
||||
import Simplex.Chat.Wallet (AccountIndex, WalletSeed (..), deriveNameKey, importRecoveryKey, nameKeySecret, newSeed, recoveryKeyPhrase, renderNameKeyPath, seedMaster)
|
||||
import Simplex.Chat.Store.Wallets (createSeed, deleteSeed, getDeviceSeed, getNextNameIndex)
|
||||
import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), deriveNameKey, importRecoveryKey, nameKeySecret, newSeed, recoveryKeyPhrase, renderNameKeyPath, seedMaster)
|
||||
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -1494,17 +1494,10 @@ processChatCommand cxt nm = \case
|
||||
pure $ CRServiceReplyAccepted user (AgentConnId connId)
|
||||
APIWallet -> withUser $ \user -> do
|
||||
withFastStore' getDeviceSeed >>= \case
|
||||
Nothing -> pure $ CRWallet user False [] []
|
||||
Nothing -> pure $ CRWallet user False []
|
||||
Just seed -> do
|
||||
acct_ <- withFastStore' (`getAccountIndex` user)
|
||||
paths <- maybe (pure []) (nameKeyRows seed) acct_
|
||||
-- other profiles are named but not numbered, so a hidden one leaves no gap
|
||||
profiles <- withFastStore' $ \db -> getSeedProfiles db (wsId seed) user
|
||||
pure $ CRWallet user True paths profiles
|
||||
APIWalletBind acct_ -> withUser $ \user -> do
|
||||
seed <- deviceSeed
|
||||
withFastStore' (\db -> bindAccountIndex db user (wsId seed) acct_) >>= either throwCmdError pure
|
||||
processChatCommand cxt nm APIWallet
|
||||
next <- withFastStore' $ \db -> getNextNameIndex db (wsId seed)
|
||||
CRWallet user True <$> nameKeyRows seed next
|
||||
APIWalletCreate -> withUser $ \_ -> do
|
||||
g <- asks random
|
||||
entropy <- atomically $ newSeed MS256 g
|
||||
@@ -1520,10 +1513,10 @@ processChatCommand cxt nm = \case
|
||||
seed <- deviceSeed
|
||||
phrase <- either throwCmdError pure $ recoveryKeyPhrase seed
|
||||
pure $ CRWalletSeedMnemonic user (safeDecodeUtf8 phrase)
|
||||
APIWalletExportDerivedSecret acct nameIdx -> withUser $ \user -> do
|
||||
APIWalletExportDerivedSecret nameIdx -> withUser $ \user -> do
|
||||
seed <- deviceSeed
|
||||
k <- either throwCmdError pure $ seedMaster seed >>= \m -> deriveNameKey m acct nameIdx
|
||||
pure $ CRWalletDerivedSecret user (renderNameKeyPath acct nameIdx) (decodeLatin1 . strEncode $ addressFromPrivateKey k) (safeDecodeUtf8 $ nameKeySecret k)
|
||||
k <- either throwCmdError pure $ seedMaster seed >>= \m -> deriveNameKey m nameIdx
|
||||
pure $ CRWalletDerivedSecret user (renderNameKeyPath nameIdx) (decodeLatin1 . strEncode $ addressFromPrivateKey k) (safeDecodeUtf8 $ nameKeySecret k)
|
||||
APIWalletDelete -> withUser $ \_ -> do
|
||||
seed <- deviceSeed
|
||||
withFastStore' $ \db -> deleteSeed db (wsId seed)
|
||||
@@ -5470,11 +5463,11 @@ walletNamesShown = 2
|
||||
deviceSeed :: CM WalletSeed
|
||||
deviceSeed = withFastStore' getDeviceSeed >>= maybe (throwCmdError "no wallet key on this device") pure
|
||||
|
||||
nameKeyRows :: WalletSeed -> AccountIndex -> CM [(Text, Text)]
|
||||
nameKeyRows seed acct = either throwCmdError pure $ do
|
||||
nameKeyRows :: WalletSeed -> NameIndex -> CM [(Text, Text)]
|
||||
nameKeyRows seed next = either throwCmdError pure $ do
|
||||
master <- seedMaster seed
|
||||
forM (take walletNamesShown [0 ..]) $ \nm ->
|
||||
(renderNameKeyPath acct nm,) . decodeLatin1 . strEncode . addressFromPrivateKey <$> deriveNameKey master acct nm
|
||||
forM (take walletNamesShown [next ..]) $ \nm ->
|
||||
(renderNameKeyPath nm,) . decodeLatin1 . strEncode . addressFromPrivateKey <$> deriveNameKey master nm
|
||||
|
||||
chatCommandP :: Parser ChatCommand
|
||||
chatCommandP =
|
||||
@@ -5594,11 +5587,9 @@ chatCommandP =
|
||||
"/_reject " *> (APIRejectContact <$> A.decimal <*> (" notify=" *> onOffP <|> pure False)),
|
||||
"/_service_request " *> (APISendServiceRequest <$> A.decimal <* A.space <*> strP <*> optional (" timeout=" *> (realToFrac <$> A.double)) <*> optional (" sign_key=" *> strP) <* A.space <*> jsonP),
|
||||
"/_service_response " *> (APISendServiceResponse <$> A.decimal <* A.space <*> strP <* A.space <*> jsonP),
|
||||
"/_wallet bind " *> (APIWalletBind . Just <$> keyIndexP),
|
||||
"/_wallet bind" $> APIWalletBind Nothing,
|
||||
"/_wallet create" $> APIWalletCreate,
|
||||
"/_wallet import " *> (APIWalletImport <$> textP),
|
||||
"/_wallet export " *> (APIWalletExportDerivedSecret <$> keyIndexP <* A.space <*> keyIndexP),
|
||||
"/_wallet export " *> (APIWalletExportDerivedSecret <$> keyIndexP),
|
||||
"/_wallet export" $> APIWalletExportSeedMnemonic,
|
||||
"/_wallet delete" $> APIWalletDelete,
|
||||
"/_wallet" $> APIWallet,
|
||||
|
||||
@@ -13,25 +13,18 @@ CREATE TABLE wallet_seeds (
|
||||
wallet_seed_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
seed BYTEA NOT NULL,
|
||||
-- see the SQLite migration
|
||||
next_account_index BIGINT NOT NULL DEFAULT 0,
|
||||
-- one key per device for now
|
||||
next_name_index BIGINT NOT NULL DEFAULT 1,
|
||||
-- one seed per device for now
|
||||
single_seed SMALLINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
ALTER TABLE users ADD COLUMN wallet_seed_id BIGINT REFERENCES wallet_seeds ON DELETE RESTRICT;
|
||||
ALTER TABLE users ADD COLUMN wallet_account_index BIGINT;
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON wallet_seeds(single_seed);
|
||||
CREATE INDEX idx_users_wallet_seed_id ON users(wallet_seed_id);
|
||||
|]
|
||||
|
||||
down_m20260908_wallet_seeds :: Text
|
||||
down_m20260908_wallet_seeds =
|
||||
[r|
|
||||
DROP INDEX idx_users_wallet_seed_id;
|
||||
|
||||
ALTER TABLE users DROP COLUMN wallet_account_index;
|
||||
ALTER TABLE users DROP COLUMN wallet_seed_id;
|
||||
DROP INDEX idx_wallet_seeds_single_seed;
|
||||
|
||||
DROP TABLE wallet_seeds;
|
||||
|]
|
||||
|
||||
@@ -1517,9 +1517,7 @@ CREATE TABLE test_chat_schema.users (
|
||||
auto_accept_member_contacts smallint DEFAULT 0 NOT NULL,
|
||||
is_user_chat_relay smallint DEFAULT 0 NOT NULL,
|
||||
client_service smallint DEFAULT 0 NOT NULL,
|
||||
auto_accept_group_invitations smallint DEFAULT 0 NOT NULL,
|
||||
wallet_seed_id bigint,
|
||||
wallet_account_index bigint
|
||||
auto_accept_group_invitations smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
|
||||
@@ -1538,7 +1536,7 @@ ALTER TABLE test_chat_schema.users ALTER COLUMN user_id ADD GENERATED ALWAYS AS
|
||||
CREATE TABLE test_chat_schema.wallet_seeds (
|
||||
wallet_seed_id bigint NOT NULL,
|
||||
seed bytea NOT NULL,
|
||||
next_account_index bigint DEFAULT 0 NOT NULL,
|
||||
next_name_index bigint DEFAULT 1 NOT NULL,
|
||||
single_seed smallint DEFAULT 1 NOT NULL
|
||||
);
|
||||
|
||||
@@ -2667,10 +2665,6 @@ CREATE UNIQUE INDEX idx_user_contact_links_group_id ON test_chat_schema.user_con
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_users_wallet_seed_id ON test_chat_schema.users USING btree (wallet_seed_id);
|
||||
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON test_chat_schema.wallet_seeds USING btree (single_seed);
|
||||
|
||||
|
||||
@@ -3350,11 +3344,6 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.users
|
||||
ADD CONSTRAINT users_wallet_seed_id_fkey FOREIGN KEY (wallet_seed_id) REFERENCES test_chat_schema.wallet_seeds(wallet_seed_id) ON DELETE RESTRICT;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.xftp_file_descriptions
|
||||
ADD CONSTRAINT xftp_file_descriptions_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
@@ -11,27 +11,20 @@ m20260908_wallet_seeds =
|
||||
CREATE TABLE wallet_seeds (
|
||||
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes
|
||||
-- known issue: after an import this starts at 0, so /_wallet bind with no
|
||||
-- account can hand out one that already owns names
|
||||
next_account_index INTEGER NOT NULL DEFAULT 0,
|
||||
-- one key per device for now
|
||||
-- known issue: after an import this starts at 1, so it can hand out a name
|
||||
-- key at a path that already owns a name
|
||||
next_name_index INTEGER NOT NULL DEFAULT 1,
|
||||
-- one seed per device for now
|
||||
single_seed INTEGER NOT NULL DEFAULT 1
|
||||
) STRICT;
|
||||
|
||||
ALTER TABLE users ADD COLUMN wallet_seed_id INTEGER REFERENCES wallet_seeds ON DELETE RESTRICT;
|
||||
ALTER TABLE users ADD COLUMN wallet_account_index INTEGER;
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON wallet_seeds(single_seed);
|
||||
CREATE INDEX idx_users_wallet_seed_id ON users(wallet_seed_id);
|
||||
|]
|
||||
|
||||
down_m20260908_wallet_seeds :: Query
|
||||
down_m20260908_wallet_seeds =
|
||||
[sql|
|
||||
DROP INDEX idx_users_wallet_seed_id;
|
||||
|
||||
ALTER TABLE users DROP COLUMN wallet_account_index;
|
||||
ALTER TABLE users DROP COLUMN wallet_seed_id;
|
||||
DROP INDEX idx_wallet_seeds_single_seed;
|
||||
|
||||
DROP TABLE wallet_seeds;
|
||||
|]
|
||||
|
||||
@@ -54,9 +54,7 @@ CREATE TABLE users(
|
||||
auto_accept_member_contacts INTEGER NOT NULL DEFAULT 0,
|
||||
is_user_chat_relay INTEGER NOT NULL DEFAULT 0,
|
||||
client_service INTEGER NOT NULL DEFAULT 0,
|
||||
auto_accept_group_invitations INTEGER NOT NULL DEFAULT 0,
|
||||
wallet_seed_id INTEGER REFERENCES wallet_seeds ON DELETE RESTRICT,
|
||||
wallet_account_index INTEGER, -- 1 for active user
|
||||
auto_accept_group_invitations INTEGER NOT NULL DEFAULT 0, -- 1 for active user
|
||||
FOREIGN KEY(user_id, local_display_name)
|
||||
REFERENCES display_names(user_id, local_display_name)
|
||||
ON DELETE RESTRICT
|
||||
@@ -857,10 +855,10 @@ CREATE TABLE rcv_roster_transfers(
|
||||
CREATE TABLE wallet_seeds(
|
||||
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
seed BLOB NOT NULL, -- BIP-39 entropy, 16-32 bytes
|
||||
-- known issue: after an import this starts at 0, so /_wallet bind with no
|
||||
-- account can hand out one that already owns names
|
||||
next_account_index INTEGER NOT NULL DEFAULT 0,
|
||||
-- one key per device for now
|
||||
-- known issue: after an import this starts at 1, so it can hand out a name
|
||||
-- key at a path that already owns a name
|
||||
next_name_index INTEGER NOT NULL DEFAULT 1,
|
||||
-- one seed per device for now
|
||||
single_seed INTEGER NOT NULL DEFAULT 1
|
||||
) STRICT;
|
||||
CREATE INDEX contact_profiles_index ON contact_profiles(
|
||||
@@ -1398,7 +1396,6 @@ CREATE INDEX idx_chat_items_item_signed_by_group_member_id ON chat_items(
|
||||
item_signed_by_group_member_id
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON wallet_seeds(single_seed);
|
||||
CREATE INDEX idx_users_wallet_seed_id ON users(wallet_seed_id);
|
||||
CREATE TRIGGER on_group_members_insert_update_summary
|
||||
AFTER INSERT ON group_members
|
||||
FOR EACH ROW
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Store.Wallets
|
||||
( getDeviceSeed,
|
||||
getAccountIndex,
|
||||
getSeedProfiles,
|
||||
getNextNameIndex,
|
||||
createSeed,
|
||||
bindAccountIndex,
|
||||
deleteSeed,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Simplex.Chat.Types (User (..))
|
||||
import Simplex.Chat.Wallet (AccountIndex, SeedId, WalletSeed (..))
|
||||
import Simplex.Chat.Wallet (NameIndex, SeedId, WalletSeed (..))
|
||||
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
|
||||
#if defined(dbPostgres)
|
||||
import Database.PostgreSQL.Simple (Only (..))
|
||||
import Database.PostgreSQL.Simple.SqlQQ (sql)
|
||||
#else
|
||||
import Database.SQLite.Simple (Only (..))
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
#endif
|
||||
|
||||
toSeed :: (Int64, ByteString) -> WalletSeed
|
||||
@@ -38,91 +30,20 @@ getDeviceSeed db =
|
||||
maybeFirstRow toSeed $
|
||||
DB.query_ db "SELECT wallet_seed_id, seed FROM wallet_seeds ORDER BY wallet_seed_id LIMIT 1"
|
||||
|
||||
getAccountIndex :: DB.Connection -> User -> IO (Maybe AccountIndex)
|
||||
getAccountIndex db User {userId} = do
|
||||
r <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT wallet_account_index FROM users WHERE user_id = ?" (Only userId)
|
||||
pure $ case r of
|
||||
Just (Just ix) -> Just $ fromIntegral (ix :: Int64)
|
||||
_ -> Nothing
|
||||
-- | The index the next name bought on this device takes.
|
||||
getNextNameIndex :: DB.Connection -> SeedId -> IO NameIndex
|
||||
getNextNameIndex db sId =
|
||||
maybe 1 (fromIntegral :: Int64 -> NameIndex)
|
||||
<$> ( maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT next_name_index FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)
|
||||
)
|
||||
|
||||
-- | Hidden profiles are left out, as they are by /users.
|
||||
getSeedProfiles :: DB.Connection -> SeedId -> User -> IO [Text]
|
||||
getSeedProfiles db sId User {userId} =
|
||||
map fromOnly
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT local_display_name FROM users
|
||||
WHERE wallet_seed_id = ? AND user_id != ? AND view_pwd_hash IS NULL
|
||||
ORDER BY local_display_name
|
||||
|]
|
||||
(sId, userId)
|
||||
|
||||
bindUser :: DB.Connection -> Int64 -> SeedId -> Int64 -> IO ()
|
||||
bindUser db uId sId acct =
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE users SET wallet_seed_id = ?, wallet_account_index = ? WHERE user_id = ?"
|
||||
(sId, acct, uId)
|
||||
|
||||
-- | False if the device already has a key. No profile is bound, as which
|
||||
-- account a profile takes is said with bind.
|
||||
-- | False if the device already has a seed.
|
||||
createSeed :: DB.Connection -> ByteString -> IO Bool
|
||||
createSeed db entropy =
|
||||
getDeviceSeed db >>= \case
|
||||
Just _ -> pure False
|
||||
Nothing -> True <$ DB.execute db "INSERT INTO wallet_seeds (seed) VALUES (?)" (Only $ DB.Binary entropy)
|
||||
|
||||
-- | Without an account the next free one is taken, which a profile that has
|
||||
-- one does not need: moving to another account is asked for by number.
|
||||
bindAccountIndex :: DB.Connection -> User -> SeedId -> Maybe AccountIndex -> IO (Either String ())
|
||||
bindAccountIndex db user@User {userId} sId = \case
|
||||
Nothing ->
|
||||
getAccountIndex db user >>= \case
|
||||
Just _ -> pure $ Left "this profile already has an account"
|
||||
Nothing -> do
|
||||
acct <- takeAccountIndex db sId
|
||||
-- BIP-32 hardens at 2^31, and every index above it is the same key again
|
||||
if acct >= 0x80000000
|
||||
then pure $ Left "no free account on this key"
|
||||
else Right () <$ bindUser db userId sId acct
|
||||
Just acct
|
||||
| acct >= 0x80000000 -> pure $ Left "account index too large"
|
||||
| otherwise -> do
|
||||
taken <-
|
||||
maybeFirstRow fromOnly $
|
||||
DB.query
|
||||
db
|
||||
"SELECT 1 FROM users WHERE wallet_seed_id = ? AND wallet_account_index = ? AND user_id != ?"
|
||||
(sId, fromIntegral acct :: Int64, userId)
|
||||
case (taken :: Maybe Int64) of
|
||||
Just _ -> pure $ Left "another profile uses this account"
|
||||
Nothing -> do
|
||||
bindUser db userId sId (fromIntegral acct)
|
||||
-- the counter moves past it, so the next profile is not handed the same one
|
||||
setNextAccountIndex db sId (fromIntegral acct + 1)
|
||||
pure $ Right ()
|
||||
|
||||
-- | So two profiles cannot be handed the same account.
|
||||
takeAccountIndex :: DB.Connection -> SeedId -> IO Int64
|
||||
takeAccountIndex db sId = do
|
||||
DB.execute db "UPDATE wallet_seeds SET next_account_index = next_account_index + 1 WHERE wallet_seed_id = ?" (Only sId)
|
||||
maybe 0 (subtract 1)
|
||||
<$> ( maybeFirstRow fromOnly $
|
||||
DB.query db "SELECT next_account_index FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)
|
||||
)
|
||||
|
||||
setNextAccountIndex :: DB.Connection -> SeedId -> Int64 -> IO ()
|
||||
setNextAccountIndex db sId acct =
|
||||
DB.execute
|
||||
db
|
||||
"UPDATE wallet_seeds SET next_account_index = ? WHERE wallet_seed_id = ? AND next_account_index < ?"
|
||||
(acct, sId, acct)
|
||||
|
||||
-- | Profiles are unbound first, as the foreign key is ON DELETE RESTRICT.
|
||||
deleteSeed :: DB.Connection -> SeedId -> IO ()
|
||||
deleteSeed db sId = do
|
||||
DB.execute db "UPDATE users SET wallet_seed_id = NULL, wallet_account_index = NULL WHERE wallet_seed_id = ?" (Only sId)
|
||||
DB.execute db "DELETE FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)
|
||||
deleteSeed db sId = DB.execute db "DELETE FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)
|
||||
|
||||
@@ -188,14 +188,9 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
|
||||
CRContactRequestRejected u UserContactRequest {localDisplayName = c} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"]
|
||||
CRServiceResponse u resp -> ttyUser u ["service response: " <> viewJSON resp]
|
||||
CRServiceReplyAccepted u (AgentConnId cId) -> ttyUser u [plain $ "service reply accepted, connection id: " <> safeDecodeUtf8 (strEncode cId)]
|
||||
CRWallet u exists paths profiles
|
||||
| not exists -> ttyUser u ["no wallet key"]
|
||||
| otherwise -> ttyUser u $ keyRows <> [plain $ "also on same seed: " <> T.intercalate ", " profiles | not (null profiles)]
|
||||
CRWallet u exists paths -> ttyUser u $ if exists then map nameRow paths else ["no wallet key"]
|
||||
where
|
||||
keyRows
|
||||
| null paths = ["no account for this profile"]
|
||||
| otherwise = zipWith nameRow [0 :: Int ..] paths
|
||||
nameRow k (path, addr) = plain $ "name " <> tshow k <> " " <> path <> " " <> addr
|
||||
nameRow (path, addr) = plain $ path <> " " <> addr
|
||||
CRWalletSeedMnemonic u phrase -> ttyUser u [plain phrase]
|
||||
CRWalletDerivedSecret u path addr secret -> ttyUser u [plain $ path <> " " <> addr <> " " <> secret]
|
||||
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
-- | BIP-39 seeds and the keys derived from them.
|
||||
--
|
||||
-- One account path per profile, one key per name under it. A name's secret is
|
||||
-- a leaf, so exporting it hands over that name only.
|
||||
-- One key per name. A name's secret is a leaf, so exporting it hands over that
|
||||
-- name only.
|
||||
module Simplex.Chat.Wallet
|
||||
( SeedId,
|
||||
WalletSeed (..),
|
||||
AccountIndex,
|
||||
NameIndex,
|
||||
newSeed,
|
||||
importRecoveryKey,
|
||||
@@ -34,10 +33,8 @@ import Simplex.Messaging.Eth.Address (ethereumPath)
|
||||
|
||||
type SeedId = Int64
|
||||
|
||||
-- | BIP-44 account index, one per chat profile.
|
||||
type AccountIndex = Word32
|
||||
|
||||
-- | BIP-44 address index, one per name.
|
||||
-- | BIP-44 address index, one per name. Names sit in account 0, from index 1:
|
||||
-- account 0 index 0 is left for the profile accounts to start beside.
|
||||
type NameIndex = Word32
|
||||
|
||||
data WalletSeed = WalletSeed
|
||||
@@ -65,11 +62,11 @@ seedMaster s = do
|
||||
m <- B39.entropyToMnemonic (wsEntropy s)
|
||||
B32.masterKey (B39.mnemonicToSeed m "")
|
||||
|
||||
renderNameKeyPath :: AccountIndex -> NameIndex -> Text
|
||||
renderNameKeyPath acc nm = decodeLatin1 . B32.renderPath $ ethereumPath acc nm
|
||||
renderNameKeyPath :: NameIndex -> Text
|
||||
renderNameKeyPath nm = decodeLatin1 . B32.renderPath $ ethereumPath 0 nm
|
||||
|
||||
deriveNameKey :: B32.ExtendedKey -> AccountIndex -> NameIndex -> Either String S.PrivateKey
|
||||
deriveNameKey master acc nm = B32.xkKey <$> B32.derivePath master (ethereumPath acc nm)
|
||||
deriveNameKey :: B32.ExtendedKey -> NameIndex -> Either String S.PrivateKey
|
||||
deriveNameKey master nm = B32.xkKey <$> B32.derivePath master (ethereumPath 0 nm)
|
||||
|
||||
-- | As wallets take it when a key is imported on its own.
|
||||
nameKeySecret :: S.PrivateKey -> ByteString
|
||||
|
||||
Reference in New Issue
Block a user