diff --git a/api/resolvers/blockHeight.js b/api/resolvers/blockHeight.js new file mode 100644 index 00000000..4fd270d9 --- /dev/null +++ b/api/resolvers/blockHeight.js @@ -0,0 +1,37 @@ +import lndService from 'ln-service' +import lnd from '../lnd' + +const cache = new Map() +const expiresIn = 1000 * 30 // 30 seconds in milliseconds + +async function fetchBlockHeight () { + let blockHeight = 0 + try { + const height = await lndService.getHeight({ lnd }) + blockHeight = height.current_block_height + } catch (err) { + console.error('fetchBlockHeight', err) + } + cache.set('block', { height: blockHeight, createdAt: Date.now() }) + return blockHeight +} + +async function getBlockHeight () { + if (cache.has('block')) { + const { height, createdAt } = cache.get('block') + const expired = createdAt + expiresIn < Date.now() + if (expired) fetchBlockHeight().catch(console.error) // update cache + return height // serve stale block height (this on the SSR critical path) + } else { + fetchBlockHeight().catch(console.error) + } + return 0 +} + +export default { + Query: { + blockHeight: async (parent, opts, ctx) => { + return await getBlockHeight() + } + } +} diff --git a/api/resolvers/index.js b/api/resolvers/index.js index aaf74143..f0b311ac 100644 --- a/api/resolvers/index.js +++ b/api/resolvers/index.js @@ -14,6 +14,7 @@ import referrals from './referrals' import price from './price' import { GraphQLJSONObject as JSONObject } from 'graphql-type-json' import admin from './admin' +import blockHeight from './blockHeight' import { GraphQLScalarType, Kind } from 'graphql' const date = new GraphQLScalarType({ @@ -44,4 +45,4 @@ const date = new GraphQLScalarType({ }) export default [user, item, message, wallet, lnurl, notifications, invite, sub, - upload, search, growth, rewards, referrals, price, admin, { JSONObject }, { Date: date }] + upload, search, growth, rewards, referrals, price, admin, blockHeight, { JSONObject }, { Date: date }] diff --git a/api/ssrApollo.js b/api/ssrApollo.js index af3a1dd0..74b55523 100644 --- a/api/ssrApollo.js +++ b/api/ssrApollo.js @@ -9,6 +9,7 @@ import lnd from './lnd' import search from './search' import { ME } from '../fragments/users' import { PRICE } from '../fragments/price' +import { BLOCK_HEIGHT } from '../fragments/blockHeight' import { getServerSession } from 'next-auth/next' import { getAuthOptions } from '../pages/api/auth/[...nextauth]' @@ -92,6 +93,10 @@ export function getGetServerSideProps ( query: PRICE, variables: { fiatCurrency: me?.fiatCurrency } }) + const { data: { blockHeight } } = await client.query({ + query: BLOCK_HEIGHT, variables: {} + }) + let error = null; let data = null; let props = {} if (query) { try { @@ -122,6 +127,7 @@ export function getGetServerSideProps ( ...props, me, price, + blockHeight, ssrData: data } } diff --git a/api/typeDefs/blockHeight.js b/api/typeDefs/blockHeight.js new file mode 100644 index 00000000..3ccebbff --- /dev/null +++ b/api/typeDefs/blockHeight.js @@ -0,0 +1,7 @@ +import { gql } from 'graphql-tag' + +export default gql` + extend type Query { + blockHeight: Int! + } +` diff --git a/api/typeDefs/index.js b/api/typeDefs/index.js index 65451e7e..fb7b39d2 100644 --- a/api/typeDefs/index.js +++ b/api/typeDefs/index.js @@ -15,6 +15,7 @@ import rewards from './rewards' import referrals from './referrals' import price from './price' import admin from './admin' +import blockHeight from './blockHeight' const common = gql` type Query { @@ -34,4 +35,4 @@ const common = gql` ` export default [common, user, item, itemForward, message, wallet, lnurl, notifications, invite, - sub, upload, growth, rewards, referrals, price, admin] + sub, upload, growth, rewards, referrals, price, admin, blockHeight] diff --git a/components/block-height.js b/components/block-height.js new file mode 100644 index 00000000..6007ac98 --- /dev/null +++ b/components/block-height.js @@ -0,0 +1,29 @@ +import { createContext, useContext } from 'react' +import { useQuery } from '@apollo/client' +import { SSR } from '../lib/constants' +import { BLOCK_HEIGHT } from '../fragments/blockHeight' + +export const BlockHeightContext = createContext({ + height: 0 +}) + +export const useBlockHeight = () => useContext(BlockHeightContext) + +export const BlockHeightProvider = ({ blockHeight, children }) => { + const { data } = useQuery(BLOCK_HEIGHT, { + ...(SSR + ? {} + : { + pollInterval: 30000, + nextFetchPolicy: 'cache-and-network' + }) + }) + const value = { + height: data?.blockHeight ?? blockHeight ?? 0 + } + return ( + + {children} + + ) +} diff --git a/components/price.js b/components/price.js index 4a36494c..acc7cfdf 100644 --- a/components/price.js +++ b/components/price.js @@ -5,6 +5,7 @@ import { useMe } from './me' import { PRICE } from '../fragments/price' import { CURRENCY_SYMBOLS } from '../lib/currency' import { SSR } from '../lib/constants' +import { useBlockHeight } from './block-height' export const PriceContext = React.createContext({ price: null, @@ -46,14 +47,20 @@ export default function Price ({ className }) { setAsSats(window.localStorage.getItem('asSats')) }, []) const { price, fiatSymbol } = usePrice() + const { height: blockHeight } = useBlockHeight() - if (!price || price < 0) return null + if (!price || price < 0 || blockHeight <= 0) return null + // Options: yep, 1btc, blockHeight, undefined + // yep -> 1btc -> blockHeight -> undefined -> yep const handleClick = () => { if (asSats === 'yep') { window.localStorage.setItem('asSats', '1btc') setAsSats('1btc') } else if (asSats === '1btc') { + window.localStorage.setItem('asSats', 'blockHeight') + setAsSats('blockHeight') + } else if (asSats === 'blockHeight') { window.localStorage.removeItem('asSats') setAsSats(undefined) } else { @@ -80,6 +87,14 @@ export default function Price ({ className }) { ) } + if (asSats === 'blockHeight') { + return ( +
+ {`block ${blockHeight}`} +
+ ) + } + return (
{fiatSymbol + fixedDecimal(price, 0)} diff --git a/fragments/blockHeight.js b/fragments/blockHeight.js new file mode 100644 index 00000000..5f32fe2a --- /dev/null +++ b/fragments/blockHeight.js @@ -0,0 +1,3 @@ +import { gql } from '@apollo/client' + +export const BLOCK_HEIGHT = gql`{ blockHeight }` diff --git a/pages/_app.js b/pages/_app.js index 5b912cee..3f1bec75 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -4,6 +4,7 @@ import { MeProvider } from '../components/me' import PlausibleProvider from 'next-plausible' import getApolloClient from '../lib/apollo' import { PriceProvider } from '../components/price' +import { BlockHeightProvider } from '../components/block-height' import Head from 'next/head' import { useRouter } from 'next/dist/client/router' import { useEffect } from 'react' @@ -73,7 +74,7 @@ function MyApp ({ Component, pageProps: { ...props } }) { If we are on the client, we populate the apollo cache with the ssr data */ - const { apollo, ssrData, me, price, ...otherProps } = props + const { apollo, ssrData, me, price, blockHeight, ...otherProps } = props useEffect(() => { writeQuery(client, apollo, ssrData) }, [client, apollo, ssrData]) @@ -92,7 +93,9 @@ function MyApp ({ Component, pageProps: { ...props } }) { - + + +