mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 17:58:47 +00:00
core: one command for the seed, whichever way it arrives
create and import differed only in where the entropy came from and already shared one store function, so they are one command with the source named: /_wallet create new, or /_wallet create seed=<phrase>. Naming it means no seed is generated by typing a prefix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rvc3HbiWBTqbAvRT45G5oX
This commit is contained in:
co-authored by
Claude Opus 5
parent
bfa510cdb1
commit
443fd43545
@@ -455,7 +455,6 @@ undocumentedCommands =
|
||||
"APIWalletDelete",
|
||||
"APIWalletExportDerivedSecret",
|
||||
"APIWalletExportSeedMnemonic",
|
||||
"APIWalletImport",
|
||||
"CheckChatRunning",
|
||||
"ConfirmRemoteCtrl",
|
||||
"ConnectRemoteCtrl",
|
||||
|
||||
@@ -66,17 +66,20 @@ at the profile level, while what a profile buys sits at the address level.
|
||||
Internal API. The names commands will call these; users will not.
|
||||
|
||||
```
|
||||
/_wallet the next name addresses
|
||||
/_wallet create generate the seed. Refused if the device has one
|
||||
/_wallet import <phrase> store a seed. Refused if the device has one
|
||||
/_wallet export the seed mnemonic
|
||||
/_wallet export <name> one derived secret, as 0x and 64 hex digits
|
||||
/_wallet delete delete the seed
|
||||
/_wallet the next name addresses
|
||||
/_wallet create new generate the seed
|
||||
/_wallet create seed=<phrase> take the seed from a phrase
|
||||
/_wallet export the seed mnemonic
|
||||
/_wallet export <name> one derived secret, as 0x and 64 hex digits
|
||||
/_wallet delete delete the seed
|
||||
```
|
||||
|
||||
A seed is created only when asked for, never at startup and never as a side
|
||||
effect of reading. None of these is forwarded to a remote host: the recovery
|
||||
phrase must not leave the device, and the raw command would be logged there.
|
||||
Creating and importing are one command, because they differ only in where the
|
||||
entropy comes from, and either is refused if the device already has a seed. The
|
||||
source is always named, so no seed is generated by typing a prefix. A seed is
|
||||
created only when asked for, never at startup and never as a side effect of
|
||||
reading. None of these is forwarded to a remote host: the recovery phrase must
|
||||
not leave the device, and the raw command would be logged there.
|
||||
|
||||
## Schema
|
||||
|
||||
|
||||
@@ -418,8 +418,7 @@ data ChatCommand
|
||||
| APISendServiceRequest {userId :: UserId, sendTarget :: ConnectTarget 'CMContact, requestTimeout :: Maybe NominalDiffTime, signKey :: Maybe (C.StoredPrivateKey 'C.Ed25519), request :: J.Object}
|
||||
| APISendServiceResponse {userId :: UserId, requestId :: AgentInvId, responseData :: J.Object}
|
||||
| APIWallet
|
||||
| APIWalletCreate
|
||||
| APIWalletImport {recoveryPhrase :: Text}
|
||||
| APIWalletCreate {recoveryPhrase :: Maybe Text}
|
||||
| APIWalletExportSeedMnemonic
|
||||
| APIWalletExportDerivedSecret {nameIndex :: NameIndex}
|
||||
| APIWalletDelete
|
||||
@@ -749,8 +748,7 @@ allowRemoteCommand = \case
|
||||
ExecChatStoreSQL _ -> False
|
||||
ExecAgentStoreSQL _ -> False
|
||||
APIWallet -> False
|
||||
APIWalletCreate -> False
|
||||
APIWalletImport _ -> False
|
||||
APIWalletCreate {} -> False
|
||||
APIWalletExportSeedMnemonic -> False
|
||||
APIWalletExportDerivedSecret {} -> False
|
||||
APIWalletDelete -> False
|
||||
|
||||
@@ -1498,14 +1498,10 @@ processChatCommand cxt nm = \case
|
||||
Just seed -> do
|
||||
next <- withFastStore' $ \db -> getNextNameIndex db (wsId seed)
|
||||
CRWallet user True <$> nameKeyRows seed next
|
||||
APIWalletCreate -> withUser $ \_ -> do
|
||||
g <- asks random
|
||||
entropy <- atomically $ newSeed MS256 g
|
||||
created <- withFastStore' $ \db -> createSeed db entropy
|
||||
unless created $ throwCmdError "this device already has a wallet key"
|
||||
processChatCommand cxt nm APIWallet
|
||||
APIWalletImport phrase -> withUser $ \_ -> do
|
||||
entropy <- either (const $ throwCmdError "bad recovery phrase") pure $ importRecoveryKey (encodeUtf8 phrase)
|
||||
APIWalletCreate phrase_ -> withUser $ \_ -> do
|
||||
entropy <- case phrase_ of
|
||||
Nothing -> asks random >>= atomically . newSeed MS256
|
||||
Just phrase -> either (const $ throwCmdError "bad recovery phrase") pure $ importRecoveryKey (encodeUtf8 phrase)
|
||||
created <- withFastStore' $ \db -> createSeed db entropy
|
||||
unless created $ throwCmdError "this device already has a wallet key"
|
||||
processChatCommand cxt nm APIWallet
|
||||
@@ -5587,8 +5583,8 @@ chatCommandP =
|
||||
"/_reject " *> (APIRejectContact <$> A.decimal <*> (" notify=" *> onOffP <|> pure False)),
|
||||
"/_service_request " *> (APISendServiceRequest <$> A.decimal <* A.space <*> strP <*> optional (" timeout=" *> (realToFrac <$> A.double)) <*> optional (" sign_key=" *> strP) <* A.space <*> jsonP),
|
||||
"/_service_response " *> (APISendServiceResponse <$> A.decimal <* A.space <*> strP <* A.space <*> jsonP),
|
||||
"/_wallet create" $> APIWalletCreate,
|
||||
"/_wallet import " *> (APIWalletImport <$> textP),
|
||||
"/_wallet create new" $> APIWalletCreate Nothing,
|
||||
"/_wallet create seed=" *> (APIWalletCreate . Just <$> textP),
|
||||
"/_wallet export " *> (APIWalletExportDerivedSecret <$> keyIndexP),
|
||||
"/_wallet export" $> APIWalletExportSeedMnemonic,
|
||||
"/_wallet delete" $> APIWalletDelete,
|
||||
|
||||
+12
-12
@@ -75,18 +75,18 @@ testWalletCreate ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice <## "no wallet key"
|
||||
alice ##> "/_wallet export"
|
||||
alice <## "bad chat command: no wallet key on this device"
|
||||
alice ##> "/_wallet create"
|
||||
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 create"
|
||||
alice ##> "/_wallet create new"
|
||||
alice <## "bad chat command: this device already has a wallet key"
|
||||
|
||||
testWalletPersists :: HasCallStack => TestParams -> IO ()
|
||||
testWalletPersists ps = do
|
||||
rows <- withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> "/_wallet create"
|
||||
alice ##> "/_wallet create new"
|
||||
nameRows alice
|
||||
-- same database, new session: a name bought at that address must stay reachable
|
||||
withTestChat ps "alice" $ \alice -> do
|
||||
@@ -96,7 +96,7 @@ testWalletPersists ps = do
|
||||
|
||||
testWalletSharedByProfiles :: HasCallStack => TestParams -> IO ()
|
||||
testWalletSharedByProfiles ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack testPhrase)
|
||||
rows <- nameRows alice
|
||||
alice ##> "/create user alisa"
|
||||
showActiveUser alice "alisa"
|
||||
@@ -109,20 +109,20 @@ testWalletSharedByProfiles ps = withNewTestChat ps "alice" aliceProfile $ \alice
|
||||
|
||||
testWalletImport :: HasCallStack => TestParams -> IO ()
|
||||
testWalletImport ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> 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 import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> 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 import " <> B.unpack (B.unwords $ replicate 12 "abandon"))
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack (B.unwords $ replicate 12 "abandon"))
|
||||
alice <## "bad chat command: bad recovery phrase"
|
||||
|
||||
testWalletExportDerivedSecret :: HasCallStack => TestParams -> IO ()
|
||||
testWalletExportDerivedSecret ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
alice ##> "/_wallet export 1"
|
||||
alice <## "m/44'/60'/0'/0/1 0x6Fac4D18c912343BF86fa7049364Dd4E424Ab9C0 0x9a983cb3d832fbde5ab49d692b7a8bf5b5d232479c99333d0fc8e1d21f1b55b6"
|
||||
@@ -135,22 +135,22 @@ testWalletExportDerivedSecret ps = withNewTestChat ps "alice" aliceProfile $ \al
|
||||
|
||||
testWalletDelete :: HasCallStack => TestParams -> IO ()
|
||||
testWalletDelete ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
alice ##> "/_wallet delete"
|
||||
alice <## "no wallet key"
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack testPhrase)
|
||||
_ <- nameRows alice
|
||||
pure ()
|
||||
|
||||
testWalletImportThenRestore :: HasCallStack => TestParams -> IO ()
|
||||
testWalletImportThenRestore ps = withNewTestChat ps "alice" aliceProfile $ \alice -> do
|
||||
alice ##> ("/_wallet import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> 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 import " <> B.unpack testPhrase)
|
||||
alice ##> ("/_wallet create seed=" <> B.unpack testPhrase)
|
||||
rows <- nameRows alice
|
||||
map fst rows `shouldBe` ["m/44'/60'/0'/0/1", "m/44'/60'/0'/0/2"]
|
||||
|
||||
Reference in New Issue
Block a user