Skip to content

Commit

Permalink
feat: add OTC contracts page
Browse files Browse the repository at this point in the history
  • Loading branch information
belopash committed Dec 24, 2024
1 parent fa68cb4 commit 2440fa4
Show file tree
Hide file tree
Showing 7 changed files with 434 additions and 0 deletions.
4 changes: 4 additions & 0 deletions src/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { DashboardPage } from '@pages/DashboardPage/DashboardPage.tsx';
import { DelegationsPage } from '@pages/DelegationsPage/DelegationsPage.tsx';
import { Gateway } from '@pages/GatewaysPage/Gateway.tsx';
import { GatewaysPage } from '@pages/GatewaysPage/GatewaysPage.tsx';
import { OtcContractsPage } from '@pages/OtcPage/OtcPage.tsx';
import { Worker } from '@pages/WorkersPage/Worker.tsx';
import { WorkersPage } from '@pages/WorkersPage/WorkersPage.tsx';

Expand Down Expand Up @@ -43,6 +44,9 @@ export const AppRoutes = () => {
<Route element={<Gateway backPath="/portals" />} path=":peerId" />
</Route>
<Route path="/gateways" element={<Navigate to="/portals" replace={true} />} />
<Route path="/otc">
<Route element={<OtcContractsPage />} index />
</Route>
<Route element={<Navigate to="/dashboard" replace={true} />} path="*" />
</Route>
</Routes>
Expand Down
73 changes: 73 additions & 0 deletions src/api/contracts/subsquid.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,30 @@ export const networkControllerAbi = [
},
] as const

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// OverTheCounter
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

export const overTheCounterAbi = [
{
type: 'function',
inputs: [{ name: 'amount', internalType: 'uint256', type: 'uint256' }],
name: 'deposit',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
{ name: 'receiver', internalType: 'address', type: 'address' },
{ name: 'amount', internalType: 'uint256', type: 'uint256' },
],
name: 'withdraw',
outputs: [],
stateMutability: 'nonpayable',
},
] as const

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// RewardTreasury
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -898,6 +922,55 @@ export const useReadNetworkControllerWorkerEpochLength =
functionName: 'workerEpochLength',
})

/**
* Wraps __{@link useWriteContract}__ with `abi` set to __{@link overTheCounterAbi}__
*/
export const useWriteOverTheCounter = /*#__PURE__*/ createUseWriteContract({
abi: overTheCounterAbi,
})

/**
* Wraps __{@link useWriteContract}__ with `abi` set to __{@link overTheCounterAbi}__ and `functionName` set to `"deposit"`
*/
export const useWriteOverTheCounterDeposit =
/*#__PURE__*/ createUseWriteContract({
abi: overTheCounterAbi,
functionName: 'deposit',
})

/**
* Wraps __{@link useWriteContract}__ with `abi` set to __{@link overTheCounterAbi}__ and `functionName` set to `"withdraw"`
*/
export const useWriteOverTheCounterWithdraw =
/*#__PURE__*/ createUseWriteContract({
abi: overTheCounterAbi,
functionName: 'withdraw',
})

/**
* Wraps __{@link useSimulateContract}__ with `abi` set to __{@link overTheCounterAbi}__
*/
export const useSimulateOverTheCounter =
/*#__PURE__*/ createUseSimulateContract({ abi: overTheCounterAbi })

/**
* Wraps __{@link useSimulateContract}__ with `abi` set to __{@link overTheCounterAbi}__ and `functionName` set to `"deposit"`
*/
export const useSimulateOverTheCounterDeposit =
/*#__PURE__*/ createUseSimulateContract({
abi: overTheCounterAbi,
functionName: 'deposit',
})

/**
* Wraps __{@link useSimulateContract}__ with `abi` set to __{@link overTheCounterAbi}__ and `functionName` set to `"withdraw"`
*/
export const useSimulateOverTheCounterWithdraw =
/*#__PURE__*/ createUseSimulateContract({
abi: overTheCounterAbi,
functionName: 'withdraw',
})

/**
* Wraps __{@link useWriteContract}__ with `abi` set to __{@link rewardTreasuryAbi}__
*/
Expand Down
3 changes: 3 additions & 0 deletions src/network/useContracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function useContracts(): {
SQD_TOKEN: string;
CHAIN_ID_L1: number;
MULTICALL: `0x${string}`;
OTC: `0x${string}`;
} {
const network = getSubsquidNetwork();

Expand All @@ -31,6 +32,7 @@ export function useContracts(): {
ROUTER: '0xD2093610c5d27c201CD47bCF1Df4071610114b64',
CHAIN_ID_L1: sepolia.id,
MULTICALL: '0x7eCfBaa8742fDf5756DAC92fbc8b90a19b8815bF',
OTC: '0xe34189ad45044e93d3af7d93ac520d02651faf72',
};
}
case NetworkName.Mainnet: {
Expand All @@ -46,6 +48,7 @@ export function useContracts(): {
ROUTER: '0x67F56D27dab93eEb07f6372274aCa277F49dA941',
CHAIN_ID_L1: mainnet.id,
MULTICALL: '0x7eCfBaa8742fDf5756DAC92fbc8b90a19b8815bF',
OTC: '0x1c77ad535552E7428630bCDF5B10B1E992F9f16c',
};
}
}
Expand Down
186 changes: 186 additions & 0 deletions src/pages/OtcPage/DepositButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, { useMemo, useState } from 'react';

import { fromSqd, toSqd } from '@lib/network/utils';
import { LoadingButton } from '@mui/lab';
import { Chip } from '@mui/material';
import * as yup from '@schema';
import { useFormik } from 'formik';
import toast from 'react-hot-toast';

import { overTheCounterAbi } from '@api/contracts';
import { useWriteSQDTransaction } from '@api/contracts/useWriteTransaction';
import { errorMessage } from '@api/contracts/utils';
import { AccountType, SourceWalletWithBalance } from '@api/subsquid-network-squid';
import { ContractCallDialog } from '@components/ContractCallDialog';
import { Form, FormikSelect, FormikTextInput, FormRow } from '@components/Form';
import { SourceWalletOption } from '@components/SourceWallet';
import { useSquidHeight } from '@hooks/useSquidNetworkHeightHooks';

export const depositSchema = yup.object({
source: yup.string().label('Source').trim().required().typeError('${path} is invalid'),
amount: yup
.decimal()
.label('Amount')
.required()
.positive()
.max(yup.ref('max'), 'Insufficient balance')
.typeError('${path} is invalid'),
max: yup.string().label('Max').required().typeError('${path} is invalid'),
});

export function DepositButton({
sources,
otc,
disabled,
variant = 'outlined',
}: {
sources?: SourceWalletWithBalance[];
otc?: string;
variant?: 'outlined' | 'contained';
disabled?: boolean;
}) {
const [open, setOpen] = useState(false);

return (
<>
<LoadingButton
loading={open}
disabled={disabled || !otc}
onClick={() => setOpen(true)}
variant={variant}
color={variant === 'contained' ? 'info' : 'secondary'}
>
DEPOSIT
</LoadingButton>
<DepositDialog open={open} onClose={() => setOpen(false)} sources={sources} otc={otc} />
</>
);
}

export function DepositDialog({
open,
sources,
otc,
onClose,
}: {
open: boolean;
sources?: SourceWalletWithBalance[];
otc?: string;
onClose: () => void;
}) {
const { writeTransactionAsync, isPending } = useWriteSQDTransaction();

const { setWaitHeight } = useSquidHeight();

const isSourceDisabled = (source: SourceWalletWithBalance) => source.balance === '0';

const initialValues = useMemo(() => {
const source = sources?.find(c => !isSourceDisabled(c)) || sources?.[0];

return {
source: source?.id || '',
amount: '0',
max: fromSqd(source?.balance).toString(),
};
}, [sources]);

const formik = useFormik({
initialValues,
validationSchema: depositSchema,
validateOnChange: true,
validateOnBlur: true,
validateOnMount: true,
enableReinitialize: true,

onSubmit: async values => {
if (!otc) return;

try {
const { amount, source: sourceId } = depositSchema.cast(values);

const source = sources?.find(w => w?.id === sourceId);
if (!source) return;

const sqdAmount = BigInt(toSqd(amount));

const receipt = await writeTransactionAsync({
abi: overTheCounterAbi,
address: otc as `0x${string}`,
functionName: 'deposit',
args: [sqdAmount],
vesting: source.type === AccountType.Vesting ? (source.id as `0x${string}`) : undefined,
approve: sqdAmount,
});
setWaitHeight(receipt.blockNumber, []);

onClose();
} catch (e) {
toast.error(errorMessage(e));
}
},
});

return (
<ContractCallDialog
title="Deposit OTC contract"
open={open}
onResult={confirmed => {
if (!confirmed) return onClose();

formik.handleSubmit();
}}
loading={isPending}
disableConfirmButton={!formik.isValid}
>
<Form onSubmit={formik.handleSubmit}>
<FormRow>
<FormikSelect
id="source"
showErrorOnlyOfTouched
options={
sources?.map(s => {
return {
label: <SourceWalletOption source={s} />,
value: s.id,
disabled: isSourceDisabled(s),
};
}) || []
}
formik={formik}
onChange={e => {
const source = sources?.find(w => w?.id === e.target.value);
if (!source) return;

formik.setFieldValue('source', source.id);
formik.setFieldValue('max', fromSqd(source.balance).toString());
}}
/>
</FormRow>
<FormRow>
<FormikTextInput
id="amount"
label="Amount"
formik={formik}
showErrorOnlyOfTouched
autoComplete="off"
InputProps={{
endAdornment: (
<Chip
clickable
disabled={formik.values.max === formik.values.amount}
onClick={() => {
formik.setValues({
...formik.values,
amount: formik.values.max,
});
}}
label="Max"
/>
),
}}
/>
</FormRow>
</Form>
</ContractCallDialog>
);
}
26 changes: 26 additions & 0 deletions src/pages/OtcPage/OtcName.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { addressFormatter } from '@lib/formatters/formatters';
import { Box, Stack, styled, Typography } from '@mui/material';

import { Avatar } from '@components/Avatar';
import { CopyToClipboard } from '@components/CopyToClipboard';

const Name = styled(Box, {
name: 'Name',
})(({ theme }) => ({
marginBottom: theme.spacing(0.25),
whiteSpace: 'nowrap',
}));

export function SourceWalletName({ source }: { source: { id: string } }) {
return (
<Stack direction="row" spacing={2} alignItems="center">
<Avatar name={source.id.slice(2)} colorDiscriminator={source.id} />
<Box>
<Name>Contract</Name>
<Typography variant="caption">
<CopyToClipboard text={source.id} content={addressFormatter(source.id, true)} />
</Typography>
</Box>
</Stack>
);
}
Loading

0 comments on commit 2440fa4

Please sign in to comment.