From 52afb097113f401a1194c60bdb1fdde2a656046a Mon Sep 17 00:00:00 2001 From: Alain Brenzikofer Date: Sun, 30 Aug 2026 21:39:24 +0200 Subject: [PATCH] names: fix API docs lists, route signing through signSnrcIntent The 12 new /name commands and 8 responses were in none of the docs lists, and APINameAddress/CRNameAddress outlived their constructors. Added alongside APINameRegister, which set the precedent. Also regenerates TYPES.md and the TS/Python types, stale since CENameRegistrationFailed gained nameRegRetryAfter. APINameSetLink now signs through signSnrcIntent, so "the only bridge to the wallet" is enforced rather than documented. tokenId is dropped and setTextTypeString unexported, both without callers. nameKeyOf no longer reads the bound account: the path is literal, so the binding never affected which key came back. The mock's idempotent is one transaction, and a buy with no link stores no link rather than an empty one. ownedNames stays device-wide, now with the reason on it. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/BadgeService/Service.hs | 16 +++++---- bots/api/TYPES.md | 1 + bots/src/API/Docs/Commands.hs | 13 ++++++- bots/src/API/Docs/Responses.hs | 9 ++++- .../types/typescript/src/types.ts | 1 + .../src/simplex_chat/types/_types.py | 1 + simplex-chat.cabal | 1 - src/Simplex/Chat/Library/Commands.hs | 35 +++++++++++-------- src/Simplex/Chat/Names/Snrc.hs | 8 +---- 9 files changed, 54 insertions(+), 31 deletions(-) diff --git a/apps/simplex-badge-service/src/BadgeService/Service.hs b/apps/simplex-badge-service/src/BadgeService/Service.hs index d25e3f9894..079b05ce0a 100644 --- a/apps/simplex-badge-service/src/BadgeService/Service.hs +++ b/apps/simplex-badge-service/src/BadgeService/Service.hs @@ -289,7 +289,7 @@ handleNamesRequest chain NamesRequest {nrVersion, nrRequest} nrYears } NRBuy {nrRequestId, nrName, nrOwner, nrCode, nrLink} -> - idempotent nrRequestId $ atomically $ do + idempotent nrRequestId $ do c <- readTVar chain let code = unRedemptionCode nrCode label = T.takeWhile (/= '.') nrName @@ -333,7 +333,7 @@ handleNamesRequest chain NamesRequest {nrVersion, nrRequest} c <- readTVar chain pure $ NRPNonce (M.findWithDefault 0 nrAddress (chainNonces c)) NRRelayIntent {nrRequestId, nrName, nrRecordKey, nrValue, nrNonce, nrDeadline, nrSig} -> - idempotent nrRequestId $ atomically $ do + idempotent nrRequestId $ do c <- readTVar chain case (parseRecordKey nrRecordKey, M.lookup nrName (chainNames c)) of (Left e, _) -> pure $ NRPError NECBadRequest (Just (T.pack e)) Nothing @@ -366,13 +366,15 @@ handleNamesRequest chain NamesRequest {nrVersion, nrRequest} -- A resent request must not execute twice: matching fields cannot tell a -- retry from a user doing the same thing again, which is why every mutating -- call carries an id. - idempotent rid act = do - prior <- atomically $ M.lookup (unRequestId rid) . chainRequests <$> readTVar chain - case prior of + -- One transaction, so two identical requests cannot both run the action. + -- A relayer that split this would pay twice for one request id. + idempotent rid act = atomically $ do + c <- readTVar chain + case M.lookup (unRequestId rid) (chainRequests c) of Just r -> pure r Nothing -> do r <- act - atomically $ modifyTVar' chain $ \c -> c {chainRequests = M.insert (unRequestId rid) r (chainRequests c)} + modifyTVar' chain $ \c' -> c' {chainRequests = M.insert (unRequestId rid) r (chainRequests c')} pure r checkGates nm = let label = T.takeWhile (/= '.') nm @@ -386,7 +388,7 @@ handleNamesRequest chain NamesRequest {nrVersion, nrRequest} Just e | neExpiry e >= now' -> pure $ NRPError NECNameTaken Nothing Nothing _ -> do modifyTVar' chain $ \c' -> - c' {chainNames = M.insert nm (NameEntry owner [link] [] expiry editsPerName) (chainNames c')} + c' {chainNames = M.insert nm (NameEntry owner (filter (not . T.null) [link]) [] expiry editsPerName) (chainNames c')} pure $ NRPRegistered nm expiry (mockTxHash "register" tag) -- A NamesResponse always encodes to a JSON object. respObj r = case J.toJSON r of J.Object o -> o; _ -> KM.empty diff --git a/bots/api/TYPES.md b/bots/api/TYPES.md index 9708bf94a2..56bec1f2c8 100644 --- a/bots/api/TYPES.md +++ b/bots/api/TYPES.md @@ -1148,6 +1148,7 @@ NameRegistrationFailed: - type: "nameRegistrationFailed" - nameRegCode: string - nameRegMessage: string? +- nameRegRetryAfter: word32? NotResolvedLocally: - type: "notResolvedLocally" diff --git a/bots/src/API/Docs/Commands.hs b/bots/src/API/Docs/Commands.hs index f41f6291f0..10d6ebd99c 100644 --- a/bots/src/API/Docs/Commands.hs +++ b/bots/src/API/Docs/Commands.hs @@ -393,8 +393,19 @@ undocumentedCommands = "APIHideUser", "APIImportArchive", "APIMuteUser", - "APINameAddress", + "APINameBuy", + "APINameInfo", + "APINameKeys", + "APINameKeysExport", + "APINameKeysImport", + "APINameKeysInit", + "APINameKeysUse", + "APINameList", + "APINameQuote", "APINameRegister", + "APINameRescan", + "APINameSetLink", + "APINameVerifyCode", "APIPlanForwardChatItems", "APIPrepareContact", "APIPrepareGroup", diff --git a/bots/src/API/Docs/Responses.hs b/bots/src/API/Docs/Responses.hs index 035bf817ca..647b2ac388 100644 --- a/bots/src/API/Docs/Responses.hs +++ b/bots/src/API/Docs/Responses.hs @@ -177,8 +177,15 @@ undocumentedResponses = "CRMemberSupportChatRead", "CRMemberSupportChatDeleted", "CRMemberSupportChats", - "CRNameAddress", + "CRNameCode", + "CRNameInfo", + "CRNameKeyPhrases", + "CRNameKeys", + "CRNameLinkSet", + "CRNameQuote", "CRNameRegistered", + "CRNameRescan", + "CRNames", "CRNetworkConfig", "CRNewMemberContact", "CRNewMemberContactSentInv", diff --git a/packages/simplex-chat-client/types/typescript/src/types.ts b/packages/simplex-chat-client/types/typescript/src/types.ts index 354cf9ddfa..c0613724ad 100644 --- a/packages/simplex-chat-client/types/typescript/src/types.ts +++ b/packages/simplex-chat-client/types/typescript/src/types.ts @@ -1326,6 +1326,7 @@ export namespace ChatErrorType { type: "nameRegistrationFailed" nameRegCode: string nameRegMessage?: string + nameRegRetryAfter?: number // word32 } export interface NotResolvedLocally extends Interface { diff --git a/packages/simplex-chat-python/src/simplex_chat/types/_types.py b/packages/simplex-chat-python/src/simplex_chat/types/_types.py index dd813edf31..78f698a635 100644 --- a/packages/simplex-chat-python/src/simplex_chat/types/_types.py +++ b/packages/simplex-chat-python/src/simplex_chat/types/_types.py @@ -824,6 +824,7 @@ class ChatErrorType_nameRegistrationFailed(TypedDict): type: Literal["nameRegistrationFailed"] nameRegCode: str nameRegMessage: NotRequired[str] + nameRegRetryAfter: NotRequired[int] # word32 class ChatErrorType_notResolvedLocally(TypedDict): type: Literal["notResolvedLocally"] diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 757685ecbf..c7b7f90ebe 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -34,7 +34,6 @@ flag client_postgres manual: True default: False - library exposed-modules: Simplex.Chat diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index f2d7134b44..90ce260be2 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -63,10 +63,10 @@ import Simplex.Chat.Library.Subscriber import Simplex.Chat.Badges (BadgeCredential (..), LocalBadge (..), maxXFTPFileSize, mkBadgeStatus, verifyCredential) import Simplex.Chat.Names (SimplexDomainProof (..), SimplexDomainClaim (..), claimDomain, mkDomainClaim) import Simplex.Chat.Names.Protocol -import Simplex.Chat.Names.Snrc (Intent (..), SnrcDeployment (..), intent712, parseRecordKey) +import Simplex.Chat.Names.Snrc (Intent (..), SignedIntent (..), SnrcDeployment (..), parseRecordKey, signSnrcIntent) import Simplex.Messaging.Eth.Address (Address, mkAddress) -import Simplex.Chat.Store.Wallets (bindSeedAccount, boundAccount, createSeed, currentSeed, getNameKeys, getOrCreateAccountRef, listSeeds, markBackedUp, nameKeyPathTaken, raiseNextAccountIndex, raiseNextNameIndex, recordNameKey, seedOfName, setCurrentSeed, setNextNameIndex, takeNameIndex) -import Simplex.Chat.Wallet (AccountIndex, AccountRef (..), NameIndex, SeedId, WalletAccount, WalletSeed (..), accountAddress, deriveAtPath, deriveNameKey, ethSignatureBytes, importRecoveryKey, newSeed, parseNameKeyPath, recoveryKeyPhrase, renderNameKeyPath, signIntent) +import Simplex.Chat.Store.Wallets (bindSeedAccount, createSeed, currentSeed, getNameKeys, getOrCreateAccountRef, listSeeds, markBackedUp, nameKeyPathTaken, raiseNextAccountIndex, raiseNextNameIndex, recordNameKey, seedOfName, setCurrentSeed, setNextNameIndex, takeNameIndex) +import Simplex.Chat.Wallet (AccountIndex, AccountRef (..), NameIndex, SeedId, WalletAccount, WalletSeed (..), accountAddress, deriveAtPath, deriveNameKey, ethSignatureBytes, importRecoveryKey, newSeed, parseNameKeyPath, recoveryKeyPhrase, renderNameKeyPath) import Simplex.Chat.Call import Simplex.Chat.Controller import Simplex.Chat.Delivery (DeliveryJobScope (..), DeliveryJobSpec (..), DeliveryWorkerScope (..)) @@ -1502,7 +1502,7 @@ processChatCommand cxt nm = \case pure $ CRNameRegistered user nm' (tshow owner) path expiry' txHash' APINameList sendTarget -> withUser $ \user -> do cReq <- resolveServiceTarget nm user sendTarget - named <- ownedNames user + named <- ownedNames rows <- forM named $ \(nm_, _) -> namesRPC user cReq (NRResolve nm_) >>= \case NRPRecord {nrName, nrContact, nrExpiry, nrEditsLeft} -> @@ -1511,7 +1511,7 @@ processChatCommand cxt nm = \case pure $ CRNames user rows APINameInfo sendTarget nm' -> withUser $ \user -> do cReq <- resolveServiceTarget nm user sendTarget - (_, path, _) <- nameKeyOf user nm' + (_, path, _) <- nameKeyOf nm' namesRPC user cReq (NRResolve nm') >>= \case NRPRecord {nrName, nrOwner, nrContact, nrChannel, nrExpiry, nrEditsLeft} -> pure $ CRNameInfo user nrName (tshow nrOwner) path nrContact nrChannel nrExpiry nrEditsLeft @@ -1519,7 +1519,7 @@ processChatCommand cxt nm = \case APINameSetLink sendTarget nm' record lnk -> withUser $ \user -> do cReq <- resolveServiceTarget nm user sendTarget rk <- either throwCmdError pure $ parseRecordKey record - (_, _, acc) <- nameKeyOf user nm' + (_, _, acc) <- nameKeyOf nm' nonce <- namesRPC user cReq (NRNonce (accountAddress acc)) >>= \case NRPNonce {nrNonce} -> pure nrNonce @@ -1529,10 +1529,10 @@ processChatCommand cxt nm = \case -- changing hands and could then be replayed against the new owner's name. let deadline = floor (utcTimeToPOSIXSeconds now) + intentTtlSeconds it = SetTextRecord nm' rk lnk nonce deadline - sig <- either (throwCmdError . ("wallet: " <>)) pure $ signIntent acc (intent712 clientDeployment it) + SignedIntent {siSignature} <- either (throwCmdError . ("wallet: " <>)) pure $ signSnrcIntent acc clientDeployment it g <- asks random rid <- RequestId <$> atomically (C.randomBytes 16 g) - namesRPC user cReq (NRRelayIntent rid nm' record lnk nonce deadline (IntentSig $ ethSignatureBytes sig)) >>= \case + namesRPC user cReq (NRRelayIntent rid nm' record lnk nonce deadline (IntentSig $ ethSignatureBytes siSignature)) >>= \case NRPRelayed {nrTxHash} -> pure $ CRNameLinkSet user nm' record nrTxHash _ -> throwCmdError "unexpected relay response" APINameRescan sendTarget more -> withUser $ \user -> do @@ -5260,21 +5260,28 @@ groupByAccount ns = Nothing -> (Nothing, [(Nothing, n, path)]) -- | Names this device holds a key for, with the path each key sits at. -ownedNames :: User -> CM [(Text, Text)] -ownedNames _ = do +-- +-- Device-wide on purpose, and not scoped to the calling profile: a seed belongs +-- to the device rather than to a profile, wallet_name_keys records no profile, +-- and a recovery scan has nothing to attribute what it finds to. Listing per +-- profile would hide exactly the names a recovery had just restored. The same +-- reasoning applies to 'nameKeyOf', which will sign for any name on the device. +ownedNames :: CM [(Text, Text)] +ownedNames = do seeds <- withFastStore' $ \db -> map fst <$> listSeeds db concat <$> mapM (\seed -> withFastStore' $ \db -> getNameKeys db (wsId seed)) seeds -- | The key that owns a name. Re-derived from the stored path, so a name found -- on a layout that is not ours still works. -nameKeyOf :: User -> Text -> CM (SeedId, Text, WalletAccount) -nameKeyOf user nm' = do +nameKeyOf :: Text -> CM (SeedId, Text, WalletAccount) +nameKeyOf nm' = do r <- withFastStore' $ \db -> seedOfName db nm' case r of Nothing -> throwCmdError $ "no key for " <> T.unpack nm' <> " on this device" Just (seed, path) -> do - acctIx <- maybe 0 (arIndex . snd) <$> withFastStore' (\db -> boundAccount db user) - acc <- either (throwCmdError . ("wallet: " <>)) pure $ deriveAtPath seed acctIx path + -- The path is literal, so the profile's current binding has no say in + -- which key comes back - reading it would only suggest otherwise. + acc <- either (throwCmdError . ("wallet: " <>)) pure $ deriveAtPath seed 0 path pure (wsId seed, path, acc) -- | Probe a seed for names it owns, across the layouts a name may have been diff --git a/src/Simplex/Chat/Names/Snrc.hs b/src/Simplex/Chat/Names/Snrc.hs index 6da38a98be..2ca4911c2f 100644 --- a/src/Simplex/Chat/Names/Snrc.hs +++ b/src/Simplex/Chat/Names/Snrc.hs @@ -17,11 +17,9 @@ module Simplex.Chat.Names.Snrc parseRecordKey, labelHash, nameHash, - tokenId, intent712, intentDigest, signSnrcIntent, - setTextTypeString, ) where @@ -31,9 +29,8 @@ import qualified Data.ByteString.Char8 as BC import Data.Text (Text) import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Wallet (Eip712Intent (..), EthSignature, WalletAccount, signIntent) -import Simplex.Messaging.Eth.EIP712 (hashTypedData) import Simplex.Messaging.Eth.Address (Address) -import Simplex.Messaging.Eth.EIP712 (Eip712Domain (..), Value (..)) +import Simplex.Messaging.Eth.EIP712 (Eip712Domain (..), Value (..), hashTypedData) import Simplex.Messaging.Eth.Keccak (keccak256) -- | Where a TLD is deployed. The verifying contract differs per intent kind, so @@ -91,9 +88,6 @@ nameHash name step lbl node = keccak256 (node <> labelHash lbl) -- | @BaseRegistrar@ token id: @uint256(keccak256(label))@. -tokenId :: ByteString -> Integer -tokenId = B.foldl' (\acc w -> acc * 256 + fromIntegral w) 0 . labelHash - -- | The typed-data an intent signs. Deliberately the only bridge to the wallet: -- 'signIntent' takes this, never a bare digest. intent712 :: SnrcDeployment -> Intent -> Eip712Intent