diff --git a/.husky/pre-commit b/.husky/pre-commit
index e65f5e7b..60e30912 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,3 +1,6 @@
#!/usr/bin/env sh
+export PATH="$PATH:/c/Program Files/Git/bin:/c/Program Files/Git/usr/bin:C:/Program Files/Git/bin:C:/Program Files/Git/usr/bin"
+
sh ./scripts/check-no-package-lock.sh
npx lint-staged
+
diff --git a/src/components/common/BondingCurveChart.tsx b/src/components/common/BondingCurveChart.tsx
new file mode 100644
index 00000000..4bd1e7b0
--- /dev/null
+++ b/src/components/common/BondingCurveChart.tsx
@@ -0,0 +1,147 @@
+import {
+ ResponsiveContainer,
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ Tooltip,
+ CartesianGrid,
+ ReferenceDot,
+} from 'recharts';
+import { cn } from '@/lib/utils';
+
+export interface BondingCurveDataPoint {
+ supply: number;
+ priceXLM: number;
+ isCurrent?: boolean;
+}
+
+export interface BondingCurveChartProps {
+ data?: BondingCurveDataPoint[];
+ currentSupply?: number;
+ className?: string;
+ width?: number | string;
+ height?: number | string;
+}
+
+export function BondingCurveChart({
+ data = [],
+ currentSupply,
+ className,
+ width = '100%',
+ height = 300,
+}: BondingCurveChartProps) {
+ if (!data || data.length === 0) {
+ return (
+
+ No data
+
+ );
+ }
+
+ const maxSupplyInData = Math.max(...data.map((d) => d.supply));
+ const xAxisMax = currentSupply ?? maxSupplyInData;
+
+ const currentPoint = data.find(
+ (d) => d.isCurrent || (currentSupply !== undefined && d.supply === currentSupply)
+ );
+
+interface CustomDotProps {
+ cx?: number;
+ cy?: number;
+ payload?: BondingCurveDataPoint;
+}
+
+ const CustomDot = (props: CustomDotProps) => {
+ const { cx, cy, payload } = props;
+ if (!cx || !cy || !payload) return null;
+
+ const isHighlighted =
+ payload.isCurrent || (currentSupply !== undefined && payload.supply === currentSupply);
+
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+
+
+ [`${value} XLM`, 'Price']}
+ labelFormatter={(label: number) => `Key Supply: ${label}`}
+ />
+ }
+ activeDot={{ r: 8, className: 'highlight-active-dot' }}
+ />
+ {currentPoint && (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/common/__tests__/BondingCurveChart.test.tsx b/src/components/common/__tests__/BondingCurveChart.test.tsx
new file mode 100644
index 00000000..7e052bbe
--- /dev/null
+++ b/src/components/common/__tests__/BondingCurveChart.test.tsx
@@ -0,0 +1,190 @@
+import type { ReactNode } from 'react';
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { BondingCurveChart, type BondingCurveDataPoint } from '../BondingCurveChart';
+import * as recharts from 'recharts';
+
+// Mock Recharts library to inspect parameters and test data shapes
+vi.mock('recharts', async (importOriginal) => {
+ const original = await importOriginal();
+ return {
+ ...original,
+ ResponsiveContainer: vi.fn(
+ ({
+ children,
+ width,
+ height,
+ }: {
+ children?: ReactNode;
+ width?: string | number;
+ height?: string | number;
+ }) => (
+
+ {children}
+
+ )
+ ),
+ LineChart: vi.fn(
+ ({
+ children,
+ data,
+ 'data-testid': testId,
+ }: {
+ children?: ReactNode;
+ data?: unknown;
+ 'data-testid'?: string;
+ }) => (
+
+ {children}
+
+ )
+ ),
+ XAxis: vi.fn(
+ ({
+ dataKey,
+ domain,
+ 'data-testid': testId,
+ }: {
+ dataKey?: string;
+ domain?: unknown;
+ 'data-testid'?: string;
+ }) => (
+
+ )
+ ),
+ YAxis: vi.fn(
+ ({ dataKey, 'data-testid': testId }: { dataKey?: string; 'data-testid'?: string }) => (
+
+ )
+ ),
+ Tooltip: vi.fn(() => ),
+ CartesianGrid: vi.fn(() => ),
+ Line: vi.fn(({ dataKey }: { dataKey?: string }) => (
+
+ )),
+ ReferenceDot: vi.fn(
+ ({
+ x,
+ y,
+ className,
+ 'data-testid': testId,
+ }: {
+ x?: number;
+ y?: number;
+ className?: string;
+ 'data-testid'?: string;
+ }) => (
+
+ )
+ ),
+ };
+});
+
+describe('BondingCurveChart', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders a message "No data" when given an empty data array', () => {
+ render();
+
+ expect(screen.getByText('No data')).toBeInTheDocument();
+ expect(screen.getByTestId('no-data-message')).toBeInTheDocument();
+ });
+
+ it('renders a message "No data" when data prop is undefined', () => {
+ render();
+
+ expect(screen.getByText('No data')).toBeInTheDocument();
+ });
+
+ it('renders the chart container with exactly N data points when given N data points for supply N', () => {
+ const sampleData: BondingCurveDataPoint[] = [
+ { supply: 1, priceXLM: 1.01 },
+ { supply: 2, priceXLM: 1.02 },
+ { supply: 3, priceXLM: 1.03 },
+ { supply: 4, priceXLM: 1.04 },
+ { supply: 5, priceXLM: 1.05 },
+ ];
+
+ render();
+
+ const chartElement = screen.getByTestId('bonding-curve-chart');
+ expect(chartElement).toBeInTheDocument();
+ expect(chartElement).toHaveAttribute('data-datapoints-count', '5');
+
+ // Assert LineChart mock received all 5 data points
+ const lineChartMock = vi.mocked(recharts.LineChart);
+ expect(lineChartMock).toHaveBeenCalled();
+ const lineChartProps = lineChartMock.mock.calls[0][0];
+ expect(lineChartProps.data).toHaveLength(5);
+ expect(lineChartProps.data).toEqual(sampleData);
+ });
+
+ it('sets the x-axis maximum to equal the current supply value', () => {
+ const sampleData: BondingCurveDataPoint[] = [
+ { supply: 1, priceXLM: 1.1 },
+ { supply: 2, priceXLM: 1.2 },
+ { supply: 3, priceXLM: 1.3 },
+ ];
+ const currentSupply = 25;
+
+ render();
+
+ const chartElement = screen.getByTestId('bonding-curve-chart');
+ expect(chartElement).toHaveAttribute('data-xaxis-max', '25');
+
+ const xAxisMock = vi.mocked(recharts.XAxis);
+ expect(xAxisMock).toHaveBeenCalled();
+ const xAxisProps = xAxisMock.mock.calls[0][0];
+ expect(xAxisProps.domain).toEqual([0, 25]);
+ });
+
+ it('marks the current price point with a distinct highlight class', () => {
+ const sampleData: BondingCurveDataPoint[] = [
+ { supply: 1, priceXLM: 1.0 },
+ { supply: 2, priceXLM: 1.1, isCurrent: true },
+ { supply: 3, priceXLM: 1.2 },
+ ];
+
+ render();
+
+ // Check reference dot rendered for current point has highlight class
+ const referenceDot = screen.getByTestId('reference-current-dot');
+ expect(referenceDot).toBeInTheDocument();
+ expect(referenceDot).toHaveClass('current-price-highlight');
+ expect(referenceDot).toHaveClass('highlight');
+ expect(referenceDot).toHaveAttribute('data-x', '2');
+ expect(referenceDot).toHaveAttribute('data-y', '1.1');
+ });
+
+ it('mocks the chart library and asserts it is called with the correct data shape', () => {
+ const sampleData: BondingCurveDataPoint[] = [
+ { supply: 10, priceXLM: 1.5 },
+ { supply: 20, priceXLM: 2.0, isCurrent: true },
+ ];
+
+ render();
+
+ // Verify ResponsiveContainer was invoked
+ expect(recharts.ResponsiveContainer).toHaveBeenCalled();
+
+ // Verify LineChart was invoked with correct data shape { supply: number, priceXLM: number }
+ const lineChartMock = vi.mocked(recharts.LineChart);
+ expect(lineChartMock).toHaveBeenCalled();
+ const firstCallProps = lineChartMock.mock.calls[0][0];
+ expect(firstCallProps.data).toEqual([
+ { supply: 10, priceXLM: 1.5 },
+ { supply: 20, priceXLM: 2.0, isCurrent: true },
+ ]);
+ });
+});