Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
7540891
initial setup
groninge01 Oct 6, 2025
213ee43
fix providers
groninge01 Oct 6, 2025
2ef9568
add looped sonic router contract
groninge01 Oct 6, 2025
ce8af3f
enable deposit
groninge01 Oct 6, 2025
9973ff3
add loops data from api
groninge01 Oct 7, 2025
87a8a64
rename route
groninge01 Oct 7, 2025
0239204
add loops contract
groninge01 Oct 7, 2025
8acba9e
rename and convert
groninge01 Oct 7, 2025
633c89c
rename
groninge01 Oct 7, 2025
8b3592d
move component
groninge01 Oct 7, 2025
9443244
make shared
groninge01 Oct 7, 2025
a044d0f
update deposit
groninge01 Oct 7, 2025
caad69f
remove comment
groninge01 Oct 7, 2025
660e9a7
make another comp shared
groninge01 Oct 7, 2025
fdb22bc
update stats
groninge01 Oct 7, 2025
f20719d
update stats
groninge01 Oct 7, 2025
258eb50
fixes
groninge01 Oct 7, 2025
0768016
fix route
groninge01 Oct 7, 2025
3bd1130
fix width
groninge01 Oct 7, 2025
e10be49
Merge branch 'main' into feat/loops
groninge01 Oct 8, 2025
4eddf43
remove provider
groninge01 Oct 8, 2025
01afa10
fix lst faq
groninge01 Oct 8, 2025
b8c96f0
parse deposit receipt
groninge01 Oct 8, 2025
1ab1c1e
update stats
groninge01 Oct 8, 2025
19ef5e2
add tertiary value
groninge01 Oct 8, 2025
2516c3f
update stats again
groninge01 Oct 8, 2025
e8cca51
fix type
groninge01 Oct 8, 2025
2b5f7dc
fix skeleton styling
groninge01 Oct 8, 2025
9557074
initial setup withdraw
groninge01 Oct 8, 2025
dff5dc6
remove target hf again
groninge01 Oct 8, 2025
b54ab8e
add api/contract calls for withdraw WIP
groninge01 Oct 8, 2025
3418b46
add logo
groninge01 Oct 9, 2025
bfdd6e6
update label
groninge01 Oct 9, 2025
0a0ad73
setup withdraw flow WIP
groninge01 Oct 9, 2025
5959505
update withdraw flow
groninge01 Oct 9, 2025
9fd712a
remove empty import
groninge01 Oct 9, 2025
3d38844
update from/to addresses
groninge01 Oct 9, 2025
ff09878
add route for transaction
groninge01 Oct 9, 2025
13125a1
get and decode transaction data
groninge01 Oct 10, 2025
5c21d9e
use raw calldata from transaction
groninge01 Oct 10, 2025
f1ee740
remove double
groninge01 Oct 10, 2025
1786fa2
use wrapped native token
groninge01 Oct 10, 2025
b0550e1
Revert "use raw calldata from transaction"
groninge01 Oct 10, 2025
c97ee05
set spender to router
groninge01 Oct 10, 2025
90c331e
change to wrapped native token
groninge01 Oct 10, 2025
5dae719
update parser
groninge01 Oct 10, 2025
e88015f
remove console.logs
groninge01 Oct 10, 2025
f348fb2
move amounts to hook
groninge01 Oct 10, 2025
0fb24e5
add slippage & minOut
groninge01 Oct 10, 2025
5f42773
add info icon text
groninge01 Oct 13, 2025
c603a65
add faq
groninge01 Oct 13, 2025
1b23509
revert change
groninge01 Oct 13, 2025
0f3b80e
add placeholder
groninge01 Oct 13, 2025
9dc47f5
remove console.log
groninge01 Oct 13, 2025
e0dc0f4
remove use client
groninge01 Oct 13, 2025
3d7a1fc
label magic number
groninge01 Oct 13, 2025
d4479ed
fix import paths
groninge01 Oct 13, 2025
ad56674
remove 'use client' here too
groninge01 Oct 13, 2025
77f6eb8
minor fixes
groninge01 Oct 13, 2025
8eedb28
update faq
groninge01 Oct 13, 2025
f782845
remove abi
groninge01 Oct 13, 2025
6f19625
no longer needed
groninge01 Oct 14, 2025
df07215
format hf
groninge01 Oct 14, 2025
6789c1c
create shared api handler
groninge01 Oct 14, 2025
958ae08
no key needed for prod
groninge01 Oct 14, 2025
ccf7e1b
add referer check
groninge01 Oct 14, 2025
03f3c16
Merge branch 'main' into feat/loops
groninge01 Oct 16, 2025
f4a2afa
update faq
groninge01 Oct 16, 2025
3b9f47f
fix font size
groninge01 Oct 16, 2025
870dbef
add info icon
groninge01 Oct 16, 2025
e22eabb
remove example
groninge01 Oct 20, 2025
b9c029e
add audit report link
groninge01 Oct 20, 2025
f428fed
Merge branch 'main' into feat/loops
groninge01 Oct 20, 2025
d91cca4
remove references to sonic points (multiplier)
groninge01 Oct 21, 2025
4f7b142
update name & link for audit
groninge01 Oct 21, 2025
50b9a6b
add fee percentage
groninge01 Oct 23, 2025
bb5736f
only show loops on dev or staging
groninge01 Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/beets-frontend-v3/app/(app)/loops/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { DefaultPageContainer } from '@repo/lib/shared/components/containers/DefaultPageContainer'
import { PropsWithChildren } from 'react'

