Technical Spec
The BitGen technical spec with working examples and code
Working Examples
Working example using local references: https://toniq-labs.github.io/bitgen-example/
Working example Github repo: https://github.com/Toniq-Labs/bitgen-example
Code Examples
Below is a fully functional version of each file type (final testing on Bitcoin mainnet in progress)
The Renderer JS
The Collection JS
The Collection JSON
The Asset Layer IMAGES
The Inscription HTML
The Provenance JSON
Content Security Policy
Rendering IFrames
Renderer JS
async function render(size, ...inscriptionIds) {
try {
const base64Images = await Promise.all(inscriptionIds.map(async (id) => await getBase64(await (await fetch(`/content/${id}`)).blob())));
const finalImageUrl = generateCombinedImageUrl(size, base64Images, false);
const resizeArtifactFix = `<img class="full-size" onload="this.remove()" src="${generateCombinedImageUrl(size, base64Images, true)}" />`;
return `<style>body, html {margin: 0; padding: 0; overflow: hidden;} .full-size {position: absolute; opacity: 1; top: calc(100% - 1px); left: calc(100% - 1px);} img {display: block; width:100%;}</style>${resizeArtifactFix}<img src="${finalImageUrl}" />`;
} catch (error) {
return `<p style="color: red;">${error?.message || String(error)}</p>`;
}
}
function generateCombinedImageUrl(size, base64Images, fullSize) {
const innerImages = base64Images.map((base64Image) => `<foreignObject x="0" y="0" width="100%" height="100%"><svg ${fullSize ? 'class="full-size"' : ''} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size.width} ${size.height}" width="${size.width}" height="${size.height}" style="image-rendering: pixelated; background: url(${base64Image}) no-repeat ${fullSize ? '' : 'center/contain'};"></svg></foreignObject>`);
return URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size.width} ${size.height}" width="${size.width}" height="${size.height}">${innerImages.join('')}</svg>`], {type: 'image/svg+xml'}));
}
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file);
});
}
Collection JS
const collectionJsonInscriptionId = '{collection-json-inscription-id-here}';
const rendererJsInscriptionId = '{renderer-js-inscription-id-here}';
const renderSize = {width: 150, height: 150};
async function createInscriptionHtml() {
const collectionMetadataPromise = fetch(
`/content/${collectionJsonInscriptionId}`,
).then((response) => response.json());
const inscriptionTraitsList = document.querySelector('script[t]').getAttribute('t').split(',');
const rendererScript = document.createElement('script');
rendererScript.setAttribute('async', '');
rendererScript.src = `/content/${rendererJsInscriptionId}`;
const renderPromise = new Promise((resolve, reject) => {
rendererScript.addEventListener('load', async () => {
try {
const collectionMetadata = await collectionMetadataPromise;
const traitInscriptionIds = inscriptionTraitsList.map(
(traitIndex, layerIndex) =>
collectionMetadata.layers[layerIndex].traits[traitIndex]?.inscriptionId,
);
resolve(await render(renderSize, ...traitInscriptionIds.filter(id => !!id)));
} catch (error) {
console.error(error);
reject(error);
}
});
});
document.head.appendChild(rendererScript);
return await renderPromise;
}
createInscriptionHtml().then((result) => (document.body.innerHTML = result));
Collection JSON
{
"collection": {
"name": "Large animals",
"description": "A collection of large animals.",
"creator": "Bob",
"collectionImageInscriptionId": "96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0"
},
"layers": [
{
"type": "Background",
"traits":[
{
"name": "Blue",
"inscriptionId": "96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0"
},
{
"name": "Black",
"inscriptionId": "96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0"
}
]
},
{
"type": "Body",
"traits":[
{
"name": "Alligator",
"inscriptionId": "96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0"
},
{
"name": "Elephant",
"inscriptionId": "96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0"
}
]
}
]
}
Asset Layer IMAGES
These inscriptions are simply inscribing each entire image file as its own inscription as bytes.
Inscription HTML
<script t="0,1,2" src="/content/{collection-js-inscription-id-here}"></script>
Provenance JSON
{
"bitcoinAddress":"bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej",
"collectionJsonInscriptionId":"9a42401296d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0",
"excludeInscriptions":[
"96d87d7e59d75ebc0e6144b09fdd96355fcdaa86fd098d64c46f19a424012bbei0",
"d75ebc0e61496d87d7e54b09fdd96355fcdaa86fd098d64c46f19a4240192bbei0"
]
}
Content Security Policy
In order to view BitGen ordinal images on your website, we recommend the following CSP header:
default-src 'unsafe-inline' 'self' blob: data:
In addition the the CSP listed, you should also include the domain (or sub-domain) where you are hosting your iframe code (see IFrames section below on why this is important).
WebKit browsers (desktop Safari and all iOS browsers) have stricter CSP parsing compared to other browsers, so make sure to test whatever CSP headers you come up with on those browsers. (The above recommended header value has already been tested on WebKit browsers).
IFrames
To display HTML ordinals on your website (such as those generated by the BitGen standard) you must load the inscription inside an iframe. For security purposes, make sure to host your iframe code on a different domain (or sub-domain) than your frontend to prevent inscription code from hijacking your website.
You might think that sandboxing the iframes on your frontend and whitelisting certain paths on your domain is enough, but in order to get inscriptions working on Safari/iPhone (webkit browsers) you have to allow scripts and cross origin on your iframes, which essentially removes any of the protections of sandboxing (a script could just remove sandboxing and access the parent window local storage or cookies). This is why we put our iframe code on a separate domain from our main website (it acts as sandboxing) without having to specify sandboxing in our iframes or having to whitelist certain paths.
Combine this with iframe sizing difficulties, and it can make displaying ordinals a daunting task. To help, we at Bioniq have created toniq-nft-frame
to smooth out the process. Check it out here.
Last updated