diff --git a/package.json b/package.json index 9a2e7586a9d..c71e89981f9 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "@react-navigation/material-top-tabs": "6.6.2", "@react-navigation/native": "6.1.6", "@react-navigation/stack": "6.3.16", - "@reservoir0x/reservoir-sdk": "1.2.1", + "@reservoir0x/reservoir-sdk": "1.4.5", "@segment/analytics-react-native": "2.15.0", "@segment/sovran-react-native": "1.0.4", "@sentry/react-native": "3.4.1", diff --git a/src/App.js b/src/App.js index b92fb27a3e7..2038bfe4a0d 100644 --- a/src/App.js +++ b/src/App.js @@ -85,7 +85,7 @@ import { migrate } from '@/migrations'; import { initListeners as initWalletConnectListeners } from '@/walletConnect'; import { saveFCMToken } from '@/notifications/tokens'; import branch from 'react-native-branch'; -import { initializeReservoirClient } from '@/resources/nftOffers/utils'; +import { initializeReservoirClient } from '@/resources/reservoir/client'; if (__DEV__) { reactNativeDisableYellowBox && LogBox.ignoreAllLogs(); diff --git a/src/analytics/event.ts b/src/analytics/event.ts index c187be4dc56..5af645fe65a 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -1,6 +1,7 @@ import { CardType } from '@/components/cards/GenericCard'; import { LearnCategory } from '@/components/cards/utils/types'; import { FiatProviderName } from '@/entities/f2c'; +import { Network } from '@/networks/types'; /** * All events, used by `analytics.track()` @@ -81,6 +82,7 @@ export const event = { poapsOpenedMintSheet: 'Opened POAP mint sheet', poapsMintedPoap: 'Minted POAP', poapsViewedOnPoap: 'Viewed POAP on poap.gallery', + positionsOpenedSheet: 'Opened position Sheet', positionsOpenedExternalDapp: 'Viewed external dapp', @@ -89,6 +91,12 @@ export const event = { mintsPressedMintButton: 'Pressed mint button in mints sheet', mintsPressedViewAllMintsButton: 'Pressed view all mints button in mints card', mintsChangedFilter: 'Changed mints filter', + + mintsOpenedSheet: 'Opened NFT Mint Sheet', + mintsOpeningMintDotFun: 'Opening Mintdotfun', + mintsMintingNFT: 'Minting NFT', + mintsMintedNFT: 'Minted NFT', + mintsErrorMintingNFT: 'Error Minting NFT', } as const; /** @@ -292,6 +300,37 @@ export type EventProperties = { rainbowFee: number; offerCurrency: { symbol: string; contractAddress: string }; }; + [event.mintsMintingNFT]: { + contract: string; + chainId: number; + quantity: number; + collectionName: string; + priceInEth: string; + }; + [event.mintsMintedNFT]: { + contract: string; + chainId: number; + quantity: number; + collectionName: string; + priceInEth: string; + }; + [event.mintsErrorMintingNFT]: { + contract: string; + chainId: number; + quantity: number; + collectionName: string; + priceInEth: string; + }; + [event.mintsOpenedSheet]: { + contract: string; + chainId: number; + collectionName: string; + }; + [event.mintsOpeningMintDotFun]: { + contract: string; + chainId: number; + collectionName: string; + }; [event.poapsMintedPoap]: { eventId: number; type: 'qrHash' | 'secretWord'; diff --git a/src/components/cards/FeaturedMintCard.tsx b/src/components/cards/FeaturedMintCard.tsx index 02149cbca2e..402904ded56 100644 --- a/src/components/cards/FeaturedMintCard.tsx +++ b/src/components/cards/FeaturedMintCard.tsx @@ -13,7 +13,7 @@ import { useColorMode, useForegroundColor, } from '@/design-system'; -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { ButtonPressAnimation } from '../animations'; import { useMints } from '@/resources/mints'; import { useAccountProfile, useDimensions } from '@/hooks'; @@ -24,11 +24,13 @@ import { convertRawAmountToRoundedDecimal, } from '@/helpers/utilities'; import { BlurView } from '@react-native-community/blur'; -import { Linking, View } from 'react-native'; +import { View } from 'react-native'; import { IS_IOS } from '@/env'; import { Media } from '../Media'; import { analyticsV2 } from '@/analytics'; import * as i18n from '@/languages'; +import { navigateToMintCollection } from '@/resources/reservoir/mints'; +import { ethereumUtils } from '@/utils'; const IMAGE_SIZE = 111; @@ -71,6 +73,24 @@ export function FeaturedMintCard() { useEffect(() => setMediaRendered(false), [imageUrl]); + const handlePress = useCallback(() => { + if (featuredMint) { + analyticsV2.track(analyticsV2.event.mintsPressedFeaturedMintCard, { + contractAddress: featuredMint.contractAddress, + chainId: featuredMint.chainId, + totalMints: featuredMint.totalMints, + mintsLastHour: featuredMint.totalMints, + priceInEth: convertRawAmountToRoundedDecimal( + featuredMint.mintStatus.price, + 18, + 6 + ), + }); + const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId); + navigateToMintCollection(featuredMint.contract, network); + } + }, [featuredMint]); + return featuredMint ? ( @@ -108,23 +128,7 @@ export function FeaturedMintCard() { overflow: 'hidden', padding: 12, }} - onPress={() => { - analyticsV2.track( - analyticsV2.event.mintsPressedFeaturedMintCard, - { - contractAddress: featuredMint.contractAddress, - chainId: featuredMint.chainId, - totalMints: featuredMint.totalMints, - mintsLastHour: featuredMint.totalMints, - priceInEth: convertRawAmountToRoundedDecimal( - featuredMint.mintStatus.price, - 18, - 6 - ), - } - ); - Linking.openURL(featuredMint.externalURL); - }} + onPress={handlePress} scaleTo={0.96} > diff --git a/src/components/cards/MintsCard/CollectionCell.tsx b/src/components/cards/MintsCard/CollectionCell.tsx index c9eae64f150..2520166f976 100644 --- a/src/components/cards/MintsCard/CollectionCell.tsx +++ b/src/components/cards/MintsCard/CollectionCell.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { globalColors } from '@/design-system/color/palettes'; import { convertRawAmountToRoundedDecimal } from '@/helpers/utilities'; import { CoinIcon } from '@/components/coin-icon'; @@ -15,12 +15,13 @@ import { ButtonPressAnimation } from '@/components/animations'; import { useTheme } from '@/theme'; import { Linking, View } from 'react-native'; import { MintableCollection } from '@/graphql/__generated__/arc'; -import { getNetworkFromChainId } from '@/utils/ethereumUtils'; +import ethereumUtils, { getNetworkFromChainId } from '@/utils/ethereumUtils'; import { getNetworkObj } from '@/networks'; import { analyticsV2 } from '@/analytics'; import * as i18n from '@/languages'; import { IS_IOS } from '@/env'; import { ImgixImage } from '@/components/images'; +import { navigateToMintCollection } from '@/resources/reservoir/mints'; export const NFT_IMAGE_SIZE = 111; @@ -90,16 +91,25 @@ export function CollectionCell({ useEffect(() => setMediaRendered(false), [imageUrl]); + const handlePress = useCallback(() => { + analyticsV2.track(analyticsV2.event.mintsPressedCollectionCell, { + contractAddress: collection.contractAddress, + chainId: collection.chainId, + priceInEth: amount, + }); + + const network = ethereumUtils.getNetworkFromChainId(collection.chainId); + navigateToMintCollection(collection.contract, network); + }, [ + amount, + collection.chainId, + collection.contract, + collection.contractAddress, + ]); + return ( { - analyticsV2.track(analyticsV2.event.mintsPressedCollectionCell, { - contractAddress: collection.contractAddress, - chainId: collection.chainId, - priceInEth: amount, - }); - Linking.openURL(collection.externalURL); - }} + onPress={handlePress} style={{ width: NFT_IMAGE_SIZE }} > + {item.network !== Network.mainnet && ( + + )} ) : ( { ]); const { isDarkMode } = useTheme(); - const shadows = useMemo(() => buildShadows(color, size, isDarkMode, colors), [ - color, - size, - isDarkMode, - colors, - ]); + const shadows = useMemo( + () => buildShadows(color, size, props?.forceDarkMode || isDarkMode, colors), + [color, size, props?.forceDarkMode, isDarkMode, colors] + ); const backgroundColor = typeof color === 'number' diff --git a/src/components/expanded-state/CustomGasState.js b/src/components/expanded-state/CustomGasState.js index 3e227b74982..5a9f629e525 100644 --- a/src/components/expanded-state/CustomGasState.js +++ b/src/components/expanded-state/CustomGasState.js @@ -41,12 +41,17 @@ const FeesPanelTabswrapper = styled(Column)(margin.object(19, 0, 24, 0)); export default function CustomGasState({ asset }) { const { setParams } = useNavigation(); const { - params: { longFormHeight, speeds, openCustomOptions } = {}, + params: { longFormHeight, speeds, openCustomOptions, fallbackColor } = {}, } = useRoute(); const { colors } = useTheme(); const { height: deviceHeight } = useDimensions(); const keyboardHeight = useKeyboardHeight(); - const colorForAsset = useColorForAsset(asset || {}, null, false, true); + const colorForAsset = useColorForAsset( + asset || {}, + fallbackColor, + false, + true + ); const { selectedGasFee, currentBlockParams, txNetwork } = useGas(); const [canGoBack, setCanGoBack] = useState(true); const { tradeDetails } = useSelector(state => state.swap); diff --git a/src/components/gas/GasSpeedButton.js b/src/components/gas/GasSpeedButton.js index e1403b9efa0..4bff48b0017 100644 --- a/src/components/gas/GasSpeedButton.js +++ b/src/components/gas/GasSpeedButton.js @@ -137,6 +137,7 @@ const GasSpeedButton = ({ asset, currentNetwork, horizontalPadding = 19, + fallbackColor, marginBottom = 20, marginTop = 18, speeds = null, @@ -151,7 +152,12 @@ const GasSpeedButton = ({ const { colors } = useTheme(); const { navigate, goBack } = useNavigation(); const { nativeCurrencySymbol, nativeCurrency } = useAccountSettings(); - const rawColorForAsset = useColorForAsset(asset || {}, null, false, true); + const rawColorForAsset = useColorForAsset( + asset || {}, + fallbackColor, + false, + true + ); const [isLongWait, setIsLongWait] = useState(false); const { inputCurrency, outputCurrency } = useSwapCurrencies(); @@ -229,6 +235,7 @@ const GasSpeedButton = ({ if (gasIsNotReady) return; navigate(Routes.CUSTOM_GAS_SHEET, { asset, + fallbackColor, flashbotTransaction, focusTo: shouldOpenCustomGasSheet.focusTo, openCustomOptions: focusTo => openCustomOptionsRef.current(focusTo), diff --git a/src/entities/transactions/transactionStatus.ts b/src/entities/transactions/transactionStatus.ts index f21b112bd72..0449bf04cfa 100644 --- a/src/entities/transactions/transactionStatus.ts +++ b/src/entities/transactions/transactionStatus.ts @@ -10,6 +10,8 @@ export enum TransactionStatus { depositing = 'depositing', dropped = 'dropped', failed = 'failed', + minted = 'minted', + minting = 'minting', purchased = 'purchased', purchasing = 'purchasing', received = 'received', @@ -39,6 +41,8 @@ export default { depositing: 'depositing', dropped: 'dropped', failed: 'failed', + minted: 'minted', + minting: 'minting', purchased: 'purchased', purchasing: 'purchasing', received: 'received', diff --git a/src/entities/transactions/transactionType.ts b/src/entities/transactions/transactionType.ts index 62ad3f83fea..b64f2275ad5 100644 --- a/src/entities/transactions/transactionType.ts +++ b/src/entities/transactions/transactionType.ts @@ -7,6 +7,7 @@ export enum TransactionType { deposit = 'deposit', dropped = 'dropped', execution = 'execution', + mint = 'mint', purchase = 'purchase', // Rainbow-specific type receive = 'receive', repay = 'repay', @@ -25,6 +26,7 @@ export default { deposit: 'deposit', dropped: 'dropped', execution: 'execution', + mint: 'mint', purchase: 'purchase', receive: 'receive', repay: 'repay', diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql index 62f542bc5ea..c05e9439b19 100644 --- a/src/graphql/queries/arc.graphql +++ b/src/graphql/queries/arc.graphql @@ -101,6 +101,51 @@ query claimPoapBySecretWord($walletAddress: String!, $secretWord: String!) { } } +query getReservoirCollection($contractAddress: String!, $chainId: Int!) { + getReservoirCollection(contractAddress: $contractAddress, chainId: $chainId) { + collection { + id + chainId + createdAt + name + image + description + sampleImages + tokenCount + creator + ownerCount + isMintingPublicSale + publicMintInfo { + stage + kind + price { + currency { + contract + name + symbol + decimals + } + amount { + raw + decimal + usd + native + } + netAmount { + raw + decimal + usd + native + } + } + startTime + endTime + maxMintsPerWallet + } + } + } +} + fragment mintStatus on MintStatus { isMintable price diff --git a/src/handlers/transactions.ts b/src/handlers/transactions.ts index 2d19dc80ca9..10507372fe4 100644 --- a/src/handlers/transactions.ts +++ b/src/handlers/transactions.ts @@ -114,6 +114,8 @@ const getConfirmedState = (type?: TransactionType): TransactionStatus => { return TransactionStatus.approved; case TransactionTypes.sell: return TransactionStatus.sold; + case TransactionTypes.mint: + return TransactionStatus.minted; case TransactionTypes.deposit: return TransactionStatus.deposited; case TransactionTypes.withdraw: diff --git a/src/helpers/transactions.ts b/src/helpers/transactions.ts index c38b59ce7f1..dfdbd5eac2a 100644 --- a/src/helpers/transactions.ts +++ b/src/helpers/transactions.ts @@ -99,6 +99,8 @@ export const getConfirmedState = ( return TransactionStatus.purchased; case TransactionTypes.sell: return TransactionStatus.sold; + case TransactionTypes.mint: + return TransactionStatus.minted; default: return TransactionStatus.sent; } diff --git a/src/hooks/useGas.ts b/src/hooks/useGas.ts index f3e3c200fd9..4e38fe7ffa0 100644 --- a/src/hooks/useGas.ts +++ b/src/hooks/useGas.ts @@ -186,6 +186,18 @@ export default function useGas({ [dispatch] ); + const getTotalGasPrice = useCallback(() => { + const txFee = gasData?.selectedGasFee?.gasFee; + const isLegacyGasNetwork = + getNetworkObj(gasData?.txNetwork).gas.gasType === 'legacy'; + const txFeeValue = isLegacyGasNetwork + ? (txFee as LegacyGasFee)?.estimatedFee + : (txFee as GasFee)?.maxFee; + + const txFeeAmount = fromWei(txFeeValue?.value?.amount); + return txFeeAmount; + }, [gasData?.selectedGasFee?.gasFee, gasData?.txNetwork]); + return { isGasReady, isSufficientGas, @@ -197,6 +209,7 @@ export default function useGas({ updateGasFeeOption, updateToCustomGasFee, updateTxFee, + getTotalGasPrice, ...gasData, }; } diff --git a/src/languages/en_US.json b/src/languages/en_US.json index de016e00a69..a6e75041b59 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1155,6 +1155,30 @@ "withdrawal_dropdown_label": "For", "withdrawal_input_label": "Get" }, + "minting": { + "hold_to_mint": "Hold To Mint", + "minting": "Minting...", + "minted": "Minted", + "mint_unavailable": "Mint Unavailable", + "mint_on_mintdotfun": "􀮶 Mint on Mint.fun", + "error_minting": "Error Minting", + "mint_price": "Mint Price", + "free": "Free", + "by": "By", + "unknown": "unknown", + "max": "Max", + "nft_count": "%{number} NFTs", + "description": "Description", + "total_minted": "Total Minted", + "first_event": "First Event", + "last_event": "Last Event", + "max_supply": "Max Supply", + "contract": "Contract", + "network": "Network", + "could_not_find_collection": "Could not find collection", + "unable_to_find_check_again": "We are unable to find this collection, double check the address and network or try again later", + "mintdotfun_unsupported_network": "Mint.fun does not support this network" + }, "nfts": { "selling": "Selling" }, @@ -1700,6 +1724,8 @@ "failed": "Failed", "purchased": "Purchased", "purchasing": "Purchasing", + "minting": "Minting", + "minted": "Minted", "received": "Received", "receiving": "Receiving", "self": "Self", diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index ccf3e1af69b..c7e79849409 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -81,6 +81,7 @@ import { NFTSingleOfferSheet } from '@/screens/NFTSingleOfferSheet'; import ShowSecretView from '@/screens/SettingsSheet/components/ShowSecretView'; import PoapSheet from '@/screens/mints/PoapSheet'; import { PositionSheet } from '@/screens/positions/PositionSheet'; +import MintSheet from '@/screens/mints/MintSheet'; import { MintsSheet } from '@/screens/MintsSheet/MintsSheet'; const Stack = createStackNavigator(); @@ -244,6 +245,7 @@ function BSNavigator() { name={Routes.EXPANDED_ASSET_SHEET} /> + + { + Alert.alert( + lang.t(lang.l.minting.could_not_find_collection), + lang.t(lang.l.minting.unable_to_find_check_again), + [{ text: lang.t(lang.l.button.ok) }], + { cancelable: false } + ); +}; +export const navigateToMintCollection = async ( + contractAddress: EthereumAddress, + network: Network +) => { + logger.debug('Mints: Navigating to Mint Collection', { + contractAddress, + network, + }); + try { + const chainId = getNetworkObj(network).id; + const res = await client.getReservoirCollection({ + contractAddress, + chainId, + }); + if (res?.getReservoirCollection?.collection) { + Navigation.handleAction(Routes.MINT_SHEET, { + collection: res.getReservoirCollection?.collection, + }); + } else { + logger.warn('Mints: No collection found', { contractAddress, network }); + showAlert(); + } + } catch (e) { + logger.warn('Mints: navigateToMintCollection error', { + contractAddress, + network, + error: e, + }); + showAlert(); + } +}; diff --git a/src/resources/nftOffers/index.ts b/src/resources/reservoir/nftOffersQuery.ts similarity index 100% rename from src/resources/nftOffers/index.ts rename to src/resources/reservoir/nftOffersQuery.ts diff --git a/src/resources/reservoir/utils.ts b/src/resources/reservoir/utils.ts new file mode 100644 index 00000000000..9003a8279d8 --- /dev/null +++ b/src/resources/reservoir/utils.ts @@ -0,0 +1,34 @@ +import { Network } from '@/networks/types'; + +const RAINBOW_FEE_ADDRESS_MAINNET = + '0x69d6d375de8c7ade7e44446df97f49e661fdad7d'; +const RAINBOW_FEE_ADDRESS_POLYGON = + '0xfb9af3db5e19c4165f413f53fe3bbe6226834548'; +const RAINBOW_FEE_ADDRESS_OPTIMISM = + '0x0d9b71891dc86400acc7ead08c80af301ccb3d71'; +const RAINBOW_FEE_ADDRESS_ARBITRUM = + '0x0f9259af03052c96afda88add62eb3b5cbc185f1'; +const RAINBOW_FEE_ADDRESS_BASE = '0x1bbe055ad3204fa4468b4e6d3a3c59b9d9ac8c19'; +const RAINBOW_FEE_ADDRESS_BSC = '0x9670271ec2e2937a2e9df536784344bbff2bbea6'; +const RAINBOW_FEE_ADDRESS_ZORA = '0x7a3d05c70581bd345fe117c06e45f9669205384f'; + +export function getRainbowFeeAddress(network: Network) { + switch (network) { + case Network.mainnet: + return RAINBOW_FEE_ADDRESS_MAINNET; + case Network.polygon: + return RAINBOW_FEE_ADDRESS_POLYGON; + case Network.optimism: + return RAINBOW_FEE_ADDRESS_OPTIMISM; + case Network.arbitrum: + return RAINBOW_FEE_ADDRESS_ARBITRUM; + case Network.base: + return RAINBOW_FEE_ADDRESS_BASE; + case Network.bsc: + return RAINBOW_FEE_ADDRESS_BSC; + case Network.zora: + return RAINBOW_FEE_ADDRESS_ZORA; + default: + return undefined; + } +} diff --git a/src/screens/MintsSheet/card/Card.tsx b/src/screens/MintsSheet/card/Card.tsx index 319f57f95ff..159df6c351b 100644 --- a/src/screens/MintsSheet/card/Card.tsx +++ b/src/screens/MintsSheet/card/Card.tsx @@ -26,6 +26,7 @@ import * as i18n from '@/languages'; import ChainBadge from '@/components/coin-icon/ChainBadge'; import { CoinIcon } from '@/components/coin-icon'; import { Network } from '@/helpers'; +import { navigateToMintCollection } from '@/resources/reservoir/mints'; export const NUM_NFTS = 3; @@ -129,7 +130,7 @@ export function Card({ collection }: { collection: MintableCollection }) { chainId: collection.chainId, priceInEth: price, }); - Linking.openURL(collection.externalURL); + navigateToMintCollection(collection.contract, network); }} style={{ borderRadius: 99, diff --git a/src/screens/NFTOffersSheet/index.tsx b/src/screens/NFTOffersSheet/index.tsx index c2adc2629d3..0615fe8f386 100644 --- a/src/screens/NFTOffersSheet/index.tsx +++ b/src/screens/NFTOffersSheet/index.tsx @@ -17,7 +17,10 @@ import { FakeOfferRow, OfferRow } from './OfferRow'; import { useAccountProfile, useDimensions } from '@/hooks'; import { ImgixImage } from '@/components/images'; import { ContactAvatar } from '@/components/contacts'; -import { nftOffersQueryKey, useNFTOffers } from '@/resources/nftOffers'; +import { + nftOffersQueryKey, + useNFTOffers, +} from '@/resources/reservoir/nftOffersQuery'; import { convertAmountToNativeDisplay } from '@/helpers/utilities'; import { SortMenu } from '@/components/nft-offers/SortMenu'; import * as i18n from '@/languages'; diff --git a/src/screens/NFTSingleOfferSheet/index.tsx b/src/screens/NFTSingleOfferSheet/index.tsx index 911123828f4..dc30430a888 100644 --- a/src/screens/NFTSingleOfferSheet/index.tsx +++ b/src/screens/NFTSingleOfferSheet/index.tsx @@ -49,46 +49,14 @@ import { Network } from '@/helpers'; import { getNetworkObj } from '@/networks'; import { CardSize } from '@/components/unique-token/CardSize'; import { queryClient } from '@/react-query'; -import { nftOffersQueryKey } from '@/resources/nftOffers'; +import { nftOffersQueryKey } from '@/resources/reservoir/nftOffersQuery'; +import { getRainbowFeeAddress } from '@/resources/reservoir/utils'; const NFT_IMAGE_HEIGHT = 160; const TWO_HOURS_MS = 2 * 60 * 60 * 1000; const RAINBOW_FEE_BIPS = 85; const BIPS_TO_DECIMAL_RATIO = 10000; -const RAINBOW_FEE_ADDRESS_MAINNET = - '0x69d6d375de8c7ade7e44446df97f49e661fdad7d'; -const RAINBOW_FEE_ADDRESS_POLYGON = - '0xfb9af3db5e19c4165f413f53fe3bbe6226834548'; -const RAINBOW_FEE_ADDRESS_OPTIMISM = - '0x0d9b71891dc86400acc7ead08c80af301ccb3d71'; -const RAINBOW_FEE_ADDRESS_ARBITRUM = - '0x0f9259af03052c96afda88add62eb3b5cbc185f1'; -const RAINBOW_FEE_ADDRESS_BASE = '0x1bbe055ad3204fa4468b4e6d3a3c59b9d9ac8c19'; -const RAINBOW_FEE_ADDRESS_BSC = '0x9670271ec2e2937a2e9df536784344bbff2bbea6'; -const RAINBOW_FEE_ADDRESS_ZORA = '0x7a3d05c70581bd345fe117c06e45f9669205384f'; - -function getRainbowFeeAddress(network: Network) { - switch (network) { - case Network.mainnet: - return RAINBOW_FEE_ADDRESS_MAINNET; - case Network.polygon: - return RAINBOW_FEE_ADDRESS_POLYGON; - case Network.optimism: - return RAINBOW_FEE_ADDRESS_OPTIMISM; - case Network.arbitrum: - return RAINBOW_FEE_ADDRESS_ARBITRUM; - case Network.base: - return RAINBOW_FEE_ADDRESS_BASE; - case Network.bsc: - return RAINBOW_FEE_ADDRESS_BSC; - case Network.zora: - return RAINBOW_FEE_ADDRESS_ZORA; - default: - return undefined; - } -} - function Row({ symbol, label, diff --git a/src/screens/discover/components/DiscoverSearch.js b/src/screens/discover/components/DiscoverSearch.js index 78649bbd42f..6c8bd913edb 100644 --- a/src/screens/discover/components/DiscoverSearch.js +++ b/src/screens/discover/components/DiscoverSearch.js @@ -22,6 +22,7 @@ import { analytics } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { fetchSuggestions } from '@/handlers/ens'; import { + useAccountSettings, useHardwareBackOnFocus, usePrevious, useSwapCurrencyList, @@ -36,6 +37,7 @@ import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord, } from '@/utils/poaps'; +import { navigateToMintCollection } from '@/resources/reservoir/mints'; export const SearchContainer = styled(Row)({ height: '100%', @@ -44,6 +46,7 @@ export const SearchContainer = styled(Row)({ export default function DiscoverSearch() { const { navigate } = useNavigation(); const dispatch = useDispatch(); + const { accountAddress } = useAccountSettings(); const { isSearching, isFetchingEns, @@ -144,6 +147,26 @@ export default function DiscoverSearch() { checkAndHandlePoaps(searchQueryForPoap); }, [searchQueryForPoap]); + useEffect(() => { + // probably dont need this entry point but seems worth keeping? + // could do the same with zora, etc + const checkAndHandleMint = async seachQueryForMint => { + if (seachQueryForMint.includes('mint.fun')) { + const mintdotfunURL = seachQueryForMint.split('https://mint.fun/'); + const query = mintdotfunURL[1]; + let network = query.split('/')[0]; + if (network === 'ethereum') { + network = Network.mainnet; + } else if (network === 'op') { + network === Network.optimism; + } + const contractAddress = query.split('/')[1]; + navigateToMintCollection(contractAddress, network); + } + }; + checkAndHandleMint(searchQuery); + }, [accountAddress, navigate, searchQuery]); + const handlePress = useCallback( item => { if (item.ens) { diff --git a/src/screens/mints/MintSheet.tsx b/src/screens/mints/MintSheet.tsx new file mode 100644 index 00000000000..1682a368b15 --- /dev/null +++ b/src/screens/mints/MintSheet.tsx @@ -0,0 +1,874 @@ +import { BlurView } from '@react-native-community/blur'; +import React, { + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react'; +import { Linking, View } from 'react-native'; +import { useSharedValue } from 'react-native-reanimated'; +import useWallets from '../../hooks/useWallets'; +import { GasSpeedButton } from '@/components/gas'; +import { Execute, getClient } from '@reservoir0x/reservoir-sdk'; +import { privateKeyToAccount } from 'viem/accounts'; +import { createWalletClient, http } from 'viem'; +import { dataAddNewTransaction } from '@/redux/data'; +import { HoldToAuthorizeButton } from '@/components/buttons'; +import Routes from '@/navigation/routesNames'; +import ImgixImage from '../../components/images/ImgixImage'; +import { SlackSheet } from '../../components/sheet'; +import { CardSize } from '../../components/unique-token/CardSize'; +import { WrappedAlert as Alert } from '@/helpers/alert'; +import { + Box, + ColorModeProvider, + Column, + Columns, + Inline, + Inset, + Separator, + Stack, + Text, +} from '@/design-system'; +import { + useAccountProfile, + useAccountSettings, + useDimensions, + useENSAvatar, + useGas, + usePersistentAspectRatio, +} from '@/hooks'; +import { useNavigation } from '@/navigation'; +import styled from '@/styled-thing'; +import { position } from '@/styles'; +import { useTheme } from '@/theme'; +import { CoinIcon, abbreviations, ethereumUtils, watchingAlert } from '@/utils'; +import { usePersistentDominantColorFromImage } from '@/hooks/usePersistentDominantColorFromImage'; +import { maybeSignUri } from '@/handlers/imgix'; +import { ButtonPressAnimation } from '@/components/animations'; +import { useFocusEffect, useRoute } from '@react-navigation/native'; +import { ReservoirCollection } from '@/graphql/__generated__/arcDev'; +import { format } from 'date-fns'; +import { TransactionStatus, TransactionType } from '@/entities'; +import * as i18n from '@/languages'; +import { analyticsV2 } from '@/analytics'; +import { event } from '@/analytics/event'; +import { ETH_ADDRESS, ETH_SYMBOL } from '@/references'; +import { RainbowNetworks, getNetworkObj } from '@/networks'; +import { Network } from '@/networks/types'; +import { fetchReverseRecord } from '@/handlers/ens'; +import { ContactAvatar } from '@/components/contacts'; +import { addressHashedColorIndex } from '@/utils/profileUtils'; +import { loadPrivateKey } from '@/model/wallet'; +import { ChainBadge } from '@/components/coin-icon'; +import { + add, + convertAmountToBalanceDisplay, + convertAmountToNativeDisplay, + convertRawAmountToBalance, + greaterThanOrEqualTo, + isZero, + multiply, +} from '@/helpers/utilities'; +import { RainbowError, logger } from '@/logger'; +import { useDispatch } from 'react-redux'; +import { QuantityButton } from './components/QuantityButton'; +import { estimateGas, getProviderForNetwork } from '@/handlers/web3'; +import { getRainbowFeeAddress } from '@/resources/reservoir/utils'; + +const NFT_IMAGE_HEIGHT = 250; +// inset * 2 -> 28 *2 +const INSET_OFFSET = 56; + +const BackgroundBlur = styled(BlurView).attrs({ + blurAmount: 100, + blurType: 'light', +})({ + ...position.coverAsObject, +}); + +const BackgroundImage = styled(View)({ + ...position.coverAsObject, +}); + +interface BlurWrapperProps { + height: number; + width: number; +} + +const BlurWrapper = styled(View).attrs({ + shouldRasterizeIOS: true, +})({ + // @ts-expect-error missing theme types + backgroundColor: ({ theme: { colors } }) => colors.trueBlack, + height: ({ height }: BlurWrapperProps) => height, + left: 0, + overflow: 'hidden', + position: 'absolute', + width: ({ width }: BlurWrapperProps) => width, + ...(android ? { borderTopLeftRadius: 30, borderTopRightRadius: 30 } : {}), +}); + +interface MintSheetProps { + collection: ReservoirCollection; + chainId: number; +} + +function MintInfoRow({ + symbol, + label, + value, +}: { + symbol: string; + label: string; + value: React.ReactNode; +}) { + return ( + + + + + + + {symbol} + + + + + {label} + + + + {value} + + + ); +} + +const getFormattedDate = (date: string) => { + return format(new Date(date), 'MMMM dd, yyyy'); +}; + +const MintSheet = () => { + const params = useRoute(); + const { collection: mintCollection } = params.params as MintSheetProps; + const { accountAddress } = useAccountProfile(); + const { nativeCurrency } = useAccountSettings(); + const { height: deviceHeight, width: deviceWidth } = useDimensions(); + const { navigate } = useNavigation(); + const dispatch = useDispatch(); + const { colors, isDarkMode } = useTheme(); + const { isReadOnlyWallet, isHardwareWallet } = useWallets(); + const [insufficientEth, setInsufficientEth] = useState(false); + const [showNativePrice, setShowNativePrice] = useState(false); + const [gasError, setGasError] = useState(false); + const currentNetwork = + RainbowNetworks.find(({ id }) => id === mintCollection.chainId)?.value || + Network.mainnet; + const [ensName, setENSName] = useState(''); + const [mintStatus, setMintStatus] = useState< + 'none' | 'minting' | 'minted' | 'error' + >('none'); + const txRef = useRef(); + + const { data: ensAvatar } = useENSAvatar(ensName, { + enabled: Boolean(ensName), + }); + + const [quantity, setQuantity] = useReducer( + (quantity: number, increment: number) => { + if (quantity === 1 && increment === -1) { + return quantity; + } + if ( + maxMintsPerWallet && + quantity === maxMintsPerWallet && + increment === 1 + ) { + return quantity; + } + return quantity + increment; + }, + 1 + ); + + // if there is no max mint info, we fallback to 1 to be safe + const maxMintsPerWallet = Number( + mintCollection.publicMintInfo?.maxMintsPerWallet + ); + + const price = convertRawAmountToBalance( + mintCollection.publicMintInfo?.price?.amount?.raw || '0', + { + decimals: mintCollection.publicMintInfo?.price?.currency?.decimals || 18, + symbol: mintCollection.publicMintInfo?.price?.currency?.symbol || 'ETH', + } + ); + + // case where mint isnt eth? prob not with our current entrypoints + const mintPriceAmount = multiply(price.amount, quantity); + const mintPriceDisplay = convertAmountToBalanceDisplay( + multiply(price.amount, quantity), + { + decimals: mintCollection.publicMintInfo?.price?.currency?.decimals || 18, + symbol: mintCollection.publicMintInfo?.price?.currency?.symbol || 'ETH', + } + ); + + const priceOfEth = ethereumUtils.getEthPriceUnit() as number; + + const nativeMintPriceDisplay = convertAmountToNativeDisplay( + parseFloat(multiply(price.amount, quantity)) * priceOfEth, + nativeCurrency + ); + + const { + updateTxFee, + startPollingGasFees, + stopPollingGasFees, + isSufficientGas, + isValidGas, + getTotalGasPrice, + } = useGas(); + + const imageUrl = maybeSignUri(mintCollection.image || ''); + const { result: aspectRatio } = usePersistentAspectRatio(imageUrl || ''); + + // isMintingPublicSale handles if theres a time based mint, otherwise if there is a price we should be able to mint + const isMintingAvailable = + !(isReadOnlyWallet || isHardwareWallet) && + (mintCollection.isMintingPublicSale || price) && + !gasError; + + const imageColor = + usePersistentDominantColorFromImage(imageUrl) ?? colors.paleBlue; + + const sheetRef = useRef(); + const yPosition = useSharedValue(0); + + useFocusEffect(() => { + if (mintCollection.name && mintCollection.id) { + analyticsV2.track(event.mintsOpenedSheet, { + collectionName: mintCollection?.name, + contract: mintCollection.id, + chainId: mintCollection.chainId, + }); + } + }); + + // check address balance + useEffect(() => { + const checkInsufficientEth = async () => { + const nativeBalance = + ( + await ethereumUtils.getNativeAssetForNetwork( + currentNetwork, + accountAddress + ) + )?.balance?.amount ?? 0; + const txFee = getTotalGasPrice(); + const totalMintPrice = multiply(price.amount, quantity); + // gas price + mint price + setInsufficientEth( + greaterThanOrEqualTo(add(txFee, totalMintPrice), nativeBalance) + ); + }; + checkInsufficientEth(); + }, [ + accountAddress, + currentNetwork, + getTotalGasPrice, + mintCollection.publicMintInfo?.price?.currency?.decimals, + mintCollection.publicMintInfo?.price?.currency?.symbol, + price, + quantity, + ]); + + // resolve ens name + useEffect(() => { + const fetchENSName = async (address: string) => { + const ensName = await fetchReverseRecord(address); + setENSName(ensName); + }; + + if (mintCollection.creator) { + fetchENSName(mintCollection.creator); + } + }, [mintCollection.creator]); + + // start poll gas price + useEffect(() => { + startPollingGasFees(currentNetwork); + + return () => { + stopPollingGasFees(); + }; + }, [currentNetwork, startPollingGasFees, stopPollingGasFees]); + + // estimate gas limit + useEffect(() => { + const estimateMintGas = async () => { + const networkObj = getNetworkObj(currentNetwork); + const provider = await getProviderForNetwork(currentNetwork); + const signer = createWalletClient({ + account: accountAddress, + chain: networkObj, + transport: http(networkObj.rpc), + }); + try { + await getClient()?.actions.buyToken({ + items: [ + { fillType: 'mint', collection: mintCollection.id!, quantity }, + ], + wallet: signer!, + chainId: networkObj.id, + precheck: true, + onProgress: async (steps: Execute['steps']) => { + steps.forEach(step => { + if (step.error) { + logger.error( + new RainbowError(`NFT Mints: Gas Step Error: ${step.error}`) + ); + return; + } + step.items?.forEach(async item => { + // could add safety here if unable to calc gas limit + const tx = { + to: item.data?.to, + from: item.data?.from, + data: item.data?.data, + value: multiply(price.amount || '0', quantity), + }; + + const gas = await estimateGas(tx, provider); + if (gas) { + setGasError(false); + updateTxFee(gas, null); + } + }); + }); + }, + }); + } catch (e) { + setGasError(true); + logger.error( + new RainbowError(`NFT Mints: Gas Step Error: ${(e as Error).message}`) + ); + } + }; + estimateMintGas(); + }, [ + accountAddress, + currentNetwork, + mintCollection.id, + price, + quantity, + updateTxFee, + ]); + + const deployerDisplay = abbreviations.address( + mintCollection.creator || '', + 4, + 6 + ); + + const contractAddressDisplay = `${abbreviations.address( + mintCollection.id || '', + 4, + 6 + )} 􀄯`; + + const buildMintDotFunUrl = (contract: string, network: Network) => { + const MintDotFunNetworks = [ + Network.mainnet, + Network.optimism, + Network.base, + Network.zora, + ]; + if (!MintDotFunNetworks.includes(network)) { + Alert.alert(i18n.t(i18n.l.minting.mintdotfun_unsupported_network)); + } + + let chainSlug = 'ethereum'; + switch (network) { + case Network.optimism: + chainSlug = 'op'; + break; + case Network.base: + chainSlug = 'base'; + break; + case Network.zora: + chainSlug = 'zora'; + break; + } + return `https://mint.fun/${chainSlug}/${contract}`; + }; + + const actionOnPress = useCallback(async () => { + if (isReadOnlyWallet) { + watchingAlert(); + return; + } + + // link to mint.fun if reservoir not supporting + if (!isMintingAvailable) { + analyticsV2.track(event.mintsOpeningMintDotFun, { + collectionName: mintCollection.name || '', + contract: mintCollection.id || '', + chainId: mintCollection.chainId, + }); + Linking.openURL(buildMintDotFunUrl(mintCollection.id!, currentNetwork)); + return; + } + + logger.info('Minting NFT', { name: mintCollection.name }); + analyticsV2.track(event.mintsMintingNFT, { + collectionName: mintCollection.name || '', + contract: mintCollection.id || '', + chainId: mintCollection.chainId, + quantity, + priceInEth: mintPriceAmount, + }); + setMintStatus('minting'); + + const privateKey = await loadPrivateKey(accountAddress, false); + // @ts-ignore + const account = privateKeyToAccount(privateKey); + const networkObj = getNetworkObj(currentNetwork); + const signer = createWalletClient({ + account, + chain: networkObj, + transport: http(networkObj.rpc), + }); + + const feeAddress = getRainbowFeeAddress(currentNetwork); + try { + await getClient()?.actions.buyToken({ + items: [ + { + fillType: 'mint', + collection: mintCollection.id!, + quantity, + ...(feeAddress && { referrer: feeAddress }), + }, + ], + wallet: signer!, + chainId: networkObj.id, + onProgress: (steps: Execute['steps']) => { + steps.forEach(step => { + if (step.error) { + logger.error( + new RainbowError(`Error minting NFT: ${step.error}`) + ); + setMintStatus('error'); + return; + } + step.items?.forEach(item => { + if ( + item.txHash && + txRef.current !== item.txHash && + item.status === 'incomplete' + ) { + const tx = { + to: item.data?.to, + from: item.data?.from, + hash: item.txHash, + network: currentNetwork, + amount: mintPriceAmount, + asset: { + address: ETH_ADDRESS, + symbol: ETH_SYMBOL, + }, + nft: { + predominantColor: imageColor, + collection: { + image: imageUrl, + }, + lowResUrl: imageUrl, + name: mintCollection.name, + }, + type: TransactionType.mint, + status: TransactionStatus.minting, + }; + + txRef.current = tx.hash; + // @ts-expect-error TODO: fix when we overhaul tx list, types are not good + dispatch(dataAddNewTransaction(tx)); + analyticsV2.track(event.mintsMintedNFT, { + collectionName: mintCollection.name || '', + contract: mintCollection.id || '', + chainId: mintCollection.chainId, + quantity, + priceInEth: mintPriceAmount, + }); + navigate(Routes.PROFILE_SCREEN); + setMintStatus('minted'); + } + }); + }); + }, + }); + } catch (e) { + setMintStatus('error'); + analyticsV2.track(event.mintsErrorMintingNFT, { + collectionName: mintCollection.name || '', + contract: mintCollection.id || '', + chainId: mintCollection.chainId, + quantity, + priceInEth: mintPriceAmount, + }); + logger.error( + new RainbowError(`Error minting NFT: ${(e as Error).message}`) + ); + } + }, [ + accountAddress, + currentNetwork, + dispatch, + imageColor, + imageUrl, + isMintingAvailable, + isReadOnlyWallet, + mintCollection.chainId, + mintCollection.id, + mintCollection.name, + mintPriceAmount, + navigate, + quantity, + ]); + + const buttonLabel = useMemo(() => { + if (!isMintingAvailable) { + return i18n.t(i18n.l.minting.mint_on_mintdotfun); + } + if (insufficientEth) { + return i18n.t(i18n.l.button.confirm_exchange.insufficient_eth); + } + + if (mintStatus === 'minting') { + return i18n.t(i18n.l.minting.minting); + } else if (mintStatus === 'minted') { + return i18n.t(i18n.l.minting.minted); + } else if (mintStatus === 'error') { + return i18n.t(i18n.l.minting.error_minting); + } + + return i18n.t(i18n.l.minting.hold_to_mint); + }, [insufficientEth, isMintingAvailable, mintStatus]); + + return ( + <> + {ios && ( + + + + + + + )} + {/* @ts-expect-error JavaScript component */} + + + + + } + > + + + + + + + + + + + {mintCollection.name} + + + + {`${i18n.t(i18n.l.minting.by)} `} + + + {ensAvatar?.imageUrl ? ( + + ) : ( + + )} + + {` ${ + ensName || + deployerDisplay || + i18n.t(i18n.l.minting.unknown) + }`} + + + + + + + + + + + + + + + {i18n.t(i18n.l.minting.mint_price)} + + setShowNativePrice(!showNativePrice)} + > + + + {isZero(mintPriceAmount) + ? i18n.t(i18n.l.minting.free) + : showNativePrice + ? nativeMintPriceDisplay + : mintPriceDisplay} + + + + + + + + + { + + {quantity === Number(maxMintsPerWallet) + ? i18n.t(i18n.l.minting.max) + : ''} + + } + + setQuantity(1)} + minusAction={() => setQuantity(-1)} + buttonColor={imageColor} + disabled={!isMintingAvailable} + maxValue={Number(maxMintsPerWallet)} + /> + + + + + {/* @ts-ignore */} + + + + {/* @ts-ignore */} + + + + + + {mintCollection.description && ( + + + {i18n.t(i18n.l.minting.description)} + + + {mintCollection.description} + + + )} + + {mintCollection?.tokenCount && ( + + {i18n.t(i18n.l.minting.nft_count, { + number: mintCollection.tokenCount, + })} + + } + /> + )} + {mintCollection?.createdAt && ( + + {getFormattedDate(mintCollection?.createdAt)} + + } + /> + )} + + {mintCollection?.id && ( + + ethereumUtils.openAddressInBlockExplorer( + mintCollection.id!, + currentNetwork + ) + } + > + + {contractAddressDisplay} + + + } + /> + )} + + + + {currentNetwork === Network.mainnet ? ( + + ) : ( + + )} + + {`${getNetworkObj(currentNetwork).name}`} + + + + } + /> + + + + + + + + ); +}; + +export default MintSheet; diff --git a/src/screens/mints/components/QuantityButton.tsx b/src/screens/mints/components/QuantityButton.tsx new file mode 100644 index 00000000000..68a7413d0af --- /dev/null +++ b/src/screens/mints/components/QuantityButton.tsx @@ -0,0 +1,197 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import ReactNativeHapticFeedback from 'react-native-haptic-feedback'; +import { delay } from '@/utils/delay'; +import { usePrevious } from '@/hooks'; +import styled from '@/styled-thing'; +import { ButtonPressAnimation } from '@/components/animations'; +import Row from '@/components/layout/Row'; +import { Box, Inline, Text } from '@/design-system'; +import { useTheme } from '@/theme'; + +const PLUS_ACTION_TYPE = 'plus'; +const MINUS_ACTION_TYPE = 'minus'; +const LONG_PRESS_DELAY_THRESHOLD = 69; +const MIN_LONG_PRESS_DELAY_THRESHOLD = 200; + +const Wrapper = styled(Row)({}); + +const StepButtonWrapper = styled(ButtonPressAnimation).attrs(() => ({ + paddingHorizontal: 7, + scaleTo: 0.75, +}))({}); + +type StepButtonProps = { + type: 'plus' | 'minus'; + onLongPress: () => void; + onLongPressEnded: () => void; + onPress: () => void; + shouldLongPressHoldPress: boolean; + buttonColor: string; + disabled?: boolean; + threshold?: number; + value: number; +}; +const StepButton = ({ + type, + onLongPress, + onLongPressEnded, + onPress, + shouldLongPressHoldPress, + buttonColor, + disabled = false, + threshold, + value, +}: StepButtonProps) => { + const { colors, lightScheme } = useTheme(); + // should prob change the color here maybe :thinky: + const atThreshold = type === 'plus' ? value === threshold : value === 1; + const color = disabled || atThreshold ? lightScheme.grey : buttonColor; + + return ( + + + + {type === 'plus' ? '􀅼' : '􀅽'} + + + + ); +}; + +type StepButtonInputProps = { + value: number; + plusAction: () => void; + minusAction: () => void; + buttonColor: string; + disabled?: boolean; + maxValue: number; +}; +export function QuantityButton({ + value, + plusAction, + minusAction, + buttonColor, + disabled = false, + maxValue, +}: StepButtonInputProps) { + const longPressHandle = useRef(null); + const [trigger, setTrigger] = useState(false); + const [actionType, setActionType] = useState<'plus' | 'minus' | null>(null); + const prevTrigger = usePrevious(trigger); + + const onMinusPress = useCallback(() => { + longPressHandle.current = false; + minusAction(); + }, [minusAction]); + + const onPlusPress = useCallback(() => { + longPressHandle.current = false; + plusAction(); + }, [plusAction]); + + const onLongPressEnded = useCallback(() => { + longPressHandle.current = false; + setActionType(null); + }, [longPressHandle]); + + const onLongPressLoop = useCallback(async () => { + setTrigger(true); + setTrigger(false); + await delay(LONG_PRESS_DELAY_THRESHOLD); + longPressHandle.current && onLongPressLoop(); + }, []); + + const onLongPress = useCallback(async () => { + longPressHandle.current = true; + onLongPressLoop(); + }, [onLongPressLoop]); + + const onPlusLongPress = useCallback(() => { + setActionType(PLUS_ACTION_TYPE); + onLongPress(); + }, [onLongPress]); + + const onMinusLongPress = useCallback(() => { + setActionType(MINUS_ACTION_TYPE); + onLongPress(); + }, [onLongPress]); + + useEffect(() => { + if (!prevTrigger && trigger) { + if (actionType === PLUS_ACTION_TYPE) { + plusAction(); + if (!android) { + ReactNativeHapticFeedback.trigger('selection'); + } + } else if (actionType === MINUS_ACTION_TYPE) { + minusAction(); + if (!android) { + ReactNativeHapticFeedback.trigger('selection'); + } + } + } + }, [trigger, prevTrigger, actionType, plusAction, minusAction]); + + return ( + + + + + {value} + + + + + ); +} diff --git a/yarn.lock b/yarn.lock index fe54973101a..2dbe3180cbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4069,10 +4069,10 @@ color "^4.2.3" warn-once "^0.1.0" -"@reservoir0x/reservoir-sdk@1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@reservoir0x/reservoir-sdk/-/reservoir-sdk-1.2.1.tgz#8f1a1330c06c658b98e4fd66d0de994c398197ba" - integrity sha512-+FN4mN5m5zEPAW41ZkO23NBc1ANQA8gIJr7bWE44fOm7T0dMtbBKEHuGsLtwbuKUx+IslbjmdhytkXRfFTPMqQ== +"@reservoir0x/reservoir-sdk@1.4.5": + version "1.4.5" + resolved "https://registry.yarnpkg.com/@reservoir0x/reservoir-sdk/-/reservoir-sdk-1.4.5.tgz#6ff0fd61fea203de4cdde019048fa9d0b0ab08e9" + integrity sha512-Yi4Z0c85fUfPB2mo1FdeCpEgrpN36fDV8gU4RDmsl6IuFYD8pwA/SMtxoo5M0uEN3at+VpVV9utyvB8XlHUFMQ== dependencies: axios "^0.27.2"