From 851fc48b0cbabc666a8a1606b5a6456b73ad33d3 Mon Sep 17 00:00:00 2001 From: borcherd Date: Wed, 17 Jan 2024 12:14:41 +0100 Subject: [PATCH 1/2] feat: init commit --- .../apps/peanut/claim/1.LinkDetails.tsx | 145 +++++++++++ .../apps/peanut/claim/2.ClaimSuccess.tsx | 38 +++ .../apps/peanut/claim/useClaimLinkPeanut.tsx | 124 ++++++++++ .../apps/peanut/create/1.ViewChainToSend.tsx | 40 +++ .../apps/peanut/create/2.ViewTokenToSend.tsx | 142 +++++++++++ .../apps/peanut/create/3.ViewAmountToSend.tsx | 232 ++++++++++++++++++ .../apps/peanut/create/4.ViewSuccesToSend.tsx | 43 ++++ components/apps/peanut/create/Logo.tsx | 119 +++++++++ .../peanut/create/useCreateLinkPeanut.tsx | 142 +++++++++++ next.config.js | 3 +- package.json | 1 + pages/index.tsx | 15 ++ pages/peanut/claim/index.tsx | 80 ++++++ pages/peanut/index.tsx | 108 ++++++++ tsconfig.json | 1 + 15 files changed, 1232 insertions(+), 1 deletion(-) create mode 100644 components/apps/peanut/claim/1.LinkDetails.tsx create mode 100644 components/apps/peanut/claim/2.ClaimSuccess.tsx create mode 100644 components/apps/peanut/claim/useClaimLinkPeanut.tsx create mode 100644 components/apps/peanut/create/1.ViewChainToSend.tsx create mode 100644 components/apps/peanut/create/2.ViewTokenToSend.tsx create mode 100644 components/apps/peanut/create/3.ViewAmountToSend.tsx create mode 100644 components/apps/peanut/create/4.ViewSuccesToSend.tsx create mode 100644 components/apps/peanut/create/Logo.tsx create mode 100644 components/apps/peanut/create/useCreateLinkPeanut.tsx create mode 100644 pages/peanut/claim/index.tsx create mode 100644 pages/peanut/index.tsx diff --git a/components/apps/peanut/claim/1.LinkDetails.tsx b/components/apps/peanut/claim/1.LinkDetails.tsx new file mode 100644 index 00000000..69f3d296 --- /dev/null +++ b/components/apps/peanut/claim/1.LinkDetails.tsx @@ -0,0 +1,145 @@ +import {useEffect, useState} from 'react'; +import {IconSpinner} from '@icons/IconSpinner'; +import {CHAIN_DETAILS, claimLinkGasless} from '@squirrel-labs/peanut-sdk'; +import {waitForTransaction} from '@wagmi/core'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; + +import {useClaimLinkPeanut} from './useClaimLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {linkDetails, set_claimTxHash, claimTxHash} = useClaimLinkPeanut(); + const {address} = useWeb3(); + const [isClient, set_isClient] = useState(false); + const [isLoading, set_isLoading] = useState(false); + + /* + * Claim link + * Function that handles peanut functionality to claim a link. + * This function is the gasless function, it will not require any wallet interaction by the user. Only the link, and the address are required. + */ + async function onClaimLink(): Promise { + set_isLoading(true); + + if (!address) { + alert('Please connect a wallet to claim to.'); + set_isLoading(false); + return; + } + + try { + const claimLinkGaslessResp = await claimLinkGasless({ + link: linkDetails.link, + recipientAddress: address ? address.toString() : '', + APIKey: process.env.PEANUT_API_KEY ?? '', + baseUrl: 'https://peanut-api-ts-9lo6.onrender.com/claim-v2' + }); + waitForTransaction({hash: claimLinkGaslessResp.txHash}); + set_claimTxHash(claimLinkGaslessResp.txHash); + set_isLoading(false); + onProceed(); + } catch (error) { + set_isLoading(false); + console.log('error', error); + } + } + + useEffect(() => { + set_isClient(true); + }, []); // to prevent hydration error + + return ( +
+
+
+
+ {'Here are some details:'} +
+
+ +
+
+

{'Chain:'}

+
+
+

+ {linkDetails.chainId ? ( + CHAIN_DETAILS[linkDetails.chainId]?.name + ) : ( + + )} +

+
+ +
+

{'Amount:'}

+
+
+

+ {linkDetails.tokenAmount ? ( + linkDetails.tokenAmount + ) : ( + + )} +

+
+
+

{'Token:'}

+
+
+

+ {linkDetails.tokenName ? ( + linkDetails.tokenName + ) : ( + + )} +

+
+
+ +
+
+ {isClient + ? linkDetails.claimed || claimTxHash + ? 'This link has already been claimed' + : address + ? 'You are claiming to ' + address + : 'Please connect a wallet to claim to.' + : ''} +
+ +
+ +
+
+
+
+ ); +} +export default ViewLinkDetails; diff --git a/components/apps/peanut/claim/2.ClaimSuccess.tsx b/components/apps/peanut/claim/2.ClaimSuccess.tsx new file mode 100644 index 00000000..391493df --- /dev/null +++ b/components/apps/peanut/claim/2.ClaimSuccess.tsx @@ -0,0 +1,38 @@ +import {useMemo} from 'react'; +import {CHAIN_DETAILS} from '@squirrel-labs/peanut-sdk'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import {useClaimLinkPeanut} from './useClaimLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewClaimSuccess(): ReactElement { + const {linkDetails, claimTxHash} = useClaimLinkPeanut(); + + const blockExplorerUrl = useMemo(() => { + return CHAIN_DETAILS[linkDetails.chainId]?.explorers[0].url; + }, [linkDetails.chainId, CHAIN_DETAILS]); + + return ( +
+
+ + + +
+
+ ); +} +export default ViewClaimSuccess; diff --git a/components/apps/peanut/claim/useClaimLinkPeanut.tsx b/components/apps/peanut/claim/useClaimLinkPeanut.tsx new file mode 100644 index 00000000..e5fffb38 --- /dev/null +++ b/components/apps/peanut/claim/useClaimLinkPeanut.tsx @@ -0,0 +1,124 @@ +import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo,useState} from 'react'; +import {useUpdateEffect} from '@react-hookz/web'; +import {getLinkDetails} from '@squirrel-labs/peanut-sdk'; +import {scrollToTargetAdjusted} from '@utils/animations'; +import {HEADER_HEIGHT} from '@utils/constants'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; + +export enum Step { + LINKDETAILS = 'link_details', + CLAIMSUCCESS = 'claim_success' +} + +export type TClaimLink = { + linkDetails: any; + set_linkDetails: Dispatch>; + currentStep: Step; + set_currentStep: Dispatch>; + claimTxHash: string; + set_claimTxHash: Dispatch>; + claimUrl: string; + set_claimUrl: Dispatch>; +}; + +const defaultProps: TClaimLink = { + linkDetails: {}, + set_linkDetails: (): void => undefined, + currentStep: Step.LINKDETAILS, + set_currentStep: (): void => undefined, + claimTxHash: '', + set_claimTxHash: (): void => undefined, + claimUrl: '', + set_claimUrl: (): void => undefined +}; + +const ClaimLinkPeanutContext = createContext(defaultProps); +export const ClaimLinkPeanutContextApp = ({children}: {children: React.ReactElement}): React.ReactElement => { + const {address, isActive, isWalletSafe, isWalletLedger, onConnect} = useWeb3(); + const [currentStep, set_currentStep] = useState(Step.LINKDETAILS); + const [linkDetails, set_linkDetails] = useState({}); + const [claimTxHash, set_claimTxHash] = useState(''); + const [claimUrl, set_claimUrl] = useState(''); + + /********************************************************************************************** + ** This effect is used to directly ask the user to connect its wallet if it's not connected + **********************************************************************************************/ + useEffect((): void => { + if (!isActive && !address) { + onConnect(); + return; + } + }, [address, isActive, onConnect]); + + /********************************************************************************************** + ** This effect is used to handle some UI transitions and sections jumps. Once the current step + ** changes, we need to scroll to the correct section. + ** This effect is ignored on mount but will be triggered on every update to set the correct + ** scroll position. + **********************************************************************************************/ + useUpdateEffect((): void => { + setTimeout((): void => { + let currentStepContainer; + const scalooor = document?.getElementById('scalooor'); + + if (currentStep === Step.LINKDETAILS) { + currentStepContainer = document?.getElementById('linkDetails'); + } else if (currentStep === Step.CLAIMSUCCESS) { + currentStepContainer = document?.getElementById('claimSuccess'); + } + const currentElementHeight = currentStepContainer?.offsetHeight; + if (scalooor?.style) { + scalooor.style.height = `calc(100vh - ${currentElementHeight}px - ${HEADER_HEIGHT}px + 36px)`; + } + if (currentStepContainer) { + scrollToTargetAdjusted(currentStepContainer); + } + }, 0); + }, [currentStep, isWalletLedger, isWalletSafe]); + + useEffect(() => { + if (claimUrl) { + peanutGetLinkDetails({claimUrl}); + } + }, [claimUrl]); + + /********************************************************************************************** + ** This function is used to get the details of the link (amount, token, chain). + **********************************************************************************************/ + async function peanutGetLinkDetails({claimUrl}: {claimUrl: string}): Promise { + try { + const linkDetails = await getLinkDetails({ + link: claimUrl + }); + console.log('linkDetails', linkDetails); + set_linkDetails(linkDetails); + } catch (error) { + console.error(error); + } + } + + const contextValue = useMemo( + (): TClaimLink => ({ + currentStep, + set_currentStep, + linkDetails, + set_linkDetails, + claimTxHash, + set_claimTxHash, + claimUrl, + set_claimUrl + }), + [currentStep, set_currentStep, linkDetails, set_linkDetails, claimTxHash, set_claimTxHash] + ); + + return ( + +
+ {children} +
+
+ + ); +}; + +export const useClaimLinkPeanut = (): TClaimLink => React.useContext(ClaimLinkPeanutContext); diff --git a/components/apps/peanut/create/1.ViewChainToSend.tsx b/components/apps/peanut/create/1.ViewChainToSend.tsx new file mode 100644 index 00000000..dbc37ff2 --- /dev/null +++ b/components/apps/peanut/create/1.ViewChainToSend.tsx @@ -0,0 +1,40 @@ +import {NetworkSelector} from 'components/common/HeaderElements'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import type {ReactElement} from 'react'; + +function ViewChainToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + return ( +
+
+ + +
+
=> e.preventDefault()} + className={ + 'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6' + }> +
+ +
+
+ +
+
+
+
+
+ ); +} +export default ViewChainToSend; diff --git a/components/apps/peanut/create/2.ViewTokenToSend.tsx b/components/apps/peanut/create/2.ViewTokenToSend.tsx new file mode 100644 index 00000000..a1e81f10 --- /dev/null +++ b/components/apps/peanut/create/2.ViewTokenToSend.tsx @@ -0,0 +1,142 @@ +import React, {useState} from 'react'; +import ComboboxAddressInput from 'components/common/ComboboxAddressInput'; +import {useTokenList} from 'contexts/useTokenList'; +import {Step} from '@disperse/useDisperse'; +import {useDeepCompareEffect, useUpdateEffect} from '@react-hookz/web'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {isZeroAddress, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {ETH_TOKEN_ADDRESS, ZERO_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ReactElement} from 'react'; +import type {TDict} from '@yearn-finance/web-lib/types'; +import type {TToken} from '@utils/types/types'; + +function ViewTokenToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {safeChainID} = useChainID(); + const {currentStep, tokenToSend, set_tokenToSend} = useCreateLinkPeanut(); + const {tokenList} = useTokenList(); + const [localTokenToSend, set_localTokenToSend] = useState(ETH_TOKEN_ADDRESS); + const [isValidTokenToReceive, set_isValidTokenToReceive] = useState(true); + const [possibleTokenToReceive, set_possibleTokenToReceive] = useState>({}); + + /* 🔵 - Yearn Finance ************************************************************************** + ** On mount, fetch the token list from the tokenlistooor repo for the cowswap token list, which + ** will be used to populate the tokenToDisperse token combobox. + ** Only the tokens in that list will be displayed as possible destinations. + **********************************************************************************************/ + useDeepCompareEffect((): void => { + const possibleDestinationsTokens: TDict = {}; + const {wrappedToken} = getNetwork(safeChainID).contracts; + if (wrappedToken) { + possibleDestinationsTokens[ETH_TOKEN_ADDRESS] = { + address: ETH_TOKEN_ADDRESS, + chainID: safeChainID, + name: wrappedToken.coinName, + symbol: wrappedToken.coinSymbol, + decimals: wrappedToken.decimals, + logoURI: `${process.env.SMOL_ASSETS_URL}/token/${safeChainID}/${ETH_TOKEN_ADDRESS}/logo-128.png` + }; + } + for (const eachToken of Object.values(tokenList)) { + if (eachToken.chainID === safeChainID) { + possibleDestinationsTokens[toAddress(eachToken.address)] = eachToken; + } + } + set_possibleTokenToReceive(possibleDestinationsTokens); + }, [tokenList, safeChainID]); + + /* 🔵 - Yearn Finance ************************************************************************** + ** When the tokenToDisperse token changes, check if it is a valid tokenToDisperse token. The check is + ** trivial as we only check if the address is valid. + **********************************************************************************************/ + useUpdateEffect((): void => { + set_isValidTokenToReceive('undetermined'); + if (!isZeroAddress(toAddress(localTokenToSend))) { + set_isValidTokenToReceive(true); + } + }, [tokenToSend]); + + return ( +
+
+ +
+
=> e.preventDefault()} + className={ + 'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6' + }> +
+ { + if ([Step.SELECTOR].includes(currentStep)) { + set_localTokenToSend(newToken); + set_tokenToSend({ + address: toAddress(newToken as string), + chainID: safeChainID, + name: possibleTokenToReceive[toAddress(newToken as string)]?.name || '', + symbol: possibleTokenToReceive[toAddress(newToken as string)]?.symbol || '', + decimals: + possibleTokenToReceive[toAddress(newToken as string)]?.decimals || 0, + logoURI: + possibleTokenToReceive[toAddress(newToken as string)]?.logoURI || '' + }); + } else { + set_localTokenToSend(newToken); + set_tokenToSend({ + address: toAddress(newToken as string), + chainID: safeChainID, + name: possibleTokenToReceive[toAddress(newToken as string)]?.name || '', + symbol: possibleTokenToReceive[toAddress(newToken as string)]?.symbol || '', + decimals: + possibleTokenToReceive[toAddress(newToken as string)]?.decimals || 0, + logoURI: + possibleTokenToReceive[toAddress(newToken as string)]?.logoURI || '' + }); + } + }} + /> +
+
+ +
+
+
+
+
+ ); +} + +export default ViewTokenToSend; diff --git a/components/apps/peanut/create/3.ViewAmountToSend.tsx b/components/apps/peanut/create/3.ViewAmountToSend.tsx new file mode 100644 index 00000000..b4a57cd3 --- /dev/null +++ b/components/apps/peanut/create/3.ViewAmountToSend.tsx @@ -0,0 +1,232 @@ +import React, {memo, useCallback, useMemo, useState} from 'react'; +import {useWallet} from 'contexts/useWallet'; +import {handleInputChangeEventValue} from 'utils/handleInputChangeEventValue'; +import {IconSpinner} from '@icons/IconSpinner'; +import {CHAIN_DETAILS, getLinksFromTx, getRandomString, prepareTxs} from '@squirrel-labs/peanut-sdk'; +import {prepareSendTransaction, sendTransaction, waitForTransaction} from '@wagmi/core'; +import {Button} from '@yearn-finance/web-lib/components/Button'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import {isZeroAddress, toAddress} from '@yearn-finance/web-lib/utils/address'; +import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ChangeEvent, ReactElement} from 'react'; +import type {TNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import type {TToken} from '@utils/types/types'; + +function AmountToSendInput({ + token, + amount, + onChange +}: { + token: TToken | undefined; + amount: TNormalizedBN | undefined; + onChange: (amount: TNormalizedBN) => void; +}): ReactElement { + /********************************************************************************************** + ** onInputChange is triggered when the user is typing in the input field. It updates the + ** amount in the state and triggers the debounced retrieval of the quote from the Cowswap API. + ** It is set as callback to avoid unnecessary re-renders. + **********************************************************************************************/ + const onInputChange = useCallback( + (e: ChangeEvent): void => { + onChange(handleInputChangeEventValue(e, token?.decimals || 18)); + }, + [onChange, token?.decimals] + ); + + return ( +
+
+ e.preventDefault()} + min={0} + step={1 / 10 ** (token?.decimals || 18)} + inputMode={'numeric'} + placeholder={'0'} + pattern={'^((?:0|[1-9]+)(?:.(?:d+?[1-9]|[1-9]))?)$'} + onChange={onInputChange} + value={amount?.normalized} + /> +
+
+ ); +} + +const ViewAmountToSend = memo(function ViewAmountToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { + const {safeChainID} = useChainID(); + const {address} = useWeb3(); + const {balances} = useWallet(); + const {tokenToSend, amountToSend, set_amountToSend, createdLink, set_createdLink, onResetCreateLink} = + useCreateLinkPeanut(); + const [loadingStates, set_loadingStates] = useState<'idle' | 'Confirm in wallet' | 'Creating'>('idle'); + + const isLoading = useMemo(() => loadingStates !== 'idle', [loadingStates]); + + const balanceOf = useMemo((): number => { + if (isZeroAddress(tokenToSend?.address)) { + return 0; + } + const balance = balances?.[toAddress(tokenToSend?.address)]?.normalized; + return balance || 0; + }, [balances, tokenToSend]); + + const isAboveBalance = (Number(amountToSend?.normalized) ?? 0) > balanceOf; + + /* + * Function to handle the creation of a link. ChainId, tokenAmount and tokenAddress are required. + * Done using advanced implementation (prepare, signing and getting the link are done seperately) + */ + const onCreateLink = async (): Promise => { + try { + set_loadingStates('Creating'); + const tokenType = + CHAIN_DETAILS[safeChainID as keyof typeof CHAIN_DETAILS]?.nativeCurrency.symbol == tokenToSend.symbol + ? 0 + : 1; + + const linkDetails = { + chainId: safeChainID, + tokenAmount: Number(amountToSend?.normalized), + tokenAddress: tokenToSend?.address, + tokenDecimals: tokenToSend?.decimals, + tokenType: tokenType, + baseUrl: `${window.location.href}/claim`, + trackId: 'smoldapp' + }; + + const password = await getRandomString(16); + + const preparedTxs = await prepareTxs({ + address: address ?? '', + linkDetails: linkDetails, + passwords: [password] + }); + const signedTxsResponse = []; + + for (const tx of preparedTxs.unsignedTxs) { + set_loadingStates('Confirm in wallet'); + + const config = await prepareSendTransaction({ + to: tx.to ?? undefined, + data: (tx.data as `0x${string}`) ?? undefined, + value: tx.value?.valueOf() ?? undefined, + nonce: tx.nonce ?? undefined + }); + const sendTxResponse = await sendTransaction(config); + set_loadingStates('Creating'); + await waitForTransaction({hash: sendTxResponse.hash, confirmations: 4}); + + signedTxsResponse.push(sendTxResponse); + } + + const getLinkFromTxResponse = await getLinksFromTx({ + linkDetails: linkDetails, + txHash: signedTxsResponse[signedTxsResponse.length - 1].hash, + passwords: [password] + }); + + set_createdLink({ + link: getLinkFromTxResponse.links[0], + hash: signedTxsResponse[signedTxsResponse.length - 1].hash + }); + set_loadingStates('idle'); + onProceed(); + } catch (error) { + console.error(error); + set_loadingStates('idle'); + } + }; + + return ( +
+
+
+
+ {'How much do you want to send?'} +

+ {'Drop the amount of tokens you want the link to hold.'} +

+
+
+ +
+
+

{'Amount'}

+
+
+ { + set_amountToSend(amount); + }} + /> +
+
+
+
+
+
{'You have'}
+ +
+ {`${formatAmount(balanceOf, tokenToSend?.decimals || 18)} ${tokenToSend?.symbol || ''}`} +
+
+
+
{'You are sending'}
+ +
+ {`${formatAmount(amountToSend?.normalized ?? '0', tokenToSend?.decimals || 18)} ${ + tokenToSend?.symbol || '' + }`} +
+
+
+
+ +
+
+
+
+ ); +}); + +export default ViewAmountToSend; diff --git a/components/apps/peanut/create/4.ViewSuccesToSend.tsx b/components/apps/peanut/create/4.ViewSuccesToSend.tsx new file mode 100644 index 00000000..9a303325 --- /dev/null +++ b/components/apps/peanut/create/4.ViewSuccesToSend.tsx @@ -0,0 +1,43 @@ +import {useMemo} from 'react'; +import {CHAIN_DETAILS} from '@squirrel-labs/peanut-sdk'; +import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; +import ViewSectionHeading from '@common/ViewSectionHeading'; + +import {useCreateLinkPeanut} from './useCreateLinkPeanut'; + +import type {ReactElement} from 'react'; + +function ViewSuccesToSend(): ReactElement { + const {createdLink} = useCreateLinkPeanut(); + const {safeChainID} = useChainID(); + + const blockExplorerUrl = useMemo(() => { + return CHAIN_DETAILS[safeChainID]?.explorers[0].url; + }, [safeChainID, CHAIN_DETAILS]); + + return ( +
+ +
+ ); +} +export default ViewSuccesToSend; diff --git a/components/apps/peanut/create/Logo.tsx b/components/apps/peanut/create/Logo.tsx new file mode 100644 index 00000000..d0e9627e --- /dev/null +++ b/components/apps/peanut/create/Logo.tsx @@ -0,0 +1,119 @@ +import React from 'react'; + +import type {ReactElement} from 'react'; + +function LogoPeanutCreator(props: React.SVGProps): ReactElement { + const st0 = {fill: '#FFFFFF', stroke: '#000000', strokeWidth: 12, strokeMiterlimit: 10}; + const st2 = {fillOpacity: 0, stroke: '#000000', strokeWidth: 12}; + const st4 = {fill: '#000000'}; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default LogoPeanutCreator; diff --git a/components/apps/peanut/create/useCreateLinkPeanut.tsx b/components/apps/peanut/create/useCreateLinkPeanut.tsx new file mode 100644 index 00000000..5aaf9ab9 --- /dev/null +++ b/components/apps/peanut/create/useCreateLinkPeanut.tsx @@ -0,0 +1,142 @@ +import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo,useState} from 'react'; +import {useUpdateEffect} from '@react-hookz/web'; +import {scrollToTargetAdjusted} from '@utils/animations'; +import {HEADER_HEIGHT} from '@utils/constants'; +import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; +import {ETH_TOKEN_ADDRESS} from '@yearn-finance/web-lib/utils/constants'; +import {getNetwork} from '@yearn-finance/web-lib/utils/wagmi/utils'; + +import type {TNormalizedBN} from '@yearn-finance/web-lib/utils/format.bigNumber'; +import type {TToken} from '@utils/types/types'; + +export enum Step { + TOSENDCHAIN = 'destination_chain', + TOSENDTOKEN = 'destination_token', + TOSENDAMOUNT = 'destination_amount', + TOSENDSUCCESS = 'destination_success' +} + +export type TCreatedLink = { + hash: string; + link: string; +}; + +export type TSelected = { + tokenToSend: TToken; + currentStep: Step; + amountToSend: TNormalizedBN | undefined; + createdLink: TCreatedLink; + set_createdLink: Dispatch>; + set_tokenToSend: Dispatch>; + set_currentStep: Dispatch>; + set_amountToSend: Dispatch>; + onResetCreateLink: () => void; +}; + +const {wrappedToken: defaultTokens} = getNetwork(1).contracts; +const defaultProps: TSelected = { + tokenToSend: { + address: ETH_TOKEN_ADDRESS, + chainID: 1, + name: defaultTokens?.coinName || 'Ether', + symbol: defaultTokens?.coinSymbol || 'ETH', + decimals: defaultTokens?.decimals || 18, + logoURI: `${process.env.SMOL_ASSETS_URL}/token/1/${ETH_TOKEN_ADDRESS}/logo-128.png` + }, + currentStep: Step.TOSENDCHAIN, + amountToSend: undefined, + createdLink: { + hash: '', + link: '' + }, + set_createdLink: (): void => undefined, + set_tokenToSend: (): void => undefined, + set_currentStep: (): void => undefined, + set_amountToSend: (): void => undefined, + onResetCreateLink: (): void => undefined +}; + +const CreateLinkPeanutContext = createContext(defaultProps); +export const CreateLinkPeanutContextApp = ({children}: {children: React.ReactElement}): React.ReactElement => { + const {address, isActive, isWalletSafe, isWalletLedger, onConnect} = useWeb3(); + const [currentStep, set_currentStep] = useState(Step.TOSENDCHAIN); + const [tokenToSend, set_tokenToSend] = useState(defaultProps.tokenToSend); + const [createdLink, set_createdLink] = useState(defaultProps.createdLink); + + const [amountToSend, set_amountToSend] = useState(undefined); + + const onResetCreateLink = (): void => { + setTimeout(() => { + set_currentStep(Step.TOSENDCHAIN); + set_tokenToSend(defaultProps.tokenToSend); + set_amountToSend(undefined); + set_createdLink(defaultProps.createdLink); + }, 500); + }; + + /********************************************************************************************** + ** This effect is used to directly ask the user to connect its wallet if it's not connected + **********************************************************************************************/ + useEffect((): void => { + if (!isActive && !address) { + onConnect(); + return; + } + }, [address, isActive, onConnect]); + + /********************************************************************************************** + ** This effect is used to handle some UI transitions and sections jumps. Once the current step + ** changes, we need to scroll to the correct section. + ** This effect is ignored on mount but will be triggered on every update to set the correct + ** scroll position. + **********************************************************************************************/ + useUpdateEffect((): void => { + setTimeout((): void => { + let currentStepContainer; + const scalooor = document?.getElementById('scalooor'); + + if (currentStep === Step.TOSENDCHAIN) { + currentStepContainer = document?.getElementById('chainToSend'); + } else if (currentStep === Step.TOSENDTOKEN) { + currentStepContainer = document?.getElementById('tokenToSend'); + } else if (currentStep === Step.TOSENDAMOUNT) { + currentStepContainer = document?.getElementById('amountToSend'); + } else if (currentStep === Step.TOSENDSUCCESS) { + currentStepContainer = document?.getElementById('successToSend'); + } + const currentElementHeight = currentStepContainer?.offsetHeight; + if (scalooor?.style) { + scalooor.style.height = `calc(100vh - ${currentElementHeight}px - ${HEADER_HEIGHT}px + 36px)`; + } + if (currentStepContainer) { + scrollToTargetAdjusted(currentStepContainer); + } + }, 0); + }, [currentStep, isWalletLedger, isWalletSafe]); + + const contextValue = useMemo( + (): TSelected => ({ + currentStep, + set_currentStep, + tokenToSend, + set_tokenToSend, + amountToSend, + set_amountToSend, + onResetCreateLink, + createdLink, + set_createdLink + }), + [currentStep, tokenToSend, amountToSend] + ); + + return ( + +
+ {children} +
+
+ + ); +}; + +export const useCreateLinkPeanut = (): TSelected => React.useContext(CreateLinkPeanutContext); diff --git a/next.config.js b/next.config.js index 7c40dcd5..62a1ff61 100755 --- a/next.config.js +++ b/next.config.js @@ -122,7 +122,8 @@ module.exports = phase => WALLETCONNECT_PROJECT_ICON: 'https://smold.app/favicons/ms-icon-310x310.png', RECEIVER_ADDRESS: '0x10001192576E8079f12d6695b0948C2F41320040', - DISPERSE_ADDRESS: '0xD152f549545093347A162Dce210e7293f1452150' + DISPERSE_ADDRESS: '0xD152f549545093347A162Dce210e7293f1452150', + PEANUT_API_KEY: process.env.PEANUT_API_KEY } }) ); diff --git a/package.json b/package.json index fe8b720a..d1be7823 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@rainbow-me/rainbowkit": "^1.1.1", "@react-hookz/web": "^23.1.0", + "@squirrel-labs/peanut-sdk": "0.4.0-alpha.12", "@tailwindcss/forms": "^0.5.6", "@vercel/analytics": "^1.1.0", "@wagmi/core": "^1.4.3", diff --git a/pages/index.tsx b/pages/index.tsx index 9521901b..4c1eef44 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -5,6 +5,7 @@ import LogoDisperse from '@disperse/Logo'; import {LogoTokenAssets} from '@icons/LogoTokenAssets'; import LogoMigratooor from '@migratooor/Logo'; import LogoNFTMigratooor from '@nftmigratooor/Logo'; +import LogoPeanutCreator from '@peanut/create/Logo'; import LogoSafeCreator from '@safeCreatooor/Logo'; import LogoStream from '@stream/Logo'; import LogoTokenlistooor from '@tokenlistooor/Logo'; @@ -13,6 +14,20 @@ import {Button} from '@yearn-finance/web-lib/components/Button'; import type {ReactElement} from 'react'; const apps = [ + { + href: '/peanut', + title: 'Peanut Protocol', + description: ( + + {'Send tokens to anyone, with a '} + + {'link'} + + {'.'} + + ), + icon: + }, { href: '/safe', title: 'MultiSafe', diff --git a/pages/peanut/claim/index.tsx b/pages/peanut/claim/index.tsx new file mode 100644 index 00000000..0c729949 --- /dev/null +++ b/pages/peanut/claim/index.tsx @@ -0,0 +1,80 @@ +import React, {useEffect} from 'react'; +import {DefaultSeo} from 'next-seo'; +import ViewLinkDetails from '@peanut/claim/1.LinkDetails'; +import ViewClaimSuccess from '@peanut/claim/2.ClaimSuccess'; +import {ClaimLinkPeanutContextApp,Step, useClaimLinkPeanut} from '@peanut/claim/useClaimLinkPeanut'; + +import type {ReactElement} from 'react'; + +function Peanut(): ReactElement { + const {currentStep, set_currentStep, set_claimUrl} = useClaimLinkPeanut(); + + useEffect(() => { + if (window.location.href) { + set_claimUrl(window.location.href); + } + }, []); + + return ( +
+
+

