Skip to content

chore(sync): merge main into develop - #8106

Merged
azebuado merged 7 commits into
developfrom
automation/sync-main-to-develop
Sep 7, 2026
Merged

chore(sync): merge main into develop#8106
azebuado merged 7 commits into
developfrom
automation/sync-main-to-develop

Conversation

@cowswap-release-sync

Copy link
Copy Markdown
Contributor

This PR contains an automated merge commit from main into develop.

azebuado and others added 5 commits September 4, 2026 14:32
…et (#8088)

# Summary

Restore Explorer's font size:

<img width="3440" height="1139" alt="Screenshot 2026-09-03 at 14 29 40"
src="https://github.com/user-attachments/assets/f7530c91-32a0-4e77-8c13-aa14a848dde1"
/>

# To Test

1. Open Explorer Solvers page
2. Check the font size in the inputs
* Should be the same as current production
3. Check the rest of the Explorer app
* Fonts size should remain as it was

# Self-checks

- [x] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)
- [x] I have manually tested changes on Vercel preview deployment
- [x] I have done self-review and (or) AI review
- [x] I have addressed all comments from @coderabbitai
- [x] I have less than three open PRs/Stacks in this repo at the moment
of creating this PR


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Restored the default readable font size for buttons, text areas,
dropdowns, and most text input fields in Explorer.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Summary

Prevent duplicated RWA token lists.

<img width="1193" height="1059" alt="image"
src="https://github.com/user-attachments/assets/0549da36-0cb3-4954-acb9-efad21961ed6"
/>

1. Dedupes token lists by path rather than commit id. New lists with a
different commit will always differ if checked fully
2. On every load, looks for stale lists and removes them. Otherwise we'd
have a growing list on every CMS update.

# To Test

1. Load existing localStorage from somewhere else, like staging
2. Open the list selector
* Not duplicates
* No lists (if in a not allowed location)

## AI description

<details>

## What changed

- `getSourceAsKey` now drops the git ref from GitHub raw URLs, so
`main/…`, `refs/heads/main/…` and a pinned SHA all resolve to the same
list identity (`owner/repo/path`). Only refs that can be delimited with
certainty are stripped; a ref that may itself contain slashes is left
verbatim rather than guessed at. It is the single key builder behind
`blockedCountriesPerList` / `consentHashPerList`, so every consumer
picks this up: `useIsListBlocked`, `useFilterBlockedLists`,
`useRestrictedListInfo`, `useIsListRequiresConsent`,
`useFilterListsWithConsent`, `useConsentAwareToggleList`,
`allTokensAtom`, `BlockedListSourcesUpdater`.
- `listsStatesMapAtom` drops re-pinned leftovers on read: when several
stored sources share that key, only the URL the app currently ships is
surfaced. Virtual widget lists are merged in afterwards and never
touched.
- `upsertListsAtom` applies the same filter to what it persists, so the
leftovers are removed from IndexedDB rather than just hidden. Its
`listsStates` argument is what the app just fetched for that chain,
which is exactly the "currently shipped" set needed to tell a leftover
from a live list.

## Why

RWA geoblocking is matched on the exact token list URL, but those URLs
are pinned commit SHAs that change every time an issuer adds a ticker.
This release re-pinned four of them:

```
- .../ondoprotocol/cowswap-global-markets-token-list/refs/heads/main/tokenlist.json
+ .../ondoprotocol/cowswap-global-markets-token-list/cf97552d.../tokenlist.json
```

Stored lists are keyed by URL and nothing prunes a URL that drops out of
the default config, so a returning user keeps the branch-ref entry
forever. The CMS moved to the pinned URL, the stored key did not,
`blockedCountriesPerList[storedKey]` came back `undefined`, and
`useFilterBlockedLists` keeps a list it has no entry for. Result on
staging: RWA lists stayed visible from a restricted location, while
incognito looked correct because it had no stored state.

Two problems, hence two layers:

- Without the ref-agnostic key this recurs silently on every re-pin —
the UI looks fine and geoblocking is just off.
- The leftovers are not free. Each one keeps a full parsed token list:
the xStocks list is **1.26 MB** and is configured on chains 1, 56, 42161
and 57073; Ondo is **334 KB** on chains 1 and 56.
`listsStatesByChainAtom` loads all of it into memory with `getOnInit`
and re-serializes it on every upsert, so a leftover costs disk, heap and
stringify work on every write until it is pruned.

## QA Testing

