Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- added: `EdgeCurrencyConfig.addCustomTokens` batch method.
- added: `EdgeToken.isUserCreated` field.
- changed: `customTokenTemplate` on `EdgeCurrencyInfo` — an empty array now indicates token support; a filled array indicates custom token support.
- removed: `EdgeCurrencyConfig.builtinTokens`. All tokens are now served through `allTokens` (backed by `customTokens`).
- removed: `EdgeCurrencyPlugin.getBuiltinTokens`. Built-in token data is now bundled in `builtinTokens.json` and migrated into `customTokens` on login.

## 2.43.1 (2026-02-23)

- changed: Upgrade `@nymproject/mix-fetch` to improve performance with new concurrency changes.
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.0.0",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"@sucrase/webpack-loader": "^2.0.0",
"@types/chai": "^4.2.16",
Expand Down
7 changes: 5 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import babel from '@rollup/plugin-babel'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import flowEntry from 'rollup-plugin-flow-entry'
import mjs from 'rollup-plugin-mjs-entry'
Expand Down Expand Up @@ -29,7 +30,8 @@ export default [
resolve(resolveOpts),
babel(babelOpts),
flowEntry({ types: './lib/flow/exports.js' }),
mjs()
mjs(),
json()
]
},
{
Expand All @@ -40,7 +42,8 @@ export default [
resolve(resolveOpts),
babel(babelOpts),
flowEntry({ types: './lib/flow/types.js' }),
mjs()
mjs(),
json()
]
}
]
3 changes: 2 additions & 1 deletion src/core/account/account-cleaners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ const asEdgeToken = asObject<EdgeToken>({
currencyCode: asString,
denominations: asArray(asEdgeDenomination),
displayName: asString,
networkLocation: asOptional(asJsonObject)
networkLocation: asOptional(asJsonObject),
isUserCreated: asOptional(asBoolean, false)
})

const asSwapSettings = asObject<SwapSettings>({
Expand Down
7 changes: 1 addition & 6 deletions src/core/account/account-pixie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,7 @@ import {
import { makeAccountApi } from './account-api'
import { loadAllWalletStates, reloadPluginSettings } from './account-files'
import { AccountState, initialCustomTokens } from './account-reducer'
import {
loadBuiltinTokens,
loadCustomTokens,
saveCustomTokens
} from './custom-tokens'
import { loadCustomTokens, saveCustomTokens } from './custom-tokens'

export const EXPEDITED_SYNC_INTERVAL = 5000

Expand Down Expand Up @@ -91,7 +87,6 @@ const accountPixie: TamePixie<AccountProps> = combinePixies({
try {
// Wait for the currency plugins (should already be loaded by now):
await waitForPlugins(ai)
await loadBuiltinTokens(ai, accountId)
log.warn('Login: currency plugins exist')

// Start the repo:
Expand Down
52 changes: 16 additions & 36 deletions src/core/account/account-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export interface AccountState {

// Plugin stuff:
readonly allTokens: EdgePluginMap<EdgeTokenMap>
readonly builtinTokens: EdgePluginMap<EdgeTokenMap>
readonly customTokens: EdgePluginMap<EdgeTokenMap>
readonly alwaysEnabledTokenIds: EdgePluginMap<string[]>
readonly swapSettings: EdgePluginMap<SwapSettings>
Expand Down Expand Up @@ -275,41 +274,7 @@ const accountInner = buildReducer<AccountState, RootAction, AccountNext>({
),

allTokens(state = {}, action, next, prev): EdgePluginMap<EdgeTokenMap> {
const { builtinTokens, customTokens } = next.self
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

allTokens[pluginId] now undefined causing runtime crashes

High Severity

The allTokens reducer now returns next.self.customTokens directly, but customTokens only contains entries for plugins that have explicitly stored tokens. The old reducer iterated over all currency plugins and created entries for each (even empty {}), guaranteeing allTokens[pluginId] was always an object. Now allTokens[pluginId] returns undefined for any plugin without custom tokens. Multiple consumers access allTokens[pluginId] without null guards — e.g., changeEnabledTokenIds does allTokens[tokenId] on the result, and getCurrencyMultiplier/upgradeCurrencyCode call Object.keys(allTokens) — all of which throw TypeError when allTokens is undefined. This affects non-token chains like Bitcoin and any wallet whose plugin has no migrated or user-created tokens yet.

Additional Locations (1)

Fix in Cursor Fix in Web


// Roll our own `memoizeReducer` implementation,
// so we can minimize our diff as much as possible:
if (
prev.self == null ||
builtinTokens !== prev.self.builtinTokens ||
customTokens !== prev.self.customTokens
) {
const out = { ...state }
for (const pluginId of Object.keys(next.root.plugins.currency)) {
if (
prev.self == null ||
builtinTokens[pluginId] !== prev.self.builtinTokens[pluginId] ||
customTokens[pluginId] !== prev.self.customTokens[pluginId]
) {
out[pluginId] = {
...customTokens[pluginId],
...builtinTokens[pluginId]
}
}
}
return out
}
return state
},

builtinTokens(state = {}, action): EdgePluginMap<EdgeTokenMap> {
switch (action.type) {
case 'ACCOUNT_BUILTIN_TOKENS_LOADED': {
const { pluginId, tokens } = action.payload
return { ...state, [pluginId]: tokens }
}
}
return state
return next.self.customTokens
},

customTokens(
Expand All @@ -331,6 +296,21 @@ const accountInner = buildReducer<AccountState, RootAction, AccountNext>({
const newList = { ...oldList, [tokenId]: token }
return { ...state, [pluginId]: newList }
}
case 'ACCOUNT_CUSTOM_TOKENS_ADDED': {
const { pluginId, tokens } = action.payload
const oldList = state[pluginId] ?? {}

let changed = false
const merged = { ...oldList }
for (const [tokenId, token] of Object.entries(tokens)) {
if (!compare(merged[tokenId], token)) {
merged[tokenId] = token
changed = true
}
}
if (!changed) return state
return { ...state, [pluginId]: merged }
}
case 'ACCOUNT_CUSTOM_TOKEN_REMOVED': {
const { pluginId, tokenId } = action.payload
const oldList = state[pluginId] ?? {}
Expand Down
Loading
Loading