Skip to content

feat(tron): re-enable WalletConnect adapter#780

Open
tomiiide wants to merge 4 commits into
mainfrom
feat/tron-walletconnect
Open

feat(tron): re-enable WalletConnect adapter#780
tomiiide wants to merge 4 commits into
mainfrom
feat/tron-walletconnect

Conversation

@tomiiide

@tomiiide tomiiide commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Which Linear task is linked to this PR?

https://linear.app/lifi-linear/issue/EMB-435/investigate-walletconnect-modal-closing-immediately-in-widget-v4

Why was it implemented this way?

WalletConnect was previously stubbed out in the Tron provider (createTronAdapters() registered no WC adapter) because of a "No accounts found in session" error: the upstream @tronweb3/tronwallet-adapters WalletConnect adapter uses the same WalletConnect Cloud project id and origin as the EVM provider, so the EVM connector's persisted eip155 namespaces bled into the Tron session proposal and wallets connected as EVM.

This re-enables it by registering WalletConnectAdapter through a new resolveTronWalletConnectConfig helper
(packages/widget-provider-tron/src/config/walletConnect.ts) that:

  • Defaults customStoragePrefix to 'tron' — isolates the Tron WalletConnect storage from the EVM connector's, which is the actual fix
    for the session-bleed bug. Integrators can still override it via options.
  • Mirrors the EVM provider's config contract — walletConnect: true uses the shipped defaults (incl. the shared LI.FI project id, so it works with no integrator setup), a config object is merged over those defaults, and false/undefined disables it.
  • Defaults network to 'Mainnet' (capitalized) — lowercase values fall through to an invalid tron: CAIP id upstream.
  • Adapter registration stays conditional (...(walletConnect ? [new WalletConnectAdapter(...)] : [])), so nothing is registered when WC
    is disabled.

Limitation

From my tests, only two wallets work over wallet connect.

Other wallets confirmed in these issues:
tronweb3/tronwallet-adapter#3
tronweb3/tronwallet-adapter#37
tronweb3/tronwallet-adapter#82 (comment)

I confirmed this by patching the wallet connect library to log the connection details

Connection logs from Token pocket

Screenshot 2026-06-11 at 8 49 02 PM

Connection logs from metamask over wallet connect

Metamask only sends EVM connections over walletconnect.
Screenshot 2026-06-11 at 8 48 48 PM

Visual showcase (Screenshots or Videos)

Screenshot 2026-06-11 at 8 48 24 PM

Checklist before requesting a review

  • I have performed a self-review and testing of my code.
  • This pull request is focused and addresses a single problem.
  • If this PR modifies the Widget API or adds new features that require documentation, I have updated the documentation in the public-docs repository.

Register WalletConnectAdapter via resolveTronWalletConnectConfig, which
forces a 'tron' storage prefix to isolate the session from the EVM
connector — without it eip155 namespaces leak in and wallets connect as EVM.
@changeset-bot

changeset-bot Bot commented Jun 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d395ad0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@lifi/widget-provider-tron Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

E2E Examples — all passed

All examples passed in the latest run.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown

E2E Playground results

passed  158 passed

Details

stats  158 tests across 10 suites
duration  1 minute, 56 seconds
commit  d395ad0

📥 Download full HTML report (open the run → Artifacts → playwright-report)

@tomiiide tomiiide added the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Jun 12, 2026
@tomiiide tomiiide self-assigned this Jun 12, 2026
@github-actions

Copy link
Copy Markdown

📦 Preview published under the preview dist-tag.

Install the exact version(s) — @preview moves with the newest preview across PRs:

npm i @lifi/widget-provider-tron@0.0.0-preview-6cc63bac
npm i @lifi/widget@0.0.0-preview-6cc63bac

