mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-23 06:22:43 +00:00
/goal iteration 4 - unconfirmed
This commit is contained in:
1 parent
4f21e68972
commit
bfc4e1fbaa
17 files changed
+798
-280
No files matched your search
+240
-116
@@ -6,159 +6,283 @@ module WalletTests where
|
||||
import ChatClient
|
||||
import ChatTests.DBUtils
|
||||
import ChatTests.Utils
|
||||
import qualified Data.ByteArray as BA
|
||||
import qualified Data.ByteArray.Encoding as BAE
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Either (isLeft)
|
||||
import Data.Either (isRight)
|
||||
import Data.List (nub)
|
||||
import qualified Data.Text as T
|
||||
import Simplex.Chat.Wallet (AccountIndex, WalletError (..), WalletSeed (..), accountSecret, deriveAccountKey, entropyFromMnemonic, renderAccountPath, seedMaster, seedMnemonic)
|
||||
import qualified Simplex.Messaging.Crypto.BIP39 as B39
|
||||
import qualified Simplex.Messaging.Crypto.Secp256k1 as S
|
||||
import Simplex.Chat.Controller (redactedCommand)
|
||||
import Simplex.Chat.Wallet (NameIndex, WalletSeed (..), deriveNameKey, importRecoveryKey, nameKeySecret, recoveryKeyPhrase, renderNameKeyPath, seedMaster)
|
||||
import Simplex.Messaging.Eth.Address (addressFromPrivateKey)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8)
|
||||
import Test.Hspec hiding (it)
|
||||
import qualified Test.Hspec as Hspec
|
||||
|
||||
-- | The standard BIP-39 test vector, so the addresses can be checked elsewhere.
|
||||
testPhrase :: ByteString
|
||||
testPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
-- | The standard BIP-39 test vector, 12 words, so the addresses below can be
|
||||
-- checked against any other wallet. Importing takes 24 words, so this phrase is
|
||||
-- only used for derivation, never through a command.
|
||||
testPhrase12 :: ByteString
|
||||
testPhrase12 = B.unwords $ replicate 11 "abandon" <> ["about"]
|
||||
|
||||
testSeed :: WalletSeed
|
||||
testSeed = WalletSeed {wsId = 1, wsEntropy = either error id $ importRecoveryKey testPhrase}
|
||||
-- | The 24 word all-zero-entropy vector, the length the commands take. It is a
|
||||
-- different seed from the 12 word one, so it reaches different addresses.
|
||||
testPhrase24 :: ByteString
|
||||
testPhrase24 = B.unwords $ replicate 23 "abandon" <> ["art"]
|
||||
|
||||
nameKey :: NameIndex -> Either String S.PrivateKey
|
||||
nameKey nm = seedMaster testSeed >>= \m -> deriveNameKey m nm
|
||||
seedFromPhrase :: ByteString -> WalletSeed
|
||||
seedFromPhrase phrase =
|
||||
WalletSeed {wsId = 1, wsEntropy = BA.convert . B39.mnemonicToEntropy . either error id $ B39.parseMnemonic phrase}
|
||||
|
||||
accountKey :: WalletSeed -> AccountIndex -> S.PrivateKey
|
||||
accountKey seed n = either (error . show) id $ seedMaster seed >>= \m -> deriveAccountKey m n
|
||||
|
||||
walletDerivationTests :: Spec
|
||||
walletDerivationTests = do
|
||||
Hspec.it "name keys line up with other wallets' derivation" $ do
|
||||
let addrOf k = either error (show . addressFromPrivateKey) (nameKey k)
|
||||
-- MetaMask accounts 2 and 3 for this phrase; account 1 is the unused m/44'/60'/0'/0/0
|
||||
addrOf 1 `shouldBe` "0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0"
|
||||
addrOf 2 `shouldBe` "0xb6716976A3ebe8D39aCEB04372f22Ff8e6802D7A"
|
||||
Hspec.it "derives the same secret as other wallets" $
|
||||
-- MetaMask account 2 for this phrase, as exported by "Show private key"
|
||||
either error nameKeySecret (nameKey 1)
|
||||
`shouldBe` "0x9a983cb3d832fbde5ab49d692b7a8bf5b5d232479c99333d0fc8e1d21f1b55b6"
|
||||
Hspec.it "renders the path a name key sits at" $ do
|
||||
renderNameKeyPath 1 `shouldBe` "m/44'/60'/0'/0/1"
|
||||
renderNameKeyPath 7 `shouldBe` "m/44'/60'/0'/0/7"
|
||||
Hspec.it "accounts are the accounts another wallet derives for the same phrase" $ do
|
||||
let addrOf = show . addressFromPrivateKey . accountKey (seedFromPhrase testPhrase12)
|
||||
-- Ledger Live accounts 1 and 2 for this phrase, the published values for it
|
||||
addrOf 0 `shouldBe` "0x9858EfFD232B4033E47d90003D41EC34EcaEda94"
|
||||
addrOf 1 `shouldBe` "0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265"
|
||||
Hspec.it "the exported secret is the one another wallet shows for that account" $ do
|
||||
let k = accountKey (seedFromPhrase testPhrase12) 0
|
||||
-- as a wallet shows it for m/44'/60'/0'/0/0 of this phrase
|
||||
accountSecret k `shouldBe` "0x1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727"
|
||||
Hspec.it "every account has its own address" $ do
|
||||
let seed = seedFromPhrase testPhrase12
|
||||
addrs = map (show . addressFromPrivateKey . accountKey seed) [0 .. 9]
|
||||
length (nub addrs) `shouldBe` 10
|
||||
Hspec.it "a secret whose first byte is zero keeps its 64 hex digits" $ do
|
||||
let k = either error id . S.mkPrivateKey $ B.pack ('\0' : replicate 31 '\1')
|
||||
secret = T.unpack $ accountSecret k
|
||||
take 4 secret `shouldBe` "0x00"
|
||||
length secret `shouldBe` 66
|
||||
Hspec.it "renders the path an account sits at" $ do
|
||||
renderAccountPath 0 `shouldBe` "m/44'/60'/0'/0/0"
|
||||
renderAccountPath 7 `shouldBe` "m/44'/60'/7'/0/0"
|
||||
Hspec.it "round-trips the phrase it was imported from" $
|
||||
recoveryKeyPhrase testSeed `shouldBe` Right testPhrase
|
||||
Hspec.it "refuses a phrase with a bad checksum" $
|
||||
importRecoveryKey (B.unwords $ replicate 12 "abandon") `shouldSatisfy` isLeft
|
||||
Hspec.it "keeps a secret out of the log a remote host writes" $ do
|
||||
redactedCommand ("/_wallet create mnemonic=" <> safeDecodeUtf8 testPhrase)
|
||||
`shouldBe` "/_wallet create mnemonic=<redacted>"
|
||||
redactedCommand "/_wallet export name 1 secret=shibboleth"
|
||||
`shouldBe` "/_wallet export name 1 secret=<redacted>"
|
||||
redactedCommand "/_wallet export name 1" `shouldBe` "/_wallet export name 1"
|
||||
seedMnemonic (seedFromPhrase testPhrase24) `shouldBe` Right (safeDecodeUtf8 testPhrase24)
|
||||
Hspec.it "takes 24 words only, with a valid checksum" $ do
|
||||
entropyFromMnemonic testPhrase24 `shouldSatisfy` isRight
|
||||
entropyFromMnemonic testPhrase12 `shouldBe` Left WEBadMnemonic
|
||||
entropyFromMnemonic (B.unwords $ replicate 24 "abandon") `shouldBe` Left WEBadMnemonic
|
||||
|
||||
testWalletHiddenProfile :: HasCallStack => TestParams -> IO ()
|
||||
testWalletHiddenProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
alice ##> "/hide user my_password"
|
||||
alice <## "current user alisa:"
|
||||
alice <## "messages are hidden (use /tail to view)"
|
||||
alice <## "profile is hidden"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "wallet: a hidden profile cannot own an account"
|
||||
|
||||
testWalletExportNotMine :: HasCallStack => TestParams -> IO ()
|
||||
testWalletExportNotMine ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 0"
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
-- account 0 is the other profile's, and its key is not this profile's to take
|
||||
alice ##> "/_wallet export account 0"
|
||||
alice <## "wallet: another profile holds this account"
|
||||
-- an account nobody holds is still derivable, which is what a scan needs
|
||||
alice ##> "/_wallet export account 7"
|
||||
row <- getTermLine alice
|
||||
words row !! 1 `shouldBe` "m/44'/60'/7'/0/0"
|
||||
|
||||
testWalletIndexTooLarge :: HasCallStack => TestParams -> IO ()
|
||||
testWalletIndexTooLarge ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
-- 2^31 is already a hardened component, so it would derive account 0's key
|
||||
alice ##> "/_wallet address account=2147483648"
|
||||
alice <## "wallet: account index is too large to harden"
|
||||
alice ##> "/_wallet bind account=2147483648"
|
||||
alice <## "wallet: account index is too large to harden"
|
||||
alice ##> "/_wallet export account 2147483648"
|
||||
alice <## "wallet: account index is too large to harden"
|
||||
-- the largest index that can be hardened is usable, and the counter follows it
|
||||
alice ##> "/_wallet bind account=2147483647"
|
||||
alice <## "accounts: 2147483647"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "wallet: account index is too large to harden"
|
||||
|
||||
-- | The address a wallet reaches when the secret is imported as a private key.
|
||||
addressFromSecret :: String -> String
|
||||
addressFromSecret secret =
|
||||
show . addressFromPrivateKey . either error id . S.mkPrivateKey . either error id $
|
||||
BAE.convertFromBase BAE.Base16 (B.drop 2 $ B.pack secret)
|
||||
|
||||
walletTests :: SpecWith TestParams
|
||||
walletTests = do
|
||||
it "creates no seed until asked, then derives addresses" testWalletCreate
|
||||
it "the seed and the addresses come back after a restart" testWalletPersists
|
||||
it "every profile sees the same names, as the seed is the device's" testWalletSharedByProfiles
|
||||
it "imports a phrase, exports it, and refuses a second import" testWalletImport
|
||||
it "exports the secret of any name key" testWalletExportDerivedSecret
|
||||
it "deletes the seed, and a seed can be imported again" testWalletDelete
|
||||
it "discards a seed imported before the database is restored" testWalletImportThenRestore
|
||||
|
||||
-- | The state a chat database backed up before the seed restores to.
|
||||
forgetSeed :: HasCallStack => TestCC -> IO ()
|
||||
forgetSeed cc = cc ##> "/sql chat DELETE FROM wallet_seeds"
|
||||
|
||||
nameRows :: HasCallStack => TestCC -> IO [(String, String)]
|
||||
nameRows cc = mapM (\_ -> nameRow <$> getTermLine cc) [0 .. 1 :: Int]
|
||||
where
|
||||
nameRow l = case words l of
|
||||
[path, addr] -> (path, addr)
|
||||
_ -> error $ "unexpected wallet row: " <> l
|
||||
it "creates no wallet until asked, and only one" testWalletCreate
|
||||
it "binds the next free account, and re-binding one it holds changes nothing" testWalletBind
|
||||
it "keeps each profile's accounts apart" testWalletAccountsPerProfile
|
||||
it "taking the next account skips one already bound by index" testWalletBindByIndexThenNext
|
||||
it "derives an address without taking it" testWalletAddress
|
||||
it "exports the master phrase and one account's secret" testWalletExport
|
||||
it "will not take a new account on an imported phrase" testWalletImport
|
||||
it "the wallet and the accounts come back after a restart" testWalletPersists
|
||||
it "deletes the wallet, and one can be made again" testWalletDelete
|
||||
it "a hidden profile is bound no account" testWalletHiddenProfile
|
||||
it "will not export an account another profile holds" testWalletExportNotMine
|
||||
it "refuses an index BIP-32 cannot harden, on every command" testWalletIndexTooLarge
|
||||
|
||||
testWalletCreate :: HasCallStack => TestParams -> IO ()
|
||||
testWalletCreate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet"
|
||||
alice <## "no wallet key"
|
||||
-- asking creates nothing
|
||||
alice <## "no wallet on this device"
|
||||
-- reading creates nothing
|
||||
alice ##> "/_wallet"
|
||||
alice <## "no wallet key"
|
||||
alice ##> "/_wallet export"
|
||||
alice <## "bad chat command: no wallet key on this device"
|
||||
alice <## "no wallet on this device"
|
||||
alice ##> "/_wallet export master"
|
||||
alice <## "wallet: this device has no wallet"
|
||||
alice ##> "/_wallet create new"
|
||||
rows <- nameRows alice
|
||||
map fst rows `shouldBe` ["m/44'/60'/0'/0/1", "m/44'/60'/0'/0/2"]
|
||||
length (nub $ map snd rows) `shouldBe` 2
|
||||
-- create is for the seed, and this device has one
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "bad chat command: this device already has a wallet key"
|
||||
alice <## "wallet: this device already has a wallet"
|
||||
alice ##> "/_wallet delete"
|
||||
alice <## "ok"
|
||||
-- a mistyped phrase says nothing about which word was wrong
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack (B.unwords $ replicate 24 "abandon"))
|
||||
alice <## "wallet: not a valid 24 word recovery phrase"
|
||||
|
||||
testWalletPersists :: HasCallStack => TestParams -> IO ()
|
||||
testWalletPersists ps = do
|
||||
rows <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
nameRows alice
|
||||
-- same database, new session: a name bought at that address must stay reachable
|
||||
withTestChat ps "alice" $ \alice -> do
|
||||
alice ##> "/_wallet"
|
||||
rows' <- nameRows alice
|
||||
rows' `shouldBe` rows
|
||||
testWalletBind :: HasCallStack => TestParams -> IO ()
|
||||
testWalletBind ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 0"
|
||||
-- a profile owns as many accounts as it owns names
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 0, 1"
|
||||
-- binding one it already holds changes nothing
|
||||
alice ##> "/_wallet bind account=0"
|
||||
alice <## "accounts: 0, 1"
|
||||
|
||||
testWalletSharedByProfiles :: HasCallStack => TestParams -> IO ()
|
||||
testWalletSharedByProfiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
rows <- nameRows alice
|
||||
testWalletAccountsPerProfile :: HasCallStack => TestParams -> IO ()
|
||||
testWalletAccountsPerProfile ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 0"
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
-- the seed belongs to the device, so a name is not a profile's to see or not
|
||||
-- the wallet is the device's, the accounts are the profile's
|
||||
alice ##> "/_wallet"
|
||||
rows' <- nameRows alice
|
||||
rows' `shouldBe` rows
|
||||
alice ##> "/_wallet export"
|
||||
alice <## B.unpack testPhrase
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 1"
|
||||
alice ##> "/_wallet bind account=0"
|
||||
alice <## "wallet: another profile holds this account"
|
||||
|
||||
testWalletBindByIndexThenNext :: HasCallStack => TestParams -> IO ()
|
||||
testWalletBindByIndexThenNext ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
-- an account a scan found, bound by index, is still taken
|
||||
alice ##> "/_wallet bind account=2"
|
||||
alice <## "accounts: 2"
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 2, 3"
|
||||
-- accounts are listed by index, not by the order they were bound
|
||||
alice ##> "/_wallet bind account=1"
|
||||
alice <## "accounts: 1, 2, 3"
|
||||
-- and binding a low index never moves the counter back onto an account held
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "accounts: 1, 2, 3, 4"
|
||||
|
||||
testWalletAddress :: HasCallStack => TestParams -> IO ()
|
||||
testWalletAddress ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
-- reading the next free account does not take it
|
||||
alice ##> "/_wallet address"
|
||||
addr <- getTermLine alice
|
||||
alice ##> "/_wallet address"
|
||||
addr' <- getTermLine alice
|
||||
addr' `shouldBe` addr
|
||||
words addr !! 1 `shouldBe` "m/44'/60'/0'/0/0"
|
||||
alice ##> "/_wallet address account=3"
|
||||
at3 <- getTermLine alice
|
||||
words at3 !! 1 `shouldBe` "m/44'/60'/3'/0/0"
|
||||
-- a malformed index is a parse error, never a silent bind of the next account
|
||||
alice ##> "/_wallet bind account=abc"
|
||||
alice <## "bad chat command: Failed reading: empty"
|
||||
alice ##> "/_wallet"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
-- an index BIP-32 cannot harden is refused rather than folded onto a low one
|
||||
alice ##> "/_wallet address account=2147483648"
|
||||
alice <## "wallet: account index is too large to harden"
|
||||
|
||||
testWalletExport :: HasCallStack => TestParams -> IO ()
|
||||
testWalletExport ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase24)
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet export master"
|
||||
alice <## B.unpack testPhrase24
|
||||
alice ##> "/_wallet export account 0"
|
||||
row <- getTermLine alice
|
||||
case words row of
|
||||
[idx, path, address, secret] -> do
|
||||
idx `shouldBe` "0"
|
||||
path `shouldBe` "m/44'/60'/0'/0/0"
|
||||
-- m/44'/60'/0'/0/0 of the 24 word vector, pinned outside this
|
||||
-- implementation, so a change of path fails here rather than shipping
|
||||
address `shouldBe` "0xF278cF59F82eDcf871d630F28EcC8056f25C1cdb"
|
||||
addressFromSecret secret `shouldBe` address
|
||||
_ -> expectationFailure $ "unexpected export row: " <> row
|
||||
|
||||
testWalletImport :: HasCallStack => TestParams -> IO ()
|
||||
testWalletImport ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
alice <## "m/44'/60'/0'/0/1 0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0"
|
||||
alice <## "m/44'/60'/0'/0/2 0xb6716976A3ebe8D39aCEB04372f22Ff8e6802D7A"
|
||||
alice ##> "/_wallet export"
|
||||
alice <## B.unpack testPhrase
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
alice <## "bad chat command: this device already has a wallet key"
|
||||
-- a mistyped phrase says nothing about which word was wrong
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack (B.unwords $ replicate 12 "abandon"))
|
||||
alice <## "bad chat command: bad recovery phrase"
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase24)
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
-- the phrase does not say how many accounts it has been used for
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "wallet: unknown how many accounts this phrase has used, a scan of the chain has to run first"
|
||||
alice ##> "/_wallet address"
|
||||
alice <## "wallet: unknown how many accounts this phrase has used, a scan of the chain has to run first"
|
||||
-- binding an account a scan found is what a restored device does
|
||||
alice ##> "/_wallet bind account=4"
|
||||
alice <## "accounts: 4"
|
||||
-- and the counter stays unknown, because the phrase still does not say
|
||||
alice ##> "/_wallet bind"
|
||||
alice <## "wallet: unknown how many accounts this phrase has used, a scan of the chain has to run first"
|
||||
|
||||
testWalletExportDerivedSecret :: HasCallStack => TestParams -> IO ()
|
||||
testWalletExportDerivedSecret ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
alice ##> "/_wallet export name 1"
|
||||
alice <## "m/44'/60'/0'/0/1 0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0 0x9a983cb3d832fbde5ab49d692b7a8bf5b5d232479c99333d0fc8e1d21f1b55b6"
|
||||
-- a secret whose first byte is zero keeps its 64 hex digits
|
||||
alice ##> "/_wallet export name 15"
|
||||
alice <## "m/44'/60'/0'/0/15 0xa25d37554EB084969C85362f7E6B1A6108e51d0e 0x009a1ccd9c667416d9db6246a35d022b1799517c0cd8547bb07ce280c119ae3c"
|
||||
-- an index BIP-32 cannot reach is rejected, not wrapped into another key
|
||||
alice ##> "/_wallet export name 4294967296"
|
||||
alice <## "bad chat command: Failed reading: empty"
|
||||
testWalletPersists :: HasCallStack => TestParams -> IO ()
|
||||
testWalletPersists ps = do
|
||||
phrase <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind account=2"
|
||||
alice <## "accounts: 2"
|
||||
alice ##> "/_wallet export master"
|
||||
getTermLine alice
|
||||
-- same database, new session: an account holding a name must stay reachable
|
||||
withTestChat ps "alice" $ \alice -> do
|
||||
alice ##> "/_wallet"
|
||||
alice <## "accounts: 2"
|
||||
alice ##> "/_wallet export master"
|
||||
alice <## phrase
|
||||
|
||||
testWalletDelete :: HasCallStack => TestParams -> IO ()
|
||||
testWalletDelete ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
alice ##> "/_wallet bind account=1"
|
||||
alice <## "accounts: 1"
|
||||
alice ##> "/_wallet delete"
|
||||
alice <## "ok"
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
pure ()
|
||||
|
||||
testWalletImportThenRestore :: HasCallStack => TestParams -> IO ()
|
||||
testWalletImportThenRestore ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
-- restoring the database replaces the seed with what the backup held, which is nothing
|
||||
forgetSeed alice
|
||||
alice ##> "/_wallet"
|
||||
alice <## "no wallet key"
|
||||
alice ##> ("/_wallet create mnemonic=" <> B.unpack testPhrase)
|
||||
rows <- nameRows alice
|
||||
map fst rows `shouldBe` ["m/44'/60'/0'/0/1", "m/44'/60'/0'/0/2"]
|
||||
alice <## "no wallet on this device"
|
||||
-- the accounts went with the entropy they were counted against
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "wallet, no accounts for this profile"
|
||||
Reference in new issue
Block a user