+ {priceDisplay !== null && (
+
+
Submission Price
+
+ {priceDisplay} ISLM per token
+
+
+ )}
{/* Only show Funds Source selection if ucDAO balance is greater than 0 */}
diff --git a/libs/burn-waitlist/src/lib/components/price-chart.tsx b/libs/burn-waitlist/src/lib/components/price-chart.tsx
new file mode 100644
index 000000000..ea292b425
--- /dev/null
+++ b/libs/burn-waitlist/src/lib/components/price-chart.tsx
@@ -0,0 +1,251 @@
+'use client';
+
+import { useId, useMemo } from 'react';
+import {
+ Area,
+ AreaChart,
+ CartesianGrid,
+ ResponsiveContainer,
+ Tooltip,
+ XAxis,
+ YAxis,
+} from 'recharts';
+import type { PriceChartPoint } from '../hooks/use-waitlist-price-chart';
+import { formatEthDecimal } from '@haqq/shell-shared';
+
+export interface PriceChartProps {
+ data: PriceChartPoint[];
+ /** Chart height in pixels */
+ height?: number;
+ /** Whether price values are in atto (wei) - will format for display */
+ priceInAtto?: boolean;
+ isLoading?: boolean;
+ error?: Error | null;
+}
+
+const DEFAULT_HEIGHT = 240;
+const ONE_HOUR = 3600;
+
+/**
+ * Expands chart data by adding a point every hour from the last point up to now.
+ * So a single API point becomes a line from that time to current time (same price).
+ */
+function expandDataWithHourlyPoints(
+ data: Array<{ timestamp: number; price: number }>,
+): Array<{ timestamp: number; price: number }> {
+ if (!data.length) return [];
+ const now = Math.floor(Date.now() / 1000);
+ const last = data[data.length - 1]!;
+ if (last.timestamp >= now) return data;
+ const result: PriceChartPoint[] = [...data];
+ for (let t = last.timestamp + ONE_HOUR; t <= now; t += ONE_HOUR) {
+ result.push({ timestamp: t, price: last.price });
+ }
+ if (result[result.length - 1]!.timestamp < now) {
+ result.push({ timestamp: now, price: last.price });
+ }
+ return result;
+}
+
+function formatChartTime(ts: number): string {
+ const d = new Date(ts * 1000);
+ const now = new Date();
+ const isToday =
+ d.getDate() === now.getDate() &&
+ d.getMonth() === now.getMonth() &&
+ d.getFullYear() === now.getFullYear();
+ if (isToday) {
+ return d.toLocaleTimeString(undefined, {
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+ }
+ return d.toLocaleDateString(undefined, {
+ month: 'short',
+ day: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit',
+ });
+}
+
+/**
+ * Price chart built with Recharts (SVG-based). Web3-style dark card.
+ * @see https://recharts.org/
+ */
+export function PriceChart({
+ data,
+ height = DEFAULT_HEIGHT,
+ priceInAtto = true,
+ isLoading = false,
+ error = null,
+}: PriceChartProps) {
+ const uid = useId();
+ const expandedData = useMemo(() => expandDataWithHourlyPoints(data), [data]);
+
+ const chartData = useMemo(
+ () =>
+ expandedData.map((d) => ({
+ ...d,
+ timeLabel: formatChartTime(d.timestamp),
+ })),
+ [expandedData],
+ );
+
+ const formatPrice = useMemo(
+ () => (p: number) =>
+ priceInAtto && p > 0 ? formatEthDecimal(BigInt(p), 2, 0) : p.toFixed(2),
+ [priceInAtto],
+ );
+
+ const currentPriceFormatted = useMemo(() => {
+ if (!chartData.length) return '0';
+ return formatPrice(chartData[chartData.length - 1]!.price);
+ }, [chartData, formatPrice]);
+
+ const isSinglePoint = chartData.length === 1;
+
+ const gradientId = `price-chart-gradient-${uid.replace(/:/g, '')}`;
+
+ if (error) {
+ return (
+
+
+ Failed to load price chart
+
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (!data.length) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ Price history
+
+
+ {isSinglePoint && (
+
+ Holding
+
+ )}
+
+ {currentPriceFormatted}
+
+ ISLM/HAQQ
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ formatChartTime(ts)}
+ stroke="#C5C5C5"
+ tick={{ fill: '#C5C5C5', fontSize: 9 }}
+ axisLine={false}
+ tickLine={false}
+ />
+ formatPrice(p)}
+ stroke="#C5C5C5"
+ tick={{ fill: '#C5C5C5', fontSize: 10 }}
+ axisLine={false}
+ tickLine={false}
+ width={56}
+ tickMargin={12}
+ />
+ ;
+ }) => {
+ if (!active || !payload?.length) return null;
+ const point = payload[0]?.payload as {
+ timestamp: number;
+ price: number;
+ timeLabel: string;
+ };
+ if (!point) return null;
+ return (
+
+
+ {point.timeLabel}
+
+
+ {formatPrice(point.price)} ISLM
+
+
+ );
+ }}
+ cursor={{ stroke: '#C5C5C5', strokeWidth: 1 }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/libs/burn-waitlist/src/lib/components/requests-list.tsx b/libs/burn-waitlist/src/lib/components/requests-list.tsx
index f49e4c830..12d4a5578 100644
--- a/libs/burn-waitlist/src/lib/components/requests-list.tsx
+++ b/libs/burn-waitlist/src/lib/components/requests-list.tsx
@@ -6,6 +6,7 @@ import { Button } from '@haqq/shell-ui-kit';
import { FundsSource } from '../constants/waitlist-config';
import type { Application } from '../hooks/use-waitlist-applications';
import type { WaitlistBalancesResponse } from '../hooks/use-waitlist-balances';
+import { formatEthDecimal } from '@haqq/shell-shared';
export interface RequestsListProps {
applications: Array<
@@ -16,8 +17,11 @@ export interface RequestsListProps {
>;
canCancel: boolean;
onCancel: (requestId: bigint) => void;
+ onMintHaqq?: (applicationId: bigint) => void;
isCancelling?: boolean;
cancellingRequestId?: bigint;
+ isMinting?: boolean;
+ mintingApplicationId?: bigint;
balances?: WaitlistBalancesResponse;
locale?: string;
}
@@ -26,8 +30,11 @@ export function RequestsList({
applications,
canCancel,
onCancel,
+ onMintHaqq,
isCancelling = false,
cancellingRequestId,
+ isMinting = false,
+ mintingApplicationId,
balances,
locale = 'en',
}: RequestsListProps) {
@@ -95,13 +102,31 @@ export function RequestsList({
- Amount:{' '}
+ Burn Amount:{' '}
{amount} ISLM
+ {app.price !== undefined && app.price !== '' && (
+
+ Minting price:{' '}
+
+ {formatEthDecimal(BigInt(app.price), 0, 0)} ISLM/HAQQ
+
+
+ )}
+ {app.receiveAmount !== undefined &&
+ app.receiveAmount !== '' && (
+
+ Mint amount:{' '}
+
+ {formatEthDecimal(BigInt(app.receiveAmount), 4, 18)}{' '}
+ HAQQ
+
+
+ )}
- Source:{' '}
+ Funds Source:{' '}
{sourceLabel}
@@ -121,17 +146,36 @@ export function RequestsList({
)}
- {canCancel && !isCancelled && !isPending && (
-
+ {onMintHaqq &&
+ !isPending &&
+ !isCancelled &&
+ app.valid &&
+ app.ready && (
+
+ )}
+ {canCancel && !isCancelled && !isPending && (
+
+ )}
+
);
diff --git a/libs/burn-waitlist/src/lib/constants/ethiq-config.ts b/libs/burn-waitlist/src/lib/constants/ethiq-config.ts
new file mode 100644
index 000000000..ab3fb942a
--- /dev/null
+++ b/libs/burn-waitlist/src/lib/constants/ethiq-config.ts
@@ -0,0 +1,5 @@
+/**
+ * Ethiq precompile contract address (same on all HAQQ chains)
+ */
+export const ETHIQ_PRECOMPILE_ADDRESS =
+ '0x0000000000000000000000000000000000000900' as `0x${string}`;
diff --git a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts
index 8c051ae53..77bd3939f 100644
--- a/libs/burn-waitlist/src/lib/constants/waitlist-config.ts
+++ b/libs/burn-waitlist/src/lib/constants/waitlist-config.ts
@@ -8,7 +8,7 @@ export const WAITLIST_CONTRACT_ADDRESSES: Record