@github-actions github-actions Bot removed the release-preview Publish a 0.0.0-preview-<sha> build of this PR to npm label Jun 12, 2026
@tomiiide tomiiide added the Agent Review Request triggers QA Agent Zeus label Jun 12, 2026
@tomiiide tomiiide requested a review from effie-ms June 12, 2026 13:00
@github-actions github-actions Bot added QA AI Reviewing and removed Agent Review Request triggers QA Agent Zeus labels Jun 12, 2026
@lifi-qa-agent

lifi-qa-agent Bot commented Jun 12, 2026

Copy link
Copy Markdown

🔍 QA Review — EMB-435

Ticket: EMB-435 — Investigate WalletConnect modal closing immediately in Widget v4
PR: #780 — feat(tron): re-enable WalletConnect adapter
Review type: 🔁 Re-review (post-response)
Reviewer: lifi-qa-agent[bot]


What this PR does

This PR re-enables the Tron WalletConnect adapter that was previously stubbed out in TronBaseProvider.tsx with a TODO. The root cause of the Tron-specific WalletConnect failure was a namespace collision: the Tron WC adapter shared the same project ID and origin as the EVM connector, so the EVM eip155 namespaces bled into the Tron session proposal — wallets connected as EVM and produced "No accounts found in session". The fix introduces resolveTronWalletConnectConfig, a resolver that isolates Tron WC storage via a forced customStoragePrefix: 'tron' and normalises the network value to capitalised form (e.g. 'Mainnet'). A companion fix in the playground corrects the pre-existing lowercase 'mainnet' value.


Re-review — item resolution

# Severity Item Status Verification
C-1 🔴 Critical EVM WalletConnect flash-and-close not addressed ✅ Accepted — out of scope Developer provided evidence that the real cause was integrator-side missing peer deps (@walletconnect/ethereum-provider, @coinbase/wallet-sdk not installed under wagmi v3). Partner confirmed this on 2026-06-11. Thomas Faber's earlier modal-injection hypothesis was not the root cause.
H-1 🔴 High customStoragePrefix overridable vs. changeset claim ✅ Fixed in code Verified: customStoragePrefix: 'tron' is now placed after ...config?.options spread, making it non-overridable. Code and changeset are aligned.
H-2 🔴 High network input not normalised — lowercase invalid CAIP ID ✅ Fixed in code Verified: network: \${network.charAt(0).toUpperCase()}${network.slice(1)}`` now normalises any caller input regardless of casing.
M-1 🟠 Medium No unit tests for resolveTronWalletConnectConfig ✅ Accepted with justification No widget-provider-* package has a unit-test setup. Introducing one here in isolation would be inconsistent. Test infrastructure should be added across the provider packages in a dedicated change.
M-2 🟠 Medium Public docs not updated for TronProviderConfig.walletConnect ✅ Acknowledged Developer commits to a follow-up in the public-docs repo. PR checklist item remains unchecked intentionally.
M-3 🟠 Medium createTronAdapters signature change not noted in changeset ✅ Fixed Changeset now explicitly states: "The change is backwards-compatible — existing callers keep the previous behaviour (WalletConnect adapter not registered)."
L-1 🟢 Low Default projectId hard-coded rather than shared constant ✅ Accepted Intentional design: avoids coupling widget-provider-tron to widget-provider-ethereum for a single constant. Comment in code documents the intent.

Code verification summary

packages/widget-provider-tron/src/config/walletConnect.ts (current HEAD 015e24ad):

options: {
  projectId: defaultProjectId,
  ...config?.options,
  // Forced last: a distinct prefix keeps the EVM connector's eip155
  // namespaces (shared project id) out of the Tron session proposal.
  customStoragePrefix: 'tron',    // ← H-1 FIXED: after spread, cannot be overridden
},
network: `${network.charAt(0).toUpperCase()}${network.slice(1)}`,  // ← H-2 FIXED: always normalised

Both fixes are confirmed in the current HEAD.


Changeset

