Describe the bug
The TRON namespace's account state never reaches AppKit's react state when using the
TronAdapter({ walletAdapters }) extension point with an injected wallet (e.g.
@tronweb3/tronwallet-adapter-tronlink's TronLinkAdapter):
useAppKitAccount({ namespace: 'tron' }) stays { isConnected: false, address: undefined }
after a successful modal connect (the wallet prompt appears, the user approves, the
adapter connects) and after every page reload.
- Meanwhile AppKit's own storage says otherwise:
@appkit/connection_status = connected,
@appkit/connected_namespaces contains tron, and tron:connected_connector_id is set.
- A previously connected session is silently destroyed on every boot, and the connector is
marked as user-disconnected, which additionally disables the second-chance
syncConnections rehydration for that boot.
Verified on @reown/appkit + @reown/appkit-adapter-tron 1.8.23 (latest stable at the
time of writing); the relevant code is unchanged on current main.
Root cause
TRON connectors register asynchronously, but boot-time connection sync runs
immediately and treats "connector not registered yet" as "user disconnected".
-
TronAdapter.syncConnectors() (packages/adapters/tron/src/adapter.ts) does not add
connectors synchronously. It delegates to TronConnectUtil.watchWalletAdapters()
(packages/adapters/tron/src/utils/TronConnectUtil.ts), which only calls
addConnector(...) once the wallet adapter's readyState is Found. For an injected
extension wallet, readyState is still Loading while createAppKit runs (the
extension's detection resolves after page scripts), so the connector is registered
later, via the readyStateChanged listener.
-
Meanwhile AppKitBaseClient.initialize() proceeds to syncExistingConnection() →
syncNamespaceConnection('tron') → syncAdapterConnection('tron')
(packages/appkit/src/client/appkit-base-client.ts). At that point
ConnectorController.getConnectors('tron') is still empty, so the lookup fails and the
method throws Adapter or connector not found for namespace tron — which the catch
turns into onDisconnectNamespace({ chainNamespace: 'tron', closeModal: false }).
-
onDisconnectNamespace resets the tron account state, removes the stored connector id
and calls StorageUtil.addDisconnectedConnectorId(...) — recording a user-initiated
disconnect that never happened.
-
Immediately after, syncAdapterConnections() → TronAdapter.syncConnections() filters
connectors with !hasDisconnected && hasConnected — poisoned by step 3 (and the connector
list is empty anyway), so nothing is rehydrated.
-
When the extension finally injects and watchWalletAdapters registers the connector,
nothing re-runs the namespace sync. Result: storage looks connected, react state stays
empty until the user manually reconnects — and on the next reload the cycle repeats, so
the tron connection never survives a reload.
The EVM (wagmi) and Solana adapters don't hit this because their connectors are available
synchronously during syncConnectors(); the tron adapter is structurally always late for
injected wallets.
A secondary symptom of the same race, observed when the wallet's site authorization makes the
adapter auto-connect at boot: AppKit records an internal tron connection whose account state
never reached ChainController, after which the modal's wallet row dead-ends at "This
account is already linked" (scaffold-ui isAlreadyConnected) without ever calling
adapter.connect() — leaving no way to (re)connect from the UI.
Minimal reproduction
React + Vite app, TronLink extension installed and the site authorized in TronLink:
import { createAppKit } from '@reown/appkit/react'
import { tronShastaTestnet } from '@reown/appkit/networks'
import { TronAdapter } from '@reown/appkit-adapter-tron'
import { TronLinkAdapter } from '@tronweb3/tronwallet-adapter-tronlink'
const tronAdapter = new TronAdapter({ walletAdapters: [new TronLinkAdapter()] })
createAppKit({
adapters: [tronAdapter],
networks: [tronShastaTestnet],
projectId: '<id>'
})
const { address, isConnected } = useAppKitAccount({ namespace: 'tron' })
// connect TronLink via the modal → prompt appears, approval succeeds,
// yet isConnected stays false; reload → still false, and the previously
// stored connection has been wiped from storage.
The adapter's own diagnostic log confirms the ordering
([TronConnectUtil] Watching wallet adapters: [{ name: 'TronLink', readyState: 'Loading', ... }]
at init, with the connector added only later), followed by the silent
AppKit couldn't sync existing connection path.
Expected behavior
- A connector that simply hasn't registered yet must not be recorded as a user
disconnect. syncAdapterConnection could distinguish "no connector registered for the
stored id" (leave storage intact / retry) from an actual failed reconnect.
- When a connector registers late (
watchWalletAdapters → addConnector), the pending
namespace sync should re-run so the stored connection rehydrates and
useAppKitAccount({ namespace: 'tron' }) populates.
Workaround we're using
We bridge account state directly from our wallet-adapter instances into app state (reading
the @tronweb3 adapter's address/connected and subscribing to its events), and keep our
own disconnect flag at the adapter level, bypassing useAppKitAccount for the tron
namespace entirely. That works, but it duplicates state AppKit already tracks and shouldn't
be necessary.
Describe the bug
The TRON namespace's account state never reaches AppKit's react state when using the
TronAdapter({ walletAdapters })extension point with an injected wallet (e.g.@tronweb3/tronwallet-adapter-tronlink'sTronLinkAdapter):useAppKitAccount({ namespace: 'tron' })stays{ isConnected: false, address: undefined }after a successful modal connect (the wallet prompt appears, the user approves, the
adapter connects) and after every page reload.
@appkit/connection_status = connected,@appkit/connected_namespacescontainstron, andtron:connected_connector_idis set.marked as user-disconnected, which additionally disables the second-chance
syncConnectionsrehydration for that boot.Verified on
@reown/appkit+@reown/appkit-adapter-tron1.8.23 (latest stable at thetime of writing); the relevant code is unchanged on current
main.Root cause
TRON connectors register asynchronously, but boot-time connection sync runs
immediately and treats "connector not registered yet" as "user disconnected".
TronAdapter.syncConnectors()(packages/adapters/tron/src/adapter.ts) does not addconnectors synchronously. It delegates to
TronConnectUtil.watchWalletAdapters()(
packages/adapters/tron/src/utils/TronConnectUtil.ts), which only callsaddConnector(...)once the wallet adapter'sreadyStateisFound. For an injectedextension wallet,
readyStateis stillLoadingwhilecreateAppKitruns (theextension's detection resolves after page scripts), so the connector is registered
later, via the
readyStateChangedlistener.Meanwhile
AppKitBaseClient.initialize()proceeds tosyncExistingConnection()→syncNamespaceConnection('tron')→syncAdapterConnection('tron')(
packages/appkit/src/client/appkit-base-client.ts). At that pointConnectorController.getConnectors('tron')is still empty, so the lookup fails and themethod throws
Adapter or connector not found for namespace tron— which thecatchturns into
onDisconnectNamespace({ chainNamespace: 'tron', closeModal: false }).onDisconnectNamespaceresets the tron account state, removes the stored connector idand calls
StorageUtil.addDisconnectedConnectorId(...)— recording a user-initiateddisconnect that never happened.
Immediately after,
syncAdapterConnections()→TronAdapter.syncConnections()filtersconnectors with
!hasDisconnected && hasConnected— poisoned by step 3 (and the connectorlist is empty anyway), so nothing is rehydrated.
When the extension finally injects and
watchWalletAdaptersregisters the connector,nothing re-runs the namespace sync. Result: storage looks connected, react state stays
empty until the user manually reconnects — and on the next reload the cycle repeats, so
the tron connection never survives a reload.
The EVM (wagmi) and Solana adapters don't hit this because their connectors are available
synchronously during
syncConnectors(); the tron adapter is structurally always late forinjected wallets.
A secondary symptom of the same race, observed when the wallet's site authorization makes the
adapter auto-connect at boot: AppKit records an internal tron connection whose account state
never reached
ChainController, after which the modal's wallet row dead-ends at "Thisaccount is already linked" (scaffold-ui
isAlreadyConnected) without ever callingadapter.connect()— leaving no way to (re)connect from the UI.Minimal reproduction
React + Vite app, TronLink extension installed and the site authorized in TronLink:
The adapter's own diagnostic log confirms the ordering
(
[TronConnectUtil] Watching wallet adapters: [{ name: 'TronLink', readyState: 'Loading', ... }]at init, with the connector added only later), followed by the silent
AppKit couldn't sync existing connectionpath.Expected behavior
disconnect.
syncAdapterConnectioncould distinguish "no connector registered for thestored id" (leave storage intact / retry) from an actual failed reconnect.
watchWalletAdapters→addConnector), the pendingnamespace sync should re-run so the stored connection rehydrates and
useAppKitAccount({ namespace: 'tron' })populates.Workaround we're using
We bridge account state directly from our wallet-adapter instances into app state (reading
the
@tronweb3adapter'saddress/connectedand subscribing to its events), and keep ourown disconnect flag at the adapter level, bypassing
useAppKitAccountfor the tronnamespace entirely. That works, but it duplicates state AppKit already tracks and shouldn't
be necessary.