Skip to main content

Ordinals

Advanced
Bitcoin
Tutorial

Overview

On Bitcoin, the term 'satoshi' refers to the smallest denomination of BTC, equal to 100 millionth of 1 bitcoin. Ordinal theory, or simply 'Ordinals', refers to a numbering scheme used to track and transfer individual satoshis. The term 'Ordinal' comes from the fact that each satoshi is numbered based on the order in which it has been mined and the order in which it is transferred, based on transaction inputs and outputs.

Digital assets, such as tokens, accounts, or NFTs, can be attached to a satoshi, using its Ordinal number as a unique identifier for that asset. Attaching an asset to an Ordinal is done through a process known as inscribing, and is achieved by sending a transaction to the Bitcoin network that contains the satoshi to be inscribed and the arbitrary asset data. Once the transaction has been added to the network, the satoshi becomes an immutable digital artifact, often referred to as an 'inscribed Ordinal' or simply an 'inscription'. Inscriptions can be tracked, bought, sold, traded, transferred, or collected.

Ordinals are available through an open source GitHub project that consists of the necessary components to facilitate Ordinal tracking, transactions, and inscribing Ordinals with digital artifacts. The Ordinal BIP document contains additional technical details.

Ordinals on ICP

Canisters deployed on ICP can sign and submit transactions directly to the Bitcoin network through ICP's Bitcoin integration and threshold signatures. Specifically, for a canister to inscribe Ordinals, it must sign transactions using threshold Schnorr signatures and have a Bitcoin taproot address.

Inscribing an Ordinal

To inscribe an Ordinal through a canister on ICP, first you will need to call the schnorr_public_key API to obtain a Schnorr public key. This public key will be used to generate a Bitcoin taproot (P2TR) address for your canister. A taproot address is required to sign and submit Ordinal inscription transactions on Bitcoin. Learn more about Bitcoin taproot addresses.

Once you have generated a taproot address for your canister, you will need to write a canister method that creates an inscription transaction, then signs that transaction with your canister's Schnorr public key. Here's an example of an inscription transaction written in Rust:

#[ic_cdk::update]
pub async fn inscribe(content_type: String, body: String, recipient: Option<String>, fee_rate: Option<u64>) -> (String, String) {
    let network = NETWORK.with(|n| n.get());
    let content_type = Some(content_type.as_bytes().to_vec());
    let body = Some(body.as_bytes().to_vec());
    bitcoin_wallet::inscribe(network, content_type, body, recipient, fee_rate.unwrap_or(10)).await
}
   
View the full example.

Querying Ordinal information

To query Ordinal information about a satoshi, you write a canister method that makes a call to an API service such as Hiro or Bitgems. Here's an example in Rust calling the Hiro API:

#[ic_cdk::update]
async fn hiro_sat_info(args: SatInfoArgs) -> Result<SatInfo, OrdError> {

    call_service(Provider::Hiro, EndPoint::SatInfo, default_args(OrdFunction::SatInfo(args))).await.map(|response| {
        match response {
            Response::SatInfo(ordinal_info) => ordinal_info,
            _ => panic!("Unexpected response type"),
        }
    })
}

This example is from the Ordinals canister project that demonstrates how to use both Bitgems and Hiro to query Ordinal information.

You can also make a call specifically for the Ordinal inscription information and content:

#[ic_cdk::update]
async fn hiro_inscription_info(args: InscriptionInfoArgs) -> Result<HiroSatInscription, OrdError> {

    call_service(Provider::Hiro, EndPoint::InscriptionInfo, default_args(OrdFunction::InscriptionInfo(args))).await.map(|response| {
        match response {
            Response::InscriptionInfo(inscription) => inscription,
            _ => panic!("Unexpected response type"),
        }
    })
}

#[ic_cdk::update]
async fn hiro_inscription_content(args: InscriptionContentArgs) -> Result<Vec<u8>, OrdError> {

    call_service(Provider::Hiro, EndPoint::InscriptionContent, default_args(OrdFunction::InscriptionContent(args))).await.map(|response| {
        match response {
            Response::InscriptionContent(content) => content,
            _ => panic!("Unexpected response type"),
        }
    })
}

View the full Ordinals canister example on GitHub.

Resources