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.
| <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/[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 = { | |
| 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); | |
| }, | |
| }; |
discussion:
screenshot: