introduce name model for db to put scanned names into

This commit is contained in:
Alain Brenzikofer committed 2026-09-23 14:10:12 +02:00
1 parent defd51cd81
commit 6999e63a66
10 files changed
+141 -11

No files matched your search

+2
View File
@@ -169,6 +169,7 @@ library
Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges
Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges
Simplex.Chat.Store.Postgres.Migrations.M20260921_wallet_seeds
Simplex.Chat.Store.Postgres.Migrations.M20260923_wallet_names
else
exposed-modules:
Simplex.Chat.Archive
@@ -345,6 +346,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges
Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges
Simplex.Chat.Store.SQLite.Migrations.M20260921_wallet_seeds
Simplex.Chat.Store.SQLite.Migrations.M20260923_wallet_names
other-modules:
Paths_simplex_chat
hs-source-dirs:
+4 -4
View File
@@ -68,7 +68,7 @@ import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), cla
import Simplex.Chat.Store.Wallets (WalletSeed (..), accountHeldByOther, bindAccount, createWalletSeed, deleteWalletSeed, getUserAccounts, 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 (OwnedNames (..))
import Simplex.Messaging.Names.Record (NameResponse, OwnedNames (..))
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..))
@@ -5971,17 +5971,17 @@ accountKey :: WalletSeed -> AccountIndex -> CM AccountKey
accountKey seed n = liftWallet $ seedMaster (wsEntropy seed) >>= (`deriveAccountKey` n)
-- | Walk the accounts until 'scanGapLimit' in a row are untouched, or 'scanMaxAccounts' is reached, asking each on a relay the scan has not used where the configured set allows.
scanAccounts :: NetworkRequestMode -> User -> WalletSeed -> CM [AccountIndex]
scanAccounts :: NetworkRequestMode -> User -> WalletSeed -> CM [(AccountIndex, [NameResponse])]
scanAccounts nm user seed = do
master <- liftWallet $ seedMaster (wsEntropy seed)
let go :: AccountIndex -> [SMPServer] -> [AccountIndex] -> Int -> CM [AccountIndex]
let go :: AccountIndex -> [SMPServer] -> [(AccountIndex, [NameResponse])] -> Int -> CM [(AccountIndex, [NameResponse])]
go n used found gap
| gap >= scanGapLimit || n >= scanMaxAccounts = pure (reverse found)
| otherwise = do
addr <- addressFromPrivateKey <$> liftWallet (deriveAccountKey master n)
(srv, owned) <- withAgent $ \a -> ownedSimplexNames a nm (aUserId user) used addr 0
if ownInUse owned
then go (n + 1) (srv : used) (n : found) 0
then go (n + 1) (srv : used) ((n, ownNames owned) : found) 0
else go (n + 1) (srv : used) found (gap + 1)
go 0 [] [] 0
@@ -52,6 +52,7 @@ import Simplex.Chat.Store.Postgres.Migrations.M20260828_file_expiry
import Simplex.Chat.Store.Postgres.Migrations.M20260904_file_badges
import Simplex.Chat.Store.Postgres.Migrations.M20260915_user_badges
import Simplex.Chat.Store.Postgres.Migrations.M20260921_wallet_seeds
import Simplex.Chat.Store.Postgres.Migrations.M20260923_wallet_names
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)]
@@ -103,7 +104,8 @@ schemaMigrations =
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges),
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260921_wallet_seeds", m20260921_wallet_seeds, Nothing)
("20260921_wallet_seeds", m20260921_wallet_seeds, Nothing),
("20260923_wallet_names", m20260923_wallet_names, Nothing)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,24 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20260923_wallet_names where
import Data.Text (Text)
import Text.RawString.QQ (r)
m20260923_wallet_names :: Text
m20260923_wallet_names =
[r|
-- the columns are commented in the SQLite migration
CREATE TABLE wallet_names (
wallet_name_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
wallet_seed_id BIGINT NOT NULL REFERENCES wallet_seeds ON DELETE CASCADE,
account_index BIGINT NOT NULL CHECK (account_index BETWEEN 0 AND 2147483647),
name TEXT NOT NULL,
name_response TEXT NOT NULL
);
CREATE UNIQUE INDEX idx_wallet_names_wallet_seed_id_name ON wallet_names(wallet_seed_id, name);
|]
-- No reverse step: wallet_seeds, which the rows hang off, has none either.
@@ -1745,6 +1745,28 @@ ALTER TABLE test_chat_schema.wallet_accounts ALTER COLUMN wallet_account_id ADD
CREATE TABLE test_chat_schema.wallet_names (
wallet_name_id bigint NOT NULL,
wallet_seed_id bigint NOT NULL,
account_index bigint NOT NULL,
name text NOT NULL,
name_response text NOT NULL,
CONSTRAINT wallet_names_account_index_check CHECK (((account_index >= 0) AND (account_index <= 2147483647)))
);
ALTER TABLE test_chat_schema.wallet_names ALTER COLUMN wallet_name_id ADD GENERATED ALWAYS AS IDENTITY (
SEQUENCE NAME test_chat_schema.wallet_names_wallet_name_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,
@@ -2185,6 +2207,11 @@ ALTER TABLE ONLY test_chat_schema.wallet_accounts
ALTER TABLE ONLY test_chat_schema.wallet_names
ADD CONSTRAINT wallet_names_pkey PRIMARY KEY (wallet_name_id);
ALTER TABLE ONLY test_chat_schema.wallet_seeds
ADD CONSTRAINT wallet_seeds_pkey PRIMARY KEY (wallet_seed_id);
@@ -2999,6 +3026,10 @@ CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON test_cha
CREATE UNIQUE INDEX idx_wallet_names_wallet_seed_id_name ON test_chat_schema.wallet_names USING btree (wallet_seed_id, name);
CREATE UNIQUE INDEX idx_wallet_seeds_single_seed ON test_chat_schema.wallet_seeds USING btree (single_seed);
@@ -3738,6 +3769,11 @@ ALTER TABLE ONLY test_chat_schema.wallet_accounts
ALTER TABLE ONLY test_chat_schema.wallet_names
ADD CONSTRAINT wallet_names_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;
+3 -1
View File
@@ -175,6 +175,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20260828_file_expiry
import Simplex.Chat.Store.SQLite.Migrations.M20260904_file_badges
import Simplex.Chat.Store.SQLite.Migrations.M20260915_user_badges
import Simplex.Chat.Store.SQLite.Migrations.M20260921_wallet_seeds
import Simplex.Chat.Store.SQLite.Migrations.M20260923_wallet_names
import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)]
@@ -349,7 +350,8 @@ schemaMigrations =
("20260828_file_expiry", m20260828_file_expiry, Just down_m20260828_file_expiry),
("20260904_file_badges", m20260904_file_badges, Just down_m20260904_file_badges),
("20260915_user_badges", m20260915_user_badges, Just down_m20260915_user_badges),
("20260921_wallet_seeds", m20260921_wallet_seeds, Nothing)
("20260921_wallet_seeds", m20260921_wallet_seeds, Nothing),
("20260923_wallet_names", m20260923_wallet_names, Nothing)
]
-- | The list of migrations in ascending order by date
@@ -0,0 +1,22 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20260923_wallet_names where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20260923_wallet_names :: Query
m20260923_wallet_names =
[sql|
CREATE TABLE wallet_names (
wallet_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_seed_id INTEGER NOT NULL REFERENCES wallet_seeds ON DELETE CASCADE,
account_index INTEGER NOT NULL CHECK (account_index BETWEEN 0 AND 2147483647),
name TEXT NOT NULL,
name_response TEXT NOT NULL -- the NameResponse a scan last read for this name
) STRICT;
CREATE UNIQUE INDEX idx_wallet_names_wallet_seed_id_name ON wallet_names(wallet_seed_id, name);
|]
-- No reverse step: wallet_seeds, which the rows hang off, has none either.
@@ -987,6 +987,13 @@ CREATE TABLE wallet_accounts(
account_index INTEGER CHECK(account_index BETWEEN 0 AND 2147483647),
user_id INTEGER REFERENCES users ON DELETE SET NULL
) STRICT;
CREATE TABLE wallet_names(
wallet_name_id INTEGER PRIMARY KEY AUTOINCREMENT,
wallet_seed_id INTEGER NOT NULL REFERENCES wallet_seeds ON DELETE CASCADE,
account_index INTEGER NOT NULL CHECK(account_index BETWEEN 0 AND 2147483647),
name TEXT NOT NULL,
name_response TEXT NOT NULL -- the NameResponse a scan last read for this name
) STRICT;
CREATE INDEX contact_profiles_index ON contact_profiles(
display_name,
full_name
@@ -1556,6 +1563,10 @@ CREATE UNIQUE INDEX idx_wallet_accounts_wallet_seed_id_account_index ON wallet_a
account_index
);
CREATE INDEX idx_wallet_accounts_user_id ON wallet_accounts(user_id);
CREATE UNIQUE INDEX idx_wallet_names_wallet_seed_id_name ON wallet_names(
wallet_seed_id,
name
);
CREATE TRIGGER on_group_members_insert_update_summary
AFTER INSERT ON group_members
FOR EACH ROW
+20 -4
View File
@@ -20,7 +20,7 @@ module Simplex.Chat.Store.Wallets
)
where
import Control.Monad (join, unless)
import Control.Monad (forM_, join, unless)
import Control.Monad.Except
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteArray as BA
@@ -28,8 +28,10 @@ import Data.ByteString (ByteString)
import Data.Int (Int64)
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), checkAccountIndex)
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Names.Record (NameRecord (..), NameRegistration (..), NameResponse (..))
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Util (encodeJSON)
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..))
@@ -145,8 +147,8 @@ setAccountUser db sId userId n =
accountHeldBy :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO Bool
accountHeldBy db sId userId n = (== Just (Just userId)) <$> accountUser db sId n
-- | The accounts a scan found, bound to the profile that ran it, and the counter raised past every account recorded, not just those found.
recordScan :: DB.Connection -> SeedId -> UserId -> [AccountIndex] -> IO ()
-- | The accounts a scan found with the names each holds, bound to the profile that ran it, and the counter raised past every account recorded, not just those found.
recordScan :: DB.Connection -> SeedId -> UserId -> [(AccountIndex, [NameResponse])] -> IO ()
recordScan db sId userId inUse = do
mapM_ insertScanned inUse
DB.execute
@@ -158,7 +160,7 @@ recordScan db sId userId inUse = do
|]
(sId, sId, sId)
where
insertScanned n =
insertScanned (n, names) = do
DB.execute
db
[sql|
@@ -166,6 +168,20 @@ recordScan db sId userId inUse = do
ON CONFLICT (wallet_seed_id, account_index) DO NOTHING
|]
(sId, accountIndexCol n, userId)
mapM_ (insertName n) names
-- a name the scan no longer sees is left alone: it reads one page per account, so unseen is not unowned
insertName n r = forM_ (registeredName r) $ \nm ->
DB.execute
db
[sql|
INSERT INTO wallet_names (wallet_seed_id, account_index, name, name_response) VALUES (?, ?, ?, ?)
ON CONFLICT (wallet_seed_id, name)
DO UPDATE SET account_index = EXCLUDED.account_index, name_response = EXCLUDED.name_response
|]
(sId, accountIndexCol n, nm, encodeJSON r)
registeredName NameResponse {registration} = case registration of
NRRegistered {nameRecord} -> Just $ nrName nameRecord
_ -> Nothing
-- | Keep the counter a high-water mark. Never lowers it, never gives one to an imported phrase that has none.
raiseNextAccount :: DB.Connection -> SeedId -> AccountIndex -> IO ()
+16 -1
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PostfixOperators #-}
@@ -12,6 +13,7 @@ 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
@@ -19,8 +21,11 @@ import NameResolver (ownedName)
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), accountSecret, deriveAccountKey, entropyFromMnemonic, renderAccountPath, seedMaster, seedMnemonic)
import qualified Simplex.Messaging.Crypto.BIP39 as B39
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Agent.Store.AgentStore (withTransaction)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
import Simplex.Messaging.Util (safeDecodeUtf8)
import Simplex.Messaging.Names.Record (NameRecord (..), NameRegistration (..), NameResponse (..))
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8)
import Test.Hspec hiding (it)
import qualified Test.Hspec as Hspec
@@ -115,6 +120,10 @@ testWalletScan ps = withSmpServerAndNames $ \reg -> withNewTestChat ps "alice" a
ownedName reg "alice.simplex" (accountAddress 1)
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_names"
map (\(n, nm, _) -> (n, nm)) names `shouldBe` [(1 :: Int64, "alice.simplex" :: Text)]
map (\(_, _, 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"
@@ -299,6 +308,12 @@ testWalletScanHiddenProfile ps = withSmpServerAndNames $ \_reg -> withNewTestCha
alice ##> "/_wallet scan"
alice <## "wallet: a hidden profile cannot own an account"
-- | The name a stored NameResponse carries, which is what makes the row findable again.
registeredName :: NameResponse -> Maybe Text
registeredName NameResponse {registration} = case registration of
NRRegistered {nameRecord} -> Just $ nrName nameRecord
_ -> Nothing
testWalletPersists :: HasCallStack => TestParams -> IO ()
testWalletPersists ps = do
phrase <- withNewTestChat ps "alice" aliceProfile $ \alice -> do