Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
ddf35b1
Remove unused networkAssetIcon from token types, configs, and consumers
Sharqiewicz Feb 5, 2026
4b205e1
Add isFromStaticConfig flag for token sorting priority
Sharqiewicz Feb 5, 2026
0039c91
Fix token icon fallback chain: prefer dynamic fallbackLogoURI and avo…
Sharqiewicz Feb 5, 2026
86327a1
wip
Sharqiewicz Feb 6, 2026
a55064e
Add PoolType.Aave to Hydration config to allow aDOT->DOT conversion
Sharqiewicz Feb 6, 2026
4001ecc
update hydration sdk version
Sharqiewicz Feb 6, 2026
01fc996
fix AXLUSDC Ethereum
Sharqiewicz Feb 6, 2026
f7ec6ff
fix AXLUSDC Ethereum
Sharqiewicz Feb 6, 2026
881d651
fix filtering dynamicEvmTokens (USDC.axl case)
Sharqiewicz Feb 6, 2026
7dffad1
Move galacticcouncil sdk definition to respective package.json
ebma Feb 6, 2026
c05b51c
Initial plan
Copilot Feb 6, 2026
a383259
Fix token lookup keying: preserve enum keys and add symbol aliases
Copilot Feb 6, 2026
c1364c8
Fix formatting: remove trailing whitespace
Copilot Feb 6, 2026
43e48c5
Improve documentation: clarify token aliasing behavior
Copilot Feb 6, 2026
1b0959f
Revert change in helpers.ts
ebma Feb 6, 2026
816333b
Merge pull request #1061 from pendulum-chain/copilot/sub-pr-1058
ebma Feb 6, 2026
b7ca318
Add new migration
ebma Feb 6, 2026
f069bee
Normalize axlUSDC currency
ebma Feb 6, 2026
1560fa1
Refactor token selection sorting to prioritize enum-like keys
ebma Feb 6, 2026
716d456
Merge pull request #1058 from pendulum-chain/chore/change-token-selec…
ebma Feb 6, 2026
9d01197
Fix type issue
ebma Feb 6, 2026
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
2 changes: 1 addition & 1 deletion apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"author": "Pendulum Chain",
"dependencies": {
"@galacticcouncil/api-augment": "^0.8.1",
"@galacticcouncil/sdk": "^9.16.0",
"@galacticcouncil/sdk": "^10.6.2",
"@paraspell/sdk-pjs": "^11.8.4",
"@pendulum-chain/api-solang": "catalog:",
"@polkadot/api": "catalog:",
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/api/middlewares/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,11 @@ export const validateSubaccountCreation: RequestHandler = (req, res, next) => {
};

export const validateCreateQuoteInput: RequestHandler<unknown, unknown, CreateQuoteRequest> = (req, res, next) => {
if (req.body) {
req.body.inputCurrency = normalizeAxlUsdcCurrency(req.body.inputCurrency) as CreateQuoteRequest["inputCurrency"];
req.body.outputCurrency = normalizeAxlUsdcCurrency(req.body.outputCurrency) as CreateQuoteRequest["outputCurrency"];
}

const { rampType, from, to, inputAmount, inputCurrency, outputCurrency } = req.body;

if (!rampType || !from || !to || !inputAmount || !inputCurrency || !outputCurrency) {
Expand All @@ -397,6 +402,11 @@ export const validateCreateBestQuoteInput: RequestHandler<unknown, unknown, Omit
res,
next
) => {
if (req.body) {
req.body.inputCurrency = normalizeAxlUsdcCurrency(req.body.inputCurrency) as CreateQuoteRequest["inputCurrency"];
req.body.outputCurrency = normalizeAxlUsdcCurrency(req.body.outputCurrency) as CreateQuoteRequest["outputCurrency"];
}

const { rampType, from, to, inputAmount, inputCurrency, outputCurrency } = req.body;

if (!rampType || !inputAmount || !inputCurrency || !outputCurrency) {
Expand All @@ -421,6 +431,12 @@ export const validateCreateBestQuoteInput: RequestHandler<unknown, unknown, Omit
next();
};

const normalizeAxlUsdcCurrency = (value: unknown): unknown => {
if (typeof value !== "string") return value;

return value.toLowerCase() === "axlusdc" ? "USDC.axl" : value;
};

export const validateGetWidgetUrlInput: RequestHandler<unknown, unknown, GetWidgetUrlLocked | GetWidgetUrlRefresh> = (
req,
res,
Expand Down
4 changes: 3 additions & 1 deletion apps/api/src/api/services/hydration/swap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export class HydrationRouter {
const apiManager = ApiManager.getInstance();
this.cachedXcmFees = {};
this.sdk = apiManager.getApi("hydration").then(async ({ api }) => {
return createSdkContext(api, { router: { includeOnly: [PoolType.Omni, PoolType.Stable] } });
return createSdkContext(api, {
router: { includeOnly: [PoolType.Omni, PoolType.Stable, PoolType.Aave] }
});
});

// Refresh transaction fees every hour
Expand Down
58 changes: 58 additions & 0 deletions apps/api/src/database/migrations/022-update-subsidy-token-enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { QueryInterface } from "sequelize";

const OLD_ENUM_VALUES = ["GLMR", "PEN", "XLM", "axlUSDC", "BRLA", "EURC"];
const NEW_ENUM_VALUES = ["GLMR", "PEN", "XLM", "USDC.axl", "BRLA", "EURC", "USDC", "MATIC", "BRL"];

export async function up(queryInterface: QueryInterface): Promise<void> {
// Phase 1: Convert enum to VARCHAR to allow value updates
await queryInterface.sequelize.query(`
ALTER TABLE subsidies ALTER COLUMN token TYPE VARCHAR(32);
`);

// Phase 2: Rename axlUSDC to USDC.axl
await queryInterface.sequelize.query(`
UPDATE subsidies SET token = 'USDC.axl' WHERE token = 'axlUSDC';
`);

// Phase 3: Replace enum type with updated values
await queryInterface.sequelize.query(`
DROP TYPE IF EXISTS enum_subsidies_token;
`);

await queryInterface.sequelize.query(`
CREATE TYPE enum_subsidies_token AS ENUM (${NEW_ENUM_VALUES.map(value => `'${value}'`).join(", ")});
`);

await queryInterface.sequelize.query(`
ALTER TABLE subsidies ALTER COLUMN token TYPE enum_subsidies_token USING token::enum_subsidies_token;
`);
}

export async function down(queryInterface: QueryInterface): Promise<void> {
// Phase 1: Convert enum to VARCHAR to allow value updates
await queryInterface.sequelize.query(`
ALTER TABLE subsidies ALTER COLUMN token TYPE VARCHAR(32);
`);

// Phase 2: Map unsupported values back to axlUSDC for the old enum
await queryInterface.sequelize.query(`
UPDATE subsidies SET token = 'axlUSDC' WHERE token = 'USDC.axl';
`);

await queryInterface.sequelize.query(`
UPDATE subsidies SET token = 'axlUSDC' WHERE token IN ('USDC', 'MATIC', 'BRL');
`);

// Phase 3: Restore old enum type
await queryInterface.sequelize.query(`
DROP TYPE IF EXISTS enum_subsidies_token;
`);

await queryInterface.sequelize.query(`
CREATE TYPE enum_subsidies_token AS ENUM (${OLD_ENUM_VALUES.map(value => `'${value}'`).join(", ")});
`);

await queryInterface.sequelize.query(`
ALTER TABLE subsidies ALTER COLUMN token TYPE enum_subsidies_token USING token::enum_subsidies_token;
`);
}
7 changes: 4 additions & 3 deletions apps/frontend/src/components/CurrencyExchange/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ export const CurrencyExchange = ({
outputNetwork,
inputIcon: inputIconProp,
outputIcon: outputIconProp,
inputFallbackIcon,
outputFallbackIcon
inputFallbackIcon: inputFallbackIconProp,
outputFallbackIcon: outputFallbackIconProp
}: CurrencyExchangeProps) => {
// Use useTokenIcon for fallback icons when explicit icon props aren't provided
const inputIconFallback = useTokenIcon(inputCurrency, inputNetwork);
const outputIconFallback = useTokenIcon(outputCurrency, outputNetwork);

const inputIcon = inputIconProp ?? inputIconFallback.iconSrc;
const outputIcon = outputIconProp ?? outputIconFallback.iconSrc;
const inputFallbackIcon = inputFallbackIconProp ?? inputIconFallback.fallbackIconSrc;
const outputFallbackIcon = outputFallbackIconProp ?? outputIconFallback.fallbackIconSrc;

if (layout === "vertical") {
return (
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/Ramp/Offramp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const Offramp = () => {
() => (
<>
<AssetNumericInput
assetIcon={fromToken.networkAssetIcon}
assetIcon={fromToken.assetSymbol}
fallbackLogoURI={fromIconInfo.fallbackIconSrc}
id="inputAmount"
logoURI={fromIconInfo.iconSrc}
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/components/Ramp/Onramp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const Onramp = () => {
const ReceiveNumericInput = useMemo(
() => (
<AssetNumericInput
assetIcon={toToken.networkAssetIcon}
assetIcon={toToken.assetSymbol}
disabled={!toAmount}
fallbackLogoURI={toIconInfo.fallbackIconSrc}
id="outputAmount"
Expand All @@ -102,7 +102,7 @@ export const Onramp = () => {
tokenSymbol={toToken.assetSymbol}
/>
),
[toToken.networkAssetIcon, toToken.assetSymbol, form, quoteLoading, toAmount, openTokenSelectModal, toIconInfo]
[toToken.assetSymbol, form, quoteLoading, toAmount, openTokenSelectModal, toIconInfo]
);

const handleConfirm = useCallback(() => {
Expand Down
20 changes: 10 additions & 10 deletions apps/frontend/src/components/TokenIcon/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,14 @@ export const TokenIcon: FC<TokenIconProps> = memo(function TokenIcon({ src, fall
const [imgError, setImgError] = useState(false);
const [fallbackError, setFallbackError] = useState(false);

const getImageSrc = () => {
if (!imgError) return src;
if (fallbackSrc && !fallbackError) return fallbackSrc;
return placeholderIcon;
};

const handleError = () => {
if (!imgError) {
setImgError(true);
setIsLoading(true);
} else if (fallbackSrc && !fallbackError) {
setFallbackError(true);
setIsLoading(true);
if (!fallbackSrc) {
setIsLoading(false);
}
} else {
setFallbackError(true);
setIsLoading(false);
}
};
Expand All @@ -36,6 +30,12 @@ export const TokenIcon: FC<TokenIconProps> = memo(function TokenIcon({ src, fall
setIsLoading(false);
};

const getImageSrc = () => {
if (!imgError) return src;
if (fallbackSrc && !fallbackError) return fallbackSrc;
return placeholderIcon;
};

return (
<div className={cn("relative", className)}>
{isLoading && <div className="absolute inset-0 rounded-full bg-gray-200" />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useVirtualizer } from "@tanstack/react-virtual";
import { isNetworkEVM } from "@vortexfi/shared";
import { useMemo, useRef } from "react";
import { useNetwork } from "../../../../contexts/network";
import { cn } from "../../../../helpers/cn";
Expand All @@ -16,7 +17,20 @@ function getBalanceKey(network: string, symbol: string): string {
return `${network}-${symbol}`;
}

function sortByBalance(
/**
* Sorts the given token definitions for optimal UX in the token selection list.
*
* Sort priority:
* 1. Tokens with higher USD balance come first.
* 2. For equal USD value, tokens with higher raw balance come first.
* 3. Tokens from "static config" (see isFromStaticConfig) or non-EVM networks come before dynamic/discovered tokens.
* 4. Fallback to asset symbol alphabetical sort.
*
* @param definitions - Array of tokens to display in the modal.
* @param balances - Map keyed by 'network-symbol', containing balance and balanceUsd for each token.
* @returns Sorted array of ExtendedTokenDefinition.
*/
function sortTokens(
definitions: ExtendedTokenDefinition[],
balances: Map<string, { balance: string; balanceUsd: string }>
): ExtendedTokenDefinition[] {
Expand All @@ -30,14 +44,19 @@ function sortByBalance(
return usdB - usdA;
}

// When USD values are equal (e.g., both 0), sort by raw balance
const rawBalanceA = parseFloat(balanceA?.balance ?? "0");
const rawBalanceB = parseFloat(balanceB?.balance ?? "0");

if (rawBalanceA !== rawBalanceB) {
return rawBalanceB - rawBalanceA;
}

const isStaticA = !isNetworkEVM(a.network) || (a.details as { isFromStaticConfig?: boolean }).isFromStaticConfig;
const isStaticB = !isNetworkEVM(b.network) || (b.details as { isFromStaticConfig?: boolean }).isFromStaticConfig;
if (isStaticA !== isStaticB) {
return isStaticA ? -1 : 1;
}

return a.assetSymbol.localeCompare(b.assetSymbol);
});
}
Expand All @@ -54,7 +73,7 @@ export const SelectionTokenList = () => {
const balances = useTokenBalances();

const currentDefinitions = useMemo(
() => (isFiatDirection ? filteredDefinitions : sortByBalance(filteredDefinitions, balances)),
() => (isFiatDirection ? filteredDefinitions : sortTokens(filteredDefinitions, balances)),
[isFiatDirection, filteredDefinitions, balances]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
FiatTokenDetails,
getEnumKeyByStringValue,
getNetworkDisplayName,
isEvmTokenDetails,
isNetworkEVM,
moonbeamTokenConfig,
Networks,
Expand Down Expand Up @@ -63,7 +64,7 @@ export function useTokenDefinitions(filter: string, selectedNetworkFilter: Netwo
function getOnChainTokensDefinitionsForNetwork(selectedNetwork: Networks): ExtendedTokenDefinition[] {
if (selectedNetwork === Networks.AssetHub) {
return Object.entries(assetHubTokenConfig).map(([key, value]) => ({
assetIcon: value.networkAssetIcon,
assetIcon: value.assetSymbol,
assetSymbol: value.assetSymbol,
details: value as OnChainTokenDetails,
logoURI: value.logoURI,
Expand All @@ -74,12 +75,30 @@ function getOnChainTokensDefinitionsForNetwork(selectedNetwork: Networks): Exten
} else if (isNetworkEVM(selectedNetwork)) {
const evmConfig = getEvmTokenConfig();
const networkConfig = evmConfig[selectedNetwork as EvmNetworks] ?? {};
return Object.entries(networkConfig).map(([key, value]) => ({
assetIcon: value?.logoURI ?? value?.networkAssetIcon ?? "",
assetSymbol: value?.assetSymbol ?? key,
details: value as OnChainTokenDetails,
fallbackLogoURI: value?.fallbackLogoURI,
logoURI: value?.logoURI,
const byToken = new Map<OnChainTokenDetails, string>();

for (const [key, value] of Object.entries(networkConfig)) {
if (!value) continue;
const token = value as OnChainTokenDetails;
const existingKey = byToken.get(token);

if (!existingKey) {
byToken.set(token, key);
continue;
}

// Prefer enum-like keys without dots (e.g., "AXLUSDC" over "USDC.AXL")
if (existingKey.includes(".") && !key.includes(".")) {
byToken.set(token, key);
}
}

return Array.from(byToken.entries()).map(([details, key]) => ({
assetIcon: details.assetSymbol ?? key,
assetSymbol: details.assetSymbol ?? key,
details,
fallbackLogoURI: isEvmTokenDetails(details) ? details.fallbackLogoURI : undefined,
logoURI: details.logoURI,
network: selectedNetwork,
networkDisplayName: getNetworkDisplayName(selectedNetwork),
type: key as OnChainToken
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/components/buttons/AssetButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function AssetButton({
>
<TokenIconWithNetwork
className="mr-1 h-5 w-5"
fallbackIconSrc={fallbackLogoURI}
fallbackIconSrc={fallbackLogoURI ?? fallbackIcon.fallbackIconSrc}
iconSrc={primaryIcon}
network={network}
showNetworkOverlay={!!network}
Expand Down
11 changes: 3 additions & 8 deletions apps/frontend/src/hooks/useTokenIcon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,14 @@ export interface TokenIconInfo {

/**
* Get logo URIs from on-chain token details.
* Supports both EVM tokens (with fallback) and AssetHub tokens.
* Supports both EVM tokens (with fallbackLogoURI) and AssetHub tokens.
*/
function getTokenLogoURIs(token: OnChainTokenDetails): { logoURI?: string; fallbackLogoURI?: string } {
if (isEvmTokenDetails(token)) {
return {
fallbackLogoURI: token.fallbackLogoURI,
logoURI: token.logoURI
};
return { fallbackLogoURI: token.fallbackLogoURI, logoURI: token.logoURI };
}
if (isAssetHubTokenDetails(token)) {
return {
logoURI: token.logoURI
};
return { logoURI: token.logoURI };
}
return {};
}
Expand Down
8 changes: 2 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/shared/src/services/squidrouter/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AXL_USDC_MOONBEAM, getNetworkId, Networks } from "../../index";
import { getNetworkId, Networks } from "../../helpers/networks";
import { AXL_USDC_MOONBEAM } from "../../tokens/moonbeam/config";

export const SQUIDROUTER_FEE_OVERPAY = 0.25; // 25% overpayment
export const MOONBEAM_SQUIDROUTER_SWAP_MIN_VALUE_RAW = "10000000000000000"; // 0.01 GLMR in raw units
Expand Down
Loading