Skip to content

[bug] TRON: boot connection sync races async connector registration — stored connection silently wiped, useAppKitAccount({ namespace: 'tron' }) never populates #5741

Description

@claudiosdc

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".

  1. 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.

  2. 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 }).

  3. onDisconnectNamespace resets the tron account state, removes the stored connector id
    and calls StorageUtil.addDisconnectedConnectorId(...) — recording a user-initiated
    disconnect that never happened.

  4. 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.

  5. 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 (watchWalletAdaptersaddConnector), 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions