From f4aef32b5c68572713b5d958b8fb9bf3b6ebbd49 Mon Sep 17 00:00:00 2001 From: Abdulhakeem Abdulazeez Date: Sun, 6 Oct 2024 09:03:08 +0100 Subject: [PATCH 1/2] feat: add swap ui --- .../mobile/src/components/TokenSwap/index.tsx | 106 ++++++++++++++ .../mobile/src/components/TokenSwap/styles.ts | 61 ++++++++ apps/mobile/src/hooks/api/useAvnu.ts | 134 ++++++++++++++++++ apps/mobile/src/screens/Defi/index.tsx | 38 ++--- apps/mobile/src/types/tab.ts | 11 +- 5 files changed, 328 insertions(+), 22 deletions(-) create mode 100644 apps/mobile/src/components/TokenSwap/index.tsx create mode 100644 apps/mobile/src/components/TokenSwap/styles.ts create mode 100644 apps/mobile/src/hooks/api/useAvnu.ts diff --git a/apps/mobile/src/components/TokenSwap/index.tsx b/apps/mobile/src/components/TokenSwap/index.tsx new file mode 100644 index 00000000..cfb1f40a --- /dev/null +++ b/apps/mobile/src/components/TokenSwap/index.tsx @@ -0,0 +1,106 @@ +import {useState} from 'react'; +import {StyleProp, TextInput, TextStyle, View, ViewStyle} from 'react-native'; + +import {TokenSymbol} from '../../constants/tokens'; +import {useStyles, useTheme} from '../../hooks'; +import {Button} from '../Button'; +import {Picker} from '../Picker'; +import stylesheet from './styles'; + +export type TokenSwapProps = { + /** + * Error message to be displayed. + * If this prop is not provided or is undefined, no error message will be displayed. + */ + onPress: () => void; + error?: string; + + left?: React.ReactNode; + right?: React.ReactNode; + + style?: StyleProp; + containerStyle?: StyleProp; + inputStyle?: StyleProp; +}; + +export const TokenSwap: React.FC = (props) => { + const { + onPress, + error, + left, + right, + style: styleProp, + containerStyle: containerStyleProp, + inputStyle: inputStyleProp, + ...TokenSwapProps + } = props; + + const {theme} = useTheme(); + const styles = useStyles(stylesheet, !!error, !!left, !!right); + const [token, setToken] = useState(TokenSymbol.ETH); + + const [amount, setAmount] = useState(''); + const handleChangeAmount = (value: string) => { + setAmount(value); + }; + + return ( + + + setToken(itemValue as TokenSymbol)} + > + {/* {Object.values(tokensIns).map((tkn) => ( + + ))} */} + + + + + setToken(itemValue as TokenSymbol)} + > + {/* {Object.values(tokensIns).map((tkn) => ( + + ))} */} + + + + + {/* Amount received in {tokenOut[CHAIN_ID].symbol}: + 0 */} + + + + ); +}; diff --git a/apps/mobile/src/components/TokenSwap/styles.ts b/apps/mobile/src/components/TokenSwap/styles.ts new file mode 100644 index 00000000..47fc0151 --- /dev/null +++ b/apps/mobile/src/components/TokenSwap/styles.ts @@ -0,0 +1,61 @@ +import {Spacing, ThemedStyleSheet, Typography} from '../../styles'; + +export default ThemedStyleSheet((theme, error: boolean, left: boolean, right: boolean) => ({ + container: { + width: '100%', + maxWidth: 500, + padding: Spacing.medium, + borderRadius: 10, + backgroundColor: theme.colors.surface, + }, + content: { + flex: 1, + borderRadius: 999, + flexDirection: 'row', + alignItems: 'center', + gap: Spacing.small, + backgroundColor: theme.colors.transparent, + height: 56, + marginBottom: Spacing.medium, + + ...(error && { + backgroundColor: theme.colors.errorLight, + borderColor: theme.colors.errorDark, + }), + + ...(left && { + paddingLeft: Spacing.small, + }), + + ...(right && { + paddingRight: Spacing.small, + }), + }, + + input: { + borderWidth: 1, + borderRadius: 999, + borderColor: theme.colors.inputBorder, + flex: 1, + height: '100%', + paddingHorizontal: Spacing.large, + paddingVertical: Spacing.large, + color: theme.colors.inputText, + backgroundColor: theme.colors.inputBackground, + fontSize: 15, + ...Typography.semiBold, + + ...(left && { + paddingLeft: Spacing.none, + }), + + ...(right && { + paddingRight: Spacing.none, + }), + }, + + errorText: { + marginTop: 3, + color: theme.colors.errorDark, + }, +})); diff --git a/apps/mobile/src/hooks/api/useAvnu.ts b/apps/mobile/src/hooks/api/useAvnu.ts new file mode 100644 index 00000000..1b736ac6 --- /dev/null +++ b/apps/mobile/src/hooks/api/useAvnu.ts @@ -0,0 +1,134 @@ +import {fetchBuildExecuteTransaction, fetchQuotes} from '@avnu/avnu-sdk'; +import {NextRequest, NextResponse} from 'next/server'; +import {Calldata} from 'starknet'; + +import {ESCROW_ADDRESSES, ETH_ADDRESSES, STRK_ADDRESSES} from '@/constants/contracts'; +import {AVNU_URL, CHAIN_ID, Entrypoint} from '@/constants/misc'; +import {account} from '@/services/account'; +import {ErrorCode} from '@/utils/errors'; +import {HTTPStatus} from '@/utils/http'; +import {ClaimSchema} from '@/utils/validation'; + +import {getClaimCallData} from '../../../../website/src/app/api/deposit/calldata'; + +export async function POST(request: NextRequest) { + const requestBody = await request.json(); + + const body = ClaimSchema.safeParse(requestBody); + if (!body.success) { + return NextResponse.json( + {code: ErrorCode.BAD_REQUEST, error: body.error}, + {status: HTTPStatus.BadRequest}, + ); + } + + let claimCallData: Calldata; + let gasTokenAddress: string; + try { + const {calldata, tokenAddress} = await getClaimCallData(body.data); + claimCallData = calldata; + gasTokenAddress = tokenAddress; + } catch (error) { + if (error instanceof Error) { + return NextResponse.json({code: error.message}, {status: HTTPStatus.BadRequest}); + } + + throw error; + } + + try { + if ( + gasTokenAddress === ETH_ADDRESSES[CHAIN_ID] || + gasTokenAddress === STRK_ADDRESSES[CHAIN_ID] + ) { + // ETH | STRK fee estimation + + const result = await account.estimateInvokeFee( + [ + { + contractAddress: ESCROW_ADDRESSES[CHAIN_ID], + entrypoint: Entrypoint.CLAIM, + calldata: claimCallData, + }, + ], + { + version: gasTokenAddress === ETH_ADDRESSES[CHAIN_ID] ? 1 : 3, + }, + ); + + // Using 1.1 as a multiplier to ensure the fee is enough + const fee = ((result.overall_fee * BigInt(11)) / BigInt(10)).toString(); + + return NextResponse.json({gasFee: fee, tokenFee: fee}, {status: HTTPStatus.OK}); + } else { + // ERC20 fee estimation + + const quotes = await fetchQuotes( + { + sellTokenAddress: ETH_ADDRESSES[CHAIN_ID], + buyTokenAddress: gasTokenAddress, + sellAmount: BigInt(1), + takerAddress: account.address, + }, + {baseUrl: AVNU_URL}, + ); + const quote = quotes[0]; + + if (!quote) { + return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest}); + } + + const {calls: swapCalls} = await fetchBuildExecuteTransaction( + quote.quoteId, + account.address, + undefined, + undefined, + {baseUrl: AVNU_URL}, + ); + + const result = await account.estimateInvokeFee( + [ + { + contractAddress: ESCROW_ADDRESSES[CHAIN_ID], + entrypoint: Entrypoint.CLAIM, + calldata: claimCallData, + }, + ...swapCalls, + ], + { + version: 1, + }, + ); + + // Using 1.1 as a multiplier to ensure the fee is enough + const ethFee = (result.overall_fee * BigInt(11)) / BigInt(10); + + const feeQuotes = await fetchQuotes( + { + sellTokenAddress: ETH_ADDRESSES[CHAIN_ID], + buyTokenAddress: gasTokenAddress, + sellAmount: ethFee, + takerAddress: account.address, + }, + {baseUrl: AVNU_URL}, + ); + const feeQuote = feeQuotes[0]; + + if (!feeQuote) { + return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest}); + } + + return NextResponse.json( + {gasFee: ethFee, tokenFee: feeQuote.buyAmount}, + {status: HTTPStatus.OK}, + ); + } + } catch (error) { + console.error(error); + + return NextResponse.json( + {code: ErrorCode.ESTIMATION_ERROR, error}, + {status: HTTPStatus.InternalServerError}, + ); + } +} diff --git a/apps/mobile/src/screens/Defi/index.tsx b/apps/mobile/src/screens/Defi/index.tsx index 834d7909..85efb603 100644 --- a/apps/mobile/src/screens/Defi/index.tsx +++ b/apps/mobile/src/screens/Defi/index.tsx @@ -1,19 +1,20 @@ -import { useState } from 'react'; -import { KeyboardAvoidingView, ScrollView, Text, View } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import {useState} from 'react'; +import {KeyboardAvoidingView, ScrollView, Text, View} from 'react-native'; +import {SafeAreaView} from 'react-native-safe-area-context'; -import { TextButton } from '../../components'; -import { Swap } from '../../components/Swap'; +import {TextButton} from '../../components'; +import {Swap} from '../../components/Swap'; +import {TokenSwap} from '../../components/TokenSwap'; import TabSelector from '../../components/TabSelector'; -import { TOKENSMINT } from '../../constants/tokens'; -import { useStyles } from '../../hooks'; -import { LightningNetworkWalletView } from '../../modules/Lightning'; -import { DefiScreenProps } from '../../types'; -import { SelectedTab, TABS_DEFI } from '../../types/tab'; +import {TOKENSMINT} from '../../constants/tokens'; +import {useStyles} from '../../hooks'; +import {CashuWalletView} from '../../modules/Cashu'; +import {LightningNetworkWalletView} from '../../modules/Lightning'; +import {DefiScreenProps} from '../../types'; +import {SelectedTab, TABS_DEFI} from '../../types/tab'; import stylesheet from './styles'; -import { CashuView, CashuWalletView } from '../../modules/Cashu'; -export const Defi: React.FC = ({ navigation }) => { +export const Defi: React.FC = ({navigation}) => { const styles = useStyles(stylesheet); const [selectedTab, setSelectedTab] = useState(SelectedTab.CASHU_WALLET); @@ -32,7 +33,6 @@ export const Defi: React.FC = ({ navigation }) => { - = ({ navigation }) => { {/* DeFi, Ramp and more soon. Stay tuned for the AFK Fi */} {selectedTab == SelectedTab.BTC_FI_VAULT && ( - + = ({ navigation }) => { /> )} + {selectedTab == SelectedTab.SWAP_AVNU && ( + + console.log('pressed!')} + /> + + )} {/* {selectedTab == SelectedTab.BTC_BRIDGE && ( @@ -69,7 +76,6 @@ export const Defi: React.FC = ({ navigation }) => { )} - {selectedTab == SelectedTab.CASHU_WALLET && ( Cashu wallet coming soon @@ -78,9 +84,7 @@ export const Defi: React.FC = ({ navigation }) => { )} - - ); }; diff --git a/apps/mobile/src/types/tab.ts b/apps/mobile/src/types/tab.ts index 3f4fc1e1..ffec1e49 100644 --- a/apps/mobile/src/types/tab.ts +++ b/apps/mobile/src/types/tab.ts @@ -33,6 +33,7 @@ export enum SelectedTab { CASHU_SETTINGS, PORTFOLIO, STARKNET_PORTFOLIO, + SWAP_AVNU, } export const TABS_TIP_LIST: { screen?: string; title: string; tab: SelectedTab }[] = [ @@ -205,11 +206,11 @@ export const TABS_DEFI: { screen?: string; title: string; tab: SelectedTab }[] = screen: 'BTCVault', tab: SelectedTab.BTC_FI_VAULT, }, - // { - // title: 'BTC Bridge', - // screen: 'BTCBridge', - // tab: SelectedTab.BTC_BRIDGE, - // }, + { + title: 'Swap', + screen: 'Swap', + tab: SelectedTab.SWAP_AVNU, + }, ]; From 768136b38d536b2ff1111efa1162e6c971882e7c Mon Sep 17 00:00:00 2001 From: Abdulhakeem Abdulazeez Date: Sun, 6 Oct 2024 14:45:50 +0100 Subject: [PATCH 2/2] feat: removed hooks file --- apps/mobile/src/hooks/api/useAvnu.ts | 134 --------------------------- 1 file changed, 134 deletions(-) delete mode 100644 apps/mobile/src/hooks/api/useAvnu.ts diff --git a/apps/mobile/src/hooks/api/useAvnu.ts b/apps/mobile/src/hooks/api/useAvnu.ts deleted file mode 100644 index 1b736ac6..00000000 --- a/apps/mobile/src/hooks/api/useAvnu.ts +++ /dev/null @@ -1,134 +0,0 @@ -import {fetchBuildExecuteTransaction, fetchQuotes} from '@avnu/avnu-sdk'; -import {NextRequest, NextResponse} from 'next/server'; -import {Calldata} from 'starknet'; - -import {ESCROW_ADDRESSES, ETH_ADDRESSES, STRK_ADDRESSES} from '@/constants/contracts'; -import {AVNU_URL, CHAIN_ID, Entrypoint} from '@/constants/misc'; -import {account} from '@/services/account'; -import {ErrorCode} from '@/utils/errors'; -import {HTTPStatus} from '@/utils/http'; -import {ClaimSchema} from '@/utils/validation'; - -import {getClaimCallData} from '../../../../website/src/app/api/deposit/calldata'; - -export async function POST(request: NextRequest) { - const requestBody = await request.json(); - - const body = ClaimSchema.safeParse(requestBody); - if (!body.success) { - return NextResponse.json( - {code: ErrorCode.BAD_REQUEST, error: body.error}, - {status: HTTPStatus.BadRequest}, - ); - } - - let claimCallData: Calldata; - let gasTokenAddress: string; - try { - const {calldata, tokenAddress} = await getClaimCallData(body.data); - claimCallData = calldata; - gasTokenAddress = tokenAddress; - } catch (error) { - if (error instanceof Error) { - return NextResponse.json({code: error.message}, {status: HTTPStatus.BadRequest}); - } - - throw error; - } - - try { - if ( - gasTokenAddress === ETH_ADDRESSES[CHAIN_ID] || - gasTokenAddress === STRK_ADDRESSES[CHAIN_ID] - ) { - // ETH | STRK fee estimation - - const result = await account.estimateInvokeFee( - [ - { - contractAddress: ESCROW_ADDRESSES[CHAIN_ID], - entrypoint: Entrypoint.CLAIM, - calldata: claimCallData, - }, - ], - { - version: gasTokenAddress === ETH_ADDRESSES[CHAIN_ID] ? 1 : 3, - }, - ); - - // Using 1.1 as a multiplier to ensure the fee is enough - const fee = ((result.overall_fee * BigInt(11)) / BigInt(10)).toString(); - - return NextResponse.json({gasFee: fee, tokenFee: fee}, {status: HTTPStatus.OK}); - } else { - // ERC20 fee estimation - - const quotes = await fetchQuotes( - { - sellTokenAddress: ETH_ADDRESSES[CHAIN_ID], - buyTokenAddress: gasTokenAddress, - sellAmount: BigInt(1), - takerAddress: account.address, - }, - {baseUrl: AVNU_URL}, - ); - const quote = quotes[0]; - - if (!quote) { - return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest}); - } - - const {calls: swapCalls} = await fetchBuildExecuteTransaction( - quote.quoteId, - account.address, - undefined, - undefined, - {baseUrl: AVNU_URL}, - ); - - const result = await account.estimateInvokeFee( - [ - { - contractAddress: ESCROW_ADDRESSES[CHAIN_ID], - entrypoint: Entrypoint.CLAIM, - calldata: claimCallData, - }, - ...swapCalls, - ], - { - version: 1, - }, - ); - - // Using 1.1 as a multiplier to ensure the fee is enough - const ethFee = (result.overall_fee * BigInt(11)) / BigInt(10); - - const feeQuotes = await fetchQuotes( - { - sellTokenAddress: ETH_ADDRESSES[CHAIN_ID], - buyTokenAddress: gasTokenAddress, - sellAmount: ethFee, - takerAddress: account.address, - }, - {baseUrl: AVNU_URL}, - ); - const feeQuote = feeQuotes[0]; - - if (!feeQuote) { - return NextResponse.json({code: ErrorCode.NO_ROUTE_FOUND}, {status: HTTPStatus.BadRequest}); - } - - return NextResponse.json( - {gasFee: ethFee, tokenFee: feeQuote.buyAmount}, - {status: HTTPStatus.OK}, - ); - } - } catch (error) { - console.error(error); - - return NextResponse.json( - {code: ErrorCode.ESTIMATION_ERROR, error}, - {status: HTTPStatus.InternalServerError}, - ); - } -}