diff --git a/package.json b/package.json index 66c7990..e542efb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@celo-tools/mento-fi", - "version": "1.2.0", + "version": "1.3.0", "description": "A simple DApp for Celo Mento exchanges", "keywords": [ "Celo", @@ -26,6 +26,7 @@ }, "dependencies": { "@celo-tools/use-contractkit": "^2.1.2", + "@ethersproject/abi": "^5.5.0", "@ethersproject/address": "^5.5.0", "@metamask/inpage-provider": "6.0.1", "@metamask/jazzicon": "https://github.com/jmrossy/jazzicon#7a8df28", diff --git a/src/blockchain/ABIs/sortedOracles.ts b/src/blockchain/ABIs/sortedOracles.ts new file mode 100644 index 0000000..e48b555 --- /dev/null +++ b/src/blockchain/ABIs/sortedOracles.ts @@ -0,0 +1,3 @@ +export const ABI = JSON.parse( + '[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"MedianUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"oracleAddress","type":"address"}],"name":"OracleAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"oracleAddress","type":"address"}],"name":"OracleRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"oracle","type":"address"}],"name":"OracleReportRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token","type":"address"},{"indexed":true,"internalType":"address","name":"oracle","type":"address"},{"indexed":false,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"OracleReported","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"reportExpiry","type":"uint256"}],"name":"ReportExpirySet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"token","type":"address"},{"indexed":false,"internalType":"uint256","name":"reportExpiry","type":"uint256"}],"name":"TokenReportExpirySet","type":"event"},{"constant":true,"inputs":[],"name":"initialized","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"isOracle","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"isOwner","outputs":[{"internalType":"bool","name":"","type":"bool"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"oracles","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"renounceOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"reportExpirySeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"tokenReportExpirySeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getVersionNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"pure","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"_reportExpirySeconds","type":"uint256"}],"name":"initialize","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"uint256","name":"_reportExpirySeconds","type":"uint256"}],"name":"setReportExpiry","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"_token","type":"address"},{"internalType":"uint256","name":"_reportExpirySeconds","type":"uint256"}],"name":"setTokenReportExpiry","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"oracleAddress","type":"address"}],"name":"addOracle","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"oracleAddress","type":"address"},{"internalType":"uint256","name":"index","type":"uint256"}],"name":"removeOracle","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"n","type":"uint256"}],"name":"removeExpiredReports","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"isOldestReportExpired","outputs":[{"internalType":"bool","name":"","type":"bool"},{"internalType":"address","name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"address","name":"lesserKey","type":"address"},{"internalType":"address","name":"greaterKey","type":"address"}],"name":"report","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"numRates","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"medianRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getRates","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"enum SortedLinkedListWithMedian.MedianRelation[]","name":"","type":"uint8[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"numTimestamps","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"medianTimestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getTimestamps","outputs":[{"internalType":"address[]","name":"","type":"address[]"},{"internalType":"uint256[]","name":"","type":"uint256[]"},{"internalType":"enum SortedLinkedListWithMedian.MedianRelation[]","name":"","type":"uint8[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getOracles","outputs":[{"internalType":"address[]","name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"getTokenReportExpirySeconds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"}]' +) diff --git a/src/blockchain/blocks.ts b/src/blockchain/blocks.ts index 51d0e0d..d4f2cdb 100644 --- a/src/blockchain/blocks.ts +++ b/src/blockchain/blocks.ts @@ -1,33 +1,26 @@ +import type { ContractKit } from '@celo/contractkit' +import BigNumber from 'bignumber.js' import { AVG_BLOCK_TIMES } from 'src/config/consts' import { logger } from 'src/utils/logger' export interface LatestBlockDetails { - nodeUrl: string number: number timestamp: number } -//TODO -const provider = { - connection: { - url: 'foobar', - }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - getBlock: (s: any) => ({ number: 9125760, timestamp: 1633205701023 }), -} +export async function getLatestBlockDetails(kit: ContractKit): Promise { + const block = await kit.web3.eth.getBlock('latest') -export async function getLatestBlockDetails(): Promise { - const nodeUrl = provider.connection?.url - const block = await provider.getBlock('latest') if (!block || !block.number) { logger.warn('Latest block is not valid') return null } + const timestamp = new BigNumber(block.timestamp).toNumber() + return { - nodeUrl, number: block.number, - timestamp: block.timestamp, + timestamp, } } diff --git a/src/components/buttons/SolidButton.tsx b/src/components/buttons/SolidButton.tsx index d3e8845..63ea1f5 100644 --- a/src/components/buttons/SolidButton.tsx +++ b/src/components/buttons/SolidButton.tsx @@ -41,8 +41,8 @@ export function SolidButton(props: PropsWithChildren) { onActive = 'active:bg-red-400' } else if (color === 'white') { baseColors = 'bg-white text-black' - onHover = 'hover:bg-gray-50' - onActive = 'active:bg-gray-100' + onHover = 'hover:bg-gray-100' + onActive = 'active:bg-gray-200' } const onDisabled = 'disabled:bg-gray-300 disabled:text-gray-500' const weight = bold ? 'font-semibold' : '' diff --git a/src/components/nav/BalancesSummary.tsx b/src/components/nav/BalancesSummary.tsx new file mode 100644 index 0000000..15630f5 --- /dev/null +++ b/src/components/nav/BalancesSummary.tsx @@ -0,0 +1,20 @@ +import { useAppSelector } from 'src/app/hooks' +import { NativeTokenId, NativeTokens } from 'src/config/tokens' +import { TokenIcon } from 'src/images/tokens/TokenIcon' +import { fromWeiRounded } from 'src/utils/amount' + +export function BalancesSummary() { + const balances = useAppSelector((s) => s.account.balances) + const tokenIds = Object.keys(balances) as NativeTokenId[] + + return ( +
+ {tokenIds.map((id) => ( +
+ +
{fromWeiRounded(balances[id])}
+
+ ))} +
+ ) +} diff --git a/src/components/nav/ConnectButton.tsx b/src/components/nav/ConnectButton.tsx index 78759a5..2c83273 100644 --- a/src/components/nav/ConnectButton.tsx +++ b/src/components/nav/ConnectButton.tsx @@ -1,16 +1,34 @@ import { useContractKit } from '@celo-tools/use-contractkit' import Image from 'next/image' +import { useState } from 'react' import useDropdownMenu from 'react-accessible-dropdown-menu-hook' import { SolidButton } from 'src/components/buttons/SolidButton' import { Identicon } from 'src/components/Identicon' +import { BalancesSummary } from 'src/components/nav/BalancesSummary' +import { NetworkModal } from 'src/components/nav/NetworkModal' +import Clipboard from 'src/images/icons/clipboard-plus.svg' +import Cube from 'src/images/icons/cube.svg' import Wallet from 'src/images/icons/wallet.svg' import XCircle from 'src/images/icons/x-circle.svg' import { shortenAddress } from 'src/utils/addresses' +import { tryClipboardSet } from 'src/utils/clipboard' export function ConnectButton() { const { connect, address, destroy } = useContractKit() - const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(1) + const { buttonProps, itemProps, isOpen, setIsOpen } = useDropdownMenu(3) + + const onClickCopy = async () => { + setIsOpen(false) + if (!address) return + await tryClipboardSet(address) + } + + const [showNetworkModal, setShowNetworkModal] = useState(false) + const onClickChangeNetwork = () => { + setIsOpen(false) + setShowNetworkModal(true) + } const onClickDisconnect = async () => { setIsOpen(false) @@ -45,18 +63,26 @@ export function ConnectButton() { )}
- + + + +
Copy Address
+
+ + +
Change Network
+
+
Disconnect
+ {showNetworkModal && ( + setShowNetworkModal(false)} /> + )} ) } @@ -76,3 +102,21 @@ function LogoutIcon() { ) } + +function NetworkIcon() { + return ( +
+ Network +
+ ) +} + +function CopyIcon() { + return ( +
+ Copy +
+ ) +} + +const menuOptionClasses = 'flex items-center cursor-pointer p-2 mt-1 rounded hover:bg-gray-100' diff --git a/src/components/nav/Footer.tsx b/src/components/nav/Footer.tsx index 7517008..f373e5d 100644 --- a/src/components/nav/Footer.tsx +++ b/src/components/nav/Footer.tsx @@ -82,7 +82,9 @@ function BlockIndicator() { >
- setShowNetworkModal(false)} /> + {showNetworkModal && ( + setShowNetworkModal(false)} /> + )} ) } diff --git a/src/config/config.ts b/src/config/config.ts index 1fec65a..83dfa09 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -3,20 +3,18 @@ interface Config { version: string | null url: string discordUrl: string - chainId: number + blockscoutUrl: string showPriceChart: boolean } const isDevMode = process?.env?.NODE_ENV === 'development' const version = process?.env?.NEXT_PUBLIC_VERSION ?? null -const configMainnet: Config = { +export const config: Config = Object.freeze({ debug: isDevMode, version, url: 'https://mento.finance', discordUrl: 'https://discord.gg/E9AqUQnWQE', - chainId: 42220, - showPriceChart: false, -} - -export const config = Object.freeze(configMainnet) + blockscoutUrl: 'https://explorer.celo.org', + showPriceChart: true, +}) diff --git a/src/config/tokens.ts b/src/config/tokens.ts index a618e55..1442c16 100644 --- a/src/config/tokens.ts +++ b/src/config/tokens.ts @@ -1,4 +1,3 @@ -import { config } from 'src/config/config' import { Color } from 'src/styles/Color' export interface Token { @@ -7,7 +6,6 @@ export interface Token { name: string color: string decimals: number - chainId: number } export interface TokenWithBalance extends Token { @@ -37,7 +35,6 @@ export const NativeTokens: INativeTokens = { name: 'Celo Native', color: Color.celoGold, decimals: 18, - chainId: config.chainId, }, cUSD: { id: NativeTokenId.cUSD, @@ -45,7 +42,6 @@ export const NativeTokens: INativeTokens = { name: 'Celo Dollar', color: Color.celoGreen, decimals: 18, - chainId: config.chainId, }, cEUR: { id: NativeTokenId.cEUR, @@ -53,7 +49,6 @@ export const NativeTokens: INativeTokens = { name: 'Celo Euro', color: Color.celoGreen, decimals: 18, - chainId: config.chainId, }, cREAL: { id: NativeTokenId.cREAL, @@ -61,7 +56,6 @@ export const NativeTokens: INativeTokens = { name: 'Celo Real', color: Color.celoGreen, decimals: 18, - chainId: config.chainId, }, } diff --git a/src/features/chart/PriceChartCelo.tsx b/src/features/chart/PriceChartCelo.tsx index 06894f1..9feec31 100644 --- a/src/features/chart/PriceChartCelo.tsx +++ b/src/features/chart/PriceChartCelo.tsx @@ -1,10 +1,14 @@ -import { useAppSelector } from 'src/app/hooks' +import { Mainnet, useContractKit } from '@celo-tools/use-contractkit' +import { useEffect } from 'react' +import { toast } from 'react-toastify' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { NativeTokenId } from 'src/config/tokens' +import { fetchTokenPrice } from 'src/features/chart/fetchPrices' import styles from 'src/features/chart/PriceChart.module.css' -// import { fetchTokenPriceActions } from 'src/features/chart/fetchPrices' import { tokenPriceHistoryToChartData } from 'src/features/chart/utils' import { FloatingBox } from 'src/layout/FloatingBox' import { Color } from 'src/styles/Color' +import { logger } from 'src/utils/logger' import ReactFrappeChart from './ReactFrappeChart' interface PriceChartProps { @@ -16,14 +20,23 @@ interface PriceChartProps { export function PriceChartCelo(props: PriceChartProps) { const { stableTokenId, containerClasses, height } = props - // const dispatch = useAppDispatch() - // useEffect(() => { - // dispatch( - // fetchTokenPriceActions.trigger({ - // baseCurrency: NativeTokenId.CELO, - // }) - // ) - // }, [dispatch]) + const { kit, initialised, network } = useContractKit() + + const dispatch = useAppDispatch() + useEffect(() => { + if (!kit || !initialised || network?.chainId !== Mainnet.chainId) return + dispatch( + fetchTokenPrice({ + kit, + baseCurrency: NativeTokenId.CELO, + }) + ) + .unwrap() + .catch((err) => { + toast.warn('Error retrieving chart data') + logger.error('Failed to token prices', err) + }) + }, [dispatch, kit, initialised, network]) const allPrices = useAppSelector((s) => s.tokenPrice.prices) const celoPrices = allPrices[NativeTokenId.CELO] @@ -31,25 +44,30 @@ export function PriceChartCelo(props: PriceChartProps) { const chartData = tokenPriceHistoryToChartData(stableTokenPrices) const chartHeight = height || 250 + // Only show chart for Mainnet + if (network?.chainId !== Mainnet.chainId) return null + return ( - -
-

CELO Price (USD)

- {/* TODO duration toggle */} -
-
-
- -
-
+
+ +
+

CELO Price (USD)

+ {/* TODO duration toggle */} +
+
+
+ +
+
+
) } diff --git a/src/features/chart/fetchPrices.ts b/src/features/chart/fetchPrices.ts index 258ff42..d58016e 100644 --- a/src/features/chart/fetchPrices.ts +++ b/src/features/chart/fetchPrices.ts @@ -1,183 +1,228 @@ -// import { createAsyncThunk } from '@reduxjs/toolkit' -// import type { AppDispatch, AppState } from 'src/app/store' -// import { getLatestBlockDetails, getNumBlocksPerInterval } from 'src/blockchain/blocks' -// // import { getContract } from 'src/blockchain/contracts' -// import { config } from 'src/config/config' -// import { MAX_TOKEN_PRICE_NUM_DAYS } from 'src/config/consts' -// import { NativeTokenId, NativeTokens, StableTokenIds } from 'src/config/tokens' -// import { -// BaseCurrencyPriceHistory, -// PairPriceUpdate, -// QuoteCurrency, -// QuoteCurrencyPriceHistory, -// TokenPricePoint, -// } from 'src/features/chart/types' -// import { findMissingPriceDays, mergePriceHistories } from 'src/features/chart/utils' -// import { areAddressesEqual, ensureLeading0x } from 'src/utils/addresses' -// import { fromFixidity } from 'src/utils/amount' -// import { -// BlockscoutTransactionLog, -// queryBlockscout, -// validateBlockscoutLog, -// } from 'src/utils/blockscout' -// import { logger } from 'src/utils/logger' - -// const DEFAULT_HISTORY_NUM_DAYS = 7 -// const SECONDS_PER_DAY = 86400 -// const BLOCK_FETCHING_INTERVAL_SIZE = 300 // 6 minutes -// const MEDIAN_UPDATED_TOPIC_0 = '0xa9981ebfc3b766a742486e898f54959b050a66006dbce1a4155c1f84a08bcf41' -// const EXPECTED_MIN_CELO_TO_STABLE = 0.1 -// const EXPECTED_MAX_CELO_TO_STABLE = 100 - -// interface FetchTokenPriceParams { -// baseCurrency: NativeTokenId -// numDays?: number // 7 by default -// } - -// export const fetchTokenPrice = createAsyncThunk< -// PairPriceUpdate[] | null, -// FetchTokenPriceParams, -// { dispatch: AppDispatch; state: AppState } -// >('chart/fetchPrices', async (params, thunkAPI) => { -// const { baseCurrency, numDays } = params -// const prices: BaseCurrencyPriceHistory = thunkAPI.getState().tokenPrice.prices -// const pairPriceUpdates = await _fetchTokenPrice(prices, baseCurrency, numDays) -// return pairPriceUpdates -// }) - -// // Currently this only fetches CELO to stable token prices -// // May eventually expand to fetch other pairs -// async function _fetchTokenPrice( -// prices: BaseCurrencyPriceHistory, -// baseCurrency: NativeTokenId, -// numDays = DEFAULT_HISTORY_NUM_DAYS -// ) { -// if (numDays > MAX_TOKEN_PRICE_NUM_DAYS) { -// throw new Error(`Cannot retrieve prices for such a wide window: ${numDays}`) -// } -// if (baseCurrency !== NativeTokenId.CELO) { -// throw new Error('Only CELO <-> Native currency is currently supported') -// } - -// const pairPriceUpdates = await fetchStableTokenPrices(numDays, prices[baseCurrency]) -// return pairPriceUpdates -// } - -// // Fetches token prices by retrieving and parsing the oracle reporting tx logs -// async function fetchStableTokenPrices(numDays: number, oldPrices?: QuoteCurrencyPriceHistory) { -// const latestBlock = await getLatestBlockDetails() -// if (!latestBlock) throw new Error('Latest block number needed for fetching prices') - -// const missingDays = findMissingPriceDays(numDays, oldPrices) -// // Skip task if all needed days are already in store -// if (!missingDays.length) return null - -// // const oracleContract = getContract(CeloContract.SortedOracles) -// const oracleContract: any = 'TODO' -// const numBlocksPerDay = getNumBlocksPerInterval(SECONDS_PER_DAY) -// const numBlocksPerInterval = getNumBlocksPerInterval(BLOCK_FETCHING_INTERVAL_SIZE) -// const priceUpdates: QuoteCurrencyPriceHistory = {} - -// for (const day of missingDays) { -// const toBlock = latestBlock.number - numBlocksPerDay * day -// const fromBlock = toBlock - numBlocksPerInterval -// const tokenToPrice = await tryFetchOracleLogs(fromBlock, toBlock, oracleContract) -// if (!tokenToPrice) continue -// // Prepends the new price point to each tokens history -// for (const [id, price] of tokenToPrice) { -// if (!priceUpdates[id]) priceUpdates[id] = [] -// priceUpdates[id]!.push(price) -// } -// } - -// const mergedPrices = mergePriceHistories(priceUpdates, oldPrices) - -// const pairPriceUpdates: PairPriceUpdate[] = [] -// for (const key of Object.keys(mergedPrices)) { -// const quoteCurrency = key as QuoteCurrency // TS limitation of Object.keys() -// const prices = mergedPrices[quoteCurrency]! -// pairPriceUpdates.push({ baseCurrency: NativeTokenId.CELO, quoteCurrency, prices }) -// } -// return pairPriceUpdates -// } - -// async function tryFetchOracleLogs(fromBlock: number, toBlock: number, oracleContract: Contract) { -// try { -// const url = `${config.blockscoutUrl}/api?module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=${toBlock}&address=${oracleContract.address}&topic0=${MEDIAN_UPDATED_TOPIC_0}` -// const txLogs = await queryBlockscout>(url) -// return parseBlockscoutOracleLogs(txLogs, oracleContract, fromBlock) -// } catch (error) { -// logger.error(`Failed to fetch and parse oracle logs for blocks ${fromBlock}-${toBlock}`, error) -// return null -// } -// } - -// function parseBlockscoutOracleLogs( -// logs: Array, -// oracleContract: Contract, -// minBlock: number -// ) { -// const tokenToPrice = new Map() -// for (const id of StableTokenIds) { -// const tokenAddress = NativeTokens[id].address -// const price = parseBlockscoutOracleLogsForToken(logs, oracleContract, tokenAddress, minBlock) -// if (price) tokenToPrice.set(id, price) -// } -// return tokenToPrice -// } - -// function parseBlockscoutOracleLogsForToken( -// logs: Array, -// oracleContract: Contract, -// searchToken: string, -// minBlock: number -// ): TokenPricePoint | null { -// if (!logs || !logs.length) throw new Error('No oracle logs found in time range') - -// for (const log of logs) { -// try { -// validateBlockscoutLog(log, MEDIAN_UPDATED_TOPIC_0, minBlock) - -// const filteredTopics = log.topics.filter((t) => !!t) -// const logDescription = oracleContract.interface.parseLog({ -// topics: filteredTopics, -// data: log.data, -// }) - -// if (logDescription.name !== 'MedianUpdated') { -// throw new Error(`Unexpected log name: ${logDescription.name}`) -// } - -// const { token, value } = logDescription.args -// if (!token || !areAddressesEqual(token, searchToken)) { -// // Log is likely for a different token -// continue -// } - -// const valueAdjusted = fromFixidity(value) -// if ( -// valueAdjusted <= EXPECTED_MIN_CELO_TO_STABLE || -// valueAdjusted >= EXPECTED_MAX_CELO_TO_STABLE -// ) { -// throw new Error(`Invalid median value: ${value}`) -// } - -// const timestamp = BigNumber.from(ensureLeading0x(log.timeStamp)).mul(1000) -// if (timestamp.lte(0) || timestamp.gt(Date.now() + 600000)) { -// throw new Error(`Invalid timestamp: ${log.timeStamp}`) -// } - -// return { timestamp: timestamp.toNumber(), price: valueAdjusted } -// } catch (error) { -// logger.warn('Unable to parse token price log, will attempt next', error) -// } -// } - -// logger.error(`All log parse attempts failed or no log found for token ${searchToken}`) -// return null -// } - -export const fetchTokenPrice = () => { - // eslint-disable-next-line no-console - console.log('TODO') +import type { ContractKit } from '@celo/contractkit' +import { Interface } from '@ethersproject/abi' +import { createAsyncThunk } from '@reduxjs/toolkit' +import BigNumber from 'bignumber.js' +import type { AppDispatch, AppState } from 'src/app/store' +import { ABI as SortedOraclesAbi } from 'src/blockchain/ABIs/sortedOracles' +import { getLatestBlockDetails, getNumBlocksPerInterval } from 'src/blockchain/blocks' +import { config } from 'src/config/config' +import { MAX_TOKEN_PRICE_NUM_DAYS } from 'src/config/consts' +import { nativeTokenToKitToken } from 'src/config/tokenMapping' +import { NativeTokenId, StableTokenIds } from 'src/config/tokens' +import { + BaseCurrencyPriceHistory, + PairPriceUpdate, + QuoteCurrency, + QuoteCurrencyPriceHistory, + TokenPricePoint, +} from 'src/features/chart/types' +import { findMissingPriceDays, mergePriceHistories } from 'src/features/chart/utils' +import { areAddressesEqual, ensureLeading0x } from 'src/utils/addresses' +import { fromFixidity } from 'src/utils/amount' +import { + BlockscoutTransactionLog, + queryBlockscout, + validateBlockscoutLog, +} from 'src/utils/blockscout' +import { logger } from 'src/utils/logger' +import { sleep } from 'src/utils/timeout' + +const DEFAULT_HISTORY_NUM_DAYS = 7 +const SECONDS_PER_DAY = 86400 +const BLOCK_FETCHING_INTERVAL_SIZE = 60 // 1 minutes +const PAUSE_BETWEEN_FETCH_REQUESTS = 250 // 1/4 second +const MAX_TIME_FROM_NOW_FOR_LOG = 600_000 // 10 minutes +const MEDIAN_UPDATED_TOPIC_0 = '0xa9981ebfc3b766a742486e898f54959b050a66006dbce1a4155c1f84a08bcf41' +const EXPECTED_MIN_CELO_TO_STABLE = 0.1 +const EXPECTED_MAX_CELO_TO_STABLE = 100 + +let oracleInterface: Interface | undefined +let oracleAddress: string | undefined +const tokenAddresses: Partial> = {} + +interface FetchTokenPriceParams { + kit: ContractKit + baseCurrency: NativeTokenId + numDays?: number // 7 by default +} + +export const fetchTokenPrice = createAsyncThunk< + PairPriceUpdate[] | null, + FetchTokenPriceParams, + { dispatch: AppDispatch; state: AppState } +>('chart/fetchPrices', async (params, thunkAPI) => { + const { kit, baseCurrency, numDays } = params + const prices: BaseCurrencyPriceHistory = thunkAPI.getState().tokenPrice.prices + const pairPriceUpdates = await _fetchTokenPrice(kit, prices, baseCurrency, numDays) + return pairPriceUpdates +}) + +// Currently this only fetches CELO to stable token prices +// May eventually expand to fetch other pairs +async function _fetchTokenPrice( + kit: ContractKit, + prices: BaseCurrencyPriceHistory, + baseCurrency: NativeTokenId, + numDays = DEFAULT_HISTORY_NUM_DAYS +) { + if (numDays > MAX_TOKEN_PRICE_NUM_DAYS) { + throw new Error(`Cannot retrieve prices for such a wide window: ${numDays}`) + } + if (baseCurrency !== NativeTokenId.CELO) { + throw new Error('Only CELO <-> Native currency is currently supported') + } + + const pairPriceUpdates = await fetchStableTokenPrices(kit, numDays, prices[baseCurrency]) + return pairPriceUpdates +} + +// Fetches token prices by retrieving and parsing the oracle reporting tx logs +async function fetchStableTokenPrices( + kit: ContractKit, + numDays: number, + oldPrices?: QuoteCurrencyPriceHistory +) { + const latestBlock = await getLatestBlockDetails(kit) + if (!latestBlock) throw new Error('Latest block number needed for fetching prices') + + const missingDays = findMissingPriceDays(numDays, oldPrices) + // Skip task if all needed days are already in store + if (!missingDays.length) return null + + const oracleInterface = getOracleInterface() + const numBlocksPerDay = getNumBlocksPerInterval(SECONDS_PER_DAY) + const numBlocksPerInterval = getNumBlocksPerInterval(BLOCK_FETCHING_INTERVAL_SIZE) + const priceUpdates: QuoteCurrencyPriceHistory = {} + + for (const day of missingDays) { + const toBlock = latestBlock.number - numBlocksPerDay * day + const fromBlock = toBlock - numBlocksPerInterval + const tokenToPrice = await tryFetchOracleLogs(kit, fromBlock, toBlock, oracleInterface) + if (!tokenToPrice) continue + // Prepends the new price point to each tokens history + for (const [id, price] of tokenToPrice) { + if (!priceUpdates[id]) priceUpdates[id] = [] + priceUpdates[id]!.push(price) + } + await sleep(PAUSE_BETWEEN_FETCH_REQUESTS) // Brief pause to help avoid overloading blockscout and/or getting rate limited + } + + const mergedPrices = mergePriceHistories(priceUpdates, oldPrices) + + const pairPriceUpdates: PairPriceUpdate[] = [] + for (const key of Object.keys(mergedPrices)) { + const quoteCurrency = key as QuoteCurrency // TS limitation of Object.keys() + const prices = mergedPrices[quoteCurrency]! + pairPriceUpdates.push({ baseCurrency: NativeTokenId.CELO, quoteCurrency, prices }) + } + return pairPriceUpdates +} + +async function tryFetchOracleLogs( + kit: ContractKit, + fromBlock: number, + toBlock: number, + oracleInterface: Interface +) { + try { + const oracleAddress = await getOracleAddress(kit) + const url = `${config.blockscoutUrl}/api?module=logs&action=getLogs&fromBlock=${fromBlock}&toBlock=${toBlock}&address=${oracleAddress}&topic0=${MEDIAN_UPDATED_TOPIC_0}` + const txLogs = await queryBlockscout>(url) + const pricePoints = await parseBlockscoutOracleLogs(kit, txLogs, oracleInterface, fromBlock) + return pricePoints + } catch (error) { + logger.error(`Failed to fetch and parse oracle logs for blocks ${fromBlock}-${toBlock}`, error) + return null + } +} + +async function parseBlockscoutOracleLogs( + kit: ContractKit, + logs: Array, + oracleInterface: Interface, + minBlock: number +) { + const tokenToPrice = new Map() + for (const id of StableTokenIds) { + const tokenAddress = await getTokenAddress(kit, id) + const price = parseBlockscoutOracleLogsForToken(logs, oracleInterface, tokenAddress, minBlock) + if (price) tokenToPrice.set(id, price) + } + return tokenToPrice +} + +function parseBlockscoutOracleLogsForToken( + logs: Array, + oracleInterface: Interface, + searchToken: string, + minBlock: number +): TokenPricePoint | null { + if (!logs || !logs.length) throw new Error('No oracle logs found in time range') + + for (const log of logs) { + try { + validateBlockscoutLog(log, MEDIAN_UPDATED_TOPIC_0, minBlock) + + const filteredTopics = log.topics.filter((t) => !!t) + const logDescription = oracleInterface.parseLog({ + topics: filteredTopics, + data: log.data, + }) + + if (logDescription.name !== 'MedianUpdated') { + throw new Error(`Unexpected log name: ${logDescription.name}`) + } + + const { token, value } = logDescription.args + if (!token || !value || !areAddressesEqual(token, searchToken)) { + // Log is likely for a different token + continue + } + + const valueAdjusted = fromFixidity(value.toString()).toNumber() + if ( + valueAdjusted <= EXPECTED_MIN_CELO_TO_STABLE || + valueAdjusted >= EXPECTED_MAX_CELO_TO_STABLE + ) { + throw new Error(`Invalid median value: ${value}`) + } + + const timestamp = new BigNumber(ensureLeading0x(log.timeStamp)).times(1000) + if (timestamp.lte(0) || timestamp.gt(Date.now() + MAX_TIME_FROM_NOW_FOR_LOG)) { + throw new Error(`Invalid timestamp: ${log.timeStamp}`) + } + + return { timestamp: timestamp.toNumber(), price: valueAdjusted } + } catch (error) { + logger.warn('Unable to parse token price log, will attempt next', error) + } + } + + logger.error(`All log parse attempts failed or no log found for token ${searchToken}`) + return null +} + +function getOracleInterface() { + if (!oracleInterface) { + oracleInterface = new Interface(SortedOraclesAbi) + } + return oracleInterface +} + +async function getOracleAddress(kit: ContractKit) { + if (!oracleAddress) { + const sortedOracles = await kit.contracts.getSortedOracles() + oracleAddress = sortedOracles.address + } + return oracleAddress +} + +async function getTokenAddress(kit: ContractKit, tokenId: NativeTokenId): Promise { + const cachedAddress = tokenAddresses[tokenId] + if (cachedAddress) return cachedAddress + + const token = nativeTokenToKitToken(tokenId) + const address = await kit.celoTokens.getAddress(token) + tokenAddresses[tokenId] = address + return address } diff --git a/src/features/chart/tokenPriceSlice.ts b/src/features/chart/tokenPriceSlice.ts index a6c6456..40e0fb9 100644 --- a/src/features/chart/tokenPriceSlice.ts +++ b/src/features/chart/tokenPriceSlice.ts @@ -1,6 +1,7 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { createSlice } from '@reduxjs/toolkit' import { getLocalStore } from 'next-persist' -import { BaseCurrencyPriceHistory, PairPriceUpdate } from 'src/features/chart/types' +import { fetchTokenPrice } from 'src/features/chart/fetchPrices' +import { BaseCurrencyPriceHistory } from 'src/features/chart/types' interface TokenPrices { // Base currency to quote currency to price list @@ -17,18 +18,22 @@ const tokenPriceSlice = createSlice({ name: 'tokenPrice', initialState: persistedState, reducers: { - updatePairPrices: (state, action: PayloadAction) => { - for (const ppu of action.payload) { + resetTokenPrices: () => initialState, + }, + extraReducers: (builder) => { + builder.addCase(fetchTokenPrice.fulfilled, (state, action) => { + const rates = action.payload + if (!rates) return + for (const ppu of rates) { const { baseCurrency, quoteCurrency, prices } = ppu state.prices[baseCurrency] = { ...state.prices[baseCurrency], [quoteCurrency]: prices, } } - }, - resetTokenPrices: () => initialState, + }) }, }) -export const { updatePairPrices, resetTokenPrices } = tokenPriceSlice.actions +export const { resetTokenPrices } = tokenPriceSlice.actions export const tokenPriceReducer = tokenPriceSlice.reducer diff --git a/src/features/swap/SettingsMenu.tsx b/src/features/swap/SettingsMenu.tsx index ede19c0..7c2cfe5 100644 --- a/src/features/swap/SettingsMenu.tsx +++ b/src/features/swap/SettingsMenu.tsx @@ -2,17 +2,23 @@ import useDropdownMenu from 'react-accessible-dropdown-menu-hook' import { useAppDispatch, useAppSelector } from 'src/app/hooks' import { IconButton } from 'src/components/buttons/IconButton' import { SwitchButton } from 'src/components/buttons/SwitchButton' -import { setShowSlippage } from 'src/features/swap/swapSlice' +import { setShowChart, setShowSlippage } from 'src/features/swap/swapSlice' import Sliders from 'src/images/icons/sliders.svg' export function SettingsMenu() { - const showSlippage = useAppSelector((s) => s.swap.showSlippage) + const { showSlippage, showChart } = useAppSelector((s) => s.swap) + const dispatch = useAppDispatch() + const onToggleSlippage = (checked: boolean) => { dispatch(setShowSlippage(checked)) } - const { buttonProps, itemProps, isOpen } = useDropdownMenu(1) + const onToggleChart = (checked: boolean) => { + dispatch(setShowChart(checked)) + } + + const { buttonProps, itemProps, isOpen } = useDropdownMenu(2) return (
@@ -31,6 +37,10 @@ export function SettingsMenu() {
Toggle Slippage
+ +
Toggle Chart
+ +
) diff --git a/src/features/swap/swapSlice.ts b/src/features/swap/swapSlice.ts index 3561021..6d0facd 100644 --- a/src/features/swap/swapSlice.ts +++ b/src/features/swap/swapSlice.ts @@ -6,12 +6,14 @@ export interface SwapState { formValues: SwapFormValues | null toCeloRates: ToCeloRates showSlippage: boolean + showChart: boolean } const initialState: SwapState = { formValues: null, toCeloRates: {}, showSlippage: false, + showChart: false, } export const swapSlice = createSlice({ @@ -24,6 +26,9 @@ export const swapSlice = createSlice({ setShowSlippage: (state, action: PayloadAction) => { state.showSlippage = action.payload }, + setShowChart: (state, action: PayloadAction) => { + state.showChart = action.payload + }, reset: () => initialState, }, extraReducers: (builder) => { @@ -35,5 +40,5 @@ export const swapSlice = createSlice({ }, }) -export const { setFormValues, setShowSlippage, reset } = swapSlice.actions +export const { setFormValues, setShowSlippage, setShowChart, reset } = swapSlice.actions export const swapReducer = swapSlice.reducer diff --git a/src/images/icons/clipboard-plus.svg b/src/images/icons/clipboard-plus.svg new file mode 100644 index 0000000..8a79efc --- /dev/null +++ b/src/images/icons/clipboard-plus.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/images/icons/cube.svg b/src/images/icons/cube.svg new file mode 100644 index 0000000..577ebf0 --- /dev/null +++ b/src/images/icons/cube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 35a3342..9cbd853 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -4,20 +4,16 @@ import { NativeTokenId } from 'src/config/tokens' import { PriceChartCelo } from 'src/features/chart/PriceChartCelo' import { SwapConfirm } from 'src/features/swap/SwapConfirm' import { SwapForm } from 'src/features/swap/SwapForm' -import { useIsMobile } from 'src/styles/mediaQueries' export default function SwapPage() { - const { formValues } = useAppSelector((state) => state.swap) - const isMobile = useIsMobile() + const { formValues, showChart } = useAppSelector((state) => state.swap) return ( -
+
{!formValues ? : }
- {!isMobile && config.showPriceChart && ( -
- -
+ {config.showPriceChart && showChart && ( + )}
) diff --git a/src/utils/amount.ts b/src/utils/amount.ts index 425fd8a..f726d7f 100644 --- a/src/utils/amount.ts +++ b/src/utils/amount.ts @@ -86,3 +86,14 @@ export function getAdjustedAmount( return amountInWei } } + +export const fixed1 = new BigNumber('1000000000000000000000000') + +export const toFixidity = (n: BigNumber.Value) => { + return fixed1.times(n).integerValue(BigNumber.ROUND_FLOOR) +} + +// Keeps the decimal portion +export const fromFixidity = (f: BigNumber.Value) => { + return new BigNumber(f).div(fixed1) +} diff --git a/src/utils/blockscout.ts b/src/utils/blockscout.ts index 0bb9d9b..664b50b 100644 --- a/src/utils/blockscout.ts +++ b/src/utils/blockscout.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js' +import { retryAsync } from 'src/utils/retry' import { fetchWithTimeout } from 'src/utils/timeout' interface BlockscoutResponse { @@ -8,6 +9,11 @@ interface BlockscoutResponse { } export async function queryBlockscout

(url: string) { + const result = await retryAsync(() => executeQuery

(url)) + return result +} + +async function executeQuery

(url: string) { const response = await fetchWithTimeout(url) if (!response.ok) { throw new Error(`Fetch response not okay: ${response.status}`) diff --git a/src/utils/clipboard.ts b/src/utils/clipboard.ts new file mode 100644 index 0000000..3d40880 --- /dev/null +++ b/src/utils/clipboard.ts @@ -0,0 +1,24 @@ +import { logger } from 'src/utils/logger' + +export function isClipboardReadSupported() { + return !!navigator?.clipboard?.readText +} + +export async function tryClipboardSet(value: string) { + try { + await navigator.clipboard.writeText(value) + } catch (error) { + logger.error('Failed to set clipboard', error) + } +} + +export async function tryClipboardGet() { + try { + // Note: doesn't work in firefox, which only allows extensions to read clipboard + const value = await navigator.clipboard.readText() + return value + } catch (error) { + logger.error('Failed to read from clipboard', error) + return null + } +} diff --git a/src/utils/retry.ts b/src/utils/retry.ts new file mode 100644 index 0000000..1283941 --- /dev/null +++ b/src/utils/retry.ts @@ -0,0 +1,20 @@ +import { logger } from 'src/utils/logger' +import { sleep } from 'src/utils/timeout' + +// If all the tries fail it raises the last thrown exception +export async function retryAsync(runner: () => T, attempts = 3, delay = 500) { + let saveError + for (let i = 0; i < attempts; i++) { + try { + const result = await runner() + if (result) return result + else throw new Error('Empty result') + } catch (error) { + await sleep(delay * (i + 1)) + saveError = error + logger.error(`retryAsync: Failed to execute function on attempt #${i}:`, error) + } + } + logger.error(`retryAsync: All attempts failed`) + throw saveError +} diff --git a/yarn.lock b/yarn.lock index 7d71a26..f9012e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -639,6 +639,21 @@ "@ethersproject/properties" "^5.0.3" "@ethersproject/strings" "^5.0.4" +"@ethersproject/abi@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613" + integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/hash" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/abstract-provider@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.4.0.tgz#415331031b0f678388971e1987305244edc04e1d" @@ -652,6 +667,19 @@ "@ethersproject/transactions" "^5.4.0" "@ethersproject/web" "^5.4.0" +"@ethersproject/abstract-provider@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5" + integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/networks" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/transactions" "^5.5.0" + "@ethersproject/web" "^5.5.0" + "@ethersproject/abstract-signer@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.4.0.tgz#cd5f50b93141ee9f9f49feb4075a0b3eafb57d65" @@ -663,6 +691,17 @@ "@ethersproject/logger" "^5.4.0" "@ethersproject/properties" "^5.4.0" +"@ethersproject/abstract-signer@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d" + integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA== + dependencies: + "@ethersproject/abstract-provider" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/address@^5.0.4", "@ethersproject/address@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.4.0.tgz#ba2d00a0f8c4c0854933b963b9a3a9f6eb4a37a3" @@ -692,6 +731,13 @@ dependencies: "@ethersproject/bytes" "^5.4.0" +"@ethersproject/base64@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090" + integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/basex@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.4.0.tgz#0a2da0f4e76c504a94f2b21d3161ed9438c7f8a6" @@ -748,6 +794,13 @@ dependencies: "@ethersproject/bignumber" "^5.4.0" +"@ethersproject/constants@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e" + integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ== + dependencies: + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/hash@^5.0.4", "@ethersproject/hash@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.4.0.tgz#d18a8e927e828e22860a011f39e429d388344ae0" @@ -762,6 +815,20 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/hash@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9" + integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg== + dependencies: + "@ethersproject/abstract-signer" "^5.5.0" + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@ethersproject/keccak256@^5.0.3", "@ethersproject/keccak256@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.4.0.tgz#7143b8eea4976080241d2bd92e3b1f1bf7025318" @@ -800,6 +867,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/networks@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.1.tgz#b7f7b9fb88dec1ea48f739b7fb9621311aa8ce6c" + integrity sha512-tYRDM4zZtSUcKnD4UMuAlj7SeXH/k5WC4SP2u1Pn57++JdXHkRu2zwNkgNogZoxHzhm9Q6qqurDBVptHOsW49Q== + dependencies: + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties@^5.0.3": version "5.4.1" resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.4.1.tgz#9f051f976ce790142c6261ccb7b826eaae1f2f36" @@ -814,6 +888,13 @@ dependencies: "@ethersproject/logger" "^5.4.0" +"@ethersproject/properties@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995" + integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA== + dependencies: + "@ethersproject/logger" "^5.5.0" + "@ethersproject/providers@^5.3.0": version "5.4.5" resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.4.5.tgz#eb2ea2a743a8115f79604a8157233a3a2c832928" @@ -884,6 +965,18 @@ elliptic "6.5.4" hash.js "1.1.7" +"@ethersproject/signing-key@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0" + integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + bn.js "^4.11.9" + elliptic "6.5.4" + hash.js "1.1.7" + "@ethersproject/strings@^5.0.4", "@ethersproject/strings@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.4.0.tgz#fb12270132dd84b02906a8d895ae7e7fa3d07d9a" @@ -893,6 +986,15 @@ "@ethersproject/constants" "^5.4.0" "@ethersproject/logger" "^5.4.0" +"@ethersproject/strings@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549" + integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ== + dependencies: + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/transactions@^5.0.0-beta.135", "@ethersproject/transactions@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.4.0.tgz#a159d035179334bd92f340ce0f77e83e9e1522e0" @@ -908,6 +1010,21 @@ "@ethersproject/rlp" "^5.4.0" "@ethersproject/signing-key" "^5.4.0" +"@ethersproject/transactions@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908" + integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA== + dependencies: + "@ethersproject/address" "^5.5.0" + "@ethersproject/bignumber" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/constants" "^5.5.0" + "@ethersproject/keccak256" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/rlp" "^5.5.0" + "@ethersproject/signing-key" "^5.5.0" + "@ethersproject/web@^5.4.0": version "5.4.0" resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.4.0.tgz#49fac173b96992334ed36a175538ba07a7413d1f" @@ -919,6 +1036,17 @@ "@ethersproject/properties" "^5.4.0" "@ethersproject/strings" "^5.4.0" +"@ethersproject/web@^5.5.0": + version "5.5.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316" + integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg== + dependencies: + "@ethersproject/base64" "^5.5.0" + "@ethersproject/bytes" "^5.5.0" + "@ethersproject/logger" "^5.5.0" + "@ethersproject/properties" "^5.5.0" + "@ethersproject/strings" "^5.5.0" + "@hapi/accept@5.0.2": version "5.0.2" resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.2.tgz#ab7043b037e68b722f93f376afb05e85c0699523"