Skip to content

Commit

Permalink
Merge pull request #140 from SecureSECODAO/dev
Browse files Browse the repository at this point in the history
v0.5.3
  • Loading branch information
Hidde-Heijnen authored Jun 15, 2023
2 parents a4164f1 + 7511703 commit 2d68a30
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 112 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dao-webapp",
"private": true,
"version": "0.5.2",
"version": "0.5.3",
"type": "module",
"license": "MIT",
"scripts": {
Expand Down
15 changes: 10 additions & 5 deletions src/components/layout/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ const navItems: NavItemData[] = [
url: 'https://secureseco.org/',
icon: HiOutlineGlobeAlt,
},
{ label: 'Discord', url: DAO_METADATA.discord, icon: FaDiscord },
{
label: 'Discord',
url: DAO_METADATA.links.discord.url,
icon: FaDiscord,
},
],
},
];
Expand Down Expand Up @@ -296,18 +300,19 @@ export const NavItemCollectionContent = ({
<div className="grid h-full w-full grid-cols-2 divide-x divide-popover-foreground/10 bg-popover-foreground/5">
{item.externalLinks &&
item.externalLinks.map((item) => (
// Note: use a instead? (for external links)
<NavLink
<a
key={item.label}
to={item.url}
href={item.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-x-2.5 p-3 font-semibold hover:bg-popover-foreground/20 ring-ring rounded-md focus:outline-none focus:ring-2"
>
<item.icon
className="h-5 w-5 flex-none opacity-80"
aria-hidden="true"
/>
{item.label}
</NavLink>
</a>
))}
</div>
</div>
Expand Down
9 changes: 6 additions & 3 deletions src/components/newProposal/steps/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,16 @@ export const Actions = () => {

throw error;
}
}
} else return action;
});