.changeset/tron-walletconnect.md correctly describes:

  • TronProviderConfig.walletConnect new API
  • The forced customStoragePrefix isolation mechanism and the EVM bleed bug it prevents
  • createTronAdapters optional argument + backwards compatibility note (M-3 fixed)

Minor residual note: The changeset still says "The resolver always forces options.customStoragePrefix to 'tron'" — this is now accurate given the corrected spread order (H-1). ✅


Acceptance Criteria — final assessment

# Criterion Status Notes
AC-1 Reproduction confirmed or ruled out ✅ Met Tron session-bleed confirmed and fixed. EVM flash-and-close confirmed as integrator-side missing-dep issue, not a widget bug.
AC-2 Root cause identified ✅ Met Both root causes now identified and documented: (1) Tron WC namespace bleed — fixed; (2) EVM peer dep not installed — integrator-side fix.
AC-3 Guidance or fix provided ✅ Met Tron WC fix shipped. EVM guidance: add @walletconnect/ethereum-provider + @coinbase/wallet-sdk as direct deps under wagmi v3. Public-docs follow-up acknowledged.

✅ Verdict: Pass

All critical and high items are resolved. The Tron WalletConnect re-enablement is technically sound:

  • Storage isolation via customStoragePrefix: 'tron' is now enforced (H-1 ✅)
  • network normalisation prevents the invalid CAIP ID bug for new integrators (H-2 ✅)
  • The EVM scope question (C-1) is resolved — the original partner report was an integrator-side dependency issue, not a widget bug
  • Changeset correctly describes all public surface changes

The open items (M-1 unit test infrastructure, M-2 public docs) are tracked and accepted with appropriate justifications consistent with the codebase's current conventions.


🤖 Automated QA review by lifi-qa-agent. Powered by Claude.

@lifi-qa-agent lifi-qa-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes on 6 items — each requires either a code fix or an explicit acceptance comment with justification before this review is considered complete.

# Severity Type Issue / File
1 🔴 Critical Scope gap EVM WalletConnect flash-and-close not addressed — packages/wallet-management/src/utils/elements.ts and EthereumListItemButton.tsx
2 🔴 High Code customStoragePrefix overridable vs. changeset claim of "always forces" — packages/widget-provider-tron/src/config/walletConnect.ts
3 🔴 High Code network input not normalised — lowercase values produce invalid CAIP ID — packages/widget-provider-tron/src/config/walletConnect.ts
4 🟠 Medium Test gap packages/widget-provider-tron/src/config/walletConnect.test.ts (new)
5 🟠 Medium Documentation Public docs not updated for TronProviderConfig.walletConnect new API surface
6 🟠 Medium Code createTronAdapters signature change not noted in changeset for direct callers

1. [Critical] EVM WalletConnect flash-and-close not addressed

The partner's original complaint (WalletConnect modal flashes open and closes when clicking WalletConnect in the widget) is caused by createWalletConnectElement() in EthereumListItemButton.tsx injecting a <w3m-modal> element into the widget's MUI dialog. AppKit adopts this stale element instead of mounting its own on document.body, and AppKit's ModalController transitions it back to open: false immediately. Thomas Faber's analysis in the Linear comments identifies exactly this mechanism and proposes dropping the injection. As verified on main today, packages/wallet-management/src/utils/elements.ts and EthereumListItemButton.tsx still contain this code unchanged. This PR only fixes the Tron WC session-bleed issue. After this PR merges, EVM WalletConnect will still flash and close for the partner. Either include Thomas Faber's fix here, or open a separate linked PR before the ticket is closed.

2. [High] customStoragePrefix overridable — changeset says "always forces"

In resolveTronWalletConnectConfig:

options: {
  projectId: defaultProjectId,
  customStoragePrefix: 'tron',
  ...config?.options,   // ← caller can override customStoragePrefix
},

