Skip to content

Commit

Permalink
Merge pull request #20 from daimo-eth/klee/link-preview
Browse files Browse the repository at this point in the history
[feat] link previews
  • Loading branch information
kayleegeorge authored Jun 5, 2024
2 parents 16662bb + 4241a26 commit 9ca5c35
Show file tree
Hide file tree
Showing 13 changed files with 600 additions and 30 deletions.
2 changes: 1 addition & 1 deletion app/api/[chainId]/[blockNumber]/[logIndex]/apiGetLog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export async function apiGetLog(params: {
...details,
};
console.log(`[API] loaded log ${chainId}/${blockNumber}/${logIndex}: ${JSON.stringify(ret)}`);
return Response.json(ret);
return new Response(JSON.stringify(ret));
}

async function fetchEventLogFromViem(
Expand Down
178 changes: 178 additions & 0 deletions app/components/image-gen/LinkPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/* eslint-disable @next/next/no-img-element */
import { formatValue, getDateDifference, truncateAddress } from '@/app/utils/formatting';
import { getAbsoluteUrl } from '@/app/utils/getAbsoluteUrl';
import stablecoinsAddresses from '@/app/utils/tokens/stablecoins';
import { AddressProfile, EventLog, Transfer } from '@/app/utils/types';
import { UserBubble } from './UserBubble';

export function LinkPreviewImg({
transferData,
addressProfileFrom,
addressProfileTo,
eventLogData,
latestFinalizedBlockNumber,
}: {
transferData: Transfer;
addressProfileFrom: AddressProfile;
addressProfileTo: AddressProfile;
eventLogData: EventLog;
latestFinalizedBlockNumber: number;
}) {
return (
<div
style={{
backgroundColor: '#F3F3F3',
height: '100%',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '40px',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '8px',
backgroundColor: 'white',
borderRadius: '24px',
paddingTop: '40px',
paddingBottom: '20px',
border: '1px solid #EEEEEE',
}}
>
<Content
transferData={transferData}
addressProfileFrom={addressProfileFrom}
addressProfileTo={addressProfileTo}
/>
<Footer
eventLogData={eventLogData}
latestFinalizedBlockNumber={latestFinalizedBlockNumber}
/>
</div>
<div style={{ display: 'flex' }}>
<img
src={`${getAbsoluteUrl('/assets/eth-receipts-one-liner.png')}`}
alt={'Profile'}
width={'20%'}
></img>
</div>
</div>
);
}

function Content({
transferData,
addressProfileFrom,
addressProfileTo,
}: {
transferData: Transfer;
addressProfileFrom: AddressProfile;
addressProfileTo: AddressProfile;
}) {
// Format token value
const { tokenSymbol, tokenDecimal, value: tokenValue } = transferData;
const value = formatValue(Number(tokenValue) / Number(10 ** Number(tokenDecimal)));

const isStablecoin = stablecoinsAddresses.includes(transferData.contractAddress);
const amountStr = `${isStablecoin ? '$' : ''}${value}`;

const memo = transferData.memo ?? '';

return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: '10px',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
}}
>
<span style={{ fontSize: '54px', fontWeight: 'bold' }}>
{amountStr} {tokenSymbol}
</span>
<span style={{ fontSize: '24px', color: '#AAAAAA' }}>{memo}</span>
</div>

<div
style={{
display: 'flex',
flexDirection: 'row',
marginTop: '20px',
marginBottom: '12px',
width: '100%',
borderBottom: '1px solid #EEEEEE',
borderTop: '1px solid #EEEEEE',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '20px',
padding: '30px 50px',
}}
>
<span style={{ fontSize: '18px', color: '#777777' }}>FROM</span>
<UserBubble addressProfile={addressProfileFrom} />
</div>
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '20px',
padding: '30px 50px',
borderLeft: '1px solid #EEEEEE',
}}
>
<span style={{ fontSize: '18px', color: '#777777' }}>TO</span>
<UserBubble addressProfile={addressProfileTo} />
</div>
</div>
</div>
);
}

function Footer({
eventLogData,
latestFinalizedBlockNumber,
}: {
eventLogData: EventLog;
latestFinalizedBlockNumber: number;
}) {
const time = new Date(Number(eventLogData.timestamp) * 1000);
const dateDifferenceStr = getDateDifference(time);

const chain = eventLogData.chainName;
const chainName = chain[0].toUpperCase() + chain.slice(1);

return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
justifyItems: 'center',
color: '#AAAAAA',
fontSize: '18px',
fontWeight: 'lighter',
}}
>
{dateDifferenceStr} on {chainName}
</div>
);
}
63 changes: 63 additions & 0 deletions app/components/image-gen/UserBubble.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable @next/next/no-img-element */
import { truncateAddress } from '@/app/utils/formatting';
import { AddressProfile } from '@/app/utils/types';

export function UserBubble({ addressProfile }: { addressProfile: AddressProfile }) {
const address = truncateAddress(addressProfile.accountAddress);
const name = addressProfile.account?.name ?? undefined;

return (
<div
style={{
display: 'flex',
gap: '20px',
alignItems: 'center',
justifyContent: 'center',
}}
>
<UserProfilePicture name={name} />
<div
style={{
display: 'flex',
flexDirection: 'column',
gap: '2px',
}}
>
{name && <div style={{ color: '#323232', fontSize: '28px' }}>{name}</div>}
{<div style={{ color: '#AAAAAA', fontWeight: 'lighter', fontSize: '20px' }}>{address}</div>}
</div>
</div>
);
}

