Skip to content

Instantly share code, notes, and snippets.

@dckc
Last active October 13, 2023 23:05
Show Gist options
  • Save dckc/5eb31c216f4bf10e8f7a0ff82ced6108 to your computer and use it in GitHub Desktop.
Save dckc/5eb31c216f4bf10e8f7a0ff82ced6108 to your computer and use it in GitHub Desktop.
Bundle Explorer for Agoric

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.

screenshot of bundle explorer showing modules and their sizes

<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"/></label>
<br />
<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 />
<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">
<h2>Compartments</h2>
<label>entry: <input name="entry" readonly size="120"/></label>
</section>
<section>
<h2>Files</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';
import { makeDocTools } from './docTools.js';
const { entries } = Object;
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 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;
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');
let totalSize = 0;
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);
totalSize += size;
}
};
// "export"
Object.assign(globalThis, { queryInstallBundleTxs, exporeTx });
</script>
</body>
/* global fetch, DecompressionStream, Response, FileReader */
import ZipLoader from 'https://esm.sh/zip-loader@1.2.0';
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 = {
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');
}
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, size };
},
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);
},
};
@dckc
Copy link
Author

dckc commented Oct 1, 2023

screenshot:

image

@dckc
Copy link
Author

dckc commented Oct 7, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment