-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
12 changed files
with
420 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { useFormikContext } from 'formik'; | ||
import { useMemo } from 'react'; | ||
import { OutlineButton } from 'src/components/buttons/OutlineButton'; | ||
import { NumberField } from 'src/components/input/NumberField'; | ||
import { formatNumberString } from 'src/components/numbers/Amount'; | ||
import { fromWeiRounded } from 'src/utils/amount'; | ||
|
||
export function AmountField({ | ||
maxValueWei, | ||
maxDescription, | ||
}: { | ||
maxValueWei: bigint; | ||
maxDescription: string; | ||
}) { | ||
const { setFieldValue } = useFormikContext(); | ||
|
||
const availableLocked = useMemo(() => fromWeiRounded(maxValueWei), [maxValueWei]); | ||
|
||
const onClickMax = async () => { | ||
await setFieldValue('amount', availableLocked); | ||
}; | ||
|
||
return ( | ||
<div> | ||
<div className="flex items-center justify-between"> | ||
<label htmlFor="amount" className="pl-0.5 text-sm"> | ||
Amount | ||
</label> | ||
<span className="text-xs">{`${formatNumberString(maxValueWei, 2)} ${maxDescription}`}</span> | ||
</div> | ||
<div className="relative mt-2"> | ||
<NumberField name="amount" className="w-full all:py-3" /> | ||
<div className="absolute right-1 top-2 z-10"> | ||
<OutlineButton onClick={onClickMax} type="button"> | ||
Max | ||
</OutlineButton> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { governanceABI } from '@celo/abis'; | ||
import { useToastError } from 'src/components/notifications/useToastError'; | ||
import { ZERO_ADDRESS } from 'src/config/consts'; | ||
import { Addresses } from 'src/config/contracts'; | ||
import { useContractRead } from 'wagmi'; | ||
|
||
export function useIsGovernanceVoting(address?: Address) { | ||
const { data, isError, isLoading, error } = useContractRead({ | ||
address: Addresses.Governance, | ||
abi: governanceABI, | ||
functionName: 'isVoting', | ||
args: [address || ZERO_ADDRESS], | ||
enabled: !!address, | ||
staleTime: 1 * 60 * 1000, // 1 minute | ||
}); | ||
|
||
useToastError(error, 'Error fetching voting status'); | ||
|
||
return { | ||
isVoting: data, | ||
isError, | ||
isLoading, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
import { Form, Formik, FormikErrors, useField } from 'formik'; | ||
import { useEffect, useMemo } from 'react'; | ||
import { SolidButton } from 'src/components/buttons/SolidButton'; | ||
import { AmountField } from 'src/components/input/AmountField'; | ||
import { useBalance } from 'src/features/account/hooks'; | ||
import { useIsGovernanceVoting } from 'src/features/governance/useVotingStatus'; | ||
import { LockActionType, LockedBalances } from 'src/features/locking/types'; | ||
import { useLockedStatus } from 'src/features/locking/useLockedStatus'; | ||
import { | ||
getTotalNonvotingLocked, | ||
getTotalPendingCelo, | ||
getTotalUnlockedCelo, | ||
} from 'src/features/locking/utils'; | ||
import { StakingBalances } from 'src/features/staking/types'; | ||
import { useStakingBalances } from 'src/features/staking/useStakingBalances'; | ||
import { toWei } from 'src/utils/amount'; | ||
import { logger } from 'src/utils/logger'; | ||
import { toTitleCase } from 'src/utils/strings'; | ||
import { isNullish } from 'src/utils/typeof'; | ||
import { useAccount } from 'wagmi'; | ||
|
||
interface LockFormValues { | ||
amount: number; | ||
type: LockActionType; | ||
} | ||
|
||
const initialValues: LockFormValues = { | ||
amount: 0, | ||
type: LockActionType.Lock, | ||
}; | ||
|
||
export function LockForm({ defaultType }: { defaultType?: LockActionType }) { | ||
const { address } = useAccount(); | ||
const { balance: walletBalance } = useBalance(address); | ||
const { lockedBalances } = useLockedStatus(address); | ||
const { stakeBalances } = useStakingBalances(address); | ||
const { isVoting } = useIsGovernanceVoting(address); | ||
|
||
const onSubmit = (values: LockFormValues) => { | ||
alert(values); | ||
}; | ||
|
||
const validate = (values: LockFormValues) => { | ||
if (!walletBalance || !lockedBalances || !stakeBalances || isNullish(isVoting)) { | ||
return { amount: 'Form not ready' }; | ||
} | ||
return validateForm(values, lockedBalances, walletBalance.value, stakeBalances, isVoting); | ||
}; | ||
|
||
return ( | ||
<Formik<LockFormValues> | ||
initialValues={{ | ||
...initialValues, | ||
type: defaultType || initialValues.type, | ||
}} | ||
onSubmit={onSubmit} | ||
validate={validate} | ||
validateOnChange={false} | ||
validateOnBlur={false} | ||
> | ||
{({ values }) => ( | ||
<Form className="mt-2 flex w-full flex-col items-stretch space-y-4"> | ||
<h2 className="font-serif text-2xl">Stake with a validator</h2> | ||
<ActionTypeField defaultType={defaultType} /> | ||
<LockAmountField | ||
lockedBalances={lockedBalances} | ||
walletBalance={walletBalance?.value} | ||
type={values.type} | ||
/> | ||
<SolidButton type="submit">{toTitleCase(values.type)}</SolidButton> | ||
</Form> | ||
)} | ||
</Formik> | ||
); | ||
} | ||
|
||
function LockAmountField({ | ||
lockedBalances, | ||
walletBalance, | ||
type, | ||
}: { | ||
lockedBalances?: LockedBalances; | ||
walletBalance?: bigint; | ||
type: LockActionType; | ||
}) { | ||
const maxAmountWei = useMemo( | ||
() => getMaxAmount(type, lockedBalances, walletBalance), | ||
[lockedBalances, walletBalance, type], | ||
); | ||
return <AmountField maxValueWei={maxAmountWei} maxDescription="Locked CELO available" />; | ||
} | ||
|
||
function ActionTypeField({ defaultType }: { defaultType?: LockActionType }) { | ||
const [field, , helpers] = useField<LockActionType>('type'); | ||
|
||
useEffect(() => { | ||
helpers.setValue(defaultType || LockActionType.Lock).catch((e) => logger.error(e)); | ||
}, [defaultType, helpers]); | ||
|
||
return ( | ||
<div> | ||
<label htmlFor="type" className="pl-0.5 text-sm"> | ||
type | ||
</label> | ||
<div> | ||
<select name="type" className="w-full" value={field.value}> | ||
{Object.values(LockActionType).map((type) => ( | ||
<option key={type} value={type}> | ||
{toTitleCase(type)} | ||
</option> | ||
))} | ||
</select> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
function validateForm( | ||
values: LockFormValues, | ||
lockedBalances: LockedBalances, | ||
walletBalance: bigint, | ||
stakeBalances: StakingBalances, | ||
isVoting: boolean, | ||
): FormikErrors<LockFormValues> { | ||
const { amount, type } = values; | ||
|
||
// TODO implement toWeiAdjusted() and use it here | ||
const amountWei = toWei(amount); | ||
|
||
const maxAmountWei = getMaxAmount(type, lockedBalances, walletBalance); | ||
if (amountWei > maxAmountWei) { | ||
const errorMsg = | ||
type === LockActionType.Withdraw ? 'No pending available to withdraw' : 'Amount exceeds max'; | ||
return { amount: errorMsg }; | ||
} | ||
|
||
// Special case handling for locking whole balance | ||
if (type === LockActionType.Lock) { | ||
const remainingAfterPending = amountWei - getTotalPendingCelo(lockedBalances); | ||
if (remainingAfterPending >= walletBalance) { | ||
return { amount: 'Cannot lock entire balance' }; | ||
} | ||
} | ||
|
||
// Ensure user isn't trying to unlock CELO used for staking | ||
if (type === LockActionType.Unlock) { | ||
if (isVoting) { | ||
return { amount: 'Locked funds have voted for governance' }; | ||
} | ||
|
||
const nonVotingLocked = getTotalNonvotingLocked(lockedBalances, stakeBalances); | ||
if (amountWei > nonVotingLocked) { | ||
return { amount: 'Locked funds in use for staking' }; | ||
} | ||
} | ||
|
||
return {}; | ||
} | ||
|
||
function getMaxAmount( | ||
type: LockActionType, | ||
lockedBalances?: LockedBalances, | ||
walletBalance?: bigint, | ||
) { | ||
if (type === LockActionType.Lock) { | ||
return getTotalUnlockedCelo(lockedBalances, walletBalance); | ||
} else if (type === LockActionType.Unlock) { | ||
return lockedBalances?.locked || 0n; | ||
} else if (type === LockActionType.Withdraw) { | ||
return lockedBalances?.pendingFree || 0n; | ||
} else { | ||
throw new Error(`Invalid lock type: ${type}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { | ||
LockActionType, | ||
LockTokenParams, | ||
LockTokenTxPlan, | ||
PendingWithdrawal, | ||
} from 'src/features/locking/types'; | ||
import { bigIntMin } from 'src/utils/math'; | ||
|
||
// Lock token operations can require varying numbers of txs in specific order | ||
// This determines the ideal tx types and order | ||
export function getLockActionTxPlan( | ||
params: LockTokenParams, | ||
pendingWithdrawals: PendingWithdrawal[], | ||
): LockTokenTxPlan { | ||
const { type, amountWei } = params; | ||
|
||
if (type === LockActionType.Unlock) { | ||
// If only all three cases were this simple :) | ||
return [{ type, amountWei }]; | ||
} else if (type === LockActionType.Lock) { | ||
const txs: LockTokenTxPlan = []; | ||
// Need relock from the pendings in reverse order | ||
// due to the way the storage is managed in the contract | ||
let amountRemaining = amountWei; | ||
const pwSorted = [...pendingWithdrawals].sort((a, b) => b.index - a.index); | ||
for (const p of pwSorted) { | ||
if (amountRemaining <= 0) break; | ||
const txAmount = bigIntMin(amountRemaining, p.value); | ||
txs.push({ | ||
type: LockActionType.Relock, | ||
amountWei: txAmount, | ||
pendingWithdrawal: p, | ||
}); | ||
amountRemaining -= txAmount; | ||
} | ||
// If pending relocks didn't cover it | ||
if (amountRemaining > 0) { | ||
txs.push({ type: LockActionType.Lock, amountWei: amountRemaining }); | ||
} | ||
return txs; | ||
} else if (type === LockActionType.Withdraw) { | ||
const txs: LockTokenTxPlan = []; | ||
const now = Date.now(); | ||
// Withdraw all available pendings | ||
for (const p of pendingWithdrawals) { | ||
if (p.timestamp <= now) | ||
txs.push({ | ||
type: LockActionType.Withdraw, | ||
amountWei: p.value, | ||
pendingWithdrawal: p, | ||
}); | ||
} | ||
return txs; | ||
} else { | ||
throw new Error(`Invalid lock token action type: ${type}`); | ||
} | ||
} | ||
|
||
// async function createLockCeloTx( | ||
// txPlanItem: LockTokenTxPlanItem, | ||
// feeEstimate: FeeEstimate, | ||
// nonce: number, | ||
// ) { | ||
// const lockedGold = getContract(CeloContract.LockedGold); | ||
// const tx = await lockedGold.populateTransaction.lock(); | ||
// tx.value = BigNumber.from(txPlanItem.amountWei); | ||
// tx.nonce = nonce; | ||
// logger.info('Signing lock celo tx'); | ||
// return signTransaction(tx, feeEstimate); | ||
// } | ||
|
||
// async function createRelockCeloTx( | ||
// txPlanItem: LockTokenTxPlanItem, | ||
// feeEstimate: FeeEstimate, | ||
// nonce: number, | ||
// ) { | ||
// const { amountWei, pendingWithdrawal } = txPlanItem; | ||
// if (!pendingWithdrawal) throw new Error('Pending withdrawal missing from relock tx'); | ||
// const lockedGold = getContract(CeloContract.LockedGold); | ||
// const tx = await lockedGold.populateTransaction.relock(pendingWithdrawal.index, amountWei); | ||
// tx.nonce = nonce; | ||
// logger.info('Signing relock celo tx'); | ||
// return signTransaction(tx, feeEstimate); | ||
// } | ||
|
||
// async function createUnlockCeloTx( | ||
// txPlanItem: LockTokenTxPlanItem, | ||
// feeEstimate: FeeEstimate, | ||
// nonce: number, | ||
// ) { | ||
// const { amountWei } = txPlanItem; | ||
// const lockedGold = getContract(CeloContract.LockedGold); | ||
// const tx = await lockedGold.populateTransaction.unlock(amountWei); | ||
// tx.nonce = nonce; | ||
// logger.info('Signing unlock celo tx'); | ||
// return signTransaction(tx, feeEstimate); | ||
// } | ||
|
||
// async function createWithdrawCeloTx( | ||
// txPlanItem: LockTokenTxPlanItem, | ||
// feeEstimate: FeeEstimate, | ||
// nonce: number, | ||
// ) { | ||
// const { pendingWithdrawal } = txPlanItem; | ||
// if (!pendingWithdrawal) throw new Error('Pending withdrawal missing from withdraw tx'); | ||
// const lockedGold = getContract(CeloContract.LockedGold); | ||
// const tx = await lockedGold.populateTransaction.withdraw(pendingWithdrawal.index); | ||
// tx.nonce = nonce; | ||
// logger.info('Signing withdraw celo tx'); | ||
// return signTransaction(tx, feeEstimate); | ||
// } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.