+ {'Peanut Protocol'} +

+ + {'You have been sent a link which holds tokens. Claim them directly to your wallet now! '} + +
+
+ { + if (currentStep === Step.LINKDETAILS) { + set_currentStep(Step.CLAIMSUCCESS); + } + }} + /> +
+ +
+ +
+
+ ); +} + +export default function PeanutWrapper(): ReactElement { + return ( + + <> + + + + + ); +} diff --git a/pages/peanut/index.tsx b/pages/peanut/index.tsx new file mode 100644 index 00000000..52d9ec14 --- /dev/null +++ b/pages/peanut/index.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import {DefaultSeo} from 'next-seo'; +import ViewChainToSend from '@peanut/create/1.ViewChainToSend'; +import ViewTokenToSend from '@peanut/create/2.ViewTokenToSend'; +import ViewAmountToSend from '@peanut/create/3.ViewAmountToSend'; +import ViewSuccesToSend from '@peanut/create/4.ViewSuccesToSend'; +import {CreateLinkPeanutContextApp, Step,useCreateLinkPeanut} from '@peanut/create/useCreateLinkPeanut'; + +import type {ReactElement} from 'react'; + +function Peanut(): ReactElement { + const {currentStep, set_currentStep} = useCreateLinkPeanut(); + + return ( +
+
+

+ {'Peanut Protocol'} +

+ + {'Create a link, send it to anyone, and they can claim the funds! '} + +
+
+ { + if (currentStep === Step.TOSENDCHAIN) { + set_currentStep(Step.TOSENDTOKEN); + } + }} + /> +
+ +
+ { + if (currentStep === Step.TOSENDTOKEN) { + set_currentStep(Step.TOSENDAMOUNT); + } + }} + /> +
+ +
+ { + if (currentStep === Step.TOSENDAMOUNT) { + set_currentStep(Step.TOSENDSUCCESS); + } + }} + /> +
+ +
+ +
+
+ ); +} + +export default function PeanutWrapper(): ReactElement { + return ( + + <> + + + + + ); +} diff --git a/tsconfig.json b/tsconfig.json index 036a72af..0242a98e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "@nftmigratooor/*": ["components/apps/nftmigratooor/*"], "@tokenlistooor/*": ["components/apps/tokenlistooor/*"], "@safeCreatooor/*": ["components/apps/safeCreator/*"], + "@peanut/*": ["components/apps/peanut/*"], "@stream/*": ["components/apps/stream/*"], "@utils/*": ["utils/*"] }, From f52ee2383ebe353bf122380c0ca0214423694d3c Mon Sep 17 00:00:00 2001 From: borcherd Date: Tue, 27 Feb 2024 11:26:20 +0100 Subject: [PATCH 2/2] feat: requested changes --- .../apps/peanut/claim/1.LinkDetails.tsx | 37 +++++++++++-- .../apps/peanut/claim/useClaimLinkPeanut.tsx | 2 +- .../apps/peanut/create/1.ViewChainToSend.tsx | 10 +++- .../apps/peanut/create/3.ViewAmountToSend.tsx | 11 ++-- .../apps/peanut/create/4.ViewSuccesToSend.tsx | 34 ++++++++---- .../peanut/create/useCreateLinkPeanut.tsx | 2 +- components/common/HeaderElements.tsx | 53 +++++++++++++------ package.json | 2 +- pages/peanut/claim/index.tsx | 2 +- pages/peanut/index.tsx | 2 +- 10 files changed, 112 insertions(+), 43 deletions(-) diff --git a/components/apps/peanut/claim/1.LinkDetails.tsx b/components/apps/peanut/claim/1.LinkDetails.tsx index 69f3d296..adb1fe2e 100644 --- a/components/apps/peanut/claim/1.LinkDetails.tsx +++ b/components/apps/peanut/claim/1.LinkDetails.tsx @@ -33,8 +33,7 @@ function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { const claimLinkGaslessResp = await claimLinkGasless({ link: linkDetails.link, recipientAddress: address ? address.toString() : '', - APIKey: process.env.PEANUT_API_KEY ?? '', - baseUrl: 'https://peanut-api-ts-9lo6.onrender.com/claim-v2' + APIKey: process.env.NEXT_PUBLIC_PEANUT_API_KEY ?? '' }); waitForTransaction({hash: claimLinkGaslessResp.txHash}); set_claimTxHash(claimLinkGaslessResp.txHash); @@ -70,7 +69,11 @@ function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { {linkDetails.chainId ? ( CHAIN_DETAILS[linkDetails.chainId]?.name ) : ( - + )}

@@ -83,7 +86,11 @@ function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { {linkDetails.tokenAmount ? ( linkDetails.tokenAmount ) : ( - + )}

@@ -95,7 +102,27 @@ function ViewLinkDetails({onProceed}: {onProceed: VoidFunction}): ReactElement { {linkDetails.tokenName ? ( linkDetails.tokenName ) : ( - + + )} +

+ +
+

{'Address:'}

+
+
+

+ {linkDetails.tokenAddress ? ( + linkDetails.tokenAddress + ) : ( + )}

diff --git a/components/apps/peanut/claim/useClaimLinkPeanut.tsx b/components/apps/peanut/claim/useClaimLinkPeanut.tsx index e5fffb38..eae2eef6 100644 --- a/components/apps/peanut/claim/useClaimLinkPeanut.tsx +++ b/components/apps/peanut/claim/useClaimLinkPeanut.tsx @@ -1,4 +1,4 @@ -import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo,useState} from 'react'; +import React, {createContext, type Dispatch, type SetStateAction, useEffect, useMemo, useState} from 'react'; import {useUpdateEffect} from '@react-hookz/web'; import {getLinkDetails} from '@squirrel-labs/peanut-sdk'; import {scrollToTargetAdjusted} from '@utils/animations'; diff --git a/components/apps/peanut/create/1.ViewChainToSend.tsx b/components/apps/peanut/create/1.ViewChainToSend.tsx index dbc37ff2..f6ef3cc8 100644 --- a/components/apps/peanut/create/1.ViewChainToSend.tsx +++ b/components/apps/peanut/create/1.ViewChainToSend.tsx @@ -21,7 +21,15 @@ function ViewChainToSend({onProceed}: {onProceed: VoidFunction}): ReactElement { 'grid w-full grid-cols-12 flex-row items-center justify-between gap-4 md:w-3/4 md:gap-6' }>
- +
+ +
{' '}