diff --git a/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-079c4b7688.zip b/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-079c4b7688.zip new file mode 100644 index 000000000..3972727bd Binary files /dev/null and b/.yarn/cache/@kurkle-color-npm-0.3.2-98f2086013-079c4b7688.zip differ diff --git a/.yarn/cache/chart.js-npm-4.4.2-e9b1497a6e-609444dfc9.zip b/.yarn/cache/chart.js-npm-4.4.2-e9b1497a6e-609444dfc9.zip new file mode 100644 index 000000000..5774a9780 Binary files /dev/null and b/.yarn/cache/chart.js-npm-4.4.2-e9b1497a6e-609444dfc9.zip differ diff --git a/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-0a70b60e1a.zip b/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-0a70b60e1a.zip new file mode 100644 index 000000000..30cf53f05 Binary files /dev/null and b/.yarn/cache/react-chartjs-2-npm-5.2.0-03632f5179-0a70b60e1a.zip differ diff --git a/liquidity/lib/useBlockchain/useBlockchain.tsx b/liquidity/lib/useBlockchain/useBlockchain.tsx index b91cbed5e..027b8f64d 100644 --- a/liquidity/lib/useBlockchain/useBlockchain.tsx +++ b/liquidity/lib/useBlockchain/useBlockchain.tsx @@ -1,5 +1,5 @@ import { ethers } from 'ethers'; -import React from 'react'; +import React, { useMemo } from 'react'; import { BaseIcon, EthereumIcon, @@ -251,6 +251,12 @@ export function useWallet() { }; } +export function useGetNetwork(chainId: string) { + return useMemo(() => { + return NETWORKS.find((n) => n.hexId === chainId); + }, [chainId]); +} + export function useNetwork() { const [{ connectedChain }, setChain] = useSetChain(); diff --git a/oracle-manager/ui/.env.example b/oracle-manager/ui/.env.example index ddaf0d78a..e3c40a8bb 100644 --- a/oracle-manager/ui/.env.example +++ b/oracle-manager/ui/.env.example @@ -1 +1,2 @@ NEXT_PUBLIC_WC_PROJECT_ID=xxx +NEXT_PUBLIC_INFURA_KEY=xxx diff --git a/ultrasound/ui/.env.example b/ultrasound/ui/.env.example new file mode 100644 index 000000000..588b0d714 --- /dev/null +++ b/ultrasound/ui/.env.example @@ -0,0 +1,3 @@ +INFURA_KEY=xxx +PYTH_MAINNET_ENDPOINT=https://hermes.pyth.network/ +PYTH_TESTNET_ENDPOINT=https://hermes.pyth.network/ diff --git a/ultrasound/ui/README.md b/ultrasound/ui/README.md new file mode 100644 index 000000000..39eaa9d11 --- /dev/null +++ b/ultrasound/ui/README.md @@ -0,0 +1 @@ +# ULTRASOUND HOMES diff --git a/ultrasound/ui/babel.config.js b/ultrasound/ui/babel.config.js new file mode 100644 index 000000000..245cf461d --- /dev/null +++ b/ultrasound/ui/babel.config.js @@ -0,0 +1,68 @@ +const path = require('path'); +require.resolve('core-js'); +require.resolve('@babel/runtime-corejs3/core-js/date/now'); + +module.exports = { + presets: [ + require.resolve('@babel/preset-typescript'), + [require.resolve('@babel/preset-react'), { runtime: 'automatic' }], + ], + + plugins: [[require.resolve('@babel/plugin-transform-runtime'), { corejs: 3 }]], + + env: { + production: { + presets: [ + [ + require.resolve('@babel/preset-env'), + { + useBuiltIns: 'usage', + corejs: { version: 3, proposals: true }, + modules: false, + targets: { + browsers: require('./package.json').browserslist, + }, + }, + ], + ], + }, + + development: { + presets: [ + [ + require.resolve('@babel/preset-env'), + { + modules: false, + targets: { browsers: ['last 1 Chrome version'] }, + }, + ], + ], + plugins: [require.resolve('react-refresh/babel')], + }, + + test: { + presets: [ + [ + require.resolve('@babel/preset-env'), + { + modules: 'commonjs', + targets: { node: 'current' }, + }, + ], + ], + plugins: [ + [ + require.resolve('babel-plugin-istanbul'), + { + cwd: path.resolve('../..'), + all: true, + excludeNodeModules: false, + include: ['v3'], + exclude: ['**/*.test.*', '**/*.cy.*', '**/*.e2e.*'], + }, + 'istanbul', + ], + ], + }, + }, +}; diff --git a/ultrasound/ui/components/BurnSNX.tsx b/ultrasound/ui/components/BurnSNX.tsx new file mode 100644 index 000000000..7bb64bc84 --- /dev/null +++ b/ultrasound/ui/components/BurnSNX.tsx @@ -0,0 +1,62 @@ +import { Button, Flex, Image, Link, Text, Tooltip, useDisclosure } from '@chakra-ui/react'; +import { BurnSNXModal } from './BurnSNXModal'; +import { isBaseAndromeda } from '@snx-v3/isBaseAndromeda'; +import { useNetwork } from '@snx-v3/useBlockchain'; +import { useSNXPrice } from '../hooks/useSNXPrice'; + +export function BurnSNX() { + const { network } = useNetwork(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const { data: SNXPrice } = useSNXPrice(); + + return ( + + + + + Sell SNX at premium and watch it burn + + + Sell your SNX at a premium price to the Buyback and Burn contract and get USDC on + Base + + + {SNXPrice?.eq(0) ? ( + 'refecthing...' + ) : ( + <> + Buyback Price: $ {SNXPrice?.toNumber().toFixed(2)} $ + {SNXPrice ? (SNXPrice?.toNumber() + SNXPrice?.toNumber() * 0.01).toFixed(2) : 0} + + )} + + + {!isBaseAndromeda(network?.id, network?.preset) ? ( + + + + ) : ( + + )} + + + + + + + + ); +} diff --git a/ultrasound/ui/components/BurnSNXModal.tsx b/ultrasound/ui/components/BurnSNXModal.tsx new file mode 100644 index 000000000..b2fdf6d40 --- /dev/null +++ b/ultrasound/ui/components/BurnSNXModal.tsx @@ -0,0 +1,205 @@ +import { + Button, + Divider, + Flex, + Image, + Input, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalHeader, + ModalOverlay, + Spinner, + Text, +} from '@chakra-ui/react'; +import { useWallet } from '@snx-v3/useBlockchain'; +import { useTokenBalance } from '@snx-v3/useTokenBalance'; +import { SNXUSDBalanceOfBuyBackContract } from '../hooks/SNXUSDBalanceOfBuyBackContract'; +import { useApprove } from '@snx-v3/useApprove'; +import Wei from '@synthetixio/wei'; +import { useState } from 'react'; +import { useSellSNX } from '../mutations/useSellSNX'; +import { useBurnEvents } from '../hooks/useBurnEvents'; +import { useSNXPrice } from '../hooks/useSNXPrice'; + +export function BurnSNXModal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) { + const [amount, setAmount] = useState(new Wei(0)); + const [receivingUSDCAmount, setReceivingUSDCAmount] = useState(0); + const { data: events } = useBurnEvents(); + const { connect, activeWallet } = useWallet(); + const { data: SNXPrice } = useSNXPrice(); + + const { data: snxBalance } = useTokenBalance('0x22e6966B799c4D5B13BE962E1D117b56327FDa66'); + const { data: usdcBalance } = useTokenBalance('0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'); + const { data: contractBalance } = SNXUSDBalanceOfBuyBackContract( + '0x632cAa10A56343C5e6C0c066735840c096291B18' + ); + + const { requireApproval, approve, refetchAllowance } = useApprove({ + contractAddress: '0x22e6966B799c4D5B13BE962E1D117b56327FDa66', + amount: amount ? amount.toBN() : 0, + spender: '0x632cAa10A56343C5e6C0c066735840c096291B18', + }); + const { mutateAsync, isPending } = useSellSNX(); + + return ( + + + + + Burn your SNX + + + + + + + Burn + + + + + + + SNX + + + { + try { + if (SNXPrice) { + const snxAmount = new Wei(e.target.value); + setAmount(snxAmount); + setReceivingUSDCAmount( + snxAmount.mul(SNXPrice).add(SNXPrice.mul(0.01)).toNumber() + ); + } + } catch (error) { + console.error('failed to parse input: ', Error); + setAmount(undefined); + } + }} + /> + + + { + if (SNXPrice && snxBalance) { + setAmount(snxBalance); + setReceivingUSDCAmount( + snxBalance.mul(SNXPrice).add(SNXPrice.mul(0.01)).toNumber() + ); + } + }} + > + Balance: {snxBalance ? snxBalance.toNumber().toFixed(2) : '-'} + + + $ + + + + max burnable:{' '} + {contractBalance && + events?.SNXPrice && + ( + new Wei(contractBalance, 18).toNumber() / + (events.SNXPrice + events.SNXPrice * 0.01) + ).toFixed(2)} + + + Receive + + + + + + USDC + + + + + + + Balance: {usdcBalance ? usdcBalance.toNumber().toFixed(2) : '-'} + + + + {isPending ? ( + + ) : ( + + )} + + + + + ); +} diff --git a/ultrasound/ui/components/Chart.tsx b/ultrasound/ui/components/Chart.tsx new file mode 100644 index 000000000..49d9036ec --- /dev/null +++ b/ultrasound/ui/components/Chart.tsx @@ -0,0 +1,82 @@ +import { Flex, Spinner, Tab, TabList, Tabs } from '@chakra-ui/react'; +import { useBurnEvents } from '../hooks/useBurnEvents'; +import { Line } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +} from 'chart.js'; +import { useState } from 'react'; + +ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend); + +export function Chart() { + const [range, setRange] = useState<'groupedByMonths' | 'groupedByLast30Days'>('groupedByMonths'); + const { data: events, isLoading } = useBurnEvents(); + + if (isLoading) return ; + if (!events) return; + return ( + + + + + setRange('groupedByLast30Days')} + color="gray.500" + _selected={{ color: 'white', bg: 'whiteAlpha.400' }} + > + 1M + + setRange('groupedByMonths')} + color="gray.500" + _selected={{ color: 'white', bg: 'whiteAlpha.400' }} + > + All + + + + + ); +} diff --git a/ultrasound/ui/components/CurrentSupplyStats.tsx b/ultrasound/ui/components/CurrentSupplyStats.tsx new file mode 100644 index 000000000..3e5045e90 --- /dev/null +++ b/ultrasound/ui/components/CurrentSupplyStats.tsx @@ -0,0 +1,46 @@ +import { Flex, Spinner, Text } from '@chakra-ui/react'; +import { useBurnEvents } from '../hooks/useBurnEvents'; + +export function CurrentSupplyStats() { + const { data: events, isLoading } = useBurnEvents(); + + return ( + + {isLoading ? ( + + ) : ( + <> + + + Curren tSupply + + + ALL + + + + {events?.totalSupply} SNX + + + )} + + ); +} diff --git a/ultrasound/ui/components/Header.tsx b/ultrasound/ui/components/Header.tsx new file mode 100644 index 000000000..7cd4eafcc --- /dev/null +++ b/ultrasound/ui/components/Header.tsx @@ -0,0 +1,69 @@ +import { Box, Button, Divider, Flex, Image, Text, useColorMode } from '@chakra-ui/react'; +import { useIsConnected, useNetwork, useWallet } from '@snx-v3/useBlockchain'; +import { shortAddress } from '../utils/addresses'; +import { NetworkSelect } from './NetworkSelect'; +import { useTokenBalance } from '@snx-v3/useTokenBalance'; +import { useEffect } from 'react'; + +export function Header() { + const { colorMode, toggleColorMode } = useColorMode(); + const { network, setNetwork } = useNetwork(); + const isWalletConnected = useIsConnected(); + const { activeWallet, connect, disconnect, walletsInfo } = useWallet(); + const { data: snxBalance } = useTokenBalance('0x22e6966B799c4D5B13BE962E1D117b56327FDa66'); + + useEffect(() => { + if (colorMode === 'light') { + toggleColorMode(); + } + }, [colorMode, toggleColorMode]); + + return ( + + + + + + + + {snxBalance ? snxBalance?.toNumber().toFixed(2) : '-'} + + + {isWalletConnected && ( + setNetwork(netowork.id)} + /> + )} + + {isWalletConnected ? ( + + ) : ( + + )} + + + + + ); +} diff --git a/ultrasound/ui/components/Main.tsx b/ultrasound/ui/components/Main.tsx new file mode 100644 index 000000000..1d3e89f63 --- /dev/null +++ b/ultrasound/ui/components/Main.tsx @@ -0,0 +1,77 @@ +import { Flex, Heading, Image, Link, Spinner, Text } from '@chakra-ui/react'; +import { Chart } from './Chart'; +import { SupplyChangeStats } from './SupplyChangeStats'; +import { CurrentSupplyStats } from './CurrentSupplyStats'; +import { BurnSNX } from './BurnSNX'; +import { useBurnEvents } from '../hooks/useBurnEvents'; + +export function Main() { + const { data: events, isLoading } = useBurnEvents(); + + return ( + + + + + ultrasound.homes + + + burning SNX for Kain's mansions + + + + + + + Mansion counter + + + {isLoading ? : events?.totalBurns} + + + + + + + + + + + + + + + + + Share + + + + + + + + + + + + + + + + ); +} diff --git a/ultrasound/ui/components/NetworkSelect.tsx b/ultrasound/ui/components/NetworkSelect.tsx new file mode 100644 index 000000000..365e5fb34 --- /dev/null +++ b/ultrasound/ui/components/NetworkSelect.tsx @@ -0,0 +1,50 @@ +import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'; +import { Button, Menu, MenuButton, MenuItem, MenuList, Text } from '@chakra-ui/react'; +import { NETWORKS, NetworkIcon } from '@snx-v3/useBlockchain'; + +export function NetworkSelect({ + id, + name, + setNetwork, +}: { + id: string | number; + name: string; + setNetwork: ({ id, name }: { id: number; name: string }) => any; +}) { + return ( + + {({ isOpen }) => ( + <> + span': { display: 'flex', alignItems: 'center' } }} + > + + <> + + {name.charAt(0).toUpperCase() + name.slice(1)} + + {isOpen ? : } + + + + {NETWORKS.filter((network) => network.name === 'base')?.map((network) => ( + setNetwork({ id: network.id, name: network.label })} + key={network.id} + > + + + {network.label} + + + ))} + + + )} + + ); +} diff --git a/ultrasound/ui/components/SupplyChangeStats.tsx b/ultrasound/ui/components/SupplyChangeStats.tsx new file mode 100644 index 000000000..107050f9f --- /dev/null +++ b/ultrasound/ui/components/SupplyChangeStats.tsx @@ -0,0 +1,86 @@ +import { Flex, Image, Link, Spinner, Text, Tooltip } from '@chakra-ui/react'; +import { useBurnEvents } from '../hooks/useBurnEvents'; +import { InfoIcon } from '@chakra-ui/icons'; + +export function SupplyChangeStats() { + const { data: events, isLoading } = useBurnEvents(); + + return ( + + {isLoading ? ( + + ) : ( + <> + + + Supply Change + + + 7D + + + + -{events?.supplyChange7Days.toString()} SNX + + + + + Burnt + + + {events?.supplyChange7Days.toString()} SNX + + + + + + Minted + + + 0.00 SNX + + + + No more SNX are being minted. Read more about The End of Synthetix Token Inflation + + } + > + + + + Why? + + + + + + )} + + ); +} diff --git a/ultrasound/ui/favicon.ico b/ultrasound/ui/favicon.ico new file mode 100644 index 000000000..5845395c4 Binary files /dev/null and b/ultrasound/ui/favicon.ico differ diff --git a/ultrasound/ui/hooks/SNXUSDBalanceOfBuyBackContract.ts b/ultrasound/ui/hooks/SNXUSDBalanceOfBuyBackContract.ts new file mode 100644 index 000000000..e63a6bacd --- /dev/null +++ b/ultrasound/ui/hooks/SNXUSDBalanceOfBuyBackContract.ts @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import { BigNumber, Contract, providers } from 'ethers'; + +const USDC = new Contract( + '0x09d51516F38980035153a554c26Df3C6f51a23C3', + ['function balanceOf(address _owner) view returns (uint256 balance)'], + new providers.JsonRpcProvider('https://base.llamarpc.com') +); + +export function SNXUSDBalanceOfBuyBackContract(contract: string) { + return useQuery({ + queryKey: ['USDCBalanceOfBuyBackContract'], + queryFn: async () => { + const balance: BigNumber = await USDC.balanceOf(contract); + return balance; + }, + }); +} diff --git a/ultrasound/ui/hooks/useBurnEvents.ts b/ultrasound/ui/hooks/useBurnEvents.ts new file mode 100644 index 000000000..511dfc2ea --- /dev/null +++ b/ultrasound/ui/hooks/useBurnEvents.ts @@ -0,0 +1,86 @@ +import { useQuery } from '@tanstack/react-query'; +import { BigNumber, Contract, providers, utils } from 'ethers'; + +interface BurnEvent { + ts: number; + snxAmount: number; + usdAmount: number; + cumulativeSnxAmount: number; + cumulativeUsdAmount: number; +} + +const SNXonL1 = new Contract( + '0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F', + ['function totalSupply() view returns(uint256)'], + new providers.JsonRpcProvider('https://eth.llamarpc.com') +); + +const now = new Date(); +now.setDate(now.getDate() - 7); +const thirtyDaysAgo = new Date(); +thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30); + +export function useBurnEvents() { + return useQuery({ + queryKey: ['burn-events'], + queryFn: async () => { + const repsonse = await fetch('https://api.synthetix.io/v3/base/snx-buyback'); + const events: BurnEvent[] = await repsonse.json(); + + const totalSupply: BigNumber = await SNXonL1.totalSupply(); + + const supplyChange7Days = events + .filter((event) => event.ts > now.getTime()) + .reduce((cur, prev) => cur + prev.snxAmount, 0) + .toFixed(2); + + const SNXPriceResponse = await fetch( + 'https://coins.llama.fi/prices/current/ethereum:0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F' + ); + + const { coins } = await SNXPriceResponse.json(); + + const groupedByMonths = events.reduce( + (cur, prev) => { + const currentDate = new Date(prev.ts); + const month = currentDate.toLocaleString('default', { month: 'long', year: 'numeric' }); + if (cur[month]) { + cur[month] = cur[month] - prev.snxAmount; + } else { + cur[month] = Number(utils.formatEther(totalSupply)) - prev.cumulativeSnxAmount; + } + return cur; + }, + {} as Record + ); + + const groupedByLast30Days = events.reduce( + (cur, prev) => { + const currentDate = new Date(prev.ts); + if (currentDate.getTime() > thirtyDaysAgo.getTime()) { + const day = currentDate.toLocaleString('default', { day: '2-digit', month: 'long' }); + if (cur[day]) { + cur[day] = cur[day] - prev.snxAmount; + } else { + cur[day] = Number(utils.formatEther(totalSupply)) - prev.cumulativeSnxAmount; + } + } + return cur; + }, + {} as Record + ); + + return { + totalBurns: events.length, + groupedByMonths, + groupedByLast30Days, + supplyChange7Days, + totalSupply: Number(utils.formatEther(totalSupply)).toLocaleString('en-US', { + maximumFractionDigits: 2, + }), + SNXPrice: + (coins['ethereum:0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F'].price as number) || 0, + }; + }, + }); +} diff --git a/ultrasound/ui/hooks/useSNXPrice.ts b/ultrasound/ui/hooks/useSNXPrice.ts new file mode 100644 index 000000000..8af4620fb --- /dev/null +++ b/ultrasound/ui/hooks/useSNXPrice.ts @@ -0,0 +1,57 @@ +import { useQuery } from '@tanstack/react-query'; +import { useGetNetwork, useProviderForChain } from '@snx-v3/useBlockchain'; +import { erc7412Call } from '@snx-v3/withERC7412'; +import { importOracleManagerProxy, OracleManagerProxyType } from '@synthetixio/v3-contracts'; +import { Contract } from 'ethers'; +import { Wei } from '@synthetixio/wei'; +import { BuyBack } from '../mutations/useSellSNX'; + +export function useSNXPrice() { + const baseNetwork = useGetNetwork(`0x${Number(8453).toString(16)}`); + const baseProvider = useProviderForChain(baseNetwork); + + return useQuery({ + refetchInterval: 5000, + enabled: !!baseProvider, + queryKey: ['snx-price', !!baseProvider], + queryFn: async () => { + if (baseProvider && baseNetwork?.id && baseNetwork?.preset) { + try { + const { address, abi } = await importOracleManagerProxy( + baseNetwork.id, + baseNetwork.preset + ); + const OracleManagerProxy = new Contract( + address, + abi, + baseProvider + ) as OracleManagerProxyType; + + const price = [ + await OracleManagerProxy.populateTransaction.process( + await BuyBack.connect(baseProvider).getSnxNodeId() + ), + ]; + + price[0].from = '0x4200000000000000000000000000000000000006'; + + return await erc7412Call( + baseNetwork, + baseProvider, + price, + (txs) => { + return new Wei( + OracleManagerProxy.interface.decodeFunctionResult('process', txs[0])[0].price + ); + }, + 'priceCall' + ); + } catch (error) { + console.error(error); + return new Wei(0); + } + } + return new Wei(0); + }, + }); +} diff --git a/ultrasound/ui/index.html b/ultrasound/ui/index.html new file mode 100644 index 000000000..ad2fb62a7 --- /dev/null +++ b/ultrasound/ui/index.html @@ -0,0 +1,23 @@ + + + + + + + + + Ultrasound Homes + + +
+ + + diff --git a/ultrasound/ui/load-env.sh b/ultrasound/ui/load-env.sh new file mode 100755 index 000000000..15734fe01 --- /dev/null +++ b/ultrasound/ui/load-env.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -a +source .env +set +a + +echo "Loaded Environment Variables:" +env | grep "NEXT" \ No newline at end of file diff --git a/ultrasound/ui/mutations/useSellSNX.ts b/ultrasound/ui/mutations/useSellSNX.ts new file mode 100644 index 000000000..56ced1546 --- /dev/null +++ b/ultrasound/ui/mutations/useSellSNX.ts @@ -0,0 +1,94 @@ +import { useMutation } from '@tanstack/react-query'; +import { useCoreProxy } from '@snx-v3/useCoreProxy'; +import { useUSDProxy } from '@snx-v3/useUSDProxy'; +import { useSpotMarketProxy } from '@snx-v3/useSpotMarketProxy'; +import Wei from '@synthetixio/wei'; +import { Contract, constants, utils } from 'ethers'; +import { getGasPrice } from '@snx-v3/useGasPrice'; +import { useDefaultProvider, useNetwork, useSigner } from '@snx-v3/useBlockchain'; +import { withERC7412 } from '@snx-v3/withERC7412'; +import { formatGasPriceForTransaction } from '@snx-v3/useGasOptions'; +import { useGasSpeed } from '@snx-v3/useGasSpeed'; +import { notNil } from '@snx-v3/tsHelpers'; +import { useSNXPrice } from '../hooks/useSNXPrice'; + +export const BuyBack = new Contract('0x632cAa10A56343C5e6C0c066735840c096291B18', [ + 'function processBuyback(uint256 snxAmount) external', + 'function getPremium() view returns(uint256)', + 'function getSnxNodeId() view returns(bytes32)', +]); + +export function useSellSNX() { + const { data: CoreProxy } = useCoreProxy(); + const { data: UsdProxy } = useUSDProxy(); + const { data: SpotProxy } = useSpotMarketProxy(); + const provider = useDefaultProvider(); + const { network } = useNetwork(); + const signer = useSigner(); + const { gasSpeed } = useGasSpeed(); + const { data: SNXPrice, refetch } = useSNXPrice(); + + return useMutation({ + mutationKey: ['sell-snx'], + mutationFn: async (amount: Wei) => { + await refetch(); + if (!CoreProxy) return; + if (!network) return; + if (!SpotProxy) return; + if (!signer) return; + if (!SNXPrice) return; + if (!provider) return; + if (!SNXPrice) return; + + const gasPricesPromised = getGasPrice({ provider }); + + const premium = await BuyBack.connect(signer).getPremium(); + + const USDCAmountPlusPremium = SNXPrice.mul(amount).add(SNXPrice.mul(premium)); + + const sellSNX = BuyBack.connect(signer).populateTransaction.processBuyback(amount.toBN()); + + const snxUSDApproval = UsdProxy?.populateTransaction.approve( + SpotProxy.address, + USDCAmountPlusPremium.toBN() + ); + + const buy_SUSD = SpotProxy.populateTransaction.buy( + 1, + USDCAmountPlusPremium.toBN(), + 0, + constants.AddressZero + ); + + const unwrapTxnPromised = SpotProxy.populateTransaction.unwrap( + 1, + USDCAmountPlusPremium.toBN(), + //2% slippage + Number( + utils.formatUnits(USDCAmountPlusPremium.toBN().mul(99).div(100).toString(), 12).toString() + ).toFixed() + ); + const [gasPrices, sellSNX_Txn, sUSDCApproval_Txn, buy_SUSD_Txn, unwrapTxn] = + await Promise.all([ + gasPricesPromised, + sellSNX, + snxUSDApproval, + buy_SUSD, + unwrapTxnPromised, + ]); + + const allCalls = [sellSNX_Txn, sUSDCApproval_Txn, buy_SUSD_Txn, unwrapTxn].filter(notNil); + + const erc7412Tx = await withERC7412(network, allCalls, 'useWithdraw', CoreProxy.interface); + + const gasOptionsForTransaction = formatGasPriceForTransaction({ + gasLimit: erc7412Tx.gasLimit, + gasPrices, + gasSpeed, + }); + + const txn = await signer.sendTransaction({ ...erc7412Tx, ...gasOptionsForTransaction }); + await txn.wait(); + }, + }); +} diff --git a/ultrasound/ui/package.json b/ultrasound/ui/package.json new file mode 100644 index 000000000..861904785 --- /dev/null +++ b/ultrasound/ui/package.json @@ -0,0 +1,92 @@ +{ + "name": "@snx-v3/ultrasound-homes", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "webpack-cli serve", + "build": "NODE_ENV=production webpack-cli", + "load-env": "source load-env.sh", + "focus": "yarn workspaces focus '@snx-v3/ultrasound-homes'" + }, + "dependencies": { + "@chakra-ui/icons": "^2.1.1", + "@chakra-ui/react": "^2.8.2", + "@emotion/react": "^11.11.1", + "@emotion/styled": "^11.11.0", + "@snx-v3/isBaseAndromeda": "workspace:*", + "@snx-v3/tsHelpers": "workspace:*", + "@snx-v3/useApprove": "workspace:*", + "@snx-v3/useBlockchain": "workspace:*", + "@snx-v3/useCoreProxy": "workspace:*", + "@snx-v3/useGasOptions": "workspace:*", + "@snx-v3/useGasPrice": "workspace:*", + "@snx-v3/useGasSpeed": "workspace:*", + "@snx-v3/useSpotMarketProxy": "workspace:*", + "@snx-v3/useTokenBalance": "workspace:*", + "@snx-v3/useUSDProxy": "workspace:*", + "@snx-v3/withERC7412": "workspace:*", + "@synthetixio/v3-contracts": "workspace:*", + "@synthetixio/v3-theme": "workspace:*", + "@synthetixio/wei": "^2.74.4", + "@tanstack/react-query": "^5.8.3", + "@tanstack/react-query-devtools": "^5.8.3", + "@web3-onboard/coinbase": "^2.2.6", + "@web3-onboard/injected-wallets": "^2.10.11", + "@web3-onboard/ledger": "^2.5.2", + "@web3-onboard/react": "^2.8.13", + "@web3-onboard/trezor": "^2.4.3", + "@web3-onboard/walletconnect": "^2.5.3", + "chart.js": "^4.0.0", + "ethers": "^5.7.2", + "framer-motion": "^10.16.5", + "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", + "react-dom": "^18.2.0", + "recoil": "^0.7.7" + }, + "devDependencies": { + "@babel/core": "^7.23.3", + "@babel/plugin-transform-runtime": "^7.23.3", + "@babel/preset-env": "^7.23.3", + "@babel/preset-react": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", + "@babel/runtime-corejs3": "^7.23.2", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "babel-loader": "^9.1.3", + "babel-plugin-istanbul": "^6.1.1", + "bn.js": "^5.2.1", + "copy-webpack-plugin": "^11.0.0", + "core-js": "^3.33.2", + "crypto-browserify": "^3.12.0", + "css-loader": "^6.8.1", + "dotenv": "^16.4.3", + "html-webpack-plugin": "^5.5.3", + "process": "^0.11.10", + "react-refresh": "^0.14.0", + "stream-browserify": "^3.0.0", + "style-loader": "^3.3.3", + "terser-webpack-plugin": "^5.3.9", + "webpack": "^5.89.0", + "webpack-bundle-analyzer": "^4.10.0", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "browserslist": [ + "last 1 Chrome version", + "last 1 iOS version", + "last 1 Safari version", + "last 1 Firefox version", + "last 1 Edge version", + "last 1 Opera version" + ], + "depcheck": { + "ignorePatterns": [ + "dist" + ], + "ignoreMatches": [ + "process" + ] + } +} diff --git a/ultrasound/ui/public/burn-snx.svg b/ultrasound/ui/public/burn-snx.svg new file mode 100644 index 000000000..381e4e889 --- /dev/null +++ b/ultrasound/ui/public/burn-snx.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ultrasound/ui/public/burn.svg b/ultrasound/ui/public/burn.svg new file mode 100644 index 000000000..f97ca035b --- /dev/null +++ b/ultrasound/ui/public/burn.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ultrasound/ui/public/favicon.ico b/ultrasound/ui/public/favicon.ico new file mode 100644 index 000000000..5845395c4 Binary files /dev/null and b/ultrasound/ui/public/favicon.ico differ diff --git a/ultrasound/ui/public/kain.svg b/ultrasound/ui/public/kain.svg new file mode 100644 index 000000000..e15a9e6c4 --- /dev/null +++ b/ultrasound/ui/public/kain.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/ultrasound/ui/public/minted.svg b/ultrasound/ui/public/minted.svg new file mode 100644 index 000000000..e98af90d7 --- /dev/null +++ b/ultrasound/ui/public/minted.svg @@ -0,0 +1,3 @@ + + + diff --git a/ultrasound/ui/public/snx-input.svg b/ultrasound/ui/public/snx-input.svg new file mode 100644 index 000000000..a0b7e4f7f --- /dev/null +++ b/ultrasound/ui/public/snx-input.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ultrasound/ui/public/snx-small.svg b/ultrasound/ui/public/snx-small.svg new file mode 100644 index 000000000..0b01cb4e3 --- /dev/null +++ b/ultrasound/ui/public/snx-small.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ultrasound/ui/public/snx.svg b/ultrasound/ui/public/snx.svg new file mode 100644 index 000000000..f15fa74b5 --- /dev/null +++ b/ultrasound/ui/public/snx.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ultrasound/ui/public/usdc.svg b/ultrasound/ui/public/usdc.svg new file mode 100644 index 000000000..f3804dd20 --- /dev/null +++ b/ultrasound/ui/public/usdc.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ultrasound/ui/src/App.tsx b/ultrasound/ui/src/App.tsx new file mode 100644 index 000000000..8730f8cc3 --- /dev/null +++ b/ultrasound/ui/src/App.tsx @@ -0,0 +1,12 @@ +import { Flex } from '@chakra-ui/react'; +import { Header } from '../components/Header'; +import { Main } from '../components/Main'; + +export function App() { + return ( + +
+
+ + ); +} diff --git a/ultrasound/ui/src/index.tsx b/ultrasound/ui/src/index.tsx new file mode 100644 index 000000000..6592cf5cf --- /dev/null +++ b/ultrasound/ui/src/index.tsx @@ -0,0 +1,30 @@ +import { createRoot } from 'react-dom/client'; +import { ChakraProvider } from '@chakra-ui/react'; +import { theme, Fonts } from '@synthetixio/v3-theme'; +import { RecoilRoot } from 'recoil'; +import { Web3OnboardProvider } from '@web3-onboard/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { onboard } from '../utils/onboard'; +import { App } from './App'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; + +if (typeof document !== 'undefined') { + // eslint-disable-next-line no-undef + const element = document.querySelector('#app'); + if (element) { + const root = createRoot(element); + root.render( + + + + + + + + + + + + ); + } +} diff --git a/ultrasound/ui/tsconfig.json b/ultrasound/ui/tsconfig.json new file mode 100644 index 000000000..f4551678e --- /dev/null +++ b/ultrasound/ui/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "jsx": "react-jsx" + }, + "extends": "../../tsconfig.json" +} diff --git a/ultrasound/ui/utils/addresses.ts b/ultrasound/ui/utils/addresses.ts new file mode 100644 index 000000000..ae8647c55 --- /dev/null +++ b/ultrasound/ui/utils/addresses.ts @@ -0,0 +1,7 @@ +export function shortAddress(address?: string) { + if (!address) return 'not found'; + return address + .substring(0, 4) + .concat('...') + .concat(address.substring(address.length - 4)); +} diff --git a/ultrasound/ui/utils/onboard.ts b/ultrasound/ui/utils/onboard.ts new file mode 100644 index 000000000..1e29c7f4d --- /dev/null +++ b/ultrasound/ui/utils/onboard.ts @@ -0,0 +1,54 @@ +import { NETWORKS, appMetadata } from '@snx-v3/useBlockchain'; +import injectedModule, { ProviderLabel } from '@web3-onboard/injected-wallets'; +import trezorModule from '@web3-onboard/trezor'; +import ledgerModule from '@web3-onboard/ledger'; +import walletConnectModule from '@web3-onboard/walletconnect'; +import coinbaseModule from '@web3-onboard/coinbase'; +import { init } from '@web3-onboard/react'; + +const supportedNetworks = [8453]; + +// Filter networks to only supported ones +export const networks = NETWORKS.filter((n) => supportedNetworks.includes(n.id)).map((n) => ({ + id: n.id, + token: n.token, + label: n.label, + rpcUrl: n.rpcUrl(), +})); + +export const onboard = init({ + wallets: [ + injectedModule({ displayUnavailable: [ProviderLabel.MetaMask, ProviderLabel.Trust] }), + trezorModule({ + appUrl: 'https://liquidity.synthetix.eth.limo', + email: 'info@synthetix.io', + }), + ledgerModule({ + projectId: 'd6eac005846a1c3be1f8eea3a294eed9', + walletConnectVersion: 2, + }), + walletConnectModule({ + version: 2, + projectId: 'd6eac005846a1c3be1f8eea3a294eed9', + dappUrl: 'liquidity.synthetix.eth.limo', + }), + // gnosisModule(), + coinbaseModule(), + ], + chains: [...networks], + appMetadata: { + ...appMetadata, + name: 'Synthetix Governance', + }, + accountCenter: { + desktop: { + enabled: false, + }, + mobile: { + enabled: false, + }, + }, + notify: { + enabled: false, + }, +}); diff --git a/ultrasound/ui/webpack.config.js b/ultrasound/ui/webpack.config.js new file mode 100644 index 000000000..11a0ec7c4 --- /dev/null +++ b/ultrasound/ui/webpack.config.js @@ -0,0 +1,188 @@ +const path = require('path'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); +const webpack = require('webpack'); +const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin'); +const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +require('dotenv').config(); + +// For depcheck to be happy +require.resolve('webpack-dev-server'); + +const isProd = process.env.NODE_ENV === 'production'; +const isTest = process.env.NODE_ENV === 'test'; + +const htmlPlugin = new HtmlWebpackPlugin({ + template: './index.html', + scriptLoading: 'defer', + minify: false, + hash: false, + xhtml: true, + excludeChunks: ['main'], +}); + +const babelRule = { + test: /\.(ts|tsx|js|jsx)$/, + include: [ + // Need to list all the folders in v3 and outside (if used) + /ultrasound\/ui/, + /theme/, + /contracts/, + /liquidity\/components/, + /liquidity\/lib/, + ], + resolve: { + fullySpecified: false, + }, + use: { + loader: require.resolve('babel-loader'), + options: { + configFile: path.resolve(__dirname, 'babel.config.js'), + }, + }, +}; + +const imgRule = { + test: /\.(png|jpg|ico|gif|woff|woff2|ttf|eot|doc|pdf|zip|wav|avi|txt|webp|svg)$/, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 4 * 1024, // 4kb + }, + }, +}; + +const cssRule = { + test: /\.css$/, + include: [new RegExp('./src'), new RegExp('reactflow')], + exclude: [], + use: [ + { + loader: require.resolve('style-loader'), + }, + { + loader: require.resolve('css-loader'), + }, + ], +}; + +const devServer = { + port: '3000', + + hot: !isTest, + liveReload: false, + + historyApiFallback: true, + + devMiddleware: { + writeToDisk: !isTest, + publicPath: '', + }, + + client: { + logging: 'log', + overlay: false, + progress: false, + }, + + static: './public', + + headers: { 'Access-Control-Allow-Origin': '*' }, + allowedHosts: 'all', + open: false, + compress: false, +}; + +module.exports = { + devtool: isProd ? 'source-map' : isTest ? false : 'eval', + devServer, + mode: isProd ? 'production' : 'development', + entry: './src/index.tsx', + + output: { + path: path.resolve(__dirname, 'dist'), + publicPath: '', + filename: '[name].js', + chunkFilename: isProd ? 'chunk/[name].[contenthash:8].js' : '[name].js', + assetModuleFilename: '[name].[contenthash:8][ext]', + clean: true, + }, + + optimization: { + runtimeChunk: false, + splitChunks: { + chunks: 'async', + maxAsyncRequests: 10, + maxInitialRequests: 10, + hidePathInfo: true, + automaticNameDelimiter: '--', + name: false, + }, + moduleIds: isProd ? 'deterministic' : 'named', + chunkIds: isProd ? 'deterministic' : 'named', + minimize: isProd, + minimizer: [new TerserPlugin()], + innerGraph: true, + emitOnErrors: false, + }, + + plugins: [htmlPlugin] + .concat(isProd ? [new CopyWebpackPlugin({ patterns: [{ from: 'public', to: '' }] })] : []) + + .concat([new webpack.NormalModuleReplacementPlugin(/^bn.js$/, require.resolve('bn.js'))]) + + .concat([ + new webpack.NormalModuleReplacementPlugin( + new RegExp(`^@synthetixio/v3-theme$`), + path.resolve(path.dirname(require.resolve(`@synthetixio/v3-theme/package.json`)), 'src') + ), + ]) + .concat([]) + + .concat([ + new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'], + process: 'process/browser.js', + }), + ]) + + .concat(isProd ? [] : isTest ? [] : [new ReactRefreshWebpackPlugin({ overlay: false })]) + .concat( + process.env.GENERATE_BUNDLE_REPORT === 'true' + ? [ + new BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: path.resolve(__dirname, 'tmp', 'webpack.html'), + openAnalyzer: false, + generateStatsFile: false, + }), + ] + : [] + ) + .concat( + new webpack.DefinePlugin({ + 'process.env.INFURA_KEY': JSON.stringify(process.env.INFURA_KEY), + 'process.env.WC_PROJECT_ID': JSON.stringify(process.env.WC_PROJECT_ID), + 'process.env.PYTH_MAINNET_ENDPOINT': JSON.stringify(process.env.PYTH_MAINNET_ENDPOINT), + 'process.env.PYTH_TESTNET_ENDPOINT': JSON.stringify(process.env.PYTH_TESTNET_ENDPOINT), + }) + ), + + resolve: { + fallback: { + buffer: require.resolve('buffer'), + stream: require.resolve('stream-browserify'), + crypto: require.resolve('crypto-browserify'), + process: require.resolve('process/browser.js'), + http: false, + https: false, + os: false, + }, + extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs'], + }, + + module: { + rules: [babelRule, imgRule, cssRule], + }, +}; diff --git a/yarn.lock b/yarn.lock index cdd2b1e77..2032bf469 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4149,6 +4149,13 @@ __metadata: languageName: node linkType: hard +"@kurkle/color@npm:^0.3.0": + version: 0.3.2 + resolution: "@kurkle/color@npm:0.3.2" + checksum: 079c4b7688061070f1d570cee4cf0e7c4085867b940688ff80356a56825a5ace7077257c9ec5863eb344a8eae78379388ab57cdf9d75c491389fad56f411cd43 + languageName: node + linkType: hard + "@ledgerhq/connect-kit-loader@npm:^1.1.0": version: 1.1.8 resolution: "@ledgerhq/connect-kit-loader@npm:1.1.8" @@ -6605,6 +6612,74 @@ __metadata: languageName: unknown linkType: soft +"@snx-v3/ultrasound-homes@workspace:ultrasound/ui": + version: 0.0.0-use.local + resolution: "@snx-v3/ultrasound-homes@workspace:ultrasound/ui" + dependencies: + "@babel/core": "npm:^7.23.3" + "@babel/plugin-transform-runtime": "npm:^7.23.3" + "@babel/preset-env": "npm:^7.23.3" + "@babel/preset-react": "npm:^7.23.3" + "@babel/preset-typescript": "npm:^7.23.3" + "@babel/runtime-corejs3": "npm:^7.23.2" + "@chakra-ui/icons": "npm:^2.1.1" + "@chakra-ui/react": "npm:^2.8.2" + "@emotion/react": "npm:^11.11.1" + "@emotion/styled": "npm:^11.11.0" + "@pmmmwh/react-refresh-webpack-plugin": "npm:^0.5.11" + "@snx-v3/isBaseAndromeda": "workspace:*" + "@snx-v3/tsHelpers": "workspace:*" + "@snx-v3/useApprove": "workspace:*" + "@snx-v3/useBlockchain": "workspace:*" + "@snx-v3/useCoreProxy": "workspace:*" + "@snx-v3/useGasOptions": "workspace:*" + "@snx-v3/useGasPrice": "workspace:*" + "@snx-v3/useGasSpeed": "workspace:*" + "@snx-v3/useSpotMarketProxy": "workspace:*" + "@snx-v3/useTokenBalance": "workspace:*" + "@snx-v3/useUSDProxy": "workspace:*" + "@snx-v3/withERC7412": "workspace:*" + "@synthetixio/v3-contracts": "workspace:*" + "@synthetixio/v3-theme": "workspace:*" + "@synthetixio/wei": "npm:^2.74.4" + "@tanstack/react-query": "npm:^5.8.3" + "@tanstack/react-query-devtools": "npm:^5.8.3" + "@types/react": "npm:^18.2.37" + "@types/react-dom": "npm:^18.2.15" + "@web3-onboard/coinbase": "npm:^2.2.6" + "@web3-onboard/injected-wallets": "npm:^2.10.11" + "@web3-onboard/ledger": "npm:^2.5.2" + "@web3-onboard/react": "npm:^2.8.13" + "@web3-onboard/trezor": "npm:^2.4.3" + "@web3-onboard/walletconnect": "npm:^2.5.3" + babel-loader: "npm:^9.1.3" + babel-plugin-istanbul: "npm:^6.1.1" + bn.js: "npm:^5.2.1" + chart.js: "npm:^4.0.0" + copy-webpack-plugin: "npm:^11.0.0" + core-js: "npm:^3.33.2" + crypto-browserify: "npm:^3.12.0" + css-loader: "npm:^6.8.1" + dotenv: "npm:^16.4.3" + ethers: "npm:^5.7.2" + framer-motion: "npm:^10.16.5" + html-webpack-plugin: "npm:^5.5.3" + process: "npm:^0.11.10" + react: "npm:^18.2.0" + react-chartjs-2: "npm:^5.2.0" + react-dom: "npm:^18.2.0" + react-refresh: "npm:^0.14.0" + recoil: "npm:^0.7.7" + stream-browserify: "npm:^3.0.0" + style-loader: "npm:^3.3.3" + terser-webpack-plugin: "npm:^5.3.9" + webpack: "npm:^5.89.0" + webpack-bundle-analyzer: "npm:^4.10.0" + webpack-cli: "npm:^5.1.4" + webpack-dev-server: "npm:^4.15.1" + languageName: unknown + linkType: soft + "@snx-v3/useAccountCollateral@workspace:*, @snx-v3/useAccountCollateral@workspace:liquidity/lib/useAccountCollateral": version: 0.0.0-use.local resolution: "@snx-v3/useAccountCollateral@workspace:liquidity/lib/useAccountCollateral" @@ -7138,7 +7213,7 @@ __metadata: languageName: unknown linkType: soft -"@snx-v3/useSpotMarketProxy@workspace:liquidity/lib/useSpotMarketProxy": +"@snx-v3/useSpotMarketProxy@workspace:*, @snx-v3/useSpotMarketProxy@workspace:liquidity/lib/useSpotMarketProxy": version: 0.0.0-use.local resolution: "@snx-v3/useSpotMarketProxy@workspace:liquidity/lib/useSpotMarketProxy" dependencies: @@ -12203,6 +12278,15 @@ __metadata: languageName: node linkType: hard +"chart.js@npm:^4.0.0": + version: 4.4.2 + resolution: "chart.js@npm:4.4.2" + dependencies: + "@kurkle/color": "npm:^0.3.0" + checksum: 609444dfc9e847e4c891884309d6083464333e39a7266996fa15f622a44d0c5202c20c86b3bfb1d72b3769096f71c80e131860270c39ce1291cac52b9f45dc6d + languageName: node + linkType: hard + "check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -22801,6 +22885,16 @@ __metadata: languageName: node linkType: hard +"react-chartjs-2@npm:^5.2.0": + version: 5.2.0 + resolution: "react-chartjs-2@npm:5.2.0" + peerDependencies: + chart.js: ^4.1.1 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 0a70b60e1a0d1f0cecdd69d70d9ac1c191618c1033d8f942d4728f5eb84c893c82c9e3318be8a3158eb3a7186c0251a8fe0356c9610ea0f0f3f2d4a9c4a0b388 + languageName: node + linkType: hard + "react-clientside-effect@npm:^1.2.6": version: 1.2.6 resolution: "react-clientside-effect@npm:1.2.6"