Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4cc5309
initial port
gsteenkamp89 Sep 19, 2025
11d6044
add chains and tokens fetchers
gsteenkamp89 Sep 22, 2025
0aa37c7
update frontend swap api client
gsteenkamp89 Sep 22, 2025
185ada7
add base strategies fro swap and bridge
gsteenkamp89 Sep 26, 2025
1ccac28
get bridge and swap routes. refactor
gsteenkamp89 Sep 26, 2025
32401c3
centralize navigation links
gsteenkamp89 Sep 27, 2025
3739878
style selectorButton
gsteenkamp89 Sep 27, 2025
d66a0da
style input form
gsteenkamp89 Sep 27, 2025
62d8fb4
style balance selector
gsteenkamp89 Sep 27, 2025
23181f3
use swap quote fees
gsteenkamp89 Sep 27, 2025
22cacbf
better button states
gsteenkamp89 Sep 27, 2025
8911b7b
refactor
gsteenkamp89 Sep 28, 2025
259f6e8
show validation
gsteenkamp89 Sep 28, 2025
28e9560
refactor
gsteenkamp89 Sep 30, 2025
2d3a9f9
clean up
gsteenkamp89 Sep 30, 2025
7fd9e83
update validation warning
gsteenkamp89 Sep 30, 2025
ff834f2
update icons and validation logic
gsteenkamp89 Sep 30, 2025
0b0b0c2
add tooltip
gsteenkamp89 Sep 30, 2025
5139625
rsolve swap token info
gsteenkamp89 Sep 30, 2025
79c617d
disable unreachable tokens
gsteenkamp89 Sep 30, 2025
526ca76
filter and sort
gsteenkamp89 Sep 30, 2025
dd6cb80
fixup
gsteenkamp89 Sep 30, 2025
efd6b76
mobile token selector
gsteenkamp89 Oct 1, 2025
d0d4509
add sections
gsteenkamp89 Oct 1, 2025
bee06c8
better types
gsteenkamp89 Oct 1, 2025
020fedb
sort
gsteenkamp89 Oct 1, 2025
5cda61a
refactor
gsteenkamp89 Oct 1, 2025
945f63a
improve searchbar styles
gsteenkamp89 Oct 2, 2025
06d5f1f
restrict tabbing inside modal
gsteenkamp89 Oct 2, 2025
2bfe97b
clean up
gsteenkamp89 Oct 2, 2025
950be65
add dialog
gsteenkamp89 Oct 2, 2025
c6f7540
fixup
gsteenkamp89 Oct 3, 2025
2062525
move balance call to endpoint
gsteenkamp89 Oct 3, 2025
77d3c74
refactor
gsteenkamp89 Oct 3, 2025
fcaaf6d
fixup
gsteenkamp89 Oct 3, 2025
8ef2453
style switch button
gsteenkamp89 Oct 3, 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
13 changes: 13 additions & 0 deletions api/_providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,16 @@ export function getProviderHeaders(

return rpcHeaders?.[String(chainId)];
}

/**
* Gets the Alchemy RPC URL for a given chain ID from the rpc-providers.json configuration
* @param chainId The chain ID to get the Alchemy RPC URL for
* @returns The Alchemy RPC URL or undefined if not available
*/
export function getAlchemyRpcFromConfigJson(
chainId: number
): string | undefined {
const { providers } = rpcProvidersJson;
const alchemyUrls = providers.urls.alchemy as Record<string, string>;
return alchemyUrls?.[String(chainId)];
}
179 changes: 179 additions & 0 deletions api/user-token-balances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { VercelResponse } from "@vercel/node";
import { assert, Infer, type } from "superstruct";
import { TypedVercelRequest } from "./_types";
import { getLogger, handleErrorCondition, validAddress } from "./_utils";
import { getAlchemyRpcFromConfigJson } from "./_providers";
import { MAINNET_CHAIN_IDs } from "@across-protocol/constants";
import { BigNumber } from "ethers";

const UserTokenBalancesQueryParamsSchema = type({
account: validAddress(),
});

type UserTokenBalancesQueryParams = Infer<
typeof UserTokenBalancesQueryParamsSchema
>;