export default function LoopsLayout({ children }: PropsWithChildren) {
return <DefaultPageContainer minH="100vh">{children}</DefaultPageContainer>
}
10 changes: 10 additions & 0 deletions apps/beets-frontend-v3/app/(app)/loops/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Loops } from '@/lib/modules/loops/Loops'
import LoopsProvidersLayout from '@/lib/modules/loops/LoopsProvidersLayout'

export default function LoopsPage() {
return (
<LoopsProvidersLayout>
<Loops />
</LoopsProvidersLayout>
)
}
59 changes: 59 additions & 0 deletions apps/beets-frontend-v3/app/api/fly/quote/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { createFlyGetHandler } from '../shared'

const FLY_API_URL = 'https://api.magpiefi.xyz/aggregator/quote'

export type FlyQuoteApiRequest = {
fromTokenAddress: string
toTokenAddress: string
sellAmount: string
slippage: string
fromAddress: string
toAddress: string
gasless: string
network: string
}

export type FlyQuoteApiResponse = {
id: string
amountOut: string
targetAddress: string
fees: Array<{
type: string
value: string
}>
resourceEstimate: {
gasLimit: string
}
typedData: {
types: {
Swap: Array<{
name: string
type: string
}>
}
domain: {
name: string
version: string
chainId: string
verifyingContract: string
}
message: {
router: string
sender: string
recipient: string
fromAsset: string
toAsset: string
deadline: string
amountOutMin: string
swapFee: string
amountIn: string
}
}
}

export const GET = createFlyGetHandler<FlyQuoteApiResponse>({
endpoint: FLY_API_URL,
invalidResponseMessage: 'Invalid quote response',
failureResponseMessage: 'Failed to fetch quote',
logContext: 'Unable to fetch quote from Magpie API',
})
86 changes: 86 additions & 0 deletions apps/beets-frontend-v3/app/api/fly/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type NextRequest, NextResponse } from 'next/server'
import { isProd } from '@repo/lib/config/app.config'
import { mins } from '@repo/lib/shared/utils/time'
import { isAllowedReferer } from '../shared/referer'

type FlyApiErrorResponse = {
error: string
message?: string
details?: unknown
code?: number
}

type CreateFlyGetHandlerOptions = {
endpoint: string
invalidResponseMessage: string
failureResponseMessage: string
logContext: string
}

function hasError(response: unknown): response is { error?: unknown } {
return typeof response === 'object' && response !== null && 'error' in response
}

export function createFlyGetHandler<T>({
endpoint,
invalidResponseMessage,
failureResponseMessage,
logContext,
}: CreateFlyGetHandlerOptions) {
return async function handle(
request: NextRequest
): Promise<NextResponse<T | FlyApiErrorResponse>> {
try {
if (!isAllowedReferer(request.headers.get('referer'))) {
return NextResponse.json(
{
error: 'Forbidden: Access denied',
code: -32000,
message: 'Access denied',
},
{ status: 403 }
)
}

const apiKey = process.env.NEXT_PRIVATE_MAGPIE_API_KEY

if (!isProd && !apiKey) {
return NextResponse.json(
{ error: 'NEXT_PRIVATE_MAGPIE_API_KEY is not configured' },
{ status: 500 }
)
}

const searchParams = request.nextUrl.searchParams

const res = await fetch(`${endpoint}?${searchParams.toString()}`, {
headers: apiKey ? { apikey: apiKey } : undefined,
next: { revalidate: mins(1).toSecs() },
})

if (!res.ok) {
throw new Error(`Magpie API returned ${res.status}: ${res.statusText}`)
}

const result = (await res.json()) as T | { error?: unknown }

if (hasError(result) && result.error) {
return NextResponse.json(
{ error: invalidResponseMessage, details: result },
{ status: 400 }
)
}

return NextResponse.json(result as T)
} catch (error) {
console.error(`${logContext}:`, error)
return NextResponse.json(
{
error: failureResponseMessage,
message: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
}
28 changes: 28 additions & 0 deletions apps/beets-frontend-v3/app/api/fly/transaction/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Address } from 'viem'
import { createFlyGetHandler } from '../shared'

const FLY_API_URL = 'https://api.magpiefi.xyz/aggregator/transaction'

export type FlyTransactionApiRequest = {
quoteId: string | undefined
estimateGas: string
}

export type FlyTransactionApiResponse = {
from: Address
to: Address
data: `0x${string}`
chainId: number
type: number
gasLimit: string
maxFeePerGas: string
maxPriorityFeePerGas: string
value: string
}

export const GET = createFlyGetHandler<FlyTransactionApiResponse>({
endpoint: FLY_API_URL,
invalidResponseMessage: 'Invalid transaction response',
failureResponseMessage: 'Failed to fetch transaction',
logContext: 'Unable to fetch transaction from Magpie API',
})
8 changes: 2 additions & 6 deletions apps/beets-frontend-v3/app/api/rpc/[chain]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { drpcUrl } from '@repo/lib/shared/utils/rpc'
import type { NextRequest } from 'next/server'
import { isAllowedReferer } from '../../shared/referer'

type Params = {
params: Promise<{
Expand All @@ -10,16 +11,11 @@ type Params = {

const DRPC_KEY = process.env.NEXT_PRIVATE_DRPC_KEY || ''

const ALLOWED_ORIGINS = [
...(process.env.NEXT_PRIVATE_ALLOWED_ORIGINS || '').split(','),
process.env.VERCEL_BRANCH_URL ? `https://${process.env.VERCEL_BRANCH_URL}` : '',
].filter(Boolean)

export async function POST(request: NextRequest, props: Params) {
const params = await props.params
const { chain } = params
const referer = request.headers.get('referer')
const isAllowedOrigin = referer && ALLOWED_ORIGINS.some(origin => referer.startsWith(origin))
const isAllowedOrigin = isAllowedReferer(referer)

if (!isAllowedOrigin) {
return new Response(
Expand Down
10 changes: 10 additions & 0 deletions apps/beets-frontend-v3/app/api/shared/referer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const allowedOrigins = [
...(process.env.NEXT_PRIVATE_ALLOWED_ORIGINS || '').split(','),
process.env.VERCEL_BRANCH_URL ? `https://${process.env.VERCEL_BRANCH_URL}` : '',
].filter(Boolean)

export function isAllowedReferer(referer: string | null): boolean {
return !!(referer && allowedOrigins.some(origin => referer.startsWith(origin)))
}

export { allowedOrigins as ALLOWED_ORIGINS }
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import { useNav } from '@repo/lib/shared/components/navs/useNav'
import { BeetsLogoType } from '../imgs/BeetsLogoType'
import { AnimatePresence, motion } from 'framer-motion'
import { PROJECT_CONFIG } from '@repo/lib/config/getProjectConfig'
import { isDev, isStaging } from '@repo/lib/config/app.config'

export function NavBarContainer() {
const {
links: { appLinks, ecosystemLinks, socialLinks },
options: { allowCreateWallet },
} = PROJECT_CONFIG
const { defaultAppLinks } = useNav()
const allAppLinks = [...defaultAppLinks, ...appLinks]

const allAppLinks = [
...defaultAppLinks,
...appLinks,
// TODO: remove here when loops goes live
...(isDev || isStaging ? [{ href: '/loops', label: 'Loop $S' }] : []),
]

const mobileNav = (
<MobileNav
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import TokenRow from '@repo/lib/modules/tokens/TokenRow/TokenRow'
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { Address } from 'viem'

export function LstTokenRow({
export function BeetsTokenRow({
label,
chain,
tokenAmount,
Expand Down
50 changes: 50 additions & 0 deletions apps/beets-frontend-v3/lib/components/shared/StatRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Box, Text, HStack, Skeleton } from '@chakra-ui/react'

export function StatRow({
label,
value,
secondaryValue,
tertiaryValue,
isLoading,
}: {
label: string
value: string
secondaryValue?: string
tertiaryValue?: string
isLoading?: boolean
}) {
return (
<HStack align="flex-start" justify="space-between" w="full">
<Text color="font.secondary">{label}</Text>
<Box alignItems="flex-end" display="flex" flexDirection="column">
{isLoading ? (
<Skeleton h="16px" my="2px" w="80px" />
) : (
<Text fontWeight="bold">{value}</Text>
)}
{secondaryValue && (
<>
{isLoading ? (
<Skeleton h="16px" my="2px" w="80px" />
) : (
<Text color="grayText" fontSize="sm">
{secondaryValue}
</Text>
)}
</>
)}
{tertiaryValue && (
<>
{isLoading ? (
<Skeleton h="16px" my="2px" w="80px" />
) : (
<Text color="grayText" fontSize="sm">
{tertiaryValue}
</Text>
)}
</>
)}
</Box>
</HStack>
)
}
63 changes: 63 additions & 0 deletions apps/beets-frontend-v3/lib/components/shared/YouWillReceive.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Box, Flex, Text, Popover, PopoverContent, PopoverTrigger, HStack } from '@chakra-ui/react'
import { TokenIcon } from '@repo/lib/modules/tokens/TokenIcon'
import FadeInOnView from '@repo/lib/shared/components/containers/FadeInOnView'
import { GqlChain } from '@repo/lib/shared/services/api/generated/graphql'
import { fNumCustom } from '@repo/lib/shared/utils/numbers'
import { InfoIcon } from '@repo/lib/shared/components/icons/InfoIcon'

type Props = {
label: string
amount: string
address: string
symbol: string
chain: GqlChain
infoText?: string
}

export function YouWillReceive({ label, amount, address, symbol, chain, infoText }: Props) {
const amountFormatted = fNumCustom(amount, '0,0.[000000]')

return (
<Box w="full">
<FadeInOnView>
<Flex alignItems="flex-end" w="full">
<Box flex="1">
{infoText ? (
<HStack alignItems="center" mb="sm">
<Text color="grayText" mb="0">
{label}
</Text>
<Popover placement="right" trigger="hover">
<PopoverTrigger>
<Box
_hover={{ opacity: 1 }}
opacity="0.5"
transition="opacity 0.2s var(--ease-out-cubic)"
>
<InfoIcon />
</Box>
</PopoverTrigger>
<PopoverContent p="sm">
<Text fontSize="sm" variant="secondary">
{infoText}
</Text>
</PopoverContent>
</Popover>
</HStack>
) : (
<Text color="grayText" mb="sm">
{label}
</Text>
)}
<Text fontSize="3xl">
{amountFormatted === 'NaN' ? amount : amountFormatted} {symbol}
</Text>
</Box>
<Box>
<TokenIcon address={address} alt={symbol} chain={chain} size={40} />
</Box>
</Flex>
</FadeInOnView>
</Box>
)
}
Loading
Loading