404
diff --git a/packages/arb-token-bridge-ui/src/pages/restricted.tsx b/packages/app/src/app/restricted/page.tsx
similarity index 100%
rename from packages/arb-token-bridge-ui/src/pages/restricted.tsx
rename to packages/app/src/app/restricted/page.tsx
diff --git a/packages/app/src/initialization.ts b/packages/app/src/initialization.ts
new file mode 100644
index 0000000000..be76639996
--- /dev/null
+++ b/packages/app/src/initialization.ts
@@ -0,0 +1,46 @@
+import dayjs from 'dayjs'
+import relativeTime from 'dayjs/plugin/relativeTime'
+import advancedFormat from 'dayjs/plugin/advancedFormat'
+import timeZone from 'dayjs/plugin/timezone'
+import utc from 'dayjs/plugin/utc'
+import { registerCustomArbitrumNetwork } from '@arbitrum/sdk'
+import { getOrbitChains } from '@/bridge/util/orbitChainsList'
+import {
+ mapCustomChainToNetworkData,
+ getCustomChainsFromLocalStorage
+} from '@/bridge/util/networks'
+
+let arbitrumSdkInitialized = false
+
+/**
+ * This file include initialization that need to be performed on client or on server
+ *
+ * Server side initialization is done in src/app/(bridge)/page.tsx
+ * Client side initialization is done in BridgeClient.tsx
+ */
+
+export function initializeDayjs() {
+ dayjs.extend(utc)
+ dayjs.extend(relativeTime)
+ dayjs.extend(timeZone)
+ dayjs.extend(advancedFormat)
+}
+
+export function addOrbitChainsToArbitrumSDK() {
+ if (arbitrumSdkInitialized) {
+ return
+ }
+
+ ;[...getOrbitChains(), ...getCustomChainsFromLocalStorage()].forEach(
+ chain => {
+ try {
+ registerCustomArbitrumNetwork(chain)
+ mapCustomChainToNetworkData(chain)
+ } catch (_) {
+ // already added
+ }
+ }
+ )
+
+ arbitrumSdkInitialized = true
+}
diff --git a/packages/app/tailwind.config.js b/packages/app/tailwind.config.js
new file mode 100644
index 0000000000..218344f94d
--- /dev/null
+++ b/packages/app/tailwind.config.js
@@ -0,0 +1,13 @@
+/** @type {import('tailwindcss').Config} */
+// eslint-disable-next-line @typescript-eslint/no-require-imports
+const rootConfig = require('../../tailwind.config.js')
+
+module.exports = {
+ ...rootConfig,
+ content: [
+ './src/app/**/*.{js,ts,jsx,tsx}',
+ './pages/**/*.{js,ts,jsx,tsx}',
+ '../arb-token-bridge-ui/src/**/*.{js,ts,jsx,tsx}',
+ '../../node_modules/@offchainlabs/cobalt/**/*.{js,ts,jsx,tsx}'
+ ]
+}
diff --git a/packages/app/tsconfig.json b/packages/app/tsconfig.json
new file mode 100644
index 0000000000..b8da93c388
--- /dev/null
+++ b/packages/app/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "noEmit": true,
+ "incremental": true,
+ "jsx": "preserve"
+ },
+ "include": [
+ "next-env.d.ts",
+ "src/app/**/*.ts",
+ "src/app/**/*.tsx",
+ ".next/types/**/*.ts",
+ "*.js",
+ "../arb-token-bridge-ui/additional.d.ts",
+ "build/types/**/*.ts"
+ ],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/arb-token-bridge-ui/package.json b/packages/arb-token-bridge-ui/package.json
index fd85a98cad..fef837db9a 100644
--- a/packages/arb-token-bridge-ui/package.json
+++ b/packages/arb-token-bridge-ui/package.json
@@ -31,15 +31,12 @@
"exponential-backoff": "^3.1.2",
"graphql": "^16.10.0",
"lodash-es": "^4.17.21",
- "next": "^14.2.32",
"next-query-params": "^5.1.0",
"overmind": "^28.0.1",
"overmind-react": "^29.0.1",
"p-limit": "^6.2.0",
"posthog-js": "^1.236.5",
"query-string": "^9.1.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
"react-loader-spinner": "^5.4.5",
"react-syntax-highlighter": "^15.6.1",
"react-toastify": "^9.1.1",
@@ -55,6 +52,11 @@
"zod": "^3.24.3",
"zustand": "^4.3.9"
},
+ "peerDependencies": {
+ "next": "^14.2.32",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
"scripts": {
"predev": "yarn generateDenylist",
"dev": "next dev",
@@ -64,9 +66,9 @@
"start": "next start",
"test": "vitest --config vitest.config.ts --watch",
"test:ci": "vitest --config vitest.config.ts --run",
- "lint": "tsc && eslint",
+ "lint": "eslint",
"lint:fix": "tsc && eslint --quiet --fix",
- "prettier:format": "prettier --config-precedence file-override --write \"src/**/*.{tsx,ts,scss,md,json}\"",
+ "prettier:format": "prettier --write \"src/**/*.{tsx,ts,scss,md,json}\"",
"generateDenylist": "ts-node --project ./scripts/tsconfig.json ./scripts/generateDenylist.ts",
"generateOpenGraphImages": "ts-node --project ./scripts/tsconfig.json ./src/generateOpenGraphImages.tsx generate",
"generateOpenGraphImages:core": "yarn generateOpenGraphImages --core",
@@ -90,45 +92,24 @@
},
"devDependencies": {
"@babel/preset-env": "^7.26.9",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "^9.26.0",
- "@next/eslint-plugin-next": "^15.3.2",
"@synthetixio/synpress": "3.7.3",
"@testing-library/react": "^16.3.0",
"@types/lodash-es": "^4.17.12",
- "@types/node": "^16.6.1",
- "@types/react": "^18.2.6",
- "@types/react-dom": "^18.2.4",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/react-virtualized": "^9.22.2",
"@types/wcag-contrast": "^3.0.3",
"@types/yargs": "^17.0.33",
- "@typescript-eslint/eslint-plugin": "^8.32.1",
- "@typescript-eslint/parser": "^8.32.1",
"autoprefixer": "^10.4.13",
"cypress-terminal-report": "^7.1.0",
"env-cmd": "^10.1.0",
- "eslint": "^9.26.0",
- "eslint-config-prettier": "^10.1.5",
- "eslint-import-resolver-alias": "^1.1.2",
- "eslint-plugin-import": "^2.31.0",
- "eslint-plugin-jsx-a11y": "^6.10.2",
- "eslint-plugin-prettier": "^5.4.0",
- "eslint-plugin-react": "^7.37.5",
- "eslint-plugin-react-hooks": "^4.6.0",
- "eslint-plugin-zustand-rules": "https://github.com/OffchainLabs/eslint-plugin-zustand-rules",
"happy-dom": "^17.4.4",
"patch-package": "^8.0.0",
"postcss": "^8.5.3",
"postinstall-postinstall": "^2.1.0",
- "prettier": "^2.7.1",
- "prettier-plugin-tailwindcss": "^0.1.11",
"satori": "^0.12.2",
"start-server-and-test": "^2.0.11",
"tailwindcss": "^3.4.16",
"ts-node": "^10.9.2",
- "typescript": "^5.2.2",
- "typescript-eslint": "^8.32.1",
"vitest": "^3.1.1",
"yargs": "^18.0.0"
}
diff --git a/packages/arb-token-bridge-ui/scripts/generateDenylist.ts b/packages/arb-token-bridge-ui/scripts/generateDenylist.ts
index 22ae50fcdb..1a8dfd10b7 100644
--- a/packages/arb-token-bridge-ui/scripts/generateDenylist.ts
+++ b/packages/arb-token-bridge-ui/scripts/generateDenylist.ts
@@ -1,4 +1,5 @@
import fs from 'fs'
+import path from 'path'
import axios from 'axios'
import { TokenList } from '@uniswap/token-lists'
import { getArbitrumNetworks } from '@arbitrum/sdk'
@@ -153,7 +154,10 @@ async function main() {
2
) + '\n'
- fs.writeFileSync('./public/__auto-generated-denylist.json', resultJson)
+ fs.writeFileSync(
+ path.join(__dirname, '../../app/public/__auto-generated-denylist.json'),
+ resultJson
+ )
}
main()
diff --git a/packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts b/packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts
similarity index 78%
rename from packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts
rename to packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts
index 4ff9a8591f..6eed61e70f 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/cctp/[type].ts
+++ b/packages/arb-token-bridge-ui/src/app/api/cctp/[type].ts
@@ -1,5 +1,5 @@
import { gql } from '@apollo/client'
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { ChainId } from '../../../types/ChainId'
import { Address } from '../../../util/AddressUtils'
@@ -9,16 +9,6 @@ import {
getSourceFromSubgraphClient
} from '../../../api-utils/ServerSubgraphUtils'
-// Extending the standard NextJs request with CCTP params
-export type NextApiRequestWithCCTPParams = NextApiRequest & {
- query: {
- walletAddress: Address
- l1ChainId: string
- pageNumber?: string
- pageSize?: string
- }
-}
-
export enum ChainDomain {
Ethereum = 0,
ArbitrumOne = 3
@@ -77,44 +67,33 @@ export type Response =
error: string
}
-export default async function handler(
- req: NextApiRequestWithCCTPParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { type: string } }
+): Promise> {
try {
- const {
- walletAddress,
- l1ChainId: l1ChainIdString,
- pageNumber = '0',
- pageSize = '10',
- type
- } = req.query
+ const { searchParams } = new URL(request.url)
+ const walletAddress = searchParams.get('walletAddress') as Address
+ const l1ChainIdString = searchParams.get('l1ChainId') || '1'
+ const pageNumber = searchParams.get('pageNumber') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
+ const type = params.type
const l1ChainId = parseInt(l1ChainIdString, 10)
if (
typeof type !== 'string' ||
(type !== 'deposits' && type !== 'withdrawals')
) {
- res.status(400).send({
- error: `invalid API route: ${type}`,
- data: {
- pending: [],
- completed: []
- }
- })
- return
- }
-
- // validate method
- if (req.method !== 'GET') {
- res.status(400).send({
- error: `invalid_method: ${req.method}`,
- data: {
- pending: [],
- completed: []
- }
- })
- return
+ return NextResponse.json(
+ {
+ error: `invalid API route: ${type}`,
+ data: {
+ pending: [],
+ completed: []
+ }
+ },
+ { status: 400 }
+ )
}
// validate the request parameters
@@ -123,26 +102,30 @@ export default async function handler(
if (!walletAddress) errorMessage.push(' is required')
if (errorMessage.length) {
- res.status(400).json({
- error: `incomplete request: ${errorMessage.join(', ')}`,
- data: {
- pending: [],
- completed: []
- }
- })
- return
+ return NextResponse.json(
+ {
+ error: `incomplete request: ${errorMessage.join(', ')}`,
+ data: {
+ pending: [],
+ completed: []
+ }
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: {
- pending: [],
- completed: []
+ return NextResponse.json(
+ {
+ data: {
+ pending: [],
+ completed: []
+ },
+ error: null
},
- error: null
- })
- return
+ { status: 200 }
+ )
}
const l2ChainId =
@@ -264,23 +247,29 @@ export default async function handler(
}
)
- res.status(200).json({
- meta: {
- source: getSourceFromSubgraphClient(l1Subgraph)
- },
- data: {
- pending,
- completed
+ return NextResponse.json(
+ {
+ meta: {
+ source: getSourceFromSubgraphClient(l1Subgraph)
+ },
+ data: {
+ pending,
+ completed
+ },
+ error: null
},
- error: null
- })
+ { status: 200 }
+ )
} catch (error: unknown) {
- res.status(500).json({
- data: {
- pending: [],
- completed: []
+ return NextResponse.json(
+ {
+ data: {
+ pending: [],
+ completed: []
+ },
+ error: (error as Error)?.message ?? 'Something went wrong'
},
- error: (error as Error)?.message ?? 'Something went wrong'
- })
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts b/packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts
similarity index 71%
rename from packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts
rename to packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts
index f23b2fd4c3..3492a38de9 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/chains/[chainId]/block-number.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/chains/[chainId]/block-number.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import { ChainId } from '../../../../types/ChainId'
@@ -31,19 +31,15 @@ function getSubgraphClient(chainId: number) {
}
}
-export default async function handler(
- req: NextApiRequest & { query: { chainId: string } },
- res: NextApiResponse<
+export async function GET(
+ request: NextRequest,
+ { params }: { params: { chainId: string } }
+): Promise<
+ NextResponse<
{ data: number; meta?: { source: string | null } } | { message: string }
>
-) {
- const { chainId } = req.query
-
- // validate method
- if (req.method !== 'GET') {
- res.status(400).json({ message: `invalid method: ${req.method}` })
- return
- }
+> {
+ const { chainId } = params
try {
const subgraphClient = getSubgraphClient(Number(chainId))
@@ -68,11 +64,14 @@ export default async function handler(
`
})
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: result.data._meta.block.number
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: result.data._meta.block.number
+ },
+ { status: 200 }
+ )
} catch (error) {
- res.status(200).json({ data: 0 })
+ return NextResponse.json({ data: 0 }, { status: 200 })
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/constants.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/constants.ts
similarity index 100%
rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/constants.ts
rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/constants.ts
diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts
similarity index 78%
rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts
rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts
index 7f0ba9d524..19afb9476f 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/lifi.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/lifi.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import {
createConfig,
TransactionRequest as LiFiTransactionRequest,
@@ -95,7 +95,7 @@ function parseLifiRouteToCrosschainTransfersQuoteWithLifiData({
}: {
route: Route
fromAddress?: string
- toAddress: string
+ toAddress?: string
fromChainId: string
toChainId: string
}): LifiCrosschainTransfersRoute {
@@ -212,55 +212,56 @@ export type LifiParams = QueryParams & {
}
/** Extending the standard NextJs request with fast bridge transfer params */
-export type NextApiRequestWithLifiParams = NextApiRequest & {
- query: LifiParams
-}
export const INTEGRATOR_ID = '_arbitrum'
-export default async function handler(
- req: NextApiRequestWithLifiParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
createConfig({
integrator: INTEGRATOR_ID,
apiKey: process.env.LIFI_KEY
})
- const {
- fromToken,
- toToken,
- fromChainId,
- toChainId,
- fromAmount,
- fromAddress,
- toAddress,
- denyBridges,
- denyExchanges,
- slippage
- } = req.query
+ const { searchParams } = new URL(request.url)
+ const fromToken = searchParams.get('fromToken')
+ const toToken = searchParams.get('toToken')
+ const fromChainId = searchParams.get('fromChainId')
+ const toChainId = searchParams.get('toChainId')
+ const fromAmount = searchParams.get('fromAmount') || '0'
+ const fromAddress = searchParams.get('fromAddress') || undefined
+ const toAddress = searchParams.get('toAddress') || undefined
+ const denyBridges = searchParams.get('denyBridges')
+ const denyExchanges = searchParams.get('denyExchanges')
+ const slippage = searchParams.get('slippage')
try {
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: null })
- return
- }
-
// Validate parameters
if (!fromToken || !utils.isAddress(fromToken)) {
- res
- .status(400)
- .send({ message: 'fromToken is not a valid address', data: null })
- return
+ return NextResponse.json(
+ { message: 'fromToken is not a valid address', data: null },
+ { status: 400 }
+ )
}
if (!toToken || !utils.isAddress(toToken)) {
- res
- .status(400)
- .send({ message: 'toToken is not a valid address', data: null })
- return
+ return NextResponse.json(
+ { message: 'toToken is not a valid address', data: null },
+ { status: 400 }
+ )
+ }
+
+ if (!fromChainId) {
+ return NextResponse.json(
+ { message: 'fromChainId is required', data: null },
+ { status: 400 }
+ )
+ }
+
+ if (!toChainId) {
+ return NextResponse.json(
+ { message: 'toChainId is required', data: null },
+ { status: 400 }
+ )
}
if (
@@ -270,11 +271,13 @@ export default async function handler(
destinationChainId: Number(toChainId)
})
) {
- res.status(400).send({
- message: `Sending fromToken (${fromToken}) from chain ${fromChainId} to chain ${toChainId} is not supported`,
- data: null
- })
- return
+ return NextResponse.json(
+ {
+ message: `Sending fromToken (${fromToken}) from chain ${fromChainId} to chain ${toChainId} is not supported`,
+ data: null
+ },
+ { status: 400 }
+ )
}
// Validate options
@@ -284,11 +287,13 @@ export default async function handler(
parsedSlippage <= 0 ||
parsedSlippage > 100
) {
- res.status(400).send({
- message: `Slippage is invalid`,
- data: null
- })
- return
+ return NextResponse.json(
+ {
+ message: `Slippage is invalid`,
+ data: null
+ },
+ { status: 400 }
+ )
}
let bridgesToExclude: string[] = []
@@ -348,7 +353,7 @@ export default async function handler(
parseLifiRouteToCrosschainTransfersQuoteWithLifiData({
route,
fromAddress,
- toAddress,
+ toAddress: toAddress || fromAddress,
fromChainId,
toChainId
})
@@ -370,35 +375,38 @@ export default async function handler(
// We didn't filter route with tags
if (tags.length === 2) {
- res.status(200).json({
- data: filteredRoutes.filter(
- route => route.protocolData.orders.length > 0
- )
- })
- return
+ return NextResponse.json(
+ {
+ data: filteredRoutes.filter(
+ route => route.protocolData.orders.length > 0
+ )
+ },
+ { status: 200 }
+ )
}
const cheapestRoute = findCheapestRoute(filteredRoutes)
const fastestRoute = findFastestRoute(filteredRoutes)
if (!cheapestRoute && !fastestRoute) {
- res.status(204).json({ data: [] })
- return
+ return NextResponse.json({ data: [] }, { status: 204 })
}
if (cheapestRoute && fastestRoute && cheapestRoute === fastestRoute) {
- res.status(200).json({
- data: [
- {
- ...cheapestRoute,
- protocolData: {
- ...cheapestRoute.protocolData,
- orders: [Order.Cheapest, Order.Fastest]
+ return NextResponse.json(
+ {
+ data: [
+ {
+ ...cheapestRoute,
+ protocolData: {
+ ...cheapestRoute.protocolData,
+ orders: [Order.Cheapest, Order.Fastest]
+ }
}
- }
- ]
- })
- return
+ ]
+ },
+ { status: 200 }
+ )
}
const data: LifiCrosschainTransfersRoute[] = []
@@ -420,13 +428,19 @@ export default async function handler(
}
})
}
- res.status(200).json({
- data
- })
+ return NextResponse.json(
+ {
+ data
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: null
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: null
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts
similarity index 97%
rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts
rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts
index 11a844ffbb..5a7f0e23fd 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/types.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/types.ts
@@ -41,6 +41,6 @@ export interface CrosschainTransfersRouteBase {
fromChainId: number
toChainId: number
fromAddress?: string
- toAddress: string
+ toAddress?: string
spenderAddress: string
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.test.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.test.ts
similarity index 100%
rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.test.ts
rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.test.ts
diff --git a/packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.ts b/packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.ts
similarity index 100%
rename from packages/arb-token-bridge-ui/src/pages/api/crosschain-transfers/utils.ts
rename to packages/arb-token-bridge-ui/src/app/api/crosschain-transfers/utils.ts
diff --git a/packages/arb-token-bridge-ui/src/app/api/denylist.ts b/packages/arb-token-bridge-ui/src/app/api/denylist.ts
new file mode 100644
index 0000000000..db614dc644
--- /dev/null
+++ b/packages/arb-token-bridge-ui/src/app/api/denylist.ts
@@ -0,0 +1,43 @@
+import { NextRequest, NextResponse } from 'next/server'
+import denylist from '@/public/__auto-generated-denylist.json'
+
+const ONE_WEEK_IN_SECONDS = 60 * 60 * 24 * 7
+
+export async function GET(
+ request: NextRequest
+): Promise> {
+ try {
+ const { searchParams } = new URL(request.url)
+ const address = searchParams.get('address')
+
+ if (typeof address !== 'string') {
+ return NextResponse.json(
+ {
+ message: `invalid_parameter: expected 'address' to be a string but got ${typeof address}`,
+ data: undefined
+ },
+ { status: 400 }
+ )
+ }
+
+ const isDenylisted = new Set(denylist.content).has(address.toLowerCase())
+
+ return NextResponse.json(
+ { data: isDenylisted },
+ {
+ status: 200,
+ headers: {
+ 'Cache-Control': `max-age=0, s-maxage=${ONE_WEEK_IN_SECONDS}`
+ }
+ }
+ )
+ } catch (error: any) {
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: undefined
+ },
+ { status: 500 }
+ )
+ }
+}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/deposits.ts b/packages/arb-token-bridge-ui/src/app/api/deposits.ts
similarity index 65%
rename from packages/arb-token-bridge-ui/src/pages/api/deposits.ts
rename to packages/arb-token-bridge-ui/src/app/api/deposits.ts
index 0250082bde..7b22a7b68e 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/deposits.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/deposits.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import { FetchDepositsFromSubgraphResult } from '../../util/deposits/fetchDepositsFromSubgraph'
@@ -7,49 +7,25 @@ import {
getSourceFromSubgraphClient
} from '../../api-utils/ServerSubgraphUtils'
-// Extending the standard NextJs request with Deposit-params
-type NextApiRequestWithDepositParams = NextApiRequest & {
- query: {
- sender?: string
- receiver?: string
- l2ChainId: string
- search?: string
- page?: string
- pageSize?: string
- fromBlock?: string
- toBlock?: string
- }
-}
-
type DepositsResponse = {
meta?: { source: string | null }
data: FetchDepositsFromSubgraphResult[]
message?: string // in case of any error
}
-export default async function handler(
- req: NextApiRequestWithDepositParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
try {
- const {
- sender,
- receiver,
- search = '',
- l2ChainId,
- page = '0',
- pageSize = '10',
- fromBlock,
- toBlock
- } = req.query
-
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: [] })
- return
- }
+ const { searchParams } = new URL(request.url)
+ const sender = searchParams.get('sender') || undefined
+ const receiver = searchParams.get('receiver') || undefined
+ const search = searchParams.get('search') || ''
+ const l2ChainId = searchParams.get('l2ChainId')
+ const page = searchParams.get('page') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
+ const fromBlock = searchParams.get('fromBlock') || undefined
+ const toBlock = searchParams.get('toBlock') || undefined
// validate the request parameters
const errorMessage = []
@@ -58,19 +34,18 @@ export default async function handler(
errorMessage.push(' or is required')
if (errorMessage.length) {
- res.status(400).json({
- message: `incomplete request: ${errorMessage.join(', ')}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `incomplete request: ${errorMessage.join(', ')}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: []
- })
- return
+ return NextResponse.json({ data: [] }, { status: 200 })
}
const additionalFilters = `${
@@ -91,11 +66,13 @@ export default async function handler(
subgraphClient = getL1SubgraphClient(Number(l2ChainId))
} catch (error: any) {
// catch attempt to query unsupported networks and throw a 400
- res.status(400).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 400 }
+ )
}
const subgraphResult = await subgraphClient.query({
@@ -142,14 +119,20 @@ export default async function handler(
const transactions: FetchDepositsFromSubgraphResult[] =
subgraphResult.data.deposits
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: transactions
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: transactions
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts b/packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts
similarity index 69%
rename from packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts
rename to packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts
index b0e5b084a9..cff831cecd 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/eth-deposits-custom-destination.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/eth-deposits-custom-destination.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import {
@@ -7,19 +7,6 @@ import {
} from '../../api-utils/ServerSubgraphUtils'
import { FetchEthDepositsToCustomDestinationFromSubgraphResult } from '../../util/deposits/fetchEthDepositsToCustomDestinationFromSubgraph'
-type NextApiRequestWithDepositParams = NextApiRequest & {
- query: {
- sender?: string
- receiver?: string
- l2ChainId: string
- search?: string
- page?: string
- pageSize?: string
- fromBlock?: string
- toBlock?: string
- }
-}
-
type RetryableFromSubgraph = {
destAddr: string
sender: string
@@ -36,28 +23,19 @@ type EthDepositsToCustomDestinationResponse = {
message?: string
}
-export default async function handler(
- req: NextApiRequestWithDepositParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
try {
- const {
- sender,
- receiver,
- search = '',
- l2ChainId,
- page = '0',
- pageSize = '10',
- fromBlock,
- toBlock
- } = req.query
-
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: [] })
- return
- }
+ const { searchParams } = new URL(request.url)
+ const sender = searchParams.get('sender') || undefined
+ const receiver = searchParams.get('receiver') || undefined
+ const search = searchParams.get('search') || ''
+ const l2ChainId = searchParams.get('l2ChainId')
+ const page = searchParams.get('page') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
+ const fromBlock = searchParams.get('fromBlock') || undefined
+ const toBlock = searchParams.get('toBlock') || undefined
const errorMessage = []
if (!l2ChainId) errorMessage.push(' is required')
@@ -65,19 +43,18 @@ export default async function handler(
errorMessage.push(' or is required')
if (errorMessage.length) {
- res.status(400).json({
- message: `incomplete request: ${errorMessage.join(', ')}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `incomplete request: ${errorMessage.join(', ')}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: []
- })
- return
+ return NextResponse.json({ data: [] }, { status: 200 })
}
const additionalFilters = `${
@@ -144,14 +121,20 @@ export default async function handler(
}
})
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: transactions
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: transactions
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/status.ts b/packages/arb-token-bridge-ui/src/app/api/status.ts
similarity index 52%
rename from packages/arb-token-bridge-ui/src/pages/api/status.ts
rename to packages/arb-token-bridge-ui/src/app/api/status.ts
index 4ddf3d889e..a49d3674bc 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/status.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/status.ts
@@ -1,5 +1,5 @@
import axios from 'axios'
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextResponse } from 'next/server'
export type ArbitrumStatusResponse = {
content: {
@@ -19,22 +19,13 @@ export type ArbitrumStatusResponse = {
const STATUS_URL = 'https://status.arbitrum.io/v2/components.json'
-export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse<{
+export async function GET(): Promise<
+ NextResponse<{
data: ArbitrumStatusResponse | undefined
message?: string
}>
-) {
+> {
try {
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: undefined })
- return
- }
-
const statusSummary = (await axios.get(STATUS_URL)).data
const resultJson = {
meta: {
@@ -43,12 +34,22 @@ export default async function handler(
content: statusSummary
}
- res.setHeader('Cache-Control', `max-age=0, s-maxage=${10 * 60}`) // cache response for 10 minutes
- res.status(200).json({ data: resultJson as ArbitrumStatusResponse })
+ return NextResponse.json(
+ { data: resultJson as ArbitrumStatusResponse },
+ {
+ status: 200,
+ headers: {
+ 'Cache-Control': `max-age=0, s-maxage=${10 * 60}`
+ }
+ }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: undefined
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: undefined
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts b/packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts
similarity index 60%
rename from packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts
rename to packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts
index 7914b1b2af..7d9ecb198a 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/teleports/erc20.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/teleports/erc20.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import {
@@ -7,36 +7,21 @@ import {
} from '../../../api-utils/ServerSubgraphUtils'
import { FetchErc20TeleportsFromSubgraphResult } from '../../../util/teleports/fetchErc20TeleportsFromSubgraph'
-// Extending the standard NextJs request with Deposit-params
-type NextApiRequestWithErc20TeleportParams = NextApiRequest & {
- query: {
- sender?: string
- l1ChainId: string
- page?: string
- pageSize?: string
- }
-}
-
type Erc20TeleportResponse = {
meta?: { source: string | null }
data: FetchErc20TeleportsFromSubgraphResult[]
message?: string // in case of any error
}
-export default async function handler(
- req: NextApiRequestWithErc20TeleportParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
try {
- const { sender, l1ChainId, page = '0', pageSize = '10' } = req.query
-
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: [] })
- return
- }
+ const { searchParams } = new URL(request.url)
+ const sender = searchParams.get('sender') || undefined
+ const l1ChainId = searchParams.get('l1ChainId')
+ const page = searchParams.get('page') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
// validate the request parameters
const errorMessage = []
@@ -44,19 +29,18 @@ export default async function handler(
if (!sender) errorMessage.push(' is required')
if (errorMessage.length) {
- res.status(400).json({
- message: `incomplete request: ${errorMessage.join(', ')}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `incomplete request: ${errorMessage.join(', ')}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: []
- })
- return
+ return NextResponse.json({ data: [] }, { status: 200 })
}
let subgraphClient
@@ -64,11 +48,13 @@ export default async function handler(
subgraphClient = getTeleporterSubgraphClient(Number(l1ChainId))
} catch (error: any) {
// catch attempt to query unsupported networks and throw a 400
- res.status(400).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 400 }
+ )
}
const subgraphResult = await subgraphClient.query({
@@ -99,14 +85,20 @@ export default async function handler(
const transactions: FetchErc20TeleportsFromSubgraphResult[] =
subgraphResult.data.teleporteds
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: transactions
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: transactions
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts b/packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts
similarity index 61%
rename from packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts
rename to packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts
index d73592bad8..72cf6edd1a 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/teleports/eth.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/teleports/eth.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import {
getL1SubgraphClient,
@@ -7,50 +7,24 @@ import {
import { getInboxAddressFromOrbitChainId } from '../../../util/orbitChainsList'
import { FetchEthTeleportsFromSubgraphResult } from '../../../util/teleports/fetchEthTeleportsFromSubgraph'
-// Extending the standard NextJs request with Deposit-params
-type NextApiRequestWithDepositParams = NextApiRequest & {
- query: {
- sender?: string
- receiver?: string
- l1ChainId: string
- l2ChainId: string
- l3ChainId: string
- search?: string
- page?: string
- pageSize?: string
- fromBlock?: string
- toBlock?: string
- }
-}
-
type EthTeleportResponse = {
meta?: { source: string | null }
data: FetchEthTeleportsFromSubgraphResult[]
message?: string // in case of any error
}
-export default async function handler(
- req: NextApiRequestWithDepositParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
try {
- const {
- sender,
- receiver,
- l1ChainId,
- l2ChainId,
- l3ChainId,
- page = '0',
- pageSize = '10'
- } = req.query
-
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: [] })
- return
- }
+ const { searchParams } = new URL(request.url)
+ const sender = searchParams.get('sender') || undefined
+ const receiver = searchParams.get('receiver') || undefined
+ const l1ChainId = searchParams.get('l1ChainId')
+ const l2ChainId = searchParams.get('l2ChainId')
+ const l3ChainId = searchParams.get('l3ChainId')
+ const page = searchParams.get('page') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
// validate the request parameters
const errorMessage = []
@@ -65,19 +39,18 @@ export default async function handler(
errorMessage.push(' or is required')
if (errorMessage.length) {
- res.status(400).json({
- message: `incomplete request: ${errorMessage.join(', ')}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `incomplete request: ${errorMessage.join(', ')}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: []
- })
- return
+ return NextResponse.json({ data: [] }, { status: 200 })
}
let subgraphClient
@@ -85,21 +58,25 @@ export default async function handler(
subgraphClient = getL1SubgraphClient(Number(l2ChainId))
} catch (error: any) {
// catch attempt to query unsupported networks and throw a 400
- res.status(400).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 400 }
+ )
}
const l3InboxAddress = getInboxAddressFromOrbitChainId(Number(l3ChainId))
if (typeof l3InboxAddress === 'undefined') {
- res.status(400).json({
- message: `inbox address not found for chain-id: ${l1ChainId} -> ${l2ChainId} -> ${l3ChainId}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `inbox address not found for chain-id: ${l1ChainId} -> ${l2ChainId} -> ${l3ChainId}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
const createRetryableFunctionSelector = '0x679b6ded'
@@ -140,14 +117,20 @@ export default async function handler(
})
)
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: transactions
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: transactions
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts b/packages/arb-token-bridge-ui/src/app/api/withdrawals.ts
similarity index 68%
rename from packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts
rename to packages/arb-token-bridge-ui/src/app/api/withdrawals.ts
index 6f203fc06a..3000017e39 100644
--- a/packages/arb-token-bridge-ui/src/pages/api/withdrawals.ts
+++ b/packages/arb-token-bridge-ui/src/app/api/withdrawals.ts
@@ -1,4 +1,4 @@
-import { NextApiRequest, NextApiResponse } from 'next'
+import { NextRequest, NextResponse } from 'next/server'
import { gql } from '@apollo/client'
import { WithdrawalFromSubgraph } from '../../util/withdrawals/fetchWithdrawalsFromSubgraph'
@@ -7,49 +7,25 @@ import {
getSourceFromSubgraphClient
} from '../../api-utils/ServerSubgraphUtils'
-// Extending the standard NextJs request with Withdrawal-params
-type NextApiRequestWithWithdrawalParams = NextApiRequest & {
- query: {
- sender?: string
- receiver?: string
- l2ChainId: string
- search?: string
- page?: string
- pageSize?: string
- fromBlock?: string
- toBlock?: string
- }
-}
-
type WithdrawalResponse = {
meta?: { source: string | null }
data: WithdrawalFromSubgraph[]
message?: string // in case of any error
}
-export default async function handler(
- req: NextApiRequestWithWithdrawalParams,
- res: NextApiResponse
-) {
+export async function GET(
+ request: NextRequest
+): Promise> {
try {
- const {
- sender,
- receiver,
- search = '',
- l2ChainId,
- page = '0',
- pageSize = '10',
- fromBlock,
- toBlock
- } = req.query
-
- // validate method
- if (req.method !== 'GET') {
- res
- .status(400)
- .send({ message: `invalid_method: ${req.method}`, data: [] })
- return
- }
+ const { searchParams } = new URL(request.url)
+ const sender = searchParams.get('sender') || undefined
+ const receiver = searchParams.get('receiver') || undefined
+ const search = searchParams.get('search') || ''
+ const l2ChainId = searchParams.get('l2ChainId')
+ const page = searchParams.get('page') || '0'
+ const pageSize = searchParams.get('pageSize') || '10'
+ const fromBlock = searchParams.get('fromBlock') || undefined
+ const toBlock = searchParams.get('toBlock') || undefined
// validate the request parameters
const errorMessage = []
@@ -58,19 +34,18 @@ export default async function handler(
errorMessage.push(' or is required')
if (errorMessage.length) {
- res.status(400).json({
- message: `incomplete request: ${errorMessage.join(', ')}`,
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: `incomplete request: ${errorMessage.join(', ')}`,
+ data: []
+ },
+ { status: 400 }
+ )
}
// if invalid pageSize, send empty data instead of error
if (isNaN(Number(pageSize)) || Number(pageSize) === 0) {
- res.status(200).json({
- data: []
- })
- return
+ return NextResponse.json({ data: [] }, { status: 200 })
}
const additionalFilters = `${
@@ -91,11 +66,13 @@ export default async function handler(
subgraphClient = getL2SubgraphClient(Number(l2ChainId))
} catch (error: any) {
// catch attempt to query unsupported networks and throw a 400
- res.status(400).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
- return
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 400 }
+ )
}
const subgraphResult = await subgraphClient.query({
@@ -164,14 +141,20 @@ export default async function handler(
}
})
- res.status(200).json({
- meta: { source: getSourceFromSubgraphClient(subgraphClient) },
- data: transactions
- })
+ return NextResponse.json(
+ {
+ meta: { source: getSourceFromSubgraphClient(subgraphClient) },
+ data: transactions
+ },
+ { status: 200 }
+ )
} catch (error: any) {
- res.status(500).json({
- message: error?.message ?? 'Something went wrong',
- data: []
- })
+ return NextResponse.json(
+ {
+ message: error?.message ?? 'Something went wrong',
+ data: []
+ },
+ { status: 500 }
+ )
}
}
diff --git a/packages/arb-token-bridge-ui/src/components/App/App.tsx b/packages/arb-token-bridge-ui/src/components/App/App.tsx
index e81b648124..20cc91739e 100644
--- a/packages/arb-token-bridge-ui/src/components/App/App.tsx
+++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx
@@ -17,8 +17,9 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship'
import { useSyncConnectedChainToAnalytics } from './useSyncConnectedChainToAnalytics'
import { useSyncConnectedChainToQueryParams } from './useSyncConnectedChainToQueryParams'
import { Layout } from '../common/Layout'
-import { AppProviders } from './AppProviders'
import { useTheme } from '../../hooks/useTheme'
+import dynamic from 'next/dynamic'
+import { Loader } from '../common/atoms/Loader'
declare global {
interface Window {
@@ -128,6 +129,21 @@ const AppContent = React.memo(() => {
AppContent.displayName = 'AppContent'
+const AppProviders = dynamic(
+ () => import('./AppProviders').then(mod => mod.AppProviders),
+ {
+ ssr: false, // use-query-params provider doesn't support SSR
+ loading: () => (
+
+ )
+ }
+)
+
export default function App() {
return (
diff --git a/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx b/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx
index c93663f38d..2340542551 100644
--- a/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx
+++ b/packages/arb-token-bridge-ui/src/components/App/AppProviders.tsx
@@ -11,7 +11,7 @@ import { AppContextProvider } from './AppContext'
import { getProps } from '../../util/wagmi/setup'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createConfig } from '@lifi/sdk'
-import { INTEGRATOR_ID } from '../../pages/api/crosschain-transfers/lifi'
+import { INTEGRATOR_ID } from '@/bridge/app/api/crosschain-transfers/lifi'
const rainbowkitTheme = merge(darkTheme(), {
colors: {
diff --git a/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx b/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx
index fa862d02dc..0b1d0f3150 100644
--- a/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx
+++ b/packages/arb-token-bridge-ui/src/components/Sidebar/AppMobileSidebar.tsx
@@ -1,20 +1,10 @@
-'use client'
import { PlusCircleIcon } from '@heroicons/react/24/outline'
import { MenuItem } from '@offchainlabs/cobalt'
import { ConnectButton } from '@rainbow-me/rainbowkit'
-import dynamic from 'next/dynamic'
import { usePostHog } from 'posthog-js/react'
import { useAccount } from 'wagmi'
import { AccountMenuItem } from './AccountMenuItem'
-
-// Dynamically import the MobileSidebar component with SSR disabled
-const DynamicMobileSidebar = dynamic(
- () =>
- import('@offchainlabs/cobalt').then(mod => ({
- default: mod.MobileSidebar
- })),
- { ssr: false }
-)
+import { MobileSidebar } from '@offchainlabs/cobalt'
export const AppMobileSidebar: React.FC = () => {
const posthog = usePostHog()
@@ -22,7 +12,7 @@ export const AppMobileSidebar: React.FC = () => {
return (
- = () => {
)}
)}
-
+
)
}
diff --git a/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx b/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx
index c6cb166b3f..1444bf07ab 100644
--- a/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx
+++ b/packages/arb-token-bridge-ui/src/components/Sidebar/AppSidebar.tsx
@@ -1,18 +1,11 @@
-'use client'
-import dynamic from 'next/dynamic'
import { usePostHog } from 'posthog-js/react'
-
-// Dynamically import the Sidebar component with SSR disabled
-const DynamicSidebar = dynamic(
- () => import('@offchainlabs/cobalt').then(mod => ({ default: mod.Sidebar })),
- { ssr: false }
-)
+import { Sidebar } from '@offchainlabs/cobalt'
export const AppSidebar = () => {
const posthog = usePostHog()
return (
-
+
)
}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx
index 2096aef006..5e205affc1 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/HighSlippageWarningDialog.tsx
@@ -3,7 +3,7 @@ import { InformationCircleIcon } from '@heroicons/react/24/outline'
import { useRouteStore } from './hooks/useRouteStore'
import { formatAmount, formatUSD } from '../../util/NumberUtils'
import { BigNumber } from 'ethers'
-import { Token } from '../../pages/api/crosschain-transfers/types'
+import { Token } from '@/bridge/app/api/crosschain-transfers/types'
import { getAmountToPay } from './useTransferReadiness'
type AmountProps = {
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx
index dea2d2545e..37724b66f7 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/LifiSettings.tsx
@@ -23,7 +23,7 @@ import {
} from '@heroicons/react/24/outline'
import { shallow } from 'zustand/shallow'
import { ExternalLink } from '../common/ExternalLink'
-import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../app/api/crosschain-transfers/utils'
function useIsLifiSupported() {
const [networks] = useNetworks()
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx
index 0331d9d4e1..37c69cbfb5 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/LifiRoute.tsx
@@ -11,7 +11,7 @@ import { useArbQueryParams } from '../../../hooks/useArbQueryParams'
import {
LifiCrosschainTransfersRoute,
Order
-} from '../../../pages/api/crosschain-transfers/lifi'
+} from '../../../app/api/crosschain-transfers/lifi'
import {
useLifiCrossTransfersRoute,
UseLifiCrossTransfersRouteParams
@@ -26,7 +26,7 @@ import { useCallback, useEffect, useMemo } from 'react'
import { useAmountBigNumber } from '../hooks/useAmountBigNumber'
import { shallow } from 'zustand/shallow'
import { Address } from 'viem'
-import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils'
import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types'
import { useRoutes } from './Routes'
import { NoteBox } from '../../common/NoteBox'
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx
index c824ecf916..a835df9ff6 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Route.tsx
@@ -25,7 +25,7 @@ import { getConfirmationTime } from '../../../util/WithdrawalUtils'
import { shortenAddress } from '../../../util/CommonUtils'
import { useAppContextState } from '../../App/AppContext'
import { useMode } from '../../../hooks/useMode'
-import { Token } from '../../../pages/api/crosschain-transfers/types'
+import { Token } from '@/bridge/app/api/crosschain-transfers/types'
import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types'
// Types
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx
index d768f35204..95f337ce76 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/Routes.tsx
@@ -18,7 +18,7 @@ import { useSelectedToken } from '../../../hooks/useSelectedToken'
import { ERC20BridgeToken } from '../../../hooks/arbTokenBridge.types'
import { twMerge } from 'tailwind-merge'
import { useMode } from '../../../hooks/useMode'
-import { isValidLifiTransfer } from '../../../pages/api/crosschain-transfers/utils'
+import { isValidLifiTransfer } from '../../../app/api/crosschain-transfers/utils'
import { useIsArbitrumCanonicalTransfer } from '../hooks/useIsCanonicalTransfer'
function Wrapper({ children }: PropsWithChildren) {
@@ -151,7 +151,13 @@ export function getRoutes({
}
return {
- ChildRoutes: <>{ChildRoutes.map(ChildRoute => ChildRoute)}>,
+ ChildRoutes: (
+ <>
+ {ChildRoutes.map((ChildRoute, index) =>
+ React.cloneElement(ChildRoute, { key: index })
+ )}
+ >
+ ),
routes
}
}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts
index ee1e387982..5d0f368009 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/Routes/getGasCostAndToken.ts
@@ -1,7 +1,7 @@
import { constants } from 'ethers'
import { UseGasSummaryResult } from '../../../hooks/TransferPanel/useGasSummary'
import { NativeCurrency } from '../../../hooks/useNativeCurrency'
-import { Token } from '../../../pages/api/crosschain-transfers/types'
+import { Token } from '@/bridge/app/api/crosschain-transfers/types'
export function getGasCostAndToken({
childChainNativeCurrency,
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx
index fee5251723..a24eb1ae5c 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/SettingsDialog.tsx
@@ -23,7 +23,7 @@ import { useArbQueryParams } from '../../hooks/useArbQueryParams'
import { useDestinationAddressError } from './hooks/useDestinationAddressError'
import { useAccountType } from '../../hooks/useAccountType'
import { Dialog, UseDialogProps } from '../common/Dialog'
-import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils'
+import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils'
import { isDepositMode as isDepositModeUtil } from '../../util/isDepositMode'
function useTools() {
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx
index b5fe26152d..b8a880bbf1 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenImportDialog.tsx
@@ -19,7 +19,6 @@ import { useNetworksRelationship } from '../../hooks/useNetworksRelationship'
import { TokenInfo } from './TokenInfo'
import { NoteBox } from '../common/NoteBox'
import { useSelectedToken } from '../../hooks/useSelectedToken'
-import { addressesEqual } from '../../util/AddressUtils'
import { constants } from 'ethers'
enum ImportStatus {
@@ -67,7 +66,7 @@ export function TokenImportDialog({
arbTokenBridge: { bridgeTokens, token }
}
} = useAppState()
- const [selectedToken, setSelectedToken] = useSelectedToken()
+ const [, setSelectedToken] = useSelectedToken()
const [networks] = useNetworks()
const { childChainProvider, parentChainProvider } =
useNetworksRelationship(networks)
@@ -173,7 +172,7 @@ export function TokenImportDialog({
)
useEffect(() => {
- if (!isOpen) {
+ if (!isOpen || isImportingToken) {
return
}
@@ -222,37 +221,8 @@ export function TokenImportDialog({
isL1AddressLoading,
isOpen,
l1Address,
- searchForTokenInLists
- ])
-
- useEffect(() => {
- if (!isOpen) {
- return
- }
-
- if (isL1AddressLoading && !l1Address) {
- return
- }
-
- const foundToken = tokensFromUser[l1Address || tokenAddress]
-
- if (typeof foundToken === 'undefined') {
- return
- }
-
- // Listen for the token to be added to the bridge so we can automatically select it
- if (!addressesEqual(foundToken.address, selectedToken?.address)) {
- onClose(true)
- selectToken(foundToken)
- }
- }, [
- isL1AddressLoading,
- tokenAddress,
- isOpen,
- l1Address,
- onClose,
- selectToken,
- tokensFromUser
+ searchForTokenInLists,
+ isImportingToken
])
async function storeNewToken(newToken: string) {
@@ -286,9 +256,15 @@ export function TokenImportDialog({
selectToken(tokenToImport!)
} else {
// Token is not added to the bridge, so we add it
- storeNewToken(l1Address).catch(() => {
- setStatus(ImportStatus.ERROR)
- })
+ storeNewToken(l1Address)
+ .then(() => {
+ if (tokenToImport) {
+ selectToken(tokenToImport)
+ }
+ })
+ .catch(() => {
+ setStatus(ImportStatus.ERROR)
+ })
}
}
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx
index 148d6efa4c..61c57eceed 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenInfo.tsx
@@ -16,7 +16,7 @@ import {
isTokenArbitrumSepoliaNativeUSDC
} from '../../util/TokenUtils'
import { SafeImage } from '../common/SafeImage'
-import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../app/api/crosschain-transfers/utils'
export function TokenLogoFallback({ className }: { className?: string }) {
return (
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx
index 4f079c9b75..44b22ef79b 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenRow.tsx
@@ -34,7 +34,7 @@ import { BlockExplorerTokenLink } from './TokenInfoTooltip'
import { addressesEqual } from '../../util/AddressUtils'
import { constants } from 'ethers'
import { ChainId } from '../../types/ChainId'
-import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../app/api/crosschain-transfers/utils'
function tokenListIdsToNames(ids: string[]): string {
return ids
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx
index 40eb046455..33b32a4c0a 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferDisabledDialog.tsx
@@ -14,7 +14,7 @@ import { useSelectedTokenIsWithdrawOnly } from './hooks/useSelectedTokenIsWithdr
import { isTransferDisabledToken } from '../../util/TokenTransferDisabledUtils'
import { isTeleportEnabledToken } from '../../util/TokenTeleportEnabledUtils'
import { addressesEqual } from '../../util/AddressUtils'
-import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils'
+import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils'
import { ERC20BridgeToken } from '../../hooks/arbTokenBridge.types'
import { isLifiEnabled } from '../../util/featureFlag'
import { CommonAddress } from '../../util/CommonAddressUtils'
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
index e249730ed4..8264587be1 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx
@@ -101,7 +101,7 @@ import { useMode } from '../../hooks/useMode'
import {
getTokenOverride,
isValidLifiTransfer
-} from '../../pages/api/crosschain-transfers/utils'
+} from '../../app/api/crosschain-transfers/utils'
import { NoteBox } from '../common/NoteBox'
const signerUndefinedError = 'Signer is undefined'
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx
index 1e07533136..880df3d36c 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx
@@ -26,7 +26,7 @@ import { useArbQueryParams } from '../../../hooks/useArbQueryParams'
import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer'
import { sanitizeTokenSymbol } from '../../../util/TokenUtils'
import { useRouteStore } from '../hooks/useRouteStore'
-import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils'
function BalanceRow({
parentErc20Address,
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx
index 5a9595f9bd..73cc6cbf4b 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx
@@ -39,7 +39,7 @@ import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer'
import { useSourceChainNativeCurrencyDecimals } from '../../../hooks/useSourceChainNativeCurrencyDecimals'
import { useIsOftV2Transfer } from '../hooks/useIsOftV2Transfer'
import { useBalances } from '../../../hooks/useBalances'
-import { getTokenOverride } from '../../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../../app/api/crosschain-transfers/utils'
function Amount2ToggleButton() {
const [networks] = useNetworks()
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx
index c9d9ecfac2..3eddd4d1b3 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.test.tsx
@@ -2,17 +2,23 @@ import { act, renderHook } from '@testing-library/react'
import { vi, it, expect } from 'vitest'
import { useAmountBigNumber } from './useAmountBigNumber'
import {
- EncodedQuery,
+ DecodedValueMap,
QueryParamAdapter,
- QueryParamOptions,
+ QueryParamConfigMap,
QueryParamProvider
} from 'use-query-params'
import React, { PropsWithChildren } from 'react'
import { makeMockAdapter } from '../../../hooks/__tests__/helpers'
+import {
+ queryParamProviderOptions,
+ SetQueryParamsParameters
+} from '../../../hooks/useArbQueryParams'
const mocks = vi.hoisted(() => {
return {
- useSelectedTokenDecimals: vi.fn()
+ useSelectedTokenDecimals: vi.fn(),
+ mockSetQueryParams: vi.fn(),
+ mockQueryParams: {} as Partial>
}
})
@@ -22,14 +28,44 @@ vi.mock('../../../hooks/TransferPanel/useSelectedTokenDecimals', () => {
}
})
-export function setupWrapper(query: EncodedQuery, options?: QueryParamOptions) {
+vi.mock('use-query-params', async () => {
+ const actual = await vi.importActual('use-query-params')
+ return {
+ ...actual,
+ useQueryParams: vi.fn(() => [
+ mocks.mockQueryParams,
+ mocks.mockSetQueryParams
+ ])
+ }
+})
+
+export function setupWrapper(
+ query: Partial>
+) {
+ mocks.mockQueryParams = Object.fromEntries(
+ new URLSearchParams(query as Record)
+ )
+
const Adapter = makeMockAdapter({
search: new URLSearchParams(query as Record).toString()
})
const adapter = Adapter.adapter as QueryParamAdapter
+
+ mocks.mockSetQueryParams.mockImplementation(
+ (updates: SetQueryParamsParameters) => {
+ const searchParams = new URLSearchParams()
+ Object.entries(updates).forEach(([key, value]) => {
+ if (value !== undefined && value !== null) {
+ searchParams.set(key, String(value))
+ }
+ })
+ adapter.push({ search: `?${searchParams.toString()}` })
+ }
+ )
+
const wrapper = ({ children }: PropsWithChildren) => (
-
+
{children}
)
@@ -59,6 +95,7 @@ it('Does not truncate if amount has more digits than number of decimals', () =>
})
it('Update amount if selectedToken changes', async () => {
+ vi.useFakeTimers()
mocks.useSelectedTokenDecimals.mockReturnValue(18)
const { wrapper, adapter } = setupWrapper({ amount: '1.23456789' })
const { result, rerender } = renderHook(() => useAmountBigNumber(), {
@@ -72,8 +109,14 @@ it('Update amount if selectedToken changes', async () => {
rerender()
})
+ await act(async () => {
+ vi.runOnlyPendingTimers()
+ })
+
expect(adapter.push).toHaveBeenCalledExactlyOnceWith({
search: '?amount=1.234567'
})
expect(result.current.toString()).toEqual('1234567')
+
+ vi.useRealTimers()
})
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts
index 6937d84c72..639e1e4fab 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useAmountBigNumber.ts
@@ -25,7 +25,7 @@ export function useAmountBigNumber() {
)
if (amount !== sanitizedAmount) {
- setQueryParams({ amount: sanitizedAmount })
+ setQueryParams({ amount: sanitizedAmount }, { debounce: true })
}
return utils.parseUnits(sanitizedAmount, selectedTokenDecimals)
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts
index ba4126084d..c3b85fbddc 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useRouteStore.ts
@@ -3,7 +3,7 @@ import { create } from 'zustand'
import { MergedTransactionLifiData } from '../../../state/app/state'
import { LiFiStep } from '@lifi/sdk'
import { Address } from 'viem'
-import { LifiCrosschainTransfersRoute } from '../../../pages/api/crosschain-transfers/lifi'
+import { LifiCrosschainTransfersRoute } from '@/bridge/app/api/crosschain-transfers/lifi'
import { BigNumber } from 'ethers'
export type RouteType =
diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts
index 91cc85c8a4..da3885c831 100644
--- a/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts
+++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/useTransferReadiness.ts
@@ -41,8 +41,8 @@ import {
} from './hooks/useRouteStore'
import { shallow } from 'zustand/shallow'
import { isLifiEnabled } from '../../util/featureFlag'
-import { isValidLifiTransfer } from '../../pages/api/crosschain-transfers/utils'
-import { Token } from '../../pages/api/crosschain-transfers/types'
+import { isValidLifiTransfer } from '../../app/api/crosschain-transfers/utils'
+import { Token } from '../../app/api/crosschain-transfers/types'
// Add chains IDs that are currently down or disabled
// It will block transfers (both deposits and withdrawals) and display an info box in the transfer panel
diff --git a/packages/arb-token-bridge-ui/src/components/common/Layout.tsx b/packages/arb-token-bridge-ui/src/components/common/Layout.tsx
index c7f0f970ac..a9c8a44f73 100644
--- a/packages/arb-token-bridge-ui/src/components/common/Layout.tsx
+++ b/packages/arb-token-bridge-ui/src/components/common/Layout.tsx
@@ -1,4 +1,4 @@
-import React from 'react'
+import React, { PropsWithChildren } from 'react'
import { twMerge } from 'tailwind-merge'
import Image from 'next/image'
import EclipseBottom from '@/images/eclipse_bottom.png'
@@ -7,28 +7,23 @@ import { SiteBanner } from './SiteBanner'
import { AppSidebar } from '../Sidebar/AppSidebar'
import { Toast } from './atoms/Toast'
-import 'react-toastify/dist/ReactToastify.css'
import { useMode } from '../../hooks/useMode'
import { unica } from './Font'
-export type LayoutProps = {
- children: React.ReactNode
-}
-
-export function Layout(props: LayoutProps) {
+export function Layout(props: PropsWithChildren) {
const { embedMode } = useMode()
if (embedMode) {
return (
-
+
{props.children}
-
+
)
}
return (
-
+
-
+
)
}
diff --git a/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx b/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx
index b8593827ee..43467b0492 100644
--- a/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx
+++ b/packages/arb-token-bridge-ui/src/components/common/SiteBanner.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import dayjs from 'dayjs'
import { ExternalLink } from './ExternalLink'
-import { ArbitrumStatusResponse } from '../../pages/api/status'
+import { ArbitrumStatusResponse } from '@/bridge/app/api/status'
import { getAPIBaseUrl } from '../../util'
const SiteBannerArbiscanIncident = ({
diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts
index 3b4b1562da..bc808a7095 100644
--- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useGasEstimates.ts
@@ -21,7 +21,7 @@ import {
useLifiCrossTransfersRoute,
UseLifiCrossTransfersRouteParams
} from '../useLifiCrossTransferRoute'
-import { getTokenOverride } from '../../pages/api/crosschain-transfers/utils'
+import { getTokenOverride } from '../../app/api/crosschain-transfers/utils'
import { Address } from 'viem'
import { useLifiSettingsStore } from '../../components/TransferPanel/hooks/useLifiSettingsStore'
import { shallow } from 'zustand/shallow'
diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts
index a9f1ba6b23..043216a515 100644
--- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSetInputAmount.ts
@@ -12,7 +12,7 @@ export function useSetInputAmount() {
(newAmount: string) => {
const correctDecimalsAmount = truncateExtraDecimals(newAmount, decimals)
- setQueryParams({ amount: correctDecimalsAmount })
+ setQueryParams({ amount: correctDecimalsAmount }, { debounce: true })
},
[decimals, setQueryParams]
)
@@ -21,7 +21,7 @@ export function useSetInputAmount() {
(newAmount: string) => {
const correctDecimalsAmount = truncateExtraDecimals(newAmount, 18)
- setQueryParams({ amount2: correctDecimalsAmount })
+ setQueryParams({ amount2: correctDecimalsAmount }, { debounce: true })
},
[setQueryParams]
)
diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts
index 20a74dea03..bdb842e50b 100644
--- a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts
+++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParams.test.ts
@@ -10,7 +10,10 @@ import {
DisabledFeaturesParam
} from '../useArbQueryParams'
import { createMockOrbitChain } from './helpers'
-import { sanitizeTabQueryParam, sanitizeTokenQueryParam } from '../../pages'
+import {
+ sanitizeTabQueryParam,
+ sanitizeTokenQueryParam
+} from '../../util/queryParamUtils'
describe('AmountQueryParam custom encoder and decoder', () => {
describe('encode input field value to query param', () => {
diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx
new file mode 100644
index 0000000000..577881d99a
--- /dev/null
+++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useArbQueryParamsDebouncing.test.tsx
@@ -0,0 +1,194 @@
+import React from 'react'
+import { vi, beforeEach, describe, it, expect, afterEach } from 'vitest'
+import { ChainId } from '../../types/ChainId'
+import { PropsWithChildren } from 'react'
+import { act, renderHook } from '@testing-library/react'
+import { useArbQueryParams } from '../useArbQueryParams'
+
+const mockSetQueryParams = vi.fn()
+let currentQueryParams = {
+ amount: '',
+ amount2: '',
+ sourceChain: ChainId.Ethereum,
+ destinationChain: ChainId.ArbitrumOne,
+ token: null
+}
+
+vi.mock('use-query-params', () => ({
+ useQueryParams: () => {
+ return [currentQueryParams, mockSetQueryParams]
+ },
+ QueryParamProvider: ({ children }: PropsWithChildren) => children,
+ BooleanParam: { encode: vi.fn(), decode: vi.fn() },
+ StringParam: { encode: vi.fn(), decode: vi.fn() },
+ withDefault: vi.fn()
+}))
+
+const TestWrapper = ({ children }: PropsWithChildren) => <>{children}>
+
+describe.sequential('useArbQueryParams debouncing', () => {
+ beforeEach(() => {
+ vi.useFakeTimers()
+
+ currentQueryParams = {
+ amount: '',
+ amount2: '',
+ sourceChain: ChainId.Ethereum,
+ destinationChain: ChainId.ArbitrumOne,
+ token: null
+ }
+
+ mockSetQueryParams.mockClear()
+ })
+
+ afterEach(() => {
+ vi.useRealTimers()
+ vi.clearAllMocks()
+ })
+
+ it('should be batched with debounce: true', async () => {
+ const { result } = renderHook(() => useArbQueryParams(), {
+ wrapper: TestWrapper
+ })
+
+ await act(async () => {
+ const [, setQueryParams] = result.current
+ setQueryParams({ amount: '10.5' }, { debounce: true })
+ setQueryParams({ sourceChain: ChainId.ArbitrumOne }, { debounce: true })
+ setQueryParams({ destinationChain: ChainId.Ethereum }, { debounce: true })
+ setQueryParams(
+ { token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' },
+ { debounce: true }
+ )
+ vi.runOnlyPendingTimers()
+ })
+
+ expect(mockSetQueryParams).toHaveBeenCalledExactlyOnceWith({
+ amount: '10.5',
+ sourceChain: ChainId.ArbitrumOne,
+ destinationChain: ChainId.Ethereum,
+ token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831'
+ })
+ })
+
+ it('should flush pending updates when receiving a call with debounce: false', async () => {
+ const { result } = renderHook(() => useArbQueryParams(), {
+ wrapper: TestWrapper
+ })
+
+ await act(async () => {
+ const [, setQueryParams] = result.current
+ setQueryParams({ amount: '10.5' }, { debounce: true })
+ setQueryParams({ sourceChain: ChainId.ArbitrumOne }, { debounce: true })
+ setQueryParams(
+ { destinationChain: ChainId.Ethereum },
+ { debounce: false }
+ )
+ setQueryParams(
+ { token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' },
+ { debounce: true }
+ )
+ vi.runOnlyPendingTimers()
+ })
+
+ expect(mockSetQueryParams).toHaveBeenCalledTimes(2)
+ // The first 2 debounced updates are merged with the first non-debounced update
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(1, {
+ amount: '10.5',
+ sourceChain: ChainId.ArbitrumOne,
+ destinationChain: ChainId.Ethereum
+ })
+
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(2, {
+ token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831'
+ })
+ })
+
+ it('should not be batched with debounce: false', async () => {
+ const { result } = renderHook(() => useArbQueryParams(), {
+ wrapper: TestWrapper
+ })
+
+ await act(async () => {
+ const [, setQueryParams] = result.current
+ setQueryParams({ amount: '10.5' })
+ setQueryParams({ sourceChain: ChainId.ArbitrumOne })
+ setQueryParams({ destinationChain: ChainId.Ethereum })
+ setQueryParams({ token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831' })
+ vi.runOnlyPendingTimers()
+ })
+
+ expect(mockSetQueryParams).toHaveBeenCalledTimes(4)
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(1, {
+ amount: '10.5'
+ })
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(2, {
+ sourceChain: ChainId.ArbitrumOne
+ })
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(3, {
+ destinationChain: ChainId.Ethereum
+ })
+ expect(mockSetQueryParams).toHaveBeenNthCalledWith(4, {
+ token: '0xaf88d065e77c8cc2239327c5edb3a432268e5831'
+ })
+ })
+
+ it('should handle mixed object and function updates correctly', async () => {
+ const { result } = renderHook(() => useArbQueryParams(), {
+ wrapper: TestWrapper
+ })
+
+ await act(async () => {
+ const [, setQueryParams] = result.current
+ setQueryParams({ amount: '100' }, { debounce: true })
+ setQueryParams({ sourceChain: ChainId.Base }, { debounce: true })
+ setQueryParams(
+ { destinationChain: ChainId.ArbitrumOne },
+ { debounce: true }
+ )
+ setQueryParams(
+ { token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913' },
+ { debounce: true }
+ )
+ setQueryParams(prevState => ({
+ ...prevState,
+ amount: (Number(prevState.amount) * 2).toString(), // 200
+ amount2: '300'
+ }))
+ setQueryParams(prevState => ({
+ ...prevState,
+ amount2: (Number(prevState.amount2) * 3).toString() // 900
+ }))
+ vi.runOnlyPendingTimers()
+ })
+
+ expect(mockSetQueryParams).toHaveBeenCalledTimes(2)
+ expect(mockSetQueryParams).toHaveBeenCalledWith(expect.any(Function))
+
+ const setQueryParams = mockSetQueryParams.mock.calls[0]?.[0]
+ const secondSetQueryParams = mockSetQueryParams.mock.calls[1]?.[0]
+
+ const expectedInputs = {
+ ...currentQueryParams,
+ amount: '100',
+ sourceChain: ChainId.Base,
+ destinationChain: ChainId.ArbitrumOne,
+ token: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913'
+ }
+
+ const setQueryParamsResult = setQueryParams(expectedInputs)
+ expect(setQueryParamsResult).toEqual({
+ ...expectedInputs,
+ amount: '200', // 100 * 2
+ amount2: '300'
+ })
+
+ const secondSetQueryParamsResult =
+ secondSetQueryParams(setQueryParamsResult)
+ expect(secondSetQueryParamsResult).toEqual({
+ ...setQueryParamsResult,
+ amount: '200',
+ amount2: '900'
+ })
+ })
+})
diff --git a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx
index ec22f28565..5628ef99be 100644
--- a/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx
+++ b/packages/arb-token-bridge-ui/src/hooks/useArbQueryParams.tsx
@@ -13,138 +13,112 @@
`setQueryParams(newAmount)`
*/
+import { useCallback } from 'react'
import queryString from 'query-string'
-import NextAdapterPages from 'next-query-params/pages'
+import NextAdapterApp from 'next-query-params/app'
import {
BooleanParam,
+ DecodedValueMap,
+ QueryParamConfigMap,
+ QueryParamOptions,
QueryParamProvider,
+ SetQuery,
StringParam,
- decodeNumber,
- decodeString,
useQueryParams,
withDefault
} from 'use-query-params'
+import { defaultTheme } from './useTheme'
import {
- ChainKeyQueryParam,
- getChainForChainKeyQueryParam,
- getChainQueryParamForChain,
- isValidChainQueryParam
-} from '../types/ChainQueryParam'
-import { ChainId } from '../types/ChainId'
-import { defaultTheme, ThemeConfig } from './useTheme'
-
-export enum TabParamEnum {
- BRIDGE = 'bridge',
- TX_HISTORY = 'tx_history'
-}
-
-export enum DisabledFeatures {
- BATCH_TRANSFERS = 'batch-transfers',
- TX_HISTORY = 'tx-history',
- NETWORK_SELECTION = 'network-selection',
- TRANSFERS_TO_NON_ARBITRUM_CHAINS = 'transfers-to-non-arbitrum-chains'
-}
-
-export enum AmountQueryParamEnum {
- MAX = 'max'
-}
-
-export enum ModeParamEnum {
- EMBED = 'embed'
- // add other modes when we have a use case for it
-}
-
-export const tabToIndex = {
- [TabParamEnum.BRIDGE]: 0,
- [TabParamEnum.TX_HISTORY]: 1
-} as const satisfies Record