diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..91dfed8d4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+node_modules
\ No newline at end of file
diff --git a/readme.md b/readme.md
index 1ff4bc95b..cd1555a7b 100644
--- a/readme.md
+++ b/readme.md
@@ -8,3 +8,22 @@ It is important that you minimally attempt the problems, even if you do not arri
## Submission ##
You can either provide a link to an online repository, attach the solution in your application, or whichever method you prefer.
We're cool as long as we can view your solution without any pain.
+
+## Project Structure ##
+
+```
+code-challenge/
+├── src/
+│ ├── problem1/ # Sum to N implementations
+│ ├── problem2/ # Token Exchange Application
+│ └── problem3/ # Wallet Page refactoring
+└── README.md
+```
+
+## Setup ##
+
+```bash
+cd src/problem2
+npm install
+npm run dev
+```
diff --git a/src/problem1/.keep b/src/problem1/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/problem1/index.js b/src/problem1/index.js
new file mode 100644
index 000000000..4d93e713c
--- /dev/null
+++ b/src/problem1/index.js
@@ -0,0 +1,21 @@
+var sum_to_n_a = function (n) {
+ // your code here
+ if (n <= 0) return 0;
+ let result = 0;
+ for (let i = 0; i <= n; i++) {
+ result += i;
+ }
+ return result;
+};
+
+var sum_to_n_b = function (n) {
+ // your code here
+ if (n <= 0) return 0;
+ return (n * (n + 1)) / 2;
+};
+
+var sum_to_n_c = function (n) {
+ // your code here
+ if (n <= 0) return 0;
+ return n + sum_to_n_c(n - 1);
+};
diff --git a/src/problem2/index.html b/src/problem2/index.html
index 4058a68bf..5eb6054c1 100644
--- a/src/problem2/index.html
+++ b/src/problem2/index.html
@@ -1,27 +1,16 @@
-
-
-
-
+
!disabled && setOpen((o) => !o)}
+ disabled={disabled}
+ aria-expanded={open}
+ aria-haspopup="listbox"
+ aria-label="Select token"
+ >
+ {selectedToken ? (
+ <>
+
+ {selectedToken.currency}
+
+
+
+ >
+ ) : (
+ Select
+ )}
+
+
+ {open && (
+ <>
+
setOpen(false)}
+ />
+
+
+ setSearch(e.target.value)}
+ className="token-selector-search-input"
+ autoFocus
+ aria-label="Search token"
+ />
+
+
+ {filtered.length === 0 ? (
+ No token found
+ ) : (
+ filtered.map((token) => (
+
+ {
+ onChange(token.currency)
+ setOpen(false)
+ }}
+ role="option"
+ aria-selected={token.currency === value}
+ data-selected={token.currency === value || undefined}
+ >
+
+ {token.currency}
+
+ ${token.price < 0.01 ? token.price.toExponential(2) : token.price.toFixed(2)}
+
+
+
+ ))
+ )}
+
+
+ >
+ )}
+
+ )
+}
diff --git a/src/problem2/src/constants/index.js b/src/problem2/src/constants/index.js
new file mode 100644
index 000000000..0dc928c57
--- /dev/null
+++ b/src/problem2/src/constants/index.js
@@ -0,0 +1,9 @@
+// API
+export const PRICES_URL = 'https://interview.switcheo.com/prices.json'
+export const TOKEN_ICON_BASE =
+ 'https://raw.githubusercontent.com/Switcheo/token-icons/main/tokens'
+
+// DEFAULT VALUES
+export const SUBMIT_DELAY_MS = 2200
+export const PREFERRED_FROM_TOKEN = 'ETH'
+export const PREFERRED_TO_TOKEN = 'USD'
diff --git a/src/problem2/src/hooks/useTokenSwap.js b/src/problem2/src/hooks/useTokenSwap.js
new file mode 100644
index 000000000..a777a416b
--- /dev/null
+++ b/src/problem2/src/hooks/useTokenSwap.js
@@ -0,0 +1,132 @@
+import { useState, useEffect, useMemo, useCallback } from 'react'
+import { fetchTokenPrices } from '../api/prices'
+import { getExchangeRate, formatToAmount } from '../util/swap'
+import {
+ SUBMIT_DELAY_MS,
+ PREFERRED_FROM_TOKEN,
+ PREFERRED_TO_TOKEN,
+} from '../constants'
+
+export function useTokenSwap(options = {}) {
+ const {
+ submitDelayMs = SUBMIT_DELAY_MS,
+ preferredFrom = PREFERRED_FROM_TOKEN,
+ preferredTo = PREFERRED_TO_TOKEN,
+ } = options
+
+ const [tokens, setTokens] = useState([])
+ const [loadingPrices, setLoadingPrices] = useState(true)
+ const [priceError, setPriceError] = useState(null)
+
+ const [fromToken, setFromToken] = useState('')
+ const [toToken, setToToken] = useState('')
+ const [fromAmount, setFromAmount] = useState('')
+ const [fieldError, setFieldError] = useState({ from: null, to: null })
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [submitSuccess, setSubmitSuccess] = useState(false)
+
+ useEffect(() => {
+ let cancelled = false
+ fetchTokenPrices()
+ .then((list) => {
+ if (!cancelled) {
+ setTokens(list)
+ if (list.length >= 2) {
+ const from = list.find((t) => t.currency === preferredFrom) ?? list[0]
+ const to = list.find((t) => t.currency === preferredTo) ?? list[1]
+ setFromToken(from.currency)
+ setToToken(to.currency)
+ }
+ }
+ })
+ .catch((err) => {
+ if (!cancelled) setPriceError(err.message || 'Failed to load prices')
+ })
+ .finally(() => {
+ if (!cancelled) setLoadingPrices(false)
+ })
+ return () => { cancelled = true }
+ }, [preferredFrom, preferredTo])
+
+ const exchangeRate = useMemo(
+ () => (fromToken && toToken ? getExchangeRate(tokens, fromToken, toToken) : null),
+ [tokens, fromToken, toToken]
+ )
+
+ const toAmount = useMemo(() => {
+ if (!exchangeRate || fromAmount === '' || fromAmount === '.') return ''
+ const num = parseFloat(fromAmount)
+ if (Number.isNaN(num)) return ''
+ return formatToAmount(num * exchangeRate)
+ }, [fromAmount, exchangeRate])
+
+ const setFromAmountWithReset = useCallback((value) => {
+ setFromAmount(value)
+ setFieldError((e) => ({ ...e, from: null }))
+ setSubmitSuccess(false)
+ }, [])
+
+ const swapDirection = useCallback(() => {
+ setFromToken(toToken)
+ setToToken(fromToken)
+ setFromAmount('')
+ setFieldError({ from: null, to: null })
+ setSubmitSuccess(false)
+ }, [fromToken, toToken])
+
+ const validate = useCallback(() => {
+ const num = parseFloat(fromAmount)
+ const fromErr =
+ fromAmount === ''
+ ? 'Enter an amount'
+ : Number.isNaN(num)
+ ? 'Enter a valid number'
+ : num <= 0
+ ? 'Amount must be greater than 0'
+ : null
+ const toErr = fromToken === toToken ? 'Choose two different tokens' : null
+ setFieldError({ from: fromErr, to: toErr })
+ return !fromErr && !toErr
+ }, [fromAmount, fromToken, toToken])
+
+ const submit = useCallback(
+ (e) => {
+ e?.preventDefault()
+ if (!validate()) return
+ setIsSubmitting(true)
+ setSubmitSuccess(false)
+ setTimeout(() => {
+ setIsSubmitting(false)
+ setSubmitSuccess(true)
+ }, submitDelayMs)
+ },
+ [validate, submitDelayMs]
+ )
+
+ return {
+ // Data
+ tokens,
+ fromToken,
+ toToken,
+ fromAmount,
+ toAmount,
+ exchangeRate,
+
+ // Loading & errors
+ loadingPrices,
+ priceError,
+ fieldError,
+ isSubmitting,
+ submitSuccess,
+
+ // Setters
+ setFromToken,
+ setToToken,
+ setFromAmount: setFromAmountWithReset,
+
+ // Actions
+ swapDirection,
+ validate,
+ submit,
+ }
+}
diff --git a/src/problem2/src/index.css b/src/problem2/src/index.css
new file mode 100644
index 000000000..b5522a356
--- /dev/null
+++ b/src/problem2/src/index.css
@@ -0,0 +1,67 @@
+:root {
+ --bg-deep: #0a0b0f;
+ --bg-card: #12141c;
+ --bg-elevated: #1a1d28;
+ --border: #2a2e3d;
+ --border-focus: #6366f1;
+ --text: #f1f5f9;
+ --text-muted: #94a3b8;
+ --text-dim: #64748b;
+ --accent: #6366f1;
+ --accent-hover: #818cf8;
+ --success: #22c55e;
+ --error: #ef4444;
+ --radius: 16px;
+ --radius-sm: 10px;
+ --shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+ --font-sans: 'DM Sans', system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', monospace;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: var(--font-sans);
+ background: var(--bg-deep);
+ color: var(--text);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+#root {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+
+button {
+ font-family: inherit;
+ cursor: pointer;
+}
+
+input {
+ font-family: inherit;
+}
+
+input::placeholder {
+ color: var(--text-dim);
+}
+
+@media (max-width: 720px) {
+ #root {
+ padding: 16px;
+ }
+}
+
+@media (max-width: 480px) {
+ #root {
+ padding: 12px;
+ align-items: flex-start;
+ }
+}
diff --git a/src/problem2/src/main.jsx b/src/problem2/src/main.jsx
new file mode 100644
index 000000000..5cc599199
--- /dev/null
+++ b/src/problem2/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './index.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/src/problem2/src/util/swap.js b/src/problem2/src/util/swap.js
new file mode 100644
index 000000000..ab52385bd
--- /dev/null
+++ b/src/problem2/src/util/swap.js
@@ -0,0 +1,11 @@
+export function getExchangeRate(tokens, fromCurrency, toCurrency) {
+ const from = tokens.find((t) => t.currency === fromCurrency)
+ const to = tokens.find((t) => t.currency === toCurrency)
+ if (!from || !to) return null
+ return from.price && to.price ? to.price / from.price : null
+}
+
+export function formatToAmount(value) {
+ if (value < 1e-12) return '0'
+ return value.toFixed(8).replace(/\.?0+$/, '')
+}
diff --git a/src/problem2/style.css b/src/problem2/style.css
deleted file mode 100644
index 915af91c7..000000000
--- a/src/problem2/style.css
+++ /dev/null
@@ -1,8 +0,0 @@
-body {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- min-width: 360px;
- font-family: Arial, Helvetica, sans-serif;
-}
diff --git a/src/problem2/vite.config.js b/src/problem2/vite.config.js
new file mode 100644
index 000000000..7727ce9a7
--- /dev/null
+++ b/src/problem2/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import path from 'path'
+import { fileURLToPath } from 'url'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+export default defineConfig({
+ plugins: [react()],
+ root: __dirname,
+})
diff --git a/src/problem3/.keep b/src/problem3/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/problem3/problem3.tsx b/src/problem3/problem3.tsx
new file mode 100644
index 000000000..afee6adee
--- /dev/null
+++ b/src/problem3/problem3.tsx
@@ -0,0 +1,85 @@
+interface WalletBalance {
+ currency: string;
+ amount: number;
+ // 1. missing blockchain propperty
+}
+interface FormattedWalletBalance {
+ currency: string;
+ amount: number;
+ formatted: string;
+}
+
+interface Props extends BoxProps {}
+const WalletPage: React.FC
= (props: Props) => {
+ const { children, ...rest } = props;
+ const balances = useWalletBalances();
+ const prices = usePrices();
+
+ // 2. use any type - not safety type
+ const getPriority = (blockchain: any): number => {
+ switch (blockchain) {
+ case "Osmosis":
+ return 100;
+ case "Ethereum":
+ return 50;
+ case "Arbitrum":
+ return 30;
+ case "Zilliqa":
+ return 20;
+ case "Neo":
+ return 20;
+ default:
+ return -99;
+ }
+ };
+
+ const sortedBalances = useMemo(() => {
+ return balances
+ .filter((balance: WalletBalance) => {
+ const balancePriority = getPriority(balance.blockchain);
+ if (lhsPriority > -99) {
+ // 3. defined balancePriority but used lhsPriority
+ if (balance.amount <= 0) {
+ // 4. keeps zero or negative balances, should be positive value
+ return true;
+ }
+ }
+ return false;
+ })
+ .sort((lhs: WalletBalance, rhs: WalletBalance) => {
+ const leftPriority = getPriority(lhs.blockchain);
+ const rightPriority = getPriority(rhs.blockchain);
+ if (leftPriority > rightPriority) {
+ return -1;
+ } else if (rightPriority > leftPriority) {
+ return 1;
+ }
+ // 5. if priorities are equal then returns undefined
+ });
+ }, [balances, prices]); // 6. dependency price is not used
+
+ // 7.formattedBalances declare but not used
+ const formattedBalances = sortedBalances.map((balance: WalletBalance) => {
+ return {
+ ...balance,
+ formatted: balance.amount.toFixed(),
+ };
+ });
+
+ const rows = sortedBalances.map(
+ (balance: FormattedWalletBalance, index: number) => {
+ const usdValue = prices[balance.currency] * balance.amount; // 8. prices[balance.currency] may be undefined
+ return (
+
+ );
+ }
+ );
+
+ return {rows}
;
+};
diff --git a/src/problem3/refactored.tsx b/src/problem3/refactored.tsx
new file mode 100644
index 000000000..3118a05bd
--- /dev/null
+++ b/src/problem3/refactored.tsx
@@ -0,0 +1,102 @@
+import React, { useMemo } from "react";
+
+// Fix: add Blockchain defination type
+type Blockchain = "Osmosis" | "Ethereum" | "Arbitrum" | "Zilliqa" | "Neo";
+
+interface WalletBalance {
+ currency: string;
+ amount: number;
+ blockchain: Blockchain; // Fix: add missing blockchain property
+}
+
+interface FormattedWalletBalance extends WalletBalance {
+ formatted: string;
+}
+
+interface WalletPageProps {
+ children?: React.ReactNode;
+ className?: string;
+}
+
+// Fix: use Map instead of switch statement
+const PRIORITY_MAP: Record = {
+ Osmosis: 100,
+ Ethereum: 50,
+ Arbitrum: 30,
+ Zilliqa: 20,
+ Neo: 20,
+};
+
+const WalletRow: React.FC<{
+ amount: number;
+ usdValue: number;
+ formattedAmount: string;
+}> = ({ formattedAmount, usdValue }) => (
+
+ {formattedAmount}
+ ${usdValue.toFixed(2)}
+
+);
+
+const WalletPage: React.FC = ({
+ children,
+ className,
+ ...rest
+}) => {
+ const balances = useWalletBalances();
+ const prices = usePrices();
+
+ const formattedBalances = useMemo(() => {
+ return (
+ balances
+ // Fix: keep positive amounts with valid priority
+ .filter(
+ (balance: WalletBalance) =>
+ balance.amount > 0 && PRIORITY_MAP[balance.blockchain] !== undefined
+ )
+
+ // Fix: correct condition sort
+ .sort((lhs: WalletBalance, rhs: WalletBalance) => {
+ const leftPriority = PRIORITY_MAP[lhs.blockchain] ?? -99;
+ const rightPriority = PRIORITY_MAP[rhs.blockchain] ?? -99;
+
+ if (leftPriority !== rightPriority) {
+ return rightPriority - leftPriority;
+ }
+
+ return rhs.amount - lhs.amount;
+ })
+ // Fix: redundant iteration, combine map with formatted
+ .map((balance: FormattedWalletBalance) => ({
+ ...balance,
+ formatted: balance.amount.toFixed(2),
+ }))
+ );
+ }, [balances]); // Fix: dependency prices is not used
+
+ // Fix: Memoize rows and use unique keys, not use index
+ const rows = useMemo(() => {
+ return formattedBalances.map((balance: FormattedWalletBalance) => {
+ // Fix: prices[balance.currency] may be undefined
+ const price = prices[balance.currency] ?? 0;
+ const usdValue = price * balance.amount;
+
+ return (
+
+ );
+ });
+ }, [formattedBalances, prices]);
+
+ return (
+
+ {children}
+ {rows}
+
+ );
+};
diff --git a/src/problem4/.keep b/src/problem4/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/src/problem5/.keep b/src/problem5/.keep
deleted file mode 100644
index e69de29bb..000000000