diff --git a/apps/dapp/src/components/InputSelect/InputSelect.tsx b/apps/dapp/src/components/InputSelect/InputSelect.tsx index bd1a3e126..e1211e2ca 100644 --- a/apps/dapp/src/components/InputSelect/InputSelect.tsx +++ b/apps/dapp/src/components/InputSelect/InputSelect.tsx @@ -17,6 +17,7 @@ export interface SelectTempleDaoProps { // use to limit the number of elements shown in the menu at anytime maxMenuItems?: number; isSearchable?: boolean; + isDisabled?: boolean; width?: CSS.Property.Width; fontSize?: CSS.Property.FontSize; fontWeight?: CSS.Property.FontWeight; @@ -52,7 +53,7 @@ export const InputSelect = (props: SelectTempleDaoProps) => { borderRadius: 0, })} styles={{ - control: (base) => ({ + control: (base, state) => ({ ...base, border: `0.0625rem /* 1/16 */ solid ${theme.palette.brand}`, borderRadius: `calc(${selectHeight} / 4)`, @@ -60,10 +61,14 @@ export const InputSelect = (props: SelectTempleDaoProps) => { fontSize: '1rem', textAlign: 'left', padding: '0 0.5rem', - cursor: 'pointer', + cursor: state.isDisabled ? 'not-allowed' : 'pointer', height: selectHeight, zIndex: props.zIndex ? Number(props.zIndex) + 1 : 2, // place it above the menu 👇 width: props.width ?? '100%', + backgroundColor: state.isDisabled + ? theme.palette.brand25 || `${theme.palette.brand}40` + : theme.palette.dark, + opacity: state.isDisabled ? 0.7 : 1, }), menu: (base, state) => ({ ...base, @@ -111,12 +116,15 @@ export const InputSelect = (props: SelectTempleDaoProps) => { padding: 0, }), dropdownIndicator: (base, state) => ({ - color: state.isFocused + color: state.isDisabled + ? theme.palette.brandDark + : state.isFocused ? theme.palette.brandLight : theme.palette.brand, display: 'flex', transform: state.isFocused ? 'rotateX(180deg)' : 'none', transition: 'transform 250ms linear', + opacity: state.isDisabled ? 0.5 : 1, }), }} /> diff --git a/apps/dapp/src/components/Pages/Safe/admin/BatchAuctionForm.tsx b/apps/dapp/src/components/Pages/Safe/admin/BatchAuctionForm.tsx new file mode 100644 index 000000000..db684e4a6 --- /dev/null +++ b/apps/dapp/src/components/Pages/Safe/admin/BatchAuctionForm.tsx @@ -0,0 +1,537 @@ +import styled from 'styled-components'; +import { useState } from 'react'; +import { InputSelect, Option } from 'components/InputSelect/InputSelect'; +import { Button } from 'components/Button/Button'; +import env from 'constants/env'; +import { getAppConfig } from 'constants/newenv'; +import { SpiceAuction__factory } from 'types/typechain/factories/contracts/templegold/SpiceAuction__factory'; +import { ERC20__factory } from 'types/typechain/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory'; +import { useSafeTransactions } from 'safe/safeContext'; +import { OperationType } from '@safe-global/safe-core-sdk-types'; +import { useNotification } from 'providers/NotificationProvider'; +import { useWallet } from 'providers/WalletProvider'; +import { parseUnits } from 'ethers/lib/utils'; + +// Get auction options from app config +const getAuctionOptions = (): Option[] => { + const spiceAuctions = getAppConfig().spiceBazaar.spiceAuctions || []; + if (spiceAuctions.length === 0) { + return [{ label: 'No auctions available', value: '' }]; + } + return spiceAuctions.map((auction) => ({ + label: `TGLD/${auction.auctionTokenSymbol}`, + value: auction.contractConfig.address, + })); +}; + +const AUCTION_OPTIONS = getAuctionOptions(); + +// Bid token options - currently only TGLD +// TODO: In the future, derive from auction config +const BID_TOKEN_OPTIONS: Option[] = [{ label: 'TGLD', value: 'tgld' }]; + +type TabType = 'config' | 'fund'; + +export const BatchAuctionForm = () => { + const [activeTab, setActiveTab] = useState('config'); + const { proposeTransaction } = useSafeTransactions(); + const { openNotification } = useNotification(); + const { wallet } = useWallet(); + + // Config + const configWallet = env.spiceAuctionAdmin.multisigAddress; + const fundWallet = env.spiceAuctionAdmin.cosechaSegundaAddress; + const recipientWallet = env.spiceAuctionAdmin.defaultRecipientAddress; + const auctionAddress = env.spiceAuctionAdmin.auctionAddress; + const templeGoldAddress = env.spiceAuctionAdmin.templeGoldAddress; + + // Config form state + const [duration, setDuration] = useState(''); + const [waitPeriod, setWaitPeriod] = useState(''); + const [minimumDistributedAmount, setMinimumDistributedAmount] = + useState(''); + const isTempleGoldAuctionToken = false; + + // Funding form state + const [tokenAmount, setTokenAmount] = useState(''); + const [startTime, setStartTime] = useState(''); + + const handleCreateBatch = async () => { + if (!proposeTransaction) { + openNotification({ + title: 'Please connect your wallet', + hash: '', + isError: true, + }); + return; + } + + try { + const spiceAuctionInterface = SpiceAuction__factory.createInterface(); + const erc20Interface = ERC20__factory.createInterface(); + + if (activeTab === 'config') { + // Encode setAuctionConfig call + const configData = spiceAuctionInterface.encodeFunctionData( + 'setAuctionConfig', + [ + { + duration: Number(duration), + waitPeriod: Number(waitPeriod), + minimumDistributedAuctionToken: parseUnits( + minimumDistributedAmount, + 18 + ).toString(), + isTempleGoldAuctionToken: false, + recipient: recipientWallet, + }, + ] + ); + + await proposeTransaction( + [ + { + to: auctionAddress, + value: '0', + data: configData, + operation: OperationType.Call, + }, + ], + configWallet + ); + + openNotification({ + title: 'Auction config transaction proposed', + hash: '', + }); + } else { + // Fund tab - encode approve + fundNextAuction + const bidTokenAddress = templeGoldAddress; + const amountWei = parseUnits(tokenAmount, 18).toString(); + const startTimeUnix = Math.floor(new Date(startTime).getTime() / 1000); + + // Approve transaction + const approveData = erc20Interface.encodeFunctionData('approve', [ + auctionAddress, + amountWei, + ]); + + // Fund auction transaction + const fundData = spiceAuctionInterface.encodeFunctionData( + 'fundNextAuction', + [amountWei, startTimeUnix] + ); + + await proposeTransaction( + [ + { + to: bidTokenAddress, + value: '0', + data: approveData, + operation: OperationType.Call, + }, + { + to: auctionAddress, + value: '0', + data: fundData, + operation: OperationType.Call, + }, + ], + fundWallet + ); + + openNotification({ + title: 'Approve & fund transaction proposed', + hash: '', + }); + } + } catch (error) { + console.error('Error proposing transaction:', error); + const errorMessage = + error instanceof Error ? error.message : 'Unknown error occurred'; + openNotification({ + title: `Error proposing transaction: ${errorMessage}`, + hash: '', + isError: true, + }); + } + }; + + const renderConfigTab = () => { + // Convert minimum amount to wei for preview + const minAmountWei = minimumDistributedAmount + ? parseUnits(minimumDistributedAmount, 18).toString() + : '0'; + + return ( + + + + + Executor Multisig + {configWallet} + + + + + + + ENA Auction + {auctionAddress} + + + + + + + setDuration(e.target.value)} + /> + + + + + setWaitPeriod(e.target.value)} + /> + + + + + + setMinimumDistributedAmount(e.target.value)} + /> + + + + + + Overlord Bot + {recipientWallet} + + + + + Preview + + Wallet: Executor Multisig ({configWallet.slice(0, 8)}... + {configWallet.slice(-6)}) + + + 1. setAuctionConfig({`{`} + duration: {duration || '0'}, waitPeriod: {waitPeriod || '0'}, + minAmount: {minAmountWei}, isTGLD:{' '} + {isTempleGoldAuctionToken.toString()}, recipient: {recipientWallet} + {`}`}) + + + + +