+ 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 100644
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 100644
index 0000000..660328f
--- /dev/null
+++ b/src/lib/components/layout/SearchBar.svelte
@@ -0,0 +1,202 @@
+
+
+
diff --git a/src/lib/db.ts b/src/lib/db.ts
new file mode 100644
index 0000000..61501ec
--- /dev/null
+++ b/src/lib/db.ts
@@ -0,0 +1,48 @@
+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.dbInstanceIdentifier}?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,
+// });
+//
+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
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 100644
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
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 100644
index 0000000..4cd772c
--- /dev/null
+++ b/src/lib/routes.ts
@@ -0,0 +1,62 @@
+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`
+ },
+
+ 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 100644
index 0000000..e273a85
--- /dev/null
+++ b/src/lib/schema.ts
@@ -0,0 +1,197 @@
+import { pgTable, pgEnum, serial, bigint, boolean, timestamp, unique, integer, doublePrecision, uniqueIndex, primaryKey, index, text, customType} from "drizzle-orm/pg-core"
+
+import db from "$lib/db";
+import { sql } from "drizzle-orm";
+import { relations } from "drizzle-orm/relations";
+
+
+export const covenant_action = pgEnum("covenant_action", ['RESERVE', 'BID', 'TRANSFER'])
+
+const bytea = customType({
+ dataType() { return "bytea" },
+ fromDriver(value: unknown): string {
+ //Why it doesn't work without this? sometimes hexstring, sometimes buffer
+ if (typeof value === 'string' && value.startsWith('\\x')) {
+ return value.slice(2)
+ }
+ return value.toString('hex')
+ // return value as Buffer;
+ },
+ toDriver(value: Buffer): Buffer {
+ return Buffer.from(value, 'hex')
+ },
+});
+
+export const goose_db_version = pgTable("goose_db_version", {
+ id: serial("id").primaryKey().notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ version_id: bigint("version_id", { mode: "number" }).notNull(),
+ is_applied: boolean("is_applied").notNull(),
+ tstamp: timestamp("tstamp", { mode: 'string' }).defaultNow(),
+});
+
+export const blocks = pgTable("blocks", {
+ hash: bytea("hash").primaryKey().notNull(),
+ size: bigint("size", { mode: "number" }).notNull(),
+ stripped_size: bigint("stripped_size", { mode: "number" }).notNull(),
+ weight: integer("weight").notNull(),
+ height: integer("height").notNull(),
+ version: integer("version").notNull(),
+ hash_merkle_root: bytea("hash_merkle_root").notNull(),
+ time: integer("time").notNull(),
+ median_time: integer("median_time").notNull(),
+ nonce: bigint("nonce", { mode: "number" }).notNull(),
+ bits: bytea("bits").notNull(),
+ difficulty: doublePrecision("difficulty").notNull(),
+ chainwork: bytea("chainwork").notNull(),
+ orphan: boolean("orphan").default(false).notNull(),
+},
+(table) => {
+ return {
+ blocks_height_key: unique("blocks_height_key").on(table.height),
+ }
+});
+
+export const transactions = pgTable("transactions", {
+ txid: bytea("txid").primaryKey().notNull(),
+ tx_hash: bytea("tx_hash"),
+ version: integer("version").notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ size: bigint("size", { mode: "number" }).notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ vsize: bigint("vsize", { mode: "number" }).notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ weight: bigint("weight", { mode: "number" }).notNull(),
+ locktime: integer("locktime").notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ fee: bigint("fee", { mode: "number" }).notNull(),
+ block_hash: bytea("block_hash").references(() => blocks.hash, { onDelete: "cascade" } ),
+ index: integer("index"),
+},
+(table) => {
+ return {
+ block_hash_idx: uniqueIndex("transactions_block_hash_index").using("btree", table.block_hash, table.index).where(sql`(block_hash IS NOT NULL)`),
+ }
+});
+
+export const tx_outputs = pgTable("tx_outputs", {
+ block_hash: bytea("block_hash").notNull().references(() => blocks.hash, { onDelete: "cascade" } ),
+ txid: bytea("txid").notNull().references(() => transactions.txid, { onDelete: "cascade" } ),
+ index: integer("index").notNull(),
+ // You can use { mode: "bigint" } if numbers are exceeding js number limitations
+ value: bigint("value", { mode: "number" }).notNull(),
+ scriptpubkey: bytea("scriptpubkey"),
+},
+(table) => {
+ return {
+ tx_outputs_pkey: primaryKey({ columns: [table.block_hash, table.txid, table.index], name: "tx_outputs_pkey"}),
+ }
+});
+
+export const tx_inputs = pgTable("tx_inputs", {
+ block_hash: bytea("block_hash").notNull().references(() => blocks.hash, { onDelete: "cascade" } ),
+ txid: bytea("txid").notNull().references(() => transactions.txid, { onDelete: "cascade" } ),
+ index: bigint("index", { mode: "number" }).notNull(),
+ hash_prevout: bytea("hash_prevout"),
+ index_prevout: bigint("index_prevout", { mode: "number" }).notNull(),
+ sequence: bigint("sequence", { mode: "number" }).notNull(),
+ coinbase: bytea("coinbase"),
+ txinwitness: bytea("txinwitness").array(),
+},
+(table) => {
+ return {
+ hash_prevout_idx: index("tx_inputs_hash_prevout_index").using("btree", table.hash_prevout, table.index_prevout).where(sql`(hash_prevout IS NOT NULL)`),
+ txid_idx: index().using("btree", table.txid),
+ tx_inputs_pkey: primaryKey({ columns: [table.block_hash, table.txid, table.index], name: "tx_inputs_pkey"}),
+ }
+});
+
+export const vmetaouts = pgTable("vmetaouts", {
+ block_hash: bytea("block_hash").notNull().references(() => blocks.hash, { onDelete: "cascade" } ),
+ txid: bytea("txid").notNull().references(() => transactions.txid, { onDelete: "cascade" } ),
+ tx_index: bigint("tx_index", { mode: "number" }).notNull(),
+ outpoint_txid: bytea("outpoint_txid").notNull().references(() => transactions.txid),
+ outpoint_index: bigint("outpoint_index", { mode: "number" }).notNull(),
+ name: text("name").notNull(),
+ burn_increment: bigint("burn_increment", { mode: "number" }),
+ covenant_action: covenant_action("covenant_action").notNull(),
+ claim_height: bigint("claim_height", { mode: "number" }),
+ expire_height: bigint("expire_height", { mode: "number" }),
+},
+(table) => {
+ return {
+ vmetaouts_pkey: primaryKey({ columns: [table.block_hash, table.txid, table.tx_index], name: "vmetaouts_pkey"}),
+ }
+});
+
+export const transactionsRelations = relations(transactions, ({one, many}) => ({
+ block: one(blocks, {
+ fields: [transactions.block_hash],
+ references: [blocks.hash]
+ }),
+ tx_outputs: many(tx_outputs),
+ tx_inputs: many(tx_inputs),
+ vmetaouts_txid: many(vmetaouts, {
+ relationName: "vmetaouts_txid_transactions_txid"
+ }),
+ vmetaouts_outpoint_txid: many(vmetaouts, {
+ relationName: "vmetaouts_outpoint_txid_transactions_txid"
+ }),
+}));
+
+export const blocksRelations = relations(blocks, ({many}) => ({
+ transactions: many(transactions),
+ tx_outputs: many(tx_outputs),
+ tx_inputs: many(tx_inputs),
+ vmetaouts: many(vmetaouts),
+}));
+
+export const tx_outputsRelations = relations(tx_outputs, ({one}) => ({
+ block: one(blocks, {
+ fields: [tx_outputs.block_hash],
+ references: [blocks.hash]
+ }),
+ transaction: one(transactions, {
+ fields: [tx_outputs.txid],
+ references: [transactions.txid]
+ }),
+}));
+
+export const tx_inputsRelations = relations(tx_inputs, ({one}) => ({
+ block: one(blocks, {
+ fields: [tx_inputs.block_hash],
+ references: [blocks.hash]
+ }),
+ transaction: one(transactions, {
+ fields: [tx_inputs.txid],
+ references: [transactions.txid]
+ }),
+}));
+
+export const vmetaoutsRelations = relations(vmetaouts, ({one}) => ({
+ block: one(blocks, {
+ fields: [vmetaouts.block_hash],
+ references: [blocks.hash]
+ }),
+ transaction_txid: one(transactions, {
+ fields: [vmetaouts.txid],
+ references: [transactions.txid],
+ relationName: "vmetaouts_txid_transactions_txid"
+ }),
+ transaction_outpoint_txid: one(transactions, {
+ fields: [vmetaouts.outpoint_txid],
+ references: [transactions.txid],
+ relationName: "vmetaouts_outpoint_txid_transactions_txid"
+ }),
+}));
+
+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 100644
index 0000000..4285ad1
--- /dev/null
+++ b/src/lib/stores/blockStore.ts
@@ -0,0 +1,108 @@
+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;
+ error: string | null;
+ pagination: {
+ currentPage: number;
+ limit: number;
+ offset: number;
+ };
+ cache: Map
;
+ }>;
+};
+
+function createBlockStore() {
+ const initialState: BlockState = {
+ currentHeight: null,
+ header: null,
+ transactions: [],
+ txCount: 0,
+ error: null,
+ 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, 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 = 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
+ if (!transactions) {
+ const txsResponse = await customFetch(`/api/block/${height}/txs?offset=${offset}&limit=${initialState.pagination.limit}`);
+ if (!txsResponse.ok) throw new Error(`Error fetching block transactions: ${txsResponse.statusText}`);
+ transactions = await txsResponse.json();
+ }
+
+ // Update cache and state
+ update(state => {
+ const updatedCache = new Map(state.cache);
+ const blockCache = updatedCache.get(cacheKey) || { header: blockHeader, pages: new Map() };
+ blockCache.pages.set(pageKey, transactions);
+ updatedCache.set(cacheKey, blockCache);
+
+ return {
+ ...state,
+ currentHeight: height,
+ header: blockHeader,
+ transactions,
+ txCount: blockHeader.tx_count,
+ 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 => Math.ceil($blockStore.txCount / $blockStore.pagination.limit)
+);
diff --git a/src/lib/styles/headers.css b/src/lib/styles/headers.css
new file mode 100644
index 0000000..d900f48
--- /dev/null
+++ b/src/lib/styles/headers.css
@@ -0,0 +1,84 @@
+@import 'variables.css';
+
+.container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-4);
+ padding: var(--space-4);
+ color: var(--text-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(--text-3xl);
+ color: var(--text-primary);
+}
+
+.hash {
+ position: relative;
+ top: var(--space-2);
+ word-break: break-all;
+ color: var(--text-muted);
+ transition: var(--transition-colors);
+ font-family: monospace;
+}
+
+.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);
+}
+
+.detail-value {
+ font-size: var(--text-xl);
+ color: var(--color-primary);
+ font-weight: 600;
+ transition: var(--transition-colors);
+ word-break: break-all;
+ font-family: monospace;
+}
+
+@media (max-width: 640px) {
+ .detail-value {
+ font-size: var(--text-lg);
+ }
+
+ .details {
+ gap: var(--space-6) var(--space-6);
+ }
+}
+
+.detail-label {
+ color: var(--text-muted);
+ transition: var(--transition-colors);
+ font-size: var(--text-lg);
+ font-weight: 500;
+}
diff --git a/src/lib/styles/link.css b/src/lib/styles/link.css
new file mode 100644
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 100644
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 100644
index 0000000..a9d440d
--- /dev/null
+++ b/src/lib/styles/variables.css
@@ -0,0 +1,108 @@
+:root {
+ /* Colors */
+ --color-primary: #ec8e32;
+ --color-gray-300: #d1d5db;
+ --color-gray-400: #9ca3af;
+ --color-gray-500: #6b7280;
+ --color-gray-600: #4b5563;
+
+ /* Light theme defaults */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8fafc;
+ --text-primary: #0f172a;
+ --text-muted: #64748b;
+ --border-color: #e2e8f0;
+ --border-hover: #cbd5e1;
+
+ /* Spacing */
+ --space-1: 0.25rem;
+ --space-2: 0.5rem;
+ --space-4: 1rem;
+ --space-6: 1.5rem;
+ --space-8: 2rem;
+ --space-10: 2.5rem;
+
+ /* Typography */
+ --text-xs: 0.75rem;
+ --text-sm: 0.875rem;
+ --text-base: 1rem;
+ --text-lg: 1.125rem;
+ --text-xl: 1.25rem;
+ --text-2xl: 1.5rem;
+ --text-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;
+}
+
+/* Dark theme */
+[data-theme="dark"] {
+ --bg-primary: #161616;
+ --bg-secondary: #1e1e1e;
+ --text-primary: #ffffff;
+ --text-muted: #9ca3af;
+ --border-color: #2d2d2d;
+ --border-hover: #404040;
+ --space-action-bg: rgb(49, 46, 43);
+ --space-action-text: rgb(255, 169, 122);
+}
+
+/* Apply theme colors to body */
+body {
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ transition: var(--transition-colors);
+ overflow-x: hidden;
+}
diff --git a/src/lib/types/address.ts b/src/lib/types/address.ts
new file mode 100644
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 100644
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 100644
index 0000000..9bea6ac
--- /dev/null
+++ b/src/lib/types/transaction.ts
@@ -0,0 +1,77 @@
+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;
+ inputs: TransactionInput[];
+ outputs: TransactionOutput[];
+ vmetaouts: TransactionVmetaout[];
+}
+
+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 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
+};
+
+// Update TransactionOutput type to include optional space_action
+export interface TransactionOutput {
+ index: number;
+ value: number;
+ scriptpubkey: string | null;
+ address: string | null;
+ spender: {
+ txid: string;
+ index: number;
+ } | null;
+ space_action?: SpaceAction;
+}
diff --git a/src/lib/utils/address-parsers.ts b/src/lib/utils/address-parsers.ts
new file mode 100644
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 100644
index 0000000..7aa49a7
--- /dev/null
+++ b/src/lib/utils/formatters.ts
@@ -0,0 +1,229 @@
+// declare module 'punycode';
+
+import * as punycode from 'punycode';
+
+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';
+ 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}`
+}
+
+
diff --git a/src/lib/utils/query.ts b/src/lib/utils/query.ts
new file mode 100644
index 0000000..082ff9b
--- /dev/null
+++ b/src/lib/utils/query.ts
@@ -0,0 +1,383 @@
+import { sql } from 'drizzle-orm';
+
+interface BlockTxsQueryParams {
+ db: DB;
+ blockIdentifier: {
+ type: 'hash' | 'height';
+ value: string | number | Buffer;
+ };
+ pagination: {
+ limit: number;
+ offset: number;
+ input_limit: number;
+ input_offset: number;
+ output_limit: number;
+ output_offset: number;
+ spaces_limit: number;
+ spaces_offset: number;
+ };
+}
+
+
+export async function getBlockTransactions({ db, blockIdentifier, pagination }: BlockTxsQueryParams) {
+ const blockCondition = blockIdentifier.type === 'hash' ? sql`blocks.hash = ${blockIdentifier.value}` : sql`blocks.height = ${blockIdentifier.value}`;
+ console.log(pagination)
+
+ 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
+ FROM transactions
+ WHERE transactions.block_hash = (
+ SELECT hash FROM blocks WHERE ${blockCondition}
+ )
+ ORDER BY transactions.index
+ LIMIT ${pagination.limit} OFFSET ${pagination.offset}),
+ limited_tx_inputs AS (
+ SELECT
+ tx_inputs.txid,
+ tx_inputs.index AS input_index,
+ tx_inputs.hash_prevout AS input_hash_prevout,
+ tx_inputs.index_prevout AS input_index_prevout,
+ tx_inputs.sequence AS input_sequence,
+ tx_inputs.coinbase AS input_coinbase,
+ tx_inputs.txinwitness AS input_txinwitness,
+ tx_inputs.scriptsig AS input_scriptsig,
+ prev_out.scriptpubkey AS input_prev_scriptpubkey,
+ prev_out.value AS input_prev_value,
+ ROW_NUMBER() OVER (PARTITION BY tx_inputs.txid ORDER BY tx_inputs.index ASC) AS rn
+ FROM tx_inputs
+ LEFT JOIN tx_outputs prev_out
+ ON tx_inputs.hash_prevout = prev_out.txid
+ AND tx_inputs.index_prevout = prev_out.index
+ WHERE tx_inputs.txid IN (SELECT txid FROM limited_transactions)
+ ORDER BY tx_inputs.index ASC
+ ),
+ limited_tx_outputs as (
+ select
+ tx_outputs.txid,
+ tx_outputs.index as output_index,
+ tx_outputs.value as output_value,
+ tx_outputs.scriptpubkey as output_scriptpubkey,
+ tx_outputs.spender_txid AS output_spender_txid,
+ tx_outputs.spender_index AS output_spender_index,
+ row_number() over (partition by tx_outputs.txid order by tx_outputs.index asc) as rn
+ from tx_outputs
+ where tx_outputs.txid in (select txid from limited_transactions)
+ order by tx_outputs.index ASC
+ ),
+ 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)
+ )
+ 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_tx_inputs.input_index AS input_index,
+ limited_tx_inputs.input_hash_prevout AS input_hash_prevout,
+ limited_tx_inputs.input_index_prevout AS input_index_prevout,
+ limited_tx_inputs.input_sequence AS input_sequence,
+ limited_tx_inputs.input_coinbase AS input_coinbase,
+ limited_tx_inputs.input_txinwitness AS input_txinwitness,
+ limited_tx_inputs.input_prev_scriptpubkey,
+ limited_tx_inputs.input_prev_value,
+ limited_tx_inputs.input_scriptsig,
+
+ limited_tx_outputs.output_index AS output_index,
+ limited_tx_outputs.output_value AS output_value,
+ limited_tx_outputs.output_scriptpubkey AS output_scriptpubkey,
+ limited_tx_outputs.output_spender_txid,
+ limited_tx_outputs.output_spender_index,
+
+ 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
+
+ FROM limited_transactions
+ LEFT JOIN limited_tx_inputs ON limited_tx_inputs.txid = limited_transactions.txid AND limited_tx_inputs.rn BETWEEN ${pagination.input_offset + 1} AND ${pagination.input_offset + pagination.input_limit}
+ LEFT JOIN limited_tx_outputs ON limited_tx_outputs.txid = limited_transactions.txid AND limited_tx_outputs.rn BETWEEN ${pagination.output_offset + 1} AND ${pagination.output_offset + pagination.output_limit}
+ 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}
+ ORDER BY limited_transactions.index;
+ `);
+ return queryResult
+}
+
+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 100644
index 0000000..ca79fab
--- /dev/null
+++ b/src/lib/utils/transaction-processor.ts
@@ -0,0 +1,128 @@
+import type { Transaction, TransactionInput, TransactionOutput, TransactionVmetaout } from '$lib/types/transaction';
+import { parseAddress } from '$lib/utils/address-parsers';
+
+
+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,
+ inputs: [],
+ outputs: [],
+ vmetaouts: []
+ };
+
+ // 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') {
+ transaction.confirmations = row.max_height - row.block_height + 1;
+ }
+ }
+
+ 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
+ };
+}
+
+export function createTransactionInput(row: any): TransactionInput {
+ return {
+ index: row.input_index,
+ hash_prevout: row.input_hash_prevout ? row.input_hash_prevout.toString('hex') : null,
+ index_prevout: row.input_index_prevout,
+ sequence: row.input_sequence,
+ coinbase: row.input_coinbase ? row.input_coinbase.toString('hex') : null,
+ txinwitness: row.input_txinwitness ? row.input_txinwitness.map(buf => buf.toString('hex')) : null,
+ scriptsig: row.input_scriptsig ? row.input_scriptsig.toString('hex') : null,
+ prev_value: row.input_prev_value,
+ prev_scriptpubkey: row.input_prev_scriptpubkey ? row.input_prev_scriptpubkey.toString('hex') : null,
+ sender_address: row.input_prev_scriptpubkey ? parseAddress(row.input_prev_scriptpubkey) : null
+ };
+}
+
+export function createTransactionOutput(row: any, parseAddresses: boolean): TransactionOutput {
+ const scriptPubKey: Buffer = row.output_scriptpubkey;
+ return {
+ index: row.output_index,
+ value: row.output_value,
+ scriptpubkey: scriptPubKey ? scriptPubKey.toString('hex') : null,
+ address: parseAddresses ? parseAddress(scriptPubKey) : null,
+ spender: row.output_spender_txid ? {
+ txid: row.output_spender_txid.toString('hex'),
+ index: row.output_spender_index
+ } : null
+ };
+}
+
+
+export function processTransactions(queryResult: any, parseAddresses = true): Transaction[] {
+ const txs: Transaction[] = [];
+ const transactionMap = new Map();
+ const inputMap = new Map();
+ const outputMap = new Map();
+ const vmetaoutMap = 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 inputKey = `${txid}_${row.input_index}`;
+ const outputKey = `${txid}_${row.output_index}`;
+ const vmetaoutKey = `${txid}_${row.vmetaout_name}`; // Using name as unique identifier
+
+ if (row.input_index != null && !inputMap.has(inputKey)) {
+ const input = createTransactionInput(row);
+ transaction.inputs.push(input);
+ inputMap.set(inputKey, true);
+ }
+
+ if (row.output_index != null && !outputMap.has(outputKey)) {
+ const output = createTransactionOutput(row, parseAddresses);
+ transaction.outputs.push(output);
+ outputMap.set(outputKey, true);
+ }
+
+ if (row.vmetaout_name && !vmetaoutMap.has(vmetaoutKey)) {
+ const vmetaout = createVMetaOutput(row);
+ if (vmetaout) {
+ transaction.vmetaouts.push(vmetaout);
+ vmetaoutMap.set(vmetaoutKey, true);
+ }
+ }
+ }
+
+ return txs;
+}
diff --git a/src/lib/utils/vmetaout.ts b/src/lib/utils/vmetaout.ts
new file mode 100644
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 100644
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 100644
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
similarity index 100%
rename from explorer/src/routes/+error.svelte
rename to src/routes/+error.svelte
diff --git a/explorer/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
similarity index 100%
rename from explorer/src/routes/+layout.server.ts
rename to src/routes/+layout.server.ts
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100644
index 0000000..463341c
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts
new file mode 100644
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 100644
index 0000000..a4970c2
--- /dev/null
+++ b/src/routes/+page.svelte
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+
+
+
+ {#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 100644
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 100644
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 100644
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 100644
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 100644
index 0000000..79adae3
--- /dev/null
+++ b/src/routes/api/auctions/mempool/+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 ({ 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
+ const countResult = await db.execute(sql`
+ SELECT COUNT(*) as total
+ FROM vmetaouts
+ WHERE block_hash = ${mempoolBlockHash}
+ AND name IS NOT NULL;
+ `);
+
+ const total = Number(countResult.rows[0].total);
+
+ // Get paginated results
+ const queryResult = await db.execute(sql`
+ SELECT
+ *,
+ -1 as height
+ FROM vmetaouts
+ WHERE block_hash = ${mempoolBlockHash}
+ AND name IS NOT NULL
+ ORDER BY name DESC
+ LIMIT ${limit}
+ OFFSET ${offset};
+ `);
+
+ const processedResult = queryResult.rows.map(row => ({
+ ...row,
+ block_hash: row.block_hash.toString('hex'),
+ txid: row.txid.toString('hex'),
+ }));
+
+ return json({
+ items: processedResult,
+ 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 100644
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 100644
index 0000000..d310fae
--- /dev/null
+++ b/src/routes/api/auctions/recent/+server.ts
@@ -0,0 +1,50 @@
+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
+ const countResult = await db.execute(sql`
+ SELECT COUNT(*) as total
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE b.height >= 0 AND v.name IS NOT NULL;
+ `);
+
+ const total = Number(countResult.rows[0].total);
+
+ // Get paginated results
+ const queryResult = await db.execute(sql`
+ SELECT
+ v.*,
+ b.height,
+ b.time
+ FROM vmetaouts v
+ JOIN blocks b ON v.block_hash = b.hash
+ WHERE b.height >= 0 AND v.name IS NOT NULL
+ ORDER BY b.height DESC
+ LIMIT ${limit}
+ OFFSET ${offset};
+ `);
+
+ const processedResult = queryResult.rows.map(row => ({
+ ...row,
+ block_hash: row.block_hash.toString('hex'),
+ txid: row.txid.toString('hex'),
+ }));
+
+ return json({
+ items: processedResult,
+ 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 100644
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 100644
index 0000000..107d54a
--- /dev/null
+++ b/src/routes/api/block/[hash=hash]/txs/+server.ts
@@ -0,0 +1,41 @@
+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 block_hash = Buffer.from(params.hash, 'hex');
+
+ if (!block_hash) {
+ error(404, "No hash provided");
+ }
+ const queryResult = await getBlockTransactions({
+ db,
+ blockIdentifier: { type: 'hash', value: block_hash },
+ pagination: {
+ limit,
+ offset,
+ input_limit: 10,
+ input_offset: 0,
+ output_limit: 10,
+ output_offset: 0,
+ spaces_limit: 10,
+ spaces_offset: 0
+ }
+ });
+
+ if (!queryResult.rows) {
+ return error(404, 'Block not found');
+ }
+
+
+ const txs = processTransactions(queryResult, true);
+
+ return json(txs);
+}
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 100644
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 100644
index 0000000..fbe9d46
--- /dev/null
+++ b/src/routes/api/block/[height=height]/txs/+server.ts
@@ -0,0 +1,35 @@
+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 queryResult = await getBlockTransactions({
+ db,
+ blockIdentifier: { type: 'height', value: params.height },
+ pagination: {
+ limit,
+ offset,
+ input_limit: 10,
+ input_offset: 0,
+ output_limit: 10,
+ output_offset: 0,
+ spaces_limit: 10,
+ spaces_offset: 0
+ }
+ });
+
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return error(404, 'Block not found');
+ }
+
+ const txs = processTransactions(queryResult);
+
+ return json(txs)
+}
diff --git a/src/routes/api/healthcheck/+server.ts b/src/routes/api/healthcheck/+server.ts
new file mode 100644
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 100644
index 0000000..893fd60
--- /dev/null
+++ b/src/routes/api/root-anchors.json/+server.ts
@@ -0,0 +1,36 @@
+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
+ root_anchor,
+ hash,
+ height
+ FROM blocks
+ WHERE root_anchor IS NOT NULL
+ ORDER BY height DESC LIMIT 120;
+ `);
+
+ if (!queryResult.rows || queryResult.rows.length === 0) {
+ return json([]);
+ }
+
+ // Transform the data to the required format
+ const formattedAnchors = queryResult.rows.map(row => ({
+ root: row.root_anchor.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 100644
index 0000000..6b2d784
--- /dev/null
+++ b/src/routes/api/search/+server.ts
@@ -0,0 +1,113 @@
+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';
+
+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+$/;
+
+ // 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]) {
+ 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]) {
+ result.push({
+ type: "block",
+ value: {
+ ...block.rows[0],
+ hash: block.rows[0].hash.toString('hex'),
+ height: block.rows[0].height
+ }
+ });
+ }
+ }
+ //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]) {
+ result.push({
+ type: "block",
+ value: {
+ ...block.rows[0],
+ hash: block.rows[0].hash.toString('hex'),
+ height: block.rows[0].height
+ }
+ });
+ }
+ }
+ }
+
+ // the rest should be a space
+ const strippedSpace = search.startsWith('@') ? search.substring(1) : search;
+ const names = await db.execute(sql`
+ 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
+ `);
+ for (const space of names.rows) {
+ result.push({ type: "space", value: space });
+ }
+
+ return json(result);
+}
diff --git a/src/routes/api/space/[name]/history/+server.ts b/src/routes/api/space/[name]/history/+server.ts
new file mode 100644
index 0000000..570ec73
--- /dev/null
+++ b/src/routes/api/space/[name]/history/+server.ts
@@ -0,0 +1,142 @@
+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 and counts
+ const historyResult = await db.execute(sql`
+ WITH counts AS (
+ SELECT
+ COUNT(*) 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
+ )
+ 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,
+ c.total_actions,
+ c.bid_count
+ 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
+ CROSS JOIN counts c
+ WHERE v.name = ${spaceName} AND b.orphan is false
+ ORDER BY
+ CASE
+ WHEN b.height = -1 THEN 1
+ ELSE 0
+ END DESC,
+ b.height DESC,
+ t.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'),
+ });
+
+ 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 100644
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 100644
index 0000000..e464512
--- /dev/null
+++ b/src/routes/api/stats/+server.ts
@@ -0,0 +1,39 @@
+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
+),
+name_burns AS (
+ SELECT
+ name,
+ MAX(total_burned) as name_total_burned
+ FROM vmetaouts join blocks on blocks.hash = vmetaouts.block_hash
+ WHERE script_error IS NULL
+ AND name IS NOT NULL and not orphan
+ GROUP BY name
+)
+SELECT
+ lb.height as latest_block_height,
+ lb.time as latest_block_time,
+ (SELECT COUNT(DISTINCT name) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan) as unique_names_count,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE not blocks.orphan ) as valid_vmetaouts_count,
+ (SELECT SUM(name_total_burned) FROM name_burns) as total_burned_sum,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan and action = 'RESERVE') as reserve_count,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan and action = 'BID') as bid_count,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan and action = 'TRANSFER') as transfer_count,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan and action = 'ROLLOUT') as rollout_count,
+ (SELECT COUNT(*) FROM vmetaouts join blocks on vmetaouts.block_hash = blocks.hash WHERE name IS NOT NULL and not orphan and action = 'REVOKE') as revoke_count
+FROM latest_block lb;
+ `);
+
+ 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 100644
index 0000000..e20d57e
--- /dev/null
+++ b/src/routes/api/transactions/[txid]/+server.ts
@@ -0,0 +1,122 @@
+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');
+
+ // First try to get non-orphan transaction
+ 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,
+ 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_inputs_data AS (
+ SELECT
+ ti.txid,
+ ti.index AS input_index,
+ ti.hash_prevout AS input_hash_prevout,
+ ti.index_prevout AS input_index_prevout,
+ ti.sequence AS input_sequence,
+ ti.coinbase AS input_coinbase,
+ ti.scriptsig AS input_scriptsig,
+ ti.txinwitness AS input_txinwitness,
+ prev_out.scriptpubkey AS input_prev_scriptpubkey,
+ prev_out.value AS input_prev_value
+ FROM tx_inputs ti
+ LEFT JOIN tx_outputs prev_out
+ ON ti.hash_prevout = prev_out.txid
+ AND ti.index_prevout = prev_out.index
+ WHERE ti.txid = ${txid}
+ ),
+ tx_outputs_data AS (
+ SELECT
+ txid,
+ index AS output_index,
+ value AS output_value,
+ scriptpubkey AS output_scriptpubkey,
+ spender_txid AS output_spender_txid,
+ spender_index AS output_spender_index
+ FROM tx_outputs
+ WHERE txid = ${txid}
+ ),
+ 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
+ )
+
+ SELECT
+ transaction_data.*,
+ tx_inputs_data.input_index,
+ tx_inputs_data.input_hash_prevout,
+ tx_inputs_data.input_index_prevout,
+ tx_inputs_data.input_sequence,
+ tx_inputs_data.input_coinbase,
+ tx_inputs_data.input_txinwitness,
+ tx_inputs_data.input_scriptsig,
+ tx_inputs_data.input_prev_scriptpubkey,
+ tx_inputs_data.input_prev_value,
+ tx_outputs_data.output_index,
+ tx_outputs_data.output_value,
+ tx_outputs_data.output_scriptpubkey,
+ tx_outputs_data.output_spender_txid,
+ tx_outputs_data.output_spender_index,
+ 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
+ FROM transaction_data
+ LEFT JOIN tx_inputs_data ON transaction_data.txid = tx_inputs_data.txid
+ LEFT JOIN tx_outputs_data ON transaction_data.txid = tx_outputs_data.txid
+ LEFT JOIN tx_vmetaout ON transaction_data.txid = tx_vmetaout.txid
+ ORDER BY tx_inputs_data.input_index, tx_outputs_data.output_index
+ `);
+
+ if (queryResult.rows.length === 0) {
+ return error(404, 'Transaction not found');
+ }
+
+ const [transaction] = processTransactions(queryResult, true);
+
+ return json(transaction);
+}
diff --git a/src/routes/auctions/current/+page.svelte b/src/routes/auctions/current/+page.svelte
new file mode 100644
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 100644
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 100644
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 100644
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 100644
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 100644
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 100644
index 0000000..56d31b8
--- /dev/null
+++ b/src/routes/block/[hash=hash]/+page.svelte
@@ -0,0 +1,68 @@
+
+
+{#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 100644
index 0000000..90804e8
--- /dev/null
+++ b/src/routes/block/[height=height]/+page.svelte
@@ -0,0 +1,67 @@
+
+
+{#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 100644
index 0000000..f5abca8
--- /dev/null
+++ b/src/routes/mempool/+page.svelte
@@ -0,0 +1,67 @@
+
+
+{#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 100644
index 0000000..ae8c756
--- /dev/null
+++ b/src/routes/space/[name]/+page.svelte
@@ -0,0 +1,720 @@
+
+
+{#if !data.stats.total_actions || data.stats.total_actions == 0}
+
+
{$page.params.name}
+
+
This name is available.
+
You can open an auction for it, learn more here.
+
+
+{: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}
+
+
+
+
+
+
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}
+
+ {#if vmetaout.script_error}
+
+ Script Error: {vmetaout.script_error}
+
+ {/if}
+ {#if vmetaout.reason}
+
+ Reason: {vmetaout.reason}
+
+ {/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 100644
index 0000000..ed4d262
--- /dev/null
+++ b/src/routes/space/[name]/+page.ts
@@ -0,0 +1,28 @@
+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] = await Promise.all([
+ fetch(ROUTES.api.space.history(name)),
+ fetch(ROUTES.api.space.stats(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}`);
+ }
+
+ const spaceHistory = await spaceHistoryResponse.json();
+ const stats = await statsResponse.json();
+
+ return {
+ items: spaceHistory.items,
+ latest: spaceHistory.latest,
+ pagination: spaceHistory.pagination,
+ currentHeight: spaceHistory.currentHeight,
+ stats
+ };
+};
diff --git a/src/routes/tx/[txid]/+page.server.ts b/src/routes/tx/[txid]/+page.server.ts
new file mode 100644
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 100644
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
+ }
+});