diff --git a/app/package.json b/app/package.json index db0bb95bad..f0505ccdc0 100644 --- a/app/package.json +++ b/app/package.json @@ -10,6 +10,7 @@ "3box-comments-react": "0.0.8", "@apollo/react-hooks": "^3.1.3", "@fvictorio/newton-raphson-method": "^1.0.5", + "@gelatonetwork/core": "^1.6.0", "@gnosis.pm/safe-apps-sdk": "1.0.1", "@kleros/erc-792": "^7.0.0", "@kleros/gtcr-encoder": "^1.1.3", diff --git a/app/src/common/constants.ts b/app/src/common/constants.ts index 53e20d04da..6d6988adc8 100644 --- a/app/src/common/constants.ts +++ b/app/src/common/constants.ts @@ -72,6 +72,11 @@ export const Logo = OmenLogo export const DEFAULT_ARBITRATOR: KnownArbitrator = 'kleros' +export const DEFAULT_GELATO_CONDITION: KnownGelatoCondition = 'time' + +export const GELATO_MIN_USD_THRESH = 250 +export const GELATO_ACTIVATED = true + export const DEFAULT_TOKEN = 'dai' export const DOCUMENT_TITLE = 'Omen' diff --git a/app/src/components/common/form/date_field/index.tsx b/app/src/components/common/form/date_field/index.tsx index d80c822ab7..4e8735aeae 100644 --- a/app/src/components/common/form/date_field/index.tsx +++ b/app/src/components/common/form/date_field/index.tsx @@ -9,6 +9,7 @@ import { CommonDisabledCSS } from '../common_styled' interface Props { disabled?: boolean minDate?: any + maxDate?: any name: string onChange: any placeholder?: string @@ -106,7 +107,7 @@ const CalendarPortal = (props: CalendarPortalProps) => { } export const DateField = (props: Props) => { - const { disabled, minDate, name, onChange, placeholder, selected, ...restProps } = props + const { disabled, maxDate, minDate, name, onChange, placeholder, selected, ...restProps } = props const handleChange = (date: Maybe) => { onChange(date ? convertLocalToUTC(date) : date) @@ -123,6 +124,7 @@ export const DateField = (props: Props) => { calendarClassName="customCalendar" dateFormat="MMMM d, yyyy h:mm aa" disabled={disabled} + maxDate={maxDate} minDate={minDate} name={name} onChange={handleChange} diff --git a/app/src/components/common/icons/IconAlert.tsx b/app/src/components/common/icons/IconAlert.tsx index f8366c2b16..d57bd36b85 100644 --- a/app/src/components/common/icons/IconAlert.tsx +++ b/app/src/components/common/icons/IconAlert.tsx @@ -1,16 +1,19 @@ import React from 'react' interface Props { + fill?: string size?: string + bg?: string } export const IconAlert = (props: Props) => { - const { size = '20' } = props + const { bg, fill = '#E57373', size = '20' } = props return ( + ) diff --git a/app/src/components/common/icons/IconClock.tsx b/app/src/components/common/icons/IconClock.tsx new file mode 100644 index 0000000000..522bf25e41 --- /dev/null +++ b/app/src/components/common/icons/IconClock.tsx @@ -0,0 +1,14 @@ +import React from 'react' + +export const IconClock = () => ( + + + + +) diff --git a/app/src/components/common/icons/IconCustomize.tsx b/app/src/components/common/icons/IconCustomize.tsx new file mode 100644 index 0000000000..6f6af50aa0 --- /dev/null +++ b/app/src/components/common/icons/IconCustomize.tsx @@ -0,0 +1,21 @@ +import React from 'react' + +interface Props { + fill?: string +} + +export const IconCustomize = (props: Props) => { + const { fill = '#37474F' } = props + return ( + + + + ) +} diff --git a/app/src/components/common/icons/IconGelato.tsx b/app/src/components/common/icons/IconGelato.tsx new file mode 100644 index 0000000000..a978ada06f --- /dev/null +++ b/app/src/components/common/icons/IconGelato.tsx @@ -0,0 +1,8 @@ +import React from 'react' + +export const IconGelato = () => ( + +) diff --git a/app/src/components/common/icons/IconTick.tsx b/app/src/components/common/icons/IconTick.tsx index 9e4d5e5107..5feacf73d5 100644 --- a/app/src/components/common/icons/IconTick.tsx +++ b/app/src/components/common/icons/IconTick.tsx @@ -3,6 +3,8 @@ import styled from 'styled-components' interface Props { disabled?: boolean selected?: boolean + fill?: string + stroke?: string } const StyledSvg = styled.svg<{ disabled?: boolean; selected?: boolean }>` filter: ${props => (props.selected ? 'saturate(0) brightness(2)' : props.disabled ? 'saturate(0)' : '')}; @@ -10,7 +12,7 @@ const StyledSvg = styled.svg<{ disabled?: boolean; selected?: boolean }>` ` export const IconTick = (props: Props) => { - const { disabled = false, selected = false } = props + const { disabled = false, fill = '#7986CB', selected = false, stroke = '#7986CB' } = props return ( { > diff --git a/app/src/components/common/icons/index.ts b/app/src/components/common/icons/index.ts index 529e37776f..aacba39fdc 100644 --- a/app/src/components/common/icons/index.ts +++ b/app/src/components/common/icons/index.ts @@ -3,6 +3,7 @@ export { IconArrowBack } from './IconArrowBack' export { IconChevronLeft } from './IconChevronLeft' export { IconChevronRight } from './IconChevronRight' export { IconClose } from './IconClose' +export { IconCustomize } from './IconCustomize' export { IconDragonBall } from './IconDragonBall' export { IconDxDao } from './IconDxDao' export { IconTick } from './IconTick' diff --git a/app/src/components/market/common/gelato_conditions/index.tsx b/app/src/components/market/common/gelato_conditions/index.tsx new file mode 100644 index 0000000000..bcd7ffff7b --- /dev/null +++ b/app/src/components/market/common/gelato_conditions/index.tsx @@ -0,0 +1,49 @@ +import React from 'react' + +import { useConnectedWeb3Context } from '../../../../hooks' +import { getGelatoConditionByNetwork } from '../../../../util/networks' +import { GelatoData } from '../../../../util/types' +import { Dropdown, DropdownDirection, DropdownItemProps, DropdownPosition } from '../../../common/form/dropdown' + +interface Props { + disabled?: boolean + onChangeGelatoCondition: (gelatoData: GelatoData) => any + value: GelatoData +} + +export const GelatoConditions = (props: Props) => { + const { disabled, onChangeGelatoCondition, value } = props + const context = useConnectedWeb3Context() + const networkId = context.networkId + const gelatoConditions = getGelatoConditionByNetwork(networkId) + + const onChange = (id: KnownGelatoCondition) => { + for (const condition of gelatoConditions) { + if (condition.id === id) { + onChangeGelatoCondition(condition) + } + } + } + + const conditionOptions: Array = gelatoConditions.map((condition: GelatoData) => { + return { + content: condition.id + ' . .', + onClick: () => { + onChange(condition.id) + console.warn(`ID: ${condition.id}`) + }, + } + }) + + const currentItem = gelatoConditions.findIndex(condition => condition.id === value.id) - 1 + + return ( + + ) +} diff --git a/app/src/components/market/common/gelato_scheduler/index.tsx b/app/src/components/market/common/gelato_scheduler/index.tsx new file mode 100644 index 0000000000..61fcc9c36e --- /dev/null +++ b/app/src/components/market/common/gelato_scheduler/index.tsx @@ -0,0 +1,525 @@ +import React, { DOMAttributes, useEffect } from 'react' +import styled from 'styled-components' + +import { GELATO_MIN_USD_THRESH } from '../../../../common/constants' +import { formatDate } from '../../../../util/tools' +import { GelatoData } from '../../../../util/types' +import { DateField, FormRow } from '../../../common' +import { IconAlert } from '../../../common/icons/IconAlert' +import { IconClock } from '../../../common/icons/IconClock' +import { IconCustomize } from '../../../common/icons/IconCustomize' +import { IconGelato } from '../../../common/icons/IconGelato' +import { IconTick } from '../../../common/icons/IconTick' +import { GelatoConditions } from '../gelato_conditions' + +const TaskInfoWrapper = styled.div` + display: flex; + flex-direction: row; + font-weight: 500; +` + +const TaskInfo = styled.div<{ color?: string }>` + color: ${props => (props.color ? props.color : props.theme.colors.textColorLightish)}; + font-size: 14px; + margin: 0 6px 0 0; + text-align: left; + vertical-align: middle; + display: inline-block; +` +const GelatoExtendedWrapper = styled.div<{ isStretch?: boolean }>` + display: flex; + flex-direction: column; + align-items: ${props => (props.isStretch ? 'stretch' : 'center')}; + border-radius: 4px; + margin-top: 20px; + padding-top: 16px; + padding-bottom: 34px; + padding-right: 25px; + padding-left: 25px; + border: 1px solid ${props => props.theme.borders.borderDisabled}; +` + +const TaskStatusWrapper = styled.div` + display: flex; + line-height: 16px; + font-size: 14px; + flex-direction: column; + align-items: flex-end; + justify-content: center; + margin-left: 8px; + flex-wrap: nowrap; + width: 90%; +` + +const ConditionWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; + margin-bottom: 20px; + vertical-align: bottom; + justify-content: space-between; +` + +const ConditionTitle = styled.div` + font-size: 14px; + letter-spacing: 0.2px; + line-height: 1.4; + text-align: left; + color: #37474f; +` + +const IconStyled = styled.div` + line-height: 1; + svg { + width: 0.9rem; + height: 0.9rem; + vertical-align: inherit; + } +` +const GelatoIconCircle = styled.button<{ active?: boolean }>` + align-items: center; + background-color: #fff; + border-radius: 50%; + border: 1px solid ${props => props.theme.colors.tertiary}; + display: flex; + flex-shrink: 0; + height: ${props => props.theme.buttonCircle.dimensions}; + justify-content: center; + outline: none; + padding: 0; + transition: border-color 0.15s linear; + user-select: none; + width: ${props => props.theme.buttonCircle.dimensions}; +` + +const Wrapper = styled.div` + border-radius: 4px; + border: ${({ theme }) => theme.borders.borderLineDisabled}; + padding: 18px 25px; + margin-bottom: 20px; +` + +const Title = styled.h2` + color: ${props => props.theme.colors.textColorDark}; + font-size: 16px; + letter-spacing: 0.4px; + line-height: 1.2; + margin: 0 0 20px; + font-weight: 400; +` + +const DescriptionWrapper = styled.div` + align-items: center; + display: flex; +` + +const CheckService = styled.div<{ isActive: boolean; disabled?: boolean }>` + width: 40px; + height: 40px; + border-radius: 50%; + text-align: center; + border: 1px solid ${props => (props.isActive ? props.theme.colors.transparent : props.theme.colors.tertiary)}; + background-color: ${props => (props.isActive ? props.theme.colors.clickable : props.theme.colors.mainBodyBackground)}; + display: flex; + align-items: center; + justify-content: center; + &:hover { + border: 1px solid ${props => (props.isActive ? 'none' : props.theme.colors.tertiaryDark)}; + cursor: ${props => (props.disabled ? 'not-allowed' : 'pointer')}; + } + &:active { + border: none; + } + path { + fill: ${props => (props.isActive ? props.theme.colors.mainBodyBackground : props.theme.textfield.textColorDark)}; + } +` + +const ToggleService = styled.div<{ isActive: boolean; disabled?: boolean }>` + width: 40px; + height: 40px; + border-radius: 50%; + text-align: center; + border: 1px solid ${props => (props.isActive ? props.theme.colors.transparent : props.theme.colors.tertiary)}; + background-color: ${props => + props.isActive ? props.theme.buttonSecondary.backgroundColor : props.theme.colors.mainBodyBackground}; + display: flex; + align-items: center; + justify-content: center; + &:hover { + border: 1px solid ${props => (props.isActive ? 'none' : props.theme.colors.tertiaryDark)}; + cursor: ${props => (props.disabled ? 'none' : 'pointer')}; + } + &:active { + border: none; + } + path { + fill: ${props => (props.isActive ? '#3F51B5' : '#37474F')}; + } +` + +const ServiceWrapper = styled.div` + color: ${props => props.theme.colors.textColorLightish}; + font-size: ${props => props.theme.textfield.fontSize}; + letter-spacing: 0.2px; + line-height: 1.4; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; +` + +const ServiceIconWrapper = styled.div` + display: flex; + padding-right: 16px; + text-align: center; + -webkit-box-align: center; +` + +const ServiceTextWrapper = styled.div<{ short?: boolean }>` + width: ${props => (props.short ? '50%' : '90%')}; +` + +const ServiceCheckWrapper = styled.div` + width: 10%; + color: transparent; +` +const ServiceToggleWrapper = styled.div` + width: 10%; + color: transparent; + margin-right: 12px; +` + +const ServiceTokenDetails = styled.div` + width: 100%; + display: flex; +` + +const GelatoServiceDescription = styled.div` + color: ${props => props.theme.colors.textColorLightish}; + font-size: ${props => props.theme.textfield.fontSize}; + letter-spacing: 0.2px; + line-height: 1.4; + margin: 0 8px 0 0; + width: 100%; +` + +const TextHeading = styled.div` + color: ${props => props.theme.colors.textColorDark}; + font-weight: 500; + font-size: 14px; + line-height: 16px; + display: flex; + align-items: center; + letter-spacing: 0.2px; + margin: 0px 6px 0px 0px; + width: 200px; +` + +const TextBody = styled.div<{ margins?: string; textAlignRight?: boolean }>` + line-height: 16px; + font-size: 14px; + height: 16px; + color: #86909e; + margin: ${props => (props.margins ? props.margins : '6px 6px 0px 0px')}; + text-align: ${props => (props.textAlignRight ? 'right' : 'left')}; +` + +const TextBodyMarker = styled.span<{ color?: string }>` + color: ${props => (props.color ? props.color : props.theme.colors.textColorLightish)}; + font-weight: 500; +` + +export type GelatoSchedulerProps = DOMAttributes & { + resolution: Date + gelatoData: GelatoData + isScheduled: boolean + execSuccess?: boolean + belowMinimum?: boolean + minimum?: number + collateralToWithdraw?: string + collateralSymbol?: string + taskStatus?: string + etherscanLink?: string + handleGelatoDataChange: (gelatoData: GelatoData) => any + handleGelatoDataInputChange: (newDate: Date | null) => any +} + +export const GelatoScheduler: React.FC = (props: GelatoSchedulerProps) => { + const { + belowMinimum, + collateralSymbol, + collateralToWithdraw, + etherscanLink, + gelatoData, + handleGelatoDataChange, + handleGelatoDataInputChange, + isScheduled, + minimum, + resolution, + taskStatus, + } = props + + const [active, setActive] = React.useState(false) + const [customizable, setCustomizable] = React.useState(false) + + // Set gelatoInput default to resolution - 3 days (or fifteen minutes from now) + const resolutionDateCopy = new Date(resolution) + const now = new Date() + now.setMinutes(now.getMinutes() + 30) + if (!gelatoData.input) { + const defaultGelatoDate = new Date(resolutionDateCopy.setDate(resolutionDateCopy.getDate() - 3)) + if (now.getTime() - defaultGelatoDate.getTime() > 0) { + gelatoData.input = now + } else { + gelatoData.input = defaultGelatoDate + } + } + + const daysBeforeWithdraw = Math.round( + (Date.parse(resolution.toString()) - Date.parse(gelatoData.input.toString())) / 1000 / 60 / 60 / 24, + ) + + const daysUntilWithdraw = Math.round( + (Date.parse(gelatoData.input.toString()) - Date.parse(new Date().toString())) / 1000 / 60 / 60 / 24, + ) + + const toggleActive = () => { + const newGelatoCondition = { + ...gelatoData, + } + const isTrue = active ? false : true + newGelatoCondition.shouldSubmit = isTrue + handleGelatoDataChange(newGelatoCondition) + setActive(isTrue) + } + + const toggleCustomizable = () => { + setCustomizable(!customizable) + } + + const getCorrectTimeString = (withdrawalDate: Date) => { + const daysUntilAutoWithdraw = Math.round( + (Date.parse(withdrawalDate.toString()) - Date.parse(new Date().toString())) / 1000 / 60 / 60 / 24, + ) + const hoursUntilAutoWithdraw = Math.round( + (Date.parse(withdrawalDate.toString()) - Date.parse(new Date().toString())) / 1000 / 60 / 60, + ) + + const minUntilAutoWithdraw = Math.round( + (Date.parse(withdrawalDate.toString()) - Date.parse(new Date().toString())) / 1000 / 60, + ) + + let displayText = `${daysUntilAutoWithdraw} days` + if (daysUntilAutoWithdraw === 0) + if (hoursUntilAutoWithdraw === 0) + if (minUntilAutoWithdraw === 0) displayText = `now` + else displayText = `${minUntilAutoWithdraw} minutes` + else displayText = `${hoursUntilAutoWithdraw} hours` + + return displayText + } + + useEffect(() => { + if (belowMinimum) { + setActive(false) + } + }, [belowMinimum]) + + const getTaskStatus = (status?: string, withdrawlDate?: Date) => { + if (withdrawlDate && status) { + const displayText = getCorrectTimeString(withdrawlDate) + switch (status) { + case 'awaitingExec': + return ( + + {`scheduled in ${displayText}`} + + + + + ) + case 'execSuccess': + return ( + + {`successful`} + + + + + ) + case 'execReverted': + return ( + + {`failed`} + + + + + ) + case 'canceled': + return ( + + {`canceled `} + + + + + ) + } + } + } + + return ( + + Recommended Services + + + + + + + + + + {!isScheduled && ( + <> + + Gelato + {active && !belowMinimum && ( + + Auto-Withdrawal scheduled in + {daysUntilWithdraw} days + + )} + {!active && ( + + Schedule withdrawal with min. funding of + + {`${ + minimum + ? ` ${Math.ceil(minimum * 1000) / 1000} ${collateralSymbol}` + : ` ${GELATO_MIN_USD_THRESH} USD` + }`} + + + )} + + {active && ( + + + + + + )} + + + + + + + )} + {isScheduled && taskStatus && ( + <> + + + {`Auto-Withdraw ${ + taskStatus === 'execSuccess' ? '' : `${collateralToWithdraw} ${collateralSymbol}` + }`} + + Powered by Gelato Network + + + {getTaskStatus(taskStatus, gelatoData.input)} + + {`${formatDate(gelatoData.input)}`} + + + + )} + + + + + {taskStatus === 'awaitingExec' && ( + + + {`Gelato will automatically withdraw your liquidity of ${collateralToWithdraw} ${collateralSymbol} on ${formatDate( + gelatoData.input, + )} (with a network fee deducted from the withdrawn ${collateralSymbol}). Cancel the auto-withdraw by manually withdrawing your liquidity.`} + + + )} + {taskStatus === 'execReverted' && ( + + + {`Your provided liquidity was insufficient on ${formatDate( + gelatoData.input, + )} to pay for for the withdrawal transaction `} + + + here + + + {'.'} + + + )} + {taskStatus === 'execSuccess' && ( + + + {`Your provided liquidity was successfully withdrawn on ${formatDate( + gelatoData.input, + )}. Check out the transaction `} + + + here + + + {'.'} + + + )} + {customizable && active && !taskStatus && ( + + + Withdraw Condition + + } + style={{ marginTop: '0' }} + /> + + + Withdraw Date and Time + + } + style={{ marginTop: '0' }} + /> + + + Gelato will automatically withdraw your liquidity + {` ${daysBeforeWithdraw} day(s) before `} + the market will close on + {` ${formatDate(gelatoData.input)}`} + + + )} + + ) +} diff --git a/app/src/components/market/sections/market_create/market_wizard_creator.tsx b/app/src/components/market/sections/market_create/market_wizard_creator.tsx index 7caa1166c3..fd759c8197 100644 --- a/app/src/components/market/sections/market_create/market_wizard_creator.tsx +++ b/app/src/components/market/sections/market_create/market_wizard_creator.tsx @@ -7,9 +7,9 @@ import { IMPORT_QUESTION_ID_KEY, MARKET_FEE } from '../../../../common/constants import { useConnectedWeb3Context } from '../../../../hooks/connectedWeb3' import { queryTopCategories } from '../../../../queries/markets_home' import { MarketCreationStatus } from '../../../../util/market_creation_status_data' -import { getArbitrator, getDefaultArbitrator, getDefaultToken } from '../../../../util/networks' +import { getArbitrator, getDefaultArbitrator, getDefaultGelatoData, getDefaultToken } from '../../../../util/networks' import { limitDecimalPlaces } from '../../../../util/tools' -import { Arbitrator, GraphResponseTopCategories, MarketData, Question, Token } from '../../../../util/types' +import { Arbitrator, GelatoData, GraphResponseTopCategories, MarketData, Question, Token } from '../../../../util/types' import { BigNumberInputReturn } from '../../../common/form/big_number_input' import { AskQuestionStep, FundingAndFeeStep, MenuStep } from './steps' @@ -29,6 +29,7 @@ export const MarketWizardCreator = (props: Props) => { const defaultCollateral = getDefaultToken(networkId) const defaultArbitrator = getDefaultArbitrator(networkId) + const defaultGelatoData = getDefaultGelatoData(networkId) const getImportQuestionId = () => { const reQuestionId = /(0x[0-9A-Fa-f]{64})/ @@ -61,6 +62,7 @@ export const MarketWizardCreator = (props: Props) => { ], question: '', resolution: null, + gelatoData: defaultGelatoData, spread: MARKET_FEE, lowerBound: null, upperBound: null, @@ -270,6 +272,21 @@ export const MarketWizardCreator = (props: Props) => { } setMarketdata(newMarketData) } + const handleGelatoDataChange = (gelatoData: GelatoData) => { + const newMarketData = { + ...marketData, + gelatoData, + } + setMarketdata(newMarketData) + } + + const handleGelatoDataInputChange = (input: Date | null) => { + const newMarketData = { + ...marketData, + } + newMarketData.gelatoData.input = input + setMarketdata(newMarketData) + } const submit = (isScalar: boolean) => { callback(marketData as MarketData, isScalar) @@ -283,6 +300,8 @@ export const MarketWizardCreator = (props: Props) => { back={back} handleChange={handleChange} handleCollateralChange={handleCollateralChange} + handleGelatoDataChange={handleGelatoDataChange} + handleGelatoDataInputChange={handleGelatoDataInputChange} handleTradingFeeChange={handleTradingFeeChange} marketCreationStatus={marketCreationStatus} resetTradingFee={resetTradingFee} diff --git a/app/src/components/market/sections/market_create/market_wizard_creator_container.tsx b/app/src/components/market/sections/market_create/market_wizard_creator_container.tsx index 1dc6ae6c3d..e45427ea62 100644 --- a/app/src/components/market/sections/market_create/market_wizard_creator_container.tsx +++ b/app/src/components/market/sections/market_create/market_wizard_creator_container.tsx @@ -21,7 +21,7 @@ const MarketWizardCreatorContainer: FC = () => { const history = useHistory() const [isModalOpen, setModalState] = useState(false) - const { conditionalTokens, marketMakerFactory, realitio } = useContracts(context) + const { conditionalTokens, gelato, marketMakerFactory, realitio } = useContracts(context) const [marketCreationStatus, setMarketCreationStatus] = useState(MarketCreationStatus.ready()) const [marketMakerAddress, setMarketMakerAddress] = useState(null) @@ -56,6 +56,7 @@ const MarketWizardCreatorContainer: FC = () => { conditionalTokens, realitio, marketMakerFactory, + gelato, }) setMarketMakerAddress(marketMakerAddress) @@ -67,6 +68,7 @@ const MarketWizardCreatorContainer: FC = () => { conditionalTokens, realitio, marketMakerFactory, + gelato, }) setMarketMakerAddress(marketMakerAddress) diff --git a/app/src/components/market/sections/market_create/steps/items/funding_and_fee_step.tsx b/app/src/components/market/sections/market_create/steps/items/funding_and_fee_step.tsx index 08ea057518..978bbe5064 100644 --- a/app/src/components/market/sections/market_create/steps/items/funding_and_fee_step.tsx +++ b/app/src/components/market/sections/market_create/steps/items/funding_and_fee_step.tsx @@ -1,6 +1,6 @@ import { Zero } from 'ethers/constants' import { BigNumber } from 'ethers/utils' -import React, { ChangeEvent, useEffect, useMemo, useState } from 'react' +import React, { ChangeEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -8,12 +8,14 @@ import { DEFAULT_TOKEN_ADDRESS, DEFAULT_TOKEN_ADDRESS_RINKEBY, DOCUMENT_FAQ, + GELATO_ACTIVATED, MAX_MARKET_FEE, } from '../../../../../../common/constants' import { useCollateralBalance, useConnectedCPKContext, useConnectedWeb3Context, + useContracts, useCpkAllowance, useCpkProxy, } from '../../../../../../hooks' @@ -23,7 +25,7 @@ import { MarketCreationStatus } from '../../../../../../util/market_creation_sta import { getNativeAsset, pseudoNativeAssetAddress } from '../../../../../../util/networks' import { RemoteData } from '../../../../../../util/remote_data' import { formatBigNumber, formatDate, formatNumber } from '../../../../../../util/tools' -import { Arbitrator, Ternary, Token } from '../../../../../../util/types' +import { Arbitrator, GelatoData, Ternary, Token } from '../../../../../../util/types' import { Button } from '../../../../../button' import { ButtonType } from '../../../../../button/button_styling_types' import { BigNumberInput, SubsectionTitle, TextfieldCustomPlaceholder } from '../../../../../common' @@ -48,6 +50,7 @@ import { import { CreateCard } from '../../../../common/create_card' import { CurrencySelector } from '../../../../common/currency_selector' import { DisplayArbitrator } from '../../../../common/display_arbitrator' +import { GelatoScheduler } from '../../../../common/gelato_scheduler' import { GridTransactionDetails } from '../../../../common/grid_transaction_details' import { MarketScale } from '../../../../common/market_scale' import { SetAllowance } from '../../../../common/set_allowance' @@ -174,6 +177,7 @@ interface Props { arbitrator: Arbitrator spread: number funding: BigNumber + gelatoData: GelatoData outcomes: Outcome[] loadedQuestionId: Maybe verifyLabel?: string @@ -185,6 +189,8 @@ interface Props { marketCreationStatus: MarketCreationStatus handleCollateralChange: (collateral: Token) => void handleTradingFeeChange: (fee: string) => void + handleGelatoDataChange: (gelatoData: GelatoData) => any + handleGelatoDataInputChange: (newDate: Date | null) => any handleChange: (event: ChangeEvent | ChangeEvent | BigNumberInputReturn) => any resetTradingFee: () => void state: string @@ -196,12 +202,16 @@ const FundingAndFeeStep: React.FC = (props: Props) => { const balance = useSelector((state: BalanceState): Maybe => state.balance && new BigNumber(state.balance)) const dispatch = useDispatch() const { account, library: provider } = context + const { gelato } = useContracts(context) + const signer = useMemo(() => provider.getSigner(), [provider]) const { back, handleChange, handleCollateralChange, + handleGelatoDataChange, + handleGelatoDataInputChange, handleTradingFeeChange, marketCreationStatus, resetTradingFee, @@ -271,6 +281,21 @@ const FundingAndFeeStep: React.FC = (props: Props) => { // eslint-disable-next-line }, [maybeCollateralBalance]) + const [belowGelatoMinimum, setBelowGelatoMinimum] = useState(false) + const [gelatoMinimum, setGelatoMinimum] = useState(0) + + const checkGelatoMinimum = useCallback(async () => { + if (cpk) { + const { belowMinimum, minimum } = await cpk.isBelowGelatoMinimum(funding, collateral, gelato) + setBelowGelatoMinimum(belowMinimum) + setGelatoMinimum(minimum) + } + }, [cpk, collateral, funding, gelato]) + + useEffect(() => { + checkGelatoMinimum() + }, [checkGelatoMinimum]) + useEffect(() => { setIsNegativeDepositAmount(formatBigNumber(funding, collateral.decimals).includes('-')) }, [funding, collateral.decimals]) @@ -513,6 +538,18 @@ const FundingAndFeeStep: React.FC = (props: Props) => { hyperlinkDescription={''} /> )} + {GELATO_ACTIVATED && state !== 'SCALAR' && ( + + )} {showSetAllowance && ( = (props: Props) => { {!MarketCreationStatus.is.ready(marketCreationStatus) && !MarketCreationStatus.is.error(marketCreationStatus) ? ( - + ) : null} ) diff --git a/app/src/components/market/sections/market_pooling/market_pool_liquidity.tsx b/app/src/components/market/sections/market_pooling/market_pool_liquidity.tsx index eb4434c76d..f2c099fa49 100644 --- a/app/src/components/market/sections/market_pooling/market_pool_liquidity.tsx +++ b/app/src/components/market/sections/market_pooling/market_pool_liquidity.tsx @@ -1,10 +1,11 @@ +import { useInterval } from '@react-corekit/use-interval' import { Zero } from 'ethers/constants' import { BigNumber } from 'ethers/utils' -import React, { useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { RouteComponentProps, useHistory, withRouter } from 'react-router-dom' import styled from 'styled-components' -import { DOCUMENT_FAQ } from '../../../../common/constants' +import { DOCUMENT_FAQ, FETCH_DETAILS_INTERVAL, GELATO_ACTIVATED } from '../../../../common/constants' import { useCollateralBalance, useConnectedCPKContext, @@ -14,8 +15,10 @@ import { useCpkProxy, useFundingBalance, } from '../../../../hooks' +import { useGelatoSubmittedTasks } from '../../../../hooks/useGelatoSubmittedTasks' +import { ERC20Service } from '../../../../services' import { getLogger } from '../../../../util/logger' -import { getNativeAsset, getWrapToken, pseudoNativeAssetAddress } from '../../../../util/networks' +import { getDefaultGelatoData, getNativeAsset, getWrapToken, pseudoNativeAssetAddress } from '../../../../util/networks' import { RemoteData } from '../../../../util/remote_data' import { calcAddFundingSendAmounts, @@ -24,7 +27,15 @@ import { formatBigNumber, formatNumber, } from '../../../../util/tools' -import { MarketDetailsTab, MarketMakerData, OutcomeTableValue, Status, Ternary, Token } from '../../../../util/types' +import { + GelatoData, + MarketDetailsTab, + MarketMakerData, + OutcomeTableValue, + Status, + Ternary, + Token, +} from '../../../../util/types' import { Button, ButtonContainer, ButtonTab } from '../../../button' import { ButtonType } from '../../../button/button_styling_types' import { BigNumberInput, TextfieldCustomPlaceholder, TitleValue } from '../../../common' @@ -33,6 +44,7 @@ import { FullLoading } from '../../../loading' import { ModalTransactionResult } from '../../../modal/modal_transaction_result' import { CurrenciesWrapper, GenericError, TabsGrid } from '../../common/common_styled' import { CurrencySelector } from '../../common/currency_selector' +import { GelatoScheduler } from '../../common/gelato_scheduler' import { GridTransactionDetails } from '../../common/grid_transaction_details' import { OutcomeTable } from '../../common/outcome_table' import { SetAllowance } from '../../common/set_allowance' @@ -116,13 +128,14 @@ const logger = getLogger('Market::Fund') const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const { fetchGraphMarketMakerData, marketMakerData } = props + const { address: marketMakerAddress, balances, fee, totalEarnings, totalPoolShares, userEarnings } = marketMakerData const history = useHistory() const context = useConnectedWeb3Context() const { account, library: provider, networkId } = context const cpk = useConnectedCPKContext() - const { buildMarketMaker, conditionalTokens } = useContracts(context) + const { buildMarketMaker, conditionalTokens, gelato } = useContracts(context) const marketMaker = buildMarketMaker(marketMakerAddress) const signer = useMemo(() => provider.getSigner(), [provider]) @@ -180,6 +193,27 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const hasEnoughAllowance = RemoteData.mapToTernary(allowance, allowance => allowance.gte(amountToFund || Zero)) const hasZeroAllowance = RemoteData.mapToTernary(allowance, allowance => allowance.isZero()) + // Gelato + const { etherscanLink, refetch, submittedTaskReceiptWrapper, withdrawDate } = useGelatoSubmittedTasks( + cpk ? cpk.address : null, + marketMakerAddress, + context, + ) + + useInterval(() => { + if (refetch) refetch() + }, FETCH_DETAILS_INTERVAL) + + useEffect(() => { + if (refetch) refetch() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const defaultGelatoData = getDefaultGelatoData(networkId) + const [gelatoData, setGelatoData] = useState(defaultGelatoData) + const [belowGelatoMinimum, setBelowGelatoMinimum] = useState(false) + const [gelatoMinimum, setGelatoMinimum] = useState(0) + const poolTokens = calcPoolTokens( amountToFund || Zero, balances.map(b => b.holdings), @@ -204,6 +238,11 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { amount.lt(min) ? amount : min, ) + const withGelato = + (gelatoData.shouldSubmit && !submittedTaskReceiptWrapper) || + (gelatoData.shouldSubmit && submittedTaskReceiptWrapper && submittedTaskReceiptWrapper.status !== 'awaitingExec') + ? true + : false const sharesAfterRemovingFunding = balances.map((balance, i) => { return balance.shares.add(sendAmountsAfterRemovingFunding[i]).sub(depositedTokens) }) @@ -240,6 +279,22 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const totalUserLiquidity = totalDepositedTokens.add(userEarnings) const symbol = collateral.address === pseudoNativeAssetAddress ? wrapToken.symbol : collateral.symbol + const checkGelatoMinimum = useCallback(async () => { + if (cpk && amountToFund) { + const { belowMinimum, minimum } = await cpk.isBelowGelatoMinimum( + amountToFund, + collateral, + gelato, + totalUserLiquidity, + ) + setBelowGelatoMinimum(belowMinimum) + setGelatoMinimum(minimum) + } + }, [cpk, collateral, amountToFund, totalUserLiquidity, gelato]) + + useEffect(() => { + checkGelatoMinimum() + }, [checkGelatoMinimum]) const addFunding = async () => { setModalTitle('Deposit Funds') @@ -262,22 +317,47 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const fundsAmount = formatBigNumber(amountToFund || Zero, collateral.decimals) setStatus(Status.Loading) - setMessage(`Depositing funds: ${fundsAmount} ${collateral.symbol}...`) + withGelato && !belowGelatoMinimum + ? setMessage( + `Depositing funds: ${fundsAmount} ${symbol}\n + and scheduling future auto-withdraw ${symbol} via Gelato Network`, + ) + : setMessage(`Depositing funds: ${fundsAmount} ${symbol}...`) + + if (!cpk.cpk.isSafeApp() && collateral.address !== pseudoNativeAssetAddress) { + const collateralAddress = await marketMaker.getCollateralToken() + const collateralService = new ERC20Service(provider, account, collateralAddress) + + if (hasEnoughAllowance === Ternary.False) { + await collateralService.approveUnlimited(cpk.address) + } + } + + const conditionId = await marketMaker.getConditionId() await cpk.addFunding({ amount: amountToFund || Zero, + priorCollateralAmount: totalUserLiquidity, collateral, marketMaker, + gelato, + gelatoData, + conditionalTokens, + conditionId, + submittedTaskReceiptWrapper, }) await fetchGraphMarketMakerData() await fetchFundingBalance() await fetchCollateralBalance() + await refetch() setStatus(Status.Ready) setAmountToFund(null) setAmountToFundDisplay('') - setMessage(`Successfully deposited ${fundsAmount} ${collateral.symbol}`) + withGelato && !belowGelatoMinimum + ? setMessage(`Successfully deposited ${fundsAmount} ${symbol}\n and scheduled auto-withdraw`) + : setMessage(`Successfully deposited ${fundsAmount} ${symbol}`) } catch (err) { setStatus(Status.Error) setMessage(`Error trying to deposit funds.`) @@ -288,6 +368,8 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const removeFunding = async () => { setModalTitle('Withdraw Funds') + const withGelato = + submittedTaskReceiptWrapper && submittedTaskReceiptWrapper.status === 'awaitingExec' ? true : false try { if (!cpk) { return @@ -296,7 +378,10 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { const fundsAmount = formatBigNumber(depositedTokensTotal, collateral.decimals) - setMessage(`Withdrawing funds: ${fundsAmount} ${symbol}...`) + withGelato + ? setMessage(`Withdrawing funds: ${fundsAmount} ${symbol}\n + and cancel future auto-withdraw`) + : setMessage(`Withdrawing funds: ${fundsAmount} ${symbol}...`) const collateralAddress = await marketMaker.getCollateralToken() const conditionId = await marketMaker.getConditionId() @@ -310,15 +395,20 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { marketMaker, outcomesCount: balances.length, sharesToBurn: amountToRemove || Zero, + taskReceiptWrapper: submittedTaskReceiptWrapper, + gelato, }) await fetchGraphMarketMakerData() await fetchFundingBalance() await fetchCollateralBalance() + await refetch() setStatus(Status.Ready) setAmountToRemove(null) setAmountToRemoveDisplay('') - setMessage(`Successfully withdrew ${fundsAmount} ${symbol}`) + withGelato + ? setMessage(`Successfully withdrew ${fundsAmount} ${symbol}\n and canceled auto-withdraw`) + : setMessage(`Successfully withdrew ${fundsAmount} ${symbol}`) setIsModalTransactionResultOpen(true) } catch (err) { setStatus(Status.Error) @@ -328,6 +418,18 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { setIsModalTransactionResultOpen(true) } + const maxCollateralReturnAmount = (fundingBalance: BigNumber) => { + const sendAmountsAfterRemovingFunding = calcRemoveFundingSendAmounts( + fundingBalance, // use instead of amountToRemove + balances.map(b => b.holdings), + totalPoolShares, + ) + + return sendAmountsAfterRemovingFunding.reduce((min: BigNumber, amount: BigNumber) => + amount.lt(min) ? amount : min, + ) + } + const unlockCollateral = async () => { if (!cpk) { return @@ -388,6 +490,12 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { collateral.address === wrapToken.address || collateral.address === pseudoNativeAssetAddress ? [wrapToken.address.toLowerCase(), pseudoNativeAssetAddress.toLowerCase()] : [] + useEffect(() => { + if (withdrawDate != null && (gelatoData.input == null || gelatoData.input.toString() != withdrawDate.toString())) { + const gelatoDataCopy = { ...gelatoData, input: withdrawDate } + setGelatoData(gelatoDataCopy) + } + }, [gelatoData, withdrawDate]) return ( <> @@ -598,6 +706,24 @@ const MarketPoolLiquidityWrapper: React.FC = (props: Props) => { hyperlinkDescription="" /> )} + {GELATO_ACTIVATED && ( + { + const gelatoDataCopy = { ...gelatoData, input: newDate } + setGelatoData(gelatoDataCopy) + }} + isScheduled={submittedTaskReceiptWrapper ? true : false} + minimum={gelatoMinimum} + resolution={resolutionDate !== null ? marketMakerData.question.resolution : new Date()} + taskStatus={submittedTaskReceiptWrapper ? submittedTaskReceiptWrapper.status : undefined} + /> + )}