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
2 changes: 1 addition & 1 deletion src/domain/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export type DomainTx = {
addressCreds?: AddressCreds;
ada: bigint;
assets: Array<{ policyId: string; assetName: string; quantity: bigint }>;
datum?: { inline?: boolean | string; hash?: string; ref?: boolean; type?: string; content?: unknown; size?: number };
datum?: { inline?: boolean | string; hash?: string; ref?: boolean; type?: string; content?: unknown; cbor?: string; size?: number };
scriptRef?: { type: "PlutusV1"|"PlutusV2"|"PlutusV3"|"Native"; bytes: string };
}>;
mint?: Array<{ policyId: string; assetName: string; quantity: bigint }>;
Expand Down
148 changes: 61 additions & 87 deletions src/features/inspector/tabs/scripts/DatumDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,110 +1,84 @@
// src/features/inspector/tabs/scripts/DatumDisplay.tsx
'use client';

import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Collapsible, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Copy, ChevronDown, ChevronRight } from 'lucide-react';
import { Copy } from 'lucide-react';
import { toast } from 'sonner';
import { DatumInfo } from '@/lib/types/script-eval';
import { safeStringify } from '@/lib/utils';
import { PlutusJsonView } from './PlutusJsonView';

interface DatumDisplayProps {
datum: DatumInfo;
}

async function copyText(text: string, label: string) {
try {
await navigator.clipboard.writeText(text);
toast.success(`${label} copied to clipboard`);
} catch {
toast.error(`Failed to copy ${label.toLowerCase()}`);
}
}

export function DatumDisplay({ datum }: DatumDisplayProps) {
const [isOpen, setIsOpen] = useState(false);
const cbor = datum.cbor;
const hasDecoded = datum.decodedContent != null;

const decodedJson = hasDecoded ? safeStringify(datum.decodedContent, 2) : null;
const isLong = hasDecoded
? (decodedJson!.length > 200)
: (datum.value.length > 128);
// Hash-only datums: nothing to inspect; the parent card already shows the hash row.
if (!cbor && !hasDecoded) {
return null;
}

const copyDatum = async () => {
const text = decodedJson || datum.value;
try {
await navigator.clipboard.writeText(text);
toast.success('Datum copied to clipboard');
} catch {
toast.error('Failed to copy datum');
}
};
const decodedJson = hasDecoded ? safeStringify(datum.decodedContent, 2) : null;

return (
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Datum</span>
<Badge
variant="outline"
className={
datum.type === 'inline'
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 text-[10px] px-1.5 py-0'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 text-[10px] px-1.5 py-0'
}
>
{datum.type}
</Badge>
{datum.decodedType && (
<Badge
variant="outline"
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200 text-[10px] px-1.5 py-0"
>
{datum.decodedType}
</Badge>
)}
<Button variant="ghost" size="sm" className="h-5 w-5 p-0 ml-auto" onClick={copyDatum}>
<Copy className="h-3 w-3" />
</Button>
</div>

{/* Decoded Plutus Data */}
{hasDecoded ? (
isLong ? (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<pre className={`text-xs bg-muted px-2 py-1 rounded block whitespace-pre-wrap break-all ${isOpen ? '' : 'max-h-32 overflow-hidden'}`}>
{isOpen ? decodedJson : `${decodedJson!.slice(0, 200)}...`}
</pre>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 text-xs mt-1 px-2">
{isOpen ? (
<><ChevronDown className="h-3 w-3 mr-1" /> Collapse</>
) : (
<><ChevronRight className="h-3 w-3 mr-1" /> Expand</>
)}
</Button>
</CollapsibleTrigger>
</Collapsible>
) : (
<pre className="text-xs bg-muted px-2 py-1 rounded block whitespace-pre-wrap break-all">
{decodedJson}
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{/* Left pane: raw CBOR hex (hidden on small screens) */}
{cbor && (
<div className="hidden md:block space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">CBOR</span>
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => copyText(cbor, 'Datum CBOR')}
>
<Copy className="h-3 w-3" />
</Button>
</div>
<pre className="text-xs font-mono bg-muted px-3 py-2 rounded block whitespace-pre-wrap break-all max-h-[60vh] overflow-auto">
{cbor}
</pre>
)
) : (
/* Raw hash / hex fallback */
isLong ? (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<code className="text-xs bg-muted px-2 py-1 rounded block break-all">
{isOpen ? datum.value : `${datum.value.slice(0, 128)}...`}
</code>
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 text-xs mt-1 px-2">
{isOpen ? (
<><ChevronDown className="h-3 w-3 mr-1" /> Collapse</>
) : (
<><ChevronRight className="h-3 w-3 mr-1" /> Expand ({datum.value.length} chars)</>
)}
</Button>
</CollapsibleTrigger>
</Collapsible>
) : (
<code className="text-xs bg-muted px-2 py-1 rounded block break-all">
{datum.value}
</code>
)
</div>
)}

{/* Right pane: decoded JSON */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">JSON</span>
{decodedJson && (
<Button
variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => copyText(decodedJson, 'Datum JSON')}
>
<Copy className="h-3 w-3" />
</Button>
)}
</div>
<div className="bg-muted px-3 py-2 rounded max-h-[60vh] overflow-auto">
{hasDecoded ? (
<PlutusJsonView data={datum.decodedContent} />
) : (
<span className="text-xs text-muted-foreground italic">
Unable to decode CBOR to JSON
</span>
)}
</div>
</div>
</div>
);
}
88 changes: 68 additions & 20 deletions src/features/inspector/tabs/scripts/DatumOutputCard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
// src/features/inspector/tabs/scripts/DatumOutputCard.tsx
'use client';

import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Copy } from 'lucide-react';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import { ChevronDown, ChevronRight, Copy } from 'lucide-react';
import { toast } from 'sonner';
import { DatumDisplay } from './DatumDisplay';
import { DatumInfo } from '@/lib/types/script-eval';
Expand All @@ -16,49 +22,73 @@ interface DatumOutputCardProps {
hash?: string;
type?: string;
content?: unknown;
cbor?: string;
size?: number;
};
defaultOpen?: boolean;
}

