diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..e220dc7
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,5 @@
+{
+ "editor.lightbulb.enabled": "onCode",
+ "editor.experimental.treeSitterTelemetry": false,
+ "editor.experimentalInlineEdit.enabled": true
+}
\ No newline at end of file
diff --git a/app/account/[address]/page.tsx b/app/account/[address]/page.tsx
index 95413f8..0f15ea6 100644
--- a/app/account/[address]/page.tsx
+++ b/app/account/[address]/page.tsx
@@ -8,9 +8,11 @@ import type { TransactionInfo } from '@/lib/solana';
interface AccountInfo {
address: string;
- balance: number;
- executable: boolean;
+ lamports: number;
owner: string;
+ executable: boolean;
+ rentEpoch: number;
+ data: Buffer;
}
export default function AccountPage() {
@@ -88,7 +90,7 @@ export default function AccountPage() {
Balance
- {accountInfo.balance.toFixed(9)} SOL
+ {(accountInfo.lamports / 1e9).toFixed(9)} SOL
@@ -117,8 +119,8 @@ export default function AccountPage() {
{tx.signature}
- Amount
- {tx.amount.toFixed(9)} SOL
+ Fee
+ {tx.fee.toFixed(9)} SOL
diff --git a/app/address/[address]/opengraph-image.tsx b/app/address/[address]/opengraph-image.tsx
index 6de7750..d7ab15c 100644
--- a/app/address/[address]/opengraph-image.tsx
+++ b/app/address/[address]/opengraph-image.tsx
@@ -15,7 +15,7 @@ export default async function Image({ params }: { params: { address: string } })
const title = 'Account Overview';
const description = account
- ? `Balance: ${account.balance.toFixed(4)} SOL`
+ ? `Balance: ${(account.lamports / 1e9).toFixed(4)} SOL`
: 'Solana Account Explorer';
return new ImageResponse(
diff --git a/app/api/block/route.ts b/app/api/block/route.ts
new file mode 100644
index 0000000..8ec9bcd
--- /dev/null
+++ b/app/api/block/route.ts
@@ -0,0 +1,44 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { connection } from '@/lib/solana';
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const slot = searchParams.get('slot');
+
+ if (!slot) {
+ return NextResponse.json(
+ { error: 'Slot parameter is required' },
+ { status: 400 }
+ );
+ }
+
+ try {
+ const slotNumber = parseInt(slot);
+ if (isNaN(slotNumber)) {
+ return NextResponse.json(
+ { error: 'Invalid slot number' },
+ { status: 400 }
+ );
+ }
+
+ const [block, blockTime] = await Promise.all([
+ connection.getBlock(slotNumber, { maxSupportedTransactionVersion: 0 }),
+ connection.getBlockTime(slotNumber),
+ ]);
+
+ if (!block) {
+ return NextResponse.json(
+ { error: 'Block not found' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({ block, blockTime });
+ } catch (error) {
+ console.error('Error fetching block:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch block data' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/api/blocks/[slot]/route.ts b/app/api/blocks/[slot]/route.ts
new file mode 100644
index 0000000..fbc8202
--- /dev/null
+++ b/app/api/blocks/[slot]/route.ts
@@ -0,0 +1,42 @@
+import { NextResponse } from 'next/server';
+import { connection } from '@/lib/solana';
+
+export async function GET(request: Request) {
+ try {
+ const slot = request.url.split('/').pop();
+ if (!slot) {
+ return NextResponse.json(
+ { error: 'Slot parameter is required' },
+ { status: 400 }
+ );
+ }
+
+ const slotNumber = parseInt(slot);
+ if (isNaN(slotNumber)) {
+ return NextResponse.json(
+ { error: 'Invalid slot number' },
+ { status: 400 }
+ );
+ }
+
+ const [block, blockTime] = await Promise.all([
+ connection.getBlock(slotNumber, { maxSupportedTransactionVersion: 0 }),
+ connection.getBlockTime(slotNumber),
+ ]);
+
+ if (!block) {
+ return NextResponse.json(
+ { error: 'Block not found' },
+ { status: 404 }
+ );
+ }
+
+ return NextResponse.json({ block, blockTime });
+ } catch (error) {
+ console.error('Error fetching block data:', error);
+ return NextResponse.json(
+ { error: 'Failed to fetch block data' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/block/[slot]/metadata.ts b/app/block/[slot]/metadata.ts
new file mode 100644
index 0000000..b973486
--- /dev/null
+++ b/app/block/[slot]/metadata.ts
@@ -0,0 +1,12 @@
+import { Metadata } from 'next';
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { slot: string };
+}): Promise {
+ return {
+ title: `Block #${params.slot} | OPENSVM`,
+ description: `View details of Solana block #${params.slot} on OPENSVM`,
+ };
+}
\ No newline at end of file
diff --git a/app/block/[slot]/opengraph-image.tsx b/app/block/[slot]/opengraph-image.tsx
new file mode 100644
index 0000000..3fb5a13
--- /dev/null
+++ b/app/block/[slot]/opengraph-image.tsx
@@ -0,0 +1,170 @@
+import { ImageResponse } from 'next/og';
+import { connection } from '@/lib/solana';
+import { formatNumber } from '@/lib/utils';
+
+export const runtime = 'edge';
+export const alt = 'Block Details';
+export const size = {
+ width: 1200,
+ height: 630,
+};
+export const contentType = 'image/png';
+
+export default async function Image({ params }: { params: { slot: string } }) {
+ try {
+ const slotNumber = parseInt(params.slot);
+ const [block, blockTime] = await Promise.all([
+ connection.getBlock(slotNumber, { maxSupportedTransactionVersion: 0 }),
+ connection.getBlockTime(slotNumber),
+ ]);
+
+ if (!block) {
+ throw new Error('Block not found');
+ }
+
+ const totalRewards = block.rewards.reduce((acc, r) => acc + r.lamports, 0) / 1e9;
+
+ return new ImageResponse(
+ (
+
+ {/* Logo */}
+
+
+ {/* Content */}
+
+
+ Block #{formatNumber(slotNumber)}
+
+
+ {blockTime ? new Date(blockTime * 1000).toLocaleString() : 'Unknown time'}
+
+
+ {formatNumber(block.transactions.length)} Transactions • {formatNumber(totalRewards)} SOL in Rewards
+
+
+ Parent Slot: {formatNumber(block.parentSlot)}
+
+
+
+ {/* Footer */}
+
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ },
+ );
+ } catch (e: any) {
+ console.log(`${e.message}`);
+ return new Response(`Failed to generate the image`, {
+ status: 500,
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/block/[slot]/page.tsx b/app/block/[slot]/page.tsx
new file mode 100644
index 0000000..2227007
--- /dev/null
+++ b/app/block/[slot]/page.tsx
@@ -0,0 +1,23 @@
+import { Metadata } from 'next';
+import BlockDetails from '@/components/BlockDetails';
+
+interface PageProps {
+ params: Promise<{ slot: string }>;
+}
+
+export async function generateMetadata({
+ params,
+}: PageProps): Promise {
+ const resolvedParams = await params;
+ return {
+ title: `Block #${resolvedParams.slot} | OPENSVM`,
+ description: `View details of Solana block #${resolvedParams.slot} on OPENSVM`,
+ };
+}
+
+export default async function BlockPage({
+ params,
+}: PageProps) {
+ const resolvedParams = await params;
+ return ;
+}
\ No newline at end of file
diff --git a/app/block/layout.tsx b/app/block/layout.tsx
new file mode 100644
index 0000000..95e9ca2
--- /dev/null
+++ b/app/block/layout.tsx
@@ -0,0 +1,18 @@
+import { Metadata } from 'next';
+
+export const metadata: Metadata = {
+ title: 'Block Details | OPENSVM',
+ description: 'View Solana block details on OPENSVM',
+};
+
+export default function BlockLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file
diff --git a/app/page.tsx b/app/page.tsx
index aa782f2..95b4b3c 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -3,83 +3,12 @@
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Text, Button } from 'rinlab';
-import RecentTransactions from '@/components/RecentTransactions';
-
-export default function HomePage() {
- const [searchQuery, setSearchQuery] = useState('');
- const router = useRouter();
-
- const handleSearch = () => {
- if (searchQuery) {
- router.push(`/account/${searchQuery}`);
- }
- };
-
- const handleKeyPress = (e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- handleSearch();
- }
- };
+import RecentBlocks from '@/components/RecentBlocks';
+export default function Home() {
return (
-
-
-
-
-
-
- Solana Block Explorer
-
-
- Search for any Solana address, transaction, token, or NFT
-
-
-
- setSearchQuery(e.target.value)}
- onKeyPress={handleKeyPress}
- placeholder="Search transactions, blocks, programs and tokens"
- className="flex-1 px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-[#00ffbd] focus:border-transparent"
- />
-
-
-
-
-
-
-
-
-
-
+
+
+
);
}
diff --git a/app/token/[mint]/opengraph-image.tsx b/app/token/[mint]/opengraph-image.tsx
new file mode 100644
index 0000000..ebe5d38
--- /dev/null
+++ b/app/token/[mint]/opengraph-image.tsx
@@ -0,0 +1,167 @@
+import { ImageResponse } from '@vercel/og';
+import { getTokenInfo } from '@/lib/solana';
+import { formatNumber } from '@/lib/utils';
+
+export const runtime = 'edge';
+export const alt = 'Token Details';
+export const size = {
+ width: 1200,
+ height: 630,
+};
+export const contentType = 'image/png';
+
+export default async function Image({ params }: { params: { mint: string } }) {
+ try {
+ const token = await getTokenInfo(params.mint);
+
+ const title = token?.metadata?.name || 'Token Overview';
+ const description = token?.metadata?.description || 'Solana Token Explorer';
+
+ return new ImageResponse(
+ (
+
+ {/* Logo */}
+
+
+ {/* Content */}
+
+
+ {title}
+
+ {token && (
+
+ Supply: {formatNumber(token.supply)} • Holders: {formatNumber(token.holders)}
+
+ )}
+
+ {description}
+
+ {token && (
+
+ {params.mint.slice(0, 20)}...{params.mint.slice(-20)}
+
+ )}
+
+
+ {/* Footer */}
+
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ },
+ );
+ } catch (e: any) {
+ console.log(`${e.message}`);
+ return new Response(`Failed to generate the image`, {
+ status: 500,
+ });
+ }
+}
\ No newline at end of file
diff --git a/app/token/[mint]/page.tsx b/app/token/[mint]/page.tsx
new file mode 100644
index 0000000..2689944
--- /dev/null
+++ b/app/token/[mint]/page.tsx
@@ -0,0 +1,23 @@
+import { Metadata } from 'next';
+import TokenDetails from '@/components/TokenDetails';
+
+interface PageProps {
+ params: Promise<{ mint: string }>;
+}
+
+export async function generateMetadata({
+ params,
+}: PageProps): Promise {
+ const resolvedParams = await params;
+ return {
+ title: `Token ${resolvedParams.mint} | OPENSVM`,
+ description: `View details of Solana token ${resolvedParams.mint} on OPENSVM`,
+ };
+}
+
+export default async function TokenPage({
+ params,
+}: PageProps) {
+ const resolvedParams = await params;
+ return ;
+}
\ No newline at end of file
diff --git a/app/tx/[signature]/page.tsx b/app/tx/[signature]/page.tsx
index 3a8769a..70e8a98 100644
--- a/app/tx/[signature]/page.tsx
+++ b/app/tx/[signature]/page.tsx
@@ -174,7 +174,7 @@ export default function TransactionPage() {
diff --git a/components/BlockDetails.tsx b/components/BlockDetails.tsx
new file mode 100644
index 0000000..27fa3c3
--- /dev/null
+++ b/components/BlockDetails.tsx
@@ -0,0 +1,182 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { formatNumber } from '@/lib/utils';
+
+interface BlockData {
+ block: any;
+ blockTime: number | null;
+}
+
+interface Props {
+ slot: string;
+}
+
+export default function BlockDetails({ slot }: Props) {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const response = await fetch(`/api/blocks/${slot}`);
+ const json = await response.json();
+
+ if (!response.ok) {
+ throw new Error(json.error || 'Failed to fetch block data');
+ }
+
+ setData(json);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch block data');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchData();
+ }, [slot]);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ if (error || !data) {
+ return Error: {error || 'Failed to load block data'}
;
+ }
+
+ const { block, blockTime } = data;
+
+ return (
+
+ {/* Block Overview */}
+
+
+ Block #{formatNumber(parseInt(slot))}
+
+
+
+
+
Block Time
+
+ {blockTime ? new Date(blockTime * 1000).toLocaleString() : 'Unknown'}
+
+
+
+
Block Height
+
+ {block.blockHeight ? formatNumber(block.blockHeight) : 'N/A'}
+
+
+
+
Parent Slot
+
+ {formatNumber(block.parentSlot)}
+
+
+
+
Transactions
+
+ {formatNumber(block.transactions.length)}
+
+
+
+
Total Rewards
+
+ {formatNumber(block.rewards.reduce((acc, r) => acc + r.lamports, 0) / 1e9)} SOL
+
+
+
+
Previous Blockhash
+
+ {block.previousBlockhash}
+
+
+
+
+
+
+ {/* Block Rewards */}
+
+
+ Block Rewards
+
+
+
+ {block.rewards.map((reward: any, index: number) => (
+
+
+
+ {reward.pubkey}
+
+
+ {reward.rewardType}
+
+
+
+
+
Reward
+
+ {formatNumber(reward.lamports / 1e9)} SOL
+
+
+
+
Post Balance
+
+ {formatNumber(reward.postBalance / 1e9)} SOL
+
+
+
+
+ ))}
+
+
+
+
+ {/* Block Transactions */}
+
+
+ Transactions
+
+
+
+ {block.transactions.map((tx: any, index: number) => (
+
+
+
+ {tx.transaction.signatures[0]}
+
+
+ Status: {tx.meta?.err ? 'Failed' : 'Success'}
+
+
+
+
+
Fee
+
+ {formatNumber((tx.meta?.fee || 0) / 1e9)} SOL
+
+
+
+
Instructions
+
+ {tx.transaction.message.instructions.length}
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/RecentBlocks.tsx b/components/RecentBlocks.tsx
new file mode 100644
index 0000000..025f0eb
--- /dev/null
+++ b/components/RecentBlocks.tsx
@@ -0,0 +1,63 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { connection } from '@/lib/solana';
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { formatNumber } from '@/lib/utils';
+import Link from 'next/link';
+
+export default function RecentBlocks() {
+ const [blocks, setBlocks] = useState([]);
+
+ useEffect(() => {
+ let mounted = true;
+
+ async function subscribeToBlocks() {
+ const slot = await connection.getSlot();
+ if (mounted) {
+ setBlocks([slot]);
+ }
+
+ const id = connection.onSlotChange(({ slot }) => {
+ if (mounted) {
+ setBlocks(prev => [slot, ...prev].slice(0, 10));
+ }
+ });
+
+ return () => {
+ connection.removeSlotChangeListener(id);
+ };
+ }
+
+ const unsubscribePromise = subscribeToBlocks();
+
+ return () => {
+ mounted = false;
+ unsubscribePromise.then(unsubscribe => unsubscribe());
+ };
+ }, []);
+
+ return (
+
+
+ Recent Blocks
+
+
+
+ {blocks.map(slot => (
+
+
+
#{formatNumber(slot)}
+
View Details →
+
+
+ ))}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/RecentTransactions.tsx b/components/RecentTransactions.tsx
index 4471c33..bd1c6b6 100644
--- a/components/RecentTransactions.tsx
+++ b/components/RecentTransactions.tsx
@@ -92,7 +92,7 @@ export default function RecentTransactions() {
-
+
{tx.type}
diff --git a/components/TokenDetails.tsx b/components/TokenDetails.tsx
new file mode 100644
index 0000000..b7b92f1
--- /dev/null
+++ b/components/TokenDetails.tsx
@@ -0,0 +1,188 @@
+'use client';
+
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
+import { getTokenInfo } from '@/lib/solana';
+import { formatNumber } from '@/lib/utils';
+import { Stack } from 'rinlab';
+import Image from 'next/image';
+import { useEffect, useState } from 'react';
+
+interface TokenData {
+ metadata?: {
+ name?: string;
+ symbol?: string;
+ description?: string;
+ image?: string;
+ updateAuthority?: string;
+ attributes?: Array<{
+ trait_type: string;
+ value: string;
+ }>;
+ };
+ price?: number;
+ marketCap?: number;
+ supply?: number;
+ holders?: number;
+ decimals: number;
+ volume24h?: number;
+}
+
+interface Props {
+ mint: string;
+}
+
+export default function TokenDetails({ mint }: Props) {
+ const [data, setData] = useState
(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ async function fetchData() {
+ try {
+ const tokenInfo = await getTokenInfo(mint);
+ setData(tokenInfo);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch token data');
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchData();
+ }, [mint]);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ if (error || !data) {
+ return Error: {error || 'Failed to load token data'}
;
+ }
+
+ return (
+
+ {/* Token Overview */}
+
+
+
+ {data.metadata?.image && (
+
+ )}
+
+
+ {data.metadata?.name || 'Unknown Token'}
+
+
+ {data.metadata?.symbol || mint}
+
+
+
+
+
+
+
+ Price
+
+ ${data.price?.toFixed(4) || 'N/A'}
+
+
+
+ Market Cap
+
+ ${formatNumber(data.marketCap || 0)}
+
+
+
+ Supply
+
+ {formatNumber(data.supply || 0)}
+
+
+
+ Holders
+
+ {formatNumber(data.holders || 0)}
+
+
+
+ Decimals
+
+ {data.decimals}
+
+
+
+ Volume (24h)
+
+ ${formatNumber(data.volume24h || 0)}
+
+
+
+
+
+
+ {/* Token Info */}
+
+
+ Token Information
+
+
+
+
+
+
Mint Address
+
+ {mint}
+
+
+ {data.metadata?.updateAuthority && (
+
+
Update Authority
+
+ {data.metadata.updateAuthority}
+
+
+ )}
+
+ {data.metadata?.description && (
+
+
Description
+
+ {data.metadata.description}
+
+
+ )}
+
+
+
+
+ {/* Token Attributes */}
+ {data.metadata?.attributes && data.metadata.attributes.length > 0 && (
+
+
+ Attributes
+
+
+
+ {data.metadata.attributes.map((attr, index) => (
+
+
+ {attr.trait_type}
+
+
+ {attr.value}
+
+
+ ))}
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/components/ui/card.tsx b/components/ui/card.tsx
new file mode 100644
index 0000000..e855d73
--- /dev/null
+++ b/components/ui/card.tsx
@@ -0,0 +1,79 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Card = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+Card.displayName = "Card"
+
+const CardHeader = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardHeader.displayName = "CardHeader"
+
+const CardTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardTitle.displayName = "CardTitle"
+
+const CardDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardDescription.displayName = "CardDescription"
+
+const CardContent = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardContent.displayName = "CardContent"
+
+const CardFooter = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+CardFooter.displayName = "CardFooter"
+
+export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
\ No newline at end of file
diff --git a/lib/solana.ts b/lib/solana.ts
index c73c4ac..34471da 100644
--- a/lib/solana.ts
+++ b/lib/solana.ts
@@ -1,37 +1,225 @@
-import { Connection, PublicKey } from '@solana/web3.js';
+import { Connection, PublicKey, SystemProgram, VersionedMessage, MessageV0, Message, MessageCompiledInstruction, CompiledInstruction } from '@solana/web3.js';
+import { TOKEN_PROGRAM_ID, getAccount, getMint } from '@solana/spl-token';
-const SOLANA_RPC_URL = 'https://solana-mainnet.core.chainstack.com/263c9f53f4e4cdb897c0edc4a64cd007';
-const SOLANA_WS_URL = 'wss://solana-mainnet.core.chainstack.com/263c9f53f4e4cdb897c0edc4a64cd007';
+export const connection = new Connection(process.env.NEXT_PUBLIC_RPC_URL || 'https://api.mainnet-beta.solana.com');
-const connection = new Connection(SOLANA_RPC_URL, {
- wsEndpoint: SOLANA_WS_URL,
- commitment: 'confirmed',
-});
+const METADATA_PROGRAM_ID = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s');
-export { connection };
+function getInstructions(message: VersionedMessage | Message) {
+ if ('compiledInstructions' in message) {
+ return (message as MessageV0).compiledInstructions;
+ }
+ return (message as Message).instructions;
+}
+
+function getProgramId(instruction: MessageCompiledInstruction | CompiledInstruction): number {
+ if ('programIdIndex' in instruction && typeof instruction.programIdIndex === 'number') {
+ return instruction.programIdIndex;
+ }
+ throw new Error('Invalid instruction type');
+}
+
+function getAccounts(instruction: MessageCompiledInstruction | CompiledInstruction) {
+ if ('accountKeyIndexes' in instruction) {
+ return instruction.accountKeyIndexes;
+ }
+ return instruction.accounts;
+}
+
+export async function getTokenInfo(mintAddress: string) {
+ try {
+ const mintPublicKey = new PublicKey(mintAddress);
+ const [mint, tokenAccounts] = await Promise.all([
+ getMint(connection, mintPublicKey),
+ connection.getProgramAccounts(TOKEN_PROGRAM_ID, {
+ filters: [
+ {
+ memcmp: {
+ offset: 0,
+ bytes: mintPublicKey.toBase58(),
+ },
+ },
+ ],
+ }),
+ ]);
+
+ // Get metadata
+ const [metadataPda] = PublicKey.findProgramAddressSync(
+ [
+ Buffer.from('metadata'),
+ METADATA_PROGRAM_ID.toBuffer(),
+ mintPublicKey.toBuffer(),
+ ],
+ METADATA_PROGRAM_ID
+ );
+
+ let metadata: any = null;
+ try {
+ const metadataAccount = await connection.getAccountInfo(metadataPda);
+ if (metadataAccount) {
+ // Parse metadata manually since we don't have access to the full Metaplex SDK
+ const name = metadataAccount.data.slice(1, 33).toString().replace(/\0/g, '');
+ const symbol = metadataAccount.data.slice(33, 65).toString().replace(/\0/g, '');
+ const uri = metadataAccount.data.slice(65, 200).toString().replace(/\0/g, '');
+ metadata = { name, symbol, uri };
+ }
+ } catch (e) {
+ console.error('Failed to fetch metadata:', e);
+ }
+
+ // Calculate total supply and holders
+ let totalSupply = 0n;
+ const holders = new Set();
+
+ for (const { account, pubkey } of tokenAccounts) {
+ const tokenAccount = await getAccount(connection, pubkey);
+ totalSupply += tokenAccount.amount;
+ if (tokenAccount.amount > 0n) {
+ holders.add(tokenAccount.owner.toBase58());
+ }
+ }
-export type TransactionInfo = {
+ return {
+ decimals: mint.decimals,
+ supply: Number(totalSupply),
+ holders: holders.size,
+ metadata: metadata ? {
+ name: metadata.name,
+ symbol: metadata.symbol,
+ uri: metadata.uri,
+ description: '',
+ sellerFeeBasisPoints: 0,
+ creators: null,
+ collection: null,
+ uses: null,
+ } : null,
+ };
+ } catch (error) {
+ console.error('Error fetching token info:', error);
+ return null;
+ }
+}
+
+export interface TransactionInfo {
signature: string;
- timestamp: Date | null;
- status: 'Success' | 'Failed';
- fee: number;
+ slot: number;
+ timestamp: number;
+ status: 'success' | 'error';
type: string;
+ fee: number;
+ signer: string;
from: string;
to: string;
amount: number;
-};
+}
+
+export async function getAccountInfo(address: string) {
+ try {
+ const publicKey = new PublicKey(address);
+ const accountInfo = await connection.getAccountInfo(publicKey);
+
+ if (!accountInfo) {
+ return null;
+ }
-export type DetailedTransactionInfo = TransactionInfo & {
+ return {
+ address,
+ lamports: accountInfo.lamports,
+ owner: accountInfo.owner.toBase58(),
+ executable: accountInfo.executable,
+ rentEpoch: accountInfo.rentEpoch,
+ data: accountInfo.data,
+ };
+ } catch (error) {
+ console.error('Error fetching account info:', error);
+ return null;
+ }
+}
+
+export async function getTransactionHistory(address: string, limit = 10) {
+ try {
+ const publicKey = new PublicKey(address);
+ const signatures = await connection.getSignaturesForAddress(publicKey, { limit });
+
+ const transactions: TransactionInfo[] = await Promise.all(
+ signatures.map(async (sig) => {
+ const tx = await connection.getTransaction(sig.signature, {
+ maxSupportedTransactionVersion: 0,
+ });
+
+ if (!tx) return null;
+
+ // Extract from and to addresses from the first transfer instruction
+ const accountKeys = tx.transaction.message.getAccountKeys();
+ let from = accountKeys.get(0)!.toBase58();
+ let to = from;
+ let amount = 0;
+ let type = 'Unknown';
+
+ // Try to determine transaction type and extract transfer details
+ const instructions = getInstructions(tx.transaction.message);
+
+ if (tx.meta && instructions.length > 0) {
+ const instruction = instructions[0];
+ const program = accountKeys.get(getProgramId(instruction));
+
+ if (program?.equals(SystemProgram.programId)) {
+ type = 'System Transfer';
+ if (instruction.data.length >= 8) {
+ // Convert data to Buffer if it's a string
+ const dataBuffer = Buffer.from(instruction.data);
+ // Assuming it's a transfer instruction
+ amount = Number(dataBuffer.readBigInt64LE(8)) / 1e9;
+ const accounts = getAccounts(instruction);
+ from = accountKeys.get(accounts[0])!.toBase58();
+ to = accountKeys.get(accounts[1])!.toBase58();
+ }
+ } else if (program?.equals(TOKEN_PROGRAM_ID)) {
+ type = 'Token Transfer';
+ // Add token transfer parsing logic here if needed
+ }
+ }
+
+ return {
+ signature: sig.signature,
+ slot: sig.slot,
+ timestamp: sig.blockTime || 0,
+ status: sig.err ? 'error' : 'success',
+ type,
+ fee: (tx.meta?.fee || 0) / 1e9,
+ signer: accountKeys.get(0)!.toBase58(),
+ from,
+ to,
+ amount,
+ };
+ })
+ );
+
+ return transactions.filter((tx): tx is TransactionInfo => tx !== null);
+ } catch (error) {
+ console.error('Error fetching transaction history:', error);
+ return [];
+ }
+}
+
+export interface DetailedTransactionInfo {
+ signature: string;
slot: number;
- blockTime: Date | null;
- recentBlockhash: string;
+ blockTime: number | null;
+ status: 'success' | 'error';
+ fee: number;
+ from: string;
+ to: string;
+ amount: number;
+ type: string;
+ computeUnits: number;
+ accounts: string[];
instructions: {
programId: string;
data: string;
}[];
logs: string[];
- computeUnits: number;
-};
+}
export async function getTransactionDetails(signature: string): Promise {
try {
@@ -39,78 +227,58 @@ export async function getTransactionDetails(signature: string): Promise Math.abs(amount)) {
- amount = balanceChange;
- from = accountKeys[i]?.toBase58() || 'Unknown';
- }
- }
+ let type = 'Unknown';
- // Find the largest balance increase (receiver)
- for (let i = 0; i < postBalances.length; i++) {
- const balanceChange = (postBalances[i] || 0) - (preBalances[i] || 0);
- if (balanceChange > 0 && balanceChange > Math.abs(amount)) {
- to = accountKeys[i]?.toBase58() || 'Unknown';
- }
- }
+ // Try to determine transaction type and extract transfer details
+ const instructions = getInstructions(tx.transaction.message);
- // If no receiver found, use the first non-sender account
- if (to === 'Unknown' && accountKeys.length > 1) {
- to = accountKeys.find(key => key?.toBase58() !== from)?.toBase58() || 'Unknown';
+ if (tx.meta && instructions.length > 0) {
+ const instruction = instructions[0];
+ const program = accountKeys.get(getProgramId(instruction));
+
+ if (program?.equals(SystemProgram.programId)) {
+ type = 'System Transfer';
+ if (instruction.data.length >= 8) {
+ // Convert data to Buffer if it's a string
+ const dataBuffer = Buffer.from(instruction.data);
+ // Assuming it's a transfer instruction
+ amount = Number(dataBuffer.readBigInt64LE(8)) / 1e9;
+ const accounts = getAccounts(instruction);
+ from = accountKeys.get(accounts[0])!.toBase58();
+ to = accountKeys.get(accounts[1])!.toBase58();
+ }
+ } else if (program?.equals(TOKEN_PROGRAM_ID)) {
+ type = 'Token Transfer';
+ // Add token transfer parsing logic here if needed
+ }
}
- // Convert lamports to SOL
- amount = Math.abs(amount) / 1e9;
-
return {
signature,
- timestamp: tx.blockTime ? new Date(tx.blockTime * 1000) : null,
- status: tx.meta?.err ? 'Failed' : 'Success',
+ slot: tx.slot,
+ blockTime: tx.blockTime,
+ status: tx.meta?.err ? 'error' : 'success',
fee: (tx.meta?.fee || 0) / 1e9,
- type: determineTransactionType(tx),
from,
to,
amount,
- slot: tx.slot || 0,
- blockTime: tx.blockTime ? new Date(tx.blockTime * 1000) : null,
- recentBlockhash: tx.transaction.message.recentBlockhash || '',
+ type,
+ computeUnits: tx.meta?.computeUnitsConsumed || 0,
+ accounts: accountKeys.staticAccountKeys.map(key => key.toBase58()),
instructions: instructions.map(ix => ({
- programId: accountKeys[ix.programIdIndex]?.toBase58() || 'Unknown',
- data: ix.data?.toString() || '',
+ programId: accountKeys.get(getProgramId(ix))!.toBase58(),
+ data: ix.data.toString('base64'),
})),
logs: tx.meta?.logMessages || [],
- computeUnits: tx.meta?.computeUnitsConsumed || 0,
};
} catch (error) {
console.error('Error fetching transaction details:', error);
@@ -118,124 +286,61 @@ export async function getTransactionDetails(signature: string): Promise {
try {
- const message = tx.transaction.message;
- if (!message) return 'Unknown';
-
- // Handle instructions for both legacy and versioned messages
- const instructions = 'instructions' in message
- ? message.instructions
- : message.compiledInstructions;
-
- if (!instructions?.[0]) return 'Unknown';
-
- const accountKeys = 'accountKeys' in message
- ? message.accountKeys
- : message.staticAccountKeys;
-
- if (!accountKeys || accountKeys.length === 0) {
- return 'Unknown';
- }
-
- const instruction = instructions[0];
- const programId = accountKeys[instruction.programIdIndex]?.toBase58();
+ const signatures = await connection.getSignaturesForAddress(SystemProgram.programId, { limit });
- if (!programId) {
- return 'Unknown';
- }
+ const transactions: TransactionInfo[] = await Promise.all(
+ signatures.map(async (sig) => {
+ const tx = await connection.getTransaction(sig.signature, {
+ maxSupportedTransactionVersion: 0,
+ });
+
+ if (!tx) return null;
- // Common program IDs
- const SYSTEM_PROGRAM = '11111111111111111111111111111111';
- const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
- const ASSOCIATED_TOKEN = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
- const METADATA_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s';
- const MEMO_PROGRAM = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
-
- switch (programId) {
- case SYSTEM_PROGRAM:
- return 'System Transfer';
- case TOKEN_PROGRAM:
- return 'Token Transfer';
- case ASSOCIATED_TOKEN:
- return 'Token Account';
- case METADATA_PROGRAM:
- return 'NFT';
- case MEMO_PROGRAM:
- return 'Memo';
- default:
- return 'Program Call';
- }
- } catch (error) {
- return 'Unknown';
- }
-}
+ // Extract from and to addresses from the first transfer instruction
+ const accountKeys = tx.transaction.message.getAccountKeys();
+ let from = accountKeys.get(0)!.toBase58();
+ let to = from;
+ let amount = 0;
+ let type = 'Unknown';
-export async function subscribeToTransactions(callback: (transaction: TransactionInfo) => void) {
- try {
- const subscriptionId = connection.onLogs(
- 'all',
- async (logs) => {
- try {
- // Get full transaction details
- const tx = await getTransactionDetails(logs.signature);
+ // Try to determine transaction type and extract transfer details
+ const instructions = getInstructions(tx.transaction.message);
+
+ if (tx.meta && instructions.length > 0) {
+ const instruction = instructions[0];
+ const program = accountKeys.get(getProgramId(instruction));
- if (tx) {
- callback(tx);
- } else {
- // Fallback to basic info if detailed fetch fails
- callback({
- signature: logs.signature,
- timestamp: new Date(),
- status: logs.err ? 'Failed' : 'Success',
- fee: 0.000005,
- type: 'Unknown',
- from: 'Unknown',
- to: 'Unknown',
- amount: 0,
- });
+ if (program?.equals(SystemProgram.programId)) {
+ type = 'System Transfer';
+ if (instruction.data.length >= 8) {
+ // Convert data to Buffer if it's a string
+ const dataBuffer = Buffer.from(instruction.data);
+ // Assuming it's a transfer instruction
+ amount = Number(dataBuffer.readBigInt64LE(8)) / 1e9;
+ const accounts = getAccounts(instruction);
+ from = accountKeys.get(accounts[0])!.toBase58();
+ to = accountKeys.get(accounts[1])!.toBase58();
+ }
+ } else if (program?.equals(TOKEN_PROGRAM_ID)) {
+ type = 'Token Transfer';
+ // Add token transfer parsing logic here if needed
}
- } catch (error) {
- console.error('Error processing transaction logs:', error);
}
- },
- 'confirmed'
- );
-
- return () => {
- connection.removeOnLogsListener(subscriptionId);
- };
- } catch (error) {
- console.error('Error setting up transaction subscription:', error);
- return () => {};
- }
-}
-export async function getInitialTransactions(): Promise {
- try {
- const signatures = await connection.getSignaturesForAddress(
- new PublicKey('11111111111111111111111111111111'),
- { limit: 10 }
- );
-
- const transactions = await Promise.all(
- signatures.map(async (sig) => {
- try {
- const tx = await getTransactionDetails(sig.signature);
- return tx || {
- signature: sig.signature,
- timestamp: sig.blockTime ? new Date(sig.blockTime * 1000) : new Date(),
- status: sig.err ? 'Failed' : 'Success',
- fee: 0.000005,
- type: 'Unknown',
- from: 'Unknown',
- to: 'Unknown',
- amount: 0
- };
- } catch (error) {
- console.error('Error fetching transaction details:', error);
- return null;
- }
+ return {
+ signature: sig.signature,
+ slot: sig.slot,
+ timestamp: sig.blockTime || 0,
+ status: sig.err ? 'error' : 'success',
+ type,
+ fee: (tx.meta?.fee || 0) / 1e9,
+ signer: accountKeys.get(0)!.toBase58(),
+ from,
+ to,
+ amount,
+ };
})
);
@@ -246,53 +351,69 @@ export async function getInitialTransactions(): Promise {
}
}
-export async function getAccountInfo(address: string) {
- try {
- const pubkey = new PublicKey(address);
- const account = await connection.getAccountInfo(pubkey);
- const balance = await connection.getBalance(pubkey);
+export function subscribeToTransactions(callback: (tx: TransactionInfo) => void): () => void {
+ const id = connection.onLogs(SystemProgram.programId, async (logs) => {
+ if (!logs.err) {
+ try {
+ const signature = logs.signature;
+ const tx = await connection.getTransaction(signature, {
+ maxSupportedTransactionVersion: 0,
+ });
+
+ if (tx) {
+ // Extract from and to addresses from the first transfer instruction
+ const accountKeys = tx.transaction.message.getAccountKeys();
+ let from = accountKeys.get(0)!.toBase58();
+ let to = from;
+ let amount = 0;
+ let type = 'Unknown';
- return {
- address,
- balance: balance / 1e9, // Convert lamports to SOL
- executable: account?.executable || false,
- owner: account?.owner?.toBase58() || 'Unknown',
- };
- } catch (error) {
- console.error('Error fetching account info:', error);
- return null;
- }
-}
+ // Try to determine transaction type and extract transfer details
+ const instructions = getInstructions(tx.transaction.message);
-export async function getTransactionHistory(address: string, limit = 10): Promise {
- try {
- const pubkey = new PublicKey(address);
- const signatures = await connection.getSignaturesForAddress(pubkey, { limit });
-
- const transactions = await Promise.all(
- signatures.map(async (sig) => {
- try {
- const tx = await getTransactionDetails(sig.signature);
- return tx || {
- signature: sig.signature,
- timestamp: sig.blockTime ? new Date(sig.blockTime * 1000) : null,
- status: sig.err ? 'Failed' : 'Success',
- fee: 0.000005,
- type: 'Unknown',
- from: address,
- to: 'Unknown',
- amount: 0
+ if (tx.meta && instructions.length > 0) {
+ const instruction = instructions[0];
+ const program = accountKeys.get(getProgramId(instruction));
+
+ if (program?.equals(SystemProgram.programId)) {
+ type = 'System Transfer';
+ if (instruction.data.length >= 8) {
+ // Convert data to Buffer if it's a string
+ const dataBuffer = Buffer.from(instruction.data);
+ // Assuming it's a transfer instruction
+ amount = Number(dataBuffer.readBigInt64LE(8)) / 1e9;
+ const accounts = getAccounts(instruction);
+ from = accountKeys.get(accounts[0])!.toBase58();
+ to = accountKeys.get(accounts[1])!.toBase58();
+ }
+ } else if (program?.equals(TOKEN_PROGRAM_ID)) {
+ type = 'Token Transfer';
+ // Add token transfer parsing logic here if needed
+ }
+ }
+
+ const transactionInfo: TransactionInfo = {
+ signature,
+ slot: tx.slot,
+ timestamp: tx.blockTime || 0,
+ status: 'success',
+ type,
+ fee: (tx.meta?.fee || 0) / 1e9,
+ signer: accountKeys.get(0)!.toBase58(),
+ from,
+ to,
+ amount,
};
- } catch (error) {
- console.error('Error fetching transaction details:', error);
- return null;
+
+ callback(transactionInfo);
}
- })
- );
+ } catch (error) {
+ console.error('Error processing transaction:', error);
+ }
+ }
+ });
- return transactions.filter((tx): tx is TransactionInfo => tx !== null);
- } catch (error) {
- console.error('Error fetching transaction history:', error);
- return [];
- }
+ return () => {
+ connection.removeOnLogsListener(id);
+ };
}
\ No newline at end of file
diff --git a/lib/utils.ts b/lib/utils.ts
new file mode 100644
index 0000000..05daa53
--- /dev/null
+++ b/lib/utils.ts
@@ -0,0 +1,19 @@
+import { type ClassValue, clsx } from "clsx"
+import { twMerge } from "tailwind-merge"
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs))
+}
+
+export function formatNumber(num: number): string {
+ if (num >= 1e9) {
+ return (num / 1e9).toFixed(2) + 'B';
+ }
+ if (num >= 1e6) {
+ return (num / 1e6).toFixed(2) + 'M';
+ }
+ if (num >= 1e3) {
+ return (num / 1e3).toFixed(2) + 'K';
+ }
+ return num.toLocaleString();
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index b3f9ede..c9739b3 100644
--- a/package.json
+++ b/package.json
@@ -9,11 +9,13 @@
"lint": "next lint"
},
"dependencies": {
+ "@metaplex-foundation/mpl-token-metadata": "^3.3.0",
"@mozilla/readability": "^0.5.0",
"@solana/spl-token": "^0.4.9",
"@solana/web3.js": "^1.98.0",
"@swc/helpers": "^0.5.15",
"@vercel/og": "^0.6.4",
+ "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"encoding": "^0.1.13",
"eventsource-parser": "^1.1.2",
@@ -24,6 +26,7 @@
"react-dom": "18.2.0",
"react-hot-toast": "^2.4.1",
"rinlab": "^0.2.0",
+ "tailwind-merge": "^2.6.0",
"together-ai": "^0.6.0-alpha.3",
"zod": "^3.23.8",
"zod-to-json-schema": "^3.23.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e960743..301ebd3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,6 +1,9 @@
lockfileVersion: '6.0'
dependencies:
+ '@metaplex-foundation/mpl-token-metadata':
+ specifier: ^3.3.0
+ version: 3.3.0(@metaplex-foundation/umi@0.9.2)
'@mozilla/readability':
specifier: ^0.5.0
version: 0.5.0
@@ -16,6 +19,9 @@ dependencies:
'@vercel/og':
specifier: ^0.6.4
version: 0.6.4
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
date-fns:
specifier: ^4.1.0
version: 4.1.0
@@ -46,6 +52,9 @@ dependencies:
rinlab:
specifier: ^0.2.0
version: 0.2.0(@radix-ui/react-toast@1.2.4)(class-variance-authority@0.7.1)(clsx@2.1.1)(framer-motion@11.15.0)(lucide-react@0.294.0)(react-dom@18.2.0)(react@18.2.0)(tailwind-merge@2.6.0)(tailwindcss@3.4.1)
+ tailwind-merge:
+ specifier: ^2.6.0
+ version: 2.6.0
together-ai:
specifier: ^0.6.0-alpha.3
version: 0.6.0-alpha.3(encoding@0.1.13)
@@ -362,6 +371,67 @@ packages:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
+ /@metaplex-foundation/mpl-token-metadata@3.3.0(@metaplex-foundation/umi@0.9.2):
+ resolution: {integrity: sha512-t5vO8Wr3ZZZPGrVrGNcosX5FMkwQSgBiVMQMRNDG2De7voYFJmIibD5jdG05EoQ4Y5kZVEiwhYaO+wJB3aO5AA==}
+ peerDependencies:
+ '@metaplex-foundation/umi': '>= 0.8.2 < 1'
+ dependencies:
+ '@metaplex-foundation/mpl-toolbox': 0.9.4(@metaplex-foundation/umi@0.9.2)
+ '@metaplex-foundation/umi': 0.9.2
+ dev: false
+
+ /@metaplex-foundation/mpl-toolbox@0.9.4(@metaplex-foundation/umi@0.9.2):
+ resolution: {integrity: sha512-fd6JxfoLbj/MM8FG2x91KYVy1U6AjBQw4qjt7+Da3trzQaWnSaYHDcYRG/53xqfvZ9qofY1T2t53GXPlD87lnQ==}
+ peerDependencies:
+ '@metaplex-foundation/umi': '>= 0.8.2 < 1'
+ dependencies:
+ '@metaplex-foundation/umi': 0.9.2
+ dev: false
+
+ /@metaplex-foundation/umi-options@0.8.9:
+ resolution: {integrity: sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==}
+ dev: false
+
+ /@metaplex-foundation/umi-public-keys@0.8.9:
+ resolution: {integrity: sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==}
+ dependencies:
+ '@metaplex-foundation/umi-serializers-encodings': 0.8.9
+ dev: false
+
+ /@metaplex-foundation/umi-serializers-core@0.8.9:
+ resolution: {integrity: sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==}
+ dev: false
+
+ /@metaplex-foundation/umi-serializers-encodings@0.8.9:
+ resolution: {integrity: sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==}
+ dependencies:
+ '@metaplex-foundation/umi-serializers-core': 0.8.9
+ dev: false
+
+ /@metaplex-foundation/umi-serializers-numbers@0.8.9:
+ resolution: {integrity: sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==}
+ dependencies:
+ '@metaplex-foundation/umi-serializers-core': 0.8.9
+ dev: false
+
+ /@metaplex-foundation/umi-serializers@0.9.0:
+ resolution: {integrity: sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==}
+ dependencies:
+ '@metaplex-foundation/umi-options': 0.8.9
+ '@metaplex-foundation/umi-public-keys': 0.8.9
+ '@metaplex-foundation/umi-serializers-core': 0.8.9
+ '@metaplex-foundation/umi-serializers-encodings': 0.8.9
+ '@metaplex-foundation/umi-serializers-numbers': 0.8.9
+ dev: false
+
+ /@metaplex-foundation/umi@0.9.2:
+ resolution: {integrity: sha512-9i4Acm4pruQfJcpRrc2EauPBwkfDN0I9QTvJyZocIlKgoZwD6A6wH0PViH1AjOVG5CQCd1YI3tJd5XjYE1ElBw==}
+ dependencies:
+ '@metaplex-foundation/umi-options': 0.8.9
+ '@metaplex-foundation/umi-public-keys': 0.8.9
+ '@metaplex-foundation/umi-serializers': 0.9.0
+ dev: false
+
/@mozilla/readability@0.5.0:
resolution: {integrity: sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==}
engines: {node: '>=14.0.0'}
diff --git a/tsconfig.json b/tsconfig.json
index 059fdda..dd589d3 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -16,7 +16,7 @@
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": false,
"strict": false,
- "target": "ES2017",
+ "target": "ES2020",
"lib": [
"dom",
"dom.iterable",