diff --git a/.gitignore b/.gitignore index 1228ac9d..f686d3ee 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,5 @@ dist # TernJS port file .tern-port + +certificates \ No newline at end of file diff --git a/package.json b/package.json index 76a3ad00..89be9b50 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@soroban-react/wallet-data": "9.1.13", "@soroban-react/xbull": "9.1.13", "@stellar/freighter-api": "1.7.1", + "@stellar/frontend-helpers": "^2.1.4", "@stellar/stellar-sdk": "12.2.0", "@types/qs": "^6.9.7", "@types/react": "18.2.33", @@ -105,4 +106,4 @@ "src" ] } -} +} \ No newline at end of file diff --git a/pages/buy/index.tsx b/pages/buy/index.tsx new file mode 100644 index 00000000..e668b68f --- /dev/null +++ b/pages/buy/index.tsx @@ -0,0 +1,24 @@ +import { useSorobanReact } from '@soroban-react/core'; +import { BuyComponent } from 'components/Buy/BuyComponent'; +import SEO from 'components/SEO'; +import { xlmTokenList } from 'constants/xlmToken'; +import { useEffect, useState } from 'react'; + +export default function BuyPage() { + const { activeChain } = useSorobanReact(); + const [xlmToken, setXlmToken] = useState(null); + + useEffect(() => { + const newXlmToken = + xlmTokenList.find((tList) => tList.network === activeChain?.id)?.assets[0].contract ?? null; + setXlmToken(newXlmToken); + }, [activeChain, xlmToken]); + + return ( + <> + + {xlmToken && } + + + ); +} diff --git a/src/components/Buy/BuyComponent.tsx b/src/components/Buy/BuyComponent.tsx new file mode 100644 index 00000000..93834090 --- /dev/null +++ b/src/components/Buy/BuyComponent.tsx @@ -0,0 +1,522 @@ +import React, { useEffect, useState } from 'react' +import { CircularProgress, Skeleton } from '@mui/material' +import { setTrustline } from '@soroban-react/contracts' +import { useSorobanReact } from '@soroban-react/core' +import BuyModal from './BuyModal' +import { WalletButton } from 'components/Buttons/WalletButton' +import { ButtonPrimary } from 'components/Buttons/Button' +import { InputPanel, Container, Aligner, StyledTokenName, StyledDropDown } from 'components/CurrencyInputPanel/SwapCurrencyInputPanel' +import StyledWrapper from 'components/Layout/StyledWrapper' +import { StyledSelect } from 'components/Layout/StyledSelect' +import { RowFixed } from 'components/Row' +import SwapHeader from 'components/Swap/SwapHeader' +import { SwapSection } from 'components/Swap/SwapComponent' +import { BodyPrimary, ButtonText } from 'components/Text' +import { getChallengeTransaction, submitChallengeTransaction } from 'functions/buy/sep10Auth/stellarAuth' +import { initInteractiveDepositFlow } from 'functions/buy/sep24Deposit/InteractiveDeposit' +import { getCurrencies } from 'functions/buy/SEP-1' +import BuyStatusModal from './BuyStatusModal' + +export interface anchor { + name: string + home_domain: string + currency?: string +}; + +export interface currency { + code: string; + desc?: string; + is_asset_anchored?: boolean; + issuer: string; + status?: string; +}; + +const anchors = [ + { + network: 'testnet', + anchors: [{ + name: 'Stellar TestAnchor 1', + home_domain: 'testanchor.stellar.org', + currency: 'SRT' + }, + { + name: 'MoneyGram', + home_domain: 'stellar.moneygram.com', + currency: 'USD' + }, + { + name: 'MyKobo', + home_domain: 'mykobo.co', + currency: 'EUR' + }, + { + name: 'Anclap', + home_domain: 'api-stage.anclap.ar', + currency: 'ARS/PEN' + },] + }, + { + network: 'mainnet', + anchors: [ + { + name: 'Anclap', + home_domain: 'api-stage.anclap.ar', + currency: 'ARS/PEN' + } + ] + } +] + +function BuyComponent() { + const sorobanContext = useSorobanReact(); + const { address, serverHorizon, activeChain, activeConnector } = sorobanContext; + const [selectedAnchor, setSelectedAnchor] = useState(undefined); + const [currencies, setCurrencies] = useState(undefined); + const [selectedAsset, setSelectedAsset] = useState(undefined); + const [needTrustline, setNeedTrustline] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [buttonText, setButtonText] = useState('Select Anchor'); + const [modalState, setModalState] = useState<{ + anchorModal: { + visible: boolean, + selectedAnchor: anchor | undefined, + isLoading: boolean + }, + assetModal: { + visible: boolean, + selectedAsset: anchor | undefined, + isLoading: boolean + }, + }>({ + anchorModal: { + visible: false, + selectedAnchor: undefined, + isLoading: false, + }, + assetModal: { + visible: false, + selectedAsset: undefined, + isLoading: false, + } + }); + + const [statusModalState, setStatusModalState] = useState<{ + isOpen: boolean, + status: { + activeStep: number, + trustline: boolean, + trustlineError: string, + settingTrustline: boolean, + depositError: string, + }, + }>({ + isOpen: false, + status: { + activeStep: 0, + trustline: false, + trustlineError: '', + settingTrustline: false, + depositError: '', + }, + }); + + const handleNextStep = () => { + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + activeStep: ++statusModalState.status.activeStep, + }, + }); + } + + const handlePrevStep = () => { + if (statusModalState.status.activeStep == 0) return; + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + activeStep: statusModalState.status.activeStep - 1, + settingTrustline: false, + }, + }); + } + + const handleCloseStatusModal = () => { + setStatusModalState({ + isOpen: false, + status: { + activeStep: 0, + trustline: false, + trustlineError: '', + settingTrustline: false, + depositError: '', + }, + }); + } + + const openModal = () => { + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + }, + }); + } + + const setDepositError = (error: string) => { + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + depositError: error, + }, + }); + } + + const checkTrustline = async () => { + if(address){ + setIsLoading(true); + let account; + try { + account = await serverHorizon?.loadAccount(address); + } catch (error) { + console.error(error); + } + const balances = account?.balances + const hasTrustline = balances?.find((bal: any) => bal.asset_code === selectedAsset?.code && bal.asset_issuer == selectedAsset?.issuer); + setNeedTrustline(!!!hasTrustline); + setIsLoading(false); + } + + } + + const sign = async (txn: any) => { + const signedTransaction = await sorobanContext?.activeConnector?.signTransaction(txn, { + networkPassphrase: activeChain?.networkPassphrase, + network: activeChain?.id, + accountToSign: address + }); + return signedTransaction; + } + + const InitDeposit = async (homeDomain: string) => { + try { + openModal(); + const { transaction } = await getChallengeTransaction({ + publicKey: address! && address, + homeDomain: homeDomain + }); + const signedTransaction = await sign(transaction) + const submittedTransaction = await submitChallengeTransaction({ + transactionXDR: signedTransaction, + homeDomain: homeDomain + }); + + handleNextStep(); + + const { url } = await initInteractiveDepositFlow({ + authToken: submittedTransaction, + homeDomain: homeDomain, + urlFields: { + asset_code: selectedAsset?.code, + asset_issuer: selectedAsset?.issuer, + account: address + } + }); + + const interactiveUrl = `${url}&callback=postMessage` + + let popup = window.open(interactiveUrl, 'interactiveDeposit', 'width=450,height=750'); + if (!popup) { + alert( + "Popups are blocked. You’ll need to enable popups for this demo to work", + ); + console.error( + "Popups are blocked. You’ll need to enable popups for this demo to work", + ); + throw new Error("Popups are blocked. You’ll need to enable popups for this demo to work",) + } + popup?.focus(); + window.addEventListener('message', (event): void => { + if (event.origin.includes(homeDomain)) { + popup?.close() + handleNextStep(); + } + }) + function checkPopupClosed() { + if (popup?.closed) { + setDepositError('The popup was closed before submitting the transaction') + // Limpia el intervalo si el popup está cerrado + clearInterval(popupCheckInterval); + } + } + let popupCheckInterval = setInterval(checkPopupClosed, 200); + } catch (error: any) { + setDepositError(error.toString()); + console.error(error); + setIsLoading(false); + } + } + + const buy = async () => { + if (!selectedAsset || !selectedAnchor) { + console.error('No asset or anchor selected'); + return; + } + await checkTrustline(); + if (needTrustline) { + try { + setIsLoading(true); + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + trustline: true, + trustlineError: '', + settingTrustline: true, + }, + }); + const res = await setTrustline({ + tokenSymbol: selectedAsset?.code!, + tokenAdmin: selectedAsset?.issuer!, + sorobanContext + }); + if (res === undefined) throw new Error('The response is undefined'); + await checkTrustline(); + console.log('trustline set'); + handleCloseStatusModal(); + } catch (error: any) { + console.log('error setting trustline'); + setStatusModalState({ + isOpen: true, + status: { + ...statusModalState.status, + trustlineError: error.toString(), + }, + }); + console.error(error); + setNeedTrustline(true); + setIsLoading(false); + handleCloseStatusModal(); + } + } else { + try { + setIsLoading(true); + InitDeposit(selectedAnchor?.home_domain!) + setIsLoading(false); + } catch (error: any) { + setDepositError(error.toString()); + console.error(error); + setIsLoading(false); + } + } + } + + const getAnchors = () => { + return anchors.find((anchor) => anchor.network === activeChain?.id)?.anchors; + } + + useEffect(() => { + checkTrustline(); + }, [selectedAsset, address, activeChain, selectedAnchor, activeConnector]) + + useEffect(() => { + if ((selectedAnchor != undefined) && (selectedAsset == undefined)) { + setButtonText('Select Token'); + } else if (selectedAnchor && selectedAsset) { + if (needTrustline) { + setButtonText(`Set trustline to ${selectedAsset.code}`); + } else if (!needTrustline) { + setButtonText(`Buy ${selectedAsset.code}`); + } + } else { + setButtonText('Select Anchor'); + } + }, [needTrustline, address, selectedAsset, selectedAnchor]); + + const fetchCurrencies = async () => { + setIsLoading(true); + setModalState({ + ...modalState, + assetModal: { + ...modalState.assetModal, + isLoading: true, + } + }) + const currencies = await getCurrencies(selectedAnchor?.home_domain!); + setCurrencies(currencies); + setIsLoading(false); + setModalState({ + ...modalState, + assetModal: { + ...modalState.assetModal, + isLoading: false, + } + }) + handleOpen('asset'); + } + + const handleOpen = (modal: string) => { + if (modal == 'anchor') { + setModalState({ + ...modalState, + anchorModal: { + ...modalState.anchorModal, + visible: true, + selectedAnchor: modalState.anchorModal.selectedAnchor, + } + }); + } else if (modal == 'asset') { + setModalState({ + ...modalState, + assetModal: { + ...modalState.assetModal, + visible: true, + selectedAsset: modalState.assetModal.selectedAsset, + } + }); + } + } + + const handleClose = (modal: string) => { + if (modal == 'anchor') { + setModalState({ + ...modalState, + anchorModal: { + ...modalState.anchorModal, + visible: false, + selectedAnchor: modalState.anchorModal.selectedAnchor, + } + }); + } else if (modal == 'asset') { + setModalState({ + ...modalState, + assetModal: { + ...modalState.assetModal, + visible: false, + selectedAsset: modalState.anchorModal.selectedAnchor, + } + }); + } + } + + const handleSelect = (modal: string, anchor?: anchor, asset?: currency) => { + if (anchor) { + setSelectedAnchor(anchor); + setSelectedAsset(undefined); + } else if (modal) { + setSelectedAsset(asset); + } + handleClose(modal); + } + + return ( + <> + + { handleClose('anchor') }} + handleSelect={(e) => handleSelect('anchor', e)} /> + { handleClose('asset') }} + handleSelect={(e) => handleSelect('asset', undefined, e)} /> + + + + + +
You pay with:
+ + + handleOpen('anchor')}> + + {selectedAnchor ? selectedAnchor.name : 'Select currency'} + + {} + + + +
+
+
+ + + +
Recieve:
+ + + {modalState.assetModal.isLoading && ( + + fetchCurrencies()} disabled={!!!selectedAnchor}> + + {selectedAsset ? selectedAsset.code : 'Select asset'} + + {} + + + )} + {!modalState.assetModal.isLoading && ( + fetchCurrencies()} disabled={!!!selectedAnchor}> + + {selectedAsset ? selectedAsset.code : 'Select asset'} + + {} + + )} + + +
+
+
+ {address ? + ( + {isLoading ? + : + + + {buttonText} + + + } + ) : + () + } +
+ + ) +} + +export { BuyComponent } \ No newline at end of file diff --git a/src/components/Buy/BuyModal.tsx b/src/components/Buy/BuyModal.tsx new file mode 100644 index 00000000..b1ac98c7 --- /dev/null +++ b/src/components/Buy/BuyModal.tsx @@ -0,0 +1,104 @@ +import { useEffect } from 'react' +import ModalBox from 'components/Modals/ModalBox' +import { styled } from 'soroswap-ui'; +import { Box, Container, Modal, useMediaQuery } from '@mui/material'; +import { BodyPrimary, BodySmall, Caption, SubHeaderLarge } from 'components/Text'; +import { anchor, currency } from './BuyComponent' + +const ContentWrapper = styled('div') <{ isMobile: boolean }>` + display: flex; + flex-direction: column; + gap: 24px; + font-family: Inter; + text-align: ${({ isMobile }) => (isMobile ? 'center' : 'left')}; +`; + +const ContainerBox = styled('div')` + cursor: pointer; + display: flex; + background-color: ${({ theme }) => theme.palette.customBackground.surface}; + border-radius: 12px; + padding: 16px; + flex-direction: row; + justify-content: space-between; + align-items: center; + align-self: stretch; +`; + +const BoxGroup = styled('div')` + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + gap: 16px; + align-self: stretch; + max-height: 50vh; + padding-right: 4px; + overflow-y: auto; + ::-webkit-scrollbar { + width: 8px; + } + + ::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.15); /* color of the tracking area */ + } + + ::-webkit-scrollbar-thumb { + background-color: rgb(100, 102, 108, 0.25); /* color of the scroll thumb */ + border-radius: 6px; + border: solid 1px rgba(0, 0, 0, 1); + } +`; +const BuyModal = ({ + isOpen, + anchors, + assets, + onClose, + handleSelect +}: { + isOpen: boolean; + anchors?: anchor[]; + assets?: currency[]; + onClose: () => void; + handleSelect: (data: any) => void; +}) => { + useEffect(() => { + + }, []) + return ( + + <> + + + + + {anchors ? Pay : Receive} + {anchors ? Select a fiat currency to pay. : Select a crypto asset to receive} + + + {anchors ? anchors.map((anchor) => ( + handleSelect(anchor)}> + {anchor.currency} + {anchor.name} + + )) : + assets ? assets.map((asset) => ( + handleSelect(asset)}> + {asset.code} + + )) : + Please, select a fiat currency first. + } + + + + + + + ) +} + +export default BuyModal \ No newline at end of file diff --git a/src/components/Buy/BuyStatusModal.tsx b/src/components/Buy/BuyStatusModal.tsx new file mode 100644 index 00000000..8c2e9045 --- /dev/null +++ b/src/components/Buy/BuyStatusModal.tsx @@ -0,0 +1,235 @@ +import ModalBox from 'components/Modals/ModalBox' +import { styled } from 'soroswap-ui'; +import { Box, Button, CircularProgress, Container, Modal } from 'soroswap-ui'; +import { BodyPrimary, BodySmall, ButtonText, Caption, HeadlineSmall } from 'components/Text'; +import { Step, StepContent, StepIconProps, StepLabel, Stepper } from '@mui/material'; +import StepConnector, { stepConnectorClasses } from '@mui/material/StepConnector'; +import { AlertTriangle, CheckCircle } from 'react-feather' +import { useTheme } from '@mui/material' +import { Check } from '@mui/icons-material'; + +const StepperConnector = styled(StepConnector)(({ theme }) => ({ + [`&.${stepConnectorClasses.alternativeLabel}`]: { + top: 10, + left: 'calc(-50% + 16px)', + right: 'calc(50% + 16px)', + }, + [`&.${stepConnectorClasses.active}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: '#784af4', + }, + }, + [`&.${stepConnectorClasses.completed}`]: { + [`& .${stepConnectorClasses.line}`]: { + borderColor: '#784af4', + }, + }, + [`& .${stepConnectorClasses.line}`]: { + borderColor: theme.palette.mode === 'dark' ? theme.palette.grey[800] : '#eaeaf0', + borderTopWidth: 3, + borderRadius: 1, + }, +})); + +const StepperIconRoot = styled('div')<{ ownerState: { active?: boolean } }>( + ({ theme, ownerState }) => ({ + color: theme.palette.mode === 'dark' ? theme.palette.grey[700] : '#eaeaf0', + display: 'flex', + height: 22, + alignItems: 'center', + ...(ownerState.active && { + color: '#784af4', + }), + '& .StepperIcon-completedIcon': { + color: '#784af4', + zIndex: 1, + fontSize: 18, + }, + '& .StepperIcon-circle': { + width: 8, + height: 8, + borderRadius: '50%', + backgroundColor: 'currentColor', + }, + }), +); + +function StepperIcon(props: StepIconProps) { + const { active, completed, className } = props; + + return ( + + {completed ? ( + + ) : ( +
+ )} + + ); +} + + + +const ContentWrapper = styled('div') <{ isMobile: boolean }>` + display: flex; + flex-direction: column; + gap: 24px; + font-family: Inter; + text-align: ${({ isMobile }) => (isMobile ? 'center' : 'left')}; +`; + +const stepperStyle = { + border: '1px solid red', +}; + + +function BuyStatusModal({ + isOpen, + activeStep, + handleNext, + handlePrev, + handleClose, + trustline, + trustlineError, + settingTrustline, + depositError +}: { + isOpen: boolean, + activeStep: number, + handleNext: () => void, + handlePrev: () => void, + handleClose: () => void, + trustline?: boolean, + trustlineError?: string, + settingTrustline?: boolean, + depositError?: string, +} +) { + const theme = useTheme() + + return ( + + <> + + + + {trustline ? ( + <> + Setting up trustline + + Setting up trustline is required to buy this token. This will allow you to receive the token after the purchase. + + {settingTrustline && (trustlineError == '') ? + <> + + Waiting for transaction completed + + + + + + : ( + <> + + + + {trustlineError} + + + + + )} + + ) : + ((depositError === '') ? (}> + + + + Requesting authorization + + + + Please, confirm the transaction in your wallet to allow Soroswap send your addres to anchor. + + + + + + + + + Fill the interactive deposit + + + + Please, fill the requested information in the new window and wait for the deposit + + + + + + + + + Await for the deposit: + + + + Everything is handled on our end. Please relax and take a break. Your funds should update automatically once the anchor completes the deposit. + + + + + + This process may take several minutes. Please, be patient. + + + + + + ) : ( + <> + + + + {depositError} + + + + + )) + } + + + + + + ) +} + +export default BuyStatusModal \ No newline at end of file diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx index 38e76728..32f75c3b 100644 --- a/src/components/Layout/Header.tsx +++ b/src/components/Layout/Header.tsx @@ -9,7 +9,7 @@ import lightModeSun from 'assets/svg/lightModeSun.svg'; import { ColorModeContext } from 'contexts'; import ProfileSection, { ActiveChainHeaderChip } from './ProfileSection'; -const MainBox = styled('div')<{ isMobile: boolean }>` +const MainBox = styled('div') <{ isMobile: boolean }>` display: flex; width: 100%; padding: ${({ isMobile }) => (isMobile ? '24px 15px' : '24px 75px')}; @@ -138,6 +138,21 @@ export default function Header() { return <>{children}; }; + interface NavItem { + href: string; + label: string; + target?: string; + test_id?: string; + } + + const navItems: NavItem[] = [ + { href: '/balance', label: 'Balance', target: '_self', test_id: 'balance__link' }, + { href: '/swap', label: 'Swap', target: '_self', test_id: 'swap__link"' }, + { href: '/liquidity', label: 'Liquidity', target: '_self', test_id: 'liquidity__link"' }, + { href: '/bridge', label: 'Bridge', target: '_self', test_id: 'bridge__link"' }, + { href: 'https://info.soroswap.finance', label: 'Info', target: '_blank', test_id: 'info__link"' }, + ]; + return ( <> diff --git a/src/components/Layout/ProfileSection.tsx b/src/components/Layout/ProfileSection.tsx index cafc8345..eed5da1e 100644 --- a/src/components/Layout/ProfileSection.tsx +++ b/src/components/Layout/ProfileSection.tsx @@ -98,10 +98,9 @@ export const HeaderChip = ({ display: 'flex', flexDirection: 'row', height: isSmall ? 30 : 56, - padding: isSmall && canDisconnect ? '8px 1px 16px 1px' : isSmall ? '8px 16px' : '16px 24px', + width: isSmall ? 100 : 200, + paddingRight: '16px', justifyContent: 'center', - alignItems: 'center', - gap: 0.5, flexShrink: 0, borderRadius: isSmall ? '4px' : '16px', backgroundColor: '#8866DD', @@ -117,7 +116,7 @@ export const HeaderChip = ({ fontFamily: 'Inter', fontWeight: 600, lineHeight: '140%', - padding: 0, + padding: '0px', }, ':hover': { backgroundColor: '#8866DD', diff --git a/src/components/Layout/StyledSelect.tsx b/src/components/Layout/StyledSelect.tsx new file mode 100644 index 00000000..ef9a9cd3 --- /dev/null +++ b/src/components/Layout/StyledSelect.tsx @@ -0,0 +1,60 @@ +// import { Trans } from '@lingui/macro' +import { ButtonBase, styled, useMediaQuery, useTheme } from 'soroswap-ui'; +import { opacify } from '../../themes/utils'; + + +export const StyledSelect = styled(ButtonBase, { + shouldForwardProp: (prop) => prop !== 'selected', +})<{ + visible: boolean | string; + selected?: boolean; + hideInput?: boolean; + disabled?: boolean; +}>` + align-items: center; + background-color: ${({ selected, theme }) => + selected ? theme.palette.customBackground.interactive : theme.palette.custom.borderColor}; + opacity: ${({ disabled }) => (!disabled ? 1 : 0.4)}; + box-shadow: ${({ selected }) => (selected ? 'none' : '0px 6px 10px rgba(0, 0, 0, 0.075)')}; + color: ${({ selected, theme }) => (selected ? theme.palette.primary.main : '#FFFFFF')}; + height: unset; + border-radius: 16px; + outline: none; + user-select: none; + border: none; + font-size: 24px; + font-weight: 400; + width: ${({ hideInput }) => (hideInput ? '100%' : 'initial')}; + padding: ${({ selected }) => (selected ? '4px 4px 4px 4px' : '6px 6px 6px 8px')}; + gap: 8px; + justify-content: space-between; + + &:hover, + &:active { + background-color: ${({ theme, selected }) => + selected ? theme.palette.customBackground.interactive : theme.palette.custom.borderColor}; + } + + &:before { + background-size: 100%; + border-radius: inherit; + + position: absolute; + top: 0; + left: 0; + + width: 100%; + height: 100%; + content: ''; + } + + &:hover:before { + background-color: ${({ theme }) => opacify(8, theme.palette.secondary.main)}; + } + + &:active:before { + background-color: ${({ theme }) => opacify(24, theme.palette.secondary.light)}; + } + + visibility: ${({ visible }) => (visible ? 'visible' : 'hidden')}; +`; \ No newline at end of file diff --git a/src/components/Layout/StyledWrapper.tsx b/src/components/Layout/StyledWrapper.tsx new file mode 100644 index 00000000..d35c5684 --- /dev/null +++ b/src/components/Layout/StyledWrapper.tsx @@ -0,0 +1,31 @@ +import { styled } from 'soroswap-ui'; +import { opacify } from 'themes/utils'; + +export const StyledWrapper = styled('main')` + position: relative; + background: ${({ theme }) => `linear-gradient(${theme.palette.customBackground.bg1}, ${ + theme.palette.customBackground.bg1 + }) padding-box, + linear-gradient(150deg, rgba(136,102,221,1) 0%, rgba(${ + theme.palette.mode == 'dark' ? '33,29,50,1' : '255,255,255,1' + }) 35%, rgba(${ + theme.palette.mode == 'dark' ? '33,29,50,1' : '255,255,255,1' + }) 65%, rgba(136,102,221,1) 100%) border-box`}; + border: 1px solid transparent; + border-radius: 16px; + padding: 32px; + padding-top: 12px; + box-shadow: 0px 40px 120px 0px #f00bdd29; + transition: transform 250ms ease; + max-width: 480px; + width: 100%; + &:hover: { + border: 1px solid ${({ theme }) => opacify(24, theme.palette.secondary.main)}; + } + + @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) { + padding: 16px; + } +`; + +export default StyledWrapper \ No newline at end of file diff --git a/src/components/Swap/SwapComponent.tsx b/src/components/Swap/SwapComponent.tsx index a87f4c92..1898e2af 100644 --- a/src/components/Swap/SwapComponent.tsx +++ b/src/components/Swap/SwapComponent.tsx @@ -175,8 +175,8 @@ export function SwapComponent({ const userHasSpecifiedInputOutput = Boolean( currencies[Field.INPUT] && - currencies[Field.OUTPUT] && - Number(parsedAmounts[independentField]?.value) > 0, + currencies[Field.OUTPUT] && + Number(parsedAmounts[independentField]?.value) > 0, ); const fiatValueInput = { data: 0, isLoading: true }; //useUSDPrice(parsedAmounts[Field.INPUT]) //TODO: create USDPrice function when available method to get this, for now it will be shown as loading @@ -405,7 +405,7 @@ export function SwapComponent({ return ( <> - + {}} + onMax={() => { }} fiatValue={showFiatValueOutput ? fiatValueOutput : undefined} //priceImpact={stablecoinPriceImpact} currency={currencies[Field.OUTPUT] ?? null} diff --git a/src/components/Swap/SwapHeader.tsx b/src/components/Swap/SwapHeader.tsx index 0a34adf8..b434631d 100644 --- a/src/components/Swap/SwapHeader.tsx +++ b/src/components/Swap/SwapHeader.tsx @@ -1,10 +1,9 @@ // import { Trans } from '@lingui/macro' //This is for localization and translation on all languages -import { styled, useTheme } from 'soroswap-ui'; +import { Link, styled, useTheme } from 'soroswap-ui'; import { RowBetween, RowFixed } from '../Row'; -import { SubHeader } from '../Text'; -import TuneRoundedIcon from '@mui/icons-material/TuneRounded'; import SettingsTab from '../Settings/index'; import { useMediaQuery } from 'soroswap-ui'; +import { useEffect, useState } from 'react'; const StyledSwapHeader = styled(RowBetween)(({ theme }) => ({ marginBottom: 10, color: theme.palette.secondary.main, @@ -15,35 +14,67 @@ const HeaderButtonContainer = styled(RowFixed)` gap: 16px; `; +const SwapLink = styled(Link, { + shouldForwardProp: (prop) => prop !== 'active', +}) <{ active?: boolean }>` + display: flex; + padding: 4px 4px; + align-items: center; + gap: 10px; + text-align: center; + color: ${({ theme, active }) => (active ? '#FFFFFF' : '#7780A0')}; + font-family: Inter; + font-size: 20px; + font-weight: 600; + line-height: 140%; + text-decoration: none; +`; + export default function SwapHeader({ autoSlippage, chainId, trade, + active, + showConfig, }: { autoSlippage?: number; chainId?: number; trade?: boolean; + active?: string; + showConfig: boolean; }) { const theme = useTheme(); const fiatOnRampButtonEnabled = true; const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const [activeAction, setActiveAction] = useState<'swap' | 'buy'>('swap'); + const href = window.location.pathname; + + useEffect(() => { + if (href == '/swap') { + setActiveAction('swap'); + } else if (href == '/buy') { + setActiveAction('buy'); + } + }, [href]); return ( - + Swap - {/* Swap */} - + {fiatOnRampButtonEnabled && ( - + Buy - + )} - - - + {showConfig ? + ( + + ) : + (false)} + ); } diff --git a/src/functions/buy/SEP-1.ts b/src/functions/buy/SEP-1.ts new file mode 100644 index 00000000..ca4b1c2a --- /dev/null +++ b/src/functions/buy/SEP-1.ts @@ -0,0 +1,43 @@ +import { TomlFields } from './types'; +import axios from 'axios'; +import toml from 'toml'; + +export enum Anchors { + TEST = 'https://testanchor.stellar.org', +} +export async function getStellarToml(home_domain: string) { + const formattedDomain = home_domain.replace(/^https?:\/\//, ''); + const tomlResponse = await axios.get(`https://${formattedDomain}/.well-known/stellar.toml`); + const parsedResponse = toml.parse(tomlResponse.data); + return parsedResponse; +} + +export async function getAuthUrl(home_domain: string) { + if(!home_domain) return + const toml = await getStellarToml(home_domain); + return toml[TomlFields.WEB_AUTH_ENDPOINT]; +} + +export async function getKycUrl(home_domain: string) { + if(!home_domain) return + const toml = await getStellarToml(home_domain); + return toml[TomlFields.WEB_AUTH_ENDPOINT]; +} + +export async function getTransferServerUrl(home_domain: string) { + if(!home_domain) return + const toml = await getStellarToml(home_domain); + return toml[TomlFields.TRANSFER_SERVER_SEP0024]; +} + +export async function getDepositServerUrl(home_domain: string) { + if(!home_domain) return + const toml = await getStellarToml(home_domain); + return toml[TomlFields.TRANSFER_SERVER_SEP0024]; +} + +export async function getCurrencies(home_domain: string){ + if(!home_domain) return + const toml = await getStellarToml(home_domain); + return toml[TomlFields.CURRENCIES]; +} diff --git a/src/functions/buy/sep10Auth/stellarAuth.ts b/src/functions/buy/sep10Auth/stellarAuth.ts new file mode 100644 index 00000000..bc627a55 --- /dev/null +++ b/src/functions/buy/sep10Auth/stellarAuth.ts @@ -0,0 +1,119 @@ +import { Operation, WebAuth, xdr } from "@stellar/stellar-sdk"; +import { getStellarToml, getAuthUrl } from "../SEP-1"; + +//TODO: Add memo to operation +export async function getChallengeTransaction({ + publicKey, + homeDomain, +}:{ + publicKey: string, + homeDomain: string, +}): Promise<{ + transaction:any, + network_passphrase:string +}>{ + let { WEB_AUTH_ENDPOINT, TRANSFER_SERVER, SIGNING_KEY } = await getStellarToml(homeDomain) + + // In order for the SEP-10 flow to work, we must have at least a server + // signing key, and a web auth endpoint (which can be the transfer server as + // a fallback) + if (!(WEB_AUTH_ENDPOINT || TRANSFER_SERVER) || !SIGNING_KEY) { + console.error(500, { + message: 'could not get challenge transaction (server missing toml entry or entries)', + }) + } + + // Request a challenge transaction for the users's account + let res = await fetch( + `${WEB_AUTH_ENDPOINT || TRANSFER_SERVER}?${new URLSearchParams({ + // Possible parameters are `account`, `memo`, `home_domain`, and + // `client_domain`. For our purposes, we only supply `account`. + account: publicKey, + /* memo: '1', */ +/* client_domain: 'lobstr.co', */ + })}` + ).catch((e)=>{ + console.log(e) + }) + let json = await res?.json() + // Validate the challenge transaction meets all the requirements for SEP-10 + validateChallengeTransaction({ + transactionXDR: json.transaction, + serverSigningKey: SIGNING_KEY, + network: json.network_passphrase, + clientPublicKey: publicKey, + homeDomain: homeDomain, + }) + return json +} + +//TODO: Fix Err400 { message: '{"name":"InvalidChallengeError"}' +// decode transaction & validate the values +function validateChallengeTransaction({ + transactionXDR, + serverSigningKey, + network, + clientPublicKey, + homeDomain, + clientDomain, +}: { + transactionXDR: any, + serverSigningKey: string, + network: string, + clientPublicKey: any, + homeDomain: string, + clientDomain?: string, +}) { + if (!clientDomain) { + clientDomain = homeDomain + } + + try { + // Use the `readChallengeTx` function from Stellar SDK to read and + // verify most of the challenge transaction information + let results = WebAuth.readChallengeTx( + transactionXDR, + serverSigningKey, + network, + homeDomain, + clientDomain + ) + + // Also make sure the transaction was created for the correct user + if (results.clientAccountID === clientPublicKey) { + return + } else { + console.error(400, { message: 'clientAccountID does not match challenge transaction' }) + } + } catch (err) { + console.error(400, { message: JSON.stringify(err) }) + } +} + +//TODO: Implement token validations https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md#token +export async function submitChallengeTransaction({ + transactionXDR, + homeDomain +}: { + transactionXDR: string | undefined, + homeDomain: string +}) { + if (!transactionXDR || transactionXDR === undefined){ + console.error('invalid transaction xdr') + } + let webAuthEndpoint = await getAuthUrl(homeDomain) + if (!webAuthEndpoint) + console.error(500, { message: 'could not authenticate with server (missing toml entry)' }) + let res = await fetch(webAuthEndpoint, { + method: 'POST', + mode: 'cors', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ transaction: transactionXDR }), + }) + let json = await res.json() + + if (!res.ok) { + console.error(400, { message: json.error }) + } + return json.token +} \ No newline at end of file diff --git a/src/functions/buy/sep24Deposit/InteractiveDeposit.ts b/src/functions/buy/sep24Deposit/InteractiveDeposit.ts new file mode 100644 index 00000000..d1254aeb --- /dev/null +++ b/src/functions/buy/sep24Deposit/InteractiveDeposit.ts @@ -0,0 +1,81 @@ +import { getTransferServerUrl } from "../SEP-1"; + +export async function checkInfo(homeDomain : string) { + let transferServerSep24 = await getTransferServerUrl(homeDomain) + + let res = await fetch(`${transferServerSep24}/info`) + let json = await res.json() + + if (!res.ok) { + console.error(res.status, { + message: json.error, + }) + } else { + return json + } +} + +export async function initInteractiveDepositFlow({ + authToken, + homeDomain, + urlFields = {} +}:{ + authToken: string, + homeDomain: string, + urlFields?: object +}) { + let transferServerSep24 = await getTransferServerUrl(homeDomain) + console.log(JSON.stringify(urlFields)) + let res = await fetch(`${transferServerSep24}/transactions/deposit/interactive`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken}`, + }, + body: JSON.stringify(urlFields), + }) + let json = await res.json() + + if (!res.ok) { + console.error(res.status, { + message: json.error, + }) + } else { + return json + } +} + +export async function queryTransfers24({ + authToken, + assetCode, + homeDomain +} : { + authToken: string, + assetCode: string, + homeDomain: string +}) { + let transferServerSep24 = await getTransferServerUrl(homeDomain) + + let res = await fetch( + `${transferServerSep24}/transactions?${new URLSearchParams({ + asset_code: assetCode, + })}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${authToken}`, + }, + } + ) + let json = await res.json() + + if (!res.ok) { + console.error(res.status, { + message: json.error, + }) + } else { + return json + } +} \ No newline at end of file diff --git a/src/functions/buy/sep24Deposit/checkInfo.ts b/src/functions/buy/sep24Deposit/checkInfo.ts new file mode 100644 index 00000000..3aa7b9f6 --- /dev/null +++ b/src/functions/buy/sep24Deposit/checkInfo.ts @@ -0,0 +1,36 @@ +import { get } from "lodash"; + +import { AnchorActionType } from "../types" + const isNativeAsset = (assetCode: string) => { + return ["XLM", "NATIVE"].includes(assetCode.toLocaleUpperCase()); +} + +export const checkInfo = async ({ + type, + toml, + assetCode, +}: { + type: AnchorActionType; + toml: any; + assetCode: string; +}) => { + console.log({ + title: `Checking \`/info\` endpoint to ensure this currency is enabled for ${ + type === AnchorActionType.DEPOSIT ? "deposit" : "withdrawal" + }`, + }); + const infoURL = `${toml.TRANSFER_SERVER_SEP0024}/info`; + console.log({ title: `GET \`${infoURL}\`` }); + + const info = await fetch(infoURL); + const infoJson = await info.json(); + const isNative = isNativeAsset(assetCode); + + console.log({ title: `GET \`${infoURL}\``, body: infoJson }); + + if (!get(infoJson, [type, isNative ? "native" : assetCode, "enabled"])) { + throw new Error("Asset is not enabled in the `/info` endpoint"); + } + + return infoJson; +}; diff --git a/src/functions/buy/sep24Deposit/pollDepositUntilComplete.ts b/src/functions/buy/sep24Deposit/pollDepositUntilComplete.ts new file mode 100644 index 00000000..d58c224d --- /dev/null +++ b/src/functions/buy/sep24Deposit/pollDepositUntilComplete.ts @@ -0,0 +1,157 @@ +import { TransactionStatus } from "../types"; +import { getCatchError } from "@stellar/frontend-helpers"; + +export const getErrorMessage = (error: Error | unknown) => { + const e = getCatchError(error); + return e.message || e.toString(); +}; + +export const pollDepositUntilComplete = async ({ + popup, + transactionId, + token, + sep24TransferServerUrl, + trustAssetCallback, + custodialMemoId, +}: { + popup: any; + transactionId: string; + token: string; + sep24TransferServerUrl: string; + trustAssetCallback: () => Promise; + custodialMemoId?: string; +}) => { + let currentStatus = TransactionStatus.INCOMPLETE; + let trustedAssetAdded; + + const transactionUrl = new URL( + `${sep24TransferServerUrl}/transaction?id=${transactionId}`, + ); + console.log({ + title: `Polling for updates \`${transactionUrl.toString()}\``, + }); + + const endStatuses = [ + TransactionStatus.PENDING_EXTERNAL, + TransactionStatus.COMPLETED, + TransactionStatus.ERROR, + ]; + + const initResponse = await fetch(transactionUrl.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + + const initTransactionJson = await initResponse.json(); + + if (initTransactionJson?.transaction?.more_info_url) { + console.log({ + title: "Transaction MORE INFO URL:", + link: initTransactionJson.transaction.more_info_url, + }); + } + + while (!popup.closed && !endStatuses.includes(currentStatus)) { + // eslint-disable-next-line no-await-in-loop + const response = await fetch(transactionUrl.toString(), { + headers: { Authorization: `Bearer ${token}` }, + }); + + // eslint-disable-next-line no-await-in-loop + const transactionJson = await response.json(); + + if (transactionJson.transaction.status !== currentStatus) { + currentStatus = transactionJson.transaction.status; + + console.log({ + title: "Transaction MORE INFO URL:", + link: initTransactionJson.transaction.more_info_url, + }); + + console.log({ + title: `Transaction \`${transactionId}\` is in \`${transactionJson.transaction.status}\` status.`, + body: transactionJson.transaction, + }); + + switch (currentStatus) { + case TransactionStatus.PENDING_USER_TRANSFER_START: { + if ( + custodialMemoId && + transactionJson.transaction.deposit_memo !== custodialMemoId + ) { + console.log({ + title: "SEP-24 deposit custodial memo doesn’t match", + body: `Expected ${custodialMemoId}, got ${transactionJson.transaction.deposit_memo}`, + }); + } + + console.log({ + title: + "The anchor is waiting on you to take the action described in the popup", + }); + break; + } + case TransactionStatus.PENDING_ANCHOR: { + console.log({ + title: "The anchor is processing the transaction", + }); + break; + } + case TransactionStatus.PENDING_STELLAR: { + console.log({ + title: "The Stellar network is processing the transaction", + }); + break; + } + case TransactionStatus.PENDING_EXTERNAL: { + console.log({ + title: "The transaction is being processed by an external system", + }); + break; + } + case TransactionStatus.PENDING_TRUST: { + console.log({ + title: + "You must add a trustline to the asset in order to receive your deposit", + }); + + try { + // eslint-disable-next-line no-await-in-loop + trustedAssetAdded = await trustAssetCallback(); + } catch (error) { + throw new Error(getErrorMessage(error)); + } + break; + } + case TransactionStatus.PENDING_USER: { + console.log({ + title: + "The anchor is waiting for you to take the action described in the popup", + }); + break; + } + case TransactionStatus.ERROR: { + console.log({ + title: "There was a problem processing your transaction", + }); + break; + } + default: + // do nothing + } + } + + // run loop every 2 seconds + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + + console.log({ title: `Transaction status \`${currentStatus}\`` }); + + if (!endStatuses.includes(currentStatus) && popup.closed) { + console.log({ + title: `The popup was closed before the transaction reached a terminal status, if your balance is not updated soon, the transaction may have failed.`, + }); + } + + return { currentStatus, trustedAssetAdded }; +}; diff --git a/src/functions/buy/types.ts b/src/functions/buy/types.ts new file mode 100644 index 00000000..3226f2f1 --- /dev/null +++ b/src/functions/buy/types.ts @@ -0,0 +1,469 @@ +import React, { ReactNode } from "react"; +import { Horizon } from "@stellar/stellar-sdk"; +import BigNumber from "bignumber.js"; + +declare global { + interface Window { + _env_: { + AMPLITUDE_API_KEY: string; + SENTRY_API_KEY: string; + HORIZON_PASSPHRASE?: string; + HORIZON_URL?: string; + WALLET_BACKEND_ENDPOINT?: string; + CLIENT_DOMAIN?: string; + }; + } +} + +export const XLM_NATIVE_ASSET = "XLM:native"; + +export enum SearchParams { + SECRET_KEY = "secretKey", + UNTRUSTED_ASSETS = "untrustedAssets", + ASSET_OVERRIDES = "assetOverrides", + CLAIMABLE_BALANCE_SUPPORTED = "claimableBalanceSupported", +} + +export enum AssetCategory { + TRUSTED = "trusted", + UNTRUSTED = "untrusted", +} + +export enum TomlFields { + ACCOUNTS = "ACCOUNTS", + ANCHOR_QUOTE_SERVER = "ANCHOR_QUOTE_SERVER", + AUTH_SERVER = "AUTH_SERVER", + DIRECT_PAYMENT_SERVER = "DIRECT_PAYMENT_SERVER", + FEDERATION_SERVER = "FEDERATION_SERVER", + HORIZON_URL = "HORIZON_URL", + KYC_SERVER = "KYC_SERVER", + NETWORK_PASSPHRASE = "NETWORK_PASSPHRASE", + SIGNING_KEY = "SIGNING_KEY", + TRANSFER_SERVER = "TRANSFER_SERVER", + TRANSFER_SERVER_SEP0024 = "TRANSFER_SERVER_SEP0024", + URI_REQUEST_SIGNING_KEY = "URI_REQUEST_SIGNING_KEY", + VERSION = "VERSION", + WEB_AUTH_ENDPOINT = "WEB_AUTH_ENDPOINT", + CURRENCIES= "CURRENCIES" +} + +export interface PresetAsset { + assetCode: string; + homeDomain?: string; + issuerPublicKey?: string; +} + +export interface Asset { + assetString: string; + assetCode: string; + assetIssuer: string; + assetType: string; + total: string; + homeDomain?: string; + supportedActions?: AssetSupportedActions; + isUntrusted?: boolean; + isOverride?: boolean; + isClaimableBalance?: boolean; + notExist?: boolean; + source: any; + category?: AssetCategory; +} + +export interface SearchParamAsset { + assetString: string; + homeDomain?: string; +} + +export interface AssetSupportedActions { + sep6?: boolean; + sep8?: boolean; + sep24?: boolean; + sep31?: boolean; +} + +export interface AccountInitialState { + data: AccountDetails | null; + assets: Asset[]; + errorString?: string; + isAuthenticated: boolean; + isUnfunded: boolean; + secretKey: string; + status: ActionStatus | undefined; +} + +export interface ActiveAssetInitialState { + action: ActiveAssetAction | undefined; + status: ActionStatus | undefined; +} + +export interface AllAssetsInitialState { + data: Asset[]; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface AssetOverridesInitialState { + data: Asset[]; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface ClaimAssetInitialState { + data: { + result: any; + trustedAssetAdded?: string; + }; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface ClaimableBalancesInitialState { + data: { + records: ClaimableAsset[] | null; + }; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface Sep24DepositAssetInitialState { + data: { + currentStatus: string; + trustedAssetAdded?: string; + }; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface LogsInitialState { + items: LogItemProps[]; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface SendPaymentInitialState { + data: Horizon.HorizonApi.SubmitTransactionResponse | null; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface SettingsInitialState { + assetOverrides: string; + secretKey: string; + untrustedAssets: string; + claimableBalanceSupported: boolean; +} + +export interface UntrustedAssetsInitialState { + data: Asset[]; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface AnyObject { + [key: string]: any; +} + +export interface AssetsObject { + [key: string]: Asset; +} + +export interface StringObject { + [key: string]: string; +} + +export interface NestedStringObject { + [key: string]: { + [key: string]: string; + }; +} + +export interface CustomerTypeItem { + type: string; + description: string; +} + +export interface Setting { + [key: string]: any; +} + +export interface TrustAssetInitialState { + assetString: string; + data: any; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface Sep24WithdrawAssetInitialState { + data: { + currentStatus: string; + }; + errorString?: string; + status: ActionStatus | undefined; +} + +export interface TrustAssetParam { + assetString: string; + assetCode: string; + assetIssuer: string; +} + +export enum LogType { + REQUEST = "request", + RESPONSE = "response", + INSTRUCTION = "instruction", + ERROR = "error", + WARNING = "warning", +} + +export interface LogItemProps { + timestamp: number; + type: LogType; + title: string; + body?: string | AnyObject; + link?: string; +} + +export interface Store { + account: AccountInitialState; + activeAsset: ActiveAssetInitialState; + allAssets: AllAssetsInitialState; + assetOverrides: AssetOverridesInitialState; + claimAsset: ClaimAssetInitialState; + claimableBalances: ClaimableBalancesInitialState; + logs: LogsInitialState; + sendPayment: SendPaymentInitialState; + sep24DepositAsset: Sep24DepositAssetInitialState; + sep24WithdrawAsset: Sep24WithdrawAssetInitialState; + settings: SettingsInitialState; + trustAsset: TrustAssetInitialState; + untrustedAssets: UntrustedAssetsInitialState; +} + +export type StoreKey = keyof Store; + +export enum ActionStatus { + ERROR = "ERROR", + PENDING = "PENDING", + SUCCESS = "SUCCESS", + NEEDS_INPUT = "NEEDS_INPUT", + CAN_PROCEED = "CAN_PROCEED", +} + +export interface RejectMessage { + errorString: string; +} + +export interface PaymentTransactionParams { + amount: string; + assetCode?: string; + assetIssuer?: string; + destination: string; + isDestinationFunded: boolean; + publicKey: string; +} + +export interface ClaimableAsset extends Asset { + id: string; + sponsor: string; + lastModifiedLedger: number; + claimants: any[]; +} + +export interface ActiveAssetAction { + assetString: string; + title: string; + description?: string | React.ReactNode; + callback: (args?: any) => void; + options?: ReactNode; +} + +export interface AssetActionItem extends ActiveAssetAction { + balance: Asset; +} + +export enum AssetActionId { + SEND_PAYMENT = "send-payment", + SEP6_DEPOSIT = "sep6-deposit", + SEP6_WITHDRAW = "sep6-withdraw", + SEP8_SEND_PAYMENT = "sep8-send-payment", + SEP24_DEPOSIT = "sep24-deposit", + SEP24_WITHDRAW = "sep24-withdraw", + SEP31_SEND = "sep31-send", + TRUST_ASSET = "trust-asset", + REMOVE_ASSET = "remove-asset", + ADD_ASSET_OVERRIDE = "add-asset-override", + REMOVE_ASSET_OVERRIDE = "remove-asset-override", +} + +export enum AssetType { + NATIVE = "native", +} + +export enum TransactionStatus { + COMPLETED = "completed", + ERROR = "error", + INCOMPLETE = "incomplete", + NON_INTERACTIVE_CUSTOMER_INFO_NEEDED = "non_interactive_customer_info_needed", + PENDING_ANCHOR = "pending_anchor", + PENDING_CUSTOMER_INFO_UPDATE = "pending_customer_info_update", + PENDING_EXTERNAL = "pending_external", + PENDING_RECEIVER = "pending_receiver", + PENDING_SENDER = "pending_sender", + PENDING_STELLAR = "pending_stellar", + PENDING_TRANSACTION_INFO_UPDATE = "pending_transaction_info_update", + PENDING_TRUST = "pending_trust", + PENDING_USER = "pending_user", + PENDING_USER_TRANSFER_START = "pending_user_transfer_start", +} + +export enum MemoTypeString { + TEXT = "text", + ID = "id", + HASH = "hash", +} + +export enum AnchorActionType { + DEPOSIT = "deposit", + WITHDRAWAL = "withdraw", +} + +interface InfoTypeData { + // eslint-disable-next-line camelcase + authentication_required: boolean; + enabled: boolean; + fields: AnyObject; + types: AnyObject; + // eslint-disable-next-line camelcase + min_amount?: number; + // eslint-disable-next-line camelcase + max_amount?: number; +} + +export interface CheckInfoData { + [AnchorActionType.DEPOSIT]: { + [asset: string]: InfoTypeData; + }; + [AnchorActionType.WITHDRAWAL]: { + [asset: string]: InfoTypeData; + }; +} + +// Anchor quotes +export type AnchorDeliveryMethod = { + name: string; + description: string; +}; + +export type AnchorQuoteAsset = { + asset: string; + /* eslint-disable camelcase */ + sell_delivery_methods?: AnchorDeliveryMethod[]; + buy_delivery_methods?: AnchorDeliveryMethod[]; + country_codes?: string[]; + /* eslint-enable camelcase */ +}; + +export type AnchorBuyAsset = { + asset: string; + price: string; + decimals: number; +}; + +export type AnchorQuote = { + id: string; + price: string; + fee: AnchorFee; + /* eslint-disable camelcase */ + expires_at: string; + total_price: string; + sell_asset: string; + sell_amount: string; + buy_asset: string; + buy_amount: string; + /* eslint-enable camelcase */ +}; + +export type AnchorFee = { + total: string; + asset: string; + details?: AnchorFeeDetail[]; +}; + +export type AnchorFeeDetail = { + name: string; + description?: string; + amount: string; +}; + +export type SepInstructions = { + [key: string]: { + description: string; + value: string; + }; +}; + +// js-stellar-wallets types +export interface Issuer { + key: string; + name?: string; + url?: string; + hostName?: string; +} + +export interface NativeToken { + type: AssetType; + code: string; +} + +export interface AssetToken { + type: AssetType; + code: string; + issuer: Issuer; + anchorAsset?: string; + numAccounts?: BigNumber; + amount?: BigNumber; + bidCount?: BigNumber; + askCount?: BigNumber; + spread?: BigNumber; +} + +export type Token = NativeToken | AssetToken; +export interface Balance { + token: Token; + + // for non-native tokens, this should be total - sellingLiabilities + // for native, it should also subtract the minimumBalance + available: BigNumber; + total: BigNumber; + buyingLiabilities: BigNumber; + sellingLiabilities: BigNumber; +} + +export interface AssetBalance extends Balance { + token: AssetToken; + sponsor?: string; +} + +export interface NativeBalance extends Balance { + token: NativeToken; + minimumBalance: BigNumber; +} + +export interface BalanceMap { + [key: string]: AssetBalance | NativeBalance; + native: NativeBalance; +} + +export interface AccountDetails { + id: string; + subentryCount: number; + sponsoringCount: number; + sponsoredCount: number; + sponsor?: string; + inflationDestination?: string; + thresholds: Horizon.HorizonApi.AccountThresholds; + signers: Horizon.ServerApi.AccountRecordSigners[]; + flags: Horizon.HorizonApi.Flags; + balances: BalanceMap; + sequenceNumber: string; +} diff --git a/yarn.lock b/yarn.lock index 1a7cf91f..a1e26623 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1903,6 +1903,69 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== +"@sentry/browser@^6.13.2": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.19.7.tgz#a40b6b72d911b5f1ed70ed3b4e7d4d4e625c0b5f" + integrity sha512-oDbklp4O3MtAM4mtuwyZLrgO1qDVYIujzNJQzXmi9YzymJCuzMLSRDvhY83NNDCRxf0pds4DShgYeZdbSyKraA== + dependencies: + "@sentry/core" "6.19.7" + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + +"@sentry/core@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.19.7.tgz#156aaa56dd7fad8c89c145be6ad7a4f7209f9785" + integrity sha512-tOfZ/umqB2AcHPGbIrsFLcvApdTm9ggpi/kQZFkej7kMphjT+SGBiQfYtjyg9jcRW+ilAR4JXC9BGKsdEQ+8Vw== + dependencies: + "@sentry/hub" "6.19.7" + "@sentry/minimal" "6.19.7" + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + +"@sentry/hub@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.19.7.tgz#58ad7776bbd31e9596a8ec46365b45cd8b9cfd11" + integrity sha512-y3OtbYFAqKHCWezF0EGGr5lcyI2KbaXW2Ik7Xp8Mu9TxbSTuwTe4rTntwg8ngPjUQU3SUHzgjqVB8qjiGqFXCA== + dependencies: + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + +"@sentry/minimal@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.19.7.tgz#b3ee46d6abef9ef3dd4837ebcb6bdfd01b9aa7b4" + integrity sha512-wcYmSJOdvk6VAPx8IcmZgN08XTXRwRtB1aOLZm+MVHjIZIhHoBGZJYTVQS/BWjldsamj2cX3YGbGXNunaCfYJQ== + dependencies: + "@sentry/hub" "6.19.7" + "@sentry/types" "6.19.7" + tslib "^1.9.3" + +"@sentry/tracing@^6.13.2": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.19.7.tgz#54bb99ed5705931cd33caf71da347af769f02a4c" + integrity sha512-ol4TupNnv9Zd+bZei7B6Ygnr9N3Gp1PUrNI761QSlHtPC25xXC5ssSD3GMhBgyQrcvpuRcCFHVNNM97tN5cZiA== + dependencies: + "@sentry/hub" "6.19.7" + "@sentry/minimal" "6.19.7" + "@sentry/types" "6.19.7" + "@sentry/utils" "6.19.7" + tslib "^1.9.3" + +"@sentry/types@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.19.7.tgz#c6b337912e588083fc2896eb012526cf7cfec7c7" + integrity sha512-jH84pDYE+hHIbVnab3Hr+ZXr1v8QABfhx39KknxqKWr2l0oEItzepV0URvbEhB446lk/S/59230dlUUIBGsXbg== + +"@sentry/utils@6.19.7": + version "6.19.7" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.19.7.tgz#6edd739f8185fd71afe49cbe351c1bbf5e7b7c79" + integrity sha512-z95ECmE3i9pbWoXQrD/7PgkBAzJYR+iXtPuTkpBjDKs86O3mT+PXOT3BAn79w2wkn7/i3vOGD2xVr1uiMl26dA== + dependencies: + "@sentry/types" "6.19.7" + tslib "^1.9.3" + "@sigstore/bundle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-2.3.2.tgz#ad4dbb95d665405fd4a7a02c8a073dbd01e4e95e" @@ -2095,6 +2158,16 @@ resolved "https://registry.yarnpkg.com/@stellar/freighter-api/-/freighter-api-1.7.1.tgz#d62b432abc7e0140a6025cd672455ecee7b3199a" integrity sha512-XvPO+XgEbkeP0VhP0U1edOkds+rGS28+y8GRGbCVXeZ9ZslbWqRFQoETAdX8IXGuykk2ib/aPokiLc5ZaWYP7w== +"@stellar/frontend-helpers@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@stellar/frontend-helpers/-/frontend-helpers-2.1.4.tgz#f3d1b9daaeee8e9cfb605cc23305dfde80ec4971" + integrity sha512-snKsHWDiS51zcEtysoxJ1qPzkhEAqd9la7QdZoFd19DYpf6q1gxzUN+bBAKP5D/SANQE2iMH7oWc/tP7ru3a2A== + dependencies: + "@sentry/browser" "^6.13.2" + "@sentry/tracing" "^6.13.2" + lodash.throttle "^4.1.1" + typescript "^4.4.3" + "@stellar/js-xdr@^3.1.1": version "3.1.1" resolved "https://registry.yarnpkg.com/@stellar/js-xdr/-/js-xdr-3.1.1.tgz#be0ff90c8a861d6e1101bca130fa20e74d5599bb" @@ -5917,6 +5990,11 @@ lodash.takeright@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.takeright/-/lodash.takeright-4.1.1.tgz#c98d84aa9b80e4d2ff675335a62e02e9a65bb210" integrity sha512-/I41i2h8VkHtv3PYD8z1P4dkLIco5Z3z35hT/FJl18AxwSdifcATaaiBOxuQOT3T/F1qfRTct3VWMFSj1xCtAw== +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + lodash@^4.17.14, lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" @@ -8348,6 +8426,11 @@ tslib@1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== +tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + tslib@^2.0.1, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.5.3, tslib@^2.6.2: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" @@ -8472,6 +8555,11 @@ typescript@5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@^4.4.3: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + unbox-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e"