Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/__tests__/OracleDataWidget.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,27 @@ describe('OracleDataWidget', () => {
expect(html).toContain('mm');
});

it('renders oracle metadata as escaped text', () => {
mockUseOracleReading.mockReturnValue({
reading: {
key: 'rainfall:1,1:2025-01',
value: '324000000',
confidence: 95,
timestamp: 1720000000,
source: '<script>alert(1)</script>',
dataType: 'weather',
},
loading: false,
error: null,
refetch: vi.fn(),
});

const html = renderToStaticMarkup(<OracleDataWidget oracleKey="rainfall:1,1:2025-01" />);
expect(html).toContain('Source: ');
expect(html).toContain('&lt;script&gt;alert(1)&lt;/script&gt;');
expect(html).not.toContain('<script>alert(1)</script>');
});

it('displays temperature value with °C unit', () => {
mockUseOracleReading.mockReturnValue({
reading: {
Expand Down
12 changes: 6 additions & 6 deletions src/__tests__/TriggerConditionBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,25 @@ function makeProduct(overrides: Partial<Product> = {}): Product {
}

describe('TriggerConditionBadge', () => {
it('renders "<" symbol for LessThan comparison', () => {
it('renders plain-language text for LessThan comparison', () => {
const html = renderToStaticMarkup(
<TriggerConditionBadge product={makeProduct({ comparison: 'LessThan', threshold: '30' })} />
);
expect(html).toContain('&lt; 30');
expect(html).toContain('Less than 30 mm');
});

it('renders ">" symbol for GreaterThan comparison', () => {
it('renders plain-language text for GreaterThan comparison', () => {
const html = renderToStaticMarkup(
<TriggerConditionBadge product={makeProduct({ comparison: 'GreaterThan', threshold: '50' })} />
);
expect(html).toContain('&gt; 50');
expect(html).toContain('Greater than 50 mm');
});

it('renders "=" symbol for Equal comparison', () => {
it('renders plain-language text for Equal comparison', () => {
const html = renderToStaticMarkup(
<TriggerConditionBadge product={makeProduct({ comparison: 'Equal', threshold: '100' })} />
);
expect(html).toContain('= 100');
expect(html).toContain('Equal to 100 mm');
});

it('displays the triggerType', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/components/OracleDataWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ function OracleDataWidgetComponent({ oracleKey, className }: OracleDataWidgetPro

const confLabel = confidenceLabel(reading.confidence);
const confColour = confidenceColour(reading.confidence);
const formattedOracleValue = String(formatOracleValue(reading.value, parseOracleKey(oracleKey).dataType));

return (
<div className={`rounded-2xl border border-white/10 bg-white/[0.03] p-4 ${className ?? ''}`}>
<p className="text-[10px] uppercase tracking-widest text-gray-400">Oracle Reading</p>
<p className="mt-1 text-xs text-gray-400 truncate">{oracleKeyLabel(oracleKey)}</p>

<p className="mt-3 text-2xl font-black text-white">
{formatOracleValue(reading.value, parseOracleKey(oracleKey).dataType)}
{formattedOracleValue}
</p>

<div className="mt-3 flex items-center justify-between text-[10px] text-gray-400">
Expand Down
12 changes: 6 additions & 6 deletions src/components/TriggerConditionBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ interface TriggerConditionBadgeProps {
className?: string;
}

function comparisonSymbol(c: Comparison): string {
if (c === 'LessThan') return '<';
if (c === 'GreaterThan') return '>';
return '=';
function comparisonLabel(c: Comparison): string {
if (c === 'LessThan') return 'Less than';
if (c === 'GreaterThan') return 'Greater than';
return 'Equal to';
}

const CATEGORY_UNITS: Record<Category, string> = {
Expand All @@ -20,15 +20,15 @@ const CATEGORY_UNITS: Record<Category, string> = {
};

export function TriggerConditionBadge({ product, className }: TriggerConditionBadgeProps) {
const symbol = comparisonSymbol(product.comparison);
const comparison = comparisonLabel(product.comparison);
const units = CATEGORY_UNITS[product.category] ?? '';
return (
<span
className={`inline-flex items-center gap-1.5 rounded-full border border-gray-200 dark:border-white/10 bg-gray-100 dark:bg-white/5 px-2.5 py-1 font-mono text-xs text-gray-600 dark:text-gray-300 ${className ?? ''}`}
>
<span className="text-teal-400">{product.triggerType}</span>
<span className="text-gray-500 dark:text-gray-400">·</span>
<span>{symbol} {product.threshold}{units ? ` ${units}` : ''}</span>
<span>{comparison} {product.threshold}{units ? ` ${units}` : ''}</span>
</span>
);
}
15 changes: 14 additions & 1 deletion src/lib/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,14 +101,27 @@ export function confidenceIcon(confidence: number): string {
return '○';
}

const rainfallKeyCache = new Map<string, string>();
const MAX_RAINFALL_KEY_CACHE_SIZE = 100;

export function buildRainfallKey(lat: number, lng: number, year: number, month: number): string {
const cacheKey = `${lat}|${lng}|${year}|${month}`;
const cachedKey = rainfallKeyCache.get(cacheKey);
if (cachedKey) return cachedKey;

const monthStr = String(month).padStart(2, '0');
// Clamp coordinate precision to 4 decimals (~11m). Without a cap, values like
// parseFloat('-0.09171234567') produce long keys that can blow past Soroban's
// 32-char limit. toFixed(4) keeps keys well within budget (issue #222).
const latStr = clampCoord(lat);
const lngStr = clampCoord(lng);
return `rainfall:${latStr},${lngStr}:${year}-${monthStr}`;
const key = `rainfall:${latStr},${lngStr}:${year}-${monthStr}`;
if (rainfallKeyCache.size >= MAX_RAINFALL_KEY_CACHE_SIZE) {
const oldestKey = rainfallKeyCache.keys().next().value;
if (oldestKey !== undefined) rainfallKeyCache.delete(oldestKey);
}
rainfallKeyCache.set(cacheKey, key);
return key;
}

// Format a coordinate to at most 4 decimals without trailing-zero padding,
Expand Down
Loading