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

Feat swap #206

Merged
merged 10 commits into from
Apr 28, 2024
Merged
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
51 changes: 49 additions & 2 deletions web-portal/backend/src/utils/utils.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Controller, Get, UseGuards, Param, Body } from '@nestjs/common';
import { AuthGuard } from '../guards/auth.guard';
import { UtilsService } from './utils.service';

@Controller('utils')
@UseGuards(AuthGuard)
export class UtilsController {
constructor(private readonly utilsService: UtilsService) {}
constructor(private readonly utilsService: UtilsService) { }

@Get('endpoints')
async getChains() {
Expand All @@ -19,4 +19,51 @@ export class UtilsController {
const ruleTypes = await this.utilsService.getRuleTypes();
return ruleTypes;
}

@Get('price/:chainId/:tokenAddress')
async getTokenPrice(
@Param('chainId') chainId: string,
@Param('tokenAddress') tokenAddress: string,
) {
// @note: this action returns price in usd for provided token address and chain id
const price = await this.utilsService.getTokenPrice(chainId, tokenAddress);
return price;
}

@Get('tokens/:chainId')
async getTokenList(@Param('chainId') chainId: string) {
// @note: this action returns popular tokens by chainId
const tokens = await this.utilsService.getTokenList(chainId);
return tokens;
}

@Get('quote/:chainName/:sellToken/:sellAmount')
async get0xQuote(
@Param('chainName') chainName: string,
@Param('sellToken') sellToken: string,
@Param('sellAmount') sellAmount: number,
) {
// @note: this action returns quote from 0x protocol for provided sellToken, sellAmount and chainName
const quote = await this.utilsService.get0xQuote(
chainName,
sellToken,
sellAmount,
);
return quote;
}

@Get('price/:chainName/:sellToken/:sellAmount')
async get0xPrice(
@Param('chainName') chainName: string,
@Param('sellToken') sellToken: string,
@Param('sellAmount') sellAmount: number,
) {
// @note: this action returns price from 0x protocol for provided sellToken, sellAmount and chainName
const price = await this.utilsService.get0xPrice(
chainName,
sellToken,
sellAmount,
);
return price;
}
}
76 changes: 75 additions & 1 deletion web-portal/backend/src/utils/utils.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { Injectable, Inject, HttpException, HttpStatus } from '@nestjs/common';
import { CustomPrismaService } from 'nestjs-prisma';
import { PrismaClient } from '@/.generated/client';


export const portrAddress = "0x54d5f8a0e0f06991e63e46420bcee1af7d9fe944";

@Injectable()
export class UtilsService {
constructor(
Expand Down Expand Up @@ -32,7 +35,6 @@ export class UtilsService {
const ruleTypes = await this.prisma.client.ruleType.findMany({
where: {
deletedAt: null,

},
select: {
id: true,
Expand All @@ -52,4 +54,76 @@ export class UtilsService {

return ruleTypes;
}
async getTokenList(chainId: string) {
const res = await fetch(`https://tokens.1inch.io/v1.2/${chainId}`);
if (!res.ok) {
throw new HttpException(
`Could not fetch token list`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data = await res.json();
return data;
}

async getTokenPrice(chainId: string, tokenAddress: string) {
const res = await fetch(
`https://api.1inch.dev/price/v1.1/${chainId}/${tokenAddress}?currency=usd`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${process.env.ONEINCH_API_KEY!}`,
},
},
);
if (!res.ok) {
throw new HttpException(
`Could not fetch token prices`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data = await res.json();
return data;
}

async get0xQuote(chainName: string, sellToken: string, sellAmount: number) {
const res = await fetch(
`https://${chainName}.api.0x.org/swap/v1/quote?sellToken=${sellToken}&buyToken=${portrAddress}&sellAmount=${sellAmount}`,
{
headers: {
Accept: "application/json",
"0x-api-key": process.env.OX_API_KEY!,
},
},
);
if (!res.ok) {
throw new HttpException(
`Could not fetch quote`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data = await res.json();
return data;
};

async get0xPrice(chainName: string, sellToken: string, sellAmount: number) {
const res = await fetch(
`https://${chainName}.api.0x.org/swap/v1/price?sellToken=${sellToken}&buyToken=${portrAddress}&sellAmount=${sellAmount}`,
{
headers: {
Accept: "application/json",
"0x-api-key": process.env.OX_API_KEY!,
},
},
);
if (!res.ok) {
throw new HttpException(
`Could not fetch quote`,
HttpStatus.INTERNAL_SERVER_ERROR,
);
}
const data = await res.json();
return data;
};

}
2 changes: 1 addition & 1 deletion web-portal/frontend/components/billing/invoiceList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { Stack, Table, Flex, Title, Card, Button } from "@mantine/core";
import { IBill } from "@frontend/utils/types";
import { usePathname, useRouter } from "next/navigation";
import { useRouter } from "next/navigation";

import Link from "next/link";
import { billingHistoryAtom, sessionAtom } from "@frontend/utils/atoms";
Expand Down
170 changes: 167 additions & 3 deletions web-portal/frontend/components/swap/Redeem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,171 @@
import { useState } from "react";
import { Flex, Stack, Button, TextInput, Text, Select } from "@mantine/core";
import _ from "lodash";
import Image from "next/image";
import { karla } from "@frontend/utils/theme";

import { portrTokenData } from "@frontend/utils/consts";
import { chains } from "@frontend/utils/Web3Provider";
import { useTokenBalance } from "@frontend/utils/hooks";

// Common styles for TextInput and Select components
const commonStyles = {
input: {
outline: "none",
border: "none",
background: "none",
fontSize: 24,
},
label: {
color: "#000",
marginLeft: 10,
},
};

const chainOptions = _.map(chains, "name").filter(
(c) => !c.includes("Ethereum"),
);

export default function Redeem() {
const [selectedChainId, setSelectedChainId] = useState(10);

const { data: selectedTokenBalance } = useTokenBalance({
token: portrTokenData.address,
chainId: portrTokenData?.chainId,
});

const [redeemValue, setRedeemValue] = useState(0);

return (
<div>
<h1>Redeem</h1>
</div>
<Stack p={8} mt={10}>
<Select
data={chainOptions}
defaultValue={_.get(_.find(chains, { id: selectedChainId }), "name")}
onChange={(val) => {
const selectedChain = _.find(
chains,
(c) => _.toLower(c.name) === _.toLower(val as string),
);
setSelectedChainId(selectedChain?.id as number);
}}
label="Select Network"
/>

<Stack gap={4}>
<Flex
style={{
bg: "white",
borderRadius: 10,
alignItems: "flex-end",
justifyContent: "space-between",
border: "1px solid #00000010",
padding: 8,
backgroundColor: "#F6EEE6",
}}
>
<TextInput
placeholder="Enter amount"
label="Redeem"
type="number"
value={redeemValue}
onChange={(e) => setRedeemValue(parseFloat(e.target.value))}
styles={{
...commonStyles,
input: { ...commonStyles.input, fill: "#fff" },
error: { marginLeft: 10 },
}}
error={
redeemValue > Number(_.get(selectedTokenBalance, "formatted"))
? "Not enough balance"
: undefined
}
/>
<Stack>
<Button
style={{
fontFamily: karla.style.fontFamily,
borderRadius: 50,
}}
bg={"blue"}
>
<Image
src={_.get(portrTokenData, "logoURI") as string}
alt={_.get(portrTokenData, "symbol") as string}
width={24}
height={24}
style={{ marginRight: 10, borderRadius: 50 }}
/>
{_.get(portrTokenData, "symbol") as string}
</Button>
</Stack>
</Flex>
<Flex justify="space-between" dir="row" mx={10}>
<Text size="sm">
{`$` +
Number(_.get(selectedTokenBalance, "formatted") ?? 0).toFixed(6)}
</Text>
<Flex align={"center"} gap={4}>
<Text size="sm">
{Number(_.get(selectedTokenBalance, "formatted") ?? 0).toFixed(6)}
</Text>

<Text
c="blue"
size="sm"
style={{ fontWeight: "bold", cursor: "pointer" }}
onClick={() =>
setRedeemValue(
_.get(selectedTokenBalance, "formatted", 0) as number,
)
}
>
max
</Text>
</Flex>
</Flex>
</Stack>
<Flex
style={{
bg: "white",
borderRadius: 10,
alignItems: "flex-end",
justifyContent: "space-between",
border: "1px solid #00000010",
paddingLeft: 8,
paddingTop: 8,
backgroundColor: "#F6EEE6",
}}
>
<TextInput
label="Your Account ID"
styles={{
...commonStyles,
input: { ...commonStyles.input, fill: "#fff" },
}}
/>
</Flex>
<Flex
style={{
bg: "white",
borderRadius: 10,
alignItems: "flex-end",
justifyContent: "space-between",
border: "1px solid #00000010",
padding: 8,
backgroundColor: "#F6EEE6",
}}
>
<TextInput
label="New Balance"
styles={{
...commonStyles,
input: { ...commonStyles.input, fill: "#fff" },
}}
readOnly
/>
</Flex>

<Button size="lg">Redeem</Button>
</Stack>
);
}
Loading