We hit this in production and it took a while to track down, so writing it up.
If a dapp calls cleanup() anywhere (we call it in our connect flow to reset state before letting the user reconnect), the next cold launch can no longer publish on any session that existed before. The relay rejects every publish with:
JSONRPCError(code: -32000, message: "Topic service: Topic(NotParticipant)")
and because that RPC error is swallowed, the publish just hangs until the 60s ACK timeout. On the user's side deposit/withdraw/sign all spin forever. Reconnecting from scratch fixes it — but only until the next cleanup() + relaunch.
The reason it comes out as NotParticipant and not something more obvious: cleanup() is quietly throwing away the relay client identity.
Tracing it:
SignClient.cleanup() → SignCleanupService.cleanupStorages() calls kms.deleteAll().
- The KMS keychain and the relay client-identity keypair live under the same keychain service,
"com.walletconnect.sdk" — see SignClientFactory and RelayClientFactory, both KeychainStorage(serviceIdentifier: "com.walletconnect.sdk", ...).
KeychainStorage.deleteAll() deletes by kSecAttrService, so it clears the whole service, including the client-identity private key that ClientIdStorage wrote under publicKey.storageId.
- The identity's public half lives in (group)
UserDefaults and is left untouched, so the store is now half-gone. On the next launch ClientIdStorage.getOrCreateKeyPair() hits privatePartNotFound and generates a fresh keypair → new client_id.
- The relay's topic service doesn't recognize that new
client_id as a participant of the old topics, so publish → NotParticipant.
Easy to confirm with Relay.instance.getClientId(): read it before the cleanup(), read it again after a relaunch, and the DID has changed.
This has obviously been latent for a long time; it only started biting us once the relay began enforcing topic participants. Before that, publishing under a regenerated identity to an existing topic apparently just worked, so the identity churn went unnoticed.
Repro:
- configure Networking/Sign (or AppKit) with a group identifier
- connect a wallet, settle a session
Sign.instance.cleanup() (or AppKit.instance.cleanup())
- cold relaunch — the session restores and re-subscribe (BatchSubscribe) succeeds
- publish anything on that session →
-32000 Topic(NotParticipant)
Reproduced on 1.6.12; the same code paths are still there on 2.3.0.
The docs describe cleanup() as deleting "pairings, sessions, keys", so wiping session/pairing material is expected — but taking the relay transport identity down with it is surprising, and now that participants are enforced it's unrecoverable for the existing sessions (a full reconnect is the only way back).
A few ways this could be fixed:
- store the client-identity keypair under its own keychain service that
kms.deleteAll() doesn't touch, or
- have
deleteAll() remove only KMS-managed keys rather than the entire service, or
- snapshot and restore the identity around the wipe in
cleanupStorages().
One more thing that made this hard to diagnose: RelayClient.handlePayloadMessage drops the relay's RPC error (log-only), so a rejected publish is indistinguishable from a plain network stall until the 60s timeout fires. Surfacing that error would help a lot.
We hit this in production and it took a while to track down, so writing it up.
If a dapp calls
cleanup()anywhere (we call it in our connect flow to reset state before letting the user reconnect), the next cold launch can no longer publish on any session that existed before. The relay rejects every publish with:and because that RPC error is swallowed, the publish just hangs until the 60s ACK timeout. On the user's side deposit/withdraw/sign all spin forever. Reconnecting from scratch fixes it — but only until the next
cleanup()+ relaunch.The reason it comes out as
NotParticipantand not something more obvious:cleanup()is quietly throwing away the relay client identity.Tracing it:
SignClient.cleanup()→SignCleanupService.cleanupStorages()callskms.deleteAll()."com.walletconnect.sdk"— seeSignClientFactoryandRelayClientFactory, bothKeychainStorage(serviceIdentifier: "com.walletconnect.sdk", ...).KeychainStorage.deleteAll()deletes bykSecAttrService, so it clears the whole service, including the client-identity private key thatClientIdStoragewrote underpublicKey.storageId.UserDefaultsand is left untouched, so the store is now half-gone. On the next launchClientIdStorage.getOrCreateKeyPair()hitsprivatePartNotFoundand generates a fresh keypair → newclient_id.client_idas a participant of the old topics, so publish →NotParticipant.Easy to confirm with
Relay.instance.getClientId(): read it before thecleanup(), read it again after a relaunch, and the DID has changed.This has obviously been latent for a long time; it only started biting us once the relay began enforcing topic participants. Before that, publishing under a regenerated identity to an existing topic apparently just worked, so the identity churn went unnoticed.
Repro:
Sign.instance.cleanup()(orAppKit.instance.cleanup())-32000 Topic(NotParticipant)Reproduced on 1.6.12; the same code paths are still there on 2.3.0.
The docs describe
cleanup()as deleting "pairings, sessions, keys", so wiping session/pairing material is expected — but taking the relay transport identity down with it is surprising, and now that participants are enforced it's unrecoverable for the existing sessions (a full reconnect is the only way back).A few ways this could be fixed:
kms.deleteAll()doesn't touch, ordeleteAll()remove only KMS-managed keys rather than the entire service, orcleanupStorages().One more thing that made this hard to diagnose:
RelayClient.handlePayloadMessagedrops the relay's RPC error (log-only), so a rejected publish is indistinguishable from a plain network stall until the 60s timeout fires. Surfacing that error would help a lot.