mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-06 01:18:11 +00:00
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:
co-authored by
Claude Opus 5
parent
2f0f8a7499
commit
52afb09711
@@ -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
|
||||
|
||||
@@ -1148,6 +1148,7 @@ NameRegistrationFailed:
|
||||
- type: "nameRegistrationFailed"
|
||||
- nameRegCode: string
|
||||
- nameRegMessage: string?
|
||||
- nameRegRetryAfter: word32?
|
||||
|
||||
NotResolvedLocally:
|
||||
- type: "notResolvedLocally"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -34,7 +34,6 @@ flag client_postgres
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Simplex.Chat
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user