Skip to content
Open
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
13 changes: 10 additions & 3 deletions applications/viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ A live Cardano explorer for your Yaci DevKit devnet, or any [Yaci Store](https:/
transaction, address, stake address, pool, DRep, policy, asset, fingerprint, script, datum or epoch.
- **Transactions you can read:** inputs flow into outputs, scripts show their execution budget, redeemers and datums
are decoded from CBOR, plus mints, metadata, collateral, reference inputs and the raw JSON.
- **Script Lab:** decode any Plutus script into searchable UPLC, see the builtins it uses and verify its hash. It runs
in your browser with the [julc](https://github.com/bloxbean/julc) WebAssembly engine.
- **Script Lab:** in your browser, with the [julc](https://github.com/bloxbean/julc) WebAssembly engine:
- decode any Plutus script into searchable UPLC, see the builtins it uses and verify its hash;
- re-run every script of a transaction with the cost model of its epoch, compare the budget with what was declared
on chain, and see the exact ScriptContext the validator received.
- Addresses, stake accounts, assets, epochs and protocol parameters, pools, rewards, certificates, Ada pots and
Conway governance.
- Light and dark themes; works on a phone.
Expand Down Expand Up @@ -69,7 +71,12 @@ into `static/julc/` (git-ignored). The engine is only downloaded by the browser
release's `julc-wasm-<version>-SHA256SUMS.txt`.
- The engine needs WebAssembly GC and exception handling (exnref): current Chrome, Edge, Firefox and Safari. Other
browsers get a message instead. Playwright's bundled Chromium is too old, so run the Script Lab test with Chrome:
`SCRIPT_LAB_HASH=<plutus script hash> PW_CHANNEL=chrome npx playwright test script-lab`.
`SCRIPT_LAB_HASH=<plutus script hash> SCRIPT_LAB_TX=<tx with redeemers> PW_CHANNEL=chrome npx playwright test script-lab`.
- Re-runs rebuild the transaction from Yaci Store data (`src/lib/julc/replay/`). Until julc builds contexts exactly
as the ledger does ([julc#244](https://github.com/bloxbean/julc/issues/244)), the viewer corrects julc's context
(asset order, the other redeemers, datums given by hash, Plutus V1 shapes) and runs the script again; each result
says what was corrected. Proposal (guardrail) scripts and most certificate kinds aren't replayed yet, and Plutus V1
(pre-2023 history) is partly reproduced.

SvelteKit 2, Svelte 5 and Tailwind. Shared code lives in `src/lib`: `api/` (Yaci Store client), `live/` (WebSocket
with a polling fallback), `ui/` (components), `shell/` (layout, navigation, search), `julc/` (Script Lab) and `format.ts`, `cbor.ts`,
Expand Down
28 changes: 27 additions & 1 deletion applications/viewer/src/lib/cbor.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { decodePlutusData, tryDecodePlutusData } from './cbor';
import { decodePlutusData, encodePlutusData, tryDecodePlutusData, type PlutusData } from './cbor';

describe('decodePlutusData', () => {
it('decodes constructors', () => {
Expand Down Expand Up @@ -28,3 +28,29 @@ describe('decodePlutusData', () => {
expect(tryDecodePlutusData(null)).toBeNull();
});
});

describe('encodePlutusData', () => {
it('encodes the way the Plutus libraries do', () => {
expect(encodePlutusData({ constructor: 0, fields: [] })).toBe('d87980');
expect(encodePlutusData({ constructor: 0, fields: [{ int: '42' }, { bytes: 'cafe' }] })).toBe('d8799f182a42cafeff');
expect(encodePlutusData({ constructor: 7, fields: [] })).toBe('d9050080');
expect(encodePlutusData({ constructor: 200, fields: [] })).toBe('d8668218c880');
expect(encodePlutusData({ int: '-1' })).toBe('20');
expect(encodePlutusData({ map: [{ k: { int: '1' }, v: { int: '2' } }] })).toBe('a10102');
expect(encodePlutusData({ int: (1n << 64n).toString() })).toBe('c249010000000000000000');
expect(encodePlutusData({ int: (-(1n << 64n) - 1n).toString() })).toBe('c349010000000000000000');
expect(encodePlutusData({ int: (-(1n << 64n)).toString() })).toBe('3bffffffffffffffff');
});

it('round-trips through the decoder, including chunked bytes', () => {
const samples: PlutusData[] = [
{ bytes: 'ab'.repeat(100) },
{ constructor: 2, fields: [{ list: [{ map: [] }, { int: '18446744073709551615' }, { int: '-18446744073709551617' }] }] },
{ map: [{ k: { bytes: '' }, v: { constructor: 130, fields: [{ int: '0' }] } }, { k: { bytes: '' }, v: { list: [] } }] }
];
for (const d of samples) expect(decodePlutusData(encodePlutusData(d))).toEqual(d);
const long = encodePlutusData({ bytes: 'ab'.repeat(100) });
expect(long.startsWith('5f5840')).toBe(true);
expect(long.endsWith('ff')).toBe(true);
});
});
72 changes: 72 additions & 0 deletions applications/viewer/src/lib/cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,75 @@ export function tryDecodePlutusData(hex: string | null | undefined): PlutusData
return null;
}
}

// Not `'constructor' in d`: every object inherits a `constructor` property.
const isConstr = (d: PlutusData): d is { constructor: number; fields: PlutusData[] } => typeof (d as { constructor: unknown }).constructor === 'number';

/*
* Encode Plutus data to CBOR hex the way the Plutus libraries do: indefinite-length lists (definite when empty),
* byte strings over 64 bytes in 64-byte chunks, integers beyond 64 bits as bignums, and constructor tags
* 121-127 / 1280-1400 / 102. Used to hand a modified ScriptContext back to the julc VM.
*/
export function encodePlutusData(d: PlutusData): string {
const out: number[] = [];
const head = (major: number, n: bigint) => {
const m = major << 5;
if (n < 24n) out.push(m | Number(n));
else if (n < 0x100n) out.push(m | 24, Number(n));
else if (n < 0x10000n) out.push(m | 25, Number(n >> 8n), Number(n & 0xffn));
else if (n < 0x100000000n) out.push(m | 26, ...[24n, 16n, 8n, 0n].map((s) => Number((n >> s) & 0xffn)));
else out.push(m | 27, ...[56n, 48n, 40n, 32n, 24n, 16n, 8n, 0n].map((s) => Number((n >> s) & 0xffn)));
};
const bytes = (b: number[]) => {
if (b.length <= 64) {
head(2, BigInt(b.length));
out.push(...b);
return;
}
out.push(0x5f);
for (let i = 0; i < b.length; i += 64) {
const chunk = b.slice(i, i + 64);
head(2, BigInt(chunk.length));
out.push(...chunk);
}
out.push(0xff);
};
const list = (items: PlutusData[]) => {
if (!items.length) return out.push(0x80);
out.push(0x9f);
items.forEach(write);
out.push(0xff);
};
const write = (x: PlutusData): void => {
if (isConstr(x)) {
const c = x.constructor;
if (c <= 6) head(6, BigInt(121 + c));
else if (c <= 127) head(6, BigInt(1280 + c - 7));
else {
head(6, 102n);
out.push(0x82);
head(0, BigInt(c));
}
list(x.fields);
} else if ('int' in x) {
const v = BigInt(x.int);
const n = v < 0n ? -1n - v : v;
if (n < 1n << 64n) head(v < 0n ? 1 : 0, n);
else {
head(6, v < 0n ? 3n : 2n);
const hex = n.toString(16);
bytes((hex.length % 2 ? '0' + hex : hex).match(/../g)!.map((h) => parseInt(h, 16)));
}
} else if ('bytes' in x) bytes((x.bytes.match(/../g) ?? []).map((h) => parseInt(h, 16)));
else if ('list' in x) list(x.list);
else {
head(5, BigInt(x.map.length));
for (const { k, v } of x.map) {
write(k);
write(v);
}
}
};
write(d);
return out.map((b) => b.toString(16).padStart(2, '0')).join('');
}
2 changes: 1 addition & 1 deletion applications/viewer/src/lib/julc/ScriptCode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
</button>
<label class="mt-3 flex items-center gap-2 text-xs text-muted">
<input type="checkbox" class="accent-[rgb(var(--c-brand))]" checked={auto} onchange={(e) => toggleAuto(e.currentTarget.checked)} />
Always decode scripts automatically
Run Script Lab automatically
</label>
</div>
{:else if failure}
Expand Down
Loading