diff --git a/package.json b/package.json index 0723b0e88..92028ea1d 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@babel/preset-env": "7.20.2", "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "^7.21.0", + "@chain-registry/osmosis": "^1.63.9", "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", "@graphql-codegen/cli": "^5.0.2", @@ -43,6 +44,8 @@ "@graphql-codegen/typescript-react-apollo": "^4.3.0", "@keplr-wallet/types": "^0.11.52", "@milkdown/kit": "^7.5.0", + "@osmonauts/math": "^1.7.0", + "@osmonauts/utils": "^1.17.0", "@octokit/types": "^13.6.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", "@rjsf/core": "^3.2.1", @@ -69,6 +72,7 @@ "babel-loader": "9.1.2", "babel-plugin-root-import": "^6.6.0", "browserify-zlib": "^0.2.0", + "chain-registry": "^1.67.0", "clean-webpack-plugin": "^4.0.0", "compression-webpack-plugin": "^6.1.1", "constants-browserify": "^1.0.0", @@ -99,6 +103,7 @@ "mini-css-extract-plugin": "^2.6.1", "ncp": "^2.0.0", "os-browserify": "^0.3.0", + "osmojs": "^16.14.0", "path-browserify": "^1.0.1", "postcss-loader": "^7.0.2", "postcss-media-minmax": "^5.0.0", diff --git a/src/components/AvailableAmount/AvailableAmount.module.scss b/src/components/AvailableAmount/AvailableAmount.module.scss index a8f828f54..72925879f 100644 --- a/src/components/AvailableAmount/AvailableAmount.module.scss +++ b/src/components/AvailableAmount/AvailableAmount.module.scss @@ -5,7 +5,10 @@ .containerValue { padding: 0 13px; font-size: 1.25rem; - color: var(--grayscale-dark); + + // color: var(--grayscale-dark); + + color: #fff; height: 42px; display: flex; align-items: center; diff --git a/src/components/AvailableAmount/AvailableAmount.tsx b/src/components/AvailableAmount/AvailableAmount.tsx index 9ed6b2ba5..f142c642c 100644 --- a/src/components/AvailableAmount/AvailableAmount.tsx +++ b/src/components/AvailableAmount/AvailableAmount.tsx @@ -3,17 +3,33 @@ import styles from './AvailableAmount.module.scss'; import LinearGradientContainer, { Color, } from '../LinearGradientContainer/LinearGradientContainer'; +import DenomArr from '../denom/denomArr'; type Props = { amountToken: number | string; title?: string; + denom?: string; + color?: Color; }; -function AvailableAmount({ amountToken, title = 'available amount' }: Props) { +function AvailableAmount({ + amountToken, + title = 'available amount', + denom, + color = Color.Black, +}: Props) { return (
- -
{formatNumber(amountToken)}
+ +
+ {formatNumber(amountToken)} + {denom && ( + + )} +
); diff --git a/src/components/LinearGradientContainer/LinearGradientContainer.module.scss b/src/components/LinearGradientContainer/LinearGradientContainer.module.scss index 054ffdc02..a1bd356b8 100644 --- a/src/components/LinearGradientContainer/LinearGradientContainer.module.scss +++ b/src/components/LinearGradientContainer/LinearGradientContainer.module.scss @@ -91,6 +91,12 @@ $box-shadow-green: rgba(54, 214, 174, 0.53); } } + &.blue { + .textboxBottomLine { + box-shadow: 0px 0px 6px 2px $box-shadow-blue; + } + } + &.yellow { .textboxBottomLine { box-shadow: 0px 0px 6px 2px $box-shadow-yellow; @@ -122,6 +128,12 @@ $box-shadow-green: rgba(54, 214, 174, 0.53); } } + &.blue { + .textboxFace { + background: $linear-gradient-blue; + } + } + &.yellow { .textboxFace { background: $linear-gradient-yellow; diff --git a/src/components/LinearGradientContainer/LinearGradientContainer.tsx b/src/components/LinearGradientContainer/LinearGradientContainer.tsx index 63dfe0e2c..4855bdfa6 100644 --- a/src/components/LinearGradientContainer/LinearGradientContainer.tsx +++ b/src/components/LinearGradientContainer/LinearGradientContainer.tsx @@ -7,6 +7,7 @@ export const enum Color { Red = 'red', Black = 'black', Green = 'green', + Blue = 'blue', } export type Props = { diff --git a/src/components/Select/Select.module.scss b/src/components/Select/Select.module.scss index ac028b90f..da2df26b1 100644 --- a/src/components/Select/Select.module.scss +++ b/src/components/Select/Select.module.scss @@ -93,7 +93,9 @@ } .dropDownHeader { - color: gray; + // color: gray; + + color: #fff; } } } diff --git a/src/components/btnGrd/Button.module.scss b/src/components/btnGrd/Button.module.scss index 678f37882..04bb06f1b 100644 --- a/src/components/btnGrd/Button.module.scss +++ b/src/components/btnGrd/Button.module.scss @@ -1,10 +1,9 @@ $color-text: #36d6ae; - @mixin linear-gradient-green($route) { background: linear-gradient( $route, rgba(0, 237, 235, 0) 0%, - rgba(0, 237, 235, 6) 100% + var(--color-gradient) 100% ); } @@ -16,8 +15,8 @@ $color-text: #36d6ae; box-sizing: border-box; top: 0; bottom: 0; - box-shadow: 0px 0px 4px 1px rgba(54, 214, 174, 0.5), - 0px 0px 15px 1px rgba(54, 214, 174, 0.5); + box-shadow: 0px 0px 4px 1px var(--color-shadow), + 0px 0px 15px 1px var(--color-shadow); } @mixin styleGradientPseudoEl { @@ -46,6 +45,14 @@ $color-text: #36d6ae; height: 52px; min-width: 217px; font-size: 18px; + --color-shadow: rgba(54, 214, 174, 0.5); + --color-gradient: rgba(0, 237, 235, 6); + + &.pending { + color: #FFCA42 !important; + --color-shadow: #FFCA42; + --color-gradient: #FFCA42; + } &:hover { color: #40ecc1; @@ -108,6 +115,24 @@ a.containerBtnGrd { align-items: center; justify-content: center; padding: 0 10px; + + &.yellow { + &::before { + @include styleGradientPseudoEl; + @include linear-gradient-green(-90deg); + left: 3px; + transform-origin: left; + transform: perspective(100px) rotateY(55deg); + } + + &::after { + @include styleGradientPseudoEl; + @include linear-gradient-green(90deg); + right: 3px; + transform-origin: right; + transform: perspective(100px) rotateY(-55deg); + } + } } .containerBtnGrd:not(:disabled) { diff --git a/src/components/btnGrd/index.tsx b/src/components/btnGrd/index.tsx index bbefd9314..0d918997d 100644 --- a/src/components/btnGrd/index.tsx +++ b/src/components/btnGrd/index.tsx @@ -8,8 +8,18 @@ import styles from './Button.module.scss'; const audioBtn = require('../../sounds/main-button.mp3'); // const audioBtnHover = require('../../sounds/main-button-hover.mp3'); -function GradientContainer({ children }: { children: React.ReactNode }) { - return
{children}
; +function GradientContainer({ + children, + color, +}: { + children: React.ReactNode; + color?: 'yellow'; +}) { + return ( +
+ {children} +
+ ); } const audioBtnObg = new Audio(audioBtn); @@ -114,12 +124,13 @@ function Button({ onClick={handleClick} className={cx(styles.containerBtnGrd, className, { [styles.smallBtn]: small, + [styles.pending]: pending, })} disabled={disabled || pending} {...props} {...componentProps} > - + {pending ? ( <> {pendingText || 'pending'} diff --git a/src/features/ibc-history/historyContext.tsx b/src/features/ibc-history/historyContext.tsx index 2c50aeb9a..48e91a39c 100644 --- a/src/features/ibc-history/historyContext.tsx +++ b/src/features/ibc-history/historyContext.tsx @@ -34,6 +34,7 @@ type HistoryContext = { ) => void; useGetHistoriesItems: () => Option>; updateStatusByTxHash: (txHash: string, status: StatusTx) => void; + getItemByTxHash: (txHash: string) => Promise; traceHistoryStatus: (item: HistoriesItem) => Promise; }; @@ -45,6 +46,7 @@ const valueContext = { useGetHistoriesItems: () => {}, updateStatusByTxHash: () => {}, traceHistoryStatus: () => {}, + getItemByTxHash: () => {}, }; export const HistoryContext = React.createContext(valueContext); @@ -55,15 +57,6 @@ export const useIbcHistory = () => { return context; }; -const historiesItemsByAddress = (addressActive: AccountValue | null) => { - if (addressActive) { - return dbIbcHistory.historiesItems - .where({ address: addressActive.bech32 }) - .toArray(); - } - return []; -}; - type UncommitedTx = { txHash: string; address: string; @@ -82,7 +75,7 @@ function HistoryContextProvider({ children }: { children: React.ReactNode }) { useState>(undefined); const { defaultAccount } = useAppSelector((state: RootState) => state.pocket); const [update, setUpdate] = useState(0); - const addressActive = defaultAccount.account?.cyber || undefined; + const addressActive = defaultAccount.account?.cyber || undefined; function getBlockSubscriber(chainId: string): PollingStatusSubscription { if (!blockSubscriberMap.has(chainId)) { @@ -198,7 +191,7 @@ function HistoryContextProvider({ children }: { children: React.ReactNode }) { timeoutUnsubscriber(); } - txTracer.close(); + txTracer.close(); if (result) { return StatusTx.COMPLETE; @@ -234,15 +227,22 @@ function HistoryContextProvider({ children }: { children: React.ReactNode }) { getItem(); }, [addressActive, update]); + const getItemByTxHash = async (txHash: string) => { + return dbIbcHistory.historiesItems + .where({ + txHash, + }) + .toArray(); + }; + const pingTxsIbc = async ( cliet: SigningStargateClient | SigningCyberClient, uncommitedTx: UncommitedTx ) => { const ping = async () => { const response = await cliet.getTx(uncommitedTx.txHash); - if (response) { - const result = parseRawLog(response.rawLog); - const dataFromEvent = parseEvents(result); + if (response && response.events) { + const dataFromEvent = parseEvents(response.events); if (dataFromEvent) { const itemHistories = { ...uncommitedTx, ...dataFromEvent }; addHistoriesItem({ @@ -285,6 +285,7 @@ function HistoryContextProvider({ children }: { children: React.ReactNode }) { useGetHistoriesItems, updateStatusByTxHash, traceHistoryStatus, + getItemByTxHash, }} > {children} diff --git a/src/features/ibc-history/useGetStatus.tsx b/src/features/ibc-history/useGetStatus.tsx index 7ab3d860f..01d4532e7 100644 --- a/src/features/ibc-history/useGetStatus.tsx +++ b/src/features/ibc-history/useGetStatus.tsx @@ -6,10 +6,12 @@ function* toGenerator(p: Promise) { return (yield p) as R; } -function useGetStatus(item: HistoriesItem) { +function useGetStatus(item?: HistoriesItem) { const { traceHistoryStatus, updateStatusByTxHash } = useIbcHistory(); - const [status, setStatus] = useState(item.status); + const [status, setStatus] = useState( + item ? item.status : StatusTx.PENDING + ); // eslint-disable-next-line react-hooks/exhaustive-deps function* tryUpdateHistoryStatus(item: HistoriesItem) { @@ -27,6 +29,9 @@ function useGetStatus(item: HistoriesItem) { } useEffect(() => { + if (!item) { + return; + } const getValue = async () => { const gen = await helper(tryUpdateHistoryStatus(item)); diff --git a/src/features/ibc-history/utils.ts b/src/features/ibc-history/utils.ts index d19f6bf6c..21d4d675d 100644 --- a/src/features/ibc-history/utils.ts +++ b/src/features/ibc-history/utils.ts @@ -1,54 +1,53 @@ -const parseEvents = (rawLog: readonly Log[]) => { +import { Event } from '@cosmjs/stargate'; + +const parseEvents = (events: readonly Event[]) => { try { - if (rawLog && Object.keys(rawLog).length > 0) { - const { events } = rawLog[0]; - if (events) { - // eslint-disable-next-line no-restricted-syntax - for (const event of events) { - if (event.type === 'send_packet') { - const { attributes } = event; - const sourceChannelAttr = attributes.find( - (attr) => attr.key === 'packet_src_channel' - ); - const sourceChannelValue = sourceChannelAttr - ? sourceChannelAttr.value - : undefined; - const destChannelAttr = attributes.find( - (attr) => attr.key === 'packet_dst_channel' - ); - const destChannelValue = destChannelAttr - ? destChannelAttr.value - : undefined; - const sequenceAttr = attributes.find( - (attr) => attr.key === 'packet_sequence' - ); - const sequence = sequenceAttr ? sequenceAttr.value : undefined; - const timeoutHeightAttr = attributes.find( - (attr) => attr.key === 'packet_timeout_height' - ); - const timeoutHeight = timeoutHeightAttr - ? timeoutHeightAttr.value - : undefined; - const timeoutTimestampAttr = attributes.find( - (attr) => attr.key === 'packet_timeout_timestamp' - ); - const timeoutTimestamp = timeoutTimestampAttr - ? timeoutTimestampAttr.value - : undefined; - if (sequence && destChannelValue && sourceChannelValue) { - return { - destChannelId: destChannelValue, - sourceChannelId: sourceChannelValue, - sequence, - timeoutHeight, - timeoutTimestamp, - }; - } - } + if (events && !events.length) { + return undefined; + } + // eslint-disable-next-line no-restricted-syntax + for (const event of events) { + if (event.type === 'send_packet') { + const { attributes } = event; + const sourceChannelAttr = attributes.find( + (attr) => attr.key === 'packet_src_channel' + ); + const sourceChannelValue = sourceChannelAttr + ? sourceChannelAttr.value + : undefined; + const destChannelAttr = attributes.find( + (attr) => attr.key === 'packet_dst_channel' + ); + const destChannelValue = destChannelAttr + ? destChannelAttr.value + : undefined; + const sequenceAttr = attributes.find( + (attr) => attr.key === 'packet_sequence' + ); + const sequence = sequenceAttr ? sequenceAttr.value : undefined; + const timeoutHeightAttr = attributes.find( + (attr) => attr.key === 'packet_timeout_height' + ); + const timeoutHeight = timeoutHeightAttr + ? timeoutHeightAttr.value + : undefined; + const timeoutTimestampAttr = attributes.find( + (attr) => attr.key === 'packet_timeout_timestamp' + ); + const timeoutTimestamp = timeoutTimestampAttr + ? timeoutTimestampAttr.value + : undefined; + if (sequence && destChannelValue && sourceChannelValue) { + return { + destChannelId: destChannelValue, + sourceChannelId: sourceChannelValue, + sequence, + timeoutHeight, + timeoutTimestamp, + }; } } } - return null; } catch (e) { console.debug('error parseLog', e); return null; diff --git a/src/pages/Energy/Energy.module.scss b/src/pages/Energy/Energy.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/pages/Energy/Energy.tsx b/src/pages/Energy/Energy.tsx new file mode 100644 index 000000000..a5a751080 --- /dev/null +++ b/src/pages/Energy/Energy.tsx @@ -0,0 +1,33 @@ +import { Route, Routes } from 'react-router-dom'; +import HistoryContextProvider from 'src/features/ibc-history/historyContext'; +import EnergyMain from './ui/pages/Main/Main'; +import OsmosisRpcProvider from './context/OsmosisRpcProvider'; +import EnergyProvider from './context/Energy.context'; +import OsmosisSignerProvider from './context/OsmosisSignerProvider'; +import Layout from './ui/Layout/Layout'; + +function EnergyRouter() { + return ( + + }> + } /> + + + ); +} + +function Energy() { + return ( + + + + + + + + + + ); +} + +export default Energy; diff --git a/src/pages/Energy/constants.ts b/src/pages/Energy/constants.ts new file mode 100644 index 000000000..710bdfa2d --- /dev/null +++ b/src/pages/Energy/constants.ts @@ -0,0 +1,11 @@ +import networkList from 'src/utils/networkListIbc'; + +export const Slippages = [1, 2.5, 3, 5]; + +export const EnergyPackages = ['1', '100', '1000']; + +export const sellTokensSymbol = ['OSMO', 'ATOM']; + +export const CHAIN_ID_OSMO = networkList['osmosis-1'].chainId; + +export const RPC_OSMO = networkList['osmosis-1'].rpc; diff --git a/src/pages/Energy/context/Energy.context.tsx b/src/pages/Energy/context/Energy.context.tsx new file mode 100644 index 000000000..01f182001 --- /dev/null +++ b/src/pages/Energy/context/Energy.context.tsx @@ -0,0 +1,53 @@ +import React, { useCallback, useContext, useMemo } from 'react'; +import { CoinDenom, PriceHash } from '@osmonauts/math/types'; +import { Coin } from '@cosmjs/stargate'; +import useBalances from '../hooks/useBalances'; +import useSwap from '../hooks/useSwap'; +import { EnergyPackageSwapRoutes } from '../types/EnergyPackages'; +import { useOsmosisSign } from './OsmosisSignerProvider'; + +const EnergyContext = React.createContext<{ + prices: PriceHash; + balances: { + assets: Coin[]; + hash: Record; + }; + energyPackageSwapRoutes?: EnergyPackageSwapRoutes[]; + refetch: () => void; +}>({ + prices: {}, + balances: { assets: [], hash: {} }, + energyPackageSwapRoutes: [], + refetch: () => {}, +}); + +export function useEnergy() { + return useContext(EnergyContext); +} + +function EnergyProvider({ children }: { children: React.ReactNode }) { + const { address } = useOsmosisSign(); + const { assets, hash, refetch: refetchBal } = useBalances(address); + const { energyPackageSwapRoutes, refetchSwapRoute, prices } = useSwap(); + + const refetchFnc = useCallback(() => { + refetchBal(); + refetchSwapRoute(); + }, [refetchBal, refetchSwapRoute]); + + const value = useMemo( + () => ({ + prices, + balances: { assets, hash }, + energyPackageSwapRoutes, + refetch: refetchFnc, + }), + [prices, assets, hash, energyPackageSwapRoutes, refetchFnc] + ); + + return ( + {children} + ); +} + +export default EnergyProvider; diff --git a/src/pages/Energy/context/OsmosisRpcProvider.tsx b/src/pages/Energy/context/OsmosisRpcProvider.tsx new file mode 100644 index 000000000..7999c787b --- /dev/null +++ b/src/pages/Energy/context/OsmosisRpcProvider.tsx @@ -0,0 +1,42 @@ +import React, { useContext } from 'react'; +import { osmosis } from 'osmojs'; +import { useQuery } from '@tanstack/react-query'; +import { Option } from 'src/types'; +import { CHAIN_ID_OSMO, RPC_OSMO } from '../constants'; + +const { createRPCQueryClient } = osmosis.ClientFactory; + +type Client = Awaited>; + +const QueryClientContext = React.createContext>(undefined); + +export function useOsmosisRpc() { + return useContext(QueryClientContext); +} + +function OsmosisRpcProvider({ children }: { children: React.ReactNode }) { + const { data, error, isFetching } = useQuery({ + queryKey: [CHAIN_ID_OSMO, 'connect'], + queryFn: async () => { + return createRPCQueryClient({ + rpcEndpoint: RPC_OSMO, + }); + }, + }); + + if (isFetching) { + return null; + } + + if (error) { + console.error('Error queryClient connect: ', error.message); + } + + return ( + + {children} + + ); +} + +export default OsmosisRpcProvider; diff --git a/src/pages/Energy/context/OsmosisSignerProvider.tsx b/src/pages/Energy/context/OsmosisSignerProvider.tsx new file mode 100644 index 000000000..08d0ce00c --- /dev/null +++ b/src/pages/Energy/context/OsmosisSignerProvider.tsx @@ -0,0 +1,78 @@ +import { SigningStargateClient } from '@cosmjs/stargate'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; +import { getKeplr } from 'src/utils/keplrUtils'; +import { OfflineSigner } from '@cosmjs/proto-signing'; +import { getSigningOsmosisClient } from 'osmojs'; +import { Option } from 'src/types'; +import { useAppSelector } from 'src/redux/hooks'; +import { CHAIN_ID_OSMO, RPC_OSMO } from '../constants'; + +const OsmosisSignerContext = React.createContext<{ + address?: string; + signingClient?: SigningStargateClient; +}>({ + address: undefined, + signingClient: undefined, +}); + +export function useOsmosisSign() { + const signingClient = useContext(OsmosisSignerContext); + return signingClient; +} + +function OsmosisSignerProvider({ children }: { children: React.ReactNode }) { + const { defaultAccount } = useAppSelector((state) => state.pocket); + const [address, setAddress] = useState>(undefined); + const [signingClient, setSigningClient] = + useState>(undefined); + + async function initSigner() { + const windowKeplr = await getKeplr(); + + if (!windowKeplr) { + return; + } + + await windowKeplr.enable(CHAIN_ID_OSMO); + + const offlineSigner: OfflineSigner = await windowKeplr.getOfflineSignerAuto( + CHAIN_ID_OSMO + ); + + const [{ address }] = await offlineSigner.getAccounts(); + + setAddress(address); + + const stargateClient = await getSigningOsmosisClient({ + rpcEndpoint: RPC_OSMO, + signer: offlineSigner, + }); + + setSigningClient(stargateClient); + } + + useEffect(() => { + (async () => { + const windowKeplr = await getKeplr(); + if (windowKeplr) { + initSigner(); + } + })(); + }, [defaultAccount]); + + const value = useMemo( + () => ({ + address, + signingClient, + }), + [address, signingClient] + ); + + return ( + + {children} + + ); +} + +export default OsmosisSignerProvider; diff --git a/src/pages/Energy/hooks/txs/useBuyPackage.ts b/src/pages/Energy/hooks/txs/useBuyPackage.ts new file mode 100644 index 000000000..e63d16ec2 --- /dev/null +++ b/src/pages/Energy/hooks/txs/useBuyPackage.ts @@ -0,0 +1,9 @@ +import useTx from '../useTx'; + +function useBuyPackage() { + const { tx } = useTx(); + + return null; +} + +export default useBuyPackage; diff --git a/src/pages/Energy/hooks/txs/useIbc.ts b/src/pages/Energy/hooks/txs/useIbc.ts new file mode 100644 index 000000000..c2b7eb8a4 --- /dev/null +++ b/src/pages/Energy/hooks/txs/useIbc.ts @@ -0,0 +1,78 @@ +import { useMutation } from '@tanstack/react-query'; +import { useHub } from 'src/contexts/hub'; +import { useAppDispatch, useAppSelector } from 'src/redux/hooks'; +import { CHAIN_ID_OSMO } from '../../constants'; +import { useOsmosisSign } from '../../context/OsmosisSignerProvider'; +import newIbcMessage from '../../utils/tranferIbc'; +import useTx from '../useTx'; +import { useEnergy } from '../../context/Energy.context'; +import { setIbcResult, setStatusOrder } from '../../redux/energy.redux'; +import { StatusOrder } from '../../redux/utils'; + +function useIbc() { + const { refetch, energyPackageSwapRoutes } = useEnergy(); + const { channels } = useHub(); + const { address, signingClient } = useOsmosisSign(); + + const dispatch = useAppDispatch(); + + const { swapResult, selectPlan, statusOrder, ibcResult } = useAppSelector( + (state) => state.energy + ); + + const { tx } = useTx(); + + const findChannels = channels && channels[CHAIN_ID_OSMO]; + + const { data, isLoading, error, mutate } = useMutation({ + mutationKey: [], + mutationFn: async () => { + if (!address || !signingClient || !findChannels || !swapResult) { + return; + } + + const ibcMsg = newIbcMessage(swapResult.tokens, findChannels, address); + + const res = await tx([...ibcMsg]); + + console.log('res', res); + + if (res.error) { + console.log('{res.errorMsg}', res.errorMsg); + } else if (res.isSuccess) { + // ibcMsg.forEach((item) => { + const { denom, amount } = swapResult.tokens[0]; + const { transactionHash } = res.response; + const tokenSelect = getOsmoAssetByDenom(denom); + const aliases = tokenSelect?.denom_units[0].aliases; + const transferData = { + txHash: transactionHash, + address: ibcMsg[0].value.receiver, + sourceChainId: findChannels.destination_chain_id, + destChainId: findChannels.source_chain_id, + sender: address, + recipient: ibcMsg[0].value.receiver, + createdAt: getNowUtcNumber(), + amount: coinFunc(amount, aliases ? aliases[0] : denom), + }; + + // pingTxsIbc(signingClient, transferData); + + dispatch( + setIbcResult({ ibcHash: transactionHash, status: StatusTx.PENDING }) + ); + + dispatch(setStatusOrder(StatusOrder.STATUS_IBC)); + // }); + + console.log('IBC Send successful'); + + refetch(); + } + }, + }); + + return null; +} + +export default useIbc; diff --git a/src/pages/Energy/hooks/useBalances.ts b/src/pages/Energy/hooks/useBalances.ts new file mode 100644 index 000000000..46264b3fa --- /dev/null +++ b/src/pages/Energy/hooks/useBalances.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import { Coin } from '@cosmjs/stargate'; +import { useOsmosisRpc } from '../context/OsmosisRpcProvider'; + +function useBalances(address?: string) { + const rpc = useOsmosisRpc(); + + const { data, refetch } = useQuery( + ['osmosis', 'allBalances', address], + async () => { + if (!address) { + return undefined; + } + return rpc?.cosmos.bank.v1beta1.allBalances({ + address, + }); + }, + { enabled: Boolean(rpc && address) } + ); + + const all = data?.balances || []; + const hash = (all.reduce( + (acc, coin) => ({ ...acc, [coin.denom]: coin }), + {} + ) || {}) as Record; + const pools = all.filter((coin) => coin.denom.startsWith('gamm')) || []; + const assets = all.filter((coin) => !coin.denom.startsWith('gamm')) || []; + + return { all, hash, pools, assets, refetch }; +} + +export default useBalances; diff --git a/src/pages/Energy/hooks/usePools.ts b/src/pages/Energy/hooks/usePools.ts new file mode 100644 index 000000000..4abd4045f --- /dev/null +++ b/src/pages/Energy/hooks/usePools.ts @@ -0,0 +1,41 @@ +import { useQuery } from '@tanstack/react-query'; +import { Pool } from 'osmojs/osmosis/gamm/v1beta1/balancerPool'; +import { useOsmosisRpc } from '../context/OsmosisRpcProvider'; +import { paginate } from '../utils/utils'; +import { Osmosis } from '../utils/assets'; + +export type PoolList = Pool[] & { + priced: Pool[]; +}; + +function usePools() { + const rpc = useOsmosisRpc(); + const { data, refetch } = useQuery( + ['osmosis', 'pools'], + async () => { + return rpc?.osmosis.poolmanager.v1beta1.allPools({ + pagination: paginate(5000n), + }); + }, + { enabled: Boolean(rpc), refetchInterval: 1_000 * 60 * 3 } + ); + + // rpc?.osmosis.poolmanager.v1beta1.estimateSinglePoolSwapExactAmountIn(); + + const all: Pool[] = data?.pools || []; + const map = all.reduce( + (map, pool) => map.set(pool.id, pool), + new Map() + ); + const freefloat = (all.filter(({ $typeUrl }) => + $typeUrl?.includes('/osmosis.gamm.v1beta1.Pool') + ) || []) as PoolList; + + freefloat.priced = freefloat.filter(({ poolAssets }) => + poolAssets.every(({ token }) => Osmosis.CoinDenomToAsset[token.denom]) + ); + + return { all, map, freefloat: freefloat.priced, refetch }; +} + +export default usePools; diff --git a/src/pages/Energy/hooks/usePrices.ts b/src/pages/Energy/hooks/usePrices.ts new file mode 100644 index 000000000..b8e86bfc0 --- /dev/null +++ b/src/pages/Energy/hooks/usePrices.ts @@ -0,0 +1,38 @@ +import { PriceHash } from '@osmonauts/math/types'; +import { useQuery } from '@tanstack/react-query'; + +const getPriceHash = async () => { + let prices = []; + + try { + const response = await fetch( + 'https://api-osmosis.imperator.co/tokens/v2/all' + ); + if (!response.ok) { + throw Error('Get price error'); + } + prices = await response.json(); + } catch (err) { + console.error(err); + } + + const priceHash = prices.reduce( + (prev: any, cur: { denom: any; price: any }) => ({ + ...prev, + [cur.denom]: cur.price, + }), + {} + ); + + return priceHash; +}; + +function usePrices() { + const { data, refetch } = useQuery(['osmosis', 'priceHash'], async () => { + return getPriceHash() as Promise; + }); + + return { data: data || {}, refetch }; +} + +export default usePrices; diff --git a/src/pages/Energy/hooks/useQueryHooks.ts b/src/pages/Energy/hooks/useQueryHooks.ts new file mode 100644 index 000000000..92f769885 --- /dev/null +++ b/src/pages/Energy/hooks/useQueryHooks.ts @@ -0,0 +1,5 @@ +function useQueryHooks() { + return null; +} + +export default useQueryHooks; diff --git a/src/pages/Energy/hooks/useSwap.ts b/src/pages/Energy/hooks/useSwap.ts new file mode 100644 index 000000000..26b5c8084 --- /dev/null +++ b/src/pages/Energy/hooks/useSwap.ts @@ -0,0 +1,83 @@ +import { useMemo } from 'react'; +import BigNumber from 'bignumber.js'; +import { useAppSelector } from 'src/redux/hooks'; +import { newCoin, newToken, newTokensRoutes } from '../utils/swap'; +import { + getOsmoAssetByDenom, + isEmptyArray, + symbolToOsmoDenom, +} from '../utils/utils'; +import { EnergyPackages } from '../constants'; +import usePrices from './usePrices'; +import usePools from './usePools'; +import { makePoolPairs } from '../utils/pool'; +import { EnergyPackageSwapRoutes } from '../types/EnergyPackages'; + +function useSwap() { + const { freefloat: pools, refetch: refetchPools } = usePools(); + const { data: prices, refetch: refetchPrice } = usePrices(); + const { tokenSell } = useAppSelector((state) => state.energy); + + const findTokenDenom = getOsmoAssetByDenom( + symbolToOsmoDenom(tokenSell) || '' + ); + + const fromToken = newToken(findTokenDenom); + + const pricesSelectedDenom = prices[fromToken.denom] || 0; + + const energyPackagesByDenom: { [key: string]: string } = + EnergyPackages.reduce( + (acc, item) => ({ + ...acc, + [item]: new BigNumber(item) + .multipliedBy(pricesSelectedDenom) + .toString(), + }), + {} + ); + + const pairs = useMemo(() => { + if (isEmptyArray(pools)) { + return []; + } + + return makePoolPairs(pools, prices); + }, [pools, prices]); + + const energyPackageSwapRoutes = useMemo(() => { + if (!pricesSelectedDenom || isEmptyArray(pairs) || isEmptyArray(pools)) { + return undefined; + } + + const result: EnergyPackageSwapRoutes[] = []; + + Object.keys(energyPackagesByDenom).forEach((key) => { + const item = energyPackagesByDenom[key]; + const swapRoute = newTokensRoutes(item, fromToken, { pairs, pools }); + if (swapRoute) { + const tokenOut = swapRoute.map((item) => item.tokenOutConvert); + + result.push({ + keyPackage: key, + tokenIn: newCoin({ denom: fromToken.denom, amount: item }), + swapInfo: swapRoute, + tokenOut, + }); + } + }); + + return result; + }, [pricesSelectedDenom, pairs, pools, energyPackagesByDenom, fromToken]); + + const refetchSwapRoute = () => { + refetchPools(); + refetchPrice(); + }; + + console.log('energyPackageSwapRoutes', energyPackageSwapRoutes); + + return { energyPackageSwapRoutes, prices, refetchSwapRoute }; +} + +export default useSwap; diff --git a/src/pages/Energy/hooks/useTx.ts b/src/pages/Energy/hooks/useTx.ts new file mode 100644 index 000000000..c9c799a24 --- /dev/null +++ b/src/pages/Energy/hooks/useTx.ts @@ -0,0 +1,75 @@ +// eslint-disable-next-line max-classes-per-file +import { isDeliverTxSuccess } from '@cosmjs/stargate'; +import { estimateOsmoFee } from '@osmonauts/utils'; +import { cosmos, DeliverTxResponse } from 'osmojs'; +import { useOsmosisSign } from '../context/OsmosisSignerProvider'; + +type Msg = { + typeUrl: string; + value: { [key: string]: any }; +}; + +class TxError extends Error { + constructor(message = 'Tx Error', options?: ErrorOptions) { + super(message, options); + this.name = 'TxError'; + } +} + +class TxResult { + error?: TxError; + + response?: DeliverTxResponse; + + constructor({ error, response }: Pick) { + this.error = error; + this.response = response; + } + + get errorMsg() { + return this.isOutOfGas + ? `Out of gas. gasWanted: ${this.response?.gasWanted} gasUsed: ${this.response?.gasUsed}` + : this.error?.message || 'Swap Failed'; + } + + get isSuccess() { + return this.response && isDeliverTxSuccess(this.response); + } + + get isOutOfGas() { + return this.response && this.response.gasUsed > this.response.gasWanted; + } +} + +function useTx() { + const { address, signingClient } = useOsmosisSign(); + + async function tx(msgs: Msg[]) { + if (!address || !signingClient) { + return new TxResult({ error: new TxError('Wallet not connected') }); + } + + try { + const txRaw = cosmos.tx.v1beta1.TxRaw; + const fee = await estimateOsmoFee(signingClient, address, [...msgs], ''); + const signed = await signingClient.sign(address, [...msgs], fee, ''); + + if (!signed) { + return new TxResult({ error: new TxError('Invalid transaction') }); + } + + const response: any = await signingClient.broadcastTx( + Uint8Array.from(txRaw.encode(signed).finish()) + ); + return isDeliverTxSuccess(response) + ? new TxResult({ response }) + : new TxResult({ response, error: new TxError(response.rawLog) }); + } catch (e: any) { + return new TxResult({ error: new TxError(e.message || 'Tx Error') }); + } + } + + return { tx }; +} + +export default useTx; diff --git a/src/pages/Energy/redux/energy.redux.ts b/src/pages/Energy/redux/energy.redux.ts new file mode 100644 index 000000000..5a30adf69 --- /dev/null +++ b/src/pages/Energy/redux/energy.redux.ts @@ -0,0 +1,100 @@ +import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import { Coin } from '@cosmjs/stargate'; +import { StatusTx } from 'src/features/ibc-history/HistoriesItem'; +import { StatusOrder } from './utils'; + +type SliceState = { + statusOrder: StatusOrder; + selectPlan?: { keyPackage: string; tokenIn: Coin }; + tokenSell: 'OSMO' | 'ATOM'; + swapResult?: { + swapTx: string; + tokens: Coin[]; + }; + ibcResult?: { + ibcHash: string; + status: StatusTx; + }; +}; + +const initStateTokenSell = 'OSMO'; + +const keyEnergyStateApp = 'energy-state-app'; + +const stateGetItem = localStorage.getItem(keyEnergyStateApp); + +const poolsInitState = stateGetItem + ? (JSON.parse(stateGetItem) as SliceState) + : undefined; + +const initialState: SliceState = { + statusOrder: poolsInitState + ? poolsInitState.statusOrder + : StatusOrder.SELECT_PACK, + selectPlan: poolsInitState?.selectPlan, + tokenSell: poolsInitState ? poolsInitState.tokenSell : initStateTokenSell, + swapResult: poolsInitState?.swapResult, + ibcResult: poolsInitState?.ibcResult, +}; + +function saveToLocalStorage(state: SliceState) { + localStorage.setItem(keyEnergyStateApp, JSON.stringify(state)); +} + +const slice = createSlice({ + name: 'energyPackages', + initialState, + reducers: { + setStatusOrder: (state, { payload }: PayloadAction) => { + state.statusOrder = payload; + saveToLocalStorage(state); + }, + setSelectPlan: ( + state, + { payload }: PayloadAction + ) => { + state.selectPlan = payload; + saveToLocalStorage(state); + }, + setTokenSell: ( + state, + { payload }: PayloadAction + ) => { + state.tokenSell = payload; + saveToLocalStorage(state); + }, + setSwapResult: ( + state, + { payload }: PayloadAction + ) => { + state.swapResult = payload; + saveToLocalStorage(state); + }, + setIbcResult: ( + state, + { payload }: PayloadAction + ) => { + state.ibcResult = payload; + saveToLocalStorage(state); + }, + resetEnergy: (state) => { + state.selectPlan = undefined; + state.swapResult = undefined; + state.ibcResult = undefined; + state.statusOrder = StatusOrder.SELECT_PACK; + + localStorage.removeItem(keyEnergyStateApp); + }, + }, +}); + +export const { + setStatusOrder, + setSelectPlan, + setSwapResult, + setTokenSell, + setIbcResult, + resetEnergy, +} = slice.actions; + +export default slice.reducer; diff --git a/src/pages/Energy/redux/utils.ts b/src/pages/Energy/redux/utils.ts new file mode 100644 index 000000000..4e929f421 --- /dev/null +++ b/src/pages/Energy/redux/utils.ts @@ -0,0 +1,7 @@ +export enum StatusOrder { + SELECT_PACK = 'select_pack', + SWAP = 'swap', + SEND_IBC = 'send_ibc', + STATUS_IBC = 'status_ibc', + FINISH_IBC = 'finish_ibc', +} diff --git a/src/pages/Energy/types/EnergyPackages.ts b/src/pages/Energy/types/EnergyPackages.ts new file mode 100644 index 000000000..5cfe51e00 --- /dev/null +++ b/src/pages/Energy/types/EnergyPackages.ts @@ -0,0 +1,12 @@ +import { Coin } from '@cosmjs/stargate'; +import { SwapTokensWithRoutes } from './swap'; + +// eslint-disable-next-line import/no-unused-modules, import/prefer-default-export +export type EnergyPackagesKey = '10' | '100' | '1000'; + +export type EnergyPackageSwapRoutes = { + keyPackage: string; + tokenIn: Coin; + tokenOut: Coin[]; + swapInfo: SwapTokensWithRoutes[]; +}; diff --git a/src/pages/Energy/types/swap.ts b/src/pages/Energy/types/swap.ts new file mode 100644 index 000000000..a9aaa6126 --- /dev/null +++ b/src/pages/Energy/types/swap.ts @@ -0,0 +1,17 @@ +import { Coin } from '@cosmjs/stargate'; +import { SwapAmountInRoute } from 'osmojs/osmosis/poolmanager/v1beta1/swap_route'; +import { Token } from './token'; + +export type Swap = { + to: Token; + from: Token; +}; + +export type SwapTokensWithRoutes = { + swap: { + tokenIn: Coin; + tokenOut: Coin; + }; + routes: SwapAmountInRoute[]; + tokenOutConvert: Coin; +}; diff --git a/src/pages/Energy/types/token.ts b/src/pages/Energy/types/token.ts new file mode 100644 index 000000000..e0fa53a2e --- /dev/null +++ b/src/pages/Energy/types/token.ts @@ -0,0 +1,21 @@ +import { Coin } from '@cosmjs/stargate'; +import { CoinDenom } from '@osmonauts/math/types'; +import { Asset, Chain } from '@chain-registry/types'; + +export type Token = { + logo?: string; + denom: string; + asset?: Asset; + chain?: Chain; + price?: number; + symbol?: string; + amount?: string; + value?: string; + $value?: string; + balance?: Coin; +}; + +export type TokenList = Token[] & { + rest: Token[]; + hash: Record; +}; diff --git a/src/pages/Energy/types/type.ts b/src/pages/Energy/types/type.ts new file mode 100644 index 000000000..ffc4a9a5a --- /dev/null +++ b/src/pages/Energy/types/type.ts @@ -0,0 +1,97 @@ +import { Colors } from 'src/components/containerGradient/types'; +import ghostIcon from '../ui/pages/SelectPackages/images/ghost.png'; +import smartIcon from '../ui/pages/SelectPackages/images/smart.png'; +import prodigyIcon from '../ui/pages/SelectPackages/images/prodigy.png'; +import geniusIcon from '../ui/pages/SelectPackages/images/genius.png'; +import { EnergyPackages } from '../constants'; + +export interface Feature { + label: string; + subLabel: string; +} + +export interface Plan { + name: string; + color: Colors; + price: string; + icon: string; + features: boolean[]; + symbols: number; + uploads: number; + fuel: number; + energy: number; + influence: number; + ampers: number; + volts: number; +} + +export const features: Feature[] = [ + { label: 'brain surf', subLabel: 'search for free' }, + { label: 'private pins', subLabel: 'unlimited' }, + { label: 'public upload', subLabel: 'cyberlinks daily' }, + { label: '.moon citizenship', subLabel: 'afford cool short name' }, + { label: 'fuel', subLabel: 'main token, liquid' }, + { label: 'energy', subLabel: 'your power, A x V = kW' }, + { label: 'influence', subLabel: 'your content visibility' }, + { label: '3 free to use aips', subLabel: '' }, + { label: 'all powered aips', subLabel: '' }, +]; + +export const plans: Plan[] = [ + { + name: 'ghost', + color: Colors.WHITE, + price: 'free', + icon: ghostIcon, + features: [true, true, false, false, false, false, false, true, false], + symbols: 8, + uploads: 0, + fuel: 0, + energy: 0, + influence: 0, + ampers: 0, + volts: 0, + }, + { + name: 'smart', + color: Colors.GREEN, + price: EnergyPackages[0], + icon: smartIcon, + features: [true, true, true, true, true, true, true, true, false], + symbols: 5, + uploads: 80, + fuel: 2, + energy: 4, + influence: 1, + ampers: 0, + volts: 0, + }, + { + name: 'prodigy', + color: Colors.BLUE, + price: EnergyPackages[1], + icon: prodigyIcon, + features: [true, true, true, true, true, true, true, true, true], + symbols: 4, + uploads: 800, + fuel: 20, + energy: 1, + influence: 7, + ampers: 0, + volts: 0, + }, + { + name: 'genius', + color: Colors.PURPLE, + price: EnergyPackages[2], + icon: geniusIcon, + features: [true, true, true, true, true, true, true, true, true], + symbols: 3, + uploads: 8000, + fuel: 200, + energy: 11, + influence: 19, + ampers: 0, + volts: 0, + }, +]; diff --git a/src/pages/Energy/ui/Layout/Layout.tsx b/src/pages/Energy/ui/Layout/Layout.tsx new file mode 100644 index 000000000..f6dc7e43b --- /dev/null +++ b/src/pages/Energy/ui/Layout/Layout.tsx @@ -0,0 +1,24 @@ +import { Outlet } from 'react-router-dom'; +import { Helmet } from 'react-helmet'; +import { MainContainer } from 'src/components'; +import { MoonAnimation, Stars } from 'src/containers/portal/components'; +// import styles from './Layout.module.scss'; + +function Layout() { + return ( + + + {/* */} + +
+ + energy | cyb + + + +
+
+ ); +} + +export default Layout; diff --git a/src/pages/Energy/ui/pages/BuyPackages/ActionBar.tsx b/src/pages/Energy/ui/pages/BuyPackages/ActionBar.tsx new file mode 100644 index 000000000..e024ff6d8 --- /dev/null +++ b/src/pages/Energy/ui/pages/BuyPackages/ActionBar.tsx @@ -0,0 +1,206 @@ +import { Coin, coin } from '@cosmjs/stargate'; +import { DeliverTxResponse } from 'osmojs'; +import { useState } from 'react'; +import { ActionBar, Button } from 'src/components'; +import { useEnergy } from 'src/pages/Energy/context/Energy.context'; +import { useOsmosisSign } from 'src/pages/Energy/context/OsmosisSignerProvider'; +import useTx from 'src/pages/Energy/hooks/useTx'; +import { newSwapMessage } from 'src/pages/Energy/utils/swap'; +import { useHub } from 'src/contexts/hub'; +import { CHAIN_ID_OSMO } from 'src/pages/Energy/constants'; +import BigNumber from 'bignumber.js'; +import { useIbcHistory } from 'src/features/ibc-history/historyContext'; +import { getNowUtcNumber } from 'src/utils/date'; +import { + resetEnergy, + setIbcResult, + setStatusOrder, + setSwapResult, +} from 'src/pages/Energy/redux/energy.redux'; +import { StatusOrder } from 'src/pages/Energy/redux/utils'; +import { useAppDispatch, useAppSelector } from 'src/redux/hooks'; +import { StatusTx } from 'src/features/ibc-history/HistoriesItem'; +import { getOsmoAssetByDenom } from 'src/pages/Energy/utils/utils'; +import newIbcMessage from 'src/pages/Energy/utils/tranferIbc'; + +const coinFunc = (amount: string, denom: string): Coin => { + return { denom, amount: new BigNumber(amount).toString(10) }; +}; + +function newTokenSwapped(res: DeliverTxResponse) { + const tokenSwapped = res.events.find((item) => item.type === 'token_swapped'); + + const tokensOut = tokenSwapped?.attributes.find( + (item) => item.key === 'tokens_out' + ); + + if (!tokensOut) { + return undefined; + } + + const value = tokensOut.value.split('ibc/'); + + return coin(value[0], `ibc/${value[1]}`); +} + +function ActionBarContainer() { + const { refetch, energyPackageSwapRoutes } = useEnergy(); + const { address, signingClient } = useOsmosisSign(); + const { pingTxsIbc } = useIbcHistory(); + const { channels } = useHub(); + const dispatch = useAppDispatch(); + const { swapResult, selectPlan, statusOrder, ibcResult } = useAppSelector( + (state) => state.energy + ); + + const findChannels = channels && channels[CHAIN_ID_OSMO]; + + const selectPackage = energyPackageSwapRoutes?.find( + (item) => item.keyPackage === selectPlan?.keyPackage + ); + + const { tx } = useTx(); + + const [isSwapping, setIsSwapping] = useState(false); + const [isIbcSending, setIsIbcSending] = useState(false); + + const onIbcTxs = async () => { + if (!address || !signingClient || !findChannels) { + return; + } + + if (!swapResult) { + return; + } + + setIsIbcSending(true); + + const ibcMsg = newIbcMessage(swapResult.tokens, findChannels, address); + + console.log('ibcMsg', ibcMsg); + + const res = await tx([...ibcMsg]); + + setIsIbcSending(false); + + console.log('res', res); + + if (res.error) { + console.log('{res.errorMsg}', res.errorMsg); + } else if (res.isSuccess) { + // ibcMsg.forEach((item) => { + const { denom, amount } = swapResult.tokens[0]; + const { transactionHash } = res.response; + const tokenSelect = getOsmoAssetByDenom(denom); + const aliases = tokenSelect?.denom_units[0].aliases; + const transferData = { + txHash: transactionHash, + address: ibcMsg[0].value.receiver, + sourceChainId: findChannels.destination_chain_id, + destChainId: findChannels.source_chain_id, + sender: address, + recipient: ibcMsg[0].value.receiver, + createdAt: getNowUtcNumber(), + amount: coinFunc(amount, aliases ? aliases[0] : denom), + }; + + pingTxsIbc(signingClient, transferData); + + dispatch( + setIbcResult({ ibcHash: transactionHash, status: StatusTx.PENDING }) + ); + + dispatch(setStatusOrder(StatusOrder.STATUS_IBC)); + // }); + + console.log('IBC Send successful'); + + refetch(); + } + }; + + const onBuyPackage = async () => { + if (!address || !signingClient) { + console.error('stargateClient undefined or address undefined.'); + return; + } + + if (!selectPackage) { + console.log('energyPackageSwapRoutes undefined'); + return; + } + console.log('address', address); + + setIsSwapping(true); + + const msgIn = newSwapMessage(selectPackage.swapInfo, address); + console.log('msgIn', msgIn); + + const res = await tx([...msgIn]); + setIsSwapping(false); + + console.log('res', res); + + if (res.error) { + console.log('{res.errorMsg}', res.errorMsg); + } else if (res.isSuccess) { + console.log('Swap successful'); + if (res.response) { + const tokenSwapped = newTokenSwapped(res.response); + console.log('tokenSwapped', tokenSwapped); + + if (!tokenSwapped) { + return; + } + dispatch( + setSwapResult({ + swapTx: res.response.transactionHash, + tokens: [tokenSwapped], + }) + ); + dispatch(setStatusOrder(StatusOrder.SEND_IBC)); + } + refetch(); + } + }; + + console.log('ibcResult', ibcResult); + + return ( + + {(statusOrder === StatusOrder.SELECT_PACK || + statusOrder === StatusOrder.SWAP) && ( +