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)

  1. The Renderer JS

  2. The Collection JS

  3. The Collection JSON

  4. The Asset Layer IMAGES

  5. The Inscription HTML

  6. The Provenance JSON

  7. Content Security Policy

  8. 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:

recommended 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