+ This is testnet4 explorer, all transactions have no value on this chain.
+
+{/if}
+
+ isOpen = false}
+ role="presentation"
+ />
+
+
+
+{/if}
+
+
diff --git a/src/lib/components/layout/NavigationLinks.svelte b/src/lib/components/layout/NavigationLinks.svelte
new file mode 100755
index 0000000..25f8884
--- /dev/null
+++ b/src/lib/components/layout/NavigationLinks.svelte
@@ -0,0 +1,16 @@
+
+
+
+ {#each menuLinks as { href, label, external }}
+
+ {label}
+
+ {/each}
+
diff --git a/src/lib/components/layout/SearchBar.svelte b/src/lib/components/layout/SearchBar.svelte
new file mode 100755
index 0000000..60a9a88
--- /dev/null
+++ b/src/lib/components/layout/SearchBar.svelte
@@ -0,0 +1,230 @@
+
+
+
+ {
+ navigatingToSpacePage = false;
+ showSearchResults = searchResults.length > 0 && search.length > 0;
+ }}
+ bind:value={search}
+ on:input={handleSearch}
+ type="text"
+ class="grow"
+ placeholder="Search"
+ />
+
+
+ {#if !navigatingToSpacePage && (searching || showSearchResults)}
+
+ {#if searching}
+
+
+
+ {:else if showSearchResults && searchResults.length > 0}
+ {#each searchResults as result, idx}
+
{
+ showSearchResults = false;
+ navigateToResult(result);
+ }}
+ on:keydown={(e) => {
+ if (e.key === 'Enter') {
+ showSearchResults = false;
+ navigateToResult(result);
+ }
+ }}
+ >
+ {#if result.type === "transaction"}
+ Transaction: {result.value.txid.slice(0, 8)}...{result.value.txid.slice(-8)}
+ {:else if result.type === "block"}
+ {#if result.value.height == -2 }
+ Orphaned block: {result.value.hash.slice(0, 8)}...{result.value.hash.slice(-8)}
+ {:else }
+ Block #{result.value.height}: {result.value.hash.slice(0, 8)}...{result.value.hash.slice(-8)}
+ {/if}
+ {:else if result.type === "external-transaction"}
+
+ Transaction: {result.value.txid.slice(0, 16)}...
+ (on mempool.space)
+
+ {:else if result.type === "external-block"}
+
+ Block #{result.value.height}: {result.value.hash.slice(0, 16)}...
+ (on mempool.space)
+
+ {:else if result.type === "space"}
+ Space: {result.value.name}
+ {:else if result.type === "address"}
+ Address: {result.value.address}
+ {/if}
+
+ {/each}
+ {:else if showSearchResults}
+
No results
+ {/if}
+
+ {/if}
+ {#if search.length}
+ e.key === 'Enter' && clearSearch()}
+ type="button"
+ class="flex items-center justify-center hover:opacity-100 opacity-70"
+ aria-label="Clear search"
+ >
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
diff --git a/src/lib/composables/useBlockPage.ts b/src/lib/composables/useBlockPage.ts
new file mode 100755
index 0000000..62dd48c
--- /dev/null
+++ b/src/lib/composables/useBlockPage.ts
@@ -0,0 +1,62 @@
+import { goto } from '$app/navigation';
+import { browser } from '$app/environment';
+import { blockStore } from '$lib/stores/blockStore';
+import { onDestroy } from 'svelte';
+
+export function useBlockPage() {
+ let previousIdentifier: string | null = null;
+ let previousPage: number | null = null;
+ let previousFilter: boolean | null = null;
+
+ async function loadBlockData(identifier: string, currentPage: number, onlyWithSpaces: boolean) {
+ try {
+ await blockStore.fetchBlockData(identifier, currentPage, onlyWithSpaces);
+ // Update previous values after successful fetch
+ previousIdentifier = identifier;
+ previousPage = currentPage;
+ previousFilter = onlyWithSpaces;
+ } catch (error) {
+ console.error('Failed to load block data:', error);
+ }
+ }
+
+ function shouldReload(identifier: string, currentPage: number, onlyWithSpaces: boolean): boolean {
+ if (!browser) return false;
+ if (!identifier) return false;
+
+ return (
+ identifier !== previousIdentifier ||
+ currentPage !== previousPage ||
+ onlyWithSpaces !== previousFilter
+ );
+ }
+
+ async function handlePageChange(newPage: number) {
+ const url = new URL(window.location.href);
+ url.searchParams.set('page', newPage.toString());
+ await goto(url.toString(), { keepFocus: true, replaceState: true });
+ }
+
+ async function handleFilterChange(showOnlySpaces: boolean) {
+ const url = new URL(window.location.href);
+ if (showOnlySpaces) {
+ url.searchParams.set('filter', 'spaces');
+ } else {
+ url.searchParams.delete('filter');
+ }
+ url.searchParams.set('page', '1'); // Reset to first page when filtering
+ await goto(url.toString(), { keepFocus: true, replaceState: true });
+ }
+
+ // Clear block data when component is destroyed
+ onDestroy(() => {
+ blockStore.clearBlock();
+ });
+
+ return {
+ loadBlockData,
+ shouldReload,
+ handlePageChange,
+ handleFilterChange
+ };
+}
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100755
index 0000000..66b8d83
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,44 @@
+import { drizzle } from "drizzle-orm/node-postgres";
+import pg from 'pg';
+const { Pool } = pg;
+import { env } from '$env/dynamic/private';
+import * as schema from '$lib/schema';
+
+let dbUrl: string;
+if (env.DB_CREDENTIALS) {
+ const dbCreds = JSON.parse(env.DB_CREDENTIALS);
+ dbUrl = `postgresql://${dbCreds.username}:${dbCreds.password}@${dbCreds.host}:${dbCreds.port}/${dbCreds.dbClusterIdentifier}?sslmode=no-verify&hot_standby_feedback=on`;
+} else if (env.DB_URL) {
+ dbUrl = env.DB_URL;
+ } else {
+ throw new Error('No database configuration found.');
+ }
+
+ const pool = new Pool({
+ connectionString: dbUrl,
+ query_timeout: 3000, // 3 seconds (was 30000)
+ statement_timeout: 3000, // PostgreSQL statement timeout
+ connectionTimeoutMillis: 5000, // Connection acquisition timeout
+ idleTimeoutMillis: 30000, // How long connections stay idle
+ max: 15, // Max connections in pool
+ min: 5, // Min connections to maintain
+ });
+
+
+ pool.on('error', (err, client) => {
+ console.error('Database pool error:', {
+ message: err.message,
+ code: err.code,
+ timestamp: new Date().toISOString()
+ });
+
+ if (err.message?.includes('conflict with recovery') ||
+ err.message?.includes('canceling statement due to conflict')) {
+ console.warn('Replica conflict detected - this is expected behavior');
+ return;
+ }
+
+ });
+
+ const db = drizzle(pool, { schema });
+ export default db;
diff --git a/explorer/src/lib/index.ts b/src/lib/index.ts
old mode 100644
new mode 100755
similarity index 100%
rename from explorer/src/lib/index.ts
rename to src/lib/index.ts
diff --git a/src/lib/links.ts b/src/lib/links.ts
new file mode 100755
index 0000000..f629e8b
--- /dev/null
+++ b/src/lib/links.ts
@@ -0,0 +1,27 @@
+export const footerLinks = [
+ {
+ href: "https://docs.spacesprotocol.org/",
+ text: "Docs",
+ image: "/footer/spacesprotocol.png"
+ },
+ {
+ href: "https://github.com/spacesprotocol/",
+ text: "GitHub",
+ image: "/footer/github.svg"
+ },
+ {
+ href: "https://t.me/spacesprotocol",
+ text: "Telegram",
+ image: "/footer/telegram.svg"
+ },
+];
+
+
+
+export const menuLinks = [
+ { href: "/auctions/current", label: "Current Auctions" },
+ { href: "/auctions/rollout", label: "Upcoming Auctions" },
+ { href: "/auctions/past", label: "Past Auctions" },
+ { href: "https://spacesprotocol.org", label: "Help", external: true }
+ ];
+
diff --git a/explorer/src/lib/request-validation.ts b/src/lib/request-validation.ts
old mode 100644
new mode 100755
similarity index 100%
rename from explorer/src/lib/request-validation.ts
rename to src/lib/request-validation.ts
diff --git a/src/lib/routes.ts b/src/lib/routes.ts
new file mode 100755
index 0000000..06d556f
--- /dev/null
+++ b/src/lib/routes.ts
@@ -0,0 +1,64 @@
+export const ROUTES = {
+ // Frontend routes
+ pages: {
+ home: '/',
+ actions: '/actions/recent',
+ mempool: '/mempool',
+ psbt: '/psbt',
+
+ auctions: {
+ current: '/auctions/current',
+ past: '/auctions/past',
+ rollout: '/auctions/rollout'
+ },
+
+ space: '/space',
+
+ block: '/block',
+
+ transaction: '/tx',
+
+ address: '/address'
+ },
+
+ // API routes
+ api: {
+ actions: {
+ rollout: '/api/actions/rollout',
+ recent: '/api/actions/recent'
+ },
+
+ address: (address: string) => `/api/address/${address}`,
+
+ auctions: {
+ current: '/api/auctions/current',
+ mempool: '/api/auctions/mempool',
+ past: '/api/auctions/past',
+ recent: '/api/auctions/recent'
+ },
+
+ block: {
+ header: {
+ byHash: (hash: string) => `/api/block/${hash}/header`,
+ byHeight: (height: number | string) => `/api/block/${height}/header`
+ },
+ transactions: {
+ byHash: (hash: string) => `/api/block/${hash}/txs`,
+ byHeight: (height: number | string) => `/api/block/${height}/txs`
+ }
+ },
+
+ search: (query: string) => `/api/search?q=${encodeURIComponent(query)}`,
+
+ space: {
+ history: (name: string, page = 1) => `/api/space/${name}/history?page=${page}`,
+ stats: (name: string) => `/api/space/${name}/stats`,
+ commitment: (name: string) => `/api/space/${name}/commitment`,
+ commitments: (name: string, page = 1) => `/api/space/${name}/commitments?page=${page}`
+ },
+
+ stats: '/api/stats',
+
+ transactions: (txid: string) => `/api/transactions/${txid}`
+ }
+} as const;
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
new file mode 100755
index 0000000..a495b48
--- /dev/null
+++ b/src/lib/schema.ts
@@ -0,0 +1,24 @@
+import db from "$lib/db";
+import { sql } from "drizzle-orm";
+
+// const bytea = customType({
+// dataType() { return "bytea" },
+// fromDriver(value: unknown): string {
+// if (typeof value === 'string' && value.startsWith('\\x')) {
+// return value.slice(2)
+// }
+// return value.toString('hex')
+// },
+// toDriver(value: Buffer): Buffer {
+// return Buffer.from(value, 'hex')
+// },
+// });
+
+export async function getMaxBlockHeight() {
+ const result = await db.execute(sql`
+ SELECT COALESCE(MAX(height), -1)::integer AS max_height
+ FROM blocks
+ `);
+
+ return result.rows[0].max_height;
+}
diff --git a/src/lib/stores/blockStore.ts b/src/lib/stores/blockStore.ts
new file mode 100755
index 0000000..c378b4f
--- /dev/null
+++ b/src/lib/stores/blockStore.ts
@@ -0,0 +1,141 @@
+import { writable, derived, get } from 'svelte/store';
+import type { Transaction } from '$lib/types/transaction';
+// import type { Block } from '$lib/types/block';
+
+type BlockState = {
+ currentHeight: string | null;
+ header: Block | null;
+ transactions: Transaction[];
+ txCount: number;
+ filteredTxCount: number;
+ error: string | null;
+ onlyWithSpaces: boolean;
+ pagination: {
+ currentPage: number;
+ limit: number;
+ offset: number;
+ };
+ cache: Map
;
+ spacesPages: Map;
+ }>;
+};
+
+function createBlockStore() {
+ const initialState: BlockState = {
+ currentHeight: null,
+ header: null,
+ transactions: [],
+ txCount: 0,
+ filteredTxCount: 0,
+ error: null,
+ onlyWithSpaces: false,
+ pagination: {
+ currentPage: 1,
+ limit: 25,
+ offset: 0
+ },
+ cache: new Map()
+ };
+
+ const { subscribe, set, update } = writable(initialState);
+
+ return {
+ subscribe,
+ async fetchBlockData(height: string, page: number = 1, onlyWithSpaces: boolean = false, customFetch: typeof fetch = fetch) {
+ update(state => ({ ...state, error: null }));
+
+ const offset = (page - 1) * initialState.pagination.limit;
+ const cacheKey = `${height}`;
+ const pageKey = page;
+
+ try {
+ // Check cache first
+ const cachedBlock = get(this).cache.get(cacheKey);
+ let blockHeader = cachedBlock?.header;
+ let transactions = onlyWithSpaces
+ ? cachedBlock?.spacesPages.get(pageKey)
+ : cachedBlock?.pages.get(pageKey);
+
+ // Fetch header if not cached
+ if (!blockHeader) {
+ const headerResponse = await customFetch(`/api/block/${height}/header`);
+ if (!headerResponse.ok) throw new Error(`Error fetching block header: ${headerResponse.statusText}`);
+ blockHeader = await headerResponse.json();
+ }
+
+ // Fetch transactions if not cached
+ let filteredTxCount = blockHeader.tx_count;
+ if (!transactions) {
+ const txsParams = new URLSearchParams({
+ offset: offset.toString(),
+ limit: initialState.pagination.limit.toString(),
+ ...(onlyWithSpaces && { onlyWithSpaces: 'true' })
+ });
+ const txsResponse = await customFetch(`/api/block/${height}/txs?${txsParams}`);
+ if (!txsResponse.ok) throw new Error(`Error fetching block transactions: ${txsResponse.statusText}`);
+ const response = await txsResponse.json();
+
+ // Handle new API format that returns { transactions, totalCount }
+ if (response.transactions) {
+ transactions = response.transactions;
+ if (onlyWithSpaces && response.totalCount !== null) {
+ filteredTxCount = response.totalCount;
+ }
+ } else {
+ // Fallback for old format (just array of transactions)
+ transactions = response;
+ }
+ }
+
+ // Update cache and state
+ update(state => {
+ const updatedCache = new Map(state.cache);
+ const blockCache = updatedCache.get(cacheKey) || { header: blockHeader, pages: new Map(), spacesPages: new Map() };
+ if (onlyWithSpaces) {
+ blockCache.spacesPages.set(pageKey, transactions);
+ } else {
+ blockCache.pages.set(pageKey, transactions);
+ }
+ updatedCache.set(cacheKey, blockCache);
+
+ return {
+ ...state,
+ currentHeight: height,
+ header: blockHeader,
+ transactions,
+ txCount: blockHeader.tx_count,
+ filteredTxCount,
+ onlyWithSpaces,
+ pagination: {
+ ...state.pagination,
+ currentPage: page,
+ offset
+ },
+ cache: updatedCache
+ };
+ });
+ } catch (error) {
+ update(state => ({
+ ...state,
+ error: error.message
+ }));
+ throw error;
+ }
+ },
+
+ clearBlock() {
+ set(initialState);
+ }
+ };
+}
+
+export const blockStore = createBlockStore();
+export const totalPages = derived(
+ blockStore,
+ $blockStore => {
+ const count = $blockStore.onlyWithSpaces ? $blockStore.filteredTxCount : $blockStore.txCount;
+ return Math.ceil(count / $blockStore.pagination.limit);
+ }
+);
diff --git a/src/lib/styles/common.css b/src/lib/styles/common.css
new file mode 100755
index 0000000..3a297ad
--- /dev/null
+++ b/src/lib/styles/common.css
@@ -0,0 +1,8 @@
+@import 'variables.css';
+
+/* Action Type Styles */
+/* Used for displaying Spaces Protocol action types: RESERVE, BID, TRANSFER, ROLLOUT, REVOKE */
+.action-type {
+ font-size: var(--font-size-base);
+ font-weight: 600;
+}
diff --git a/src/lib/styles/headers.css b/src/lib/styles/headers.css
new file mode 100755
index 0000000..b97c22d
--- /dev/null
+++ b/src/lib/styles/headers.css
@@ -0,0 +1,86 @@
+@import 'variables.css';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ padding: var(--space-4);
+ color: var(--font-size-primary);
+ transition: var(--transition-colors);
+}
+
+@media (min-width: 768px) {
+ .container {
+ padding: var(--space-6) var(--space-10);
+ }
+}
+
+.header {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-2);
+ align-items: center;
+ margin-bottom: var(--space-6);
+}
+
+.title {
+ font-weight: 700;
+ font-size: var(--font-size-3xl);
+ color: var(--font-size-primary);
+}
+
+.hash {
+ word-break: break-all;
+ color: var(--font-size-muted);
+ transition: var(--transition-colors);
+ font-family: monospace;
+ font-size: var(--font-size-lg);
+}
+
+.details {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--space-8) var(--space-10);
+ margin-bottom: var(--space-8);
+}
+
+@media (min-width: 1280px) {
+ .details {
+ gap: var(--space-6);
+ }
+}
+
+.detail-item {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: var(--space-2);
+ align-items: center;
+ text-align: center;
+}
+
+.detail-value {
+ font-size: var(--font-size-lg);
+ color: var(--color-primary);
+ font-weight: 600;
+ transition: var(--transition-colors);
+ font-family: monospace;
+ white-space: break-all;
+}
+
+@media (max-width: 640px) {
+ .detail-value {
+ font-size: var(--font-size-lg);
+ }
+
+ .details {
+ gap: var(--space-6) var(--space-6);
+ }
+}
+
+.detail-label {
+ color: var(--font-size-muted);
+ transition: var(--transition-colors);
+ font-size: var(--font-size-lg);
+ font-weight: 500;
+ white-space: nowrap;
+}
diff --git a/src/lib/styles/link.css b/src/lib/styles/link.css
new file mode 100755
index 0000000..56a69ad
--- /dev/null
+++ b/src/lib/styles/link.css
@@ -0,0 +1,12 @@
+.mono-link {
+ color: #6c757d;
+ text-decoration: none;
+ font-family: monospace;
+ font-size: inherit;
+ transition: color 0.3s ease-in-out;
+ word-break: break-all;
+}
+
+.mono-link:hover {
+ color: #fd7e14;
+}
diff --git a/src/lib/styles/mainpage.css b/src/lib/styles/mainpage.css
new file mode 100755
index 0000000..f8f7b7a
--- /dev/null
+++ b/src/lib/styles/mainpage.css
@@ -0,0 +1,74 @@
+.layout-container {
+ display: grid;
+ gap: var(--space-8);
+ padding: var(--space-4);
+ max-width: 1600px;
+ margin: 0 auto;
+ grid-template-areas:
+ "recent-actions"
+ "rollouts"
+ "auctions";
+ /* Prevent horizontal scroll */
+ width: 100%;
+ min-width: 0; /* Important for grid items */
+ overflow-x: hidden;
+}
+
+.recent-actions {
+ grid-area: recent-actions;
+ min-width: 0; /* Allow content to shrink */
+ width: 100%;
+}
+
+.rollouts-section {
+ grid-area: rollouts;
+ width: 100%;
+ min-width: 0;
+}
+
+.auctions-section {
+ grid-area: auctions;
+ width: 100%;
+ min-width: 0;
+}
+
+.grid-container {
+ display: grid;
+ gap: var(--space-4);
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ width: 100%;
+ min-width: 0;
+ padding: 0; /* Removed horizontal padding */
+}
+
+@media (min-width: 640px) {
+ .grid-container {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (min-width: 1024px) {
+ .layout-container {
+ grid-template-columns: 360px minmax(0, 1fr); /* minmax(0, 1fr) prevents overflow */
+ grid-template-areas:
+ "recent-actions rollouts"
+ "recent-actions auctions";
+ gap: var(--space-8) var(--space-12);
+ padding: var(--space-8);
+ }
+
+ .grid-container {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (min-width: 1280px) {
+ .layout-container {
+ padding: var(--space-8) var(--space-16);
+ }
+
+ .grid-container {
+ grid-template-columns: repeat(4, 1fr);
+ }
+}
+
diff --git a/src/lib/styles/variables.css b/src/lib/styles/variables.css
new file mode 100755
index 0000000..846aa70
--- /dev/null
+++ b/src/lib/styles/variables.css
@@ -0,0 +1,102 @@
+:root {
+ /* Colors */
+ --color-primary: #ec8e32;
+ /* --color-link: #ec8e32; */
+ --color-link: black;
+ --color-link-hover: var(--color-primary);
+ --color-gray-300: #d1d5db;
+ --color-gray-400: #9ca3af;
+ --color-gray-500: #6b7280;
+ --color-gray-600: #4b5563;
+
+--font-size-xs: 0.75rem;
+--font-size-sm: 0.875rem;
+--font-size-base: 1rem;
+--font-size-lg: 1.125rem;
+--font-size-xl: 1.25rem;
+--font-size-2xl: 1.5rem;
+--font-size-3xl: 1.875rem;
+
+
+
+ /* Light theme defaults */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8fafc;
+ --font-size-primary: #0f172a;
+ --font-size-muted: #64748b;
+ --border-color: #e2e8f0;
+ --border-hover: #cbd5e1;
+
+ /* Spacing */
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-3: 0.75rem;
+ --space-4: 1rem;
+ --space-6: 1.5rem;
+ --space-8: 2rem;
+ --space-10: 2.5rem;
+
+ /* Typography */
+ --font-size-xs: 0.75rem;
+ --font-size-sm: 0.875rem;
+ --font-size-base: 1rem;
+ --font-size-lg: 1.125rem;
+ --font-size-xl: 1.25rem;
+ --font-size-2xl: 1.5rem;
+ --font-size-3xl: 1.875rem;
+
+ /* Borders */
+ --border-radius-sm: 0.25rem;
+ --border-radius-md: 0.375rem;
+ --border-radius-lg: 0.5rem;
+ --border-radius-xl: 0.75rem;
+ --border-radius-2xl: 1rem;
+ --border-radius-3xl: 1.5rem;
+
+ /* Border widths */
+ --border-width-1: 1px;
+ --border-width-2: 2px;
+ --border-width-4: 4px;
+ --border-width-8: 8px;
+
+ /* Transitions */
+ --transition-all: all 0.2s ease-in-out;
+ --transition-transform: transform 0.2s ease-in-out;
+ --transition-colors: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out, color 0.2s ease-in-out;
+ --transition-opacity: opacity 0.2s ease-in-out;
+ --transition-shadow: box-shadow 0.2s ease-in-out;
+
+ --transition-duration: 200ms;
+ --transition-timing: ease-in-out;
+
+ /* Colors */
+ --color-primary-dark: #c76a1c;
+
+ /* Container */
+ --container-width: 1280px;
+
+ /* Shadows */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+
+ --bg-warning-50: #fffbeb;
+ --bg-warning-100: #fef3c7;
+ --bg-warning-200: #fde68a;
+ --bg-warning-300: #fcd34d;
+ --bg-warning-400: #fbbf24;
+ --bg-warning-500: #f59e0b;
+ --bg-warning-600: #d97706;
+ --bg-warning-700: #b45309;
+ --bg-warning-800: #92400e;
+ --bg-warning-900: #78350f;
+
+ /* Highlight colors */
+ --bg-highlight: var(--bg-warning-50);
+ --bg-highlight-active: var(--bg-warning-100);
+ --highlight-border: var(--color-primary);
+
+ /* Errors */
+ --bg-error-50: #fef2f2;
+ --color-error: #dc2626;
+}
diff --git a/src/lib/types/address.ts b/src/lib/types/address.ts
new file mode 100755
index 0000000..7de9dd5
--- /dev/null
+++ b/src/lib/types/address.ts
@@ -0,0 +1,15 @@
+export interface AddressStats {
+ txCount: number;
+ receivedCount: number;
+ spentCount: number;
+ totalReceived: bigint;
+ totalSpent: bigint;
+ balance: bigint;
+}
+
+export interface AddressData {
+ stats: AddressStats;
+ transactions: any[]; // Replace with your transaction type
+ hasMore: boolean;
+ nextCursor?: string;
+}
diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts
new file mode 100755
index 0000000..a45adc7
--- /dev/null
+++ b/src/lib/types/api.ts
@@ -0,0 +1,73 @@
+export type CovenantAction = 'RESERVE' | 'BID' | 'TRANSFER';
+
+export type ApiSearchResponse = {
+ block?: Block;
+ transaction?: Transaction;
+ names?: string[];
+};
+
+export type Vmetaout = {
+ block_hash: Bytes;
+ txid: Bytes;
+ tx_index: number;
+ outpoint_txid: Bytes;
+ outpoint_index: number;
+ name: string;
+ burn_increment: number | null;
+ covenant_action: CovenantAction;
+ claim_height: number | null;
+ expire_height: number | null;
+};
+
+// Translated Bytes type (from Go []byte)
+export type Bytes = Uint8Array;
+
+export type Block = {
+ hash: Bytes;
+ size: number;
+ stripped_size: number;
+ weight: number;
+ height: number;
+ version: number;
+ hash_merkle_root: Bytes;
+ time: number;
+ median_time: number;
+ nonce: number;
+ bits: Bytes;
+ difficulty: number;
+ chainwork: Bytes;
+ orphan: boolean;
+};
+
+export type Transaction = {
+ txid: Bytes;
+ tx_hash: Bytes | null;
+ version: number;
+ size: number;
+ vsize: number;
+ weight: number;
+ locktime: number;
+ fee: number;
+ block_hash: Bytes | null;
+ index: number | null;
+};
+
+export type TxInput = {
+ block_hash: Bytes;
+ txid: Bytes;
+ index: number;
+ hash_prevout: Bytes | null;
+ index_prevout: number;
+ sequence: number;
+ coinbase: Bytes | null;
+ txinwitness: Bytes[];
+};
+
+export type TxOutput = {
+ block_hash: Bytes;
+ txid: Bytes;
+ index: number;
+ value: number;
+ scriptpubkey: Bytes | null;
+};
+
diff --git a/src/lib/types/transaction.ts b/src/lib/types/transaction.ts
new file mode 100755
index 0000000..789cbff
--- /dev/null
+++ b/src/lib/types/transaction.ts
@@ -0,0 +1,79 @@
+export interface Transaction {
+ txid: string;
+ tx_hash: string;
+ version: number;
+ size: number;
+ vsize: number;
+ weight: number;
+ index: number;
+ locktime: number;
+ fee: number;
+ block?: {
+ height: number;
+ time: number;
+ hash?: string;
+ };
+ confirmations: number;
+ input_count: number;
+ output_count: number;
+ total_output_value: number;
+ vmetaouts: TransactionVmetaout[];
+ commitments: SpaceCommitment[];
+ // commitment_name?: string;
+ // commitment_state_root?: string;
+}
+
+export interface TransactionVmetaout {
+ value: number | null;
+ name: string | null;
+ action: string | null;
+ burn_increment: number | null;
+ total_burned: number | null;
+ claim_height: number | null;
+ expire_height: number | null;
+ script_error: string | null;
+ reason?: string;
+ scriptPubKey: string;
+ signature?: string;
+}
+
+export interface SpaceCommitment {
+ name: string;
+ state_root: string | null;
+ history_hash: string | null;
+ revocation: boolean;
+}
+
+// TransactionInput and TransactionOutput are no longer stored or returned by the API
+// We now only store aggregate counts and totals in the transactions table
+// For detailed input/output information, refer users to mempool.space
+
+// export interface TransactionInput {
+// index: number;
+// hash_prevout: string;
+// index_prevout: number;
+// sequence: number;
+// coinbase: string | null;
+// txinwitness: string | null;
+// prev_scriptpubkey?: string;
+// sender_address?: string;
+// prev_value?: number;
+// }
+
+// export interface TransactionOutput {
+// index: number;
+// value: number;
+// scriptpubkey: string | null;
+// address: string | null;
+// spender: {
+// txid: string;
+// index: number;
+// } | null;
+// }
+
+// export type SpaceAction = {
+// type: 'bid' | 'register' | 'transfer' | 'reserve';
+// value?: number; // for bids
+// address?: string; // for transfers
+// name: string; // Name involved in the action
+// };
diff --git a/src/lib/utils/address-parsers.ts b/src/lib/utils/address-parsers.ts
new file mode 100755
index 0000000..d1bfbde
--- /dev/null
+++ b/src/lib/utils/address-parsers.ts
@@ -0,0 +1,172 @@
+import { bech32, bech32m } from 'bech32';
+import bs58 from 'bs58';
+import { env } from "$env/dynamic/public";
+import { Buffer } from 'buffer';
+import { sha256 as sha256Hasher } from '@noble/hashes/sha256';
+
+function sha256Sync(data: Uint8Array): Uint8Array {
+ return sha256Hasher.create().update(data).digest();
+}
+
+export function parseAddress(scriptPubKey: Buffer): string | null {
+ return parseP2PKHScriptPubKey(scriptPubKey) ||
+ parseP2SHScriptPubKey(scriptPubKey) || // Added P2SH parsing
+ parseP2WPKH(scriptPubKey) ||
+ parseP2WSH(scriptPubKey) ||
+ decodeScriptPubKeyToTaprootAddress(scriptPubKey, env.PUBLIC_BTC_NETWORK);
+}
+
+export function parseP2SHScriptPubKey(scriptPubKey: Buffer): string | null {
+ // Check P2SH pattern: OP_HASH160 (0xa9) + Push 20 bytes (0x14) + <20 bytes> + OP_EQUAL (0x87)
+ if (scriptPubKey.length !== 23 ||
+ scriptPubKey[0] !== 0xa9 ||
+ scriptPubKey[1] !== 0x14 ||
+ scriptPubKey[22] !== 0x87) {
+ return null;
+ }
+
+ const scriptHash = scriptPubKey.slice(2, 22);
+ const prefix = env.PUBLIC_BTC_NETWORK === 'mainnet' ? 0x05 : 0xc4; // 0x05 for mainnet, 0xc4 for testnet
+ const payload = Buffer.concat([Buffer.from([prefix]), scriptHash]);
+
+ // Double SHA256 for checksum
+ const hash1 = sha256Sync(payload);
+ const hash2 = sha256Sync(hash1);
+ const checksum = hash2.slice(0, 4);
+
+ // Combine version, script hash, and checksum
+ const finalPayload = Buffer.concat([payload, Buffer.from(checksum)]);
+
+ return bs58.encode(finalPayload);
+}
+
+export function decodeScriptPubKeyToTaprootAddress(scriptPubKey: Buffer, network = 'mainnet') {
+ if (scriptPubKey.length !== 34 || scriptPubKey[0] !== 0x51 || scriptPubKey[1] !== 0x20) {
+ return null;
+ }
+ const pubkeyBytes = scriptPubKey.slice(2);
+ const hrp = network === 'mainnet' ? 'bc' : 'tb';
+ const pubkeyBits = bech32m.toWords(pubkeyBytes);
+ return bech32m.encode(hrp, [1].concat(pubkeyBits));
+}
+
+export function parseP2PKHScriptPubKey(scriptPubKey: Buffer): string | null {
+ if (scriptPubKey.length !== 25 ||
+ scriptPubKey[0] !== 0x76 ||
+ scriptPubKey[1] !== 0xa9 ||
+ scriptPubKey[2] !== 0x14 ||
+ scriptPubKey[23] !== 0x88 ||
+ scriptPubKey[24] !== 0xac) {
+ return null;
+ }
+
+ const pubKeyHash = scriptPubKey.slice(3, 23);
+ const prefix = env.PUBLIC_BTC_NETWORK === 'mainnet' ? 0x00 : 0x6f;
+ const payload = Buffer.concat([Buffer.from([prefix]), pubKeyHash]);
+
+ // Calculate checksum (double SHA256)
+ const hash = sha256Sync(payload);
+ const hash2 = sha256Sync(hash);
+ const checksum = hash2.slice(0, 4);
+
+ // Combine version, pubkey hash, and checksum
+ const finalPayload = Buffer.concat([payload, checksum]);
+
+ return bs58.encode(finalPayload);
+}
+
+export function parseP2WPKH(scriptPubKey: Buffer) {
+ if (scriptPubKey.length !== 22 || scriptPubKey[0] !== 0x00 || scriptPubKey[1] !== 0x14) {
+ return null;
+ }
+ const pubKeyHash = scriptPubKey.slice(2);
+ const words = bech32m.toWords(pubKeyHash);
+ const prefix = env.PUBLIC_BTC_NETWORK === 'mainnet' ? 'bc' : 'tb';
+ return bech32m.encode(prefix, [0].concat(words));
+}
+
+export function parseP2WSH(scriptPubKey: Buffer) {
+ if (scriptPubKey.length !== 34 || scriptPubKey[0] !== 0x00 || scriptPubKey[1] !== 0x20) {
+ return null;
+ }
+ const scriptHash = scriptPubKey.slice(2);
+ const words = bech32m.toWords(scriptHash);
+ const prefix = env.PUBLIC_BTC_NETWORK === 'mainnet' ? 'bc' : 'tb';
+ return bech32m.encode(prefix, [0].concat(words));
+}
+
+
+export function addressToScriptPubKey(address: string): string {
+ try {
+ // Handle bech32/bech32m addresses (starting with bc1 or tb1)
+ if (address.toLowerCase().startsWith('bc1') || address.toLowerCase().startsWith('tb1')) {
+ let decoded;
+ try {
+ // Try bech32m first (for taproot addresses)
+ decoded = bech32m.decode(address);
+ } catch {
+ // Fall back to bech32 (for SegWit v0 addresses)
+ decoded = bech32.decode(address);
+ }
+
+ const words = decoded.words;
+ const version = words[0];
+ const data = Buffer.from(bech32.fromWords(words.slice(1)));
+
+ // P2WPKH (version 0, length 20)
+ if (version === 0 && data.length === 20) {
+ return Buffer.concat([
+ Buffer.from('0014', 'hex'), // OP_0 + Push 20 bytes
+ data
+ ]).toString('hex');
+ }
+
+ // P2WSH (version 0, length 32)
+ if (version === 0 && data.length === 32) {
+ return Buffer.concat([
+ Buffer.from('0020', 'hex'), // OP_0 + Push 32 bytes
+ data
+ ]).toString('hex');
+ }
+
+ // P2TR (Taproot, version 1, length 32)
+ if (version === 1 && data.length === 32) {
+ return Buffer.concat([
+ Buffer.from('5120', 'hex'), // OP_1 + Push 32 bytes
+ data
+ ]).toString('hex');
+ }
+
+ throw new Error('Unsupported witness version or program length');
+ }
+
+ // Legacy address decoding
+ const decoded = Buffer.from(bs58.decode(address));
+ const version = decoded[0];
+ const hash = decoded.slice(1, -4); // Remove version byte and checksum
+
+ // P2PKH (starts with 1 or m/n)
+ if (version === 0x00 || version === 0x6f) {
+ return Buffer.concat([
+ Buffer.from('76a914', 'hex'), // OP_DUP + OP_HASH160 + Push 20 bytes
+ hash,
+ Buffer.from('88ac', 'hex') // OP_EQUALVERIFY + OP_CHECKSIG
+ ]).toString('hex');
+ }
+
+ // P2SH (starts with 3 or 2)
+ if (version === 0x05 || version === 0xc4) {
+ return Buffer.concat([
+ Buffer.from('a914', 'hex'), // OP_HASH160 + Push 20 bytes
+ hash,
+ Buffer.from('87', 'hex') // OP_EQUAL
+ ]).toString('hex');
+ }
+
+ throw new Error('Unsupported address format');
+
+ } catch (error) {
+ console.error('Error converting address to scriptPubKey:', error);
+ throw error;
+ }
+}
diff --git a/src/lib/utils/formatters.ts b/src/lib/utils/formatters.ts
new file mode 100755
index 0000000..5407e14
--- /dev/null
+++ b/src/lib/utils/formatters.ts
@@ -0,0 +1,244 @@
+// declare module 'punycode';
+
+import * as punycode from 'punycode';
+import { env } from '$env/dynamic/public';
+
+export function formatNumberWithSpaces(num: number): string {
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ");
+}
+
+export const numberFormatter = {
+ format: formatNumberWithSpaces
+};
+
+export function getActionColor(action: string): string {
+ switch (action) {
+ case 'RESERVE': return 'text-blue-500';
+ case 'BID': return 'text-green-500';
+ case 'TRANSFER': return 'text-purple-500';
+ case 'ROLLOUT': return 'text-yellow-500';
+ case 'REVOKE': return 'text-red-500';
+ case 'COMMITMENT': return 'text-cyan-500';
+ case 'COMMITMENT REVOCATION': return 'text-red-500';
+ default: return 'text-gray-500';
+ }
+}
+
+export function getHighestBid(vmetaouts): number {
+ // Find the last rollout
+ const lastRollout = vmetaouts
+ .filter(v => v.action === 'ROLLOUT')
+ .sort((a, b) => b.block_height - a.block_height)[0];
+
+ // If no rollout, get highest bid from all bids
+ if (!lastRollout) {
+ return Math.max(0, ...vmetaouts .filter(v => v.action === 'BID') .map(v => Number(v.total_burned ?? 0)));
+ }
+
+ // Get the last bid before rollout and all bids after
+ const relevantBids = vmetaouts
+ .filter(v => v.action === 'BID' && (v.block_height > lastRollout.block_height ||
+ v === vmetaouts .filter(bid => bid.action === 'BID' && bid.block_height < lastRollout.block_height)
+ .sort((a, b) => b.block_height - a.block_height)[0]));
+
+ return Math.max(0, ...relevantBids.map(v => Number(v.total_burned ?? 0)));
+}
+
+
+
+export function calculateTimeRemaining(targetHeight: number, currentHeight: number): string {
+ const BLOCK_TIME_MINUTES = 10;
+
+ if (targetHeight <= currentHeight) {
+ return "Recently";
+ }
+
+ const remainingBlocks = targetHeight - currentHeight;
+ const totalMinutesRemaining = remainingBlocks * BLOCK_TIME_MINUTES;
+
+ const days = Math.floor(totalMinutesRemaining / (24 * 60));
+ const hours = Math.floor((totalMinutesRemaining % (24 * 60)) / 60);
+ const minutes = totalMinutesRemaining % 60;
+
+ return `${days}d ${hours}h ${minutes}m`;
+}
+
+export function formatDuration(seconds: number): string {
+ const days = Math.floor(seconds / (24 * 3600));
+ seconds %= 24 * 3600;
+ const hours = Math.floor(seconds / 3600);
+ seconds %= 3600;
+ const minutes = Math.floor(seconds / 60);
+ seconds %= 60;
+
+ let result = '';
+ if (days > 0) result = `${days} day${days > 1 ? 's' : ''}`;
+ else if (hours > 0) result = `${hours} hour${hours > 1 ? 's' : ''}`;
+ else result = `${minutes} minute${minutes > 1 ? 's' : ''} `;
+
+ return result;
+}
+
+export function formatBTC(satoshis: number | undefined): string {
+ if (satoshis === undefined || satoshis === null) {
+ return '0 sat';
+ }
+ const BTC_THRESHOLD = 10000n;
+ if (satoshis >= BTC_THRESHOLD) {
+ const btc = Number(satoshis) / 100000000;
+ const btcString = btc.toString();
+ const [whole, decimal] = btcString.split('.');
+
+ // Format whole number part with spaces
+ const formattedWhole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
+
+ if (!decimal) {
+ return `${formattedWhole} BTC`;
+ }
+
+ // Find last non-zero digit
+ const lastSignificantIndex = decimal.split('').reverse().findIndex(char => char !== '0');
+ if (lastSignificantIndex === -1) {
+ return `${formattedWhole} BTC`;
+ }
+
+ // Calculate required decimal places (minimum 3, maximum 8)
+ const significantDecimals = Math.max(3, Math.min(8, decimal.length - lastSignificantIndex));
+ const formattedDecimal = decimal.slice(0, significantDecimals);
+
+ return `${formattedWhole}.${formattedDecimal} BTC`;
+ }
+ // Format satoshis with spaces
+ return satoshis.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ') + ' sat';
+}
+
+
+/**
+ * Normalizes a space name by removing '@' prefix and converting to lowercase
+ *
+ * @param {string} space - The space name to normalize
+ * @returns {string} - The normalized space name
+ */
+export function normalizeSpace(space: string): string {
+ if (!space) return '';
+ space = space.startsWith('@') ? space.substring(1) : space;
+ return space.toLowerCase();
+}
+
+/**
+ * Checks if a space name is in punycode format
+ *
+ * @param {string} space - The space name to check
+ * @returns {boolean} - True if in punycode format
+ */
+export function isPunycode(space: string): boolean {
+ if (!space || typeof space !== 'string') {
+ return false;
+ }
+
+ return space.includes('xn--');
+}
+
+/**
+ * Converts a Punycode (ASCII) space name to Unicode for display
+ *
+ * @param {string} space - The Punycode space name
+ * @returns {string} - The Unicode representation
+ */
+export function spaceToUnicode(space: string): string {
+ try {
+ // Skip conversion if not punycode
+ if (!space.includes('xn--')) {
+ return space;
+ }
+
+ // Split space into parts
+ const parts = space.split('.');
+
+ // Convert each xn-- part to unicode
+ const unicodePartsArray = parts.map(part => {
+ if (part.startsWith('xn--')) {
+ // Remove the xn-- prefix and decode
+ return punycode.decode(part.slice(4));
+ }
+ return part;
+ });
+
+ // Join parts back with dots
+ return unicodePartsArray.join('.');
+ } catch (error) {
+ console.error('Error converting to Unicode:', error);
+
+ // Remove the Intl.DisplayNames fallback as it's causing TypeScript errors
+ // and the main punycode method should be sufficient
+ return space;
+ }
+}
+
+/**
+ * Converts a Unicode space name to Punycode (ASCII)
+ *
+ * @param {string} space - The Unicode space name
+ * @returns {string} - The Punycode representation
+ */
+export function spaceToPunycode(space: string): string {
+ try {
+ // First normalize
+ space = normalizeSpace(space);
+
+ // Skip conversion if already punycode
+ if (isPunycode(space)) {
+ return space;
+ }
+
+ // Split space into parts
+ const parts = space.split('.');
+
+ // Convert each Unicode part to punycode if needed
+ const punycodePartsArray = parts.map(part => {
+ // Check if part contains non-ASCII characters
+ if (/[^\x00-\x7F]/.test(part)) {
+ return 'xn--' + punycode.encode(part);
+ }
+ return part;
+ });
+
+ // Join parts back with dots
+ return punycodePartsArray.join('.');
+ } catch (error) {
+ console.error('Error converting to Punycode:', error);
+
+ // Fallback to browser's URL constructor
+ try {
+ const url = new URL(`https://${space}`);
+ return url.hostname;
+ } catch (urlError) {
+ console.error('URL fallback failed:', urlError);
+ return space;
+ }
+ }
+}
+
+export function displayUnicodeSpace(space : string) {
+ if (isPunycode(space)) {
+ const decoded = spaceToUnicode(space);
+ if (decoded !== space) {
+ return `${space} (${decoded})`;
+ }
+ }
+ return `${space}`
+}
+
+/**
+ * Gets the correct mempool.space URL based on the Bitcoin network
+ *
+ * @param {string} path - The path to append (e.g., 'tx/abc123')
+ * @returns {string} - The full mempool.space URL
+ */
+export function getMempoolUrl(path: string): string {
+ const isTestnet = env.PUBLIC_BTC_NETWORK && env.PUBLIC_BTC_NETWORK !== 'mainnet';
+ const network = isTestnet ? `${env.PUBLIC_BTC_NETWORK}/` : '';
+ return `https://mempool.space/${network}${path}`;
+}
+
+
diff --git a/src/lib/utils/query.ts b/src/lib/utils/query.ts
new file mode 100755
index 0000000..a6aaaf9
--- /dev/null
+++ b/src/lib/utils/query.ts
@@ -0,0 +1,376 @@
+import { sql } from 'drizzle-orm';
+
+interface BlockTxsQueryParams {
+ db: DB;
+ blockIdentifier: {
+ type: 'hash' | 'height';
+ value: string | number | Buffer;
+ };
+ pagination: {
+ limit: number;
+ offset: number;
+ spaces_limit: number;
+ spaces_offset: number;
+ };
+ onlyWithSpaces?: boolean;
+}
+
+
+export async function getBlockTransactions({ db, blockIdentifier, pagination, onlyWithSpaces = false }: BlockTxsQueryParams) {
+ const blockCondition = blockIdentifier.type === 'hash' ? sql`blocks.hash = ${blockIdentifier.value}` : sql`blocks.height = ${blockIdentifier.value}`;
+ const blockValue = blockIdentifier.value;
+
+ // Add filter for transactions with Space actions if requested
+ const spacesFilter = onlyWithSpaces
+ ? sql`AND EXISTS (SELECT 1 FROM vmetaouts WHERE vmetaouts.txid = transactions.txid AND vmetaouts.name IS NOT NULL)`
+ : sql``;
+
+ // Get count if filtering
+ let totalCount: number | null = null;
+ if (onlyWithSpaces) {
+ const countResult = await db.execute(sql`
+ SELECT COUNT(DISTINCT transactions.txid)::integer as count
+ FROM transactions
+ WHERE transactions.block_hash = (
+ SELECT hash FROM blocks WHERE ${blockCondition}
+ )
+ AND EXISTS (
+ SELECT 1 FROM vmetaouts
+ WHERE vmetaouts.txid = transactions.txid
+ AND vmetaouts.name IS NOT NULL
+ )
+ `);
+ totalCount = countResult.rows[0]?.count || 0;
+ }
+
+ const queryResult = await db.execute(sql`
+ WITH limited_transactions AS (
+ SELECT
+ transactions.txid,
+ transactions.block_hash,
+ transactions.tx_hash,
+ transactions.version,
+ transactions.size,
+ transactions.index,
+ transactions.vsize,
+ transactions.weight,
+ transactions.locktime,
+ transactions.fee,
+ transactions.input_count,
+ transactions.output_count,
+ transactions.total_output_value
+ FROM transactions
+ WHERE transactions.block_hash = (
+ SELECT hash FROM blocks WHERE ${blockCondition}
+ )
+ ${spacesFilter}
+ ORDER BY transactions.index
+ LIMIT ${pagination.limit} OFFSET ${pagination.offset}),
+ limited_vmetaouts AS (
+ select
+ vmetaouts.txid as vmetaout_txid,
+ vmetaouts.value as vmetaout_value,
+ vmetaouts.name as vmetaout_name,
+ vmetaouts.action as vmetaout_action,
+ vmetaouts.burn_increment as vmetaout_burn_increment,
+ vmetaouts.total_burned as vmetaout_total_burned,
+ vmetaouts.claim_height as vmetaout_claim_height,
+ vmetaouts.expire_height as vmetaout_expire_height,
+ vmetaouts.script_error as vmetaout_script_error,
+ ROW_NUMBER() OVER (PARTITION BY vmetaouts.txid ORDER BY vmetaouts.name ASC) AS rn
+ FROM vmetaouts
+ WHERE vmetaouts.txid IN (SELECT txid FROM limited_transactions)
+ ),
+ limited_commitments AS (
+ SELECT
+ commitments.txid as commitment_txid,
+ commitments.name as commitment_name,
+ commitments.state_root as commitment_state_root,
+ commitments.revocation as commitment_revocation,
+ commitments.history_hash as commitment_history_hash,
+ ROW_NUMBER() OVER (PARTITION BY commitments.txid ORDER BY commitments.name ASC) AS rn
+ FROM commitments
+ WHERE commitments.txid IN (SELECT txid FROM limited_transactions)
+ )
+ SELECT
+ limited_transactions.txid AS txid,
+ limited_transactions.tx_hash AS tx_hash,
+ limited_transactions.version AS tx_version,
+ limited_transactions.size AS tx_size,
+ limited_transactions.index AS tx_index,
+ limited_transactions.vsize AS tx_vsize,
+ limited_transactions.weight AS tx_weight,
+ limited_transactions.locktime AS tx_locktime,
+ limited_transactions.fee AS tx_fee,
+ limited_transactions.input_count,
+ limited_transactions.output_count,
+ limited_transactions.total_output_value,
+
+ limited_vmetaouts.vmetaout_value,
+ limited_vmetaouts.vmetaout_name,
+ limited_vmetaouts.vmetaout_action,
+ limited_vmetaouts.vmetaout_burn_increment,
+ limited_vmetaouts.vmetaout_total_burned,
+ limited_vmetaouts.vmetaout_claim_height,
+ limited_vmetaouts.vmetaout_expire_height,
+ limited_vmetaouts.vmetaout_script_error,
+
+ limited_commitments.commitment_name,
+ limited_commitments.commitment_state_root,
+ limited_commitments.commitment_history_hash,
+ limited_commitments.commitment_revocation
+
+ FROM limited_transactions
+ LEFT JOIN limited_vmetaouts ON limited_vmetaouts.vmetaout_txid = limited_transactions.txid AND limited_vmetaouts.rn BETWEEN ${pagination.spaces_offset} AND ${pagination.spaces_offset+pagination.spaces_limit}
+ LEFT JOIN limited_commitments ON limited_commitments.commitment_txid = limited_transactions.txid AND limited_commitments.rn BETWEEN ${pagination.spaces_offset} AND ${pagination.spaces_offset+pagination.spaces_limit}
+ ORDER BY limited_transactions.index;
+ `);
+ return { queryResult, totalCount };
+}
+
+export async function getAuctions({
+ db,
+ limit = 20,
+ offset = 0,
+ sortBy = 'height',
+ sortDirection = 'desc'
+}) {
+ const orderByClause = {
+ height: sql`auction_end_height ${sql.raw(sortDirection)}, name ASC`,
+ name: sql`name ${sql.raw(sortDirection)}`,
+ total_burned: sql`max_total_burned ${sql.raw(sortDirection)}, auction_end_height ASC`,
+ value: sql`max_total_burned ${sql.raw(sortDirection)}, auction_end_height ASC`,
+ bid_count: sql`bid_count ${sql.raw(sortDirection)}, auction_end_height ASC`
+ }[sortBy];
+
+ const queryResult = await db.execute(sql`
+ WITH current_rollouts AS (
+ -- Get the ROLLOUT with highest claim_height for each name
+ SELECT DISTINCT ON (v.name)
+ v.*,
+ b.height as rollout_height,
+ t.index as rollout_tx_index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ WHERE v.action = 'ROLLOUT'
+ AND b.orphan = false
+ ORDER BY v.name, v.claim_height DESC),
+ auction_bids AS (
+ -- Get all valid bids for current auctions (including pre-ROLLOUT ones)
+ SELECT
+ v.*,
+ b.height,
+ t.index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ JOIN current_rollouts r ON v.name = r.name
+ WHERE v.action = 'BID'
+ AND b.orphan = false
+ AND NOT EXISTS (
+ -- No REVOKE after this bid but before/at rollout
+ SELECT 1
+ FROM vmetaouts rev
+ JOIN blocks rb ON rev.block_hash = rb.hash
+ JOIN transactions rt ON rt.block_hash = rev.block_hash AND rt.txid = rev.txid
+ WHERE rev.name = v.name
+ AND rev.action = 'REVOKE'
+ AND rb.orphan = false
+ AND (
+ rb.height > b.height
+ OR (rb.height = b.height AND rt.index > t.index)
+ )
+ AND (
+ rb.height < r.rollout_height
+ OR (rb.height = r.rollout_height AND rt.index < r.rollout_tx_index)
+ )
+ )
+ ),
+ auction_stats AS (
+ -- Calculate stats for active auctions
+ SELECT
+ r.name,
+ r.claim_height as rollout_claim_height,
+ COUNT(b.*) as bid_count,
+ COALESCE(MAX(b.total_burned), r.total_burned) as max_total_burned,
+ -- Get the latest claim height from bids or rollout
+ COALESCE(MAX(b.claim_height), r.claim_height) as auction_end_height
+ FROM current_rollouts r
+ LEFT JOIN auction_bids b ON b.name = r.name
+ WHERE NOT EXISTS (
+ -- Check the auction hasn't been ended by TRANSFER or REVOKE
+ SELECT 1
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ WHERE v.name = r.name
+ AND (v.action = 'TRANSFER' OR v.action = 'REVOKE')
+ AND b.orphan = false
+ AND (
+ b.height > r.rollout_height
+ OR (b.height = r.rollout_height AND t.index > r.rollout_tx_index)
+ )
+ )
+ GROUP BY r.name, r.claim_height, r.total_burned
+ ),
+ full_auction_data AS (
+ SELECT
+ r.*,
+ s.bid_count,
+ s.max_total_burned,
+ s.auction_end_height,
+ COUNT(*) OVER() as total_count
+ FROM current_rollouts r
+ JOIN auction_stats s ON s.name = r.name
+ ORDER BY ${
+ sortBy === 'total_burned' ? sql`s.max_total_burned ${sql.raw(sortDirection)}, s.auction_end_height ASC` :
+ sortBy === 'bid_count' ? sql`s.bid_count ${sql.raw(sortDirection)}, s.auction_end_height ASC` :
+ sql`s.auction_end_height ${sql.raw(sortDirection)}, r.name ASC`
+ }
+ LIMIT ${limit}
+ OFFSET ${offset}
+ ),
+ latest_actions AS (
+ -- Get latest valid bid/rollout for each auction
+ SELECT DISTINCT ON (v.name)
+ v.*,
+ b.height,
+ b.time,
+ f.total_count,
+ f.bid_count,
+ f.rollout_height,
+ f.max_total_burned,
+ f.auction_end_height
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ JOIN full_auction_data f ON v.name = f.name
+ WHERE v.action IN ('BID', 'ROLLOUT')
+ AND b.orphan = false
+ ORDER BY v.name, b.height DESC, t.index DESC
+ )
+ SELECT * FROM latest_actions
+ ORDER BY ${orderByClause}
+ `);
+
+ const totalCount = queryResult.rows[0]?.total_count || 0;
+ const page = Math.floor(offset / limit) + 1;
+ const totalPages = Math.ceil(totalCount / limit);
+
+ const processedResult = queryResult.rows.map(row => ({
+ ...row,
+ block_hash: row.block_hash.toString('hex'),
+ txid: row.txid.toString('hex'),
+ }));
+
+ return {
+ items: processedResult,
+ pagination: {
+ page,
+ limit,
+ total_items: totalCount,
+ total_pages: totalPages,
+ has_next: page < totalPages,
+ has_prev: page > 1
+ }
+ };
+}
+
+export async function getEndedAuctions({
+ db,
+ limit = 20,
+ offset = 0,
+ sortBy = 'height',
+ sortDirection = 'desc'
+}: AuctionQueryParams): Promise {
+ const orderByClause = {
+ height: sql`height ${sql.raw(sortDirection)}, name ASC`,
+ name: sql`name ${sql.raw(sortDirection)}`,
+ total_burned: sql`total_burned ${sql.raw(sortDirection)}, height DESC`,
+ value: sql`total_burned ${sql.raw(sortDirection)}, height DESC`,
+ bid_count: sql`bid_count ${sql.raw(sortDirection)}, height DESC`
+ }[sortBy];
+
+ const queryResult = await db.execute(sql`
+WITH latest_rollouts AS (
+ SELECT DISTINCT ON (vmetaouts.name)
+ vmetaouts.*,
+ blocks.height as rollout_height FROM vmetaouts
+ JOIN blocks ON vmetaouts.block_hash = blocks.hash
+ WHERE vmetaouts.action = 'ROLLOUT'
+ ORDER BY vmetaouts.name, blocks.height DESC
+),
+non_ended_with_stats AS (
+ SELECT
+ lr.*,
+ COALESCE(bid_stats.bid_count, 0) as bid_count,
+ COALESCE(bid_stats.max_total_burned, lr.total_burned) as max_total_burned,
+ COUNT(*) OVER() as total_count
+ FROM latest_rollouts lr
+ LEFT JOIN (
+ SELECT
+ vmetaouts.name,
+ COUNT(*) as bid_count,
+ MAX(vmetaouts.total_burned) as max_total_burned
+ FROM vmetaouts
+ JOIN blocks ON vmetaouts.block_hash = blocks.hash
+ WHERE vmetaouts.action = 'BID'
+ GROUP BY vmetaouts.name
+ ) bid_stats ON bid_stats.name = lr.name
+ WHERE EXISTS (
+ SELECT 1
+ FROM vmetaouts
+ JOIN blocks ON vmetaouts.block_hash = blocks.hash
+ WHERE vmetaouts.name = lr.name
+ AND (vmetaouts.action = 'TRANSFER' or vmetaouts.action = 'REVOKE')
+ AND blocks.height > lr.rollout_height
+ )
+ ORDER BY ${
+ sortBy === 'total_burned' ? sql`max_total_burned ${sql.raw(sortDirection)}, rollout_height DESC` :
+ sortBy === 'bid_count' ? sql`bid_count ${sql.raw(sortDirection)}, rollout_height DESC` :
+ sql`rollout_height ${sql.raw(sortDirection)}, name ASC`
+ }
+ LIMIT ${limit}
+ OFFSET ${offset}
+),
+latest_actions AS (
+ SELECT DISTINCT ON (vmetaouts.name)
+ vmetaouts.*,
+ blocks.height,
+ blocks.time,
+ non_ended_with_stats.total_count,
+ non_ended_with_stats.bid_count
+ FROM vmetaouts
+ JOIN blocks ON vmetaouts.block_hash = blocks.hash
+ JOIN non_ended_with_stats ON vmetaouts.name = non_ended_with_stats.name
+ WHERE (vmetaouts.action = 'BID' or vmetaouts.action = 'ROLLOUT')
+ AND blocks.height >= non_ended_with_stats.rollout_height
+ ORDER BY vmetaouts.name, blocks.height DESC
+)
+SELECT * FROM latest_actions
+ORDER BY ${orderByClause}
+`);
+
+ const totalCount = queryResult.rows[0]?.total_count || 0;
+ const page = Math.floor(offset / limit) + 1;
+ const totalPages = Math.ceil(totalCount / limit);
+
+ const processedResult = queryResult.rows.map(row => ({
+ ...row,
+ block_hash: row.block_hash.toString('hex'),
+ txid: row.txid.toString('hex'),
+ }));
+
+ return {
+ items: processedResult,
+ pagination: {
+ page,
+ limit,
+ total_items: totalCount,
+ total_pages: totalPages,
+ has_next: page < totalPages,
+ has_prev: page > 1
+ }
+ };
+}
diff --git a/src/lib/utils/transaction-processor.ts b/src/lib/utils/transaction-processor.ts
new file mode 100755
index 0000000..d6c1a0f
--- /dev/null
+++ b/src/lib/utils/transaction-processor.ts
@@ -0,0 +1,116 @@
+import type { Transaction, TransactionVmetaout, SpaceCommitment } from '$lib/types/transaction';
+
+export function createTransaction(row: any): Transaction {
+ const transaction: Transaction = {
+ txid: row.txid.toString('hex'),
+ tx_hash: row.tx_hash.toString('hex'),
+ version: row.tx_version,
+ size: row.tx_size,
+ vsize: row.tx_vsize,
+ weight: row.tx_weight,
+ index: row.tx_index,
+ locktime: row.tx_locktime,
+ fee: row.tx_fee,
+ input_count: row.input_count || 0,
+ output_count: row.output_count || 0,
+ total_output_value: row.total_output_value || 0,
+ vmetaouts: [],
+ commitments: []
+ };
+
+ // Add block and confirmations only if block data exists
+ if (row.block_height != null && row.block_time != null) {
+ transaction.block = {
+ height: row.block_height,
+ time: row.block_time,
+ ...(row.block_hash && { hash: row.block_hash.toString('hex') })
+ };
+
+ if (typeof row.max_height === 'number') {
+ // For mempool transactions (block_height = -1), confirmations should be 0
+ if (row.block_height === -1) {
+ transaction.confirmations = 0;
+ } else {
+ transaction.confirmations = row.max_height - row.block_height + 1;
+ }
+ }
+ }
+
+ // Add commitment data if present
+ if (row.commitment_name && row.commitment_state_root) {
+ transaction.commitment_name = row.commitment_name;
+ transaction.commitment_history_hash = row.history_hash;
+ transaction.commitment_state_root = row.commitment_state_root;
+ }
+
+ return transaction;
+}
+
+function createVMetaOutput(row: any): TransactionVmetaout | null {
+ if (!row.vmetaout_name) return null;
+
+ return {
+ value: row.vmetaout_value,
+ name: row.vmetaout_name,
+ action: row.vmetaout_action,
+ burn_increment: row.vmetaout_burn_increment,
+ total_burned: row.vmetaout_total_burned,
+ claim_height: row.vmetaout_claim_height,
+ expire_height: row.vmetaout_expire_height,
+ script_error: row.vmetaout_script_error,
+ scriptPubKey: row.vmetaout_scriptpubkey ? row.vmetaout_scriptpubkey.toString('hex') : null,
+ reason: row.vmetaout_reason,
+ signature: row.vmetaout_signature ? row.vmetaout_signature.toString('hex') : null
+ };
+}
+
+function createCommitment(row: any): SpaceCommitment | null {
+ if (!row.commitment_name) return null;
+
+ return {
+ name: row.commitment_name,
+ state_root: row.commitment_state_root ? row.commitment_state_root.toString('hex') : null,
+ state_root: row.commitment_history_hash ? row.commitment_history_hash.toString('hex') : null,
+ revocation: row.commitment_revocation || false
+ };
+}
+
+export function processTransactions(queryResult: any): Transaction[] {
+ const txs: Transaction[] = [];
+ const transactionMap = new Map();
+ const vmetaoutMap = new Map();
+ const commitmentMap = new Map();
+
+ for (const row of queryResult.rows) {
+ const txid = row.txid.toString('hex');
+ let transaction = transactionMap.get(txid);
+
+ if (!transaction) {
+ transaction = createTransaction(row);
+ transactionMap.set(txid, transaction);
+ txs.push(transaction);
+ }
+
+ const vmetaoutKey = `${txid}_${row.vmetaout_name}`; // Using name as unique identifier
+
+ if (row.vmetaout_name && !vmetaoutMap.has(vmetaoutKey)) {
+ const vmetaout = createVMetaOutput(row);
+ if (vmetaout) {
+ transaction.vmetaouts.push(vmetaout);
+ vmetaoutMap.set(vmetaoutKey, true);
+ }
+ }
+
+ const commitmentKey = `${txid}_${row.commitment_name}`; // Using txid + name as unique identifier
+
+ if (row.commitment_name && !commitmentMap.has(commitmentKey)) {
+ const commitment = createCommitment(row);
+ if (commitment) {
+ transaction.commitments.push(commitment);
+ commitmentMap.set(commitmentKey, true);
+ }
+ }
+ }
+
+ return txs;
+}
diff --git a/src/lib/utils/vmetaout.ts b/src/lib/utils/vmetaout.ts
new file mode 100755
index 0000000..dbefbbe
--- /dev/null
+++ b/src/lib/utils/vmetaout.ts
@@ -0,0 +1,74 @@
+export function getSpaceStatus(vmetaout) {
+ if (vmetaout.action == "BID" && !vmetaout.claim_height) {
+ return "pre-auction"
+ } else if (vmetaout.action == "ROLLOUT") {
+ return "rollout"
+ } else if (vmetaout.action == "BID" && vmetaout.claim_height) {
+ return "auctioned"
+ } else if (vmetaout.action == "TRANSFER") {
+ return "registered"
+ } else if (vmetaout.action == "REVOKE") {
+ return "revoked"
+ }
+
+}
+
+export function computeTimeline(vmetaout: Vmetaout, currentHeight: number): SpaceTimelineEvent[] {
+ const blockTimeInSeconds = 600; // 10 minutes per block
+ const status = vmetaout?.action;
+ const claimHeight = vmetaout?.claim_height;
+ const expireHeight = vmetaout?.expire_height;
+
+ //bid without claim height => pre-auction, nomination for rollout
+ //rollout => rolled out
+ //bid with claim height => auction is on going
+ //register => registered
+ //revoke => revoked
+
+ return [
+ {
+ name: "Open",
+ description: "Submit an open transaction to propose the space for auction",
+ done: !['REVOKE', 'OPEN'].includes(status),
+ current: status === 'OPEN'
+ },
+ {
+ name: "Pre-auction",
+ description: "Top 10 highest-bid spaces advance to auctions daily",
+ done: status === 'BID' && claimHeight || ['TRANSFER', 'ROLLOUT'].includes(status),
+ current: status === 'RESERVE' || status === 'BID' && !claimHeight
+ },
+ {
+ name: "In Auction",
+ description: claimHeight ?
+ `Auction last block: #${claimHeight-1}` :
+ "Awaiting auction start",
+ done: status === 'TRANSFER',
+ current: status === 'ROLLOUT' || status === 'BID' && claimHeight,
+ estimatedTime: (status === 'BID' && claimHeight) ?
+ ((claimHeight - currentHeight) > 0
+ ? (claimHeight - currentHeight) * blockTimeInSeconds
+ : undefined)
+ : undefined
+ },
+ {
+ name: "Awaiting claim",
+ description: "Winner must claim within the claim period",
+ done: status == 'TRANSFER',
+ current: status === 'BID' && claimHeight && claimHeight <= currentHeight,
+ /* current: status === 'BID' && claimHeight && claimHeight <= currentHeight, */
+ elapsedTime: (status === 'BID' && claimHeight && claimHeight <= currentHeight) ?
+ (currentHeight - claimHeight) * blockTimeInSeconds :
+ undefined
+ },
+ {
+ name: "Registered",
+ description: expireHeight ? `Registration expires at block #${expireHeight}` : "Space is registered",
+ done: status === 'TRANSFER',
+ current: status === 'TRANSFER',
+ estimatedTime: (expireHeight && ['TRANSFER', 'ROLLOUT'].includes(status)) ?
+ (expireHeight - currentHeight) * blockTimeInSeconds : undefined
+ }
+ ];
+ }
+
diff --git a/src/params/hash.ts b/src/params/hash.ts
new file mode 100755
index 0000000..296623e
--- /dev/null
+++ b/src/params/hash.ts
@@ -0,0 +1,3 @@
+export function match(params : string) {
+ return /^[a-fA-F0-9]{64}$/.test(params);
+}
diff --git a/src/params/height.ts b/src/params/height.ts
new file mode 100755
index 0000000..269faaa
--- /dev/null
+++ b/src/params/height.ts
@@ -0,0 +1,3 @@
+export function match(params : string) {
+ return /^\d+$/.test(params);
+}
diff --git a/explorer/src/routes/+error.svelte b/src/routes/+error.svelte
old mode 100644
new mode 100755
similarity index 100%
rename from explorer/src/routes/+error.svelte
rename to src/routes/+error.svelte
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
new file mode 100755
index 0000000..f4dff84
--- /dev/null
+++ b/src/routes/+layout.server.ts
@@ -0,0 +1,9 @@
+import '$lib/request-validation';
+
+import pkg from '../../package.json';
+
+export function load() {
+ return {
+ appVersion: pkg.version
+ };
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100755
index 0000000..a53f387
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
new file mode 100755
index 0000000..a9d8c5b
--- /dev/null
+++ b/src/routes/+page.server.ts
@@ -0,0 +1,17 @@
+import { type ServerLoad } from '@sveltejs/kit';
+
+export const load: ServerLoad = async ({ fetch, url }) => {
+
+ const searchParams = new URLSearchParams(url.search);
+
+ searchParams.set('status', 'auction');
+
+ if (!searchParams.get('sort'))
+ searchParams.set('sort', 'ending');
+
+ const [spaces, stats] = await Promise.all([
+ fetch(`/api/auctions/current`).then(x => x.body ? x.json() : null),
+ fetch('/api/stats').then(x => x.body ? x.json() : null)
+ ]);
+ return { spaces, stats };
+};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
new file mode 100755
index 0000000..ec7533f
--- /dev/null
+++ b/src/routes/+page.svelte
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+
+
+ {#if $navigating}
+
+ {:else if data.spaces?.items?.length === 0}
+
+ {:else if data.spaces?.items}
+ {#each data.spaces.items.slice(0,9) as space (space.name)}
+
+
+
+ {/each}
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+
diff --git a/src/routes/actions/mempool/+page.svelte b/src/routes/actions/mempool/+page.svelte
new file mode 100755
index 0000000..817e117
--- /dev/null
+++ b/src/routes/actions/mempool/+page.svelte
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/routes/actions/recent/+page.svelte b/src/routes/actions/recent/+page.svelte
new file mode 100755
index 0000000..76b7247
--- /dev/null
+++ b/src/routes/actions/recent/+page.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/src/routes/api/actions/rollout/+server.ts b/src/routes/api/actions/rollout/+server.ts
new file mode 100755
index 0000000..eaf6100
--- /dev/null
+++ b/src/routes/api/actions/rollout/+server.ts
@@ -0,0 +1,35 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ url }) {
+ const page = Number(url.searchParams.get('page')) || 1;
+ const limit = Number(url.searchParams.get('limit')) || 10;
+ const offset = (page - 1) * limit;
+
+ const countResult = await db.execute(sql`
+ SELECT COUNT(*) as total
+ FROM rollouts;
+ `);
+
+ const total = Number(countResult.rows[0].total);
+
+ const queryResult = await db.execute(sql`
+ SELECT *
+ FROM rollouts r
+ ORDER BY target ASC, bid desc
+ LIMIT ${limit}
+ OFFSET ${offset}
+ `);
+
+ return json({
+ items: queryResult.rows,
+ pagination: {
+ total,
+ page,
+ totalPages: Math.ceil(total / limit),
+ itemsPerPage: limit
+ }
+ });
+};
diff --git a/src/routes/api/auctions/current/+server.ts b/src/routes/api/auctions/current/+server.ts
new file mode 100755
index 0000000..767c24e
--- /dev/null
+++ b/src/routes/api/auctions/current/+server.ts
@@ -0,0 +1,34 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { getAuctions } from '$lib/utils/query';
+
+const DEFAULT_LIMIT = 20;
+const MAX_LIMIT = 100;
+
+export const GET: RequestHandler = async function ({ request, url }) {
+ const page = parseInt(url.searchParams.get('page') || '1');
+ let limit = parseInt(url.searchParams.get('limit') || String(DEFAULT_LIMIT));
+
+ if (isNaN(page) || page < 1) {
+ throw error(400, 'Invalid page parameter');
+ }
+ if (isNaN(limit) || limit < 1) {
+ throw error(400, 'Invalid limit parameter');
+ }
+
+ limit = Math.min(limit, MAX_LIMIT);
+ const offset = (page - 1) * limit;
+
+ const sortBy = (url.searchParams.get('sortBy') || 'height');
+ // const sortBy = (url.searchParams.get('sortBy') || 'bid_count');
+ const sortDirection = (url.searchParams.get('direction') || 'asc');
+ return json(await getAuctions({
+ db,
+ ended: false, // or false for current auctions
+ limit,
+ offset,
+ sortBy: sortBy as 'height' | 'name' | 'total_burned' | 'bid_count',
+ sortDirection: sortDirection as 'asc' | 'desc'
+ }));
+}
diff --git a/src/routes/api/auctions/mempool/+server.ts b/src/routes/api/auctions/mempool/+server.ts
new file mode 100755
index 0000000..1b63566
--- /dev/null
+++ b/src/routes/api/auctions/mempool/+server.ts
@@ -0,0 +1,68 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ url }) {
+ const page = Number(url.searchParams.get('page')) || 1;
+ const limit = Number(url.searchParams.get('limit')) || 20;
+ const offset = (page - 1) * limit;
+
+ const mempoolBlockHash = Buffer.from('deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef', 'hex');
+
+ // Get total count (vmetaouts + commitments)
+ const countResult = await db.execute(sql`
+ SELECT
+ (SELECT COUNT(*) FROM vmetaouts WHERE block_hash = ${mempoolBlockHash} AND name IS NOT NULL) +
+ (SELECT COUNT(*) FROM commitments WHERE block_hash = ${mempoolBlockHash}) as total
+ `);
+
+ const total = Number(countResult.rows[0].total);
+
+ // Get paginated results (vmetaouts + commitments)
+ const queryResult = await db.execute(sql`
+ WITH all_events AS (
+ SELECT
+ action::text as action,
+ name,
+ encode(txid, 'hex') as txid,
+ -1 as height,
+ NULL::bigint as time,
+ total_burned,
+ NULL as revocation,
+ 'vmetaout' as event_type
+ FROM vmetaouts
+ WHERE block_hash = ${mempoolBlockHash}
+ AND name IS NOT NULL
+
+ UNION ALL
+
+ SELECT
+ CASE WHEN revocation THEN 'COMMITMENT REVOCATION' ELSE 'COMMITMENT' END as action,
+ name,
+ encode(txid, 'hex') as txid,
+ -1 as height,
+ NULL::bigint as time,
+ NULL as total_burned,
+ revocation,
+ 'commitment' as event_type
+ FROM commitments
+ WHERE block_hash = ${mempoolBlockHash}
+ )
+ SELECT *
+ FROM all_events
+ ORDER BY name DESC
+ LIMIT ${limit}
+ OFFSET ${offset}
+ `);
+
+ return json({
+ items: queryResult.rows,
+ pagination: {
+ total,
+ page,
+ totalPages: Math.ceil(total / limit),
+ itemsPerPage: limit
+ }
+ });
+};
diff --git a/src/routes/api/auctions/past/+server.ts b/src/routes/api/auctions/past/+server.ts
new file mode 100755
index 0000000..b4d942b
--- /dev/null
+++ b/src/routes/api/auctions/past/+server.ts
@@ -0,0 +1,34 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { getEndedAuctions } from '$lib/utils/query';
+
+const DEFAULT_LIMIT = 20;
+const MAX_LIMIT = 100;
+
+export const GET: RequestHandler = async function ({ request, url }) {
+ const page = parseInt(url.searchParams.get('page') || '1');
+ let limit = parseInt(url.searchParams.get('limit') || String(DEFAULT_LIMIT));
+
+ if (isNaN(page) || page < 1) {
+ throw error(400, 'Invalid page parameter');
+ }
+ if (isNaN(limit) || limit < 1) {
+ throw error(400, 'Invalid limit parameter');
+ }
+
+ limit = Math.min(limit, MAX_LIMIT);
+ const offset = (page - 1) * limit;
+
+ const sortBy = (url.searchParams.get('sortBy') || 'bid_count');
+ const sortDirection = (url.searchParams.get('direction') || 'desc');
+ return json(await getEndedAuctions({
+ db,
+ ended: false, // or false for current auctions
+ limit,
+ offset,
+ sortBy: sortBy as 'height' | 'name' | 'total_burned' | 'bid_count',
+ sortDirection: sortDirection as 'asc' | 'desc'
+ }));
+}
+
diff --git a/src/routes/api/auctions/recent/+server.ts b/src/routes/api/auctions/recent/+server.ts
new file mode 100755
index 0000000..a90468c
--- /dev/null
+++ b/src/routes/api/auctions/recent/+server.ts
@@ -0,0 +1,67 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ url }) {
+ const page = Number(url.searchParams.get('page')) || 1;
+ const limit = Number(url.searchParams.get('limit')) || 20;
+ const offset = (page - 1) * limit;
+
+ // Get total count (vmetaouts + commitments)
+ const countResult = await db.execute(sql`
+ SELECT
+ (SELECT COUNT(*) FROM vmetaouts v JOIN blocks b ON v.block_hash = b.hash WHERE b.height >= 0 AND v.name IS NOT NULL) +
+ (SELECT COUNT(*) FROM commitments c JOIN blocks b ON c.block_hash = b.hash WHERE b.orphan = false) as total
+ `);
+
+ const total = Number(countResult.rows[0].total);
+
+ // Get paginated results (vmetaouts + commitments)
+ const queryResult = await db.execute(sql`
+ WITH all_events AS (
+ SELECT
+ v.action::text as action,
+ v.name,
+ encode(v.txid, 'hex') as txid,
+ b.height,
+ b.time,
+ v.total_burned,
+ NULL as revocation,
+ 'vmetaout' as event_type
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE b.height >= 0 AND v.name IS NOT NULL
+
+ UNION ALL
+
+ SELECT
+ CASE WHEN c.revocation THEN 'COMMITMENT REVOCATION' ELSE 'COMMITMENT' END as action,
+ c.name,
+ encode(c.txid, 'hex') as txid,
+ b.height,
+ b.time,
+ NULL as total_burned,
+ c.revocation,
+ 'commitment' as event_type
+ FROM commitments c
+ JOIN blocks b ON c.block_hash = b.hash
+ WHERE b.orphan = false
+ )
+ SELECT *
+ FROM all_events
+ ORDER BY height DESC, time DESC
+ LIMIT ${limit}
+ OFFSET ${offset}
+ `);
+
+ return json({
+ items: queryResult.rows,
+ pagination: {
+ total,
+ page,
+ totalPages: Math.ceil(total / limit),
+ itemsPerPage: limit
+ }
+ });
+};
diff --git a/src/routes/api/block/[hash=hash]/header/+server.ts b/src/routes/api/block/[hash=hash]/header/+server.ts
new file mode 100755
index 0000000..b8c55ff
--- /dev/null
+++ b/src/routes/api/block/[hash=hash]/header/+server.ts
@@ -0,0 +1,53 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ params }) {
+ const bufHash = Buffer.from(params.hash, 'hex');
+ const queryResult = await db.execute(sql `
+ SELECT
+ blocks.*,
+ COALESCE(max_block.max_height, 0) as max_height,
+ COALESCE(tx_count.total_transactions, 0) as total_transactions,
+ COALESCE(vmetaout_count.total_vmetaouts, 0) as total_vmetaouts
+ FROM blocks
+ CROSS JOIN ( SELECT COALESCE(MAX(height), 0) as max_height FROM blocks) as max_block
+ LEFT JOIN (
+ SELECT COUNT(*) as total_transactions
+ FROM transactions
+ WHERE block_hash = ${bufHash}
+ ) as tx_count ON true
+ LEFT JOIN (
+ SELECT COUNT(*) as total_vmetaouts
+ FROM vmetaouts
+ WHERE block_hash = ${bufHash} and action is not null
+ ) as vmetaout_count ON true
+ WHERE blocks.hash = ${bufHash};`)
+
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return error(404, 'Block not found');
+ }
+
+ const blockHeader = {
+ hash: queryResult.rows[0].hash.toString('hex'),
+ size: queryResult.rows[0].size,
+ stripped_size: queryResult.rows[0].stripped_size,
+ weight: queryResult.rows[0].weight,
+ height: queryResult.rows[0].height,
+ version: queryResult.rows[0].version,
+ hash_merkle_root: queryResult.rows[0].hash_merkle_root.toString('hex'),
+ time: queryResult.rows[0].time,
+ median_time: queryResult.rows[0].median_time,
+ nonce: queryResult.rows[0].nonce,
+ bits: queryResult.rows[0].bits.toString('hex'),
+ difficulty: queryResult.rows[0].difficulty,
+ chainwork: queryResult.rows[0].chainwork.toString('hex'),
+ orphan: queryResult.rows[0].orphan,
+ confirmations: queryResult.rows[0].max_height - queryResult.rows[0].height,
+ tx_count: queryResult.rows[0].total_transactions,
+ vmetaout_count: queryResult.rows[0].total_vmetaouts,
+ };
+
+ return json(blockHeader);
+};
diff --git a/src/routes/api/block/[hash=hash]/txs/+server.ts b/src/routes/api/block/[hash=hash]/txs/+server.ts
new file mode 100755
index 0000000..431fc7f
--- /dev/null
+++ b/src/routes/api/block/[hash=hash]/txs/+server.ts
@@ -0,0 +1,46 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { processTransactions } from '$lib/utils/transaction-processor';
+import { getBlockTransactions } from '$lib/utils/query';
+
+export const GET: RequestHandler = async function ({ url, params }) {
+ let limit = parseInt(url.searchParams.get('limit') || '25');
+ if (limit > 50) {
+ limit = 50
+ }
+ const offset = parseInt(url.searchParams.get('offset') || '0');
+ const onlyWithSpaces = url.searchParams.get('onlyWithSpaces') === 'true';
+ const block_hash = Buffer.from(params.hash, 'hex');
+
+ if (!block_hash) {
+ error(404, "No hash provided");
+ }
+ const { queryResult, totalCount } = await getBlockTransactions({
+ db,
+ blockIdentifier: { type: 'hash', value: block_hash },
+ pagination: {
+ limit,
+ offset,
+ spaces_limit: 10,
+ spaces_offset: 0
+ },
+ onlyWithSpaces
+ });
+
+ // If filtering by spaces and we have a totalCount, we know the block exists
+ // Return empty array if no transactions match the filter
+ if (onlyWithSpaces && totalCount !== null) {
+ const txs = processTransactions(queryResult);
+ return json({ transactions: txs, totalCount });
+ }
+
+ // If not filtering and we get no results, the block doesn't exist
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return error(404, 'Block not found');
+ }
+
+ const txs = processTransactions(queryResult);
+
+ return json({ transactions: txs, totalCount });
+}
diff --git a/src/routes/api/block/[height=height]/header/+server.ts b/src/routes/api/block/[height=height]/header/+server.ts
new file mode 100755
index 0000000..c422c9a
--- /dev/null
+++ b/src/routes/api/block/[height=height]/header/+server.ts
@@ -0,0 +1,53 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ params }) {
+ const queryResult = await db.execute(sql `
+ SELECT
+ blocks.*,
+ COALESCE(max_block.max_height, 0) as max_height,
+ COALESCE(tx_count.total_transactions, 0) as total_transactions,
+ COALESCE(vmetaout_count.total_vmetaouts, 0) as total_vmetaouts
+ FROM blocks
+ CROSS JOIN ( SELECT COALESCE(MAX(height), 0) as max_height FROM blocks) as max_block
+ LEFT JOIN (
+ SELECT COUNT(*) as total_transactions
+ FROM transactions
+ WHERE transactions.block_hash = (SELECT hash FROM blocks WHERE blocks.height = ${params.height})
+ ) as tx_count ON true
+ LEFT JOIN (
+ SELECT COUNT(*) as total_vmetaouts
+ FROM vmetaouts
+ WHERE block_hash = (select hash from blocks where blocks.height = ${params.height}) and action is not null
+ ) as vmetaout_count ON true
+
+ WHERE blocks.height = ${params.height};`)
+
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return error(404, 'Block not found');
+ }
+
+ const blockHeader = {
+ hash: queryResult.rows[0].hash.toString('hex'),
+ size: queryResult.rows[0].size,
+ stripped_size: queryResult.rows[0].stripped_size,
+ weight: queryResult.rows[0].weight,
+ height: queryResult.rows[0].height,
+ version: queryResult.rows[0].version,
+ hash_merkle_root: queryResult.rows[0].hash_merkle_root.toString('hex'),
+ time: queryResult.rows[0].time,
+ median_time: queryResult.rows[0].median_time,
+ nonce: queryResult.rows[0].nonce,
+ bits: queryResult.rows[0].bits.toString('hex'),
+ difficulty: queryResult.rows[0].difficulty,
+ chainwork: queryResult.rows[0].chainwork.toString('hex'),
+ orphan: queryResult.rows[0].orphan,
+ confirmations: queryResult.rows[0].max_height - queryResult.rows[0].height,
+ tx_count: queryResult.rows[0].total_transactions,
+ vmetaout_count: queryResult.rows[0].total_vmetaouts,
+ };
+
+ return json(blockHeader);
+};
diff --git a/src/routes/api/block/[height=height]/txs/+server.ts b/src/routes/api/block/[height=height]/txs/+server.ts
new file mode 100755
index 0000000..b72020d
--- /dev/null
+++ b/src/routes/api/block/[height=height]/txs/+server.ts
@@ -0,0 +1,42 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { processTransactions } from '$lib/utils/transaction-processor';
+import { getBlockTransactions } from '$lib/utils/query';
+
+export const GET: RequestHandler = async function ({ url, params }) {
+ let limit = parseInt(url.searchParams.get('limit') || '25');
+ if (limit > 50) {
+ limit = 50
+ }
+ const offset = parseInt(url.searchParams.get('offset') || '0');
+ const onlyWithSpaces = url.searchParams.get('onlyWithSpaces') === 'true';
+
+ const { queryResult, totalCount } = await getBlockTransactions({
+ db,
+ blockIdentifier: { type: 'height', value: params.height },
+ pagination: {
+ limit,
+ offset,
+ spaces_limit: 10,
+ spaces_offset: 0
+ },
+ onlyWithSpaces
+ });
+
+ // If filtering by spaces and we have a totalCount, we know the block exists
+ // Return empty array if no transactions match the filter
+ if (onlyWithSpaces && totalCount !== null) {
+ const txs = processTransactions(queryResult);
+ return json({ transactions: txs, totalCount });
+ }
+
+ // If not filtering and we get no results, the block doesn't exist
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return error(404, 'Block not found');
+ }
+
+ const txs = processTransactions(queryResult);
+
+ return json({ transactions: txs, totalCount })
+}
diff --git a/src/routes/api/healthcheck/+server.ts b/src/routes/api/healthcheck/+server.ts
new file mode 100755
index 0000000..8ba414f
--- /dev/null
+++ b/src/routes/api/healthcheck/+server.ts
@@ -0,0 +1,23 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ request, url }) {
+
+ const queryResult = await db.execute(sql`
+WITH latest_block AS (
+ SELECT height, time
+ FROM blocks
+ WHERE NOT orphan
+ ORDER BY height DESC
+ LIMIT 1
+)
+SELECT
+ lb.height as latest_block_height
+FROM latest_block lb;
+ `);
+
+ return json(queryResult.rows[0])
+}
+
diff --git a/src/routes/api/root-anchors.json/+server.ts b/src/routes/api/root-anchors.json/+server.ts
new file mode 100755
index 0000000..fbe0b21
--- /dev/null
+++ b/src/routes/api/root-anchors.json/+server.ts
@@ -0,0 +1,37 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function () {
+ try {
+ const queryResult = await db.execute(sql`
+ SELECT
+ spaces_root,
+ pointers_root,
+ hash,
+ height
+ FROM blocks
+ WHERE spaces_root IS NOT NULL
+ ORDER BY height DESC LIMIT 120;
+ `);
+
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return json([]);
+ }
+
+ const formattedAnchors = queryResult.rows.map(row => ({
+ spaces_root: row.spaces_root.toString('hex'),
+ ptrs_root: row.pointers_root.toString('hex'),
+ block: {
+ hash: row.hash.toString('hex'),
+ height: row.height
+ }
+ }));
+
+ return json(formattedAnchors);
+ } catch (error) {
+ console.error('Error fetching root anchors:', error);
+ return json({ error: 'Failed to fetch root anchors' }, { status: 500 });
+ }
+};
diff --git a/src/routes/api/search/+server.ts b/src/routes/api/search/+server.ts
new file mode 100755
index 0000000..8f796ab
--- /dev/null
+++ b/src/routes/api/search/+server.ts
@@ -0,0 +1,215 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+import { addressToScriptPubKey } from '$lib/utils/address-parsers';
+import { env } from '$env/dynamic/public';
+
+// Helper to get mempool.space API URL
+function getMempoolApiUrl(path: string): string {
+ const isTestnet = env.PUBLIC_BTC_NETWORK && env.PUBLIC_BTC_NETWORK !== 'mainnet';
+ const network = isTestnet ? `${env.PUBLIC_BTC_NETWORK}/` : '';
+ return `https://mempool.space/${network}api/${path}`;
+}
+
+function getMempoolUrl(path: string): string {
+ const isTestnet = env.PUBLIC_BTC_NETWORK && env.PUBLIC_BTC_NETWORK !== 'mainnet';
+ const network = isTestnet ? `${env.PUBLIC_BTC_NETWORK}/` : '';
+ return `https://mempool.space/${network}${path}`;
+}
+
+export const GET: RequestHandler = async function ({ url }) {
+ const search = url.searchParams.get('q');
+ if (!search)
+ return json([]);
+ const result = [];
+ const hashRegexp = /^[a-fA-F0-9]{64}$/;
+ const heightRegexp = /^\d+$/;
+ let foundLocally = false;
+
+ // Try to parse as address first
+ // try {
+ // const scriptPubKey = Buffer.from(addressToScriptPubKey(search), 'hex');
+ //
+ // const addressTx = await db.execute(sql`
+ // SELECT 1 FROM tx_outputs
+ // WHERE scriptPubKey = ${scriptPubKey}
+ // LIMIT 1
+ // `);
+ //
+ // if (addressTx.rows[0]) {
+ // result.push({
+ // type: "address",
+ // value: {
+ // address: search
+ // }
+ // });
+ // }
+ // } catch (e) {
+ // // Not a valid address, continue with other searches
+ // }
+
+ //looks like hash, search for txid or block hash
+ if (hashRegexp.test(search)) {
+ const hexString = Buffer.from(search, 'hex');
+
+ const transaction = await db.execute(sql`
+ SELECT transactions.txid, transactions.block_hash
+ FROM transactions
+ WHERE txid = ${hexString}
+ LIMIT 1
+ `);
+ if (transaction.rows[0]) {
+ foundLocally = true;
+ result.push({
+ type: "transaction",
+ value: {
+ ...transaction.rows[0],
+ txid: transaction.rows[0].txid.toString('hex'),
+ block_hash: transaction.rows[0].block_hash.toString('hex')
+ }
+ });
+ }
+ const block = await db.execute(sql`
+ SELECT blocks.hash, blocks.height
+ FROM blocks
+ WHERE hash = ${hexString}
+ LIMIT 1
+ `);
+ if (block.rows[0]) {
+ foundLocally = true;
+ result.push({
+ type: "block",
+ value: {
+ ...block.rows[0],
+ hash: block.rows[0].hash.toString('hex'),
+ height: block.rows[0].height
+ }
+ });
+ }
+
+ // If not found locally, check mempool.space
+ if (!foundLocally) {
+ try {
+ // Try as transaction first
+ const txResponse = await fetch(getMempoolApiUrl(`tx/${search}`));
+ if (txResponse.ok) {
+ result.push({
+ type: "external-transaction",
+ value: {
+ txid: search,
+ url: getMempoolUrl(`tx/${search}`)
+ }
+ });
+ } else {
+ // Try as block hash
+ const blockResponse = await fetch(getMempoolApiUrl(`block/${search}`));
+ if (blockResponse.ok) {
+ const blockData = await blockResponse.json();
+ result.push({
+ type: "external-block",
+ value: {
+ hash: search,
+ height: blockData.height,
+ url: getMempoolUrl(`block/${search}`)
+ }
+ });
+ }
+ }
+ } catch (e) {
+ // Ignore errors from external API
+ }
+ }
+ }
+ //looks like height
+ else if (heightRegexp.test(search)) {
+ const height = +search;
+ if (height <= 2**32) {
+ const block = await db.execute(sql`
+ SELECT blocks.hash, blocks.height
+ FROM blocks
+ WHERE height = ${height}
+ LIMIT 1
+ `);
+ if (block.rows[0]) {
+ foundLocally = true;
+ result.push({
+ type: "block",
+ value: {
+ ...block.rows[0],
+ hash: block.rows[0].hash.toString('hex'),
+ height: block.rows[0].height
+ }
+ });
+ }
+
+ // If not found locally, check mempool.space
+ if (!foundLocally) {
+ try {
+ const blockResponse = await fetch(getMempoolApiUrl(`block-height/${height}`));
+ if (blockResponse.ok) {
+ const blockHash = await blockResponse.text();
+ result.push({
+ type: "external-block",
+ value: {
+ hash: blockHash,
+ height: height,
+ url: getMempoolUrl(`block/${blockHash}`)
+ }
+ });
+ }
+ } catch (e) {
+ // Ignore errors from external API
+ }
+ }
+ }
+ }
+
+ // the rest should be a space
+ const strippedSpace = search.startsWith('@') ? search.substring(1) : search;
+ const names = await db.execute(sql`
+ WITH matching_spaces AS (
+ SELECT DISTINCT
+ name,
+ similarity(name, ${strippedSpace}) AS similarity_score
+ FROM vmetaouts
+ WHERE similarity(name, ${strippedSpace}) > 0
+ ORDER BY similarity_score DESC, name ASC
+ LIMIT 3
+ ),
+ latest_actions AS (
+ SELECT DISTINCT ON (v.name)
+ v.name,
+ v.action,
+ v.claim_height,
+ v.expire_height,
+ b.height AS block_height
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE v.name IN (SELECT name FROM matching_spaces)
+ AND b.orphan = false
+ AND v.action != 'REJECT'
+ ORDER BY v.name, b.height DESC
+ ),
+ current_height AS (
+ SELECT MAX(height) as height FROM blocks
+ )
+ SELECT
+ ms.name,
+ ms.similarity_score,
+ la.action,
+ la.claim_height,
+ la.expire_height,
+ la.block_height,
+ ch.height as current_height
+ FROM matching_spaces ms
+ LEFT JOIN latest_actions la ON ms.name = la.name
+ CROSS JOIN current_height ch
+ ORDER BY ms.similarity_score DESC, ms.name ASC
+ `);
+ for (const space of names.rows) {
+ result.push({ type: "space", value: space });
+ }
+
+ return json(result);
+}
diff --git a/src/routes/api/space/[name]/commitment/+server.ts b/src/routes/api/space/[name]/commitment/+server.ts
new file mode 100755
index 0000000..0f7b5d3
--- /dev/null
+++ b/src/routes/api/space/[name]/commitment/+server.ts
@@ -0,0 +1,42 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ params }) {
+ const spaceName = params.name;
+
+ if (!spaceName) {
+ throw error(400, 'Space name is required');
+ }
+
+ const queryResult = await db.execute(sql`
+ SELECT
+ c.name,
+ encode(c.block_hash, 'hex') as block_hash,
+ encode(c.txid, 'hex') as txid,
+ encode(c.state_root, 'hex') as state_root,
+ encode(c.history_hash, 'hex') as history_hash,
+ c.revocation,
+ b.height as block_height,
+ t.index as tx_index
+ FROM commitments c
+ JOIN blocks b ON c.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = c.block_hash AND t.txid = c.txid
+ WHERE c.name = ${spaceName}
+ AND b.orphan = false
+ ORDER BY b.height DESC, t.index DESC
+ LIMIT 1
+ `);
+
+ if (queryResult.rows.length === 0) {
+ throw error(404, 'No commitment found for this space');
+ }
+
+ // Return null if the commitment has been revoked
+ if (queryResult.rows[0].revocation) {
+ return json(null);
+ }
+
+ return json(queryResult.rows[0]);
+};
diff --git a/src/routes/api/space/[name]/commitments/+server.ts b/src/routes/api/space/[name]/commitments/+server.ts
new file mode 100755
index 0000000..8924cfe
--- /dev/null
+++ b/src/routes/api/space/[name]/commitments/+server.ts
@@ -0,0 +1,57 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+const ITEMS_PER_PAGE = 10;
+
+export const GET: RequestHandler = async function ({ params, url }) {
+ let spaceName = params.name.toLowerCase();
+ const page = Number(url.searchParams.get('page')) || 1;
+ const offset = (page - 1) * ITEMS_PER_PAGE;
+
+ if (spaceName.startsWith('@')) {
+ spaceName = spaceName.slice(1);
+ }
+
+ const queryResult = await db.execute(sql`
+ WITH counts AS (
+ SELECT COUNT(*) as total_commitments
+ FROM commitments c
+ JOIN blocks b ON c.block_hash = b.hash
+ WHERE c.name = ${spaceName} AND b.orphan = false
+ )
+ SELECT
+ c.name,
+ encode(c.block_hash, 'hex') as block_hash,
+ encode(c.txid, 'hex') as txid,
+ encode(c.state_root, 'hex') as state_root,
+ encode(c.history_hash, 'hex') as history_hash,
+ c.revocation,
+ b.height as block_height,
+ b.time as block_time,
+ t.index as tx_index,
+ counts.total_commitments
+ FROM commitments c
+ JOIN blocks b ON c.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = c.block_hash AND t.txid = c.txid
+ CROSS JOIN counts
+ WHERE c.name = ${spaceName}
+ AND b.orphan = false
+ ORDER BY b.height DESC, t.index DESC
+ LIMIT ${ITEMS_PER_PAGE}
+ OFFSET ${offset}
+ `);
+
+ const total = queryResult.rows[0]?.total_commitments || 0;
+
+ return json({
+ items: queryResult.rows,
+ pagination: {
+ total,
+ page,
+ totalPages: Math.ceil(total / ITEMS_PER_PAGE),
+ itemsPerPage: ITEMS_PER_PAGE
+ }
+ });
+};
diff --git a/src/routes/api/space/[name]/history/+server.ts b/src/routes/api/space/[name]/history/+server.ts
new file mode 100755
index 0000000..9b58cc7
--- /dev/null
+++ b/src/routes/api/space/[name]/history/+server.ts
@@ -0,0 +1,180 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+import type { SpaceData, Vmetaout } from '$lib/types/space';
+
+const ITEMS_PER_PAGE = 10;
+
+export const GET: RequestHandler = async function ({ params, url }) {
+ let spaceName = params.name.toLowerCase();
+ const page = Number(url.searchParams.get('page')) || 1;
+ const offset = (page - 1) * ITEMS_PER_PAGE;
+
+ if (spaceName.startsWith('@')) {
+ spaceName = spaceName.slice(1);
+ }
+
+ // Query 1: Get latest state and current height
+ const latestResult = await db.execute(sql`
+ WITH max_block AS (
+ SELECT MAX(height) as max_height
+ FROM blocks
+ ),
+ latest_action AS (
+ SELECT
+ v.block_hash,
+ v.txid,
+ v.name,
+ v.burn_increment,
+ v.total_burned,
+ v.value,
+ v.action,
+ v.claim_height,
+ v.expire_height,
+ v.reason,
+ v.script_error,
+ b.height AS block_height,
+ b.time AS block_time,
+ t.index as tx_index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.txid = v.txid AND t.block_hash = v.block_hash
+ WHERE v.name = ${spaceName} AND b.orphan is false AND v.action != 'REJECT'
+ ORDER BY b.height DESC, t.index DESC
+ LIMIT 1
+ )
+ SELECT
+ l.*,
+ m.max_height as current_height
+ FROM latest_action l
+ CROSS JOIN max_block m
+ `);
+
+ // Query 2: Get paginated history (vmetaouts + commitments) and counts
+ const historyResult = await db.execute(sql`
+ WITH counts AS (
+ SELECT
+ (SELECT COUNT(*) FROM vmetaouts v JOIN blocks b ON v.block_hash = b.hash WHERE v.name = ${spaceName} AND b.orphan is false) +
+ (SELECT COUNT(*) FROM commitments c JOIN blocks b ON c.block_hash = b.hash WHERE c.name = ${spaceName} AND b.orphan is false) as total_actions,
+ COUNT(CASE WHEN action = 'BID' THEN 1 END) as bid_count
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE v.name = ${spaceName} AND b.orphan is false
+ ),
+ all_events AS (
+ SELECT
+ v.block_hash,
+ v.txid,
+ v.name,
+ v.burn_increment,
+ v.total_burned,
+ v.value,
+ v.action::text as action,
+ v.claim_height,
+ v.expire_height,
+ v.reason,
+ v.script_error,
+ NULL::bytea as state_root,
+ NULL::bytea as history_hash,
+ false as revocation,
+ 'vmetaout' as event_type,
+ b.height AS block_height,
+ b.time AS block_time,
+ t.index as tx_index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.txid = v.txid AND t.block_hash = v.block_hash
+ WHERE v.name = ${spaceName} AND b.orphan is false
+
+ UNION ALL
+
+ SELECT
+ c.block_hash,
+ c.txid,
+ c.name,
+ NULL as burn_increment,
+ NULL as total_burned,
+ NULL as value,
+ CASE WHEN c.revocation THEN 'COMMITMENT REVOCATION' ELSE 'COMMITMENT' END as action,
+ NULL as claim_height,
+ NULL as expire_height,
+ NULL as reason,
+ NULL as script_error,
+ c.state_root,
+ c.history_hash,
+ c.revocation,
+ 'commitment' as event_type,
+ b.height AS block_height,
+ b.time AS block_time,
+ t.index as tx_index
+ FROM commitments c
+ JOIN blocks b ON c.block_hash = b.hash
+ JOIN transactions t ON t.txid = c.txid AND t.block_hash = c.block_hash
+ WHERE c.name = ${spaceName} AND b.orphan is false
+ )
+ SELECT
+ e.*,
+ c.total_actions,
+ c.bid_count
+ FROM all_events e
+ CROSS JOIN counts c
+ ORDER BY
+ CASE
+ WHEN e.block_height = -1 THEN 1
+ ELSE 0
+ END DESC,
+ e.block_height DESC,
+ e.tx_index DESC
+ LIMIT ${ITEMS_PER_PAGE}
+ OFFSET ${offset}
+ `);
+
+
+ const processVmetaout = (row: any): Vmetaout => ({
+ ...row,
+ block_hash: row.block_hash.toString('hex'),
+ txid: row.txid.toString('hex'),
+ state_root: row.state_root ? row.state_root.toString('hex') : null,
+ history_hash: row.history_hash ? row.history_hash.toString('hex') : null,
+ });
+
+ const total = historyResult.rows[0]?.total_actions || 0;
+
+ // If no data found
+ if (total === 0) {
+ return json({
+ latest: null,
+ items: [],
+ stats: {
+ total: 0,
+ bidCount: 0,
+ },
+ pagination: {
+ total: 0,
+ page: 1,
+ totalPages: 0,
+ itemsPerPage: ITEMS_PER_PAGE
+ },
+ currentHeight: latestResult.rows[0]?.current_height || 0
+ });
+ }
+
+ const items = historyResult.rows.map(processVmetaout);
+
+ return json({
+ latest: latestResult.rows[0] ? processVmetaout(latestResult.rows[0]) : null,
+ items,
+ stats: {
+ total,
+ bidCount: historyResult.rows[0].bid_count
+ },
+ pagination: {
+ total,
+ page,
+ totalPages: Math.ceil(total / ITEMS_PER_PAGE),
+ itemsPerPage: ITEMS_PER_PAGE
+ },
+ currentHeight: latestResult.rows[0]?.current_height || 0
+ });
+};
diff --git a/src/routes/api/space/[name]/stats/+server.ts b/src/routes/api/space/[name]/stats/+server.ts
new file mode 100755
index 0000000..c8c6958
--- /dev/null
+++ b/src/routes/api/space/[name]/stats/+server.ts
@@ -0,0 +1,194 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import type { RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+import { env } from '$env/dynamic/private';
+
+const MARKETPLACE_URI = env.MARKETPLACE_URI || 'https://spaces.market/';
+
+async function checkMarketplaceListing(spaceName: string): Promise {
+ try {
+ const response = await fetch(`${MARKETPLACE_URI}/api/space/${spaceName}`, {
+ method: 'GET',
+ headers: { 'Accept': 'application/json', },
+ signal: AbortSignal.timeout(2500)
+ });
+ return response.ok;
+ } catch (error) {
+ // console.warn(`Failed to check marketplace for space ${spaceName}:`, error);
+ return false;
+ }
+}
+
+export const GET: RequestHandler = async function ({ params }) {
+ const spaceName = params.name;
+
+ if (!spaceName) {
+ throw error(400, 'Space name is required');
+ }
+
+ const [queryResult, isListedInMarketplace] = await Promise.all([
+ db.execute(sql`
+ WITH current_rollout AS (
+ -- Get the latest non-revoked ROLLOUT
+ SELECT DISTINCT ON (v.name)
+ v.*,
+ b.height as rollout_height,
+ t.index as rollout_tx_index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ WHERE v.action = 'ROLLOUT'
+ AND v.name = ${spaceName}
+ AND b.orphan = false
+ AND NOT EXISTS (
+ -- Check if this ROLLOUT wasn't revoked
+ SELECT 1
+ FROM vmetaouts rev
+ JOIN blocks rb ON rev.block_hash = rb.hash
+ JOIN transactions rt ON rt.block_hash = rev.block_hash AND rt.txid = rev.txid
+ WHERE rev.name = v.name
+ AND rev.action = 'REVOKE'
+ AND rb.orphan = false
+ AND (rb.height > b.height OR (rb.height = b.height AND rt.index > t.index))
+ )
+ ORDER BY v.name, v.claim_height DESC
+ ),
+ valid_bids AS (
+ -- Get all valid bids for current auction (both pre and post ROLLOUT)
+ SELECT
+ b.*,
+ bb.height as bid_height,
+ bt.index as bid_index,
+ CASE
+ WHEN bb.height < r.rollout_height OR
+ (bb.height = r.rollout_height AND bt.index < r.rollout_tx_index)
+ THEN 'pre_rollout'
+ ELSE 'post_rollout'
+ END as bid_timing
+ FROM vmetaouts b
+ JOIN blocks bb ON b.block_hash = bb.hash
+ JOIN transactions bt ON bt.block_hash = b.block_hash AND bt.txid = b.txid
+ JOIN current_rollout r ON b.name = r.name
+ WHERE b.action = 'BID'
+ AND bb.orphan = false
+ AND NOT EXISTS (
+ -- No REVOKE after this bid but before/at ROLLOUT
+ SELECT 1
+ FROM vmetaouts rev
+ JOIN blocks rb ON rev.block_hash = rb.hash
+ JOIN transactions rt ON rt.block_hash = rev.block_hash AND rt.txid = rev.txid
+ WHERE rev.name = b.name
+ AND rev.action = 'REVOKE'
+ AND rb.orphan = false
+ AND (
+ rb.height > bb.height OR
+ (rb.height = bb.height AND rt.index > bt.index)
+ )
+ AND (
+ rb.height < r.rollout_height OR
+ (rb.height = r.rollout_height AND rt.index <= r.rollout_tx_index)
+ )
+ )
+ ),
+ historical_stats AS (
+ -- Get all historical stats regardless of validity
+ SELECT
+ COUNT(*) as total_actions,
+ COUNT(CASE WHEN action = 'BID' THEN 1 END) as total_bids_all_time,
+ MAX(CASE WHEN action = 'BID' THEN total_burned END) as highest_bid_all_time
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE v.name = ${spaceName}
+ AND b.orphan = false
+ ),
+ latest_outpoint AS (
+ -- Get the latest valid outpoint for this name
+ SELECT DISTINCT ON (v.name)
+ v.outpoint_txid as txid,
+ v.outpoint_index as index
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ WHERE v.name = ${spaceName}
+ AND b.orphan = false
+ AND v.outpoint_txid IS NOT NULL
+ AND v.outpoint_index IS NOT NULL
+ ORDER BY v.name, b.height DESC, t.index DESC
+ ),
+ auction_status AS (
+ -- Calculate current auction stats
+ SELECT
+ r.name,
+ r.rollout_height as block_height,
+ r.rollout_tx_index as tx_index,
+ COALESCE(MAX(b.claim_height), r.claim_height) as claim_height,
+ r.total_burned as winning_bid,
+ COUNT(b.*) as total_bids,
+ MAX(b.total_burned) as highest_bid,
+ COUNT(CASE WHEN b.bid_timing = 'pre_rollout' THEN 1 END) as pre_rollout_bids,
+ COUNT(CASE WHEN b.bid_timing = 'post_rollout' THEN 1 END) as post_rollout_bids,
+ NOT EXISTS (
+ -- Check if auction is still active
+ SELECT 1
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ JOIN transactions t ON t.block_hash = v.block_hash AND t.txid = v.txid
+ WHERE v.name = r.name
+ AND (v.action = 'TRANSFER' OR v.action = 'REVOKE')
+ AND b.orphan = false
+ AND (
+ b.height > r.rollout_height OR
+ (b.height = r.rollout_height AND t.index > r.rollout_tx_index)
+ )
+ ) as is_active
+ FROM current_rollout r
+ LEFT JOIN valid_bids b ON true
+ GROUP BY r.name, r.rollout_height, r.rollout_tx_index, r.claim_height, r.total_burned
+ )
+ SELECT
+ ${spaceName} as name,
+ a.block_height,
+ a.tx_index,
+ a.claim_height,
+ a.winning_bid,
+ CASE WHEN a.is_active THEN a.total_bids ELSE 0 END as total_bids,
+ CASE WHEN a.is_active THEN a.highest_bid ELSE NULL END as highest_bid,
+ CASE WHEN a.is_active THEN a.pre_rollout_bids ELSE 0 END as pre_rollout_bids,
+ CASE WHEN a.is_active THEN a.post_rollout_bids ELSE 0 END as post_rollout_bids,
+ h.total_actions,
+ h.total_bids_all_time,
+ h.highest_bid_all_time,
+ encode(o.txid, 'hex') as outpoint_txid,
+ o.index as outpoint_index
+ FROM historical_stats h
+ LEFT JOIN auction_status a ON true
+ LEFT JOIN latest_outpoint o ON true;
+ `),
+ checkMarketplaceListing(spaceName)
+ ]);
+
+ if (queryResult.rows.length === 0) {
+ return json({
+ name: spaceName,
+ block_height: null,
+ tx_index: null,
+ claim_height: null,
+ winning_bid: null,
+ total_bids: 0,
+ pre_rollout_bids: 0,
+ post_rollout_bids: 0,
+ total_actions: 0,
+ total_bids_all_time: 0,
+ highest_bid_all_time: null,
+ outpoint_txid: null,
+ outpoint_index: null,
+ is_listed_in_marketplace: isListedInMarketplace
+ });
+ }
+
+ return json({
+ ...queryResult.rows[0],
+ is_listed_in_marketplace: isListedInMarketplace
+ });
+};
diff --git a/src/routes/api/stats/+server.ts b/src/routes/api/stats/+server.ts
new file mode 100755
index 0000000..4aae5aa
--- /dev/null
+++ b/src/routes/api/stats/+server.ts
@@ -0,0 +1,51 @@
+import db from '$lib/db';
+import { json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+
+export const GET: RequestHandler = async function ({ request, url }) {
+ const queryResult = await db.execute(sql`
+WITH latest_block AS (
+ SELECT height, time
+ FROM blocks
+ WHERE NOT orphan
+ ORDER BY height DESC
+ LIMIT 1
+),
+vmetaouts_stats AS (
+ SELECT
+ v.name,
+ v.action,
+ v.total_burned,
+ v.script_error
+ FROM vmetaouts v
+ INNER JOIN blocks b ON b.hash = v.block_hash
+ WHERE NOT b.orphan
+),
+name_burns AS (
+ SELECT
+ name,
+ MAX(total_burned) as name_total_burned
+ FROM vmetaouts_stats
+ WHERE script_error IS NULL
+ AND name IS NOT NULL
+ GROUP BY name
+)
+SELECT
+ lb.height as latest_block_height,
+ lb.time as latest_block_time,
+ COUNT(DISTINCT CASE WHEN vs.name IS NOT NULL THEN vs.name END) as unique_names_count,
+ COUNT(*) as valid_vmetaouts_count,
+ (SELECT SUM(name_total_burned) FROM name_burns) as total_burned_sum,
+ COUNT(*) FILTER (WHERE vs.name IS NOT NULL AND vs.action = 'RESERVE') as reserve_count,
+ COUNT(*) FILTER (WHERE vs.name IS NOT NULL AND vs.action = 'BID') as bid_count,
+ COUNT(*) FILTER (WHERE vs.name IS NOT NULL AND vs.action = 'TRANSFER') as transfer_count,
+ COUNT(*) FILTER (WHERE vs.name IS NOT NULL AND vs.action = 'ROLLOUT') as rollout_count,
+ COUNT(*) FILTER (WHERE vs.name IS NOT NULL AND vs.action = 'REVOKE') as revoke_count
+FROM latest_block lb
+CROSS JOIN vmetaouts_stats vs
+GROUP BY lb.height, lb.time;
+ `);
+
+ return json(queryResult.rows[0])
+}
diff --git a/src/routes/api/transactions/[txid]/+server.ts b/src/routes/api/transactions/[txid]/+server.ts
new file mode 100755
index 0000000..4153581
--- /dev/null
+++ b/src/routes/api/transactions/[txid]/+server.ts
@@ -0,0 +1,94 @@
+import db from '$lib/db';
+import { error, json } from '@sveltejs/kit';
+import { type RequestHandler } from '@sveltejs/kit';
+import { sql } from 'drizzle-orm';
+import { processTransactions } from '$lib/utils/transaction-processor';
+
+export const GET: RequestHandler = async function ({ params }) {
+ const txid = Buffer.from(params.txid, 'hex');
+
+ // Get transaction with aggregate data and Spaces protocol metadata
+ const queryResult = await db.execute(sql`
+ WITH transaction_data AS (
+ SELECT
+ transactions.txid,
+ transactions.tx_hash,
+ transactions.version AS tx_version,
+ transactions.index AS tx_index,
+ transactions.size AS tx_size,
+ transactions.vsize AS tx_vsize,
+ transactions.weight AS tx_weight,
+ transactions.locktime AS tx_locktime,
+ transactions.fee AS tx_fee,
+ transactions.input_count,
+ transactions.output_count,
+ transactions.total_output_value,
+ blocks.time AS block_time,
+ blocks.height AS block_height,
+ blocks.hash AS block_hash,
+ blocks.orphan AS block_orphan,
+ (SELECT COALESCE(MAX(height), -1) FROM blocks)::integer AS max_height
+ FROM transactions
+ JOIN blocks ON transactions.block_hash = blocks.hash
+ WHERE transactions.txid = ${txid}
+ ORDER by block_height DESC
+ LIMIT 1
+ ),
+ tx_vmetaout AS (
+ SELECT
+ txid,
+ value AS vmetaout_value,
+ name AS vmetaout_name,
+ reason AS vmetaout_reason,
+ action AS vmetaout_action,
+ burn_increment AS vmetaout_burn_increment,
+ total_burned AS vmetaout_total_burned,
+ claim_height AS vmetaout_claim_height,
+ expire_height AS vmetaout_expire_height,
+ script_error AS vmetaout_script_error,
+ signature AS vmetaout_signature,
+ scriptPubKey AS vmetaout_scriptPubKey
+ FROM vmetaouts
+ WHERE txid = ${txid} AND name is not null
+ ),
+ tx_commitments AS (
+ SELECT
+ txid,
+ name AS commitment_name,
+ state_root AS commitment_state_root,
+ history_hash AS commitment_history_hash,
+ revocation AS commitment_revocation
+ FROM commitments
+ WHERE txid = ${txid}
+ )
+
+ SELECT
+ transaction_data.*,
+ tx_vmetaout.vmetaout_value,
+ tx_vmetaout.vmetaout_name,
+ tx_vmetaout.vmetaout_action,
+ tx_vmetaout.vmetaout_burn_increment,
+ tx_vmetaout.vmetaout_total_burned,
+ tx_vmetaout.vmetaout_claim_height,
+ tx_vmetaout.vmetaout_expire_height,
+ tx_vmetaout.vmetaout_script_error,
+ tx_vmetaout.vmetaout_scriptPubKey,
+ tx_vmetaout.vmetaout_signature,
+ tx_vmetaout.vmetaout_reason,
+ tx_commitments.commitment_name,
+ encode(tx_commitments.commitment_state_root, 'hex') AS commitment_state_root,
+ encode(tx_commitments.commitment_history_hash, 'hex') AS commitment_history_hash,
+ tx_commitments.commitment_revocation
+ FROM transaction_data
+ LEFT JOIN tx_vmetaout ON transaction_data.txid = tx_vmetaout.txid
+ LEFT JOIN tx_commitments ON transaction_data.txid = tx_commitments.txid
+ `);
+
+ if (queryResult.rows.length === 0) {
+ return error(404, 'Transaction not found');
+ }
+
+ const [transaction] = processTransactions(queryResult);
+
+ return json(transaction);
+}
diff --git a/src/routes/auctions/current/+page.svelte b/src/routes/auctions/current/+page.svelte
new file mode 100755
index 0000000..40fe5bd
--- /dev/null
+++ b/src/routes/auctions/current/+page.svelte
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/routes/auctions/current/+page.ts b/src/routes/auctions/current/+page.ts
new file mode 100755
index 0000000..9506820
--- /dev/null
+++ b/src/routes/auctions/current/+page.ts
@@ -0,0 +1,12 @@
+import { error, type ServerLoad } from '@sveltejs/kit';
+export const load : ServerLoad = async ({ fetch, params }) => {
+ // const page = 1; // Initial page
+
+ // const [vmetaouts, stats] = await Promise.all([
+ const [stats] = await Promise.all([
+ // fetch(`/api/auctions/current?page=${page}`).then(x => x.body ? x.json() : null),
+ fetch('/api/stats').then(x => x.body ? x.json() : null)
+ ]);
+ return { stats };
+ throw error(404, 'Space not found');
+};
diff --git a/src/routes/auctions/past/+page.svelte b/src/routes/auctions/past/+page.svelte
new file mode 100755
index 0000000..0505f30
--- /dev/null
+++ b/src/routes/auctions/past/+page.svelte
@@ -0,0 +1,11 @@
+
+
+
+
diff --git a/src/routes/auctions/past/+page.ts b/src/routes/auctions/past/+page.ts
new file mode 100755
index 0000000..0c52639
--- /dev/null
+++ b/src/routes/auctions/past/+page.ts
@@ -0,0 +1,11 @@
+import { error, type ServerLoad } from '@sveltejs/kit';
+export const load : ServerLoad = async ({ fetch, params }) => {
+ const page = 1; // Initial page
+
+ const [vmetaouts, stats] = await Promise.all([
+ fetch(`/api/auctions/past?page=${page}`).then(x => x.body ? x.json() : null),
+ fetch('/api/stats').then(x => x.body ? x.json() : null)
+ ]);
+ return { vmetaouts, stats };
+ throw error(404, 'Space not found');
+};
diff --git a/src/routes/auctions/rollout/+page.svelte b/src/routes/auctions/rollout/+page.svelte
new file mode 100755
index 0000000..957ec36
--- /dev/null
+++ b/src/routes/auctions/rollout/+page.svelte
@@ -0,0 +1,7 @@
+
+
+
diff --git a/src/routes/auctions/rollout/+page.ts b/src/routes/auctions/rollout/+page.ts
new file mode 100755
index 0000000..b95c3e7
--- /dev/null
+++ b/src/routes/auctions/rollout/+page.ts
@@ -0,0 +1,10 @@
+import { error, type ServerLoad } from '@sveltejs/kit';
+export const load : ServerLoad = async ({ fetch, params }) => {
+ const page = 1; // Initial page
+
+ const [stats] = await Promise.all([
+ fetch('/api/stats').then(x => x.body ? x.json() : null)
+ ]);
+ return { stats };
+ throw error(404, 'Space not found');
+};
diff --git a/src/routes/block/[hash=hash]/+page.svelte b/src/routes/block/[hash=hash]/+page.svelte
new file mode 100755
index 0000000..1e9c5b1
--- /dev/null
+++ b/src/routes/block/[hash=hash]/+page.svelte
@@ -0,0 +1,46 @@
+
+
+{#if $blockStore.error}
+
+ Error loading block: {$blockStore.error}
+
+{:else if !$blockStore.header}
+ Loading block data...
+{:else}
+
+
+{/if}
diff --git a/src/routes/block/[height=height]/+page.svelte b/src/routes/block/[height=height]/+page.svelte
new file mode 100755
index 0000000..22d0fa4
--- /dev/null
+++ b/src/routes/block/[height=height]/+page.svelte
@@ -0,0 +1,46 @@
+
+
+{#if $blockStore.error}
+
+ Error loading block: {$blockStore.error}
+
+{:else if !$blockStore.header}
+ Loading block data...
+{:else}
+
+
+{/if}
diff --git a/src/routes/mempool/+page.svelte b/src/routes/mempool/+page.svelte
new file mode 100755
index 0000000..777cfdf
--- /dev/null
+++ b/src/routes/mempool/+page.svelte
@@ -0,0 +1,44 @@
+
+
+{#if $blockStore.error}
+
+ Error loading block: {$blockStore.error}
+
+{:else if !$blockStore.header}
+ Loading block data...
+{:else}
+
+{/if}
+
diff --git a/src/routes/space/[name]/+page.svelte b/src/routes/space/[name]/+page.svelte
new file mode 100755
index 0000000..bb57dac
--- /dev/null
+++ b/src/routes/space/[name]/+page.svelte
@@ -0,0 +1,804 @@
+
+
+{#if !data.stats.total_actions || data.stats.total_actions == 0}
+
+
{$page.params.name}
+
+ {#if ['example', 'test', 'local'].includes(rawSpaceName)}
+
This name is reserved and not available for purchase.
+ {:else}
+
This name is available.
+
You can open an auction for it, learn more here.
+ {/if}
+
+
+{:else}
+
+
+
+
+ {#if highestBid && highestBid !=0}
+
+ {formatBTC(highestBid)}
+ Highest bid
+
+ {/if}
+ {#if numberOfBids > 0}
+
+ {numberOfBids}
+ Number of bids
+
+ {/if}
+
+ {data.stats.total_actions}
+ Total events
+
+ {#if data.stats.claim_height}
+
+
+ {#if data.stats.claim_height <= currentBlockHeight }
+
+ {:else }
+ Block {data.stats.claim_height}
+ {/if}
+
+ Claim height
+
+ {/if}
+ {#if expiryHeight}
+
+
+ {#if expiryHeight <= currentBlockHeight}
+
+ {:else}
+
+ Block #{expiryHeight}
+ in {formatDuration((expiryHeight - currentBlockHeight) * 10 * 60)}
+
+ {/if}
+
+
Expires at
+
+ {/if}
+ {#if outpointTxid}
+
+
+
+
+ Outpoint
+
+ {/if}
+
+ {#if latestCommitment && !latestCommitment.revocation}
+
+
+
+
+
+
+ {:else}
+ Empty
+ {/if}
+
State Root
+
+
+ {#if latestCommitment }
+
+
+
+
+
+
+ {:else}
+ Empty
+ {/if}
+
History Hash
+
+
+
+
+
+
+
+
Space Timeline
+
+
+
+
+
Transaction History
+
+
+
+
+
+
+
+ {#if bidsPresent > 0}
+
+ {/if}
+
+
+
+ {#each vmetaouts as vmetaout}
+
+
+
+
+ {vmetaout.action}
+
+
+
+
+
+
+
+
+
+
+ {#if vmetaout.block_height !== -1}
+
+ {dayjs.unix(vmetaout.block_time).format('MMM DD HH:mm')}
+
+ {/if}
+
+
+
+ {#if vmetaout.script_error || vmetaout.reason || vmetaout.state_root}
+
+ {#if vmetaout.script_error}
+
+ Script Error: {vmetaout.script_error}
+
+ {/if}
+ {#if vmetaout.reason}
+
+ Reason: {vmetaout.reason}
+
+ {/if}
+ {#if vmetaout.state_root}
+
+ State Root:
+
+
+
+ {/if}
+ {#if vmetaout.history_hash}
+
+ History Hash
+
+
+
+ {/if}
+
+
+ {/if}
+
+
+ {#if bidsPresent }
+
+ {#if vmetaout.action === 'BID'}
+ {formatBTC(vmetaout.total_burned)}
+ {/if}
+
+ {/if}
+
+ {/each}
+
+
+
+
+ {#if pagination && pagination.totalPages > 1}
+
+ {/if}
+
+
+
+
+
+{/if}
+
+
diff --git a/src/routes/space/[name]/+page.ts b/src/routes/space/[name]/+page.ts
new file mode 100755
index 0000000..40bd13d
--- /dev/null
+++ b/src/routes/space/[name]/+page.ts
@@ -0,0 +1,37 @@
+import type { PageLoad } from './$types';
+import { ROUTES } from '$lib/routes';
+
+export const load: PageLoad = async ({ fetch, params }) => {
+ const name = params.name.toLowerCase();
+ const [spaceHistoryResponse, statsResponse, spaceCommitmentsResponse] = await Promise.all([
+ fetch(ROUTES.api.space.history(name)),
+ fetch(ROUTES.api.space.stats(name)),
+ fetch(ROUTES.api.space.commitments(name)),
+ ]);
+
+ if (!spaceHistoryResponse.ok) {
+ throw new Error(`Failed to fetch space history: ${spaceHistoryResponse.statusText}`);
+ }
+ if (!statsResponse.ok) {
+ throw new Error(`Failed to fetch stats: ${statsResponse.statusText}`);
+ }
+
+ if (!spaceCommitmentsResponse.ok) {
+ throw new Error(`Failed to fetch stats: ${spaceCommitmentsResponse.statusText}`);
+ }
+
+
+ const spaceHistory = await spaceHistoryResponse.json();
+ const stats = await statsResponse.json();
+ const commitments = await spaceCommitmentsResponse.json();
+
+
+ return {
+ items: spaceHistory.items,
+ latest: spaceHistory.latest,
+ pagination: spaceHistory.pagination,
+ currentHeight: spaceHistory.currentHeight,
+ stats,
+ latestCommitment: commitments.items[0] || null
+ };
+};
diff --git a/src/routes/tx/[txid]/+page.server.ts b/src/routes/tx/[txid]/+page.server.ts
new file mode 100755
index 0000000..c162329
--- /dev/null
+++ b/src/routes/tx/[txid]/+page.server.ts
@@ -0,0 +1,16 @@
+import { error } from '@sveltejs/kit';
+import { type ServerLoad } from '@sveltejs/kit';
+
+export const load: ServerLoad = async ({ fetch, params }) => {
+ const transaction = await fetch(`/api/transactions/${params.txid}`);
+ if (transaction.status != 200)
+ error(transaction.status, { message: 'Transaction not found'});
+
+ const data = await transaction.json();
+
+ // const testnet = PUBLIC_BTC_NETWORK == "testnet4" ? "testnet4/" : "";
+ // if (!data.spaceHistories.length)
+ // redirect(302, `https://mempool.space/${testnet}tx/${params.txid}`);
+
+ return data;
+};
diff --git a/src/routes/tx/[txid]/+page.svelte b/src/routes/tx/[txid]/+page.svelte
new file mode 100755
index 0000000..9428662
--- /dev/null
+++ b/src/routes/tx/[txid]/+page.svelte
@@ -0,0 +1,6 @@
+
+
+
diff --git a/explorer/static/action/bid.svg b/static/action/bid.svg
similarity index 100%
rename from explorer/static/action/bid.svg
rename to static/action/bid.svg
diff --git a/explorer/static/action/reject.svg b/static/action/reject.svg
similarity index 100%
rename from explorer/static/action/reject.svg
rename to static/action/reject.svg
diff --git a/explorer/static/action/revoke.svg b/static/action/revoke.svg
similarity index 100%
rename from explorer/static/action/revoke.svg
rename to static/action/revoke.svg
diff --git a/explorer/static/action/rollout.svg b/static/action/rollout.svg
similarity index 100%
rename from explorer/static/action/rollout.svg
rename to static/action/rollout.svg
diff --git a/explorer/static/action/transfer.svg b/static/action/transfer.svg
similarity index 100%
rename from explorer/static/action/transfer.svg
rename to static/action/transfer.svg
diff --git a/explorer/static/arrow-right.svg b/static/arrow-right.svg
similarity index 100%
rename from explorer/static/arrow-right.svg
rename to static/arrow-right.svg
diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png
new file mode 100644
index 0000000..11c2a44
Binary files /dev/null and b/static/favicon-16x16.png differ
diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png
new file mode 100644
index 0000000..691c34d
Binary files /dev/null and b/static/favicon-32x32.png differ
diff --git a/static/favicon-48x48.png b/static/favicon-48x48.png
new file mode 100644
index 0000000..2807698
Binary files /dev/null and b/static/favicon-48x48.png differ
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..2e95c96
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/footer/github.svg b/static/footer/github.svg
new file mode 100644
index 0000000..37fa923
--- /dev/null
+++ b/static/footer/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/explorer/static/favicon.png b/static/footer/spacesprotocol.png
similarity index 100%
rename from explorer/static/favicon.png
rename to static/footer/spacesprotocol.png
diff --git a/static/footer/telegram.svg b/static/footer/telegram.svg
new file mode 100644
index 0000000..25f1f87
--- /dev/null
+++ b/static/footer/telegram.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/static/logo.png b/static/logo.png
new file mode 100644
index 0000000..cbcc76f
Binary files /dev/null and b/static/logo.png differ
diff --git a/static/robots.txt b/static/robots.txt
new file mode 100644
index 0000000..a9476aa
--- /dev/null
+++ b/static/robots.txt
@@ -0,0 +1,15 @@
+User-agent: *
+# Set strict crawl delay
+Crawl-delay: 10
+
+# Allow only main page
+Allow: /$
+
+# Disallow everything else
+Disallow: /api/
+Disallow: /tx/
+Disallow: /space/
+Disallow: /address/
+Disallow: /actions/
+Disallow: /auctions/
+Disallow: /*
diff --git a/svelte.config.js b/svelte.config.js
new file mode 100644
index 0000000..0cc6d12
--- /dev/null
+++ b/svelte.config.js
@@ -0,0 +1,41 @@
+// svelte.config.js
+import adapter from '@sveltejs/adapter-node'; // or your preferred adapter
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ kit: {
+ adapter: adapter({
+ // Production optimization settings
+ precompress: true, // Enable Brotli & Gzip precompression
+ polyfill: false, // Disable Node polyfills if not needed
+ out: 'build' // Output directory
+ }),
+
+ // Asset optimization
+ inlineStyleThreshold: 8192, // Inline small styles
+
+ // CSP settings if needed
+ csp: {
+ mode: 'hash',
+ directives: {
+ 'script-src': ['self']
+ }
+ },
+
+ // Additional optimizations
+ prerender: {
+ handleMissingId: 'ignore' // More aggressive prerendering
+ },
+
+ // Environment configuration
+ env: {
+ dir: '.'
+ }
+ },
+
+ // Enable preprocessing
+ preprocess: [vitePreprocess()]
+};
+
+export default config;
diff --git a/explorer/tailwind.config.js b/tailwind.config.js
similarity index 100%
rename from explorer/tailwind.config.js
rename to tailwind.config.js
diff --git a/explorer/tsconfig.json b/tsconfig.json
similarity index 100%
rename from explorer/tsconfig.json
rename to tsconfig.json
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..dcacdf5
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,102 @@
+import { sveltekit } from '@sveltejs/kit/vite';
+import { defineConfig, type ProxyOptions } from 'vite';
+import fs from 'fs';
+import dotenv from 'dotenv';
+
+dotenv.config();
+const { SSL_CERT_PATH, SSL_KEY_PATH } = process.env;
+
+const server: { proxy: Record, https?: object} = {
+ proxy: {},
+}
+
+if (SSL_CERT_PATH && SSL_KEY_PATH) {
+ server.https = {
+ key: fs.readFileSync(SSL_KEY_PATH),
+ cert: fs.readFileSync(SSL_CERT_PATH)
+ }
+}
+
+export default defineConfig({
+ plugins: [sveltekit()],
+
+ build: {
+ // Disable source maps in production
+ sourcemap: false,
+
+ // Production minification
+ minify: 'esbuild',
+ target: 'esnext',
+
+ // Optimize output chunks
+ rollupOptions: {
+ output: {
+ // Optimize chunk size
+ chunkFileNames: 'chunks/[name].[hash].js',
+ entryFileNames: 'entries/[name].[hash].js',
+ assetFileNames: 'assets/[name].[hash][extname]',
+
+ // Manual chunk splitting
+ manualChunks: (id) => {
+ // Group dayjs and its plugins
+ if (id.includes('dayjs')) {
+ return 'vendor-dayjs';
+ }
+
+ // Group common components
+ if (id.includes('/lib/components/layout/')) {
+ return 'common-layout';
+ }
+
+ // Group feature components
+ if (id.includes('/lib/components/')) {
+ if (id.includes('RecentActions') || id.includes('Rollout') || id.includes('Stats')) {
+ return 'features';
+ }
+ }
+
+ // Group other node_modules
+ if (id.includes('node_modules')) {
+ const module = id.split('node_modules/').pop()?.split('/')[0];
+ if (module) {
+ return `vendor-${module}`;
+ }
+ }
+ }
+ }
+ },
+
+ // Reduce chunk sizes
+ chunkSizeWarningLimit: 1000,
+ },
+
+ // Optimize CSS
+ css: {
+ preprocessorOptions: {
+ css: {
+ imports: true
+ }
+ },
+ devSourcemap: false
+ },
+
+ // Your server config
+ server,
+
+ optimizeDeps: {
+ // Include frequently used dependencies
+ include: [
+ 'dayjs',
+ 'dayjs/plugin/utc',
+ 'dayjs/plugin/relativeTime',
+ 'dayjs/plugin/localizedFormat'
+ ]
+ },
+
+ // Enable modern browser optimizations
+ esbuild: {
+ target: 'esnext',
+ platform: 'browser',
+ treeShaking: true
+ }
+});