const fetchTokenBalancesForChain = async (
chainId: number,
account: string
): Promise<{
chainId: number;
balances: Array<{ address: string; balance: string }>;
}> => {
const logger = getLogger();
const rpcUrl = getAlchemyRpcFromConfigJson(chainId);

if (!rpcUrl) {
logger.warn({
at: "fetchTokenBalancesForChain",
message: "No Alchemy RPC URL found for chain, returning empty balances",
chainId,
});
return {
chainId,
balances: [],
};
}

try {
const requestBody = {
jsonrpc: "2.0",
id: 1,
method: "alchemy_getTokenBalances",
params: [account],
};

logger.debug({
at: "fetchTokenBalancesForChain",
message: "Making request to Alchemy API",
chainId,
account,
rpcUrl,
});

const response = await fetch(rpcUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});

if (!response.ok) {
logger.warn({
at: "fetchTokenBalancesForChain",
message: "HTTP error from Alchemy API, returning empty balances",
chainId,
status: response.status,
statusText: response.statusText,
});
return {
chainId,
balances: [],
};
}

const data = await response.json();

logger.debug({
at: "fetchTokenBalancesForChain",
message: "Received response from Alchemy API",
chainId,
responseData: data,
});

// Validate the response structure
if (!data || !data.result || !data.result.tokenBalances) {
logger.warn({
at: "fetchTokenBalancesForChain",
message: "Invalid response from Alchemy API, returning empty balances",
chainId,
responseData: data,
});
return {
chainId,
balances: [],
};
}

const balances = (
data.result.tokenBalances as {
contractAddress: string;
tokenBalance: string;
}[]
)
.filter((t) => !!t.tokenBalance && BigNumber.from(t.tokenBalance).gt(0))
.map((t) => ({
address: t.contractAddress,
balance: BigNumber.from(t.tokenBalance).toString(),
}));

return {
chainId,
balances,
};
} catch (error) {
logger.warn({
at: "fetchTokenBalancesForChain",
message:
"Error fetching token balances from Alchemy API, returning empty balances",
chainId,
error: error instanceof Error ? error.message : String(error),
});
return {
chainId,
balances: [],
};
}
};

const handler = async (
request: TypedVercelRequest<UserTokenBalancesQueryParams>,
response: VercelResponse
) => {
const logger = getLogger();

try {
const { query } = request;
assert(query, UserTokenBalancesQueryParamsSchema);
const { account } = query;

// Get all available chain IDs that have Alchemy RPC URLs
const chainIdsAvailable = Object.values(MAINNET_CHAIN_IDs)
.sort((a, b) => a - b)
.filter((chainId) => !!getAlchemyRpcFromConfigJson(chainId));

// Fetch balances for all chains in parallel
const balancePromises = chainIdsAvailable.map((chainId) =>
fetchTokenBalancesForChain(chainId, account)
);

const chainBalances = await Promise.all(balancePromises);

const responseData = {
account,
balances: chainBalances.map(({ chainId, balances }) => ({
chainId: chainId.toString(),
balances,
})),
};

logger.debug({
at: "UserTokenBalances",
message: "Response data",
responseJson: responseData,
});

// Cache for 3 minutes
response.setHeader(
"Cache-Control",
"s-maxage=180, stale-while-revalidate=60"
);
response.status(200).json(responseData);
} catch (error: unknown) {
return handleErrorCondition("user-token-balances", response, logger, error);
}
};

export default handler;
25 changes: 16 additions & 9 deletions src/Routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,25 @@ import {
} from "react-router-dom";
import { Header, Sidebar } from "components";
import { useConnection, useError } from "hooks";
import {
enableMigration,
stringValueInArray,
getConfig,
chainEndpointToId,
} from "utils";
import { stringValueInArray, getConfig, chainEndpointToId } from "utils";
import lazyWithRetry from "utils/lazy-with-retry";

import { enableMigration } from "utils";
import Toast from "components/Toast";
import BouncingDotsLoader from "components/BouncingDotsLoader";
import NotFound from "./views/NotFound";
import ScrollToTop from "components/ScrollToTop";
import { AmpliTrace } from "components/AmpliTrace";
import Banners from "components/Banners";

export const NAVIGATION_LINKS = !enableMigration
? [
{ href: "/bridge-and-swap", name: "Bridge & Swap" },
{ href: "/pool", name: "Pool" },
{ href: "/rewards", name: "Rewards" },
{ href: "/transactions", name: "Transactions" },
]
: [];

const LiquidityPool = lazyWithRetry(
() => import(/* webpackChunkName: "LiquidityPools" */ "./views/LiquidityPool")
);
Expand Down Expand Up @@ -50,6 +54,9 @@ const Transactions = lazyWithRetry(
const Staking = lazyWithRetry(
() => import(/* webpackChunkName: "RewardStaking" */ "./views/Staking")
);
const SwapAndBridge = lazyWithRetry(
() => import(/* webpackChunkName: "RewardStaking" */ "./views/SwapAndBridge")
);
const DepositStatus = lazyWithRetry(() => import("./views/DepositStatus"));

function useRoutes() {
Expand Down Expand Up @@ -137,13 +144,13 @@ const Routes: React.FC = () => {
}
}}
/>
<Route exact path="/bridge" component={Send} />
<Route exact path="/bridge-and-swap" component={SwapAndBridge} />
<Route path="/bridge/:depositTxHash" component={DepositStatus} />
<Redirect
exact
path="/"
to={{
pathname: "/bridge",
pathname: "/bridge-and-swap",
search: location.search,
}}
/>
Expand Down
Binary file added src/assets/chain-logos/all-swap-chain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/assets/icons/arrows-cross.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/dollar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/gas.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icons/loading-2.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icons/product.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/research.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/route.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/assets/icons/search.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/shield.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/assets/icons/siren.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/assets/icons/time.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions src/assets/icons/wallet.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/icons/warning_triangle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/assets/mask/token-mask-corner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading