Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swaps #1138

Open
wants to merge 18 commits into
base: dev
Choose a base branch
from
Open

Swaps #1138

Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .env.local.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ NEXT_PUBLIC_URL=http://localhost:3000
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=33fe0df7cd4f445f4e2ba7e0fd6ed314
NEXT_PUBLIC_CDP_APP_ID="0000000-0000-0000-0000-000000000000"
NEXT_PUBLIC_ONCHAINKIT_API_KEY="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
NEXT_PUBLIC_KYBER_SWAP_BASE_URL=https://aggregator-api.kyberswap.com
NEXT_PUBLIC_KYBER_CLIENT_ID=SendApp
NEXT_PUBLIC_SWAP_ALLOWLIST=0000000-0000-0000-0000-000000000000,1111111-1111-1111-1111-111111111111
SECRET_SHOP_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
SEND_ACCOUNT_FACTORY_PRIVATE_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
SNAPLET_HASH_KEY=sendapp
Expand Down
25 changes: 25 additions & 0 deletions apps/next/pages/swap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SwapForm } from 'app/features/swap/SwapForm'
import { HomeLayout } from 'app/features/home/layout.web'
import Head from 'next/head'
import { userProtectedGetSSP } from 'utils/userProtected'
import type { NextPageWithLayout } from '../_app'
import { TopNav } from 'app/components/TopNav'

export const Page: NextPageWithLayout = () => {
return (
<>
<Head>
<title>Send | Swap Form</title>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for Form in the title

</Head>
<SwapForm />
</>
)
}

export const getServerSideProps = userProtectedGetSSP()

Page.getLayout = (children) => (
<HomeLayout TopNav={<TopNav header="Swap" backFunction="router" />}>{children}</HomeLayout>
)

export default Page
27 changes: 27 additions & 0 deletions apps/next/pages/swap/summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SwapSummary } from 'app/features/swap/SwapSummary'
import { HomeLayout } from 'app/features/home/layout.web'
import Head from 'next/head'
import { userProtectedGetSSP } from 'utils/userProtected'
import type { NextPageWithLayout } from '../_app'
import { TopNav } from 'app/components/TopNav'

export const Page: NextPageWithLayout = () => {
return (
<>
<Head>
<title>Send | Swap Summary</title>
</Head>
<SwapSummary />
</>
)
}

export const getServerSideProps = userProtectedGetSSP()

Page.getLayout = (children) => (
<HomeLayout TopNav={<TopNav header="Swap Summary" backFunction="router" />}>
{children}
</HomeLayout>
)

export default Page
15 changes: 15 additions & 0 deletions environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ declare global {
* Onramp Allowlist (comma separated list of user ids that can see the debit card option)
*/
NEXT_PUBLIC_ONRAMP_ALLOWLIST: string

/**
* Kyberswap aggregator API URL for base mainnet
*/
NEXT_PUBLIC_KYBER_SWAP_BASE_URL: string

/**
* Kyberswap clientId, a stricter rate limit will be applied if a clientId is not provided
*/
NEXT_PUBLIC_KYBER_CLIENT_ID: string

/**
* Swap Allowlist (comma separated list of user ids that can see the debit card option)
*/
NEXT_PUBLIC_SWAP_ALLOWLIST: string
}
}
/**
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/routers/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { secretShopRouter } from './secretShop'
import { sendAccountRouter } from './sendAccount'
import { accountRecoveryRouter } from './account-recovery/router'
import { referralsRouter } from './referrals'
import { swapRouter } from './swap/router'

export const appRouter = createTRPCRouter({
chainAddress: chainAddressRouter,
Expand All @@ -18,6 +19,7 @@ export const appRouter = createTRPCRouter({
secretShop: secretShopRouter,
sendAccount: sendAccountRouter,
referrals: referralsRouter,
swap: swapRouter,
})

export type AppRouter = typeof appRouter
Expand Down
118 changes: 118 additions & 0 deletions packages/api/src/routers/swap/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { createTRPCRouter, protectedProcedure } from '../../trpc'
import {
type KyberEncodeRouteRequest,
KyberEncodeRouteRequestSchema,
type KyberEncodeRouteResponse,
type KyberGetSwapRouteRequest,
KyberGetSwapRouteRequestSchema,
type KyberGetSwapRouteResponse,
} from './types'
import debug from 'debug'
import { baseMainnetClient, sendSwapsRevenueSafeAddress } from '@my/wagmi'
import { TRPCError } from '@trpc/server'

const log = debug('api:routers:swap')

const CHAIN = 'base'
const SWAP_FEE = '75' // 0.75% feeAmount is the percentage of fees that we will take with base unit = 10000
const KYBER_NATIVE_TOKEN_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? lol


const getHeaders = () => {
if (!process.env.NEXT_PUBLIC_KYBER_CLIENT_ID) {
return undefined
}

return { 'x-client-id': process.env.NEXT_PUBLIC_KYBER_CLIENT_ID }
}

const adjustTokenIfNeed = (token: string) => {
return token === 'eth' ? KYBER_NATIVE_TOKEN_ADDRESS : token
}

const fetchKyberSwapRoute = async ({ tokenIn, tokenOut, amountIn }: KyberGetSwapRouteRequest) => {
try {
const url = new URL(`${process.env.NEXT_PUBLIC_KYBER_SWAP_BASE_URL}/${CHAIN}/api/v1/routes`)
url.searchParams.append('tokenIn', adjustTokenIfNeed(tokenIn))
url.searchParams.append('tokenOut', adjustTokenIfNeed(tokenOut))
url.searchParams.append('amountIn', amountIn)
url.searchParams.append('feeAmount', SWAP_FEE)
url.searchParams.append('chargeFeeBy', 'currency_out')
url.searchParams.append('isInBps', 'true')
url.searchParams.append('feeReceiver', sendSwapsRevenueSafeAddress[baseMainnetClient.chain.id])

const response = (await fetch(url.toString(), {
method: 'GET',
headers: getHeaders(),
}).then((res) => res.json())) as KyberGetSwapRouteResponse

if (response.code !== 0) {
throw new Error(response.message)
}

const {
data: { routeSummary, routerAddress },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should create a zod schema

} = response

return { routeSummary, routerAddress }
} catch (error) {
log('Error calling fetchKyberSwapRoute', error)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to find swap route.',
})
}
}

const encodeKyberSwapRoute = async ({
routeSummary,
recipient,
sender,
slippageTolerance,
}: KyberEncodeRouteRequest) => {
try {
const url = `${process.env.NEXT_PUBLIC_KYBER_SWAP_BASE_URL}/${CHAIN}/api/v1/route/build`

const response = (await fetch(url, {
method: 'POST',
headers: getHeaders(),
body: JSON.stringify({
sender,
recipient,
slippageTolerance,
routeSummary,
}),
}).then((res) => res.json())) as KyberEncodeRouteResponse

if (response.code !== 0) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this code from kybers API?

throw new Error(response.message)
}

return response.data
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another place to consider a zod schema

} catch (error) {
log('Error calling encodeKyberSwapRoute', error)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to encode swap route',
})
}
}

export const swapRouter = createTRPCRouter({
fetchSwapRoute: protectedProcedure
.input(KyberGetSwapRouteRequestSchema)
.query(async ({ input: { tokenIn, tokenOut, amountIn } }) => {
log('calling fetchSwapRoute with input: ', { tokenIn, tokenOut, amountIn })
return await fetchKyberSwapRoute({ tokenIn, tokenOut, amountIn })
}),
encodeSwapRoute: protectedProcedure
.input(KyberEncodeRouteRequestSchema)
.mutation(async ({ input: { routeSummary, slippageTolerance, sender, recipient } }) => {
log('calling encodeSwapRoute with input: ', {
routeSummary,
slippageTolerance,
sender,
recipient,
})
return await encodeKyberSwapRoute({ routeSummary, slippageTolerance, sender, recipient })
}),
})
101 changes: 101 additions & 0 deletions packages/api/src/routers/swap/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { z } from 'zod'
import type { Hex } from 'viem'

export const KyberRouteSummarySchema = z.object({
tokenIn: z.string(),
amountIn: z.string(),
amountInUsd: z.string(),
tokenInMarketPriceAvailable: z.boolean(),
tokenOut: z.string(),
amountOut: z.string(),
amountOutUsd: z.string(),
tokenOutMarketPriceAvailable: z.boolean(),
gas: z.string(),
gasPrice: z.string(),
gasUsd: z.string(),
extraFee: z
.object({
feeAmount: z.string(),
chargeFeeBy: z.string(),
isInBps: z.boolean(),
feeReceiver: z.string(),
})
.optional(),
route: z.array(
z.array(
z.object({
pool: z.string().optional(),
tokenIn: z.string().optional(),
tokenOut: z.string().optional(),
limitReturnAmount: z.string().optional(),
swapAmount: z.string().optional(),
amountOut: z.string().optional(),
exchange: z.string().optional(),
poolLength: z.number().optional(),
poolType: z.string().optional(),
poolExtra: z.nullable(
z.object({
fee: z.number().optional(),
feePrecision: z.number().optional(),
blockNumber: z.number().optional(),
})
),
extra: z.nullable(z.unknown()),
})
)
),
checksum: z.string(),
timestamp: z.number(),
})
export type KyberRouteSummary = z.infer<typeof KyberRouteSummarySchema>

export const KyberGetSwapRouteRequestSchema = z.object({
tokenIn: z.string(),
tokenOut: z.string(),
amountIn: z.string(),
})

export type KyberGetSwapRouteRequest = z.infer<typeof KyberGetSwapRouteRequestSchema>

export type KyberGetSwapRouteResponse = {
code: number
message: string
data: {
routerAddress: string
routeSummary: KyberRouteSummary
}
}

export const KyberEncodeRouteRequestSchema = z.object({
sender: z.string(),
recipient: z.string(),
slippageTolerance: z.number().min(0).max(2000),
routeSummary: KyberRouteSummarySchema,
})

export type KyberEncodeRouteRequest = {
sender: string
recipient: string
slippageTolerance: number
routeSummary: KyberRouteSummary
}

export type KyberEncodeRouteResponse = {
code: number
message: string
data: {
amountIn: string
amountInUsd: string
amountOut: string
amountOutUsd: string
gas: string
gasUsd: string
data: Hex
routerAddress: Hex
outputChange: {
amount: string
percent: number
level: number
}
}
}
Loading
Loading