Skip to content

Instantly share code, notes, and snippets.

@dckc
Last active October 13, 2023 23:05
Show Gist options
  • Select an option

  • Save dckc/5eb31c216f4bf10e8f7a0ff82ced6108 to your computer and use it in GitHub Desktop.

Select an option

Save dckc/5eb31c216f4bf10e8f7a0ff82ced6108 to your computer and use it in GitHub Desktop.

Revisions

  1. dckc revised this gist Oct 13, 2023. 2 changed files with 27 additions and 17 deletions.
    35 changes: 18 additions & 17 deletions unbundle.html
    Original file line number Diff line number Diff line change
    @@ -28,9 +28,13 @@ <h1>Agoric Bundle Explorer</h1>
    <br />
    <label>node: <input name="node" value="devnet.api.agoric.net"/></label>
    <br />
    <button type="button" onclick="explore()">Explore</button>
    <button type="button" onclick="exporeTx()">Explore</button>

    <hr />
    <label
    >sha512: <small><input name="sha512" size="128" readonly/></small
    ></label>
    <br />
    <label>stored size: <input name="storedSize" readonly /> bytes</label>
    <br />

    @@ -51,7 +55,7 @@ <h2>Compartments</h2>
    </section>

    <section>
    <h2>Modules</h2>
    <h2>Files</h2>
    <table class="report">
    <thead>
    <tr>
    @@ -66,29 +70,26 @@ <h2>Modules</h2>
    <body>
    <script type="module">
    import { Cosmos, Agoric } from './unbundle.js';
    import { makeDocTools } from './docTools.js';

    const { entries } = Object;
    const $ = document.querySelector.bind(document);
    const elt = (tag, attrs = {}, children = []) => {
    const it = document.createElement(tag);
    entries(attrs).forEach(([name, value]) => it.setAttribute(name, value));
    children.forEach(ch => {
    if (typeof ch === 'string') {
    it.appendChild(document.createTextNode(ch));
    } else {
    it.appendChild(ch);
    }
    });
    return it;
    const { $, $field, elt, setChoices } = makeDocTools(document);

    const queryInstallBundleTxs = async () => {
    const node = $field('node').value;
    const txs = await Agoric.queryBundleInstalls(node);
    console.log('query', txs);
    setChoices($field('txCandidates'), txs, 'txHash', 'txHash');
    };

    const explore = async () => {
    const exporeTx = async () => {
    console.log('explore');
    const txHash = $('input[name="txHash"]').value;
    const node = $('input[name="node"]').value;
    const [m0] = await Cosmos.txMessages(txHash, node);
    const { bundle, size: storedSize } = await Agoric.getBundle(m0);

    const { endoZipBase64Sha512: sha512 } = bundle;
    $('input[name="sha512"]').value = sha512;
    const storagePrice = parseFloat($('input[name="storagePrice"]').value);
    $('input[name="storedSize"]').value = storedSize;
    $('input[name="storageFee"]').value = storedSize * storagePrice;
    @@ -117,6 +118,6 @@ <h2>Modules</h2>
    };

    // "export"
    Object.assign(globalThis, { explore });
    Object.assign(globalThis, { queryInstallBundleTxs, exporeTx });
    </script>
    </body>
    9 changes: 9 additions & 0 deletions unbundle.js
    Original file line number Diff line number Diff line change
    @@ -30,6 +30,15 @@ export const Cosmos = {
    };

    export const Agoric = {
    queryBundleInstalls: (node, action = 'agoric.swingset.MsgInstallBundle') =>
    // "accept: application/json"?
    fetch(
    `https://${node}/tx_search?query="message.action='/${action}'"&prove=false&page=1&per_page=1&order_by="desc"&match_events=true`,
    )
    // TODO: non-ok statuses
    .then(res => res.json())
    // { hash, height, index }
    .then(obj => obj),
    getBundle: async msg => {
    if (!('compressed_bundle' in msg)) {
    throw Error('no compressed_bundle - TODO: uncompressed bundle support');
  2. dckc revised this gist Oct 5, 2023. 2 changed files with 26 additions and 7 deletions.
    31 changes: 25 additions & 6 deletions unbundle.html
    Original file line number Diff line number Diff line change
    @@ -26,12 +26,23 @@ <h1>Agoric Bundle Explorer</h1>
    ></label>
    <small>of InstallBundle tx</small>
    <br />
    <label
    >node: <input name="node" value="devnet.api.agoric.net" readonly
    /></label>
    <small><em>TODO: support other nodes</em></small>
    <label>node: <input name="node" value="devnet.api.agoric.net"/></label>
    <br />
    <button type="button" onclick="explore()">Explore</button>

    <hr />
    <label>stored size: <input name="storedSize" readonly /> bytes</label>
    <br />

    <label
    >storage price:
    <input name="storagePrice" value="0.002" readonly /> IST/byte</label
    >
    <small><em>(TODO: fetch dynamically from chain)</em></small>
    <br />

    <label>storage cost: <input name="storageFee" readonly /> IST</label>
    <br />
    </fieldset>

    <section id="sec-compartments">
    @@ -74,8 +85,14 @@ <h2>Modules</h2>
    const explore = async () => {
    console.log('explore');
    const txHash = $('input[name="txHash"]').value;
    const [m0] = await Cosmos.txMessages(txHash);
    const bundle = await Agoric.getBundle(m0);
    const node = $('input[name="node"]').value;
    const [m0] = await Cosmos.txMessages(txHash, node);
    const { bundle, size: storedSize } = await Agoric.getBundle(m0);

    const storagePrice = parseFloat($('input[name="storagePrice"]').value);
    $('input[name="storedSize"]').value = storedSize;
    $('input[name="storageFee"]').value = storedSize * storagePrice;

    const loader = await Agoric.getZipLoader(bundle);

    const cmap = loader.extractAsJSON('compartment-map.json');
    @@ -86,6 +103,7 @@ <h2>Modules</h2>
    const { files } = loader;

    const tbody = $('tbody');
    let totalSize = 0;
    for (const name of Object.keys(files)) {
    const size = loader.extractAsText(name).length;
    console.log(size, name);
    @@ -94,6 +112,7 @@ <h2>Modules</h2>
    elt('td', {}, [name]),
    ]);
    tbody.appendChild(row);
    totalSize += size;
    }
    };

    2 changes: 1 addition & 1 deletion unbundle.js
    Original file line number Diff line number Diff line change
    @@ -44,7 +44,7 @@ export const Agoric = {
    if (!('moduleFormat' in bundle)) {
    throw Error('no moduleFormat');
    }
    return bundle;
    return { bundle, size };
    },
    getZipLoader: async bundle => {
    const { moduleFormat } = bundle;
  3. dckc revised this gist Oct 1, 2023. No changes.
  4. dckc revised this gist Oct 1, 2023. 1 changed file with 4 additions and 1 deletion.
    5 changes: 4 additions & 1 deletion 0-bundle-explorer.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,6 @@
    serve up the .html and .js in a web server (using vs-code "go live" or python httpserver.py or whatever).

    Paste a devnet tx id, hit explore.
    Paste a devnet tx id, hit explore.


    ![screenshot of bundle explorer showing modules and their sizes](https://user-images.githubusercontent.com/150986/271812727-601e5dd4-08cd-4e57-981e-8c9d96c86782.png)
  5. dckc created this gist Oct 1, 2023.
    3 changes: 3 additions & 0 deletions 0-bundle-explorer.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    serve up the .html and .js in a web server (using vs-code "go live" or python httpserver.py or whatever).

    Paste a devnet tx id, hit explore.
    103 changes: 103 additions & 0 deletions unbundle.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,103 @@
    <head>
    <title>Bundle Explorer - Agoric</title>
    <style>
    .report {
    border-collapse: collapse;
    font-family: sans-serif;
    }
    .report tr:nth-child(odd) {
    background-color: #fff;
    }

    .report tr:nth-child(even) {
    background-color: #eee;
    }
    th,
    td {
    border: 1px solid black;
    padding: 4px;
    }
    </style>
    </head>
    <h1>Agoric Bundle Explorer</h1>
    <fieldset>
    <label
    >txHash: <small><input name="txHash"/></small
    ></label>
    <small>of InstallBundle tx</small>
    <br />
    <label
    >node: <input name="node" value="devnet.api.agoric.net" readonly
    /></label>
    <small><em>TODO: support other nodes</em></small>
    <br />
    <button type="button" onclick="explore()">Explore</button>
    </fieldset>

    <section id="sec-compartments">
    <h2>Compartments</h2>
    <label>entry: <input name="entry" readonly size="120"/></label>
    </section>

    <section>
    <h2>Modules</h2>
    <table class="report">
    <thead>
    <tr>
    <th>Size</th>
    <th>Module</th>
    </tr>
    </thead>
    <tbody></tbody>
    </table>
    </section>

    <body>
    <script type="module">
    import { Cosmos, Agoric } from './unbundle.js';

    const { entries } = Object;
    const $ = document.querySelector.bind(document);
    const elt = (tag, attrs = {}, children = []) => {
    const it = document.createElement(tag);
    entries(attrs).forEach(([name, value]) => it.setAttribute(name, value));
    children.forEach(ch => {
    if (typeof ch === 'string') {
    it.appendChild(document.createTextNode(ch));
    } else {
    it.appendChild(ch);
    }
    });
    return it;
    };

    const explore = async () => {
    console.log('explore');
    const txHash = $('input[name="txHash"]').value;
    const [m0] = await Cosmos.txMessages(txHash);
    const bundle = await Agoric.getBundle(m0);
    const loader = await Agoric.getZipLoader(bundle);

    const cmap = loader.extractAsJSON('compartment-map.json');
    $('input[name="entry"]').value = JSON.stringify(cmap.entry);
    // TODO: cmap.compartments
    // cmap.tags ???

    const { files } = loader;

    const tbody = $('tbody');
    for (const name of Object.keys(files)) {
    const size = loader.extractAsText(name).length;
    console.log(size, name);
    const row = elt('tr', {}, [
    elt('td', {}, [`${size}`]),
    elt('td', {}, [name]),
    ]);
    tbody.appendChild(row);
    }
    };

    // "export"
    Object.assign(globalThis, { explore });
    </script>
    </body>
    56 changes: 56 additions & 0 deletions unbundle.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,56 @@
    /* global fetch, DecompressionStream, Response, FileReader */
    import ZipLoader from 'https://esm.sh/[email protected]';

    export const Browser = {
    toBlob: (base64, type = 'application/octet-stream') =>
    fetch(`data:${type};base64,${base64}`).then(res => res.blob()),
    decompressBlob: async blob => {
    const ds = new DecompressionStream('gzip');
    const decompressedStream = blob.stream().pipeThrough(ds);
    const r = await new Response(decompressedStream).blob();
    return r;
    },
    };

    const logged = label => x => {
    console.log(label, x);
    return x;
    };

    export const Cosmos = {
    txURL: (txHash, node = 'devnet.api.agoric.net') =>
    `https://${node}/cosmos/tx/v1beta1/txs/${txHash}`,
    txMessages: (txHash, node = 'devnet.api.agoric.net') =>
    fetch(Cosmos.txURL(txHash, node))
    .then(res => {
    console.log('status', res.status);
    return res.json();
    })
    .then(j => j.tx.body.messages),
    };

    export const Agoric = {
    getBundle: async msg => {
    if (!('compressed_bundle' in msg)) {
    throw Error('no compressed_bundle - TODO: uncompressed bundle support');
    }
    const { compressed_bundle: b64gzip, uncompressed_size: size } = msg;
    const gzipBlob = await Browser.toBlob(b64gzip);
    const fullText = await Browser.decompressBlob(gzipBlob).then(b => b.text());
    if (fullText.length !== parseInt(size, 10)) {
    throw Error('bundle size mismatch');
    }
    const bundle = JSON.parse(fullText);
    if (!('moduleFormat' in bundle)) {
    throw Error('no moduleFormat');
    }
    return bundle;
    },
    getZipLoader: async bundle => {
    const { moduleFormat } = bundle;
    console.log(moduleFormat, 'TODO: check for endo type');
    const { endoZipBase64 } = bundle;
    const zipBlob = await Browser.toBlob(endoZipBase64);
    return ZipLoader.unzip(zipBlob);
    },
    };