Promise.all(actionPromises)
.then(() => {
.then((mappedActions) => {
// If everything went OK, go to next step
setDataStep3(data);
setDataStep3({
...data,
actions: mappedActions,
});
setStep(4);
})
.catch((error) => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/proposal/ProposalResources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const ProposalResources = ({
<Card size="sm" variant="light">
<a
href={sanitizeHref(resource.url)}
rel="noreferrer"
rel="noopener noreferrer"
target="_blank"
className="flex flex-row items-center gap-x-2 font-medium text-primary transition-colors duration-200 hover:text-primary/80"
>
Expand Down
3 changes: 2 additions & 1 deletion src/components/proposal/actions/MergeAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const MergeAction = ({ action, ...props }: MergeActionProps) => {
className="w-fit flex flex-row items-center gap-x-2 text-primary-highlight transition-colors duration-200 hover:text-primary-highlight/80"
href={`https://github.com/${action.params._owner}/${action.params._repo}/pull/${action.params._pull_number}`}
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
>
View on GitHub
<HiArrowTopRightOnSquare className="h-4 w-4 shrink-0" />
Expand All @@ -73,6 +73,7 @@ const MergeAction = ({ action, ...props }: MergeActionProps) => {
<Address
className="font-medium"
address={action.params._sha}
copyTooltip="Copy commit hash"
showCopy
/>
</Card>
Expand Down
25 changes: 17 additions & 8 deletions src/components/ui/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,19 @@ type AddressProps = {
replaceYou?: boolean;
jazziconSize?: keyof typeof jazziconVariants | number;
className?: string;
copyTooltip?: string;
};

/**
* A component for displaying Ethereum addresses, optionally with a link to Etherscan and a copy-to-clipboard button.
* @param props - Props for the `Address` component.
* @param props.address - The Ethereum address to display.
* @param props.maxLength - The maximum length of the displayed address. If the address is longer than this, it will be truncated in the middle with an ellipsis. A negative value means no truncation.
* @param props.hasLink - Whether the displayed address should be linked to its corresponding page on Etherscan.
* @param props.showCopy - Whether to display a copy-to-clipboard button next to the address.
* @param props.jazziconSize - The size of the Jazzicon to display next to the address (0 to show no Jazzicon)
* @param [props.replaceYou=true] - Whether to replace the user's own Ethereum address with "you" in the displayed text.
* @param props.length - The length of the displayed address. Can be one of `sm`, `md`, `lg`, `full`, or a number. Defaults to `md`.
* @param props.hasLink - Whether to display a link to block explorer. Defaults to `false`.
* @param props.showCopy - Whether to display a copy-to-clipboard button. Defaults to `false`.
* @param props.replaceYou - Whether to replace the given with the string "you" if connected wallet is equal to given address. Defaults to `false`.
* @param props.jazziconSize - The size of the Jazzicon. Can be one of `none`, `sm`, `md`, `lg`, or a number. Defaults to `none`.
* @param props.copyTooltip - The tooltip text for the copy-to-clipboard button. Defaults to "Copy address".
* @param props.className - Class names to pass to the root div of this component.
* @returns - A React element representing the `Address` component.
* @remarks
* This component uses the `useAccount` hook from the `wagmi` package to check the current user's Ethereum address.
Expand All @@ -69,6 +71,7 @@ export const Address: React.FC<AddressProps> = ({
showCopy = false,
replaceYou = false,
jazziconSize = 'none',
copyTooltip = 'Copy address',
className,
}) => {
const [status, setStatus] = useState<'idle' | 'copied'>('idle');
Expand Down Expand Up @@ -124,7 +127,13 @@ export const Address: React.FC<AddressProps> = ({
)}
</TooltipTrigger>
<TooltipContent>
{hasLink ? <p>Open in block explorer</p> : <p>{address}</p>}
{hasLink ? (
<p>Open in block explorer</p>
) : (
<p>
{address.length > 50 ? truncateMiddle(address, 50) : address}
</p>
)}
</TooltipContent>
</Tooltip>

Expand All @@ -143,7 +152,7 @@ export const Address: React.FC<AddressProps> = ({
</button>
</TooltipTrigger>
<TooltipContent>
<p>Copy address</p>
<p>{copyTooltip}</p>
</TooltipContent>
</Tooltip>
)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/verification/StampCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ const StampCard = ({
<a
href={stampInfo.url}
target="_blank"
rel="noreferrer"
rel="noopener noreferrer"
className="mx-1 rounded-sm text-primary-highlight outline-ring transition-colors duration-200 hover:text-primary-highlight/80"
>
{stampInfo.url}
Expand Down
91 changes: 74 additions & 17 deletions src/hooks/useMarketMaker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { useEffect, useState } from 'react';
import { DiamondGovernanceClient } from '@plopmenz/diamond-governance-sdk';
import { BigNumber } from 'ethers';
import { BigNumber, constants } from 'ethers';
import { useProvider } from 'wagmi';

import { useDiamondSDKContext } from '../context/DiamondGovernanceSDK';
Expand All @@ -21,14 +21,31 @@ import {

export type SwapKind = 'Mint' | 'Burn';

export interface useSwapProps {
export interface useMarketMakerProps {
/*
* Mint: DAI is swapped for SECOIN, Burn: SECOIN is swapped for DAI.
*/
swapKind: SwapKind;
/*
* Amount of DAI/SECOIN to be swapped, spend from the users wallet
*/
amount: BigNumber | undefined | null;
//number between 0 and 100
/*
* Slippage percentage to be applied to the expected return.
* That is, when the actual return will be lower than the expected return,
* what percentage of the expected return is still acceptable for the user as actual return.
* Slippage must be a number between(inclusive) 0 and 100, where 100 corresponds to 100% slippage.
*/
slippage: number | undefined | null;
/*
* Data fetching will only happen iff enabled is true.
* This can be useful to prevent unneeded computation.
*/
enabled?: boolean;
}

// Special Error case indicating error is caused by invalid user input,
// rather than an error raised by the smart contracts.
class ValidationError extends Error {
constructor(message: string) {
super(message);
Expand All @@ -41,61 +58,98 @@ class ValidationError extends Error {
* @param slippage The slippage to apply, must be a between/equal 0 and 100. Only one decimal of precision will be used
*/
export const applySlippage = (amount: BigNumber, slippage: number) => {
if (slippage > 100) throw new ValidationError('Slippage is more than 100%');
if (slippage < 0) throw new ValidationError('Slippage is less than 0%');

const slippageBN = BigNumber.from((slippage * 10).toFixed(0)); // e.g.: 12.3456% becomes 123 (essentially per mille)
const slippageFactor = BigNumber.from(1000).sub(slippageBN); // e.g. 123 becomes 877
return amount.mul(slippageFactor).div(1000); // times slippageFactor, divide by 1000 to correct for per mille
};

/*
* useMarketMaker is a hook for swapping SECOIN and DAI.
*
* MarketMaker is provided by the DiamondGovernanceSDK and smart contracts.
* MarketMaker provides the ability to swap SECOIN with DAI (token with fixed/stable monetary value) and vice versa.
* The useMarketMaker hook is a convenient react hook around the MarketMaker.
* Apart from the DiamondGovernanceSDK, this hook also relies on a wagmi provider.
* This is needed to convert GAS fees to an actual price.
*
* @param props - see useMarketMakerProps for parameters
*/
export const useMarketMaker = ({
swapKind,
amount,
slippage,
enabled = true,
}: useSwapProps) => {
}: useMarketMakerProps) => {
const { client } = useDiamondSDKContext();
const [estimatedGas, setEstimatedGas] = useState<null | BigNumber>(null);
const [expectedReturn, setExpectedReturn] = useState<null | BigNumber>(null);
const [contractAddress, setContractAddress] = useState<null | string>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
// Provider is needed for converting GAS to a gas Price.
const provider = useProvider();

const fetchData = async (client: DiamondGovernanceClient) => {
try {
setIsLoading(true);
setError(null);
// Reset data to null
// Except for contractAddress, as this is a constant. Thus if it has been set, it is always correct.
setEstimatedGas(null);
setExpectedReturn(null);

// Try to set contract address as soon as possible.
const marketMaker = await client.sugar.GetABCMarketMaker();
setContractAddress(marketMaker.address);

if (isNullOrUndefined(amount) || amount.isZero())
// Start promise as soon as possible/needed
const gasPricePromise = provider.getGasPrice();

if (isNullOrUndefined(amount))
throw new ValidationError('Amount is not valid');
if (amount.isZero()) {
setExpectedReturn(constants.Zero);
throw new ValidationError('Amount is zero');
}
if (isNullOrUndefined(slippage) || isNaN(slippage))
throw new ValidationError('Slippage is not valid');

if (swapKind === 'Mint') {
// Get and set expected return
const mintAmount = await marketMaker.calculateMint(amount);
const minAmount = applySlippage(mintAmount, slippage);
const gas = await marketMaker.estimateGas.mint(amount, minAmount);
const gasPrice = await provider.getGasPrice();

setEstimatedGas(gas.mul(gasPrice));
setExpectedReturn(mintAmount);

// Get and set estimated gas
const minAmount = applySlippage(mintAmount, slippage);
const gasValues = await promiseObjectAll({
gas: marketMaker.estimateGas.mint(amount, minAmount),
gasPrice: gasPricePromise,
});
setEstimatedGas(gasValues.gas.mul(gasValues.gasPrice));
}

if (swapKind === 'Burn') {
const burnAmount = await marketMaker.calculateBurn(amount);
// Get and set expected return
const burnWithoutFee = await marketMaker.calculateBurn(amount);
const exitFee = await marketMaker.calculateExitFee(burnWithoutFee);
const burnAmount = burnWithoutFee.sub(exitFee);
setExpectedReturn(burnAmount);

// Get and set estimated gas
const minAmount = applySlippage(burnAmount, slippage);
const values = await promiseObjectAll({
const gasValues = await promiseObjectAll({
gas: marketMaker.estimateGas.burn(amount, minAmount),
exitFee: marketMaker.calculateExitFee(amount),
gasPrice: gasPricePromise,
});
const gasPrice = await provider.getGasPrice();

setEstimatedGas(values.gas.mul(gasPrice));
setExpectedReturn(burnAmount.sub(values.exitFee));
setEstimatedGas(gasValues.gas.mul(gasValues.gasPrice));
}
} catch (e) {
// Set error. If it is a ValidationError, the error message can be used to show to the user.
// Otherwise, the user error message should be a more general message.
if (e instanceof ValidationError) {
setError(getErrorMessage(e));
} else {
Expand All @@ -108,15 +162,16 @@ export const useMarketMaker = ({
};

const performSwap = async () => {
// Thrown errors should be caught by the caller of performSwap.
if (!client) throw new Error('Client is not set');
if (isNullOrUndefined(amount) || isNullOrUndefined(expectedReturn))
throw new Error('Amount is not valid');
if (isNullOrUndefined(slippage) || isNaN(slippage))
throw new Error('Slippage is not valid');

const marketMaker = await client.sugar.GetABCMarketMaker();

const minAmount = applySlippage(expectedReturn, slippage);

if (swapKind === 'Mint') {
return marketMaker.mint(amount, minAmount);
}
Expand All @@ -130,6 +185,8 @@ export const useMarketMaker = ({
useEffect(() => {
if (!enabled || !client) return;
fetchData(client);

//Set interval such that data is fetched every 10 seconds
const id = setInterval(() => fetchData(client), 10000);
return () => clearInterval(id);
}, [client, amount?._hex, slippage, swapKind, enabled]);
Expand Down
Loading

1 comment on commit 2d68a30

@vercel
Copy link

@vercel vercel bot commented on 2d68a30 Jun 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.