Needs the `isRwaGeoblockEnabled` flag on, and a VPN for the
restricted-location cases (BNB below; `PL`, `DE`, `US` are all in
`RESTRICTED_COUNTRIES`).

The bug only reproduces with list state stored *before* the re-pin, so a
clean preview profile cannot show it. Seed the origin under test first,
using either recipe.

**The fixture is consumed by the fix.** Once the preview loads and
finishes fetching lists, the leftover is pruned from storage for good.
Re-seed before every run, and do the staging control step on its own
seed.

**Recipe A — copy an existing profile's state onto the preview**

The full `allTokenListsInfoAtom:v7` value is megabytes — every list with
all its tokens — so do not move it wholesale. Only the GitHub-raw list
entries matter here, and only their identity, not their tokens.

1. Open DevTools on an origin that still holds the old lists
(https://staging.swap.cow.fi or https://swap.cow.fi) and run this. It
keeps just the re-pinnable entries and empties their token arrays, which
cuts the payload to a few hundred characters:

```js
await (async () => {
  const db = await new Promise((ok, err) => {
    const req = indexedDB.open('cowswap_jotai')
    req.onsuccess = () => ok(req.result)
    req.onerror = () => err(req.error)
  })
  const store = db.transaction('keyvaluepairs', 'readonly').objectStore('keyvaluepairs')
  const raw = await new Promise((ok) => {
    const req = store.get('allTokenListsInfoAtom:v7')
    req.onsuccess = () => ok(req.result)
  })

  const slim = {}

  for (const [chainId, chain] of Object.entries(JSON.parse(raw))) {
    for (const [source, state] of Object.entries(chain)) {
      if (state === 'deleted' || !source.includes('raw.githubusercontent.com')) continue

      slim[chainId] ??= {}
      // tokens play no part in list hiding and are what makes the dump unmanageable
      slim[chainId][source] = { ...state, list: { ...state.list, tokens: [] } }
    }
  }

  const payload = JSON.stringify(slim)
  console.log(`${payload.length} chars, select the next line and copy it:`)
  console.log(payload)
})()
```

Copy the logged line by selecting it. Do not use `copy()` — it is
silently a no-op in Firefox on macOS.

2. Open
https://swap-dev-git-fix-rwa-list-migration-cowswap-dev.vercel.app
**once and let it load** — localforage only creates the `cowswap_jotai`
database and its `keyvaluepairs` store on first app start, and the
import throws `NotFoundError` without it.
3. In the preview's console, paste the copied JSON where marked and run.
This merges the entries in; the preview's own lists are left alone:

```js
const STALE = /* paste the copied JSON here */

await (async () => {
  const KEY = 'allTokenListsInfoAtom:v7'
  const db = await new Promise((ok, err) => {
    const req = indexedDB.open('cowswap_jotai')
    req.onsuccess = () => ok(req.result)
    req.onerror = () => err(req.error)
  })
  const read = db.transaction('keyvaluepairs', 'readonly').objectStore('keyvaluepairs')
  const raw = await new Promise((ok) => {
    const req = read.get(KEY)
    req.onsuccess = () => ok(req.result)
  })

  const data = raw ? JSON.parse(raw) : {}
  let merged = 0

  for (const [chainId, chain] of Object.entries(STALE)) {
    data[chainId] ??= {}

    for (const [source, state] of Object.entries(chain)) {
      if (data[chainId][source]) continue

      data[chainId][source] = state
      merged++
    }
  }

  db.transaction('keyvaluepairs', 'readwrite').objectStore('keyvaluepairs').put(JSON.stringify(data), KEY)
  console.log(`merged ${merged} entries, reload now`)
})()
```

4. Reload.

**Recipe B — synthesize the pre-re-pin state (no second profile
needed)**

Open the origin once, then run this and reload. It clones every
SHA-pinned list into its old `refs/heads/main` URL, which is exactly the
state a returning user has:

```js
await (async () => {
  const KEY = 'allTokenListsInfoAtom:v7'
  const db = await new Promise((ok, err) => {
    const req = indexedDB.open('cowswap_jotai')
    req.onsuccess = () => ok(req.result)
    req.onerror = () => err(req.error)
  })
  const read = db.transaction('keyvaluepairs', 'readonly').objectStore('keyvaluepairs')
  const raw = await new Promise((ok) => {
    const req = read.get(KEY)
    req.onsuccess = () => ok(req.result)
  })

  const data = JSON.parse(raw)
  let seeded = 0

  for (const chain of Object.values(data)) {
    for (const [source, state] of Object.entries(chain)) {
      const stale = source.replace(
        /^(https:\/\/raw\.githubusercontent\.com\/[^/]+\/[^/]+\/)[0-9a-f]{40}\//,
        '$1refs/heads/main/',
      )
      if (stale === source || chain[stale]) continue

      chain[stale] = { ...state, source: stale }
      seeded++
    }
  }

  db.transaction('keyvaluepairs', 'readwrite').objectStore('keyvaluepairs').put(JSON.stringify(data), KEY)
  console.log(`seeded ${seeded} pre-re-pin entries`)
})()
```

Preview URL QA (run after seeding, on
https://swap-dev-git-fix-rwa-list-migration-cowswap-dev.vercel.app/#/56/swap/USDC/WBNB):

- Control first, on its own seed: seed https://staging.swap.cow.fi and
open Manage → Lists from a restricted location. Ondo, xStocks and
Reserve Restricted DTFs BNB are listed, each twice — once with its real
token count and once with the seeded `0 tokens`. That is the bug.
- Seed the preview and repeat from a restricted location: all three must
be absent.
- Seed the preview again and open it from a non-restricted location:
each of the three appears **exactly once**, with its real token count.
The seeded `0 tokens` row must not be there.
- Storage is cleaned, not just hidden. After the page has finished
loading lists, run this and expect `0`:

```js
await (async () => {
  const db = await new Promise((ok, err) => {
    const req = indexedDB.open('cowswap_jotai')
    req.onsuccess = () => ok(req.result)
    req.onerror = () => err(req.error)
  })
  const store = db.transaction('keyvaluepairs', 'readonly').objectStore('keyvaluepairs')
  const raw = await new Promise((ok) => {
    const req = store.get('allTokenListsInfoAtom:v7')
    req.onsuccess = () => ok(req.result)
  })

  const leftovers = Object.values(JSON.parse(raw)).flatMap((chain) =>
    Object.keys(chain).filter((source) => source.includes('/refs/heads/')),
  )
  console.log(`${leftovers.length} leftovers still stored`, leftovers)
})()
```

- Regression: with the flag off, or from a non-restricted location, all
other lists (CoW Swap, CoinGecko, Uniswap) behave as before, and adding,
toggling and deleting a custom list still works.

Developer verification:

- `libs/tokens/src/hooks/lists/useIsListBlocked.test.tsx` pins the key
normalization: `main`, `master`, `refs/heads/` and SHA forms collapse to
one key; nested paths survive; non-GitHub sources are untouched; a ref
that cannot be delimited is returned verbatim; two different files in
the same repo never merge.
- `libs/tokens/src/state/tokenLists/tokenListsStateAtom.test.ts` adds
three cases: `listsStatesMapAtom` surfaces only the shipped URL and
leaves unrelated lists alone, keeps one entry when no stored URL is the
shipped one, and `upsertListsAtom` removes the leftover from
`listsStatesByChainAtom` itself rather than only from the rendered map.
Each fails with its filter stubbed out. The first derives its fixture
from `DEFAULT_TOKENS_LISTS`, so it will not rot on the next re-pin.
- `npx jest --config libs/tokens/jest.config.ts` — 9 suites, 61 tests.

Reviewer note:

- This deliberately does not clean storage with an IndexedDB migration.
`listsStatesByChainAtom` loads with `getOnInit` and is re-persisted
wholesale by `upsertLists`, so a write made directly to localForage
races the atom's initial read and is clobbered by the in-memory copy.
Both filters therefore go through jotai.
- The two layers cover different cases and neither is redundant: the
read filter is correct immediately, including before the first fetch
lands and on chains that are never upserted; the write filter is what
actually reclaims the storage.
- CodeRabbit flagged that a branch or tag name containing `/` normalizes
to the wrong key ([review
comment](#8089 (comment))).
Confirmed, and the consequence is worse than a missed match:
`refs/heads/release/v1/tokenlist.json` and
`refs/heads/main/v1/tokenlist.json` both produced
`…/acme/repo/v1/tokenlist.json`, so `dropRepinnedDuplicates` would have
deleted a list that is not a duplicate. Fixed by stripping only refs
that can be identified with certainty — a 40-character SHA, or
`main`/`master` in short or `refs/heads/` form. Anything else is left
verbatim, which costs a missed match and nothing else. Both cases are
pinned by tests.
- Unchanged by this PR: a per-list enabled/disabled preference still
does not survive a re-pin, because `upsertListsAtom` carries `isEnabled`
over by exact URL. Worth a follow-up, not bundled here.

</details>

# Self-checks

- [x] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)
- [x] I have manually tested changes on Vercel preview deployment
- [x] I have done self-review and (or) AI review
- [x] I have addressed all comments from @coderabbitai
- [x] I have less than three open PRs/Stacks in this repo at the moment
of creating this PR
## What changed

- Keep page scroll locking on `<body>`.
- Stop turning `<html>` into an overflow container while the page is
locked.

Demo:


https://github.com/user-attachments/assets/6608ce09-2d31-40b2-abb4-e39467e39cf6

## Why

- Locking `<html>` after the page had been scrolled displaced the sticky
mobile header, hiding the close control and first menu items.

## QA Testing

Preview URL QA:

- Open the [swap-dev
preview](https://swap-dev-git-hotfix-fix-mobile-menu-header-cowswap-dev.vercel.app)
at `375 × 667` or on a mobile device.
- Scroll down, open the header menu, and confirm the header, close
control, and `Trade` item remain fully visible.
- Close the menu, then confirm normal page scrolling is restored.
- Open a token selector and confirm the background page remains locked
while it is open.
- Close the token selector and confirm normal page scrolling is
restored.

## Preview URLs

| Surface | URL |
| --- | --- |
| swap-dev - branch preview URL |
https://swap-dev-git-hotfix-fix-mobile-menu-header-cowswap-dev.vercel.app
|
| explorer-dev - branch preview URL |
https://explorer-dev-git-hotfix-fix-mobile-menu-header-cowswap-dev.vercel.app
|
| cowfi - branch preview URL |
https://cowfi-git-hotfix-fix-mobile-menu-header-cowswap.vercel.app |
| storybook - branch preview URL |
https://storybook-git-hotfix-fix-mobile-menu-header-cowswap-dev.vercel.app
|
| sdk-tools - branch preview URL |
https://sdk-tools-git-hotfix-fix-mobile-menu-header-cowswap-dev.vercel.app
|
| widget-configurator - branch preview URL |
https://widget-configurator-git-hotfix-fix-mobile-me-6f8d0e-cowswap-dev.vercel.app
|

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Bug Fixes**
- Adjusted scroll-lock behavior so modal and overlay states continue
locking the page through the `<body>` element without additionally
locking the `<html>` element.
- Improved scrolling behavior in scenarios where the page uses the
`noScroll` state, reducing unintended scroll-container restrictions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
# Summary

HOTFIX

## Changes

526bbce fix: keep mobile menu header visible after scroll (#8098)
1457cda fix: do not show partial approval when unsupported  (#8093)
673c643 fix(rwa): fix rwa list migration (#8089)
cd7691e fix(explorer): restore explorer's font size removed on the
global reset (#8088)
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cowfi Ready Ready Preview Sep 7, 2026 10:44am UTC
explorer-dev Ready Ready Preview Sep 7, 2026 10:44am UTC
storybook Ready Ready Preview Sep 7, 2026 10:44am UTC
swap-dev Ready Ready Preview Sep 7, 2026 10:44am UTC
widget-configurator Ready Ready Preview Sep 7, 2026 10:44am UTC
2 Skipped Deployments
Project Deployment Actions Updated
cosmos Ignored Ignored Sep 7, 2026 10:44am UTC
sdk-tools Ignored Ignored Preview Sep 7, 2026 10:44am UTC

Request Review

cow-protocol and others added 2 commits September 7, 2026 11:36
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.


# Releases

> The changelog information of each package has been omitted from this
message, as the content exceeds the size limit.

## @cowprotocol/cow-fi@2.13.1


## @cowprotocol/cowswap@3.25.1


## @cowprotocol/explorer@4.11.1


## @cowprotocol/widget-configurator@3.12.1


## @cowprotocol/balances-and-allowances@3.12.1


## @cowprotocol/multicall@3.6.1


## @cowprotocol/snackbars@2.3.1


## @cowprotocol/tokens@3.11.1


## @cowprotocol/ui@3.12.1


## @cowprotocol/wallet@3.12.1

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@cowswap-release-sync
cowswap-release-sync Bot force-pushed the automation/sync-main-to-develop branch from 01d8391 to 4914e04 Compare September 7, 2026 10:36
@azebuado
azebuado merged commit ed2366a into develop Sep 7, 2026
17 checks passed
@azebuado
azebuado deleted the automation/sync-main-to-develop branch September 7, 2026 12:37
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 7, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants