mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-22 13:11:55 +00:00
/goal iteration 5 - unconfirmed
This commit is contained in:
1 parent
bfc4e1fbaa
commit
768a230f21
14 files changed
+328
-238
No files matched your search
@@ -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 (accountHeldByOther, bindAccount, createWalletSeed, deleteWalletSeed, getNextAccountIndex, getUserAccounts, getWalletSeed)
|
||||
import Simplex.Chat.Wallet (AccountIndex, AccountKey, WalletAddress (..), WalletError (..), WalletSeed (..), accountSecret, checkAccountIndex, deriveAccountKey, entropyFromMnemonic, newSeedEntropy, renderAccountPath, seedMaster, seedMnemonic)
|
||||
import Simplex.Chat.Store.Wallets (WalletSeed (..), accountHeldByOther, bindAccount, createWalletSeed, deleteWalletSeed, getUserAccounts, getWalletSeed, resolveAccount)
|
||||
import Simplex.Chat.Wallet (AccountIndex, AccountKey, WalletAddress (..), WalletError (..), accountSecret, checkAccountIndex, deriveAccountKey, entropyFromMnemonic, newSeedEntropy, renderAccountPath, seedMaster, seedMnemonic)
|
||||
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
|
||||
import Simplex.Chat.Call
|
||||
import Simplex.Chat.Controller
|
||||
@@ -1503,27 +1503,25 @@ processChatCommand cxt nm = \case
|
||||
unless created $ throwWalletError WEMasterExists
|
||||
processChatCommand cxt nm APIGetWallet
|
||||
APIBindWalletAccount accountIdx_ -> withUser $ \User {userId, viewPwdHash} -> do
|
||||
seed <- walletSeed
|
||||
when (isJust viewPwdHash) $ throwWalletError WEHiddenProfile
|
||||
_ <- liftWallet =<< withFastStore' (\db -> bindAccount db (wsId seed) userId accountIdx_)
|
||||
liftWallet =<< withFastStore' (\db -> bindAccount db userId accountIdx_)
|
||||
processChatCommand cxt nm APIGetWallet
|
||||
APIGetWalletAddress accountIdx_ -> withUser $ \user -> do
|
||||
seed <- walletSeed
|
||||
n <- resolveAccount seed accountIdx_
|
||||
(seed, n) <- liftWallet =<< withFastStore' (`resolveAccount` accountIdx_)
|
||||
CRWalletAddress user . accountAddress n <$> accountKey seed n
|
||||
APIExportWalletMnemonic -> withUser $ \user ->
|
||||
CRWalletMnemonic user <$> (liftWallet . seedMnemonic =<< walletSeed)
|
||||
APIExportWalletAccount accountIdx -> withUser $ \user@User {userId} -> do
|
||||
CRWalletMnemonic user <$> (liftWallet . seedMnemonic . wsEntropy =<< walletSeed)
|
||||
APIExportWalletAccount n -> withUser $ \user@User {userId} -> do
|
||||
seed <- walletSeed
|
||||
n <- resolveAccount seed (Just accountIdx)
|
||||
liftWallet $ checkAccountIndex n
|
||||
-- a key another profile holds is not this profile's to hand out
|
||||
heldByOther <- withFastStore' $ \db -> accountHeldByOther db (wsId seed) userId n
|
||||
when heldByOther $ throwWalletError WEAccountBound
|
||||
k <- accountKey seed n
|
||||
pure $ CRWalletAccountSecret user (accountAddress n k) (accountSecret k)
|
||||
APIDeleteWallet -> withUser $ \_ -> do
|
||||
seed <- walletSeed
|
||||
withFastStore' $ \db -> deleteWalletSeed db (wsId seed)
|
||||
deleted <- withFastStore' deleteWalletSeed
|
||||
unless deleted $ throwWalletError WENoMaster
|
||||
ok_
|
||||
APISendCallInvitation contactId callType -> withUser $ \user -> do
|
||||
-- party initiating call
|
||||
@@ -5470,19 +5468,8 @@ throwWalletError = throwChatError . CEWallet
|
||||
liftWallet :: Either WalletError a -> CM a
|
||||
liftWallet = either throwWalletError pure
|
||||
|
||||
-- | The account a command names, or the next free one when it names none.
|
||||
-- Refuses any index BIP-32 cannot harden, the counter's included.
|
||||
resolveAccount :: WalletSeed -> Maybe AccountIndex -> CM AccountIndex
|
||||
resolveAccount seed accountIdx_ = do
|
||||
n <- maybe nextFreeAccount pure accountIdx_
|
||||
n <$ liftWallet (checkAccountIndex n)
|
||||
where
|
||||
nextFreeAccount =
|
||||
withFastStore' (\db -> getNextAccountIndex db (wsId seed))
|
||||
>>= maybe (throwWalletError WECounterUnknown) pure
|
||||
|
||||
accountKey :: WalletSeed -> AccountIndex -> CM AccountKey
|
||||
accountKey seed n = liftWallet $ seedMaster seed >>= (`deriveAccountKey` n)
|
||||
accountKey seed n = liftWallet $ seedMaster (wsEntropy seed) >>= (`deriveAccountKey` n)
|
||||
|
||||
accountAddress :: AccountIndex -> AccountKey -> WalletAddress
|
||||
accountAddress n k =
|
||||
@@ -6156,12 +6143,15 @@ chatCommandP =
|
||||
quotedP = safeDecodeUtf8 <$> (A.char '"' *> A.takeTill (== '"') <* A.char '"')
|
||||
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
|
||||
char_ = optional . A.char
|
||||
-- Digits are counted before they are read, as reading a very long number is
|
||||
-- not free. The hardening bound is a typed error when the command runs.
|
||||
-- The digits are counted before they are read, as reading a very long
|
||||
-- number is not free. Ten of them fit an Int with room to spare, and the
|
||||
-- hardening bound is a typed error when the command runs, so that a caller
|
||||
-- is told which index was refused and why.
|
||||
accountIndexP = do
|
||||
ds <- A.takeWhile1 isDigit
|
||||
let i = read (B.unpack ds) :: Integer
|
||||
if B.length ds <= 10 && i <= toInteger (maxBound :: AccountIndex) then pure (fromIntegral i) else fail "account index too large"
|
||||
case if B.length ds <= 10 then B.readInt ds else Nothing of
|
||||
Just (i, _) | i <= fromIntegral (maxBound :: AccountIndex) -> pure $ fromIntegral i
|
||||
_ -> fail "account index too large"
|
||||
|
||||
displayNameP :: Parser Text
|
||||
displayNameP = safeDecodeUtf8 <$> displayNameP_
|
||||
|
||||
@@ -99,7 +99,7 @@ schemaMigrations =
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
|
||||
("20260908_wallet_seeds", m20260908_wallet_seeds, Just down_m20260908_wallet_seeds)
|
||||
("20260908_wallet_seeds", m20260908_wallet_seeds, Nothing)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -9,10 +9,10 @@ import Text.RawString.QQ (r)
|
||||
m20260908_wallet_seeds :: Text
|
||||
m20260908_wallet_seeds =
|
||||
[r|
|
||||
-- the columns are commented in the SQLite migration
|
||||
CREATE TABLE wallet_seeds (
|
||||
wallet_seed_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
entropy BYTEA NOT NULL CHECK (length(entropy) = 32),
|
||||
-- see the SQLite migration
|
||||
next_account_index BIGINT CHECK (next_account_index BETWEEN 0 AND 2147483648),
|
||||
single_seed SMALLINT NOT NULL DEFAULT 1
|
||||
);
|
||||
@@ -25,20 +25,8 @@ CREATE TABLE wallet_accounts (
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON wallet_seeds(single_seed);
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_index ON wallet_accounts(wallet_seed_id, account_index);
|
||||
CREATE INDEX idx_wallet_accounts_user ON wallet_accounts(user_id);
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON wallet_accounts(wallet_seed_id, account_index);
|
||||
CREATE INDEX idx_wallet_accounts_user_id ON wallet_accounts(user_id);
|
||||
|]
|
||||
|
||||
down_m20260908_wallet_seeds :: Text
|
||||
down_m20260908_wallet_seeds =
|
||||
[r|
|
||||
DROP INDEX idx_wallet_accounts_user;
|
||||
|
||||
DROP INDEX idx_wallet_accounts_index;
|
||||
|
||||
DROP INDEX idx_wallet_seeds_single_seed;
|
||||
|
||||
DROP TABLE wallet_accounts;
|
||||
|
||||
DROP TABLE wallet_seeds;
|
||||
|]
|
||||
-- no reverse step, see the SQLite migration
|
||||
@@ -1533,11 +1533,34 @@ ALTER TABLE test_chat_schema.users ALTER COLUMN user_id ADD GENERATED ALWAYS AS
|
||||
|
||||
|
||||
|
||||
CREATE TABLE test_chat_schema.wallet_accounts (
|
||||
wallet_account_id bigint NOT NULL,
|
||||
wallet_seed_id bigint NOT NULL,
|
||||
account_index bigint,
|
||||
user_id bigint,
|
||||
CONSTRAINT wallet_accounts_account_index_check CHECK (((account_index >= 0) AND (account_index <= 2147483647)))
|
||||
);
|
||||
|
||||
|
||||
|
||||
ALTER TABLE test_chat_schema.wallet_accounts ALTER COLUMN wallet_account_id ADD GENERATED ALWAYS AS IDENTITY (
|
||||
SEQUENCE NAME test_chat_schema.wallet_accounts_wallet_account_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1
|
||||
);
|
||||
|
||||
|
||||
|
||||
CREATE TABLE test_chat_schema.wallet_seeds (
|
||||
wallet_seed_id bigint NOT NULL,
|
||||
entropy bytea NOT NULL,
|
||||
next_name_index bigint DEFAULT 1 NOT NULL,
|
||||
single_seed smallint DEFAULT 1 NOT NULL
|
||||
next_account_index bigint,
|
||||
single_seed smallint DEFAULT 1 NOT NULL,
|
||||
CONSTRAINT wallet_seeds_entropy_check CHECK ((length(entropy) = 32)),
|
||||
CONSTRAINT wallet_seeds_next_account_index_check CHECK (((next_account_index >= 0) AND (next_account_index <= '2147483648'::bigint)))
|
||||
);
|
||||
|
||||
|
||||
@@ -1911,6 +1934,11 @@ ALTER TABLE ONLY test_chat_schema.users
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.wallet_accounts
|
||||
ADD CONSTRAINT wallet_accounts_pkey PRIMARY KEY (wallet_account_id);
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.wallet_seeds
|
||||
ADD CONSTRAINT wallet_seeds_pkey PRIMARY KEY (wallet_seed_id);
|
||||
|
||||
@@ -2665,6 +2693,14 @@ CREATE UNIQUE INDEX idx_user_contact_links_group_id ON test_chat_schema.user_con
|
||||
|
||||
|
||||
|
||||
CREATE INDEX idx_wallet_accounts_user_id ON test_chat_schema.wallet_accounts USING btree (user_id);
|
||||
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON test_chat_schema.wallet_accounts USING btree (wallet_seed_id, account_index);
|
||||
|
||||
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON test_chat_schema.wallet_seeds USING btree (single_seed);
|
||||
|
||||
|
||||
@@ -3344,6 +3380,16 @@ ALTER TABLE ONLY test_chat_schema.user_contact_links
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.wallet_accounts
|
||||
ADD CONSTRAINT wallet_accounts_user_id_fkey FOREIGN KEY (user_id) REFERENCES test_chat_schema.users(user_id) ON DELETE SET NULL;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY test_chat_schema.wallet_accounts
|
||||
ADD CONSTRAINT wallet_accounts_wallet_seed_id_fkey FOREIGN KEY (wallet_seed_id) REFERENCES test_chat_schema.wallet_seeds(wallet_seed_id) ON DELETE CASCADE;
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -345,7 +345,7 @@ schemaMigrations =
|
||||
("20260813_auto_accept_group_invitations", m20260813_auto_accept_group_invitations, Just down_m20260813_auto_accept_group_invitations),
|
||||
("20260822_forward_link", m20260822_forward_link, Just down_m20260822_forward_link),
|
||||
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
|
||||
("20260908_wallet_seeds", m20260908_wallet_seeds, Just down_m20260908_wallet_seeds)
|
||||
("20260908_wallet_seeds", m20260908_wallet_seeds, Nothing)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
|
||||
@@ -23,20 +23,11 @@ CREATE TABLE wallet_accounts (
|
||||
) STRICT;
|
||||
|
||||
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON wallet_seeds(single_seed);
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_index ON wallet_accounts(wallet_seed_id, account_index);
|
||||
CREATE INDEX idx_wallet_accounts_user ON wallet_accounts(user_id);
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON wallet_accounts(wallet_seed_id, account_index);
|
||||
CREATE INDEX idx_wallet_accounts_user_id ON wallet_accounts(user_id);
|
||||
|]
|
||||
|
||||
down_m20260908_wallet_seeds :: Query
|
||||
down_m20260908_wallet_seeds =
|
||||
[sql|
|
||||
DROP INDEX idx_wallet_accounts_user;
|
||||
|
||||
DROP INDEX idx_wallet_accounts_index;
|
||||
|
||||
DROP INDEX idx_wallet_seeds_single_seed;
|
||||
|
||||
DROP TABLE wallet_accounts;
|
||||
|
||||
DROP TABLE wallet_seeds;
|
||||
|]
|
||||
-- There is no reverse step. Reversing this would drop the only copy of the
|
||||
-- master entropy, and a downgrade that takes every key on the device with it is
|
||||
-- worse than one that refuses: without a reverse step the older app reports that
|
||||
-- the database is newer than it is and changes nothing.
|
||||
@@ -1399,11 +1399,11 @@ 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 UNIQUE INDEX idx_wallet_accounts_index ON wallet_accounts(
|
||||
CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON wallet_accounts(
|
||||
wallet_seed_id,
|
||||
account_index
|
||||
);
|
||||
CREATE INDEX idx_wallet_accounts_user ON wallet_accounts(user_id);
|
||||
CREATE INDEX idx_wallet_accounts_user_id ON wallet_accounts(user_id);
|
||||
CREATE TRIGGER on_group_members_insert_update_summary
|
||||
AFTER INSERT ON group_members
|
||||
FOR EACH ROW
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
|
||||
-- | The device seed, and which chat profile each account belongs to.
|
||||
module Simplex.Chat.Store.Wallets
|
||||
( getWalletSeed,
|
||||
( SeedId,
|
||||
WalletSeed (..),
|
||||
getWalletSeed,
|
||||
createWalletSeed,
|
||||
deleteWalletSeed,
|
||||
getNextAccountIndex,
|
||||
resolveAccount,
|
||||
getUserAccounts,
|
||||
accountHeldByOther,
|
||||
bindAccount,
|
||||
)
|
||||
where
|
||||
|
||||
import Control.Monad (join, when)
|
||||
import Control.Monad (join, unless, when)
|
||||
import Control.Monad.Except
|
||||
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, SeedId, WalletError (..), WalletSeed (..), checkAccountIndex)
|
||||
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), checkAccountIndex)
|
||||
import Simplex.Messaging.Agent.Protocol (UserId)
|
||||
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
|
||||
import qualified Simplex.Messaging.Agent.Store.DB as DB
|
||||
@@ -35,6 +38,17 @@ import Database.SQLite.Simple (Only (..))
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
#endif
|
||||
|
||||
type SeedId = Int64
|
||||
|
||||
-- | The device seed, as a row: the id the account rows are counted against, and
|
||||
-- the entropy every key is derived from. The entropy is 'BA.ScrubbedBytes', so
|
||||
-- a derived 'Show' does not print it.
|
||||
data WalletSeed = WalletSeed
|
||||
{ wsId :: SeedId,
|
||||
wsEntropy :: BA.ScrubbedBytes
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
toSeed :: (Int64, ByteString) -> WalletSeed
|
||||
toSeed (sId, entropy) = WalletSeed {wsId = sId, wsEntropy = BA.convert entropy}
|
||||
|
||||
@@ -56,8 +70,26 @@ createWalletSeed db entropy nextAccount =
|
||||
"INSERT INTO wallet_seeds (entropy, next_account_index) VALUES (?, ?)"
|
||||
(DB.Binary (BA.convert entropy :: ByteString), accountIndexCol <$> nextAccount)
|
||||
|
||||
deleteWalletSeed :: DB.Connection -> SeedId -> IO ()
|
||||
deleteWalletSeed db sId = DB.execute db "DELETE FROM wallet_seeds WHERE wallet_seed_id = ?" (Only sId)
|
||||
-- | False if the device had no seed to delete. The account rows go with it.
|
||||
deleteWalletSeed :: DB.Connection -> IO Bool
|
||||
deleteWalletSeed db =
|
||||
getWalletSeed db >>= \case
|
||||
Nothing -> pure False
|
||||
Just WalletSeed {wsId} ->
|
||||
True <$ DB.execute db "DELETE FROM wallet_seeds WHERE wallet_seed_id = ?" (Only wsId)
|
||||
|
||||
-- | The seed, and the account an argument names or the next free one from the
|
||||
-- counter when it names none. Refuses an index BIP-32 cannot harden, the
|
||||
-- counter's included. The seed comes back with it so that a caller derives from
|
||||
-- the one the index was resolved against.
|
||||
resolveAccount :: DB.Connection -> Maybe AccountIndex -> IO (Either WalletError (WalletSeed, AccountIndex))
|
||||
resolveAccount db accountIdx_ = runExceptT $ do
|
||||
seed <- ExceptT $ maybe (Left WENoMaster) Right <$> getWalletSeed db
|
||||
n <- maybe (nextFreeAccount $ wsId seed) pure accountIdx_
|
||||
liftEither $ checkAccountIndex n
|
||||
pure (seed, n)
|
||||
where
|
||||
nextFreeAccount sId = ExceptT $ maybe (Left WECounterUnknown) Right <$> getNextAccountIndex db sId
|
||||
|
||||
-- | The index the next account takes. Nothing after an import, where the phrase
|
||||
-- does not say how many accounts it has been used for.
|
||||
@@ -87,9 +119,10 @@ accountUser db sId n =
|
||||
maybeFirstRow (fromOnly @(Maybe Int64)) $
|
||||
DB.query db "SELECT user_id FROM wallet_accounts WHERE wallet_seed_id = ? AND account_index = ?" (sId, accountIndexCol n)
|
||||
|
||||
-- | True when a profile other than this one holds the account, which is what
|
||||
-- keeps one profile from exporting another profile's key. An account no profile
|
||||
-- holds is not another profile's.
|
||||
-- | True when a profile other than this one holds the account, which keeps one
|
||||
-- profile from handing out another's account key. It is a guard, not a
|
||||
-- boundary: @export master@ reaches every account from any profile. An account
|
||||
-- no profile holds is not another profile's.
|
||||
heldByOther :: UserId -> Maybe (Maybe Int64) -> Bool
|
||||
heldByOther userId = \case
|
||||
Just (Just heldBy) -> heldBy /= userId
|
||||
@@ -99,32 +132,37 @@ accountHeldByOther :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO Bo
|
||||
accountHeldByOther db sId userId n = heldByOther userId <$> accountUser db sId n
|
||||
|
||||
-- | Bind an account to a profile: one the device already knows about, one a
|
||||
-- scan found, or the next free one when no index is given. Reading the counter
|
||||
-- and taking the account happen in one transaction, so two binds racing cannot
|
||||
-- both take it.
|
||||
bindAccount :: DB.Connection -> SeedId -> UserId -> Maybe AccountIndex -> IO (Either WalletError AccountIndex)
|
||||
bindAccount db sId userId accountIdx_ = runExceptT $ do
|
||||
n <- maybe nextFreeAccount pure accountIdx_
|
||||
liftEither $ checkAccountIndex n
|
||||
-- scan found, or the next free one when no index is given. The whole command is
|
||||
-- one transaction, so nothing between reading the counter and taking the
|
||||
-- account can hand the same one out twice.
|
||||
bindAccount :: DB.Connection -> UserId -> Maybe AccountIndex -> IO (Either WalletError ())
|
||||
bindAccount db userId accountIdx_ = runExceptT $ do
|
||||
(WalletSeed {wsId = sId}, n) <- ExceptT $ resolveAccount db accountIdx_
|
||||
held <- liftIO $ accountUser db sId n
|
||||
when (heldByOther userId held) $ throwError WEAccountBound
|
||||
liftIO $ do
|
||||
case held of
|
||||
Just (Just _) -> pure () -- already this profile's
|
||||
Just Nothing -> setAccountUser db sId userId n
|
||||
Nothing -> insertAccount db sId userId n
|
||||
raiseNextAccount db sId n
|
||||
pure n
|
||||
where
|
||||
nextFreeAccount = ExceptT $ maybe (Left WECounterUnknown) Right <$> getNextAccountIndex db sId
|
||||
taken <- liftIO $ case held of
|
||||
Just (Just _) -> pure True -- already this profile's
|
||||
-- the update takes the account only while no profile holds it, and the read
|
||||
-- after it says whether this one got it: where transactions are not
|
||||
-- serialised, two of them can both read the account as free
|
||||
Just Nothing -> setAccountUser db sId userId n >> accountHeldBy db sId userId n
|
||||
Nothing -> True <$ insertAccount db sId userId n
|
||||
unless taken $ throwError WEAccountBound
|
||||
liftIO $ raiseNextAccount db sId n
|
||||
|
||||
setAccountUser :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO ()
|
||||
setAccountUser db sId userId n =
|
||||
DB.execute
|
||||
db
|
||||
[sql| UPDATE wallet_accounts SET user_id = ? WHERE wallet_seed_id = ? AND account_index = ? |]
|
||||
[sql|
|
||||
UPDATE wallet_accounts SET user_id = ?
|
||||
WHERE wallet_seed_id = ? AND account_index = ? AND user_id IS NULL
|
||||
|]
|
||||
(userId, sId, accountIndexCol n)
|
||||
|
||||
accountHeldBy :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO Bool
|
||||
accountHeldBy db sId userId n = (== Just (Just userId)) <$> accountUser db sId n
|
||||
|
||||
-- | Keep the counter a high-water mark, so an account taken by index is not
|
||||
-- handed out again as the next free one. Never lowers it, and never gives a
|
||||
-- value to the imported phrase that has none.
|
||||
|
||||
+10
-23
@@ -10,10 +10,8 @@
|
||||
-- Nothing here knows about chat profiles. Which profile an account belongs to is
|
||||
-- a mapping in "Simplex.Chat.Store.Wallets".
|
||||
module Simplex.Chat.Wallet
|
||||
( SeedId,
|
||||
AccountIndex,
|
||||
( AccountIndex,
|
||||
AccountKey,
|
||||
WalletSeed (..),
|
||||
WalletAddress (..),
|
||||
WalletError (..),
|
||||
newSeedEntropy,
|
||||
@@ -33,7 +31,6 @@ import qualified Data.Aeson.TH as JQ
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteArray.Encoding as BAE
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int64)
|
||||
import Data.Text (Text)
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Word (Word32)
|
||||
@@ -43,8 +40,6 @@ import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Messaging.Eth.Address (ethereumPath)
|
||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
|
||||
|
||||
type SeedId = Int64
|
||||
|
||||
-- | BIP-44 account index. One account owns one thing on chain, one name to
|
||||
-- begin with.
|
||||
type AccountIndex = Word32
|
||||
@@ -52,15 +47,6 @@ type AccountIndex = Word32
|
||||
-- | The key at an account index. It owns whatever that account owns.
|
||||
type AccountKey = S.PrivateKey
|
||||
|
||||
-- | The device seed. The entropy is 'BA.ScrubbedBytes', so a derived 'Show'
|
||||
-- does not print it. Copies made for BIP-39 are plain 'ByteString' and are not
|
||||
-- wiped.
|
||||
data WalletSeed = WalletSeed
|
||||
{ wsId :: SeedId,
|
||||
wsEntropy :: BA.ScrubbedBytes
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- | One derived address, with the index it came from, so a caller that left the
|
||||
-- index out knows what it got.
|
||||
data WalletAddress = WalletAddress
|
||||
@@ -101,14 +87,14 @@ entropyFromMnemonic phrase = case B39.parseMnemonic phrase of
|
||||
Right . BA.convert $ B39.mnemonicToEntropy m
|
||||
_ -> Left WEBadMnemonic
|
||||
|
||||
seedMnemonic :: WalletSeed -> Either WalletError Text
|
||||
seedMnemonic s =
|
||||
bipError . fmap (decodeLatin1 . B39.mnemonicPhrase) . B39.entropyToMnemonic $ entropyBytes s
|
||||
seedMnemonic :: BA.ScrubbedBytes -> Either WalletError Text
|
||||
seedMnemonic entropy =
|
||||
bipError . fmap (decodeLatin1 . B39.mnemonicPhrase) . B39.entropyToMnemonic $ entropyBytes entropy
|
||||
|
||||
-- | Deriving this runs PBKDF2, so it is done once per command.
|
||||
seedMaster :: WalletSeed -> Either WalletError B32.ExtendedKey
|
||||
seedMaster s = do
|
||||
m <- bipError . B39.entropyToMnemonic $ entropyBytes s
|
||||
seedMaster :: BA.ScrubbedBytes -> Either WalletError B32.ExtendedKey
|
||||
seedMaster entropy = do
|
||||
m <- bipError . B39.entropyToMnemonic $ entropyBytes entropy
|
||||
bipError . B32.masterKey $ B39.mnemonicToSeed m ""
|
||||
|
||||
accountPath :: AccountIndex -> [Word32]
|
||||
@@ -124,8 +110,9 @@ deriveAccountKey master n = B32.xkKey <$> bipError (B32.derivePath master $ acco
|
||||
accountSecret :: AccountKey -> Text
|
||||
accountSecret k = "0x" <> decodeLatin1 (BAE.convertToBase BAE.Base16 $ S.unPrivateKey k)
|
||||
|
||||
entropyBytes :: WalletSeed -> ByteString
|
||||
entropyBytes = BA.convert . wsEntropy
|
||||
-- | The copy BIP-39 takes is a plain 'ByteString' and is not wiped.
|
||||
entropyBytes :: BA.ScrubbedBytes -> ByteString
|
||||
entropyBytes = BA.convert
|
||||
|
||||
-- | The BIP-32 and BIP-39 functions report failure as a string. For entropy
|
||||
-- this module produced only 'B32.masterKey' and 'B32.derivePath' can fail at
|
||||
|
||||
Reference in new issue
Block a user