Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5fd3542
feat: toggle plasma on
gomesalexandre Dec 19, 2025
40dcfdf
Merge branch 'develop' into feat_plasma_on
gomesalexandre Dec 22, 2025
ce58760
Merge remote-tracking branch 'origin/develop' into feat_plasma_on
gomesalexandre Dec 22, 2025
fe28897
Merge remote-tracking branch 'origin/develop' into feat_usdt0_happy_a…
gomesalexandre Dec 22, 2025
31fa779
fix: prevent USDT/USDT0 cross-contamination in related asset grouping
gomesalexandre Dec 22, 2025
790a506
feat: progress
gomesalexandre Dec 22, 2025
76574c4
feat: reregen
gomesalexandre Dec 22, 2025
bc5b2e5
feat: implement bridged asset canonical detection and UI improvements
gomesalexandre Dec 23, 2025
0ad6b67
feat: implement bridged asset canonical detection
gomesalexandre Dec 23, 2025
4fa03b4
feat: don't include related/asset data in bundle
gomesalexandre Dec 23, 2025
838af7a
feat: don't include related/asset data in bundle
gomesalexandre Dec 23, 2025
afdaa17
fix: move AssetLoader inside PersistGate to prevent assets being cleared
gomesalexandre Dec 23, 2025
fd058e4
debug: add comprehensive logging throughout asset loading flow
gomesalexandre Dec 23, 2025
f5086af
debug: add logging to useAssetSearchWorker hook
gomesalexandre Dec 23, 2025
0e3eef4
debug: add logging to GlobalSearchModal and selectAssetsBySearchQuery
gomesalexandre Dec 23, 2025
d912f0d
debug: add detailed logging to selectPrimaryAssets
gomesalexandre Dec 23, 2025
008c477
debug: compact logging in selectPrimaryAssets (counts not individual …
gomesalexandre Dec 23, 2025
cff2a3b
fix: compute isPrimary and isChainSpecific when loading assets
gomesalexandre Dec 23, 2025
23ec445
chore: remove debug logging
gomesalexandre Dec 23, 2025
ca9450f
refactor: rename files to generatedAssetData.json and relatedAssetInd…
gomesalexandre Dec 23, 2025
c4b60d6
feat: add cache busting with content hashes
gomesalexandre Dec 23, 2025
ae6825b
perf: remove redundant chain properties from assets (4MB savings)
gomesalexandre Dec 23, 2025
a22ad12
refactor: replace 'as any' with proper getters, remove verbose comments
gomesalexandre Dec 23, 2025
ac2ef82
chore: simplify comment in AssetService
gomesalexandre Dec 23, 2025
46b798d
refactor: consolidate test mocks, cleanup portals utils, remove migra…
gomesalexandre Dec 24, 2025
ab4fab0
Merge remote-tracking branch 'origin/develop' into feat_bridged_grouping
gomesalexandre Dec 24, 2025
6e3d5bb
Merge remote-tracking branch 'origin/feat_bridged_grouping' into feat…
gomesalexandre Dec 26, 2025
2f53431
Merge remote-tracking branch 'remotes/origin/develop' into feat_poc_r…
gomesalexandre Dec 26, 2025
e0aa4b1
[skip ci] refactor: remove AssetLoader antipattern, use useQuery for …
gomesalexandre Dec 26, 2025
4361e88
Merge remote-tracking branch 'origin/develop' into feat_poc_regen_public
gomesalexandre Dec 27, 2025
21c46da
[skip ci] chore: minimize diff by using localAssetData alias
gomesalexandre Dec 28, 2025
e69ffc1
[skip ci] feat: add error handling for asset service initialization
gomesalexandre Dec 28, 2025
34c6c8c
fix: address coderabbit review - use axios for asset fetches, fix tes…
gomesalexandre Dec 29, 2025
4fcbf4f
Merge remote-tracking branch 'origin/develop' into feat_poc_regen_public
gomesalexandre Dec 29, 2025
56f7f0a
fix: address remaining coderabbit review comments
gomesalexandre Dec 29, 2025
f1b5d59
Merge remote-tracking branch 'origin/develop' into feat_poc_regen_public
gomesalexandre Dec 29, 2025
2e3a23b
fix: preserve buy/sell assets on trade input clear()
gomesalexandre Dec 29, 2025
7917962
fix: axios mock preserves create method in test setup
gomesalexandre Dec 29, 2025
acfd798
fix: add missing validateAddress mock to Base and Optimism tests
gomesalexandre Dec 29, 2025
73126f1
fix: regen
NeOMakinG Dec 30, 2025
d248020
Merge branch 'feat_poc_regen_public' of github.com:shapeshift/web int…
NeOMakinG Dec 30, 2025
2820299
Merge remote-tracking branch 'origin/develop' into feat_poc_regen_public
NeOMakinG Dec 30, 2025
149b4a7
fix: mock asset service in thorchainsavers test
gomesalexandre Dec 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { toAddressNList } from '../../utils'
import type { ChainAdapterArgs } from '../EvmBaseAdapter'
import * as base from './BaseChainAdapter'

vi.mock('../../utils/validateAddress', () => ({
assertAddressNotSanctioned: vi.fn(),
}))

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const EOA_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import { toAddressNList } from '../../utils'
import type { ChainAdapterArgs } from '../EvmBaseAdapter'
import * as optimism from './OptimismChainAdapter'

vi.mock('../../utils/validateAddress', () => ({
assertAddressNotSanctioned: vi.fn(),
}))

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'
const EOA_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'

Expand Down
4 changes: 4 additions & 0 deletions public/generated/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"assetData": "0d99b638",
"relatedAssetIndex": "94703324"
}
374,391 changes: 374,391 additions & 0 deletions public/generated/generatedAssetData.json