The changeset states the resolver "always forces options.customStoragePrefix to 'tron'" but because ...config?.options is spread after, any caller passing config.options.customStoragePrefix silently overrides it, re-introducing the namespace bleed. Decide: (a) if override is intentional, update the changeset/comment to say "defaults to 'tron'" and add a JSDoc warning; (b) if it must be forced, move customStoragePrefix: 'tron' after ...config?.options.

3. [High] network input not normalised

network: config?.network ?? 'Mainnet',

Only applies 'Mainnet' when the field is absent. A caller passing { network: 'mainnet' } (lowercase) gets an invalid CAIP chain ID at the adapter level — exactly the bug the defaultWidgetConfig.ts fix in this PR corrects for the playground. Normalise the value (.replace(/^\w/, c => c.toUpperCase())) or validate and throw/warn, so new integrators don't hit this silently.

4. [Medium] Test gap — walletConnect.test.ts (new)

  • Missing: walletConnect: undefined / false → returns undefined
  • Missing: walletConnect: true{ network: 'Mainnet', options: { customStoragePrefix: 'tron', projectId: '...' } }
  • Missing: explicit network value preserved (e.g. 'Shasta')
  • Missing: options.customStoragePrefix override behaviour (clarifies H-2 intent)
  • Missing: caller projectId overrides default

5. [Medium] Public docs not updated

TronProviderConfig.walletConnect (WalletConnectAdapterConfig | boolean) is a new public API. The PR checklist item for public-docs is explicitly unchecked. At minimum document: what true means, what a config object can contain, and the network capitalisation requirement.

6. [Medium] createTronAdapters signature change not noted in changeset

createTronAdapters is a public export. Its signature changed from () => Adapter[] to (walletConnect?: WalletConnectAdapterConfig) => Adapter[]. The changeset focuses on TronProviderConfig.walletConnect but doesn't mention that direct callers of createTronAdapters must pass the new parameter to get WalletConnect support. Add a note for direct callers.


💡 Once you've addressed the items above, re-apply the "Agent Review Request" label to trigger an automated re-review.

@tomiiide

Copy link
Copy Markdown
Contributor Author

🔴 C-1 — Out of scope, and the EVM issue is already resolved

This is built on Thomas Faber's 2026-06-09 hypothesis (the createWalletConnectElement() /
<w3m-modal> AppKit collision). That was superseded two days later. On 2026-06-11 the reporting
partner found and fixed the real cause themselves:

"wagmi v3 (@wagmi/connectors) ships the connector backends as optional peer dependencies, so pnpm
never installed @walletconnect/ethereum-provider or @coinbase/wallet-sdk. With no backend
installed, the connector mounts the modal and tears it down immediately with no error, which matches
the flash and close. We added both as direct dependencies and now WalletConnect and Coinbase both
work."

So the EVM flash-and-close was an integrator-side missing-dependency issue, not a widget bug, and
it's confirmed resolved. The createWalletConnectElement() collision was never confirmed as the
cause. This PR's scope is the separate request from that same thread — enabling Tron wallet
connection over WalletConnect. I'm intentionally not touching EthereumListItemButton.tsx /
elements.ts against an unverified theory with no reproduction; that would be scope creep. Happy to
revisit the modal-mounting helper as a standalone cleanup if a real repro surfaces.

🔴 H-1 — Fixed

Good catch on the spread order. customStoragePrefix: 'tron' is now applied after
...config?.options, so it can no longer be overridden — the changeset's "always forces" guarantee
now matches the code.

🔴 H-2 — Fixed

network is now normalized (first letter uppercased) regardless of caller input, so a lowercase
'mainnet' resolves to 'Mainnet' instead of an invalid tron:<name> CAIP id.

🟠 M-1 — Won't add (consistency)

None of the widget-provider-* packages have a unit-test setup — adding one to widget-provider-tron
alone would make it the lone outlier with its own vitest config and dep. I'd rather introduce test
infra across the provider packages in a dedicated change than bolt it onto this one. The resolver is
covered by check:types and exercised through the playground in the meantime.