export function DatumOutputCard({ index, address, datum }: DatumOutputCardProps) {
export function DatumOutputCard({ index, address, datum, defaultOpen = false }: DatumOutputCardProps) {
const isInline = !!datum.inline;
const inspectable = isInline && (datum.cbor || datum.content != null);
const [open, setOpen] = useState(defaultOpen);

const datumInfo: DatumInfo = {
type: isInline ? 'inline' : 'hash',
value: datum.hash || '',
cbor: typeof datum.cbor === 'string' ? datum.cbor : undefined,
decodedType: datum.type,
decodedContent: datum.content,
};

return (
const card = (
<div className="border rounded-lg p-3 space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0">
{inspectable ? (
<CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 px-1 -ml-1">
{open ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button>
</CollapsibleTrigger>
) : null}
<span className="text-sm font-medium">Output #{index}</span>
<code className="text-xs text-muted-foreground truncate max-w-[180px]">
{address.slice(0, 24)}...
</code>
</div>
<Badge
variant="outline"
className={
isInline
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
}
>
{isInline ? 'inline' : 'hash'}
</Badge>
<div className="flex items-center gap-2 shrink-0">
{datum.type && (
<Badge
variant="outline"
className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200 text-[10px] px-1.5 py-0"
>
{datum.type}
</Badge>
)}
<Badge
variant="outline"
className={
isInline
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'
}
>
{isInline ? 'inline' : 'hash'}
</Badge>
</div>
</div>

<DatumDisplay datum={datumInfo} />

{datum.hash && (
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">Datum hash</span>
<div className="flex items-center gap-1">
<code className="text-xs bg-muted px-2 py-0.5 rounded truncate max-w-[200px]">
{datum.hash.slice(0, 16)}...
<div className="flex items-center gap-1 min-w-0">
<code className="text-xs bg-muted px-2 py-0.5 rounded truncate max-w-[260px]">
{datum.hash}
</code>
<Button
variant="ghost"
Expand All @@ -81,6 +111,24 @@ export function DatumOutputCard({ index, address, datum }: DatumOutputCardProps)
<span className="text-xs">{datum.size} bytes</span>
</div>
)}

{inspectable && (
<CollapsibleContent>
<div className="pt-2 border-t">
<DatumDisplay datum={datumInfo} />
</div>
</CollapsibleContent>
)}
</div>
);

if (!inspectable) {
return card;
}

return (
<Collapsible open={open} onOpenChange={setOpen}>
{card}
</Collapsible>
);
}
3 changes: 2 additions & 1 deletion src/features/inspector/tabs/scripts/OutputDatumsInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ export function OutputDatumsInfo({ tx }: OutputDatumsInfoProps) {
</CardHeader>
<CardContent>
<div className="space-y-3">
{outputsWithDatum.map(({ output, index }) => (
{outputsWithDatum.map(({ output, index }, i) => (
<DatumOutputCard
key={index}
index={index}
address={output.address}
datum={output.datum!}
defaultOpen={i === 0}
/>
))}
</div>
Expand Down
Loading
Loading