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