function UserProfilePicture({ name }: { name: string | undefined }) {
const nameInitial = name ? name[0].toUpperCase() : '0x';

return (
<div
style={{
display: 'flex',
width: '84px',
height: '84px',
borderRadius: '9999px',
backgroundColor: 'white',
border: '0.5px solid #535353',
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
}}
>
<div
style={{
color: '#535353',
fontSize: '40px',
fontWeight: 'bold',
textAlign: 'center',
lineHeight: '100px',
}}
>
{nameInitial}
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions app/components/shared/Wiggle.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useMemo } from 'react';
export const wiggleSvgDataUri =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzkiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDM5IDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xNDJfNSkiPgo8cGF0aCBkPSJNMCA2TDUgMUwxMCA2TDE1IDFMMTkuNSA2TDI0IDFMMjkgNkwzNCAxTDM5IDYiIHN0cm9rZT0iI0YzRjNGMyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzE0Ml81Ij4KPHJlY3Qgd2lkdGg9IjM5IiBoZWlnaHQ9IjciIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==';

export function Wiggle() {
const wiggleSvgDataUri =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzkiIGhlaWdodD0iNyIgdmlld0JveD0iMCAwIDM5IDciIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xNDJfNSkiPgo8cGF0aCBkPSJNMCA2TDUgMUwxMCA2TDE1IDFMMTkuNSA2TDI0IDFMMjkgNkwzNCAxTDM5IDYiIHN0cm9rZT0iI0YzRjNGMyIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L2c+CjxkZWZzPgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzE0Ml81Ij4KPHJlY3Qgd2lkdGg9IjM5IiBoZWlnaHQ9IjciIGZpbGw9IndoaXRlIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==';
const style = useMemo(
() => ({
width: '100%',
Expand Down
32 changes: 14 additions & 18 deletions app/l/[chainId]/[blockNumber]/[logIndex]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,13 @@
import { apiGetLog } from '@/app/api/[chainId]/[blockNumber]/[logIndex]/apiGetLog';
import ERC20TransferSection from '@/app/components/logs/ERC20TransferSection';
import EventLogSection from '@/app/components/logs/EventLogSection';
import UnsupportedLogSection from '@/app/components/logs/UnsupportedLogSection';
import { Wiggle } from '@/app/components/shared/Wiggle';
import { getLogData, LogData } from '@/app/utils/getLogData';
import { createMetadataForTransfer } from '@/app/utils/linkMetaTags';
import { Metadata } from 'next';

/**
* Fetch log data from API.
*
* @param {string} chainId - The chain ID.
* @param {string} blockNumber - The block number.
* @param {string} logIndex - The log index.
* @returns {Object} The result from API fetch.
*/
async function getLogData(chainId: string, blockNumber: string, logIndex: string) {
// Revalidate every 10 minutes.
const res = await apiGetLog({ chainId, blockNumber, logIndex });
if (!res.ok) {
console.error('Failed to fetch log', res.status);
return null;
}
return res.json();
interface LinkProps {
params: { chainId: string; blockNumber: string; logIndex: string };
}

/**
Expand All @@ -37,7 +25,7 @@ export default async function Page({
params: { chainId: string; blockNumber: string; logIndex: string };
}) {
console.log(`[LOG PAGE] chainId: ${chainId}, blockNumber: ${blockNumber}, logIndex: ${logIndex}`);
const logData = await getLogData(chainId, blockNumber, logIndex);
const logData: LogData = await getLogData(chainId, blockNumber, logIndex);

return (
<div className='flex flex-col m-auto px-8'>
Expand Down Expand Up @@ -65,3 +53,11 @@ export default async function Page({
</div>
);
}

// Generate metadata for a transfer log.
export async function generateMetadata(props: LinkProps): Promise<Metadata> {
const { chainId, blockNumber, logIndex } = props.params;
const logData: LogData = await getLogData(chainId, blockNumber, logIndex);
const metadata = createMetadataForTransfer(logData);
return metadata;
}
47 changes: 47 additions & 0 deletions app/preview/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ImageResponse } from '@vercel/og';
import { LinkPreviewImg } from '../components/image-gen/LinkPreview';
import { getLogData } from '../utils/getLogData';

export const runtime = 'edge';

// Generate link preview image.
// Note: tailwind CSS is not supported in Vercel previews.
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);

// Transfer preview parameters.
if (
!searchParams.has('chainId') ||
!searchParams.has('blockNumber') ||
!searchParams.has('logIndex')
) {
throw new Error('Invalid preview parameters');
}
const chainId = searchParams.get('chainId')!;
const blockNumber = searchParams.get('blockNumber')!;
const logIndex = searchParams.get('logIndex')!;
console.log(
`[PREVIEW] generating preview for transfer (chainId: ${chainId}, blockNumber: ${blockNumber}, logIndex: ${logIndex})`,
);

const logData = await getLogData(chainId, blockNumber, logIndex);
if (!logData) {
console.log('Transfer log not found');
}

return new ImageResponse(
(
<LinkPreviewImg
transferData={logData.transferData}
addressProfileFrom={logData.fromAddressProfile}
addressProfileTo={logData.toAddressProfile}
eventLogData={logData.eventLogData}
latestFinalizedBlockNumber={logData.latestFinalizedBlockNumber}
/>
),
{
width: 1200,
height: 630,
},
);
}
16 changes: 16 additions & 0 deletions app/utils/getAbsoluteUrl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const publicUrl = (function () {
if (process.env.NEXT_PUBLIC_URL) {
return process.env.NEXT_PUBLIC_URL;
} else if (process.env.VERCEL_URL) {
return `https://${process.env.VERCEL_URL}`;
} else {
return 'https://ethreceipts.org';
}
})();

export function getAbsoluteUrl(path: string) {
if (!path.startsWith('/')) {
path = `/${path}`;
}
return `${publicUrl}${path}`;
}
Loading

0 comments on commit 9ca5c35

Please sign in to comment.