Large diffs are not rendered by default.

11,388 changes: 11,388 additions & 0 deletions public/generated/relatedAssetIndex.json

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions scripts/generateAssetData/constants.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import path from 'path'

export const ASSET_DATA_PATH = path.join(
__dirname,
'../../src/lib/asset-service/service/encodedAssetData.json',
)
export const GENERATED_DIR = path.join(__dirname, '../../public/generated')

export const RELATED_ASSET_INDEX_PATH = path.join(
__dirname,
'../../src/lib/asset-service/service/encodedRelatedAssetIndex.json',
)
export const ASSET_DATA_PATH = path.join(GENERATED_DIR, 'generatedAssetData.json')

export const RELATED_ASSET_INDEX_PATH = path.join(GENERATED_DIR, 'relatedAssetIndex.json')
101 changes: 56 additions & 45 deletions scripts/generateAssetData/generateAssetData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import {
atom,
bitcoin,
bitcoincash,
decodeAssetData,
decodeRelatedAssetIndex,
dogecoin,
encodeAssetData,
encodeRelatedAssetIndex,
litecoin,
maya,
mayachain,
Expand All @@ -21,16 +17,18 @@ import {
unfreeze,
zcash,
} from '@shapeshiftoss/utils'
import crypto from 'crypto'
import fs from 'fs'
import merge from 'lodash/merge'
import orderBy from 'lodash/orderBy'
import path from 'path'

import * as arbitrum from './arbitrum'
import * as arbitrumNova from './arbitrumNova'
import * as avalanche from './avalanche'
import * as base from './base'
import * as bnbsmartchain from './bnbsmartchain'
import { ASSET_DATA_PATH, RELATED_ASSET_INDEX_PATH } from './constants'
import { ASSET_DATA_PATH, GENERATED_DIR, RELATED_ASSET_INDEX_PATH } from './constants'
import * as ethereum from './ethereum'
import { generateRelatedAssetIndex } from './generateRelatedAssetIndex/generateRelatedAssetIndex'
import * as gnosis from './gnosis'
Expand All @@ -45,10 +43,18 @@ import * as sui from './sui'
import * as tronModule from './tron'
import { filterOutBlacklistedAssets, getSortedAssetIds } from './utils'

import { getAssetService } from '@/lib/asset-service'

// To regenerate all relatedAssetKey values, run: REGEN_ALL=true yarn generate:asset-data
const REGEN_ALL = process.env.REGEN_ALL === 'true'

const generateAssetData = async () => {
// Ensure the generated directory exists
await fs.promises.mkdir(GENERATED_DIR, { recursive: true })

// Initialize AssetService with existing data (needed by portals and other asset generators)
await getAssetService()

const ethAssets = await ethereum.getAssets()
const avalancheAssets = await avalanche.getAssets()
const optimismAssets = await optimism.getAssets()
Expand Down Expand Up @@ -101,8 +107,15 @@ const generateAssetData = async () => {
// deterministic order so diffs are readable
const orderedAssetList = orderBy(filteredAssetData, 'assetId')

const encodedAssetData = JSON.parse(await fs.promises.readFile(ASSET_DATA_PATH, 'utf8'))
const { assetData: currentGeneratedAssetData } = decodeAssetData(encodedAssetData)
let currentGeneratedAssetData: AssetsById = {}
if (!REGEN_ALL) {
try {
const existingAssetDataJson = JSON.parse(await fs.promises.readFile(ASSET_DATA_PATH, 'utf8'))
currentGeneratedAssetData = existingAssetDataJson.byId || {}
} catch (err) {
console.warn('No existing asset data found, doing full regeneration')
}
}

const generatedAssetData = orderedAssetList.reduce<AssetsById>((acc, asset) => {
const currentGeneratedAssetId = currentGeneratedAssetData[asset.assetId]
Expand Down Expand Up @@ -148,44 +161,47 @@ const generateAssetData = async () => {

const sortedAssetIds = await getSortedAssetIds(assetsWithOverridesApplied)

// Encode the assets for minimal size while preserving ordering
const reEncodedAssetData = encodeAssetData(sortedAssetIds, assetsWithOverridesApplied)
await fs.promises.writeFile(ASSET_DATA_PATH, JSON.stringify(reEncodedAssetData))
const outputData = { byId: assetsWithOverridesApplied, ids: sortedAssetIds }
await fs.promises.writeFile(ASSET_DATA_PATH, JSON.stringify(outputData, null, 2))

return { sortedAssetIds, assetData: assetsWithOverridesApplied }
}

const readRelatedAssetIndex = () => {
const encodedAssetData = JSON.parse(fs.readFileSync(ASSET_DATA_PATH, 'utf8'))
const encodedRelatedAssetIndex = JSON.parse(fs.readFileSync(RELATED_ASSET_INDEX_PATH, 'utf8'))
const relatedAssetIndexJson = JSON.parse(fs.readFileSync(RELATED_ASSET_INDEX_PATH, 'utf8'))
return relatedAssetIndexJson
}

const { sortedAssetIds: originalSortedAssetIds } = decodeAssetData(encodedAssetData)
const relatedAssetIndex = decodeRelatedAssetIndex(
encodedRelatedAssetIndex,
originalSortedAssetIds,
const writeRelatedAssetIndex = (relatedAssetIndex: Record<AssetId, AssetId[]>) => {
const filteredOutputData = Object.fromEntries(
Object.entries(relatedAssetIndex).filter(([_, value]) => value !== undefined),
)

return relatedAssetIndex
fs.writeFileSync(RELATED_ASSET_INDEX_PATH, JSON.stringify(filteredOutputData, null, 2))
}

const reEncodeAndWriteRelatedAssetIndex = (
originalRelatedAssetIndex: Record<AssetId, AssetId[]>,
updatedSortedAssetIds: AssetId[],
) => {
const updatedEncodedRelatedAssetIndex = encodeRelatedAssetIndex(
originalRelatedAssetIndex,
updatedSortedAssetIds,
)
const generateManifest = async () => {
const assetDataHash = crypto
.createHash('sha256')
.update(await fs.promises.readFile(ASSET_DATA_PATH, 'utf8'))
.digest('hex')
.slice(0, 8)

const relatedAssetIndexHash = crypto
.createHash('sha256')
.update(await fs.promises.readFile(RELATED_ASSET_INDEX_PATH, 'utf8'))
.digest('hex')
.slice(0, 8)

const manifest = {
assetData: assetDataHash,
relatedAssetIndex: relatedAssetIndexHash,
}

// Remove any undefined values from the updated encoded related asset index
const filteredUpdatedEncodedRelatedAssetIndex = Object.fromEntries(
Object.entries(updatedEncodedRelatedAssetIndex).filter(([_, value]) => value !== undefined),
)
const manifestPath = path.join(GENERATED_DIR, 'asset-manifest.json')
await fs.promises.writeFile(manifestPath, JSON.stringify(manifest, null, 2))

fs.writeFileSync(
RELATED_ASSET_INDEX_PATH,
JSON.stringify(filteredUpdatedEncodedRelatedAssetIndex),
)
console.info('Generated asset-manifest.json with content hashes')
}

const main = async () => {
Expand All @@ -194,22 +210,17 @@ const main = async () => {
const originalRelatedAssetIndex = readRelatedAssetIndex()

// Generate the new assetData and sortedAssetIds
const { sortedAssetIds: updatedSortedAssetIds } = await generateAssetData()

// We need to update the relatedAssetIndex to match the new asset ordering:
// - The original relatedAssetIndex references assets by their index in the original
// sortedAssetIds array
// - After regenerating assetData, the positions in the sortedAssetIds may have changed, which
// means a given index in the relatedAssetIndex will point to a different asset in the new
// sortedAssetIds
// - To prevent corruption, we rewrite the relatedAssetIndex using the new positions, resulting
// in a new relatedAssetIndex that references assets by their index in the updated
// sortedAssetIds
reEncodeAndWriteRelatedAssetIndex(originalRelatedAssetIndex, updatedSortedAssetIds)
await generateAssetData()

// Write the related asset index
writeRelatedAssetIndex(originalRelatedAssetIndex)

// Generate the new related asset index
await generateRelatedAssetIndex()

// Generate manifest with content hashes for cache busting
await generateManifest()

console.info('Assets and related assets data generated.')

process.exit(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,7 @@ import {
optimismAssetId,
} from '@shapeshiftoss/caip'
import type { Asset } from '@shapeshiftoss/types'
import {
createThrottle,
decodeAssetData,
decodeRelatedAssetIndex,
encodeAssetData,
encodeRelatedAssetIndex,
isToken,
} from '@shapeshiftoss/utils'
import { createThrottle, isToken } from '@shapeshiftoss/utils'
import axios from 'axios'
import axiosRetry from 'axios-retry'
import fs from 'fs'
Expand Down Expand Up @@ -424,15 +417,22 @@ const processRelatedAssetIds = async (
export const generateRelatedAssetIndex = async () => {
console.log('generateRelatedAssetIndex() starting')

const encodedAssetData = JSON.parse(await fs.promises.readFile(ASSET_DATA_PATH, 'utf8'))
const encodedRelatedAssetIndex = JSON.parse(
const assetDataJson = JSON.parse(await fs.promises.readFile(ASSET_DATA_PATH, 'utf8'))
const relatedAssetIndexJson = JSON.parse(
await fs.promises.readFile(RELATED_ASSET_INDEX_PATH, 'utf8'),
)

const { assetData: generatedAssetData, sortedAssetIds } = decodeAssetData(encodedAssetData)
const relatedAssetIndex = REGEN_ALL
? {}
: decodeRelatedAssetIndex(encodedRelatedAssetIndex, sortedAssetIds)
if (!assetDataJson.byId || !assetDataJson.ids) {
throw new Error(
`Invalid asset data structure: expected { byId, ids } but got ${JSON.stringify(
Object.keys(assetDataJson),
)}`,
)
}

const generatedAssetData: Record<AssetId, Asset> = assetDataJson.byId
const sortedAssetIds: AssetId[] = assetDataJson.ids
const relatedAssetIndex: Record<AssetId, AssetId[]> = REGEN_ALL ? {} : relatedAssetIndexJson

// Remove stale related asset data from the assetData where the primary related asset no longer exists
Object.values(generatedAssetData).forEach(asset => {
Expand Down Expand Up @@ -485,11 +485,11 @@ export const generateRelatedAssetIndex = async () => {

clearThrottleInterval()

const reEncodedRelatedAssetIndex = encodeRelatedAssetIndex(relatedAssetIndex, sortedAssetIds)
const reEncodedAssetData = encodeAssetData(sortedAssetIds, generatedAssetData)

await fs.promises.writeFile(ASSET_DATA_PATH, JSON.stringify(reEncodedAssetData))
await fs.promises.writeFile(RELATED_ASSET_INDEX_PATH, JSON.stringify(reEncodedRelatedAssetIndex))
await fs.promises.writeFile(
ASSET_DATA_PATH,
JSON.stringify({ byId: generatedAssetData, ids: sortedAssetIds }, null, 2),
)
await fs.promises.writeFile(RELATED_ASSET_INDEX_PATH, JSON.stringify(relatedAssetIndex, null, 2))

console.info(`generateRelatedAssetIndex() done. Successes: ${happyCount}, Failures: ${sadCount}`)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AssetChainRow } from './AssetChainRow'

import { getStyledMenuButtonProps } from '@/components/AssetSelection/helpers'
import { useModalChildZIndex } from '@/context/ModalStackProvider'
import { usePlugins } from '@/context/PluginProvider/PluginProvider'
import { useWallet } from '@/hooks/useWallet/useWallet'
import { assertGetChainAdapter } from '@/lib/utils'
import { portfolio } from '@/state/slices/portfolioSlice/portfolioSlice'
Expand Down Expand Up @@ -64,6 +65,7 @@ export const AssetChainDropdown: React.FC<AssetChainDropdownProps> = memo(
const {
state: { wallet },
} = useWallet()
const { supportedChains } = usePlugins()
const translate = useTranslate()
const modalChildZIndex = useModalChildZIndex()
const chainDisplayName = useAppSelector(state =>
Expand Down Expand Up @@ -190,7 +192,10 @@ export const AssetChainDropdown: React.FC<AssetChainDropdownProps> = memo(
return translate('trade.tooltip.noRelatedAssets', { chainDisplayName })
}, [chainDisplayName, translate])

if (!assetId || isLoading) return <AssetRowLoading {...buttonProps} />
const assetChainId = assetId ? fromAssetId(assetId).chainId : undefined
if (!assetId || isLoading || (assetChainId && !supportedChains.includes(assetChainId))) {
return <AssetRowLoading {...buttonProps} />
}

return (
<Menu isLazy>
Expand Down
58 changes: 57 additions & 1 deletion src/context/AppProvider/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { usePrevious, useToast } from '@chakra-ui/react'
import type { AssetId } from '@shapeshiftoss/caip'
import { btcAssetId, ethAssetId, foxAssetId, usdcAssetId } from '@shapeshiftoss/caip'
import type { LedgerOpenAppEventArgs } from '@shapeshiftoss/chain-adapters'
import { emitter } from '@shapeshiftoss/chain-adapters'
import { useQueries } from '@tanstack/react-query'
import { useQueries, useQuery } from '@tanstack/react-query'
import difference from 'lodash/difference'
import React, { useEffect, useMemo } from 'react'
import { useTranslate } from 'react-polyglot'
Expand All @@ -24,7 +25,10 @@ import { useTransactionsSubscriber } from '@/hooks/useTransactionsSubscriber'
import { useUser } from '@/hooks/useUser/useUser'
import { useWallet } from '@/hooks/useWallet/useWallet'
import { walletSupportsChain } from '@/hooks/useWalletSupportsChain/useWalletSupportsChain'
import { getAssetService, initAssetService } from '@/lib/asset-service'
import { useGetFiatRampsQuery } from '@/state/apis/fiatRamps/fiatRamps'
import { assets } from '@/state/slices/assetsSlice/assetsSlice'
import { limitOrderInput } from '@/state/slices/limitOrderInputSlice/limitOrderInputSlice'
import {
marketApi,
useFindAllMarketDataQuery,
Expand All @@ -39,6 +43,7 @@ import {
selectWalletId,
} from '@/state/slices/selectors'
import { tradeInput } from '@/state/slices/tradeInputSlice/tradeInputSlice'
import { tradeRampInput } from '@/state/slices/tradeRampInputSlice/tradeRampInputSlice'
import { useAppDispatch, useAppSelector } from '@/state/store'

const MARKET_DATA_POLLING_INTERVAL_MS = 60 * 1000 // refetch market-data every minute
Expand Down Expand Up @@ -78,6 +83,57 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
// Initialize user system
useUser()

// Initialize asset service and populate Redux with assets
const { isError: isAssetServiceError } = useQuery({
queryKey: ['assetService'],
queryFn: async () => {
await initAssetService()
const service = getAssetService()

dispatch(
assets.actions.upsertAssets({
byId: service.assetsById,
ids: service.assetIds,
}),
)
dispatch(assets.actions.setRelatedAssetIndex(service.relatedAssetIndex))

const btcAsset = service.assetsById[btcAssetId]
const ethAsset = service.assetsById[ethAssetId]
if (btcAsset && ethAsset) {
dispatch(tradeInput.actions.setBuyAsset(btcAsset))
dispatch(tradeInput.actions.setSellAsset(ethAsset))
dispatch(tradeRampInput.actions.setBuyAsset(btcAsset))
dispatch(tradeRampInput.actions.setSellAsset(ethAsset))
}

const foxAsset = service.assetsById[foxAssetId]
const usdcAsset = service.assetsById[usdcAssetId]
if (foxAsset && usdcAsset) {
dispatch(limitOrderInput.actions.setBuyAsset(foxAsset))
dispatch(limitOrderInput.actions.setSellAsset(usdcAsset))
}

return null
},
staleTime: Infinity,
gcTime: Infinity,
})

// Show error toast if asset loading fails
useEffect(() => {
if (isAssetServiceError) {
toast({
position: 'top-right',
title: translate('common.somethingWentWrong'),
description: translate('common.somethingWentWrongBody'),
status: 'error',
duration: null,
isClosable: true,
})
}
}, [isAssetServiceError, toast, translate])

useEffect(() => {
const handleLedgerOpenApp = ({ chainId, reject }: LedgerOpenAppEventArgs) => {
const onCancel = () => {
Expand Down
Loading