diff --git a/apps/common/components/SearchBar.tsx b/apps/common/components/SearchBar.tsx index 845d0d293..16f923692 100644 --- a/apps/common/components/SearchBar.tsx +++ b/apps/common/components/SearchBar.tsx @@ -1,20 +1,29 @@ +import {cl} from '@yearn-finance/web-lib/utils/cl'; + import type {ChangeEvent, ReactElement} from 'react'; export type TSearchBar = { searchPlaceholder: string; searchValue: string; set_searchValue: (searchValue: string) => void; -} + className?: string; +}; -export function SearchBar({searchPlaceholder, searchValue, set_searchValue}: TSearchBar): ReactElement { +export function SearchBar({searchPlaceholder, searchValue, set_searchValue, className}: TSearchBar): ReactElement { return ( <> -
+
+ }} + />
+ d={ + 'M10 1C5.02972 1 1 5.02972 1 10C1 14.9703 5.02972 19 10 19C12.1249 19 14.0779 18.2635 15.6176 17.0318L21.2929 22.7071C21.6834 23.0976 22.3166 23.0976 22.7071 22.7071C23.0976 22.3166 23.0976 21.6834 22.7071 21.2929L17.0318 15.6176C18.2635 14.0779 19 12.1249 19 10C19 5.02972 14.9703 1 10 1ZM3 10C3 6.13428 6.13428 3 10 3C13.8657 3 17 6.13428 17 10C17 13.8657 13.8657 17 10 17C6.13428 17 3 13.8657 3 10Z' + } + fill={'currentcolor'} + />
-
diff --git a/apps/common/components/Table.tsx b/apps/common/components/Table.tsx index 656779321..f99767056 100644 --- a/apps/common/components/Table.tsx +++ b/apps/common/components/Table.tsx @@ -2,13 +2,11 @@ import {useCallback, useMemo, useState} from 'react'; import {sort} from '@veYFI/utils'; import {cl} from '@yearn-finance/web-lib/utils/cl'; -import {isZero} from '@yearn-finance/web-lib/utils/isZero'; import {Pagination} from '@common/components/Pagination'; import {usePagination} from '@common/hooks/usePagination'; import {IconChevronPlain} from '@common/icons/IconChevronPlain'; import type {ReactElement} from 'react'; -import type {TNormalizedBN} from '@common/types/types'; type TSortOrder = 'asc' | 'desc'; @@ -26,6 +24,7 @@ type TMetadata = { sortable?: boolean; fullWidth?: boolean; columnSpan?: number; + isDisabled?: (item: T) => boolean; format?: (item: T) => string | number; transform?: (item: T) => ReactElement; } @@ -67,10 +66,11 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it 9: 'md:grid-cols-9', 10: 'md:grid-cols-10', 11: 'md:grid-cols-11', - 12: 'md:grid-cols-12' + 12: 'md:grid-cols-12', + 13: 'md:grid-cols-13' }; - const numberOfColumns = Math.min(columns ?? (metadata.length), 12) as keyof typeof gridColsVariants; + const numberOfColumns = Math.min(columns ?? (metadata.length), 13) as keyof typeof gridColsVariants; const colSpanVariants = { 1: 'md:col-span-1', @@ -84,7 +84,8 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it 9: 'md:col-span-9', 10: 'md:col-span-10', 11: 'md:col-span-11', - 12: 'md:col-span-12' + 12: 'md:col-span-12', + 13: 'md:col-span-13' }; return ( @@ -105,7 +106,7 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it className || '' )} > -

+

{label}

{sortable && sortedBy === key && } @@ -143,7 +144,7 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it )} onClick={(): void => onRowClick?.(item)} > - {metadata.map(({key, label, className, fullWidth, columnSpan, format, transform}): ReactElement => { + {metadata.map(({key, label, className, fullWidth, columnSpan, format, transform, isDisabled}): ReactElement => { let isNumberLike = false; if (typeof item[key] === 'bigint') { isNumberLike = true; @@ -165,11 +166,7 @@ export function Table({metadata, data, columns, initialSortBy, onRowClick, it
{ + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const adapter = useMemo((): any => { + return { + replace(location: PartialLocation): void { + router.replace(pathname + location.search); + }, + push(location: PartialLocation): void { + router.push(pathname + location.search); + }, + get location(): {search: string} { + return { + search: searchParams.toString() + }; + } + }; + }, [router, pathname, searchParams]); + + return children(adapter); +}; diff --git a/apps/veyfi/Wrapper.tsx b/apps/veyfi/Wrapper.tsx index 86ae1fa96..818dbd2b0 100644 --- a/apps/veyfi/Wrapper.tsx +++ b/apps/veyfi/Wrapper.tsx @@ -21,7 +21,7 @@ export function Wrapper({children, router}: {children: ReactElement, router: Nex {!isApproved && ( @@ -120,14 +127,14 @@ function GaugeTabButtons({isApproved, vaultAddress, gaugeAddress, vaultDeposited ); } -export function GaugesTab(): ReactElement { - const {address} = useWeb3(); +function StakeUnstake(): ReactElement { + const {isActive, address} = useWeb3(); const {gaugesMap, positionsMap, allowancesMap} = useGauge(); const {vaults, prices} = useYearn(); const {balances} = useWallet(); const {dYFIPrice} = useOption(); const [isLoadingGauges, set_isLoadingGauges] = useState(true); - const userAddress = address as TAddress; + const {search, onSearch} = useQueryArguments(); const gaugesData = useDeepCompareMemo((): TGaugeData[] => { if (!vaults || Object.values(vaults).length === 0) { @@ -136,11 +143,11 @@ export function GaugesTab(): ReactElement { const data: TGaugeData[] = []; for (const gauge of Object.values(gaugesMap)) { - if (!gauge) { + const vault = vaults[toAddress(gauge?.vaultAddress)]; + if (!gauge || !vault) { continue; } - const vault = vaults[toAddress(gauge?.vaultAddress)]; const tokenPrice = formatToNormalizedValue(toBigInt(prices?.[vault.token.address] || 0), 6); const boost = Number(positionsMap[gauge.address]?.boost || 1); let APRFor10xBoost = Number(gauge?.rewardRate.normalized || 0) * dYFIPrice * SECONDS_PER_YEAR / Number(gauge?.totalStaked.normalized || 0) / tokenPrice * 100; @@ -148,15 +155,6 @@ export function GaugesTab(): ReactElement { APRFor10xBoost = 0; } - console.warn(`${Number(gauge?.rewardRate.normalized || 0)} x ${dYFIPrice} x ${SECONDS_PER_YEAR} / ${Number(gauge?.totalStaked.normalized || 0)} / ${tokenPrice} = ${APRFor10xBoost}`); - - // gauge.rewardRate() * dYFI_price * seconds_per_year / gauge.totalAssets() / vault_token_price - - - // const APR = rewardRate * 31556952n / totalAssets.raw; - // console.log(APR) - // gauge.rewardRate() * seconds_per_year / gauge.totalAssets() / vault_token_price - data.push({ gaugeAddress: gauge.address, vaultAddress: vault.address, @@ -168,95 +166,209 @@ export function GaugesTab(): ReactElement { gaugeAPR: APRFor10xBoost, gaugeBoost: boost, gaugeStaked: positionsMap[gauge.address]?.deposit ?? toNormalizedBN(0), - allowance: allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, userAddress)], - isApproved: toBigInt(allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, userAddress)]?.raw) >= toBigInt(balances[vault.address]?.raw), + allowance: allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, toAddress(address))], + isApproved: toBigInt(allowancesMap[allowanceKey(VEYFI_CHAIN_ID, vault.address, gauge.address, toAddress(address))]?.raw) >= toBigInt(balances[vault.address]?.raw), actions: undefined }); } set_isLoadingGauges(false); return data; - }, [gaugesMap, vaults, balances, positionsMap, allowancesMap, userAddress]); + }, [gaugesMap, vaults, balances, positionsMap, allowancesMap, address]); + const searchedGaugesData = useMemo((): TGaugeData[] => { + if (!search) { + return gaugesData; + } + return gaugesData.filter((gauge: TGaugeData): boolean => { + const lowercaseSearch = search.toLowerCase(); + const splitted = + `${gauge.gaugeAddress} ${gauge.vaultName}` + .replaceAll('-', ' ') + .toLowerCase() + .split(' '); + return splitted.some((word): boolean => word.startsWith(lowercaseSearch)); + }); + }, [gaugesData, search]); return ( -
- ( -
-
- +
+
+

+ {'Stake/Unstake'} +

+
+

+ {'To earn rewards deposit into the Yearn Vault you want to vote for, and then stake that Vault token into its gauge below.\n'} + {'e.g yETH into curve-yETH and then stake curve-yETH into its gauge.'} +

+
+
+

{'Search'}

+ +
+
+
+
( +
+
+ +
+

{vaultName}

-

{vaultName}

- - ) - }, - { - key: 'vaultApy', - label: 'Vault APY', - sortable: true, - format: ({vaultApy}): string => formatPercent((vaultApy) * 100, 2, 2, 500) - }, - { - key: 'vaultDeposited', - label: 'Deposited', - sortable: true, - format: ({vaultDeposited}): string => formatAmount(vaultDeposited?.normalized || 0, 2, 6) - }, - { - key: 'gaugeAPR', - label: 'Gauge APR', - columnSpan: 2, - sortable: true, - className: 'whitespace-break text-right', - format: ({gaugeAPR}): string => { - // if (gaugeAPR === 0) { - // return formatAmount(gaugeAPR, 2, 2); - // } - return `${formatAmount(gaugeAPR / 10, 2, 2)}% → ${formatAmount(gaugeAPR, 2, 2)}%`; + ) + }, + { + key: 'vaultApy', + label: 'Vault APY', + sortable: true, + format: ({vaultApy}): string => formatPercent((vaultApy) * 100, 2, 2, 500) + }, + { + key: 'vaultDeposited', + label: 'Deposited in vault', + columnSpan: 2, + className: 'mr-0 md:mr-4', + sortable: true, + isDisabled: ({vaultDeposited}): boolean => toBigInt(vaultDeposited?.raw) === 0n, + format: ({vaultDeposited}): string => formatAmount(vaultDeposited?.normalized || 0, 2, 6) + }, + { + key: 'gaugeAPR', + label: 'Gauge APR', + columnSpan: 2, + sortable: true, + className: 'whitespace-break text-right', + transform: ({gaugeAPR}): ReactElement => ( +
+

+ {`${formatAmount(gaugeAPR / 10, 2, 2)}% → ${formatAmount(gaugeAPR, 2, 2)}%`} +

+
+ ) + }, + { + key: 'gaugeStaked', + label: 'Staked in Gauge', + className: 'mr-0 md:mr-10', + columnSpan: 2, + sortable: true, + isDisabled: ({gaugeStaked}): boolean => toBigInt(gaugeStaked?.raw) === 0n, + format: ({gaugeStaked}): string => formatAmount(gaugeStaked?.normalized || 0, 2, 6) + }, + + { + key: 'gaugeBoost', + label: 'Boost', + className: 'mr-0 md:mr-10', + columnSpan: 1, + sortable: true, + isDisabled: ({gaugeStaked}): boolean => toBigInt(gaugeStaked?.raw) === 0n, + transform: ({gaugeBoost, gaugeStaked}): ReactElement => { + if (toBigInt(gaugeStaked?.raw) === 0n) { + return ( +

{'N/A'}

+ ); + } + return ( +

{`${gaugeBoost.toFixed(2)}x`}

+ ); + } + }, + + { + key: 'actions', + label: '', + columnSpan: 2, + className: 'my-4 md:my-0', + fullWidth: true, + transform: (props): ReactElement => { + if (toBigInt(props?.vaultDeposited?.raw) === 0n) { + return ( + + + + ); + } + return ; + } } - }, - { - key: 'gaugeBoost', - label: 'Boost', - sortable: true, - format: ({gaugeBoost}): string => `${gaugeBoost.toFixed(2)}x` - }, - { - key: 'gaugeStaked', - label: 'Staked', - sortable: true, - className: 'mr-4', - format: ({gaugeStaked}): string => formatAmount(gaugeStaked?.normalized || 0, 2, 6) - }, - { - key: 'actions', - label: '', - columnSpan: 2, - fullWidth: true, - className: 'my-4 md:my-0', - transform: (props): ReactElement => - } - ]} - isLoading={isLoadingGauges} - data={gaugesData} - columns={11} - initialSortBy={'gaugeAPR'} - /> + ]} + isLoading={isLoadingGauges} + data={searchedGaugesData} + columns={13} + initialSortBy={'gaugeAPR'} + /> + + + + ); +} + +function Vote(): ReactElement { + return ( +
+
+
+

+ {'Vote for Gauge'} +

+
+

{'Vote to direct future YFI rewards to a particular gauge.'}

+
+
+ + + +
+
+
+
+ ); +} + + +export function GaugesTab(): ReactElement { + return ( +
+ +
+
+ + + +
); } diff --git a/apps/veyfi/components/ManageLockTab.tsx b/apps/veyfi/components/ManageLockTab.tsx index 9bcbcaff6..3dac07a4f 100644 --- a/apps/veyfi/components/ManageLockTab.tsx +++ b/apps/veyfi/components/ManageLockTab.tsx @@ -13,10 +13,14 @@ import {defaultTxStatus} from '@yearn-finance/web-lib/utils/web3/transaction'; import {AmountInput} from '@common/components/AmountInput'; import {useWallet} from '@common/contexts/useWallet'; +import {ClaimTab} from './ClaimTab'; +import {LockTab} from './LockTab'; + import type {ReactElement} from 'react'; import type {TNormalizedBN} from '@common/types/types'; -export function ManageLockTab(): ReactElement { + +function ExtendLock(): ReactElement { const [lockTime, set_lockTime] = useState(toNormalizedBN(0, 0)); const {provider, address, isActive} = useWeb3(); const {refresh: refreshBalances} = useWallet(); @@ -26,9 +30,7 @@ export function ManageLockTab(): ReactElement { const timeUntilUnlock = positions?.unlockTime ? getTimeUntil(positions?.unlockTime) : undefined; const weeksToUnlock = toNormalizedBN(toWeeks(timeUntilUnlock), 0); const newUnlockTime = toTime(positions?.unlockTime) + fromWeeks(toTime(lockTime.normalized)); - const hasPenalty = toBigInt(positions?.penalty) > 0n; const [extendLockTimeStatus, set_extendLockTimeStatus] = useState(defaultTxStatus); - const [withdrawLockedStatus, set_withdrawLockedStatus] = useState(defaultTxStatus); const onTxSuccess = useCallback(async (): Promise => { await Promise.all([refreshVotingEscrow(), refreshBalances(), set_lockTime(toNormalizedBN(0, 0))]); @@ -47,18 +49,6 @@ export function ManageLockTab(): ReactElement { } }, [newUnlockTime, onTxSuccess, provider, votingEscrow?.address]); - const onWithdrawLocked = useCallback(async (): Promise => { - const result = await withdrawLockedVeYFI({ - connector: provider, - chainID: VEYFI_CHAIN_ID, - contractAddress: votingEscrow?.address, - statusHandler: set_withdrawLockedStatus - }); - if (result.isSuccessful) { - onTxSuccess(); - } - }, [onTxSuccess, provider, votingEscrow?.address]); - const votingPower = useMemo((): TNormalizedBN => { if(!positions?.deposit || !newUnlockTime) { return toNormalizedBN(0); @@ -74,16 +64,17 @@ export function ManageLockTab(): ReactElement { const maxTime = MAX_LOCK_TIME - Number(weeksToUnlock?.normalized || 0) > 0 ? MAX_LOCK_TIME - Number(weeksToUnlock?.normalized || 0) : 0; return (
-
-
-

- {'Extend lock'} -

-
-

{'Want to lock for longer? Extend your lock period to increase your gauge boost weight.'}

-
+
+

+ {'Extend lock'} +

+
+

{'Want to lock for longer? Extend your lock period to increase your gauge boost weight.'}

-
+
+ +
+
+
+ ); +} -
-
-

- {'Early exit'} -

-
-

{'Or you can exit early by paying a penalty based on lock duration.'}

-
+function EarlyExit(): ReactElement { + const {provider, address, isActive} = useWeb3(); + const {refresh: refreshBalances} = useWallet(); + const {votingEscrow, positions, refresh: refreshVotingEscrow} = useVotingEscrow(); + const timeUntilUnlock = positions?.unlockTime ? getTimeUntil(positions?.unlockTime) : undefined; + const weeksToUnlock = toNormalizedBN(toWeeks(timeUntilUnlock), 0); + const hasPenalty = toBigInt(positions?.penalty) > 0n; + const [withdrawLockedStatus, set_withdrawLockedStatus] = useState(defaultTxStatus); + + const onTxSuccess = useCallback(async (): Promise => { + await Promise.all([refreshVotingEscrow(), refreshBalances()]); + }, [refreshBalances, refreshVotingEscrow]); + + const onWithdrawLocked = useCallback(async (): Promise => { + const result = await withdrawLockedVeYFI({ + connector: provider, + chainID: VEYFI_CHAIN_ID, + contractAddress: votingEscrow?.address, + statusHandler: set_withdrawLockedStatus + }); + if (result.isSuccessful) { + onTxSuccess(); + } + }, [onTxSuccess, provider, votingEscrow?.address]); + + return ( +
+
+

+ {'Early exit'} +

+
+

{'Or you can exit early by paying a penalty based on lock duration.'}

-
+
+ +
+
); } + +export function ManageLockTab(): ReactElement { + const {positions} = useVotingEscrow(); + const hasLock = toNormalizedBN(toBigInt(positions?.deposit?.underlyingBalance), 18); + const timeUntilUnlock = positions?.unlockTime ? getTimeUntil(positions?.unlockTime) : undefined; + const weeksToUnlock = toWeeks(timeUntilUnlock); + + return ( +
+ + {(hasLock && weeksToUnlock > 0) ? ( + <> +
+ +
+ +
+ + + ) : null} +
+ ); +} diff --git a/apps/veyfi/components/RedeemTab.tsx b/apps/veyfi/components/RedeemTab.tsx index 0028e6cca..ab5fac34a 100644 --- a/apps/veyfi/components/RedeemTab.tsx +++ b/apps/veyfi/components/RedeemTab.tsx @@ -116,7 +116,7 @@ export function RedeemTab(): ReactElement { error={redeemAmountError} /> void; +}; +function useQueryArguments(): TQueryArgs { + const [searchParam, set_searchParam] = useQueryParam('search', StringParam); + const [search, set_search] = useState(searchParam); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the search state with the query parameter + *********************************************************************************************/ + useEffect((): void => { + if (searchParam === search) { + return; + } + if (search === undefined && searchParam !== undefined) { + set_search(searchParam); + return; + } + if (!search) { + set_searchParam(undefined); + } else { + set_searchParam(search); + } + }, [searchParam, search, set_searchParam]); + + return { + search, + onSearch: set_search + }; +} + +export {useQueryArguments}; diff --git a/package.json b/package.json index f3b6acafb..cc40f3bd6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "recharts": "^2.8.0", "swr": "2.1.5", "telegraf": "^4.13.1", + "use-query-params": "^2.2.1", "viem": "^1.16.5", "vite": "^4.2.0", "wagmi": "^1.4.3", diff --git a/pages/_app.tsx b/pages/_app.tsx index 812c82423..2512ed6c8 100755 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -38,6 +38,10 @@ const aeonik = localFont({ path: '../public/fonts/Aeonik-Bold.woff2', weight: '700', style: 'normal' + }, { + path: '../public/fonts/Aeonik-Black.ttf', + weight: '900', + style: 'normal' } ] }); diff --git a/pages/veyfi/index.tsx b/pages/veyfi/index.tsx index 574759d7c..025e80f1a 100755 --- a/pages/veyfi/index.tsx +++ b/pages/veyfi/index.tsx @@ -1,10 +1,7 @@ -import {ClaimTab} from '@veYFI/components/ClaimTab'; import {GaugesTab} from '@veYFI/components/GaugesTab'; -import {LockTab} from '@veYFI/components/LockTab'; import {ManageLockTab} from '@veYFI/components/ManageLockTab'; import {RedeemTab} from '@veYFI/components/RedeemTab'; import {RewardsTab} from '@veYFI/components/RewardsTab'; -import {VoteTab} from '@veYFI/components/VoteTab'; import {useVotingEscrow} from '@veYFI/contexts/useVotingEscrow'; import {Wrapper} from '@veYFI/Wrapper'; import {formatToNormalizedValue, toBigInt} from '@yearn-finance/web-lib/utils/format.bigNumber'; @@ -24,13 +21,12 @@ function Index(): ReactElement { const yourLockedYFI = formatToNormalizedValue(toBigInt(positions?.deposit?.underlyingBalance), 18); const tabs = [ - {id: 'lock', label: 'Lock YFI', content: }, - {id: 'manage', label: 'Manage lock', content: }, - {id: 'claim', label: 'Claim', content: }, - {id: 'gauges', label: 'Stake / Unstake', content: }, + // {id: 'lock', label: 'Lock YFI', content: }, + {id: 'manage', label: 'Manage veYFI', content: }, + // {id: 'claim', label: 'Claim', content: }, + {id: 'gauges', label: 'Manage Gauges', content: }, {id: 'rewards', label: 'Rewards', content: }, - {id: 'redeem', label: 'Redeem dYFI', content: }, - {id: 'vote', label: 'Vote for Gauge', content: } + {id: 'redeem', label: 'Redeem dYFI', content: } ].filter(Boolean); return ( diff --git a/public/fonts/Aeonik-Black.ttf b/public/fonts/Aeonik-Black.ttf new file mode 100644 index 000000000..b4914cb88 Binary files /dev/null and b/public/fonts/Aeonik-Black.ttf differ diff --git a/tailwind.config.js b/tailwind.config.js index e3c5a845b..0b00a6dbf 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -40,7 +40,9 @@ module.exports = { }, gridTemplateColumns: { '13': 'repeat(13, minmax(0, 1fr))', - '14': 'repeat(14, minmax(0, 1fr))' + '14': 'repeat(14, minmax(0, 1fr))', + '20': 'repeat(20, minmax(0, 1fr))', + '30': 'repeat(30, minmax(0, 1fr))' }, fontSize: { 'xxs': ['10px', '16px'], diff --git a/yarn.lock b/yarn.lock index e5ff280d7..d616de020 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8092,6 +8092,11 @@ serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" +serialize-query-params@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/serialize-query-params/-/serialize-query-params-2.0.2.tgz#598a3fb9e13f4ea1c1992fbd20231aa16b31db81" + integrity sha512-1chMo1dST4pFA9RDXAtF0Rbjaut4is7bzFbI1Z26IuMub68pNCILku85aYmeFhvnY//BXUPUhoRMjYcsT93J/Q== + set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" @@ -8992,6 +8997,13 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-query-params@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/use-query-params/-/use-query-params-2.2.1.tgz#c558ab70706f319112fbccabf6867b9f904e947d" + integrity sha512-i6alcyLB8w9i3ZK3caNftdb+UnbfBRNPDnc89CNQWkGRmDrm/gfydHvMBfVsQJRq3NoHOM2dt/ceBWG2397v1Q== + dependencies: + serialize-query-params "^2.0.2" + use-sidecar@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"