Skip to content

feat(frontend): inform holders of 1Sec-bridged balances - #13933

Draft
sbpublic wants to merge 8 commits into
mainfrom
feat/frontend/onesec-bridged-balance-warning
Draft

feat(frontend): inform holders of 1Sec-bridged balances#13933
sbpublic wants to merge 8 commits into
mainfrom
feat/frontend/onesec-bridged-balance-warning

Conversation

@sbpublic

@sbpublic sbpublic commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Stacked on #13928, which restricts 1Sec swaps to the unwrapping direction. That closes the way in, but says nothing to the users who already hold a bridged balance — and many of them never added the token deliberately, since the wrapped ERC-20 of ICP is a suggested token on Ethereum, Base and Arbitrum. This PR tells them the exit exists, rather than leaving them to open each token to find out.

Changes

  • Add isOneSecWrappedToken, the holder's-eye view of feat(frontend): restrict 1Sec swaps to the unwrapping direction #13928's direction rule: the ICRC ledger is the wrapped side of an EVM-native (locker) token, the ERC-20 is the wrapped side of an ICP-native (minter) token. The same OneSec config drives both the swap gate and this check, so the two cannot drift.
  • Bound the EVM branch to the chains OneSec bridges. A minter token reuses one contract address across all of them, so without the bound an unrelated token deployed at that address on another chain would match.
  • Add oneSecBridgedTokensWithBalance, pairing that check with the balances store. Scoped to enabled tokens, since that is the set balances load for, and empty while OneSec swaps are off — with no swap available there would be no action to point the user at.
  • Add OneSecBridgedBalanceWarning, a dismissible MessageBox level="warning" mounted at the top of the page content, naming the tokens and recommending they be swapped back to their native network.
  • Name each token with its network ("ICP (Base)", not "ICP"). This reuses the $token_list / formatList pattern of the Activity page's index-canister warnings, but adds the network qualifier those do not need: a wrapped token here shares its symbol with the native token the user should be holding, so a bare "ICP" would name precisely the wrong thing.
  • Hold the dismissal in the component rather than delegating to MessageBox, so it survives the store re-emitting and remounting the box. It lasts for the session only and is not stored in the user's profile — the warning is about the balance in front of the user, so it returns while that balance is still there, and disappears on its own once it reaches zero.
  • CHAT is deliberately not named. It has never been routable through OISY, so no user could have acquired a wrapped CHAT balance here, and it needs no special case in the code: it is absent from the OneSec config, so it never matches.
  • Translate the new key into all 14 shipped locales. ar.json is left empty on purpose — it is not in the Languages enum, so it is unreachable at runtime.
  • Document the warning in docs/ai/PRODUCT.md.

Tests

  • npm run format, npm run lint (0 errors, 0 warnings), npm run check (6010 files, 0 errors) and npm run check:tests (7304 files, 0 errors) all pass.
  • onesec-swap.utils.spec.ts (80 tests) covers isOneSecWrappedToken per token and per side: true for the wrapped side of both a minter and a locker pair, false for either native side, false for an unknown ledger or address, false for CHAT, false on a network OneSec does not bridge even at a wrapped address, and case-insensitive on the address.
  • onesec-bridged-balances.derived.spec.ts (7 tests) covers a held wrapped ERC-20, a wrapped ICRC ledger, a zero balance, a balance that has not loaded, an EVM-native token the bridge merely supports, an ICP-native ICRC ledger, and the empty case.
  • OneSecBridgedBalanceWarning.spec.ts (5 tests) covers not rendering with nothing held, rendering with a held token, the network-qualified label, several tokens joined into one localized list, and staying dismissed once closed.
  • routes/, tokens.derived, tokens-ui.derived and the swap specs pass unchanged.
  • Two notes on pre-existing local failures, both verified to reproduce identically on a pristine checkout and unrelated to this PR: onesec-swap.services.spec.ts has 14 localStorage.clear is not a function failures, and routes/ reports 2 unhandled errors originating in the root +layout.svelte and the same localStorage clash.
  • Not visually verified. The banner only renders for a signed-in user actually holding a bridged balance, which I could not reproduce locally. It is a thin reuse of MessageBox level="warning" with the same props as the existing Activity-page warnings, and MessageBox carries its own mb-4, so no wrapper was needed for spacing — but a screenshot in the running app is still worth having before merge.

Divergence from the spec

The spec said the warning would name the affected tokens; it did not specify how. Implementation added the network qualifier described above, and the spec has been updated to record that decision and why it differs from the pattern it borrows from.


🤖 Implemented with Claude Code — Claude Opus 5 (claude-opus-5)

sbpublic and others added 6 commits September 2, 2026 10:26
Offer only the leg of each 1Sec pair that returns a token to the chain
it is native to, so the wallet no longer routes a user into a bridged
position while balances already held keep a way back out.

The direction to keep is per token, not per chain, because 1Sec wraps
both ways: `evmMode` records the chain a token is native to, so
`minter` tokens (ICP, BOB, GLDT, ckBTC) keep EVM->ICP and `locker`
tokens (USDC, USDT, cbBTC) keep ICP->EVM. Reading the classification
from the config rather than hardcoding a token list keeps a token 1Sec
adds later correct without a code change.

Enforced once, in `oneSecCompatibleDestinations`: it is the provider's
`getSupportedDestinations`, so the destination token list and the
destination network filter both narrow from the same data. The pay-side
lists are left broad on purpose — narrowing them could only hide a
disabled token from the pay list, the opposite of the goal.

Behind `ONESEC_UNWRAP_ONLY`, matching the existing provider kill-switch
convention, so both directions come back by flipping one const.
The spec records why the wrapped ERC-20 of ICP cannot simply be dropped
(it is a suggested token, so a holder may have no custom-token entry for
it), and the facts established by querying the 1Sec canister directly:
CHAT is still bridged by 1Sec but has never shipped in the
`onesec-bridge` package, so it is unreachable in OISY and excluded here.
Adds `isOneSecWrappedToken`, the holder's-eye view of the direction rule:
the ICRC ledger is the wrapped side of an EVM-native (`locker`) token and
the ERC-20 is the wrapped side of an ICP-native (`minter`) token, so the
same OneSec config drives both the swap gate and this check.

The EVM branch is bounded to the chains OneSec bridges, because a
`minter` token reuses one contract address across all of them — without
the bound, an unrelated token deployed at that address on another chain
would match.

`oneSecBridgedTokensWithBalance` pairs that with the balances store.
Scoped to enabled tokens, since that is the set balances load for, and
empty while OneSec swaps are off: with no swap available there would be
no action to point the user at. CHAT needs no special case — it is absent
from the config, so it never matches.
A dismissible warning at the top of the page names the bridged tokens
the user holds and recommends swapping them back to their native
network, so a holder is told the exit exists rather than having to open
each token to find out.

Reuses the `$token_list` / `formatList` pattern of the Activity page's
index-canister warnings, but qualifies each entry by network
("ICP (Base)"): a wrapped token shares its symbol with the native token
the user should hold, so a bare "ICP" would name the wrong thing.

Dismissal is held in the component rather than delegated to MessageBox,
so it survives the store re-emitting and remounting the box, and lasts
for the session only — the warning is about the balance in front of the
user, so it returns while that balance is still there and disappears on
its own once it reaches zero.
@sbpublic sbpublic changed the title feat(frontend): warn holders of 1Sec-bridged balances feat(frontend): inform holders of 1Sec-bridged balances Sep 2, 2026
Base automatically changed from impr/frontend/onesec-unwrap-only-swaps to main September 8, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant