review iterations

This commit is contained in:
Alain Brenzikofer
2026-09-26 14:29:40 +02:00
parent 143d073980
commit 7b4ac1bb8b
13 changed files with 161 additions and 134 deletions
+3 -2
View File
@@ -69,8 +69,9 @@ import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme
import Simplex.Chat.Util (liftIOEither)
import Simplex.Chat.Wallet (AccountIndex, WalletAddress, WalletError)
import Simplex.Chat.Wallet (WalletAddress, WalletError, WalletInfo)
import Simplex.FileTransfer.Description (FileDescriptionURI)
import Simplex.Messaging.Crypto.BIP44 (AccountIndex)
import Simplex.Messaging.Server.Information (ServerPublicInfo)
import Simplex.Messaging.Agent (AgentClient, DatabaseDiff, SubscriptionsInfo)
import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo)
@@ -879,7 +880,7 @@ data ChatResponse
| CRBadgeRedeemed {user :: User, redeemedBadge :: LocalBadge, newBadge :: Bool, badgeState :: Maybe BadgeState}
| CRBadgeState {user :: User, badgeState :: Maybe BadgeState}
| CRBadgeLedger {user :: User, badgeLedger :: [StatementEntry]}
| CRWallet {user :: User, accountIndexes_ :: Maybe [AccountIndex]}
| CRWallet {user :: User, walletInfo_ :: Maybe WalletInfo}
| CRWalletMnemonic {user :: User, mnemonic :: Text}
| CRWalletAddress {user :: User, walletAddress :: WalletAddress}
| CRWalletAccountSecret {user :: User, walletAddress :: WalletAddress, secret :: Text}
+23 -20
View File
@@ -65,7 +65,7 @@ import Simplex.Chat.Badges.Types (BadgeAlert (..), BadgeAlertKind (..), BadgeIss
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.Wallet (AccountIndex, AccountKey, WalletAddress, WalletError (..), accountSecret, deriveAccount, importWalletMaster, newWalletMaster, seedMnemonic)
import Simplex.Chat.Wallet (AccountKey, WalletAddress, WalletError (..), WalletInfo (..), accountSecret, deriveAccount, importWalletMaster, masterMnemonic, newWalletMaster)
import Simplex.Chat.Call
import Simplex.Chat.Controller
import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..))
@@ -96,7 +96,7 @@ import Simplex.Chat.Store.Messages
import Simplex.Chat.Store.NoteFolders
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Chat.Store.Wallets (Wallet (..), accountHeldBy, bindAccount, createWallet, deleteWallet, getUserAccounts, getWallet, resolveAccount)
import Simplex.Chat.Store.Wallets (Wallet (..), bindAccount, createWallet, deleteWallet, getUserAccounts, getWallet, heldAccount, resolveAccount)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
@@ -116,7 +116,8 @@ import Simplex.Messaging.Agent.Store.Interface (getCurrentMigrations)
import Simplex.Messaging.Client (NetworkConfig (..), NetworkRequestMode (..), NetworkTimeout (..), SMPWebPortServers (..), SocksMode (SMAlways), pattern NRMInteractive, textToHostMode)
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.ShortLink as SL
import Simplex.Messaging.Crypto.BIP44 (mkAccountIndex)
import Simplex.Messaging.Crypto.BIP32 (WalletMaster)
import Simplex.Messaging.Crypto.BIP44 (AccountIndex, mkAccountIndex)
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
import qualified Simplex.Messaging.Crypto.File as CF
import Simplex.Messaging.Crypto.Ratchet (E2ERatchetParamsUri (..), InitialKeys (..), PQEncryption (..), PQSupport (..), pattern IKPQOff, pattern IKPQOn, pattern PQSupportOff, pattern PQSupportOn)
@@ -1501,32 +1502,30 @@ processChatCommand cxt nm = \case
connId <- withAgent $ \a -> sendServiceReplyAsync a "" (aUserId user) invId (LB.toStrict $ J.encode responseData)
pure $ CRServiceReplyAccepted user (AgentConnId connId)
APIGetWallet userId -> withUserId userId $ \user ->
CRWallet user <$> withFastStore (\db -> getWallet db >>= mapM (\Wallet {walletId} -> liftIO $ getUserAccounts db walletId userId))
CRWallet user <$> withFastStore (\db -> getWallet db >>= mapM (\Wallet {walletId, nextAccountIndex} -> liftIO $ (`WalletInfo` nextAccountIndex) <$> getUserAccounts db userId walletId))
APICreateWallet mnemonic_ -> withUser $ \user -> do
wallet_ <- withFastStore getWallet
when (isJust wallet_) $ throwWalletError WEMasterExists
-- the counter starts at 0 for a generated seed and is unknown for an imported one
-- the counter starts at 1 for a generated seed, leaving account 0 to other wallets, and is unknown for an imported one
(master, nextAccount) <- case mnemonic_ of
Nothing -> (,Just 0) <$> (liftIO . newWalletMaster =<< asks random)
Nothing -> (,Just 1) <$> (liftIO . newWalletMaster =<< asks random)
Just phrase -> (,Nothing) <$> liftWallet (importWalletMaster phrase)
created <- withFastStore' $ \db -> createWallet db master nextAccount
unless created $ throwWalletError WEMasterExists
pure $ CRWallet user (Just [])
pure $ CRWallet user (Just $ WalletInfo [] nextAccount)
APIBindWalletAccount userId accountIdx_ -> withUserId userId $ \user@User {viewPwdHash} -> do
when (isJust viewPwdHash) $ throwWalletError WEHiddenProfile
(wallet, n) <- withWalletStore $ \db -> bindAccount db userId accountIdx_
CRWalletAddress user . snd <$> walletAccount wallet n
(master, n) <- withWalletStore $ \db -> bindAccount db userId accountIdx_
CRWalletAddress user . snd <$> walletAccount master n
APIGetWalletAddress accountIdx_ -> withUser $ \user -> do
(wallet, n) <- withWalletStore (`resolveAccount` accountIdx_)
CRWalletAddress user . snd <$> walletAccount wallet n
(master, n) <- withWalletStore (`resolveAccount` accountIdx_)
CRWalletAddress user . snd <$> walletAccount master n
APIExportWalletMnemonic -> withUser $ \user -> do
Wallet {walletMaster} <- withFastStore getWallet >>= maybe (throwWalletError WENoMaster) pure
pure $ CRWalletMnemonic user (seedMnemonic walletMaster)
pure $ CRWalletMnemonic user (masterMnemonic walletMaster)
APIExportWalletAccount userId n -> withUserId userId $ \user -> do
(wallet@Wallet {walletId}, _) <- withWalletStore (`resolveAccount` Just n)
held <- withFastStore' $ \db -> accountHeldBy db walletId userId n
unless held $ throwWalletError WEAccountNotHeld
(k, a) <- walletAccount wallet n
master <- withWalletStore $ \db -> heldAccount db userId n
(k, a) <- walletAccount master n
pure $ CRWalletAccountSecret user a (accountSecret k)
APIDeleteWallet -> withUser_ $ do
deleted <- withFastStore' deleteWallet
@@ -6035,10 +6034,10 @@ liftWallet = liftEitherWith (ChatError . CEWallet)
withWalletStore :: (DB.Connection -> ExceptT StoreError IO (Either WalletError a)) -> CM a
withWalletStore action = liftWallet =<< withFastStore action
walletAccount :: Wallet -> AccountIndex -> CM (AccountKey, WalletAddress)
walletAccount Wallet {walletMaster} n = do
walletAccount :: WalletMaster -> AccountIndex -> CM (AccountKey, WalletAddress)
walletAccount master n = do
g <- asks random
liftIO $ deriveAccount g walletMaster n
liftIO $ deriveAccount g master n
chatCommandP :: Parser ChatCommand
chatCommandP =
@@ -6714,8 +6713,12 @@ chatCommandP =
text1P = safeDecodeUtf8 <$> A.takeTill (== ' ')
char_ = optional . A.char
accountIndexP = do
ds <- A.lookAhead $ A.takeWhile1 isDigit
when (B.length (B.dropWhile (== '0') ds) > 10) tooLarge
i <- A.decimal
maybe (fail "account index too large") pure $ if i <= toInteger (maxBound :: Word32) then mkAccountIndex (fromInteger i) else Nothing
if i <= toInteger (maxBound :: Word32) then either fail pure $ mkAccountIndex (fromInteger i) else tooLarge
where
tooLarge = fail "account index too large"
displayNameP :: Parser Text
displayNameP = safeDecodeUtf8 <$> displayNameP_
@@ -11,7 +11,7 @@ m20260924_wallet_seeds =
[r|
CREATE TABLE wallet_seeds (
wallet_seed_id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
entropy BYTEA NOT NULL CHECK (length(entropy) = 32),
entropy BYTEA NOT NULL CHECK (length(entropy) IN (16, 20, 24, 28, 32)),
master BYTEA NOT NULL CHECK (length(master) = 64),
next_account_index BIGINT CHECK (next_account_index BETWEEN 0 AND 2147483648),
single_seed SMALLINT NOT NULL DEFAULT 1
@@ -1757,7 +1757,7 @@ CREATE TABLE test_chat_schema.wallet_seeds (
master bytea 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_entropy_check CHECK ((length(entropy) = ANY (ARRAY[16, 20, 24, 28, 32]))),
CONSTRAINT wallet_seeds_master_check CHECK ((length(master) = 64)),
CONSTRAINT wallet_seeds_next_account_index_check CHECK (((next_account_index >= 0) AND (next_account_index <= '2147483648'::bigint)))
);
@@ -10,7 +10,7 @@ m20260924_wallet_seeds =
[sql|
CREATE TABLE wallet_seeds (
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
entropy BLOB NOT NULL CHECK (length(entropy) = 32),
entropy BLOB NOT NULL CHECK (length(entropy) IN (16, 20, 24, 28, 32)),
master BLOB NOT NULL CHECK (length(master) = 64),
next_account_index INTEGER CHECK (next_account_index BETWEEN 0 AND 2147483648),
single_seed INTEGER NOT NULL DEFAULT 1
@@ -983,7 +983,7 @@ CREATE TABLE badge_code_redemptions(
) STRICT;
CREATE TABLE wallet_seeds(
wallet_seed_id INTEGER PRIMARY KEY AUTOINCREMENT,
entropy BLOB NOT NULL CHECK(length(entropy) = 32),
entropy BLOB NOT NULL CHECK(length(entropy) IN(16, 20, 24, 28, 32)),
master BLOB NOT NULL CHECK(length(master) = 64),
next_account_index INTEGER CHECK(next_account_index BETWEEN 0 AND 2147483648),
single_seed INTEGER NOT NULL DEFAULT 1
+31 -24
View File
@@ -13,25 +13,26 @@ module Simplex.Chat.Store.Wallets
deleteWallet,
resolveAccount,
getUserAccounts,
accountHeldBy,
heldAccount,
bindAccount,
)
where
import Control.Monad.Except
import Data.Bifunctor (first)
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import Data.Int (Int64)
import Data.Word (Word32)
import Simplex.Chat.Store.Shared (StoreError (..))
import Simplex.Chat.Wallet (AccountIndex, WalletError (..))
import Simplex.Chat.Wallet (WalletError (..))
import Simplex.Messaging.Agent.Protocol (UserId)
import Simplex.Messaging.Agent.Store.AgentStore (maybeFirstRow)
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Crypto.BIP32 (WalletMaster, masterBytes, masterEntropy, parseWalletMaster)
import Simplex.Messaging.Crypto.BIP39 (unEntropy)
import Simplex.Messaging.Crypto.BIP44 (mkAccountIndex, unAccountIndex)
import Simplex.Messaging.Crypto.BIP44 (AccountIndex, mkAccountIndex, unAccountIndex)
import Simplex.Messaging.Util (liftEitherWith)
#if defined(dbPostgres)
@@ -47,7 +48,7 @@ type SeedId = Int64
data Wallet = Wallet
{ walletId :: SeedId,
walletMaster :: WalletMaster,
nextAccount :: Maybe Word32
nextAccountIndex :: Maybe Word32
}
getWallet :: DB.Connection -> ExceptT StoreError IO (Maybe Wallet)
@@ -56,8 +57,8 @@ getWallet db =
>>= mapM toWallet
where
toWallet :: (SeedId, ByteString, ByteString, Maybe Word32) -> ExceptT StoreError IO Wallet
toWallet (walletId, entropy, master, nextAccount) =
liftEitherWith SEInternalError $ (\walletMaster -> Wallet {walletId, walletMaster, nextAccount}) <$> parseWalletMaster (BA.convert entropy) (BA.convert master)
toWallet (walletId, entropy, master, nextAccountIndex) =
liftEitherWith SEInternalError $ (\walletMaster -> Wallet {walletId, walletMaster, nextAccountIndex}) <$> parseWalletMaster (BA.convert entropy) (BA.convert master)
createWallet :: DB.Connection -> WalletMaster -> Maybe Word32 -> IO Bool
createWallet db master nextAccount =
@@ -75,16 +76,19 @@ deleteWallet :: DB.Connection -> IO Bool
deleteWallet db =
rowReturned $ DB.query_ db "DELETE FROM wallet_seeds RETURNING wallet_seed_id"
resolveAccount :: DB.Connection -> Maybe AccountIndex -> ExceptT StoreError IO (Either WalletError (Wallet, AccountIndex))
resolveAccount db accountIdx_ =
resolveAccount :: DB.Connection -> Maybe AccountIndex -> ExceptT StoreError IO (Either WalletError (WalletMaster, AccountIndex))
resolveAccount db accountIdx_ = fmap (first walletMaster) <$> resolveAccount_ db accountIdx_
resolveAccount_ :: DB.Connection -> Maybe AccountIndex -> ExceptT StoreError IO (Either WalletError (Wallet, AccountIndex))
resolveAccount_ db accountIdx_ =
getWallet db >>= \case
Nothing -> pure $ Left WENoMaster
Just w@Wallet {nextAccount} -> pure $ (w,) <$> maybe next Right accountIdx_
Just w@Wallet {nextAccountIndex} -> pure $ (w,) <$> maybe next Right accountIdx_
where
next = maybe (Left WECounterUnknown) (maybe (Left WEAccountsExhausted) Right . mkAccountIndex) nextAccount
next = maybe (Left WECounterUnknown) (first (const WEAccountsExhausted) . mkAccountIndex) nextAccountIndex
getUserAccounts :: DB.Connection -> SeedId -> UserId -> IO [AccountIndex]
getUserAccounts db sId userId =
getUserAccounts :: DB.Connection -> UserId -> SeedId -> IO [AccountIndex]
getUserAccounts db userId sId =
map fromOnly
<$> DB.query
db
@@ -101,19 +105,19 @@ accountUser db sId n =
maybeFirstRow fromOnly $
DB.query db "SELECT user_id FROM wallet_accounts WHERE wallet_seed_id = ? AND account_index = ?" (sId, n)
bindAccount :: DB.Connection -> UserId -> Maybe AccountIndex -> ExceptT StoreError IO (Either WalletError (Wallet, AccountIndex))
bindAccount db userId accountIdx_ = resolveAccount db accountIdx_ >>= either (pure . Left) (liftIO . bind)
bindAccount :: DB.Connection -> UserId -> Maybe AccountIndex -> ExceptT StoreError IO (Either WalletError (WalletMaster, AccountIndex))
bindAccount db userId accountIdx_ = resolveAccount_ db accountIdx_ >>= either (pure . Left) (liftIO . bind)
where
bind r@(Wallet {walletId}, n) = do
bind (Wallet {walletId, walletMaster}, n) = do
held <-
accountUser db walletId n >>= \case
Just (Just heldBy) -> pure $ heldBy == userId
Just Nothing -> setAccountUser db walletId userId n
Nothing -> True <$ insertAccount db walletId userId n
if held then Right r <$ raiseNextAccount db walletId n else pure $ Left WEAccountBound
Just Nothing -> setAccountUser db userId walletId n
Nothing -> True <$ insertAccount db userId walletId n
if held then Right (walletMaster, n) <$ raiseNextAccount db walletId n else pure $ Left WEAccountBound
setAccountUser :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO Bool
setAccountUser db sId userId n =
setAccountUser :: DB.Connection -> UserId -> SeedId -> AccountIndex -> IO Bool
setAccountUser db userId sId n =
rowReturned $
DB.query
db
@@ -124,8 +128,11 @@ setAccountUser db sId userId n =
|]
(userId, sId, n)
accountHeldBy :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO Bool
accountHeldBy db sId userId n = (== Just (Just userId)) <$> accountUser db sId n
heldAccount :: DB.Connection -> UserId -> AccountIndex -> ExceptT StoreError IO (Either WalletError WalletMaster)
heldAccount db userId n =
getWallet db >>= \case
Nothing -> pure $ Left WENoMaster
Just Wallet {walletId, walletMaster} -> liftIO $ (\held -> if held == Just (Just userId) then Right walletMaster else Left WEAccountNotHeld) <$> accountUser db walletId n
raiseNextAccount :: DB.Connection -> SeedId -> AccountIndex -> IO ()
raiseNextAccount db sId n =
@@ -137,8 +144,8 @@ raiseNextAccount db sId n =
|]
(unAccountIndex n + 1, sId, n)
insertAccount :: DB.Connection -> SeedId -> UserId -> AccountIndex -> IO ()
insertAccount db sId userId n =
insertAccount :: DB.Connection -> UserId -> SeedId -> AccountIndex -> IO ()
insertAccount db userId sId n =
DB.execute db "INSERT INTO wallet_accounts (wallet_seed_id, account_index, user_id) VALUES (?, ?, ?)" (sId, n, userId)
rowReturned :: IO [Only Int64] -> IO Bool
+8 -6
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 (..), WalletInfo (..))
import qualified Simplex.FileTransfer.Transport as XFTP
import Simplex.Messaging.Agent (DatabaseDiff (..))
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..))
@@ -197,10 +197,12 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, showFullLinks, te
CRBadgeRedeemed u badge newBadge _ -> ttyUser u $ if newBadge then "badge redeemed" : viewContactBadge (Just badge) else ["badge already redeemed"]
CRBadgeState u st -> ttyUser u $ viewUserBadgeState st
CRBadgeLedger u entries -> ttyUser u $ viewBadgeLedger entries
CRWallet u accounts_ -> ttyUser u $ case accounts_ of
CRWallet u info_ -> ttyUser u $ case info_ of
Nothing -> ["no wallet on this device"]
Just [] -> ["wallet, no accounts for this profile"]
Just accounts -> [plain $ "accounts: " <> T.intercalate ", " (map (tshow . unAccountIndex) accounts)]
Just WalletInfo {accountIndexes, nextAccountIndex} ->
let next = maybe "unknown" tshow nextAccountIndex
accounts = if null accountIndexes then "no accounts for this profile" else "accounts: " <> T.intercalate ", " (map (tshow . unAccountIndex) accountIndexes)
in [plain $ "wallet, next account " <> next <> ", " <> accounts]
CRWalletMnemonic u mnemonic -> ttyUser u [plain mnemonic]
CRWalletAddress u a -> ttyUser u [walletAddressRow a]
CRWalletAccountSecret u a secret -> ttyUser u [walletAddressRow a <> " " <> plain secret]
@@ -1113,7 +1115,7 @@ viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of
walletAddressRow :: WalletAddress -> StyledString
walletAddressRow WalletAddress {accountIndex, keyPath, address} =
plain $ tshow (unAccountIndex accountIndex) <> " " <> keyPath <> " " <> address
plain $ tshow (unAccountIndex accountIndex) <> " " <> keyPath <> " " <> safeDecodeUtf8 (strEncode address)
viewContactsList :: [Contact] -> [StyledString]
viewContactsList =
@@ -2817,7 +2819,7 @@ viewChatError isCmd logLevel testView = \case
reason = case walletErr of
WENoMaster -> "this device has no wallet"
WEMasterExists -> "this device already has a wallet"
WEBadMnemonic -> "not a valid 24 word recovery phrase"
WEBadMnemonic -> "not a valid recovery phrase"
WEHiddenProfile -> "an account cannot be bound to a hidden profile"
WEAccountBound -> "another profile holds this account"
WEAccountNotHeld -> "this profile does not hold this account"
+21 -14
View File
@@ -2,14 +2,14 @@
{-# LANGUAGE TemplateHaskell #-}
module Simplex.Chat.Wallet
( AccountIndex,
AccountKey,
( AccountKey,
WalletAddress (..),
WalletInfo (..),
WalletError (..),
newWalletMaster,
entropyFromMnemonic,
importWalletMaster,
seedMnemonic,
masterMnemonic,
deriveAccount,
accountSecret,
)
@@ -18,15 +18,16 @@ where
import Control.Concurrent.STM
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson.TH as JQ
import Data.Bifunctor (first)
import qualified Data.ByteArray.Encoding as BAE
import Data.Text (Text)
import Data.Text.Encoding (decodeLatin1)
import Data.Word (Word32)
import qualified Simplex.Messaging.Crypto.BIP32 as B32
import qualified Simplex.Messaging.Crypto.BIP39 as B39
import Simplex.Messaging.Crypto.BIP44 (AccountIndex, CoinType (..), bip44Path)
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
import Simplex.Messaging.Encoding.String (strEncode)
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
import Simplex.Messaging.Eth.Address (Address, addressFromPrivateKey)
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON)
type AccountKey = S.Secp256k1PrivateKey
@@ -34,7 +35,13 @@ type AccountKey = S.Secp256k1PrivateKey
data WalletAddress = WalletAddress
{ accountIndex :: AccountIndex,
keyPath :: Text,
address :: Text
address :: Address
}
deriving (Show)
data WalletInfo = WalletInfo
{ accountIndexes :: [AccountIndex],
nextAccountIndex :: Maybe Word32
}
deriving (Show)
@@ -53,24 +60,22 @@ masterStrength :: B39.EntropyStrength
masterStrength = B39.ES256
newWalletMaster :: TVar ChaChaDRG -> IO B32.WalletMaster
newWalletMaster g = (`B32.mkWalletMaster` "") <$> atomically (B39.randomEntropy masterStrength g)
newWalletMaster g = B32.mkWalletMaster <$> atomically (B39.randomEntropy masterStrength g)
entropyFromMnemonic :: Text -> Either WalletError B39.WalletEntropy
entropyFromMnemonic phrase = case B39.parsePhrase phrase of
Right ent | B39.entropyWordCount ent == 24 -> Right ent
_ -> Left WEBadMnemonic
entropyFromMnemonic = first (const WEBadMnemonic) . B39.parsePhrase
importWalletMaster :: Text -> Either WalletError B32.WalletMaster
importWalletMaster phrase = (`B32.mkWalletMaster` "") <$> entropyFromMnemonic phrase
importWalletMaster phrase = B32.mkWalletMaster <$> entropyFromMnemonic phrase
seedMnemonic :: B32.WalletMaster -> Text
seedMnemonic = decodeLatin1 . B39.entropyPhrase . B32.masterEntropy
masterMnemonic :: B32.WalletMaster -> Text
masterMnemonic = decodeLatin1 . B39.entropyPhrase . B32.masterEntropy
deriveAccount :: TVar ChaChaDRG -> B32.WalletMaster -> AccountIndex -> IO (AccountKey, WalletAddress)
deriveAccount g master n = do
k <- B32.xkKey <$> B32.derivePath g (B32.walletMasterKey master) path
a <- addressFromPrivateKey g k
pure (k, WalletAddress {accountIndex = n, keyPath = decodeLatin1 $ B32.renderPath path, address = decodeLatin1 $ strEncode a})
pure (k, WalletAddress {accountIndex = n, keyPath = decodeLatin1 $ B32.renderPath path, address = a})
where
path = bip44Path Ethereum n
@@ -79,4 +84,6 @@ accountSecret k = "0x" <> decodeLatin1 (BAE.convertToBase BAE.Base16 $ S.unPriva
$(JQ.deriveJSON defaultJSON ''WalletAddress)
$(JQ.deriveJSON defaultJSON ''WalletInfo)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "WE") ''WalletError)