🟠 M-3 — Done

Changeset now notes that createTronAdapters gained an optional WalletConnectAdapterConfig
argument and that the change is backwards-compatible (existing callers keep WC disabled).

🟠 M-2 — Acknowledged

Public-docs update for TronProviderConfig.walletConnect will follow in the public-docs repo; the
checklist item stays unchecked until then.

🟢 L-1 — Leaving as-is

The default projectId intentionally duplicates the EVM provider's so walletConnect: true works
with zero integrator setup. A shared constant would couple widget-provider-tron to
widget-provider-ethereum for a single string — not worth it. The comment documents the intent.

@tomiiide tomiiide added the Agent Review Request triggers QA Agent Zeus label Jun 12, 2026
@github-actions github-actions Bot removed the Agent Review Request triggers QA Agent Zeus label Jun 12, 2026

@lifi-qa-agent lifi-qa-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review complete — all raised items resolved. H-1 (customStoragePrefix ordering) and H-2 (network normalisation) verified fixed in code. C-1 scope accepted: EVM flash-and-close was integrator-side missing peer dep, confirmed resolved by partner. ✅ QA Pass.

Comment thread pnpm-lock.yaml

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you revert these lockfile changes if they are unrelated?

return {
...config,
// Lowercase yields an invalid `tron:<name>` CAIP id upstream.
network: `${network.charAt(0).toUpperCase()}${network.slice(1)}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you think, is there a way we could prevent users from passing a network name with the wrong casing?

I mean without capitalizing the first letter on our side. Maybe with some constant/enum?

I am thinking there is still a possibility they would pass 'MAINNET' - and then it would be invalid as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, addressed in d395ad0.

- match network names case-insensitively, snap to canonical ChainNetwork value
- add resolveTronWalletConnectConfig tests
- revert unrelated lockfile dev-tooling bumps
@vinzenzLIFI

vinzenzLIFI commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

e2e tested the flow with wallet connect on both evm and tron and it works!

There are a couple of findings I had and I don't know if you can do anything about it @tomiiide.
Both trace back to the Tron WC adapter, and a lot of it is in the third-party @tronweb3/walletconnect-tron@4.0.2 wrapper rather than our code:

Finding 1 — Tron and EVM resolve to the same address

  1. Connect any EVM-only wallet (e.g. MetaMask) via WalletConnect.
  2. Click "Connect another wallet" → select WalletConnect for Tron.
  3. Instead of scanning the QR code using a Tron wallet, scan using the previously used App with EVM wallet again.

Result:

Same wallet address is used for EVM and TRON
Screenshot 2026-06-15 at 16 40 25

Root cause:

the wrapper's address extractor doesn't filter by namespace. In @tronweb3/walletconnect-tron@4.0.2 lib/esm/adapter.js (extractAddressFromSession, ~L59-71):

const accounts = Object.values(session.namespaces).flatMap(ns => ns.accounts);
const account = accounts[0];                 // first account from ANY namespace
const address = account.split(':')[2];       // "eip155:1:0x95.." -> "0x95.."

It takes accounts[0] across all namespaces, so an eip155 account is happily returned as the Tron address. This isn't only cosmetic — it flows into the route request as the Tron sender:

  • useAccount({ chainType }) returns the Tron adapter's account → account.address (useAccount.ts:57)
  • which becomes fromAddress (useRoutes.ts:116, useRoutes.ts:228)
    So a Tron→X swap can be built with a 0x… sender. A guard at the Tron provider boundary (reject a connected Tron account whose address isn't a valid base58 T…) would stop this regardless of the wrapper. Upstream fix would be to filter to tron: accounts in extractAddressFromSession.

Finding 2 — a previously-connected WC wallet disappears after a while; refresh re-opens the QR

Repro: connect both an EVM and a Tron wallet via WalletConnect, use the widget for a few minutes. One of the two wallets vanishes from the connected list; on refresh, its QR pops up again.

Root cause: the two WalletConnect cores silently share one storage namespace. The branch's "storage isolation" (customStoragePrefix: 'tron') is ineffective as installed — the prefix is set but never reaches WalletConnect.

  1. We set it in walletConnect.ts (customStoragePrefix: 'tron', ~L23) precisely to isolate Tron from EVM (both use the same project id 5432e3507d41270bee46b7b85bbc2ef8).
  2. But the wrapper drops it — @tronweb3/walletconnect-tron@4.0.2 lib/esm/adapter.js:43-48 only forwards 4 fields to UniversalProvider.init:
    UniversalProvider.init({ projectId, logger: this._options?.logger,
      relayUrl: this._options?.relayUrl, metadata: this._options?.metadata })
    // customStoragePrefix is NOT forwarded
  3. WC keys are namespaced only by that prefix, never by project id. In @walletconnect/core:
    get storageKey(){ return this.storagePrefix + this.version + this.core.customStoragePrefix + "//" + this.name }
    // customStoragePrefix = opts?.customStoragePrefix ? `:${prefix}` : ""
    With both cores at "", EVM and Tron write to identical keys: wc@2:client:0.3//session, wc@2:core:0.3//keychain, //pairing, //expirer. (EVM and Tron pull different core versions, but same key format + same 0.3 store version → full collision.)
  4. Each store persists its entire in-memory map (persist()setItem(storageKey, this.values)), and each core only holds its own session. So whenever either core writes, it overwrites the shared //session blob with a version that omits the other ecosystem's session — last writer wins.

Why refresh → QR

On reload each adapter rehydrates from storage and auto-connects; the Tron adapter does client.find(getConnectParams(...)).filter(s => s.acknowledged) in connect() (walletconnect-tron adapter.js, ~L186). Whichever session wasn't the last writer is no longer in storage → find() returns []appKit.open() → QR. Which wallet it is depends on persist ordering, matching "one is missing."

Why it also disappears live, no refresh (same root cause; exact trigger less pinned)

Once the victim core loses its backing state (symkey/subscription), its still-live session is torn down and it emits session_delete. That propagates cleanly: walletconnect-tron adapter.js:141-150 (session_delete handler) → emit('disconnect') → WC adapter emit('stateChanged', Disconnect) → react-hooks WalletProviderTronProviderValuesuseAccount filters it out → wallet vanishes from the menu. It's purely event-driven, no polling in widget code. Plain TTL expiry is not the cause (session TTL is 7 days).

Notes

  • On the EVM side specifically, the disconnect tends to surface via syncWagmiConfig calling reconnect(wagmiConfig) on every chain-list change (syncWagmiConfig.ts:25, useSyncWagmiConfig.ts:29) — reconnect probes storage, finds the clobbered EVM session gone, and flips to disconnected.
  • There's also a globalThis._walletConnectCore_ singleton (keyed by the same empty prefix) that may merge the two cores' relayer/expirer/pairing — if so, the core-level stores are shared-single rather than clobbered-by-two, but the session-store clobber above holds either way. Worth a runtime check.

Fix (in our control, doesn't need the third-party wrapper)

Since the Tron wrapper drops the prefix, isolate the EVM side instead — set customStoragePrefix on the EVM walletConnect connector params (wagmi → @walletconnect/ethereum-provider does forward it to UniversalProvider.init). Then EVM writes wc@2:…:evm//…, Tron stays "", and they no longer collide. Alternatively patch-package the Tron wrapper to forward options.customStoragePrefix (or upgrade if a fixed version exists).

Confirm at runtime

DevTools → Application → IndexedDB (WALLET_CONNECT_V2_INDEXED_DB) — you'll see only un-prefixed wc@2:… keys (no …:tron//…), which alone proves the isolation isn't active. Watch wc@2:client:0.3//session flip between holding one vs. both sessions as each core persists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants