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) <noreply@anthropic.com>
This commit is contained in:
Alain Brenzikofer
2026-08-30 21:39:24 +02:00
co-authored by Claude Opus 5
parent 2f0f8a7499
commit 52afb09711
9 changed files with 54 additions and 31 deletions
@@ -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
+1
View File
@@ -1148,6 +1148,7 @@ NameRegistrationFailed:
- type: "nameRegistrationFailed"
- nameRegCode: string
- nameRegMessage: string?
- nameRegRetryAfter: word32?
NotResolvedLocally:
- type: "notResolvedLocally"
+12 -1
View File
@@ -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",
+8 -1
View File
@@ -177,8 +177,15 @@ undocumentedResponses =
"CRMemberSupportChatRead",
"CRMemberSupportChatDeleted",
"CRMemberSupportChats",
"CRNameAddress",
"CRNameCode",
"CRNameInfo",
"CRNameKeyPhrases",
"CRNameKeys",
"CRNameLinkSet",
"CRNameQuote",
"CRNameRegistered",
"CRNameRescan",
"CRNames",
"CRNetworkConfig",
"CRNewMemberContact",
"CRNewMemberContactSentInv",
@@ -1326,6 +1326,7 @@ export namespace ChatErrorType {
type: "nameRegistrationFailed"
nameRegCode: string
nameRegMessage?: string
nameRegRetryAfter?: number // word32
}
export interface NotResolvedLocally extends Interface {
@@ -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"]
-1
View File
@@ -34,7 +34,6 @@ flag client_postgres
manual: True
default: False
library
exposed-modules:
Simplex.Chat
+21 -14
View File
@@ -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
+1 -7
View File
@@ -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