Skip to content

Commit 67c98ce

Browse files
authored
Merge pull request #3659 from superhero-com/release/v2.9.0
Release v2.9.0
2 parents b592a2e + d1838e2 commit 67c98ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2604
-782
lines changed

CHANGELOG.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,42 @@
22

33
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
44

5+
## [2.9.0](https://github.com/Superhero-com/superhero-wallet/compare/v2.8.1...v2.9.0) (2025-10-07)
6+
7+
8+
### Features
9+
10+
* **address-book:** add ability to sort by chain ([73ba0b4](https://github.com/Superhero-com/superhero-wallet/commit/73ba0b4e07a7003bdfe93366e1e021f16ee1884c))
11+
* be able to search a protocol ([817a8f1](https://github.com/Superhero-com/superhero-wallet/commit/817a8f108ae91ce10df18bc4f0f8562e61edc809))
12+
* collapse AccountDetailsBase header on transaction list scroll ([3ad4bc8](https://github.com/Superhero-com/superhero-wallet/commit/3ad4bc8c974fb7596c3a18052244d7503b9a3393))
13+
* **evm:** propogate active account to the dApp ([0bd90db](https://github.com/Superhero-com/superhero-wallet/commit/0bd90dbebf497954901bbc09ae8f376fe555aa1b))
14+
* search by input and chain in account selector ([8be540f](https://github.com/Superhero-com/superhero-wallet/commit/8be540f359cbcbe03e312928958b38be399b09a3))
15+
* show spinner in AssetSelector on tokens loading ([97f6470](https://github.com/Superhero-com/superhero-wallet/commit/97f6470b38f181b357530aa50776f022297f21be))
16+
* update Chinese translation ([66b641c](https://github.com/Superhero-com/superhero-wallet/commit/66b641c9a61881b9c1471574347467e14edbb79d))
17+
* **wallet-connect:** propagate account change ([9b819e8](https://github.com/Superhero-com/superhero-wallet/commit/9b819e80aab1652fe367ca2d578e24702243ac61))
18+
19+
20+
### Bug Fixes
21+
22+
* webApp wallet connect link redirection ([ea87fb5](https://github.com/Superhero-com/superhero-wallet/commit/ea87fb56fe1d9838f063ef9eda0cd5d79fd5eecc))
23+
24+
25+
### Maintenance
26+
27+
* enable transitions on chrome ([61ebb72](https://github.com/Superhero-com/superhero-wallet/commit/61ebb72bf7467270d9b9052dc6773a88f018a0b9))
28+
29+
30+
### Tests
31+
32+
* add several walletConnect tests ([9c7061b](https://github.com/Superhero-com/superhero-wallet/commit/9c7061be1fa2942a748f64d9d7715b7c27dd469f))
33+
* be able to login using password ([bb9d89a](https://github.com/Superhero-com/superhero-wallet/commit/bb9d89a50ba6c9feb51393f3f77b16ef821b1f90))
34+
35+
36+
### Performance
37+
38+
* do not store all aeternity tokens in localStorage ([6f2c99a](https://github.com/Superhero-com/superhero-wallet/commit/6f2c99aa1ec1d39e66e4b4ef6a51ced125c74ffb))
39+
* optimize infiniteScroll component ([abc1f9b](https://github.com/Superhero-com/superhero-wallet/commit/abc1f9ba824637961cf8c719a2020b0eb69ca6be))
40+
541
### [2.8.1](https://github.com/Superhero-com/superhero-wallet/compare/v2.8.0...v2.8.1) (2025-09-19)
642

743

android/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ android {
77
applicationId "com.superhero.cordova"
88
minSdkVersion rootProject.ext.minSdkVersion
99
targetSdkVersion rootProject.ext.targetSdkVersion
10-
versionCode 20801
11-
versionName "2.8.1"
10+
versionCode 20900
11+
versionName "2.9.0"
1212
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
1313
aaptOptions {
1414
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

ios/App/App.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@
365365
"$(inherited)",
366366
"@executable_path/Frameworks",
367367
);
368-
MARKETING_VERSION = 2.8.1;
368+
MARKETING_VERSION = 2.9.0;
369369
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
370370
PRODUCT_BUNDLE_IDENTIFIER = com.superhero.cordova;
371371
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -390,7 +390,7 @@
390390
"$(inherited)",
391391
"@executable_path/Frameworks",
392392
);
393-
MARKETING_VERSION = 2.8.1;
393+
MARKETING_VERSION = 2.9.0;
394394
PRODUCT_BUNDLE_IDENTIFIER = com.superhero.cordova;
395395
PRODUCT_NAME = "$(TARGET_NAME)";
396396
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "superhero-wallet",
3-
"version": "2.8.1",
3+
"version": "2.9.0",
44
"description": "Superhero wallet",
55
"author": "Superhero",
66
"license": "ISC",

src/background/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,18 @@ const handleMessage: Parameters<typeof browser.runtime.onMessage.addListener>[0]
5959
}
6060
});
6161
break;
62+
case 'accountsChanged':
63+
browser.tabs.query({ active: true, lastFocusedWindow: true }).then(([tab]) => {
64+
if (tab.id) {
65+
browser.tabs.sendMessage(tab.id, {
66+
superheroWalletApproved: true,
67+
method: msg.method,
68+
result: msg.params?.rpcMethodParams?.result,
69+
type: 'result',
70+
});
71+
}
72+
});
73+
break;
6274
case 'openPopup':
6375
openPopup(popupType!, aepp!, popupProps).then((popupConfig) => {
6476
sendResponse(popupConfig);

src/composables/fungibleTokens.ts

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable no-param-reassign */
22

3-
import { computed, watch } from 'vue';
3+
import { computed, ref, watch } from 'vue';
44
import { uniqBy, isEmpty } from 'lodash-es';
55
import BigNumber from 'bignumber.js';
66
import { Contract, Encoding, Tag } from '@aeternity/aepp-sdk';
@@ -46,6 +46,12 @@ const defaultTokensAvailable = useStorageRef<ProtocolRecord<AssetList>>(
4646
STORAGE_KEYS.fungibleTokenList,
4747
);
4848

49+
/**
50+
* Ephemeral (in-memory) list of available tokens per protocol.
51+
* Used to avoid persisting large lists (notably Aeternity) into localStorage.
52+
*/
53+
const ephemeralTokensAvailable = ref<ProtocolRecord<AssetList>>({});
54+
4955
/**
5056
* List of user account's token asset balances.
5157
*/
@@ -59,6 +65,11 @@ const lastAvailableTokensLoadTime = useStorageRef<number>(
5965
STORAGE_KEYS.lastAvailableTokensLoadTime,
6066
);
6167

68+
/**
69+
* Loading state for available tokens fetch per session.
70+
*/
71+
const isAvailableTokensLoading = ref(false);
72+
6273
/**
6374
* List of all fungible tokens available on user's protocols.
6475
* Includes tokens from the user's account that are not on the main list.
@@ -90,7 +101,10 @@ const tokensAvailable = computed((): ProtocolRecord<AssetList> => {
90101

91102
return Object.values(PROTOCOLS).reduce((allTokens, protocol) => {
92103
allTokens[protocol] = {
104+
// Persisted tokens
93105
...(defaultTokensAvailable.value[protocol] ?? {}),
106+
// Ephemeral tokens (e.g., Aeternity full list)
107+
...(ephemeralTokensAvailable.value[protocol] ?? {}),
94108
...customTokensAvailable[protocol],
95109
};
96110
return allTokens;
@@ -177,35 +191,48 @@ export function useFungibleTokens() {
177191
if (Date.now() - lastAvailableTokensLoadTime.value < 1000 * 60) {
178192
return;
179193
}
180-
const tokensFetchPromises = protocolsInUse.value.map(
181-
(protocol) => ProtocolAdapterFactory.getAdapter(protocol).fetchAvailableTokens?.(),
182-
);
183-
const currentNetworkName = activeNetwork.value.name;
184-
// for each promise check if it returned null, if so, use cached data
185-
// because it means that we couldn't fetch new data
186-
const tokens: IToken[] = (await Promise.all(tokensFetchPromises)).map(
187-
(protocolTokens, index) => (
188-
protocolTokens
189-
|| Object.values(defaultTokensAvailable.value[protocolsInUse.value[index]] || {})
190-
),
191-
).flat();
192-
193-
// This is necessary in case the user switches between networks faster,
194-
// than the available tokens are returned (limitations of the free Ethereum middleware)
195-
if (currentNetworkName !== activeNetwork.value.name) {
196-
return;
197-
}
198-
199-
defaultTokensAvailable.value = tokens.reduce((accumulator, token) => {
200-
const { contractId, protocol } = token;
201-
if (!accumulator[protocol]) {
202-
accumulator[protocol] = {} as AssetList;
194+
isAvailableTokensLoading.value = true;
195+
try {
196+
const tokensFetchPromises = protocolsInUse.value.map(
197+
(protocol) => ProtocolAdapterFactory.getAdapter(protocol).fetchAvailableTokens?.(),
198+
);
199+
const currentNetworkName = activeNetwork.value.name;
200+
// for each promise check if it returned null, if so, use cached data
201+
// because it means that we couldn't fetch new data
202+
const results = await Promise.all(tokensFetchPromises);
203+
const tokensByProtocol: ProtocolRecord<IToken[]> = {} as any;
204+
results.forEach((protocolTokens, index) => {
205+
const protocol = protocolsInUse.value[index];
206+
const persistedFallback = Object.values(defaultTokensAvailable.value[protocol] || {});
207+
const ephemeralFallback = Object.values(ephemeralTokensAvailable.value[protocol] || {});
208+
tokensByProtocol[protocol] = (protocolTokens as IToken[] | null)
209+
|| (protocol === PROTOCOLS.aeternity ? ephemeralFallback : persistedFallback);
210+
});
211+
212+
// This is necessary in case the user switches between networks faster,
213+
// than the available tokens are returned (limitations of the free Ethereum middleware)
214+
if (currentNetworkName !== activeNetwork.value.name) {
215+
return;
203216
}
204-
accumulator[protocol]![contractId] = token;
205-
return accumulator;
206-
}, {} as typeof defaultTokensAvailable.value);
207217

208-
lastAvailableTokensLoadTime.value = Date.now();
218+
// Store fetched tokens per protocol
219+
Object.entries(tokensByProtocol).forEach(([protocol, tokens]) => {
220+
const pr = protocol as Protocol;
221+
const target = pr === PROTOCOLS.aeternity
222+
? ephemeralTokensAvailable
223+
: defaultTokensAvailable;
224+
const currentMap = (target.value[pr] ?? {}) as AssetList;
225+
const nextMap = (tokens as IToken[]).reduce((acc, token) => {
226+
acc[token.contractId] = token;
227+
return acc;
228+
}, { ...currentMap } as AssetList);
229+
target.value[pr] = nextMap;
230+
});
231+
232+
lastAvailableTokensLoadTime.value = Date.now();
233+
} finally {
234+
isAvailableTokensLoading.value = false;
235+
}
209236
}
210237

211238
async function loadTokenBalances() {
@@ -370,6 +397,7 @@ export function useFungibleTokens() {
370397
if (newMiddlewareUrl !== oldMiddlewareUrl) {
371398
tokenBalances.value = [];
372399
defaultTokensAvailable.value = {};
400+
ephemeralTokensAvailable.value = {};
373401
lastAvailableTokensLoadTime.value = 0;
374402
await loadTokenBalances();
375403
}
@@ -380,6 +408,7 @@ export function useFungibleTokens() {
380408
accountsTotalTokenBalance,
381409
tokenBalances,
382410
tokensAvailable,
411+
isAvailableTokensLoading,
383412
createOrChangeAllowance,
384413
loadSingleToken,
385414
getAccountTokenBalance,

src/composables/viewport.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ export const useViewport = () => {
2424
}
2525

2626
function onViewportScroll(onScrollMethod: OnViewportScrollCallback) {
27-
if (viewportElement.value) {
28-
viewportElement.value.addEventListener('scroll', () => viewportScroll(onScrollMethod));
29-
}
30-
window.addEventListener('scroll', () => viewportScroll(onScrollMethod));
27+
const handler = () => viewportScroll(onScrollMethod);
28+
const target = viewportElement.value ?? window;
29+
// Prefer passive listener to avoid layout thrash
30+
target.addEventListener('scroll', handler as EventListener, { passive: true } as any);
31+
3132
onBeforeUnmount(() => {
32-
if (viewportElement.value) {
33-
viewportElement.value.removeEventListener('scroll', () => viewportScroll(onScrollMethod));
33+
target.removeEventListener('scroll', handler as EventListener);
34+
if (viewportElement.value === target) {
3435
viewportElement.value = undefined;
3536
}
36-
window.removeEventListener('scroll', () => viewportScroll(onScrollMethod));
3737
});
3838
}
3939

src/composables/walletConnect.ts

Lines changed: 49 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -352,28 +352,58 @@ export function useWalletConnect({ offscreen } = { offscreen: false }) {
352352
* Notify the DAPP about the active wallet account change.
353353
*/
354354
function monitorActiveAccountAndNetwork() {
355-
const unwatch = watch([activeAccount, activeNetwork], async ([newAccount]) => {
356-
if (!EVM_PROTOCOLS.includes(newAccount.protocol)) {
357-
return;
358-
}
359-
if (web3wallet && wcSession.value) {
355+
const unwatch = watch(
356+
[activeAccount, activeNetwork],
357+
async ([newAccount], [oldAccount]) => {
358+
if (!EVM_PROTOCOLS.includes(newAccount.protocol)) return;
359+
if (!web3wallet || !wcSession.value) { unwatch(); return; }
360+
360361
const { accounts, chains } = getFormattedAccountsAndChains();
361362

362363
wcSession.value.namespaces[ETH_CHAIN_NAMESPACE].chains = chains;
363364
wcSession.value.namespaces[ETH_CHAIN_NAMESPACE].accounts = accounts;
364365

365366
try {
366367
const activeProtocols = (networks.value[activeNetwork.value.name].protocols as any);
367-
const preferredChainId = `${ETH_CHAIN_NAMESPACE}:${activeProtocols[newAccount.protocol].chainId}`;
368-
await web3wallet.emitSessionEvent({
368+
const preferredChainIdDec = activeProtocols[newAccount.protocol].chainId.toString();
369+
const preferredChainId = `${ETH_CHAIN_NAMESPACE}:${preferredChainIdDec}`;
370+
371+
// Reorder accounts so the active address is first for every chain group
372+
// Dapps (like Uniswap) often pick the first account for the chain they're using
373+
const byChain: Record<string, string[]> = {};
374+
accounts.forEach((acc) => {
375+
const [, chainId] = acc.split(':');
376+
if (!byChain[chainId]) byChain[chainId] = [];
377+
byChain[chainId].push(acc);
378+
});
379+
const reorderedAccounts = Object.entries(byChain).flatMap(([chainId, accs]) => {
380+
const target = `${ETH_CHAIN_NAMESPACE}:${chainId}:${newAccount.address}`;
381+
const first = accs.filter((a) => a === target);
382+
const rest = accs.filter((a) => a !== target);
383+
return [...first, ...rest];
384+
});
385+
wcSession.value.namespaces[ETH_CHAIN_NAMESPACE].accounts = reorderedAccounts;
386+
387+
// Update session first so dapps relying on session_update pick the correct account
388+
const { acknowledged } = await web3wallet.updateSession({
369389
topic: wcSession.value.topic,
370-
event: {
371-
name: 'accountsChanged',
372-
data: [newAccount.address],
373-
},
374-
chainId: preferredChainId,
390+
namespaces: wcSession.value.namespaces,
375391
});
392+
await acknowledged();
376393

394+
// Emit accountsChanged only when address actually changes
395+
if (!oldAccount || oldAccount.address !== newAccount.address) {
396+
await web3wallet.emitSessionEvent({
397+
topic: wcSession.value.topic,
398+
event: {
399+
name: 'accountsChanged',
400+
data: [newAccount.address],
401+
},
402+
chainId: preferredChainId,
403+
});
404+
}
405+
406+
// Keep chain in sync for the selected protocol
377407
await web3wallet.emitSessionEvent({
378408
topic: wcSession.value.topic,
379409
event: {
@@ -382,24 +412,11 @@ export function useWalletConnect({ offscreen } = { offscreen: false }) {
382412
},
383413
chainId: preferredChainId,
384414
});
385-
386-
const { acknowledged } = await web3wallet.updateSession({
387-
topic: wcSession.value.topic,
388-
namespaces: wcSession.value.namespaces,
389-
});
390-
391-
// Even if the session update is acknowledged some DAPPS does not update the UI
392-
// to match the change. For example Uniswap does not switch the account if user did it
393-
// on the wallet side.
394-
await acknowledged();
395415
} catch (error) {
396416
disconnect();
397417
}
398-
} else {
399-
unwatch();
400-
disconnect();
401-
}
402-
});
418+
},
419+
);
403420
}
404421

405422
/**
@@ -466,7 +483,11 @@ export function useWalletConnect({ offscreen } = { offscreen: false }) {
466483
[ETH_CHAIN_NAMESPACE]: {
467484
accounts,
468485
chains,
469-
events: uniq([...proposal.requiredNamespaces[ETH_CHAIN_NAMESPACE]?.events || [], 'accountsChanged']),
486+
events: uniq([
487+
...proposal.requiredNamespaces[ETH_CHAIN_NAMESPACE]?.events || [],
488+
'accountsChanged',
489+
'chainChanged',
490+
]),
470491
methods,
471492
},
472493
},

src/constants/environment.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,3 @@ export const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY!;
7575
export const ETHPLORER_API_KEY = process.env.ETHPLORER_API_KEY!;
7676

7777
export const WALLET_CONNECT_PROJECT_ID = process.env.WALLET_CONNECT_PROJECT_ID!;
78-
79-
export const IS_TRANSITIONS_DISABLED = IS_CHROME_BASED && !IS_ANDROID && !IS_IOS;

0 commit comments

Comments
 (0)