/goal iteration 5 - unconfirmed

This commit is contained in:
Alain Brenzikofer committed 2026-09-21 17:03:54 +02:00
1 parent bfc4e1fbaa
commit 768a230f21
14 files changed
+328 -238

No files matched your search

+17 -27
View File
@@ -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;
+1 -1
View File
@@ -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
+64 -26
View File
@@ -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
View File
@@ -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