The Digital Rosetta Stone by OCM

Embedded Image Figure 1: Early Ordinals artworks exploring Bitcoin as a medium for code-based art. Each of these works either uses recursion referencing an OnChainMonkey (OCM) inscription or was created in collaboration with OCM. Presented from top-left to bottom-right:
  1. OCM Genesis, Inscription 20219, February 2023
  2. OCM Dimensions, Inscription 203112, February 2023
  3. OCM Deconstructed, Inscription 464551, March 2023
  4. Infinites AI by FAR, Inscription 14277151, June 2023
  5. Squared by Rabbi, Inscription 14569090, July 2023
  6. Apoptosis by OTO, Inscription 17779695, July 2023
  7. Rising Echoes by Medusa, Inscription 20761338, July 2023
  8. L'Orphelinat by Lemonhaze, Inscription 29387557, September 2023
  9. Asprey Bugatti Egg, Inscription -208185, September 2023
  10. Nakamoto Archives by lifofifo, Inscription -211925, September 2023
  11. Boutique by Solemn, Inscription 35365440, October 2023
  12. OCM Certificate of Ownership, Inscription -276890, November 2023
  13. Remnants by Nullish, Inscription 53276458, January 2024
  14. BTC Substance by MDV, Inscription 53866990, January 2024
  15. Cosmogenesis by Steven Morse, Inscription 60356621, February 2024
  16. The Golden Ratio by Harto, Inscription -294769, November 2023
  17. Deus Ex Machina by CyberSea, Inscription 59317556, February 2024
  18. FloraForms by Harto, Inscription 60656573, February 2024
  19. Frontline by Alexis André, Inscription 72947925, July 2024
  20. Mitosis by Moodsoup, Inscription 64076929, March 2024
  21. Disobedients by Patrick Amadon, Inscription 70651412, May 2024
  22. FL1CK3R by Bryan Brinkman, Inscription 82737074, January 2025
  23. PoorTraits by Rax and MDV, Inscription 83182757, January 2025
  24. Hafftka OnChainMonkey 01 by Hafftka, Inscription 88340600, March 2025
  25. OCM Katoshi, Inscription 92583864, April 2025
* Negative inscription numbers are “cursed” inscriptions.

The Digital Rosetta Stone: A Monument to Art on Bitcoin

In a moment for digital preservation, a 4 megabyte inscription, maxing out the capacity of a Bitcoin block, has been created containing the complete source code of the Ordinals protocol. This work, the Digital Rosetta Stone, is more than a technical archive. It is a conceptual artwork. By encoding the very protocol that enabled inscriptions, this artifact serves as a fail-safe: a seed from which all Ordinals art can be reconstituted, even if the protocol itself is lost to time. Like the original Rosetta Stone enabled the decoding of ancient Egyptian, this inscription preserves the cryptographic language of a new digital art era.

This inscription doesn’t just store code, it crystallizes a cultural shift. Digital art is being immutably inscribed on the world’s most secure ledger: Bitcoin. Unlike other blockchains, where data permanence is subject to protocol changes, token migrations, or chain reorgs, Bitcoin offers an unparalleled foundation. When you inscribe on Bitcoin, you are embedding your work in the most resilient digital substrate humanity has created. Art on Bitcoin is not a passing trend; it is a new medium, one where code, image, and meaning merge into a permanent, decentralized canvas.

This Rosetta Stone inscription is both a tribute and a tool. It invites future civilizations, or survivors of some digital dark age, to rediscover the entire corpus of work on Bitcoin. Armed with just this inscription, they could reconstruct provenance, understand inscriptions, and relive the visual and conceptual breakthroughs that marked this unique epoch of creativity. Nothing external is needed. No servers, no APIs. Just Bitcoin.

Among the earliest works in this lineage are OCM Genesis and OCM Dimensions. Genesis, inscribed in a single transaction, pioneered fully on-chain generative art and demonstrated the potential of putting an entire 10,000 piece collection directly on Bitcoin. It was a feat of technical precision and artistic vision. Dimensions advanced this further, introducing recursive inscriptions, modular code libraries, and parent-child provenance. These two works helped define the grammar of on-chain expression on Bitcoin: generative systems encoded directly into the blockchain, art composed from reusable building blocks, and nested provenance that mirrors the evolution of culture itself.

These innovations are medium-defining. They transformed Bitcoin from passive store-of-value into a dynamic creative substrate. OnChainMonkey didn’t just use Bitcoin, they extended it. Their code became the foundation upon which many subsequent recursive and generative artworks were built.

Now, with this Digital Rosetta Stone, the entire ecosystem is future-proofed. No matter what happens to software libraries on GitHub or external indexers, the keys to understanding on-chain art are embedded forever on Bitcoin. It is both a time capsule and a declaration: Art belongs on Bitcoin. And with the right primitives, it will outlast us all.


Table of Contents

  1. The Digital Rosetta Stone: Decipher Keys
  2. Introduction to the Ordinals Protocol
  3. Ordinal Theory: Numbering Satoshis and Rarity
  4. Transfers: First-In-First-Out Sat Propagation
  5. Inscriptions: Digital Artifacts on Bitcoin
  6. Additional Features: Recursion, Provenance, and More
  7. Examples: OCM Genesis and OCM Dimensions
  8. Conclusion and Significance
  9. Appendix 1: The Ultimate Bitcoin Ordinals Glossary
  10. Appendix 2: What is Bitcoin?
  11. Appendix 3: Ordinals Communities
  12. Appendix 4: Core Ordinals Protocol Source Code

The Digital Rosetta Stone: Decipher Keys


######                                 
#     # # #####  ####   ####  # #    # 
#     # #   #   #    # #    # # ##   # 
######  #   #   #      #    # # # #  # 
#     # #   #   #      #    # # #  # # 
#     # #   #   #    # #    # # #   ## 
######  #   #    ####   ####  # #    # 


#######                                             
#     # #####  #####  # #    #   ##   #       ####  
#     # #    # #    # # ##   #  #  #  #      #      
#     # #    # #    # # # #  # #    # #       ####  
#     # #####  #    # # #  # # ###### #           # 
#     # #   #  #    # # #   ## #    # #      #    # 
####### #    # #####  # #    # #    # ######  ####  

                                                       
#####  #####   ####  #####  ####   ####   ####  #      
#    # #    # #    #   #   #    # #    # #    # #      
#    # #    # #    #   #   #    # #      #    # #      
#####  #####  #    #   #   #    # #      #    # #      
#      #   #  #    #   #   #    # #    # #    # #      
#      #    #  ####    #    ####   ####   ####  ###### 
                                                      
                  
#####  ######  ####   ####  #####  #####  ###### #####     #####  #   # 
#    # #      #    # #    # #    # #    # #      #    #    #    #  # #  
#    # #####  #      #    # #    # #    # #####  #    #    #####    #   
#####  #      #      #    # #####  #    # #      #    #    #    #   #   
#   #  #      #    # #    # #   #  #    # #      #    #    #    #   #   
#    # ######  ####   ####  #    # #####  ###### #####     #####    #   
                                                                       
 
#######         #####                         #     #                                   
#     # #    # #     # #    #   ##   # #    # ##   ##  ####  #    # #    # ###### #   # 
#     # ##   # #       #    #  #  #  # ##   # # # # # #    # ##   # #   #  #       # #  
#     # # #  # #       ###### #    # # # #  # #  #  # #    # # #  # ####   #####    #   
#     # #  # # #       #    # ###### # #  # # #     # #    # #  # # #  #   #        #   
#     # #   ## #     # #    # #    # # #   ## #     # #    # #   ## #   #  #        #   
####### #    #  #####  #    # #    # # #    # #     #  ####  #    # #    # ######   #    

This document is a digital "Rosetta Stone" for translating inscriptions on Bitcoin. The purpose is to future-proof Ordinals and make Bitcoin a self-referential ledger of its own cultural and technical heritage. This document is a fully self-contained HTML file designed to be inscribed directly on-chain as a “4 megger”, occupying a full Bitcoin block.

Included within is the source code of the Ordinals protocol, up to version 0.23.2. Anyone, even in the distant future and without any external resources, can fully reconstruct the Ordinals protocol and recover all inscriptions from the Bitcoin blockchain with this document, regardless of whether the original Ordinals software or documentation survives.

Alphabet

ABCDEFGHIJKLMNOPQRSTUVWXYZ

abcdefghijklmnopqrstuvwxyz

Numerals

0123456789

Punctuation

.,?!;:-'"()[]{}<>/@#&%$^*

Language Encoding Declaration

This document is written in plain English using the Latin alphabet. It is encoded using UTF-8 Unicode, a standard for encoding text characters into bytes. Each visible character above and below is represented by 1–4 bytes in UTF-8.

Pangrams (contain every letter of the alphabet)

The quick brown fox jumps over the lazy dog.

Pack my box with five dozen liquor jugs.

Basic Vocabulary (English Reference)

WordDefinition
earthOur planet
sunThe star at the center of our solar system
humanA person of our species
BitcoinA decentralized monetary system secured by a global network
monkeyAn intelligent mammal, often playful and curious

Sample Numbering and Timestamps

Year: 2025

Date: July 3

Unix Timestamp: 1751548800

Time Capsule for the Future

This HTML document is preserved permanently on the Bitcoin blockchain. It is meant to be a time capsule, a message across centuries. Its purpose is to remain understandable even if the means of decoding Bitcoin inscriptions are lost. The contents contain natural human language and symbols as used in the early 21st century. OnChainMonkey demonstrated the power of preserving meaning through minimal encoding and cultural significance.

Figure 2: SVG image of a single OnChainMonkey Genesis #9990.

Introduction to the Ordinals Protocol

Ordinals is a protocol that gives individual satoshis unique identities so they can be tracked, transferred, and imbued with meaning. In Bitcoin, 1 BTC is divisible into 100,000,000 satoshis (sats), and Ordinals assigns each satoshi a serial number (an ordinal number) in the exact order the sats are mined. This protocol does not require any changes to Bitcoin’s consensus rules or any new tokens – it works on mainnet Bitcoin as-is. By treating sats as distinct digital items, ordinal theory enables Bitcoin-native NFTs, dubbed digital artifacts, via a process called inscription. Inscriptions attach arbitrary data (like images or text) to individual sats, effectively turning them into unique NFTs that are stored entirely on-chain in the Bitcoin ledger.

Ordinals refers both to the theory and to the reference implementation (“ord” by Casey Rodarmor and contributors) which provides an indexer, wallet, and explorer for ordinal functionality. This document aims to comprehensively explain the Ordinals protocol (version 0.23.2) – from core concepts and rules to actual source code – within a single self-contained document. We will cover how satoshis are ordered and classified, how inscriptions are serialized and tracked, and we’ll include core protocol code from the ord repository alongside real-world examples (such as the innovative OnChainMonkey collections on Bitcoin). The goal is to provide a complete, human-readable reference of Ordinals in one place.

Why Ordinals are significant: By enabling NFTs and other assets directly on Bitcoin without sidechains or tokens, Ordinals unleash new uses for Bitcoin’s block space and have catalyzed innovation in on-chain storage, compression, and even scaling techniques. This has led to a surge of creativity and activity on Bitcoin, while also sparking debates on digital artifacts vs. “pure” currency use of Bitcoin. The protocol’s evolution (up to v0.23.2) has added features like inscription provenance, recursion, and “cursed” inscription rectification, which we will detail. Let’s begin by understanding how Ordinals serializes sats and imbues them with collectible value.

Ordinal Theory: Numbering Satoshis and Rarity

At the heart of Ordinal Theory is the satoshi numbering scheme. Every satoshi is assigned an ordinal number in the order they are mined in blocks. The first satoshi from the Genesis block (Block 0) is ordinal 0, the second satoshi ordinal 1, and so on. Essentially, the running index of all sats increases as new Bitcoins are minted in each block’s coinbase reward. Bitcoin’s fixed issuance schedule means there is a maximum of 2.1 quadrillion sats that will ever exist (the constant total supply = 2,099,999,997,690,000 sats). Ordinals assigns numbers from 0 up to SUPPLY – 1 (where SUPPLY = 2099999997690000 in the code) for each sat. For example, a satoshi with ordinal number 5000000000 would be the first satoshi of Block 1 (since Block 0 had 5 billion sats from the 50 BTC reward).

Each ordinal number can be represented in multiple notations for human-friendliness:

Ordinal Rarity. Beyond just numbering sats, Rodarmor introduced a fun rarity classification based on prominent Bitcoin epochs and events:

Degree notation encodes these levels in an intuitive way: A°B′C″D‴ corresponds to Cycle = A, Halving epoch index = B, Difficulty period index = C, Offset in block = D. In this notation, any value of D > 0 means the sat is not the first in its block (hence common). For example, 0°0′0″0‴ represents the Genesis sat (Cycle 0, first halving epoch, first difficulty period, first sat in block) – a mythic. 1°0′0″0‴ would be legendary (cycle 1, first sat of epoch and period and block). As another example, 0°1′1″0‴ denotes an uncommon sat (cycle 0, not first epoch/period but D=0 first in its block). These labels add numismatic flair to sats, making them collectible beyond their face value.

Ordinal transfers. How do we track a specific satoshi as transactions spend coins? Ordinal theory defines a simple rule: satoshis are transferred according to the first-in, first-out (FIFO) order of transaction inputs and outputs. In other words, when a UTXO is spent, its sats (which have specific ordinal numbers in sorted order) are assigned to the new outputs in ascending order. The first satoshi from the inputs goes to the first output, the second to the second output (or still the first output if it has capacity), and so on. This preserves the global ordering through transactions.

Ord’s implementation uses the rule that all sats in the inputs form a sorted list, and outputs are “slots” that receive sats in sequence. If a transaction’s inputs carry, say, 6 sats labeled a–f (where a is the lowest ordinal) and there are two outputs of 4 sats and 2 sats respectively, the first 4 sats (a,b,c,d) go to output0 and the next 2 (e,f) go to output1. The Ordinal FAQ provides a helpful illustration:

Inputs:  [a b] [c] [d e f]    (values 2, 1, 3 sats)

Outputs: [ ?  ?  ?  ? ] [ ?  ? ]    (values 4, 2 sats)

Assign sats to outputs in order:

Inputs:  [a b] [c] [d e f]    ->    Outputs: [ a  b  c  d ] [ e  f ]

In the above, satoshi a was the first in the inputs and went to the first output, and f was last and went to the last output. If a transaction uses some sats as fee (i.e., total input sats > total output sats), those “excess” sats are effectively assigned to the coinbase transaction of that block, as if the miner took them in an extra output at the end of the block. Implementation-wise, ord treats fees as burned from the spending transaction and reappearing as inputs to the next block’s coinbase (ordered by tx position). This way, even sats paid as fees continue to be tracked (they just move to the miner’s UTXO). Importantly, this transfer scheme requires no changes to Bitcoin – it’s an interpretative layer. However, it implies that to send a specific satoshi, one must control the exact ordering of inputs/outputs in transactions; wallets using ord need to be careful not to mix or lose desired sats.

Transfers: First-In-First-Out Sat Propagation

To reinforce the concept, here’s the First-In-First-Out transfer algorithm more formally:

  1. Combine all input sats of a transaction in the order of the transaction’s inputs (and the order of sats within each input UTXO). This yields a sorted list of sat ordinal numbers entering the tx.
  2. For each transaction output in index order, assign it as many leading sats from the list as its output value (number of sats) requires. The first output takes the first N sats from the list (where N is the output’s value in sats), the second output takes the next M sats, and so on.
  3. Fees: If the inputs list has more sats than the sum of all outputs (i.e., some sats are not assigned because they were spent as fee), those sats are considered to be assigned to the miner’s coinbase reward in the next block. Essentially, the block’s coinbase transaction is treated as if it had extra “virtual inputs” corresponding to each fee-paying transaction, containing those fee sats in the order the transactions appear in the block. Thus, fee sats end up with the mining coinbase output (usually they get mixed with newly minted coins, but ord can track them distinctively by ordinal numbers).

Because of this rule, ordinal-aware wallets must construct transactions carefully. If you want to send a particular sat (say an inscribed satoshi that carries an NFT), you often need to isolate it in its own input and ensure it lands in the first output of the new tx. Otherwise, it might get inadvertently sent to a change output or to fees. The ord software provides tooling to automatically handle “sat control” (e.g., ord wallet send will try to keep target sats separate) – but users are warned not to use standard Bitcoin Core wallet commands on these UTXOs, as Core has no concept of ordinals and might merge/spend sats without regard to their identities.

In summary, ordinal theory extends Bitcoin by treating sats as discrete collectibles. All 2.1 quadrillion sats are indexed and carry their provenance (which block and position they were mined in). A sat’s ordinal number never changes, and the transfer algorithm ensures that if you follow the chain of transactions, you can track exactly where a specific sat moved over time. Next, we delve into inscriptions – the feature that makes these sats truly behave like NFTs by attaching data to them.

Inscriptions: Digital Artifacts on Bitcoin

Inscriptions are the mechanism by which arbitrary data (images, text, video, software, etc.) can be written into the Bitcoin blockchain and bound to a specific satoshi. An inscribed satoshi carries its payload with it wherever it goes, enabling purely on-chain NFTs (often called digital artifacts to emphasize they are complete and immutable on-chain). Notably, inscriptions do not require any altcoins or separate protocols – they use Bitcoin Taproot transactions in a clever way to store data.

The Commit–Reveal Procedure

Creating an inscription involves a two-step transaction process on Bitcoin:

  1. Commit transaction: The user prepares a Taproot output that commits to the inscription content. In practice, this means funding a Taproot address whose script tree will contain the data (but the data is not exposed on-chain yet). The commit transaction usually sends some sats (including the one to be inscribed) to this Taproot output and is mined in a block. The output looks like a normal Taproot UTXO, but its internal script is set up to include the inscription data in the next step.
  2. Reveal transaction: Using the Taproot output from the commit as input, the user spends it in a new transaction that reveals the inscription content in the witness. When this input is spent, the Taproot script (which contains the inscription data in a special “envelope” format) is executed/not-executed in a way that makes the content appear in the witness field of the transaction. At this point, the full inscription (the content bytes and metadata like content type) become part of the blockchain record. The satoshi that was allocated in the commit output is now considered inscribed.

In the ord indexing logic, an inscription is attached to the first satoshi of the first input of the reveal transaction (unless a different offset is specified by an optional “pointer” field). From then on, that sat carries the data. The reveal transaction’s ID and the input index together form the unique Inscription ID (formatted as <txid>i<index>) which identifies that particular inscription. For example, an Inscription ID might look like: 521f8ecc...5c79da**i0** (meaning the first input of tx 521f8ecc...5c79da).

The commit–reveal scheme is necessary because of Taproot’s design: to store a large chunk of data, it must reside in a Taproot script path. You can’t just dump arbitrary megabytes in a single output script without first creating that output. Thus, an initial commit TX sets it up, and the reveal spends it to actually dump the data on-chain. These two steps are automated by tooling (ord wallet inscribe handles creating both transactions under the hood).

The Inscription Envelope Format

How exactly is the inscription content embedded in the Taproot input’s witness? Ordinals uses a specific envelope format within the unlocking script that marker “ord” and the content. This is done in a non-executing Taproot branch using an OP_FALSE OP_IF ... OP_ENDIF wrapper. Everything inside the OP_FALSE OP_IF … OP_ENDIF is never executed (because the condition is false), but it remains in the witness for anyone to read. This is a trick to include arbitrary data without affecting script execution (and without using OP_RETURN, thereby bypassing the 80-byte limit).

Inside this envelope, the inscription data is structured as a sequence of pushes:

OP_FALSE OP_IF

  OP_PUSH "ord"

  OP_PUSH 1

  OP_PUSH "<mime_type_string>"

  OP_PUSH 0

  OP_PUSH "<content_chunk_1>"

  [OP_PUSH "<content_chunk_2>"] ... [OP_PUSH "<content_chunk_n>"]

OP_ENDIF

For a concrete example, the inscription of the text “Hello, world!” would be serialized as follows (each line is one push or opcode):

OP_FALSE

OP_IF

  OP_PUSH "ord"                        ; Magic string to identify inscription

  OP_PUSH 1                            ; Signal that next push is content type

  OP_PUSH "text/plain;charset=utf-8"   ; MIME type for the content

  OP_PUSH 0                            ; Signal that subsequent pushes are content

  OP_PUSH "Hello, world!"              ; The actual content bytes (fits in one push here)

OP_ENDIF

This sequence would live in the Taproot input’s witness. Because it’s wrapped in the OP_FALSE OP_IF ... OP_ENDIF which is never executed, it doesn’t interfere with the spending logic or require any special conditions – it’s basically ignored by Bitcoin’s validation (aside from counting towards witness size) but is still there for indexing.

Why not use OP_RETURN? Unlike the traditional 80-byte OP_RETURN transactions used for BTC-based NFTs in the past, inscriptions can be much larger (up to the block weight limit) and are committed in Taproot where the data is in the input script rather than an output. This allows the content to be carried along with the sat (since the sat is an input of the reveal tx). Also, Taproot’s witness data receives a 75% discount in weight, making it cheaper to store data (by bytes) than using OP_RETURN outputs. As a result, storing megabytes of data in a single inscription became economically feasible, which is why in early 2023 we saw images, videos, and even games inscribed on Bitcoin.

The ordinals indexer scans each new transaction’s inputs for this pattern. When the magic bytes “ord” are found, followed by the proper structure, the bytes after the 0 tag are assembled to reconstruct the full file content. The MIME type is recorded, and the indexer then assigns the inscription to the appropriate sat (as determined by the input position). The content bytes themselves are stored as part of that transaction in the blockchain – meaning the NFT is completely on-chain. Anyone can retrieve the content by reading the blockchain (running an ord viewer or any parser that follows the envelope format).

One subtle point: multiple inscriptions in one transaction – It’s possible to inscribe more than one sat in the same reveal transaction (for example, if a reveal tx input has many sats and the script includes several OP_FALSE OP_IF ... OP_ENDIF envelopes). Ord’s convention is that it will find all envelopes in the inputs in order and assign inscription numbers in the order they appear. The indexing rules state that inscriptions are numbered first by the block they are revealed in (block ordering), then by the position of the reveal transaction in the block, then by the order of the envelopes in that transaction’s inputs. So if one transaction reveals two inscriptions in the same input, one will get the next number and the other the following. The inscription ID format <txid>i<index> uses an incrementing index for each envelope in the tx (so ...i0, ...i1, etc). This makes each inscription unique even if they share a transaction.

Inscription Identification and Numbering

Every inscription, once revealed, is assigned a sequential inscription number by the ord indexer (this is separate from the sat’s ordinal number). The very first inscription on Bitcoin (the genesis inscription) was number 0, and counting increases with each new inscription. These numbers reflect the chronological order of when inscriptions were mined onto the blockchain. (Notably, in early 2023 this caused some intense “mint races” for low inscription numbers, as they became coveted.)

Cursed Inscriptions: During the rapid development of ord, certain edge-case inscriptions were initially not recognized or given special treatment. For example, before the envelope format was fully standardized, some content that looked like inscriptions but didn’t follow the expected pattern were considered “invalid” by older versions of ord. When the rules later evolved to support them, the developers faced a dilemma: assigning them new positive numbers would shift a lot of existing numbering (which the community did not want to break). The solution introduced was to assign these oddball cases negative inscription numbers, calling them “cursed” inscriptions. Cursed inscription IDs started at -1 and counted downwards for any inscription that an older indexer would not have recognized (so as to not disturb the official count). Examples of “cursed” events included certain bug exploits or out-of-order reveals that ord didn’t originally handle.

In a network-wide event dubbed the Jubilee (which occurred at Bitcoin block height #824,544), the ord protocol “vindicated” some types of cursed inscriptions by upgrading them to normal status. Specifically, after that block, certain formerly disallowed inscription methods became allowed, so those negative-number inscriptions were re-assigned positive numbers beyond the main sequence, effectively integrating them into the fold. The details are complex, but the takeaway is that cursed inscriptions on and after block 824,544 are remapped into the ordinary inscription sequence. This was a one-time adjustment to fold in what could be thought of as “missed” inscriptions due to older client limitations. Modern ord (v0.23.2) has fully defined rules for what constitutes a valid inscription envelope and should not produce new cursed inscriptions under normal circumstances (the goal is to eventually reach Ordinal Theory v1.0 with stable consensus on inscription parsing). Takeaway: cursed inscriptions (such as OnChainMonkey Genesis and Dimensions) are rare artifacts of the early Ordinals protocol.

Viewing and security: Once inscribed, the content can be viewed via ord-enabled block explorers or by running the ord server which serves the files under an HTTP interface. Inscriptions are rendered in a sandboxed manner for safety – for example, HTML or SVG inscriptions are displayed in an isolated iframe with no external network access. This ensures that inscribed content remains immutable and self-contained (it cannot just load off-chain resources), reinforcing the notion that the NFT is entirely on Bitcoin. The Ordinals protocol thus treats the Bitcoin blockchain as the authoritative store for the artifact’s data, unlike many Ethereum NFTs which often just store a pointer or hash to off-chain data.

At this point, we’ve covered how individual sats are numbered and tracked, and how inscriptions package data into Bitcoin transactions to create NFTs. Next, we’ll look at some features added to Ordinals as the ecosystem grew – such as parent/child provenance, recursion, and other metaprotocol enhancements.

Additional Features: Recursion, Provenance, and More

The Ordinals protocol (and ord implementation) is under active development (especially during the early period of 2023 to 2024), and up to version 0.23.2 a number of new capabilities have been introduced:

With recursion and these other features, creators have pushed Bitcoin to do things like fully on-chain 3D games, music players, and complex generative art – all within the constraints of Bitcoin transactions. It’s an exciting evolution, and to cement the concepts, let’s look at a notable example: the OnChainMonkey collections and how they utilized Ordinals.

Examples: OCM Genesis and OCM Dimensions

One of the pioneering efforts in Ordinals has been OnChainMonkey (OCM), originally an on-chain Ethereum NFT collection that moved to Bitcoin and embraced the new possibilities of inscriptions. OCM provides an illuminating case study for Ordinals:

These examples underline several points: (1) the Ordinals protocol is flexible and being pushed in new directions by creators; (2) techniques like recursion and careful use of satoshis (like choosing rare sats for art) add rich dimensions (pun intended) to the NFT story; and (3) the community around Ordinals is innovating (OCM’s work was at the cutting edge and advanced the protocol and shaped generative art on Bitcoin).

Conclusion and Significance

Ordinals (the protocol and the movement around it) has unlocked a new dimension for Bitcoin. It repurposes the network – traditionally used only for currency – into a decentralized storage and ownership ledger for digital art, collectibles, tokens, and more. The fact that one can inscribe arbitrary data and have it be part of Bitcoin forever (as durable as the blockchain itself) is profound. Inscriptions are immutable and permissionless – once created, they cannot be changed or removed, and no smart contract or third-party platform can rugpull the content. This is a sharp contrast to many NFT systems on other chains where the content might sit on IPFS or be subject to smart contract logic changes.

The Ordinals protocol up to v0.23.2 has matured to handle a surprising array of use cases: from simple images to generative art with recursion, from single NFTs to entire 10k collections in one go. It respects Bitcoin’s ethos by not requiring any forks or new tokens – everything is just data in Taproot. Yet it introduces a cultural shift: Bitcoin blocks are now being used to carry art and culture, not just financial transactions. This has driven block sizes up (lots of full 4 MB blocks in 2023 due to popular image inscriptions) and spurred debate on miner fees and resource use. Some see it as spam, others as a renaissance for Bitcoin. Either way, it has increased fees (miners earned significant revenue from inscription-heavy blocks) and revived interest in building on Bitcoin.

From a technical perspective, Ordinals showcases the flexibility of Bitcoin’s script and witness – who would have thought you could fit image files and even video games (like Doom) inside a Bitcoin transaction? It’s a testament to Bitcoin’s robustness that without any new opcodes, we got this new protocol.

In summary, this is a comprehensive one-stop resource: we covered the ordinal numbering system and sat rarity, the rules of sat transfer (FIFO), how inscriptions are constructed and identified, features like recursion, parent-child provenance, and delegation, real examples like OnChainMonkey that pushed the boundaries, and we’ve included the actual source code to see under the hood of ord (verbatim from the repository). All information is self-contained, and even the images included above are stored as data within this document to mirror the spirit of inscriptions (everything needed is on-chain/on-file, with no external dependencies).

Whether you are a developer, an artist, or just a curious Bitcoiner, Ordinals open up a new world where sats are not just sats – they can be valuable assets, historic art, OnChainMonkeys, or other rare collectibles, each provably on Bitcoin. This enriches the Bitcoin ecosystem and challenges us to think about on-chain space in novel ways (as a scarce canvas for human expression, not merely a ledger for payments). It’s still early days for Ordinals, but it has already made history: from Inscription 0 on January 20, 2023, to nearly 100 million inscriptions today, including art, text, tokens, and more.

In a way, Ordinals has brought a bit of fun and culture to Bitcoin, and perhaps a bit of controversy – but undeniably a lot of innovation. With this reference, you now possess the knowledge to understand and even recreate the core of the Ordinals protocol up to version 0.23.2. Feel free to explore further: the open-source code, the ordinals.com explorer, and the many communities documenting rare sats and creative inscriptions. Who knows – maybe you will inscribe the next iconic artifact on Bitcoin, adding to its growing digital history.

Appendix 1: The Ultimate Bitcoin Ordinals Glossary

👉1. Sat - short for satoshi, the smallest unit of a bitcoin. Each sat is uniquely identified and numbered. There are 100 million sats in 1 bitcoin. Sats are just bitcoin, so people own sats just as they own bitcoin.

👉2. Inscription - content recorded immutably on the Bitcoin blockchain. Folks might think of this as an “NFT”, but there’s much more to it, so read on!

👉3. Ordinal theory - assigning unique serial numbers to sats, starting at 0 (for the first sat of the first bitcoin mined). Allows sats to be tracked on the Bitcoin blockchain across transactions.

👉4. Ordinals (protocol) - a protocol that uses Ordinal theory, and enables inscriptions that are paired with a sat. The combo of the Inscription and the Sat is the closest to a “Bitcoin NFT” but there’s more!

👉5. Inscription Number - each inscription is numbered uniquely, starting at 0. There is also a set of negative inscription numbers (see "cursed inscriptions”) that are numbered from -1 to -472,043. Future inscription numbers will all be positive.

👉6. Reinscription - each inscription is paired with a single sat, but each sat can have multiple inscriptions! Reinscription is the act of inscribing a sat two or more times. You can think of a sat as having multiple canvases and reinscription allows you to use additional canvases for that sat. When you transfer an inscription, you transfer the sat, so when you transfer a reinscription, you transfer all reinscriptions on that sat. You can think of these inscriptions on the same sat as a group of “soul-bound” NFTs that are forever bound together.

👉7. Parent-Child Provenance/Collections - this is the standard for creating a collection of inscriptions in Ordinals. Collections are one of the most important concepts in Ordinals! Parent-Child allows inscriptions to form a family tree that is immutably recorded on Bitcoin. Parent inscriptions can have Child inscriptions, and all the children form a collection on Bitcoin with clear provenance. If you’re familiar with Ethereum NFTs, this is kind of like the ERC-721 standard for NFT collections. The Bitcoin blockchain does not have smart contracts, so Parent-Child is the standard for an Ordinal collection. Parent-Child is more powerful than it first seems. Children can be Parents too, and you can have multi-generational family trees of grandchildren, and great-grandchildren on Bitcoin. Multiple parents can together create a single Multi-Parent-Child collection! Thinking about Parent-Child Provenance can make your mind explode with possibilities for the future!

👉8. Recursive Inscriptions - inscriptions can refer to and use other inscriptions in their inscription. This is one of the most powerful features of Ordinals. Recursive Inscriptions enable composable and modular building with Ordinals. Every inscription is a building block that can be used in the future. For example, many generative art works use of libraries such as p5.js and three.js, and recursive inscriptions allow for the use of these libraries. Most generative art on Bitcoin uses recursive inscriptions.

👉9. Cursed inscriptions - a set of inscriptions from the early days of Ordinals that were not recognized by the developing Ordinals protocol. Cursed inscriptions are valid inscriptions that implemented features that were later added to Ordinals (such as Reinscription and Parent-Child Provenance). Cursed inscriptions were officially recognized at the Jubilee (a major update to Ordinals where the definition of an inscription was finalized in January 2024) and assigned negative inscription numbers from -1 to -472,043. There are unlikely to be any more cursed inscriptions in the future, so cursed inscriptions are a rare group of inscriptions (that can be identified with a negative inscription number).

👉10. Satributes - definitions of special sats. Casey defined certain special sats in Rodarmor Rarity. For example the uncommon sat is the first sat in a bitcoin block. An example would be sat 45000000000, the first sat in block 9. Other special sats in Rodarmor rarity are: rare (the first sat of each difficulty adjustment period), epic (the first sat of each halving epoch), legendary (the first sat of each cycle), and mythic (the first sat of the genesis block). Other satributes include: Block 9 (sats from block 9, numbered 45000000000-49999999999). 450x (the first bitcoin in block 9, numbered 45000000000-45099999999). Palindrome (sat numbers that are palindromes). Others such as Vintage, Nakamoto, First Transaction, Pizza.

Appendix 2: What is Bitcoin?

Bitcoin is an open-source, peer-to-peer digital cash system launched on 3 January 2009 by the pseudonymous Satoshi Nakamoto. It uses cryptographic signatures, proof-of-work mining, and a public ledger (the blockchain) to let anyone, anywhere, send value without banks or governments. Born amid 2008 crisis, it offers an open alternative to inflationary state money.

Every ten minutes miners bundle pending transfers into a block, solve an energy-costly mathematical challenge, and broadcast the result. Nodes accept the longest valid chain, making history immutable without centralized oversight. New coins enter circulation through this process in a schedule that halves roughly every four years, ensuring the total supply can never exceed 21 million bitcoins (each broken into 100 000 000 satoshis).

Because the rules—fixed supply, deterministic issuance, transparent auditability, and censorship-resistant settlement—are enforced by thousands of independently run nodes, Bitcoin functions as a neutral, permissionless global monetary network. Users can self-custody funds with a mnemonic phrase, verify balances with inexpensive hardware, and transact across borders for negligible fees relative to legacy rails.

Economically, bitcoin acts as a scarce bearer asset—divisible, unforgeable, and hard to seize; socially, it offers an opt-in, rules-based alternative to inflationary fiat; technically, its script enables multi-sig vaults and higher-layer tools like the Lightning Network without altering base consensus.

In sum, Bitcoin is best understood as the first successful synthesis of sound money principles with modern cryptography and distributed systems engineering—a decentralized ledger, currency, and payment rail that anyone can verify and nobody can control.

Appendix 3: Ordinals Communities

Embedded Image Figure 4: Ordinals Communities:
  1. Ordinals, Casey Rodarmor's “Genesis” skull, Inscription 0, December 2022
  2. Sub 10k, Bitcoin Shrooms, Inscription 19, January 2023
  3. Taproot Wizards, Inscription 652, February 2023
  4. OnChainMonkey, Inscription 20219, February 2023
  5. NodeMonkes, Inscription 83522, February 2023
  6. Ordinal Maxi Biz (OMB), Inscription 89945, February 2023
  7. BRC-20, Inscription 348020, March 2023
  8. Bitcoin Frogs, Inscription 381224, March 2023
  9. Rare Sats, First block 9 inscription, Inscription 9235048, May 2023
  10. Bitmap, Inscription 10425971, June 2023
  11. The Wizards of Ord, Inscription 11301998, June 2023
  12. Bitcoin Puppets, Inscription 53105612, January 2024
  13. Cursed Inscriptions, Inscription -471334, January 2024
  14. Pizza Ninjas, Inscription 56122947, January 2024
  15. Runes, Runestone, Inscription 63140674, March 2024
  16. CENTS by Rutherford Chang, Inscription 65890305, March 2024

Appendix 4: Core Ordinals Protocol Source Code


ord/docs/src/SUMMARY.md


Summary
=======

[Introduction](introduction.md)
- [Overview](overview.md)
- [Digital Artifacts](digital-artifacts.md)
- [Inscriptions](inscriptions.md)
  - [Burning](inscriptions/burning.md)
  - [Delegate](inscriptions/delegate.md)
  - [Metadata](inscriptions/metadata.md)
  - [Pointer](inscriptions/pointer.md)
  - [Properties](inscriptions/properties.md)
  - [Provenance](inscriptions/provenance.md)
  - [Recursion](inscriptions/recursion.md)
  - [Rendering](inscriptions/rendering.md)
  - [URIs](inscriptions/uris.md)
  - [Examples](inscriptions/examples.md)
- [Runes](runes.md)
  - [Specification](runes/specification.md)
- [Security](security.md)
- [FAQ](faq.md)
- [Contributing](contributing.md)
- [Donate](donate.md)
- [Guides](guides.md)
  - [API](guides/api.md)
  - [Batch Inscribing](guides/batch-inscribing.md)
  - [Collecting](guides/collecting.md)
    - [Sparrow Wallet](guides/collecting/sparrow-wallet.md)
  - [Explorer](guides/explorer.md)
  - [Moderation](guides/moderation.md)
  - [Reindexing](guides/reindexing.md)
  - [Sat Hunting](guides/sat-hunting.md)
  - [Satscards](guides/satscards.md)
  - [Settings](guides/settings.md)
  - [Splitting](guides/splitting.md)
  - [Teleburning](guides/teleburning.md)
  - [Testing](guides/testing.md)
  - [Wallet](guides/wallet.md)
- [Bounties](bounties.md)
  - [Bounty 0: 100,000 sats Claimed!](bounty/0.md)
  - [Bounty 1: 200,000 sats Claimed!](bounty/1.md)
  - [Bounty 2: 300,000 sats Claimed!](bounty/2.md)
  - [Bounty 3: 400,000 sats](bounty/3.md)

ord/docs/src/bounties.md


Ordinal Bounty Hunting Hints
============================

- The `ord` wallet can send and receive specific satoshis. Additionally,
  ordinal theory is extremely simple. A clever hacker should be able to write
  code from scratch to manipulate satoshis using ordinal theory in no time.

- For more information about ordinal theory, check out the [FAQ](./faq.md) for
  an overview, the
  [BIP](https://github.com/ordinals/ord/blob/master/bip.mediawiki) for the
  technical details, and the [ord repo](https://github.com/ordinals/ord) for the
  `ord` wallet and block explorer.

- Satoshi was the original developer of ordinal theory. However, he knew that
  others would consider it heretical and dangerous, so he hid his knowledge,
  and it was lost to the sands of time. This potent theory is only now being
  rediscovered. You can help by researching rare satoshis.

Good luck and godspeed!

ord/docs/src/bounty/0.md


Ordinal Bounty 0
================

Criteria
--------

Send a sat whose ordinal number ends with a zero to the submission address:

✅: [1857578125803250](https://ordinals.com/ordinal/1857578125803250)

❌: [1857578125803251](https://ordinals.com/ordinal/1857578125803251)

The sat must be the first sat of the output you send.

Reward
------

100,000 sats

Submission Address
------------------

[`1PE7u4wbDP2RqfKN6geD1bG57v9Gj9FXm3`](https://mempool.space/address/1PE7u4wbDP2RqfKN6geD1bG57v9Gj9FXm3)

Status
------

Claimed by [@count_null](https://twitter.com/rodarmor/status/1560793241473400833)!

ord/docs/src/bounty/1.md


Ordinal Bounty 1
================

Criteria
--------

The transaction that submits a UTXO containing the oldest sat, i.e., that with
the lowest number, amongst all submitted UTXOs will be judged the winner.

The bounty is open for submissions until block 753984—the first block of
difficulty adjustment period 374. Submissions included in block 753984 or later
will not be considered.

Reward
------

200,000 sats

Submission Address
------------------

[`145Z7PFHyVrwiMWwEcUmDgFbmUbQSU9aap`](https://mempool.space/address/145Z7PFHyVrwiMWwEcUmDgFbmUbQSU9aap)

Status
------

Claimed by [@ordinalsindex](https://twitter.com/rodarmor/status/1569883266508853251)!

ord/docs/src/bounty/2.md


Ordinal Bounty 2
================

Criteria
--------

Send an <span class=uncommon>uncommon</span> sat to the submission address:

✅: [347100000000000](https://ordinals.com/sat/347100000000000)

❌: [6685000001337](https://ordinals.com/sat/6685000001337)

Confirm that the submission address has not received transactions before submitting your entry. Only the first successful submission will be rewarded.

Reward
------

300,000 sats

Submission Address
------------------

[`1Hyr94uypwWq5CQffaXHvwUMEyBPp3TUZH`](https://mempool.space/address/1Hyr94uypwWq5CQffaXHvwUMEyBPp3TUZH)

Status
------

Claimed by [@utxoset](https://twitter.com/rodarmor/status/1582424455615172608)!

ord/docs/src/bounty/3.md


Ordinal Bounty 3
================

Criteria
--------

Ordinal bounty 3 has two parts, both of which are based on *ordinal names*.
Ordinal names are a modified base-26 encoding of ordinal numbers. To avoid
locking short names inside the unspendable genesis block coinbase reward,
ordinal names get *shorter* as the ordinal number gets *longer*. The name of
sat 0, the first sat to be mined is `nvtdijuwxlp` and the name of sat
2,099,999,997,689,999, the last sat to be mined, is `a`.

The bounty is open for submissions until block 840000—the first block after the
fourth halving. Submissions included in block 840000 or later will not be
considered.

Both parts use [frequency.tsv](frequency.tsv), a list of words and the number
of times they occur in the [Google Books Ngram
dataset](http://storage.googleapis.com/books/ngrams/books/datasetsv2.html).
filtered to only include the names of sats which will have been mined by the
end of the submission period, that appear at least 5000 times in the corpus.

`frequency.tsv` is a file of tab-separated values. The first column is the
word, and the second is the number of times it appears in the corpus. The
entries are sorted from least-frequently occurring to most-frequently
occurring.

`frequency.tsv` was compiled using [this
program](https://github.com/casey/onegrams).

To search an `ord` wallet for sats with a name in `frequency.tsv`, use the
following [`ord`](https://github.com/ordinals/ord) command:

```
ord wallet sats --tsv frequency.tsv
```

This command requires the sat index, so `--index-sats` must be passed to ord
when first creating the index.

### Part 0

*Rare sats pair best with rare words.*

The transaction that submits the UTXO containing the sat whose name appears
with the lowest number of occurrences in `frequency.tsv` shall be the winner of
part 0.

### Part 1

*Popularity is the fount of value.*

The transaction that submits the UTXO containing the sat whose name appears
with the highest number of occurrences in `frequency.tsv` shall be the winner
of part 1.

### Tie Breaking

In the case of a tie, where two submissions occur with the same frequency, the
earlier submission shall be the winner.

Reward
------

- Part 0: 200,000 sats
- Part 1: 200,000 sats
- Total: 400,000 sats

Submission Address
------------------

[`17m5rvMpi78zG8RUpCRd6NWWMJtWmu65kg`](https://mempool.space/address/17m5rvMpi78zG8RUpCRd6NWWMJtWmu65kg)

Status
------

Unclaimed!

ord/docs/src/contributing.md


Contributing to `ord`
=====================

Suggested Steps
---------------

1. Find an issue you want to work on.
2. Figure out what would be a good first step towards resolving the issue. This
   could be in the form of code, research, a proposal, or suggesting that it be
   closed, if it's out of date or not a good idea in the first place.
3. Comment on the issue with an outline of your suggested first step, and
   asking for feedback. Of course, you can dive in and start writing code or
   tests immediately, but this avoids potentially wasted effort, if the issue
   is out of date, not clearly specified, blocked on something else, or
   otherwise not ready to implement.
4. If the issue requires a code change or bugfix, open a draft PR with tests,
   and ask for feedback. This makes sure that everyone is on the same page
   about what needs to be done, or what the first step in solving the issue
   should be. Also, since tests are required, writing the tests first makes it
   easy to confirm that the change can be tested easily.
5. Mash the keyboard randomly until the tests pass, and refactor until the code
   is ready to submit.
6. Mark the PR as ready to review.
7. Revise the PR as needed.
8. And finally, mergies!

Start small
-----------

Small changes will allow you to make an impact
quickly, and if you take the wrong tack, you won't have wasted much time.

Ideas for small issues:
- Add a new test or test case that increases test coverage
- Add or improve documentation
- Find an issue that needs more research, and do that research and summarize it
  in a comment
- Find an out-of-date issue and comment that it can be closed
- Find an issue that shouldn't be done, and provide constructive feedback
  detailing why you think that is the case

Merge early and often
---------------------

Break up large tasks into multiple smaller steps that individually make
progress. If there's a bug, you can open a PR that adds a failing ignored test.
This can be merged, and the next step can be to fix the bug and unignore the
test. Do research or testing, and report on your results. Break a feature into
small sub-features, and implement them one at a time.

Figuring out how to break down a larger PR into smaller PRs where each can be
merged is an art form well-worth practicing. The hard part is that each PR must
itself be an improvement.

I strive to follow this advice myself, and am always better off when I do.

Small changes are fast to write, review, and merge, which is much more fun than
laboring over a single giant PR that takes forever to write, review, and merge.
Small changes don't take much time, so if you need to stop working on a small
change, you won't have wasted much time as compared to a larger change that
represents many hours of work. Getting a PR in quickly improves the project a
little bit immediately, instead of having to wait a long time for larger
improvement. Small changes are less likely to accumulate merge conflict. As the
Athenians said: *The fast commit what they will, the slow merge what they
must.*

Get help
--------

If you're stuck for more than 15 minutes, ask for help, like a Rust Discord,
Stack Exchange, or in a project issue or discussion.

Practice hypothesis-driven debugging
------------------------------------

Formulate a hypothesis as to what is causing the problem. Figure out how to
test that hypothesis. Perform that tests. If it works, great, you fixed the
issue or now you know how to fix the issue. If not, repeat with a new
hypothesis.

Pay attention to error messages
-------------------------------

Read all error messages and don't tolerate warnings.

ord/docs/src/digital-artifacts.md


Digital Artifacts
=================

Imagine a physical artifact. A rare coin, say, held safe for untold years in
the dark, secret clutch of a Viking hoard, now dug from the earth by your
grasping hands. It…

…has an owner. You. As long as you keep it safe, nobody can take it from you.

…is complete. It has no missing parts.

…can only be changed by you. If you were a trader, and you made your way to
18th century China, none but you could stamp it with your chop-mark.

…can only be disposed of by you. The sale, trade, or gift is yours to make,
to whomever you wish.

What are digital artifacts? Simply put, they are the digital equivalent of
physical artifacts.

For a digital thing to be a digital artifact, it must be like that coin of
yours:

- Digital artifacts can have owners. A number is not a digital artifact,
  because nobody can own it.

- Digital artifacts are complete. An NFT that points to off-chain content
  on IPFS or Arweave is incomplete, and thus not a digital artifact.

- Digital artifacts are permissionless. An NFT which cannot be sold without
  paying a royalty is not permissionless, and thus not a digital artifact.

- Digital artifacts are uncensorable. Perhaps you can change a database entry
  on a centralized ledger today, but maybe not tomorrow, and thus one cannot be
  a digital artifact.

- Digital artifacts are immutable. An NFT with an upgrade key is not a digital
  artifact.

The definition of a digital artifact is intended to reflect what NFTs *should*
be, sometimes are, and what inscriptions *always* are, by their very nature.


Donate
======

Ordinals is open-source and community funded. The current lead maintainer of
`ord` is [raphjaph](https://github.com/raphjaph/). Raph's work on `ord` is
entirely funded by donations. If you can, please consider donating!

The donation address for Bitcoin is
[bc1q8kt9pyd6r27k2840l8g5d7zshz3cg9v6rfda0m248lva3ve5072q3sxelt](https://mempool.space/address/bc1q8kt9pyd6r27k2840l8g5d7zshz3cg9v6rfda0m248lva3ve5072q3sxelt). The donation address for inscriptions is [bc1qn3map8m9hmk5jyqdkkwlwvt335g94zvxwd9aql7q3vdkdw9r5eyqvlvec0](https://mempool.space/address/bc1qn3map8m9hmk5jyqdkkwlwvt335g94zvxwd9aql7q3vdkdw9r5eyqvlvec0).

Both addresses are in a 2 of 4 multisig wallet with keys held by
[raphjaph](https://twitter.com/raphjaph),
[erin](https://twitter.com/realizingerin),
[rodarmor](https://twitter.com/rodarmor), and
[ordinally](https://twitter.com/veryordinally).

Donations received will go towards funding maintenance and development of `ord`,
as well as hosting costs for [ordinals.com](https://ordinals.com).

Thank you for donating!

ord/docs/src/faq.md


Ordinal Theory FAQ
==================

What is ordinal theory?
-----------------------

Ordinal theory is a protocol for assigning serial numbers to satoshis, the
smallest subdivision of a bitcoin, and tracking those satoshis as they are
spent by transactions.

These serial numbers are large numbers, like this 804766073970493. Every
satoshi, which is ¹⁄₁₀₀₀₀₀₀₀₀ of a bitcoin, has an ordinal number.

Does ordinal theory require a side chain, a separate token, or changes to Bitcoin?
----------------------------------------------------------------------------------

Nope! Ordinal theory works right now, without a side chain, and the only token
needed is bitcoin itself.

What is ordinal theory good for?
--------------------------------

Collecting, trading, and scheming. Ordinal theory assigns identities to
individual satoshis, allowing them to be individually tracked and traded, as
curios and for numismatic value.

Ordinal theory also enables inscriptions, a protocol for attaching arbitrary
content to individual satoshis, turning them into bitcoin-native digital
artifacts.

How does ordinal theory work?
-----------------------------

Ordinal numbers are assigned to satoshis in the order in which they are mined.
The first satoshi in the first block has ordinal number 0, the second has
ordinal number 1, and the last satoshi of the first block has ordinal number
4,999,999,999.

Satoshis live in outputs, but transactions destroy outputs and create new ones,
so ordinal theory uses an algorithm to determine how satoshis hop from the
inputs of a transaction to its outputs.

Fortunately, that algorithm is very simple.

Satoshis transfer in first-in-first-out order. Think of the inputs to a
transaction as being a list of satoshis, and the outputs as a list of slots,
waiting to receive a satoshi. To assign input satoshis to slots, go through
each satoshi in the inputs in order, and assign each to the first available
slot in the outputs.

Let's imagine a transaction with three inputs and two outputs. The inputs are
on the left of the arrow and the outputs are on the right, all labeled with
their values:

```
[2] [1] [3] → [4] [2]
```

Now let's label the same transaction with the ordinal numbers of the satoshis
that each input contains, and question marks for each output slot. Ordinal
numbers are large, so let's use letters to represent them:

```
[a b] [c] [d e f] → [? ? ? ?] [? ?]
```

To figure out which satoshi goes to which output, go through the input satoshis
in order and assign each to a question mark:

```
[a b] [c] [d e f] → [a b c d] [e f]
```

What about fees, you might ask? Good question! Let's imagine the same
transaction, this time with a two satoshi fee. Transactions with fees send more
satoshis in the inputs than are received by the outputs, so to make our
transaction into one that pays fees, we'll remove the second output:

```
[2] [1] [3] → [4]
```

The satoshis <var>e</var> and <var>f</var> now have nowhere to go in the
outputs:

```
[a b] [c] [d e f] → [a b c d]
```

So they go to the miner who mined the block as fees. [The
BIP](https://github.com/ordinals/ord/blob/master/bip.mediawiki) has the details,
but in short, fees paid by transactions are treated as extra inputs to the
coinbase transaction, and are ordered how their corresponding transactions are
ordered in the block. The coinbase transaction of the block might look like
this:

```
[SUBSIDY] [e f] → [SUBSIDY e f]
```

Where can I find the nitty-gritty details?
------------------------------------------

[The BIP!](https://github.com/ordinals/ord/blob/master/bip.mediawiki)

Why are sat inscriptions called "digital artifacts" instead of "NFTs"?
----------------------------------------------------------------------

An inscription is an NFT, but the term "digital artifact" is used instead,
because it's simple, suggestive, and familiar.

The phrase "digital artifact" is highly suggestive, even to someone who has
never heard the term before. In comparison, NFT is an acronym, and doesn't
provide any indication of what it means if you haven't heard the term before.

Additionally, "NFT" feels like financial terminology, and both the word
"fungible" and sense of the word "token" as used in "NFT" is uncommon outside
of financial contexts.

How do sat inscriptions compare to…
-----------------------------------

### Ethereum NFTs?

*Inscriptions are always immutable.*

There is simply no way to for the creator of an inscription, or the owner of an
inscription, to modify it after it has been created.

Ethereum NFTs *can* be immutable, but many are not, and can be changed or
deleted by the NFT contract owner.

In order to make sure that a particular Ethereum NFT is immutable, the contract
code must be audited, which requires detailed knowledge of the EVM and Solidity
semantics.

It is very hard for a non-technical user to determine whether or not a given
Ethereum NFT is mutable or immutable, and Ethereum NFT platforms make no effort
to distinguish whether an NFT is mutable or immutable, and whether the contract
source code is available and has been audited.

*Inscription content is always on-chain.*

There is no way for an inscription to refer to off-chain content. This makes
inscriptions more durable, because content cannot be lost, and scarcer, because
inscription creators must pay fees proportional to the size of the content.

Some Ethereum NFT content is on-chain, but much is off-chain, and is stored on
platforms like IPFS or Arweave, or on traditional, fully centralized web
servers. Content on IPFS is not guaranteed to continue to be available, and
some NFT content stored on IPFS has already been lost. Platforms like Arweave
rely on weak economic assumptions, and will likely fail catastrophically when
these economic assumptions are no longer met. Centralized web servers may
disappear at any time.

It is very hard for a non-technical user to determine where the content of a
given Ethereum NFT is stored.

*Inscriptions are much simpler.*

Ethereum NFTs depend on the Ethereum network and virtual machine, which are
highly complex, constantly changing, and which introduce changes via
backwards-incompatible hard forks.

Inscriptions, on the other hand, depend on the Bitcoin blockchain, which is
relatively simple and conservative, and which introduces changes via
backwards-compatible soft forks.

*Inscriptions are more secure.*

Inscriptions inherit Bitcoin's transaction model, which allow a user to see
exactly which inscriptions are being transferred by a transaction before they
sign it. Inscriptions can be offered for sale using partially signed
transactions, which don't require allowing a third party, such as an exchange
or marketplace, to transfer them on the user's behalf.

By comparison, Ethereum NFTs are plagued with end-user security
vulnerabilities. It is commonplace to blind-sign transactions, grant
third-party apps unlimited permissions over a user's NFTs, and interact with
complex and unpredictable smart contracts. This creates a minefield of hazards
for Ethereum NFT users which are simply not a concern for ordinal theorists.

*Inscriptions are scarcer.*

Inscriptions require bitcoin to mint, transfer, and store. This seems like a
downside on the surface, but the raison d'etre of digital artifacts is to be
scarce and thus valuable.

Ethereum NFTs, on the other hand, can be minted in virtually unlimited
qualities with a single transaction, making them inherently less scarce, and
thus, potentially less valuable.

*Inscriptions do not pretend to support on-chain royalties.*

On-chain royalties are a good idea in theory but not in practice. Royalty
payment cannot be enforced on-chain without complex and invasive restrictions.
The Ethereum NFT ecosystem is currently grappling with confusion around
royalties, and is collectively coming to grips with the reality that on-chain
royalties, which were messaged to artists as an advantage of NFTs, are not
possible, while platforms race to the bottom and remove royalty support.

Inscriptions avoid this situation entirely by making no false promises of
supporting royalties on-chain, thus avoiding the confusion, chaos, and
negativity of the Ethereum NFT situation.

*Inscriptions unlock new markets.*

Bitcoin's market capitalization and liquidity are greater than Ethereum's by a
large margin. Much of this liquidity is not available to Ethereum NFTs, since
many Bitcoiners prefer not to interact with the Ethereum ecosystem due to
concerns related to simplicity, security, and decentralization.

Such Bitcoiners may be more interested in inscriptions than Ethereum NFTs,
unlocking new classes of collector.

*Inscriptions have a richer data model.*

Inscriptions consist of a content type, also known as a MIME type, and content,
which is an arbitrary byte string. This is the same data model used by the web,
and allows inscription content to evolve with the web, and come to support any
kind of content supported by web browsers, without requiring changes to the
underlying protocol.

### RGB and Taro assets?

RGB and Taro are both second-layer asset protocols built on Bitcoin. Compared
to inscriptions, they are much more complicated, but much more featureful.

Ordinal theory has been designed from the ground up for digital artifacts,
whereas the primary use-case of RGB and Taro are fungible tokens, so the user
experience for inscriptions is likely to be simpler and more polished than the
user experience for RGB and Taro NFTs.

RGB and Taro both store content off-chain, which requires additional
infrastructure, and which may be lost. By contrast, inscription content is
stored on-chain, and cannot be lost.

Ordinal theory, RGB, and Taro are all very early, so this is speculation, but
ordinal theory's focus may give it the edge in terms of features for digital
artifacts, including a better content model, and features like globally unique
symbols.

### Counterparty assets?

Counterparty has its own token, XCP, which is required for some functionality,
which makes most bitcoiners regard it as an altcoin, and not an extension or
second layer for bitcoin.

Ordinal theory has been designed from the ground up for digital artifacts,
whereas Counterparty was primarily designed for financial token issuance.

Inscriptions for…
-----------------

### Artists

*Inscriptions are on Bitcoin.* Bitcoin is the digital currency with the highest
status and greatest chance of long-term survival. If you want to guarantee that
your art survives into the future, there is no better way to publish it than as
inscriptions.

*Cheaper on-chain storage.* At $20,000 per BTC and the minimum relay fee of 1
satoshi per vbyte, publishing inscription content costs $50 per 1 million
bytes.

*Inscriptions are early!* Inscriptions are still in development, and have not
yet launched on mainnet. This gives you an opportunity to be an early adopter,
and explore the medium as it evolves.

*Inscriptions are simple.* Inscriptions do not require writing or understanding
smart contracts.

*Inscriptions unlock new liquidity.* Inscriptions are more accessible and
appealing to bitcoin holders, unlocking an entirely new class of collector.

*Inscriptions are designed for digital artifacts.* Inscriptions are designed
from the ground up to support NFTs, and feature a better data model, and
features like globally unique symbols and enhanced provenance.

*Inscriptions do not support on-chain royalties.* This is negative, but only
depending on how you look at it. On-chain royalties have been a boon for
creators, but have also created a huge amount of confusion in the Ethereum NFT
ecosystem. The ecosystem now grapples with this issue, and is engaged in a
race to the bottom, towards a royalties-optional future. Inscriptions have no
support for on-chain royalties, because they are technically infeasible. If you
choose to create inscriptions, there are many ways you can work around this
limitation: withhold a portion of your inscriptions for future sale, to benefit
from future appreciation, or perhaps offer perks for users who respect optional
royalties.

### Collectors

*Inscriptions are simple, clear, and have no surprises.* They are always
immutable and on-chain, with no special due diligence required.

*Inscriptions are on Bitcoin.* You can verify the location and properties of
inscriptions easily with Bitcoin full node that you control.

### Bitcoiners

Let me begin this section by saying: the most important thing that the Bitcoin
network does is decentralize money. All other use-cases are secondary,
including ordinal theory. The developers of ordinal theory understand and
acknowledge this, and believe that ordinal theory helps, at least in a small
way, Bitcoin's primary mission.

Unlike many other things in the altcoin space, digital artifacts have merit.
There are, of course, a great deal of NFTs that are ugly, stupid, and
fraudulent. However, there are many that are fantastically creative, and
creating and collecting art has been a part of the human story since its
inception, and predates even trade and money, which are also ancient
technologies.

Bitcoin provides an amazing platform for creating and collecting digital
artifacts in a secure, decentralized way, that protects users and artists in
the same way that it provides an amazing platform for sending and receiving
value, and for all the same reasons.

Ordinals and inscriptions increase demand for Bitcoin block space, which
increase Bitcoin's security budget, which is vital for safeguarding Bitcoin's
transition to a fee-dependent security model, as the block subsidy is halved
into insignificance.

Inscription content is stored on-chain, and thus the demand for block space for
use in inscriptions is unlimited. This creates a buyer of last resort for *all*
Bitcoin block space. This will help support a robust fee market, which ensures
that Bitcoin remains secure.

Inscriptions also counter the narrative that Bitcoin cannot be extended or used
for new use-cases. If you follow projects like DLCs, Fedimint, Lightning, Taro,
and RGB, you know that this narrative is false, but inscriptions provide a
counter argument which is easy to understand, and which targets a popular and
proven use case, NFTs, which makes it highly legible.

If inscriptions prove, as the authors hope, to be highly sought after digital
artifacts with a rich history, they will serve as a powerful hook for Bitcoin
adoption: come for the fun, rich art, stay for the decentralized digital money.

Inscriptions are an extremely benign source of demand for block space. Unlike,
for example, stablecoins, which potentially give large stablecoin issuers
influence over the future of Bitcoin development, or DeFi, which might
centralize mining by introducing opportunities for MEV, digital art and
collectables on Bitcoin, are unlikely to produce individual entities with
enough power to corrupt Bitcoin. Art is decentralized.

Inscription users and service providers are incentivized to run Bitcoin full
nodes, to publish and track inscriptions, and thus throw their economic weight
behind the honest chain.

Ordinal theory and inscriptions do not meaningfully affect Bitcoin's
fungibility. Bitcoin users can ignore both and be unaffected.

We hope that ordinal theory strengthens and enriches bitcoin, and gives it
another dimension of appeal and functionality, enabling it more effectively
serve its primary use case as humanity's decentralized store of value.

ord/docs/src/guides.md


Ordinal Theory Guides
=====================

See the table of contents for a list of guides, including a guide to the
explorer, a guide for sat hunters, and a guide to inscriptions.

ord/docs/src/guides/api.md


# JSON-API

By default, the `ord server` gives access to endpoints that return JSON instead of HTML if you set the HTTP `Accept: application/json` header. The structure of these objects closely follows what is shown in the HTML.  These endpoints are:

## Endpoints

<details>
  <summary>
    <code>GET</code>
    <code><b>/address/&lt;ADDRESS&gt;</b></code>
  </summary>

### Description

List all assets of an address. Requires index with `--index-addresses` flag.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/address/bc1pdrm7tcyk4k6c3cdcjwkp49jmfrwmtvt0dvqyy7y4qp79tgks4lmqdpj6rw
```

```json
{
  "outputs": [
    "ddf44a0e0080f458a1a1b6255a9fa0957f2611883a483c1901ccb0f59e3eb302:0",
    "77c5a00da7dcf2c8f965effd25dda16ec8ec8d6b8937e89bbbdf10a1dc5aeb0d:0",
    "36f5a76644ee3002483e08345feaa97a71c7a210050333a8f02e942af1294227:1434",
    "e2a15acfb519ac6d95bbfd411f1f3dba4692672ea0b0a8f868da8b3f565fb428:0",
    "2b84aab0b4b9869a005ae2571a94064163652f2aeffecd4fedf0397dd6b7cf41:1",
    "e267548a8cc0c6e6033a6f82b355163bc1d041879206d27feb46e605b3e82759:246",
    "f5b586cf0e61b7d89c18a74c47a1f8df9ff530a66ed62c02cec72fde9a23a45a:0",
    "4fd271181e901809f6e2d5f89ce95ddfeb886f8db1582a35c812401af8e77661:42",
    "29f8633939e956b078fb2fa0e1219089bbe2544169e7a2755e97cc254b783cb2:0",
    "7aeca5c346aec84acde229e5927dd09aef680992223cfa57fe6f1ff7698b12da:0",
    "cccc35d597cd5a8079f6fe54bb9c743e5297d9165b0dcfa74e74687514c66be0:0",
    "590745241244d41a90df7e2cf0d7745877e4cedac573525946cc8ac7f18757e8:0",
    "590745241244d41a90df7e2cf0d7745877e4cedac573525946cc8ac7f18757e8:1",
    "590745241244d41a90df7e2cf0d7745877e4cedac573525946cc8ac7f18757e8:2",
    "6b23a6cf6d2850f437a50f1673fc8410ae36146541b3101d8573539871a91bf0:0",
    "fe130d3ca1577c65ac768f4b5b9d12a88d947ddcc31196bcf870ed5ff18403f5:2",
    "5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0",
    "c63c4910be259007e1119dbbe6fe0d923b207e78058a4f69bd54df6a3a6488f6:0"
  ],
  "inscriptions": [
    "77c5a00da7dcf2c8f965effd25dda16ec8ec8d6b8937e89bbbdf10a1dc5aeb0di0",
    "1417086d6abf96f68287b799b13b0081ec895d0b4a5fb7b70d2fde404eeb8aa1i0",
    "eb6636995ba074472e4193dbf65bb268ef5379509d9fffb20ddd5857039f80abi1",
    "4fd271181e901809f6e2d5f89ce95ddfeb886f8db1582a35c812401af8e77661i42",
    "40ab704e6123c681554102556ae3f37b0525863968311f845322fe2f2403a4c6i0",
    "0b36fa5ebce6c0e028b61647a89f9488a9c9f6ad0b90a215d10eb96ee8aedf9ei0",
    "87a0088e83e43a79e0e9b451037067bca726f5fd3da083e8684996dd1e6b6c70i0",
    "54abce9b4380e2fe90ac0cb49b442afee76838ffd91f1ffcac46f6a6fea790c5i72",
    "54abce9b4380e2fe90ac0cb49b442afee76838ffd91f1ffcac46f6a6fea790c5i768",
    "b4ba20c4eb45425f4960820f493a04a3b1c2e1364927d6001e7dc7dd524cf922i931",
    "781938d9e2e93698d41f30b4d1c7f7bfcd403761bce3c0ab579be47b408809e2i0",
    "fe130d3ca1577c65ac768f4b5b9d12a88d947ddcc31196bcf870ed5ff18403f5i1",
    "26482871f33f1051f450f2da9af275794c0b5f1c61ebf35e4467fb42c2813403i0"
  ],
  "sat_balance": 22635,
  "runes_balances": [
    [
      "RSIC•AUBERGINE",
      "1100000000",
      "🍆"
    ],
    [
      "SPACEY•CODARMOR",
      "279550",
      "🚀"
    ],
    [
      "ISABEL•FOXEN•DUKE",
      "10000",
      "⚡"
    ],
    [
      "EPIC•EPIC•EPIC•EPIC",
      "1000",
      "💥"
    ]
  ]
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/block/&lt;BLOCKHASH&gt;</b></code>
  </summary>

### Description

Returns info about the specified block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
```

```json
{
  "best_height": 864325,
  "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
  "height": 0,
  "inscriptions": [],
  "runes": [],
  "target": "00000000ffff0000000000000000000000000000000000000000000000000000",
  "transactions": [
    {
      "version": 1,
      "lock_time": 0,
      "input": [
        {
          "previous_output": "0000000000000000000000000000000000000000000000000000000000000000:4294967295",
          "script_sig": "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73",
          "sequence": 4294967295,
          "witness": []
        }
      ],
      "output": [
        {
          "value": 5000000000,
          "script_pubkey": "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac"
        }
      ]
    }
  ]
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/block/&lt;BLOCKHEIGHT&gt;</b></code>
  </summary>

### Description

Returns info about the specified block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/block/0
```

```json
  {
    "best_height": 864325,
    "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    "height": 0,
    "inscriptions": [],
    "runes": [],
    "target": "00000000ffff0000000000000000000000000000000000000000000000000000",
    "transactions": [
      {
        "version": 1,
        "lock_time": 0,
        "input": [
          {
            "previous_output": "0000000000000000000000000000000000000000000000000000000000000000:4294967295",
            "script_sig": "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73",
            "sequence": 4294967295,
            "witness": []
          }
        ],
        "output": [
          {
            "value": 5000000000,
            "script_pubkey": "4104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac"
          }
        ]
      }
    ]
  }
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blockcount</b></code>
  </summary>

### Description

Returns the height of the latest block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blockcount
```

```json
864328
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blockhash</b></code>
  </summary>

### Description

Returns blockhash for the latest block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blockhash
```

```text
00000000000000000000c82c12a925a224605b1bb767f696ae4ff10332dbe9bc
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blockhash/&lt;BLOCKHEIGHT&gt;</b></code>
    &emsp;&emsp;&emsp;
  </summary>

### Description

Returns blockhash of specified block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blockhash/840000
```

```text
0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blockheight</b></code>
  </summary>

### Description

Returns the height of the latest block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blockheight
```

```json
864330
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blocks</b></code>
  </summary>

### Description

Returns the height of the latest block, the blockhashes of the last 100 blocks, and featured inscriptions from them.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blocks
```

```json
{
  "last": 864335,
  "blocks": [
    "00000000000000000002794398a350a04cc371ee33659296a980214f0f060adc",
    "000000000000000000000470180b94350be751ea1ade67c4235c5b9515380b1f",
    "000000000000000000016e1769c5aa0f3781dd99ce2d5172a696c546d442e481",
    "00000000000000000002043c5ed07ad806a1c7133cf34670333326009d6195a6",
    "000000000000000000017cd6200b2711c024094e64797619263d74433c2bc880",
    "00000000000000000002dd6a13fffde71c09e67855d03340787e6a9b951c44df",
    "00000000000000000000c82c12a925a224605b1bb767f696ae4ff10332dbe9bc",
    "000000000000000000024ea66d1cddf1cfd8a3926a8e691844143da1596526db",
    "00000000000000000001bd9376dfbd9689239e9c5d11d579d6c8885a0efa199c",
    "0000000000000000000081f76cbccc29d92024f07f0e0b7e6b7dd063bed69bcc",
    "00000000000000000000e1ca2bab230aeb6cb75b1bb5b766cb55f1a391a7d408",
    "00000000000000000001fc567723ff6ccf674981202617384ae2152a711710d3",
    "00000000000000000002381ab1fa4661bfecc3429424c415788cef2c62c630bb",
    "000000000000000000022a1cacf15fa28d4d3698506c7b76fc62d7e50053be1f",
    "000000000000000000023b6d0182255bcc633e27ecdf8a86918830fdfd4f9612",
    "00000000000000000001135bd270114428c2c021e6c4161be93ba7ec9dc4e720",
    "0000000000000000000269e44d995970caf720ecc272f3554d923b74c57e84ed",
    "00000000000000000000e0224234536f4724c144c8da5cbaea486f3b26ef808a",
    "0000000000000000000102ae83593c0b5046cc6ec3beadf133e2a9b69fb761da",
    "000000000000000000014d52c9b6d9ca1fd2419562d24ff87214fcdf1688b8c4",
    "00000000000000000001bc24775ec320b6af4c1210395a4092c29b7af265153c",
    "00000000000000000000f4498b608a6a476bed5c4164478f618d19bcc02da3fa",
    "0000000000000000000255810324c89ec4ef87a0d028968dc70aed1817bac8e8",
    "0000000000000000000213bbddd4cce2831bda4865ae7025074b2a30fb228c7c",
    "00000000000000000002b4cf1c7c051fd712df4447ac5e90ecde1d4429a06358",
    "000000000000000000006b39a84f7bfc592293bc044c28fb57dfa660d41acc36",
    "00000000000000000001cd132f83def8f13b87974eb4d2629b11f52e3016c097",
    "00000000000000000001963de3de854dd9da9f384fb2ef753ba94c105cc807c6",
    "00000000000000000000fc1b08733842cb0f2d3dae7f56545805b403aa0d3621",
    "0000000000000000000049464eaf610aa71edaaf33e465c47981811395c3cdc7",
    "0000000000000000000137881c0f7bc6b762daf8370935444fdb13b98ed4572e",
    "00000000000000000001e7cc406d66013c17db6e9f8c90b807c93936fa18f192",
    "0000000000000000000084d8e77f14bcdc71acedf0ba5be6b70562dcf76e2ba2",
    "00000000000000000002e278d6c35e96eebb964694c430527db43301efdf367f",
    "00000000000000000002ace24c94d6f927e4cad8d72839508a275d6a2882c408",
    "00000000000000000002c165514bb47cef5b8eacedbabce070fc7147f6b8a48e",
    "0000000000000000000251f0eabbbf2bb58837cd284a1a44275e76d11b6da62a",
    "00000000000000000000650e34e08c4bc732961ce33a2b9051044ed95e95d82f",
    "00000000000000000000ecd0dfe9c0a52b2a7bcf48edcdcb2df19b827afcbed2",
    "0000000000000000000048131b07192e8f4466e36d025ea773e0dadcf442713f",
    "000000000000000000012eb14a615f799bf628e371ee5e7dd0b518d108fc74cd",
    "0000000000000000000025d47721b228c712aeb50bfd13768d8925274c1015ef",
    "0000000000000000000326c89fe7dfe7737f75368ce78404c1ffb1b08c422641",
    "00000000000000000002ff417f03781bbce1a1082cfaff8cf5c066c9a7547a28",
    "00000000000000000002bd4acc44f416975f25aa719e07abc2c0dd12761e4d17",
    "0000000000000000000188b4408d6131395ef6ca544b35cf37e7575779b15471",
    "00000000000000000003253f74e3f5d35aacbef57aee3225c9e071036309aad6",
    "0000000000000000000322bfda974265420bb6b604cd577410b9ca5cccbeae17",
    "00000000000000000001535fdb2eb0efe673bd505bcec47a9fdedd7b83d22a6c",
    "0000000000000000000169fb1a4daaaf4e08d12fcf670a81ed0f7bb4f5328494",
    "0000000000000000000315eb8d0ea1cbd251c7ea2404041c352823e29a6f376a",
    "000000000000000000021aef6c217e2eae81d1702d1331ab8f91360e55a60c51",
    "00000000000000000000ffb1ee2423e399153433e634db68ca4aad8a829b61da",
    "00000000000000000000a6e99c9e050d4345606016673d674da4aade02a8ff8a",
    "00000000000000000000349de7338756bdb425cc13a3e22e986b4035d00f097b",
    "00000000000000000000045218f05f939e0386ddec2460c815e5c671bfd20892",
    "000000000000000000007f99d51dd0738c42ce7dc83e59061a2b33f971b6d3ab",
    "00000000000000000002fc37d0f7ec804a1063a4ff8613521fcc99f1ab8fe07a",
    "00000000000000000002daff4047da69c658a1badb00d14d7d3e709f76b8bf3b",
    "00000000000000000001a427c71546cda9a5577d5e38bc95a5d3450df7c1d26f",
    "000000000000000000004648af338d38563d26c3a5bef3ca9582ea2ccb72f8ea",
    "00000000000000000001428e153a325e9aa859589a80e8b0271d1ba48e8749c7",
    "000000000000000000005ea10805f8ab474b9888bdc2c2840cd2e5529bbd0d49",
    "00000000000000000002c7b5bcf3372c7441e79bda1310c53f35eb59483b9092",
    "00000000000000000001e2486f12c01ca0f76481b40181bf6f8f48802ade8c49",
    "000000000000000000024590edc9d2d4878b32a4944dfda1a3929a6e4c9c3592",
    "000000000000000000028f255235dca42b10e5da593c2d4eb006cb329a041587",
    "000000000000000000006fb8f4a5d906e9c0112d5a97188f392407ab8e95bd81",
    "00000000000000000002864156220f1093e76caf233009c1b6be9ee0d810ac29",
    "000000000000000000009d28b5b1336abfe552aa8d92e56c1c254a1eee0e0b4b",
    "000000000000000000029ad7c816c8a4f79f93e60defbc6aee7cf25e61b46008",
    "00000000000000000002af0693f1c73282516b97031b7d956d07756a6f8a13d0",
    "00000000000000000001376f75d1785015b1c4717b2612a7c1bc8de69817c768",
    "00000000000000000000cd6e3f3ec308a26831d8866ea51beab6b02d3a5d0812",
    "00000000000000000002cf32a666fabf1789ffa4fa4215f78b52406b716936d2",
    "0000000000000000000310254c2c405a46c9710e52a7a7728bac5079b90e25ba",
    "00000000000000000002cb11925574071edd904390823344b7ca616640971081",
    "000000000000000000022e5bf2570eaee0532c0edee2a2682d4a74488ca0522f",
    "000000000000000000031542e9c2b0dbd43b4e7caa3f24537af0d39bfa3997cf",
    "00000000000000000002215bb1138bbc4a7611826b13e532b51d5b4e82eeac3d",
    "00000000000000000001c3a1c78d27f0072f27dc1d0060273e0ef03f1bfc0ce9",
    "00000000000000000001fe6ba288a1b9a14d15d3e915418cbfb54685595b0cc1",
    "0000000000000000000067f8164cd2e75b3ba172cb98cd00f0894faee5c6f763",
    "000000000000000000018ef9990389ca9052a0c1c93b65f780d3071346e531f3",
    "000000000000000000023e7bee6b1b4647411b0279df23c9ad470d91c1b99081",
    "00000000000000000000a085f77681ddf175c74b897758e9f406a17f1a278030",
    "000000000000000000001bf9c32af2d6a8a4f3d50c40f927e0867d4ad9481fdd",
    "00000000000000000000cde89e34036ece454ca2d07ddd7f71ab46307ca87423",
    "00000000000000000001141c91e70decadd60a93f32b70b08a8ec6d74b270b08",
    "000000000000000000023562ac878ab6f62329a70a15954bd56e088f3a836426",
    "000000000000000000006a4455949ef37cf3c3ee6b4cc2da27137f24445c7058",
    "0000000000000000000297397401eee3019168e761464c3716892951a5e33cbc",
    "000000000000000000015b68955519ab2925858ebbd02f897ff81cfc4a360dd4",
    "000000000000000000018a0932deb92c6bc40d46a34e654f8a2afbd6c745c6a3",
    "00000000000000000001996de65cc72f1fdeaebc3141db0a2a2dd269233c8e56",
    "00000000000000000000d0434cc36c19d49b9e873661ff171d632543d5c2f454",
    "00000000000000000003184a301f7c76332ec629a51bcaab5652f2ba82da55d8",
    "00000000000000000001e47fd13c25e24f8933b02a38c3490c0a430c0b71ea9e",
    "000000000000000000027fe376111297406696afa48be122d6596b13ac15156a",
    "00000000000000000002ab8ba2529a468c0f2781e3afc0f832209c94f95d4f1d"
  ],
  "featured_blocks": {
    "000000000000000000000470180b94350be751ea1ade67c4235c5b9515380b1f": [
      "0ae94b05b21aa6b7f0620075db618a70124cb422fc5ced577bffbd0d103d4ce7i0",
      "65f1922bc83ee43485ed884dbec24c0c1cef6c4f6d999a8ac0c09d7adc8b39dbi0",
      "e87c21c7c8ba8b194bd8e389f6cb9ecb2312c076139aff31c629f93df86b98ffi0",
      "aeb8d90de7e92efc11ffa6b411e829b6dcb0e00b7fd4f912947065b9084d99bai0",
      "6d8f58c7f24e277d614bc6c9bb6648543e47db5431c6c073a6bd5e3be1e47c5ci0",
      "770cde7a5c49ae8a4f109bd83fb364ef9b83bc6f72d3654c793f5452d7b30831i0",
      "65e51357e67da9dd64a65fff1d9d26153c9969f4acfbab028e74b408559dfc07i0",
      "7c63687fabdcd421de925e99b4152b2327328afe51c63903aa4a9cc9fba31872i0"
    ],
    "000000000000000000017cd6200b2711c024094e64797619263d74433c2bc880": [
      "c970b695f491a8812b5293da2673f4e6c9ae3d8be07d9da1fbb9c33a45f6fd1fi0",
      "d001827b7c48e44399587f12e2fa33b2c0b1eb12c309f1c21729f1e3bc95c5fci0",
      "facefc9cd6dec1cc25d7b7321cbbdaed735049a9a3da834a66975d98e23ac4dfi0",
      "09353363c2e95891db553f3742a40a74c5dd1b7668669f732d58e52e7c132b92i0",
      "48f3f7cbf3061957c06f66c0fe66be9ad4ad73df65b9ded1345e05f904e1e63di0",
      "4e65b1d0b36c6727c646d5d6f45f00db35158a49a139282d6544f127734db9adi0",
      "b8c744320e735aaaec18fd6b306d6dd678f99461e88dfa25f178627b8480e483i0",
      "55d27ab1b4321addc5c34c10ef2ac4957add8b8485f465df7f2883315c9cf5f5i0"
    ],
    "000000000000000000016e1769c5aa0f3781dd99ce2d5172a696c546d442e481": [
      "cc2415293c275bea4d73ff8f45f68f269686b819de447f50ec6988ac04a62d1bi0",
      "c642cd4cc7a075c61d3a32b949217990aa91dfc928f12a2cdba1f2f228c699c7i0",
      "5342721d044e9e9999484b988ce9fb71097d9209c77f6549df9e31ec9b344c5bi0",
      "a75f792be155a0b53691289433a6413c1efb1aeaf970f752ee70be3c6e755a06i0",
      "19c0d770abaaeb5b24e718231684d53b768450cc324c8fee435910de65c459e2i0",
      "30eb7c46bf4f5af33e665a119af40dd45d127cb6cdc2596de75e08f094651fa5i0",
      "122631e7b8bab4238582229273a9dbe08544d2d97ad0c9a80b5829ae10ac3f27i0",
      "41c304db88c60a27f45957442b857c0affefdfdca45bdf72ab4cbf9fce4d97a0i0"
    ],
    "00000000000000000002043c5ed07ad806a1c7133cf34670333326009d6195a6": [
      "2f62d6ed309f838bab143cf3a53ba758eb940b43c30c32e22d9dbf6fe7882613i0",
      "83642352c5b670387874995954f79e270cb78b05a9a88b9d4d65e6f94c6df0a3i0",
      "68831e3c8669ad5e8fc3585a9e8a55673123ada4c33a699e98e4d9e0297f1800i0",
      "20fa9d317af18cc976a6b77797ceb5884127ac5dd7e3f131565a18dd712311c6i0",
      "a286d7f705fd410cdd3f1081c4c22f196bdea4c64cfbd963f45302cdec1fe968i0",
      "11eb110f86d880d8dcac852edcca7007904fda34ad031fc01f24a3e6b02ef47ci0",
      "9fbec6d72d71169dc041693e740dae7bb7bb195ccd4a7f40c4c12bd4afbf7354i0",
      "7c823fe74fa783debea8339fbea44b8395805295652749a651aa2133d9a1832di0"
    ],
    "00000000000000000002794398a350a04cc371ee33659296a980214f0f060adc": [
      "2596a275dca4b5cc18cd1060ab92d6df3df5507738b8f2b6b7c18c4ff1d1b36ai0",
      "93256e5da147f0067d6b11e09d853b838ad1d95cf59664cccbcd52859f9ea1aci0",
      "f404b5ebabd4b7fb8b88df52289b983b28f3e36fcbb63e649edea6e7ba62e582i1",
      "f404b5ebabd4b7fb8b88df52289b983b28f3e36fcbb63e649edea6e7ba62e582i0",
      "1bfbd226fded339cbe197153ab8b6da622c9a20e7d4911013abd385da7e05b89i0",
      "af7b8810755bdf7bd62dbb6c5f2639e107a6d9d2c7199ae3650f1e7583d4bd66i0",
      "9c594cb991bfecdf9d2116b644262927365f20f03ccdc8a64cbb640c11a58907i0",
      "29628c91948bc100185605d11cde0aebda572d73b752bd6ed668bd86e455aa8di0"
    ]
  }
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/blocktime</b></code>
  </summary>

### Description

Returns the UNIX timestamp of when the latest block was mined.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/blocktime
```

```json
1728158372
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/decode/&lt;TRANSCATION_ID&gt;</b></code>
  </summary>

### Description

Decode a transaction, congruent to the `ord decode` command

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/decode/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799
```

```json
{
  "inscriptions": [
    {
      "input": 0,
      "offset": 0,
      "payload": {
        "body": [
          137,
          80,
          78,
          71,
          13,
          10,
          26,
          10,
          0,
          0,
          0,
          13,
          73,
          72,
          68,
          82,
          0,
          0,
          0,
          100,
          0,
          0,
          0,
          100,
          1,
          3,
          0,
          0,
          0,
          74,
          44,
          7,
          23,
          0,
          0,
          0,
          6,
          80,
          76,
          84,
          69,
          255,
          255,
          255,
          0,
          0,
          0,
          85,
          194,
          211,
          126,
          0,
          0,
          2,
          206,
          73,
          68,
          65,
          84,
          56,
          203,
          149,
          212,
          75,
          104,
          19,
          65,
          24,
          7,
          240,
          148,
          74,
          19,
          16,
          93,
          20,
          180,
          20,
          105,
          22,
          193,
          179,
          61,
          21,
          11,
          125,
          44,
          228,
          90,
          176,
          39,
          41,
          90,
          75,
          14,
          30,
          74,
          91,
          74,
          43,
          69,
          18,
          250,
          200,
          86,
          60,
          120,
          80,
          154,
          187,
          104,
          5,
          17,
          81,
          170,
          205,
          161,
          96,
          11,
          77,
          178,
          161,
          120,
          145,
          98,
          2,
          30,
          4,
          109,
          147,
          77,
          201,
          33,
          133,
          154,
          221,
          196,
          144,
          108,
          146,
          221,
          157,
          191,
          33,
          51,
          59,
          1,
          193,
          67,
          231,
          246,
          227,
          155,
          239,
          49,
          51,
          236,
          186,
          206,
          184,
          36,
          172,
          181,
          209,
          9,
          212,
          218,
          18,
          178,
          46,
          210,
          214,
          136,
          8,
          161,
          189,
          181,
          14,
          148,
          179,
          60,
          205,
          0,
          108,
          158,
          232,
          214,
          36,
          172,
          91,
          142,
          196,
          180,
          170,
          4,
          100,
          222,
          237,
          80,
          85,
          150,
          101,
          167,
          140,
          172,
          198,
          186,
          21,
          57,
          193,
          4,
          77,
          246,
          39,
          193,
          138,
          118,
          168,
          165,
          198,
          104,
          9,
          11,
          172,
          65,
          24,
          68,
          7,
          25,
          96,
          13,
          194,
          168,
          22,
          64,
          134,
          168,
          4,
          213,
          172,
          84,
          76,
          132,
          152,
          74,
          90,
          161,
          170,
          149,
          8,
          149,
          247,
          38,
          10,
          85,
          104,
          97,
          170,
          222,
          95,
          234,
          92,
          85,
          157,
          13,
          48,
          77,
          162,
          82,
          129,
          49,
          206,
          78,
          167,
          106,
          149,
          138,
          134,
          121,
          58,
          154,
          148,
          55,
          235,
          101,
          211,
          2,
          83,
          29,
          181,
          28,
          202,
          76,
          50,
          113,
          141,
          232,
          107,
          2,
          232,
          216,
          193,
          58,
          146,
          183,
          229,
          114,
          142,
          42,
          16,
          128,
          49,
          9,
          59,
          77,
          53,
          55,
          15,
          243,
          26,
          200,
          15,
          170,
          25,
          213,
          109,
          238,
          89,
          226,
          12,
          139,
          69,
          175,
          219,
          59,
          197,
          167,
          159,
          89,
          222,
          195,
          115,
          171,
          198,
          151,
          75,
          44,
          47,
          152,
          93,
          24,
          54,
          30,
          39,
          88,
          77,
          249,
          32,
          19,
          215,
          35,
          75,
          50,
          213,
          224,
          126,
          195,
          171,
          159,
          63,
          89,
          166,
          179,
          244,
          135,
          177,
          17,
          145,
          72,
          63,
          213,
          224,
          38,
          89,
          215,
          250,
          26,
          123,
          84,
          146,
          128,
          245,
          45,
          137,
          136,
          84,
          189,
          34,
          146,
          154,
          140,
          110,
          246,
          40,
          227,
          24,
          139,
          251,
          109,
          63,
          149,
          32,
          34,
          109,
          200,
          240,
          50,
          9,
          120,
          45,
          74,
          132,
          201,
          189,
          73,
          94,
          25,
          125,
          141,
          40,
          191,
          121,
          69,
          84,
          200,
          16,
          211,
          126,
          67,
          194,
          179,
          147,
          21,
          166,
          131,
          204,
          106,
          119,
          106,
          201,
          81,
          118,
          193,
          54,
          142,
          19,
          22,
          83,
          176,
          211,
          220,
          171,
          117,
          30,
          81,
          117,
          141,
          8,
          166,
          73,
          132,
          231,
          44,
          38,
          195,
          232,
          5,
          142,
          184,
          170,
          23,
          184,
          4,
          25,
          191,
          223,
          1,
          25,
          42,
          17,
          248,
          185,
          45,
          3,
          84,
          18,
          176,
          125,
          87,
          129,
          243,
          42,
          50,
          201,
          223,
          215,
          161,
          210,
          91,
          90,
          133,
          153,
          212,
          150,
          97,
          80,
          25,
          10,
          54,
          116,
          9,
          177,
          108,
          75,
          150,
          10,
          209,
          47,
          98,
          170,
          165,
          142,
          211,
          113,
          226,
          247,
          143,
          217,
          247,
          138,
          45,
          153,
          119,
          106,
          170,
          242,
          160,
          50,
          65,
          101,
          169,
          127,
          82,
          241,
          105,
          76,
          81,
          65,
          169,
          78,
          69,
          191,
          65,
          161,
          58,
          197,
          206,
          98,
          79,
          90,
          105,
          180,
          228,
          170,
          146,
          239,
          75,
          163,
          95,
          231,
          11,
          180,
          67,
          125,
          115,
          160,
          120,
          156,
          123,
          177,
          77,
          21,
          90,
          33,
          110,
          75,
          200,
          167,
          216,
          107,
          62,
          209,
          109,
          131,
          212,
          36,
          42,
          9,
          101,
          211,
          172,
          59,
          103,
          16,
          176,
          146,
          143,
          230,
          85,
          194,
          110,
          130,
          40,
          205,
          57,
          35,
          22,
          85,
          7,
          81,
          136,
          142,
          72,
          209,
          249,
          252,
          82,
          68,
          39,
          139,
          89,
          166,
          91,
          1,
          162,
          219,
          233,
          53,
          166,
          158,
          212,
          225,
          68,
          104,
          145,
          193,
          229,
          141,
          223,
          120,
          27,
          142,
          56,
          186,
          24,
          59,
          52,
          3,
          91,
          142,
          4,
          25,
          64,
          134,
          11,
          0,
          8,
          223,
          169,
          120,
          124,
          34,
          223,
          233,
          14,
          26,
          177,
          220,
          17,
          87,
          224,
          101,
          126,
          184,
          173,
          57,
          143,
          239,
          106,
          91,
          211,
          30,
          223,
          229,
          255,
          197,
          116,
          143,
          207,
          107,
          113,
          205,
          2,
          13,
          30,
          235,
          250,
          208,
          204,
          251,
          200,
          245,
          169,
          153,
          199,
          229,
          126,
          228,
          241,
          93,
          105,
          87,
          49,
          154,
          221,
          121,
          149,
          206,
          221,
          230,
          100,
          187,
          92,
          104,
          78,
          93,
          115,
          212,
          161,
          3,
          164,
          232,
          200,
          101,
          2,
          102,
          150,
          43,
          244,
          230,
          125,
          36,
          193,
          37,
          218,
          227,
          248,
          231,
          63,
          120,
          182,
          245,
          23,
          127,
          181,
          197,
          106,
          45,
          115,
          252,
          75,
          0,
          0,
          0,
          0,
          73,
          69,
          78,
          68,
          174,
          66,
          96,
          130
        ],
        "content_encoding": null,
        "content_type": [
          105,
          109,
          97,
          103,
          101,
          47,
          112,
          110,
          103
        ],
        "delegate": null,
        "duplicate_field": false,
        "incomplete_field": false,
        "metadata": null,
        "metaprotocol": null,
        "parents": [],
        "pointer": null,
        "rune": null,
        "unrecognized_even_field": false
      },
      "pushnum": false,
      "stutter": false
    }
  ],
  "runestone": null
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/inscription/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

Fetch details about a specific inscription by its ID.

### Example

```bash
curl -s -H "Accept: application/json" /
  http://0.0.0.0:80/inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```

```json
{
  "address": "bc1ppth27qnr74qhusy9pmcyeaelgvsfky6qzquv9nf56gqmte59vfhqwkqguh",
  "charms": [],
  "children": [
    "681b5373c03e3f819231afd9227f54101395299c9e58356bda278e2f32bef2cdi0",
    "b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0",
    "47c7260764af2ee17aa584d9c035f2e5429aefd96b8016cfe0e3f0bcf04869a3i0"
  ],
  "content_length": 793,
  "content_type": "image/png",
  "effective_content_type": "image/png",
  "fee": 322,
  "height": 767430,
  "id": "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0",
  "next": "26482871f33f1051f450f2da9af275794c0b5f1c61ebf35e4467fb42c2813403i0",
  "number": 0,
  "parents": [],
  "previous": null,
  "rune": null,
  "sat": null,
  "satpoint": "47c7260764af2ee17aa584d9c035f2e5429aefd96b8016cfe0e3f0bcf04869a3:0:0",
  "timestamp": 1671049920,
  "value": 606
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/inscription/&lt;INSCRIPTION_ID&gt;/&lt;CHILD&gt;</b></code>
  </summary>

### Description

Returns the inscription information for the specified child.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/inscription/b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0/0
```

```json
{
  "address": "bc1pnhyyzpetra3zvm376ng8ncnv9phtt45fczpt7sv2eatedtjj9vjqwhj080",
  "charms": [
    "vindicated"
  ],
  "children": [],
  "content_length": 106268,
  "content_type": "image/avif",
  "effective_content_type": "image/avif",
  "fee": 1470535,
  "height": 839704,
  "id": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi0",
  "next": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1",
  "number": 69994605,
  "parents": [
    "b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0"
  ],
  "previous": "e2619e0fa641ed2dfba083dc57a15ca1d3f195f15d187de353e1576a0cb6e87ci8",
  "rune": null,
  "sat": null,
  "satpoint": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527f:1:0",
  "timestamp": 1713399652,
  "value": 10000
}
```
</details>

<details>
  <summary>
    <code>POST</code>
    <code><b>/inscriptions</b></code>
  </summary>

### Description

Fetch details for a list of inscription IDs.

### Example

```bash
curl -s -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '["ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1", "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi0"]' \
  http://0.0.0.0:80/inscriptions
```

```json
[
  {
    "address": "bc1pnhyyzpetra3zvm376ng8ncnv9phtt45fczpt7sv2eatedtjj9vjqwhj080",
    "charms": [
      "vindicated"
    ],
    "children": [],
    "content_length": 116597,
    "content_type": "image/avif",
    "effective_content_type": "image/avif",
    "fee": 1470535,
    "height": 839704,
    "id": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1",
    "next": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi2",
    "number": 69994606,
    "parents": [
      "b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0"
    ],
    "previous": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi0",
    "rune": null,
    "sat": null,
    "satpoint": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527f:2:0",
    "timestamp": 1713399652,
    "value": 10000
  },
  {
    "address": "bc1pnhyyzpetra3zvm376ng8ncnv9phtt45fczpt7sv2eatedtjj9vjqwhj080",
    "charms": [
      "vindicated"
    ],
    "children": [],
    "content_length": 106268,
    "content_type": "image/avif",
    "effective_content_type": "image/avif",
    "fee": 1470535,
    "height": 839704,
    "id": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi0",
    "next": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1",
    "number": 69994605,
    "parents": [
      "b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0"
    ],
    "previous": "e2619e0fa641ed2dfba083dc57a15ca1d3f195f15d187de353e1576a0cb6e87ci8",
    "rune": null,
    "sat": null,
    "satpoint": "ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527f:1:0",
    "timestamp": 1713399652,
    "value": 10000
  }
]
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/inscriptions</b></code>
  </summary>

### Description

Get a list of the latest 100 inscriptions.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/inscriptions
```

```json
{
  "ids": [
    "dca3da701a2607de6c89dd0bfe6106532dcefe279d13b105301a2d85eb4ffaafi0",
    "0e50a465fc0ca415f3cb8a4aac1555b12a4bf3f33bc039f2a4d39f809e83af7ai0",
    "934905624f847731e7f173ba70bfa3a1389b0a7fe2a4ffce8793eef2730b9ab9i0",
    "50a42e51e6ce0ef76699f017a1017d7b5b6203e67d283c625ba7d1567b2e43bai0",
    "65a78bdbc1e01ac02cda181a71304a8d82305bc2a24bf01e62bea4cfff3e2dd8i0",
    "05ab6d843099fb30a1da1bbfe31117cb56466b3ba40a4b3f389cc37174d339b8i0",
    "47825a32dd6e3de5fd7d97488d755e6d1005e5c8552b9ede5bc67900b074d09bi0",
    "737552653d4424a523f8c652710d0f9416561ea67ee25242f8606b49fb428d9ai0",
    "1d7d15ab48fccf7011435584556ee9106be71f7073a857689594c143d7899333i0",
    "321e4f598ae0f4841af04d1a84f3abafa44802c7d35315ead91b32ffed0f400di0",
    "eb1578eaca0a04eaf174296382fc5d77530f0feceb7747938b29c433c21d1afdi0",
    "70d6136e949b5f07b6ac7d50aa9aea1fa6573e1b0e4f490170235ac74738bf5ai0",
    "aab2c8514876fb81cb28f0f0516620cf189222e0ffc6fe6282863bb846955409i0",
    "ef36dd247b98f12d19d15bab92ea7f8491b0766fb0b8074b7606614dbbab6c13i0",
    "cec42963619240ede36fb03cd95d8fba883c9c1af72b1e2fc9746151a60729dci0",
    "3124d086c59ce2205f52a108e21380e2c98b1ac6a21fc2f457fb5750317997d2i0",
    "c2d19ab0d9e508ed20eb6620a4ed6b5700bcee835278eb171ad15e3d9e9cf3cci0",
    "6aa9e8efbc0410adebca732a2baa6812bd4d9678771023503d20c8e90f632853i0",
    "96fd8d9b06c9d55d57c926889716b05f03e508d05320ffbe052aed38f49a8a4fi0",
    "9429a355eecc994380920e8c9a2fd17adcb2e745bc1c8a460ed016d37e02d11ei0",
    "196fa44615bd2215e17f428d9cb6ea5de62e4fc6635e45089623f757189cc3b1i0",
    "077ccaf7424917873fe217bc45cfe923d20a9732373fc2b08749106569a198a8i0",
    "ea5d4f47955e9ac306113ebd616587d2eaef3fb242474fb5819562ec007db32fi0",
    "db377bb1c8ad40dfa6bf69b2ff8f5417b419ee6b0657e75060e088b1ec8b1c93i0",
    "3c9720eeaad27cad478404905c9d5dcd332878f95dd65fc9912bfd598041af0bi0",
    "61ef119d102389c3daaa5c057514f30cf1cd410b7d5c41a28c58a9a902cb265ai0",
    "f971ea01b40b35b8548a902e013a3a1b799d4c2c1613d37ef3a994120d65c10ei0",
    "6021306cf760dfbb0da58bce59ffbc703db5c7d9b180a3ae5268ce4c5341bf34i0",
    "10aebd52ccd20124d5aa1c7d3e52fa81776ac6a3fb79ada582495328fa968ca4i0",
    "9ed8d1fc12ab4d4b50c869bda1a38bb0e82b6eb18d2c14ef880aa2bb1757dbf7i0",
    "5f5db3b301aa766f1a22f796248b2cceb8c111419bcefb4a3365d8bb1ff6ca05i0",
    "15737d13a3583ef3559090431d5ea846e5126963046041b1f4d42b2fcc9a03eci0",
    "05fdf04307e006eefec908ef93806f96c472af9e073837f4b1f5ad52e1d719a9i0",
    "559280eb5ceefadee232a5b4dcc2c05dddfc1f123293482fef30ab7632855b85i0",
    "76939794300cac687832e68253640e69993b46e0b75e5af4678b3c4b2037bb4bi0",
    "1967e5170c27e707545ec05624db313735799fc58142e2bd2b475b088b761022i0",
    "b320472502a8750fc6b3cb87ac6a0b3eaef402fa5f218f1153f658642e3d1b3di0",
    "456b48e7ef556004e4a9a8b98aee8c797d75e1027dc56982ef6936f8627eeda7i0",
    "02fb3081fc7317cbec7adc63761ea373ec239c7703a13f5752c3344acf6312eei0",
    "75e567e30b84205ca9f5b6280b29581310bed27504949996b64858110d38c5e8i0",
    "47214c34652fda5745b56ac80512e7d353db9d949fac9e0e5a6d8b27507fe4c6i0",
    "c3ccc1508fb08a6bc487b25e4d5a994ff73cb44251749619c53d87c7626d74c6i0",
    "a8ce87da5b67d9782846d2f718058873c51bcdbbee536540266f868bb5376c8fi0",
    "47dd14ebf43bc35ff753cb5acf8335eb1acb788d05a2b0b9d83302e16170127bi0",
    "ae7c0ebe825c2bf0d5820bc28da095cfa1cb6913a5913142bc327ab985b3dd7ai0",
    "cf68e9b9d1967859b7d832a9c815ae3c837c94031dc8e56d848d151ae24e4776i0",
    "0fe7d513cf8c19734f84321a3c49d0e0e39255702ba18546740c2bb1a95c5170i0",
    "e760f5028719b2130e0d2305c3531174f4f6167282251adebfc968d127b79369i0",
    "aa81f43a1412b0d04c2ef825c3829707aa32cf4cca3452077c2819f012905b5ci0",
    "cd3675b40f8056c7b816c02a537a6d997912a26302f77cd3d0ad83b657d24e4bi0",
    "3f8bf38d3cd3e50693b9bf187e1a374ff9990ba8e8f6337829f0d7312805741bi0",
    "415a9516e1dcad84a60cc7d012c2475361d575f713b1d3aa16f982d2e43e330di0",
    "6db363228406a71744cbf9b86e2b58c21b4f2dd0a0ad0affa211b32af20e8809i0",
    "ff1aa5bec2a626c8b6f90e6765ceb227d44565a90f9e54cf05f5360ef6e33708i0",
    "161191b5de6a1b1ed53e816545176d47e214c50711474b1a4e3ab34d70634189i0",
    "f3f7488bc66000965a36f4ddf000c3d3ca3cf94d7cd4defaf3ca0b68e86b3af8i0",
    "b2fb38073ade49a3f0f2522a15f4f63122a60d03a9eaed5c1c4198d339a32a1ei0",
    "2f99c317739ca8cb6eb904915648ac2044f815d01ecfae6762ecf3885ee3778ai0",
    "9d30636a2c5b6e064e6868fe796986014ac4cf9ea7a859d12e2dea07128c04f5i0",
    "62ea57535dbe1c748d79c693e507d787af60076eaec7629365c31f52607f1279i0",
    "9540b2f1d24ad5750f155ee232b03e4bfe258fde8c396844471bd595cbf0d4e9i0",
    "98bfe331d267749357857e86433f974595bdb1d76ff60d35e576b217d7eae4e3i0",
    "00bea5fcc8723ce5d177ca1cd4e87f7f2792fa3043231554d584b869d791a9e0i0",
    "2ca9e9aedb2bf622c5c499701ce74a1dae456569082704ade20ba125019ea5f9i0",
    "83290426401ac68ce29306f6a1ec5c86c69ce66049a1d85fefa49088a0f5a11fi0",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i8",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i7",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i6",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i5",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i4",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i3",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i2",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i1",
    "ebc452becb7438e43281317045ab5c675376486a9344625b5dec09d5a65a9905i0",
    "d431778a7290951d463f356f637a801c4c8b77767f2f53686176202bfd3a1af7i0",
    "8bc9f9d88f91d851eeb84481fb33baabd6ea172c0c5152e5b8d4140f8102671ai0",
    "12454b1620904b63e8c47f31e17939735515923e674fc42f299b5466258b640ai0",
    "a67d21421a27918ab052c4dee3dcca86ad0610ccf4a449f98d3316008953d54ci0",
    "920512aa32b5d525495832a3146f32efb0bfa308519bc3e1d5bc151ec6c9412ai0",
    "8defe7abfd7d4f9dc94be83ca0b2f823f196a80ea37ebe217702368ffd2c7807i0",
    "6b26e994bdabb558d41f5824b3d427ec628e7a1e7596ac20dcf05e889a994fd2i0",
    "327610662171136ee252724b6860d0b64b45f81cb2bf8a0606256db730946a39i0",
    "e01ec43055caa4bdb73f300076501deb85780891181d07773231db700a7d2099i0",
    "9c2dc67e959bf949396d31157f16b6d60e4469ff43ba1ed44957d197f3ebf78bi0",
    "89126f596c644721edc65ef293730078f16f0894baf29a1d807aab4afc013d72i0",
    "3ea79b7a166ed230046e3d890d6c39a7c64dec8443de933860534449fa3180a0i0",
    "2f1b248a957dcfb442b89c4684f65ba7bab7061cd0dfa4eaae8f5c65d7b41985i0",
    "ac0fb3d3d301a28d3d979ca7f522eb4fdf0b0fed9d8062ff4944d5dac353092fi0",
    "d0ebf39a32d409eb92bdefb354a99408367709830d03d4c6bc36786e79782720i0",
    "7bfa54eb0141a93cd9cd2d3a6a52de5c1116653035bd8179608e115c823b7574i0",
    "14fbc773b0b7635c4fd598c102a0a5019aee75a1184ac8d189c59478931ba6e9i0",
    "0101a253d50138b4ef67c4036246df3e2a74d70874ad3c8f943af54e4b37648ci0",
    "d5b41d45f3c45e2bdf36a415d9ece493cca23e762ff5de34a6abcf79936fc614i0",
    "4c92503dc1f38bf77c2b1219504bb6eb82dd1d8af172f84d86d433f7bc557d4fi0",
    "7b6df715cf052fdf28dbe213fada59b910c9e339137f0bd35698f23d0140a826i0",
    "da06b7b4d298c837566b8daafae7cba1d4be19ca3b9e63d867cc2a9f06dd6315i0",
    "958655c68793fe9e4dc6c8155c28c6b14c0ed58c5aa340d6bd6ef085134d3fb7i0",
    "f8dbba73e65bf996e7cd8388ad85f7303f2caa52acf1ce793d8141dd9f70f6e6i0",
    "8d166e2e3ea2a9e5d6460964d533b61656b6a3d671e5f046030319bb73f93e9fi0",
    "2a60d61dff2ba192ca81614f8f0bda6c24eaac2c45f879ef84302e8c4c859bc9i2"
  ],
  "more": true,
  "page_index": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/inscriptions/&lt;PAGE&gt;</b></code>
  </summary>

### Description

Pagination allows you to choose which page of 100 inscriptions to return.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/inscriptions/9
```

```json
{
  "ids": [
    "6fa8b4d1840fdd2172b51a40bde3c8b88ff2ca8b668e56fe05edb1d5eec91fc7i0",
    "c6f11a3269e7ea108abb9d596c4273067f33f7e951bb4762b915a6c3c3e1ebc6i0",
    "24829232f529c1c4d1bfc5c1c1410313c6388c1db14137fdc351f8659eab72c6i0",
    "c068402416ec57e773d9d072ad51950b77359eddbf515a775bc6c70bf75869c6i0",
    "3ffdf269a5a6a306c6e2e03b73b505a4f2dac3e0708257bca37c12d2ceec3ac6i0",
    "f505cc5a01e603bee41e3986c0bfe020cd4054cbdfd0a35b57d89e375ba1e6c5i0",
    "3caeb09bc1a6c7e3ac33528f69b9b10755072aac2c7b6b4f58878df45572ccc5i0",
    "2233ee78d07be90ae18d12d51cc89734eb691b550b687c1547b0791de668b2c5i0",
    "86475391a0e7f13f3b475e3b4aedb8ada36b63bf9bc4f9ac9203fb083a39a2c5i0",
    "18fa7b8a0949b57fa4798ccf48e4ba4a16ecb14651edd5a5adc3806eaea0c9c4i0",
    "fb6a338c0de40e88e03e7ae5231b036e5f452343db128b849049c2e63d0bc6c4i0",
    "374e71e371dfedcfa2f9ac1d6f2d0664effe46ca27907792e396a3176a82a3c4i0",
    "bc2b2fef1231c07232cd1333978366255e317e000a04c050262a7d71eaab0cc4i0",
    "d627b48539c497f768279669be7690af5af8f302bfb2641989dacce8c4eed8c3i0",
    "632cf2db36977e4e091ed50d61185ad78d97e7a6c6ba468b844bfd7ac9b8aec3i0",
    "2fc44592a0d8924c8f48c9fcea8b189f9008f2795380446c0d13a9e452f284c3i0",
    "2e84632f9f2965d8648a36e2695070e3f9a06fab1fa72176d95652a19d6d3dc3i0",
    "c78e74a90bb23e55d23b221d6f184581d75f0e97acd94b6ab9c2536bae79f2c2i0",
    "b4eb0dc05c24f48105d80c38c2ced8789c7910960d07db3e7326cbfae5ded9c2i0",
    "5f166abe3f70f72479518451f11d67b6217a67e539c08440f844c6f71f2ea1c2i0",
    "32c2d37d9bd7f6a019e48bc8bbcd0b07cee07314724f517935b1e0ff490e5cc2i0",
    "0876e126bf57724045c799b0f1f6ae206d2bd15c4533212ec243951f03d834c2i0",
    "6492faedbf75e28c4637b6a1e518d063c0da130c461bb193bf7215364c7bf5c1i0",
    "be6f1f3e8ac1841f05dc0d67b650890dc845fbfd2d3833f48a0adb5016a6a2c1i0",
    "1cb2cb5519aea30e3921d59862bb1ca7d2a61430fdf6b64dc2d84a35fcc52dc1i0",
    "00d0f2dab82c0f1ba5208cd95cf204505617cdbaab854675875035f584fc0fc1i0",
    "4fd6ea5ecc0660d4b238deeeef7c7a238ed324a5343e5a83d0cd34d0cba7f0c0i0",
    "cb1d5b0b9c88e1cd2646939e2809119ba857770e0aacfa069ecf992745435bc0i0",
    "30394ffad8c25f93083e9044b3faca9fdcce9610af522a3d72c8bf6478e612c0i0",
    "2c80a5b7628e1cba9b890d4946d202fa9d534e0d4edb575ca18fc8fede1d05c0i0",
    "e3bca997a4494d2c43b441eefda53ec1c63277fb79e93204787d3733bf9f91bfi0",
    "7efeed6060c4a0749bd537b36d469fd874e66914b661de992a053e4702d618bfi0",
    "39cc481cad92dfbe5a7db974a8f40f0b945ec0a10cf0e525a1e40214ae9b9ebei0",
    "776725263fec5b995932dde0c79a511838b2f4da976d767ec357490d8e5142bei0",
    "01d5456b25bf80cf0bc661f5fe65167382cb67c324ab88f9a622c0722f3934bei0",
    "6cd9d02f08c818eca61fd40362855dce8157af0708460023710b2982053b2fbei0",
    "ff4f062a8e1fba6d5089a7517bfadf996a24a79181cfffa479fb5142227c0bbei0",
    "da23c5f3ca73c51fecdbfb7a77f028eb269bc438192e08fa7828850f7907b9bdi0",
    "804a382fe000066845dd2f53bb33d880dce201b0595da73843f115d85f789dbdi0",
    "3a837c80348691f965dbacf9414498c19eea184be8872509830ddc8e555611bdi0",
    "d87bdf8547ff587af6ab4e9ba58cfabd81e9dbae29ebee7f91ee4ce504b1e7bci0",
    "47f448eab72fa27e3ecd48cd9366f3900e13e3f385081a63027c3252452dcebci0",
    "f98248bd62d1893623d07789d2b77c76c726343272fe33cffd0598496792bfbci0",
    "9f4f89d78bf18eec65fad5a7e1e4b48023733678df1f831f762713aa28a7adbci0",
    "99603a91e9c394b8a08e41292afa612773054a1852ad50b70b926e8ed5ff98bci0",
    "ab9a8bc85f80436eb801f0b44525e735949b702b88165f276d9d5370a08792bci0",
    "68a66f966af6a8df8a697d026f53ac3d1bbf16fe60e4c00046c38ca42e4c6abci0",
    "f85395c84a44416973091c7b5b54093511a4e420d79b8a95f25392f60ee164bci0",
    "0d94b03575c0abcd9b50463402c57c05a8fb13fdc4838b3ea38fdb4214a93fbci0",
    "9101836abf01e3c2ec3b131bd392063aab15aafc15c83331e33bd5f27bddeabbi0",
    "a6b1b98105d3a8b6552e191e0bc300ac432bdba02b87d7e69cca7a5f22e9cebbi0",
    "38031a62f117119561f095109367359b1ec5b513cce605e99d3ad4fb3d73ccbbi0",
    "518505a149382542af4a249a0ea3e8393eb11baaf1e607bb7fa089ccb0acb7bbi0",
    "d6033366e191c597b5d060ccd11213625f7ca276a8dd3649db9463c401d654bbi0",
    "23c94df33db29f2068237528c50bccb9af14dabeb1b4c370c1ce2cfaf2bb12bbi0",
    "11407eeb6ecd4b5f721d3bdbb24d80c57bf978438466d44a37f4400dcf40dcbai0",
    "26fd15fe036f3ff842e060207150594d5327963a5af729d9d7bb37f9b27cc9bai0",
    "c85d49988d0a9e63b57a42b0f43b085ac848b4eec3c7567c6ff9835b28b7bfbai0",
    "6f9d8c063ebd8777d42609563d5a2753739ba9822afdbd3f30248aa3622c1bbai0",
    "59a5c6c8ebf33e8af27c5ca3a1fc34c6ec4a3933024431d74a7107c4cdb518bai0",
    "113a792a0665cc766fe1725e94da88af51d637f0b4b2d8bab8acefc60a7fa2b9i0",
    "74f75991f2f1f877c01834c8840778a67a66403ec6fe6db4889bd773a0c8f2b8i0",
    "1aea70b0b26f38543f5ac323c88287b8b128f275eac1b26e316a86e14bf6c4b8i0",
    "fb24445c829b8e9739be2153bf44f8962191c9ef470fa5a0a8cf6014d3939ab8i0",
    "4f7bd6fb95500aad569bd9772f49545f997ffed98782938e6d030d1f0ea482b8i0",
    "340716bc1585d9b57fc6c21e298caed04c84b27bd45873799b31b63d7fb965b8i0",
    "cc515eb5a3125b80a8d7a2ac8e0ba54206185715332ebe6434dfbc86661053b8i0",
    "e1ad8b866a5b25b67ccaf2b4e63eddb02b24e2a7abd8c3fd2c5d4ae488f83bb8i0",
    "bd723e4bc055e8a43d52e80041664b94dd24a7e1a1c4aa02f39841596a0d76b7i0",
    "45efc579e0fbbc539eeaf6fedc30fdd156fca6e32d7d0fff87c568b411a651b7i0",
    "6ff468ac685ea84a44977322e23371aee5c6eb75d35207a60dd8b43d32632db7i0",
    "9adda4d80df93b592ed215aee39da04fe4a43aec06a97f7228b483a747f4ebb6i0",
    "adf97725b496134ebfd0eaaceb63f23d94052a585f557206f33443c2d659e6b6i0",
    "41565db258d48adc4e0ff3467534890ee6a12beaed5378847667735affb8e2b6i0",
    "fab5be5f8860e29eb394e56bd0a668752c346d1bdda73dc6a2fc2e824a17dbb6i0",
    "1307d9531f2759ffcd125bdaf31ed9116c103a991a17d5b43b2e41a7e17460b6i0",
    "5494d587b738c901b727c39628d94eb021a836bd78e82b20f6e331ed5c2850b6i0",
    "6e98fb69311cf79bd271b13411df9e6b6138705fd08db20fe36a897eb4b513b6i0",
    "f6f5d494bd9211ec6b71e9270f4a87237647e7f655ce7c10392fe1c80d8affb5i0",
    "7fe37c78b2be6788af0fe810d5b6aedb1bb9c166b70667105e43de13234ee6b5i0",
    "8093e0684c094a22b23f328b1dbd50c487c3ab37bc230de456a12b7fde95bcb5i0",
    "7511c5ef23ab23f8e009e368b7954c4ed7e67a7a1cd94bae99b7d93a192a90b5i0",
    "c98658f7731c9b5342c6a51f0860fec09fbcab9867b986d4704736abf1b0f6b4i0",
    "b56001aa7fc59eb40068ea41e0f35a54f4d73c3483cd69ae0c26bb95dfc9e9b4i0",
    "4147fbe40586287b1e6144c066731e43959e1aa7d3c7c8ea301ee44fd0b37fb4i0",
    "3d43b7b45e4c0e062b21147be0ebdd68f9094f4e9c7b8a686aeb2948b40fbfb3i0",
    "6e66c9e03e18250806515a3a60e4a6012f37e87aa1446a679ade384c7e55a3b3i0",
    "8215caa5d781be0d5fae9ce7cb1a04efa17f82fb66cb2fa99e4c7bb1a2f479b3i0",
    "ce288cac29042474740fa477163767a0fcf74b228e48748630ac7193118429b3i0",
    "6d35d614a3574e85d80e27fdc5854a055c484dbf09f155411e279a839aa8ddb2i0",
    "906804e50f92a51329b5009d65e5f6e3c32e512279c835c3171ea6765eaca6b2i0",
    "70bd0c3531d62ab836187dd956e1e3fb7ef9903124b818a78e5ecd5198f5a3b2i0",
    "92c2668efad88467edded7ffc50fb05a063e7b2b555ccc2073f41d599bb037b2i0",
    "e97700fc461598ac01bcb2b74cde9ee31e608bfc7f53047e9e494697509f1fb2i0",
    "f9d7f767ae23e67ccb9ffd21d9f83ef9a7b6617f5988a08481e1f722de05d1b1i0",
    "262f07835303d1e3a8dce57c93488ed1512ad8ed633c9f129c1bc82535c99ab1i0",
    "4ea5e8e9cc2c7414d2652c8db87ef556b48e61d60f68cef9c319eb87566e3db1i0",
    "acfae264071fa0bb8bd7875e2d607ad48fac549c0817c2dba40858ee95571eb1i0",
    "ed150d8980b923b214b8ea115a31933bbebf82666f93c68a1e11ebd3fee3d9b0i0",
    "d9ea50a1c374d2feaf87a4ba82967aab419c1ecc4caac3964f69dac7323ca0b0i0"
  ],
  "more": true,
  "page_index": 9
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/inscriptions/block/&lt;BLOCKHEIGHT&gt;</b></code>
  </summary>

### Description

Get inscriptions for a specific block.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/inscriptions/block/767430
```

```json
{
  "ids": [
    "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
  ],
  "more": false,
  "page_index": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/install.sh</b></code>
  </summary>

### Description

Installs the latest pre-built binary of `ord`

### Example

See [wallet.md](wallet.md#installing-ord)
</details>

<details>
 <summary>
    <code>GET</code>
    <code><b>/output/&lt;OUTPOINT&gt;</b></code>
 </summary>

### Description

Returns information about a UTXO, including inscriptions within it.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/output/bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0
```

```json
{
  "address": "bc1pz4kvfpurqc2hwgrq0nwtfve2lfxvdpfcdpzc6ujchyr3ztj6gd9sfr6ayf",
  "confirmations": 6,
  "indexed": false,
  "inscriptions": [],
  "outpoint": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0",
  "runes": {},
  "sat_ranges": null,
  "script_pubkey": "OP_PUSHNUM_1 OP_PUSHBYTES_32 156cc4878306157720607cdcb4b32afa4cc6853868458d7258b907112e5a434b",
  "spent": true,
  "transaction": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed",
  "value": 10000
}
```
</details>

<details>
 <summary>
    <code>POST</code>
    <code><b>/outputs</b></code>
 </summary>

### Description

List information from a list of outputs.

### Example

```bash
curl -s -X POST \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '["bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0", "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:1"]' \
  http://0.0.0.0:80/outputs
```

```json
[
  {
    "address": "bc1pz4kvfpurqc2hwgrq0nwtfve2lfxvdpfcdpzc6ujchyr3ztj6gd9sfr6ayf",
    "indexed": false,
    "inscriptions": [],
    "outpoint": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0",
    "runes": {},
    "sat_ranges": null,
    "script_pubkey": "OP_PUSHNUM_1 OP_PUSHBYTES_32 156cc4878306157720607cdcb4b32afa4cc6853868458d7258b907112e5a434b",
    "spent": true,
    "transaction": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed",
    "value": 10000
  },
  {
    "address": "bc1pkc2cdnm6xermt2vzxg9wwcur5prgpl6pms3xf9ydtyax5pnqsgwqvuu5cq",
    "indexed": false,
    "inscriptions": [],
    "outpoint": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:1",
    "runes": {},
    "sat_ranges": null,
    "script_pubkey": "5120b61586cf7a3647b5a982320ae76383a04680ff41dc2264948d593a6a0660821c",
    "spent": true,
    "transaction": "bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed",
    "value": 483528
  }
]
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/outputs/&lt;ADDRESS&gt;</b></code>
  </summary>

### Description

Get UTXOs held by `<ADDRESS>`.

### Query Parameters

#### `type` (optional)

| Value       | Description |
|-------------|-------------|
| `any`       | return all UTXOs |
| `cardinal`  | return UTXOs not containing inscriptions or runes |
| `inscribed` | return UTXOs containing inscriptions |
| `runic`     | return UTXOs containing runes |

### Example

```bash
curl -s -H "Accept: application/json" \
  "http://0.0.0.0:80/outputs/358mMRwcxuCSkKheuVWaXHJBGKrXo3f6JW?type=cardinal"
```

```json
[
  {
    "address": "358mMRwcxuCSkKheuVWaXHJBGKrXo3f6JW",
    "indexed": true,
    "inscriptions": [],
    "outpoint": "6737d77ee9fba5f37e5f4128b03479209030bf44f78ffa3f4e94bf9783691b00:0",
    "runes": {},
    "sat_ranges": [
      [
        567775159437503,
        567775159443555
      ],
      [
        1266853954166100,
        1266853954177531
      ],
      [
        1210436862054339,
        1210436862084993
      ],
      [
        690914221328806,
        690914221362332
      ],
      [
        957021421066680,
        957021421075017
      ]
    ],
    "script_pubkey": "a91425c70777dfcf84ba7479483e262e1bc7bb0bf4d587",
    "spent": false,
    "transaction": "6737d77ee9fba5f37e5f4128b03479209030bf44f78ffa3f4e94bf9783691b00",
    "value": 90000
  },
  {
    "address": "358mMRwcxuCSkKheuVWaXHJBGKrXo3f6JW",
    "indexed": true,
    "inscriptions": [],
    "outpoint": "0cfa3e55f14812c119e47936d95abbb4e04f3094f6d86ac16c6e10018b0b2900:0",
    "runes": {},
    "sat_ranges": [
      [
        1773029001419378,
        1773029001509378
      ]
    ],
    "script_pubkey": "a91425c70777dfcf84ba7479483e262e1bc7bb0bf4d587",
    "spent": false,
    "transaction": "0cfa3e55f14812c119e47936d95abbb4e04f3094f6d86ac16c6e10018b0b2900",
    "value": 90000
  }
]
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/rune/&lt;RUNE&gt;</b></code>
  </summary>

### Description

Returns details about the specified rune. Requires index with `--index-runes` flag.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://localhost/rune/UNCOMMONGOODS
```

```json
{
  "entry": {
    "block": 1,
    "burned": 139,
    "divisibility": 0,
    "etching": "0000000000000000000000000000000000000000000000000000000000000000",
    "mints": 33891693,
    "number": 0,
    "premine": 0,
    "spaced_rune": "UNCOMMON•GOODS",
    "symbol": "⧉",
    "terms": {
      "amount": 1,
      "cap": 340282366920938463463374607431768211455,
      "height": [
        840000,
        1050000
      ],
      "offset": [
        null,
        null
      ]
    },
    "timestamp": 0,
    "turbo": true
  },
  "id": "1:0",
  "mintable": true,
  "parent": null
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/runes</b></code>
  </summary>

### Description

Returns details for last 100 inscribed runes.  Requires index with `--index-runes` flag.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/runes
```

```json
{
  "entries": [
    [
      "864348:823",
      {
        "block": 864348,
        "burned": 0,
        "divisibility": 0,
        "etching": "645431123f5ff8b92d057803f2ba786689fd04f2d968d8fb6a4162b63cabc4fd",
        "mints": 0,
        "number": 119793,
        "premine": 0,
        "spaced_rune": "ZKSKOOUGYPXB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 87187755,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728166072,
        "turbo": false
      }
    ],
    [
      "864348:822",
      {
        "block": 864348,
        "burned": 0,
        "divisibility": 0,
        "etching": "9d3a1200adfcb2e0ef07e4975120980befcc265cd85b9f2300bc12d4a1ab1beb",
        "mints": 0,
        "number": 119792,
        "premine": 0,
        "spaced_rune": "VEMRWZCGQRLL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 183543298,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728166072,
        "turbo": false
      }
    ],
    [
      "864346:427",
      {
        "block": 864346,
        "burned": 0,
        "divisibility": 0,
        "etching": "2acaba44a6dc31cc5f8a8f4ee3a10eb9ca74e47d62975709cb8e81723d91a20d",
        "mints": 0,
        "number": 119791,
        "premine": 0,
        "spaced_rune": "LBQPCHACURXD",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 12894945,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728165011,
        "turbo": false
      }
    ],
    [
      "864343:2413",
      {
        "block": 864343,
        "burned": 0,
        "divisibility": 0,
        "etching": "6698cd13f630107ccc4b3058cc09b1718aa435e8f9c4eba6b08eea5d13ee809b",
        "mints": 0,
        "number": 119790,
        "premine": 1000000000,
        "spaced_rune": "BABY•LEN•SASSAMAN",
        "symbol": "Ⱡ",
        "terms": {
          "amount": 100000,
          "cap": 11000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728162943,
        "turbo": false
      }
    ],
    [
      "864342:2591",
      {
        "block": 864342,
        "burned": 0,
        "divisibility": 1,
        "etching": "095513866c6e7aca84a39f403caac493eaa2f53eda848aaee3e96463571ec6d6",
        "mints": 0,
        "number": 119789,
        "premine": 30000,
        "spaced_rune": "COMPLETED•IT•MATE",
        "symbol": "⚽",
        "terms": {
          "amount": 100,
          "cap": 299999700,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728162376,
        "turbo": true
      }
    ],
    [
      "864338:4768",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "0d04505188efc69d4e2cb389607663ff556c062e1e2f8c890bfc598c637700ab",
        "mints": 0,
        "number": 119788,
        "premine": 0,
        "spaced_rune": "IJEIKMFKELRFRGRGRGEFREFGR",
        "symbol": "d",
        "terms": {
          "amount": 211,
          "cap": 554553,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864338:4767",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "e0490721505254c83a69ce1411b1659b6ecd0690751cf43ac45240ca7d3ab4fb",
        "mints": 0,
        "number": 119787,
        "premine": 0,
        "spaced_rune": "CQHMUFFTWWPF",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 14372222,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864338:4766",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "ada836a0e9c834977161543ba7bace0b552e55f88da0398626b1c49a170502dd",
        "mints": 0,
        "number": 119786,
        "premine": 0,
        "spaced_rune": "KJMKPVMKREMVBVBFBVFD",
        "symbol": "3",
        "terms": {
          "amount": 332,
          "cap": 211222,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864337:4402",
      {
        "block": 864337,
        "burned": 0,
        "divisibility": 0,
        "etching": "ed45aaf2e9b82d55e35a8d0654d0bb044d1d3e2fdd3eb8787d572854316c53c2",
        "mints": 0,
        "number": 119785,
        "premine": 0,
        "spaced_rune": "JNJKMLKMNJCMPMCESCVDSV•DV",
        "symbol": "2",
        "terms": {
          "amount": 3222,
          "cap": 1111111,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160097,
        "turbo": false
      }
    ],
    [
      "864335:913",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "435cc412c946ced0a5ae5a50ee41d2b541f06f09b6f587619507dfbcc61b8842",
        "mints": 0,
        "number": 119784,
        "premine": 0,
        "spaced_rune": "UOBYCVAGPLNO",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 194090811,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:912",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "79d77e44d66af6ec82ff7970eb3f15b9537408e3888ed0348a265810e99ddd3a",
        "mints": 0,
        "number": 119783,
        "premine": 0,
        "spaced_rune": "YNJMQPGPUGWN",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 71782828,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:910",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "b014db8f651ec05a1f261f3569c66973318787ad4c7410d6677fc6fcc45e5cfe",
        "mints": 0,
        "number": 119782,
        "premine": 0,
        "spaced_rune": "FDLQGMGRYAMF",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 135966360,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:909",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "bd649ba830b262ddcf24b0d6da5091f2dbf1276af26ad0809b65a95c42ddbec2",
        "mints": 0,
        "number": 119781,
        "premine": 0,
        "spaced_rune": "LBPOUDNUAIDK",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 128338720,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:908",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "4ee02e12ba76c8c85208510e078810efbb3843fdaa1323d4e84f40a753d97380",
        "mints": 0,
        "number": 119780,
        "premine": 0,
        "spaced_rune": "RNVHGUYHAUCM",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 3346818,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:907",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "c9b47a71a2a552450f6259262fc0c23c45148fccb52ee32cd5bb668a467a9f5d",
        "mints": 0,
        "number": 119779,
        "premine": 0,
        "spaced_rune": "RTSQQFKTEEBX",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 85692692,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:906",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "df772301fef3107549d200fea54f47e46d6aae197f85e93b0068749640028055",
        "mints": 0,
        "number": 119778,
        "premine": 0,
        "spaced_rune": "IWHXSPKPYQOX",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 166869547,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:905",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "186049ed6091d0a4d9e1abf6d436a6af7bc7603a33c71031b8bb0ba02f386b3a",
        "mints": 0,
        "number": 119777,
        "premine": 0,
        "spaced_rune": "OHDKZWZHYLVL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 189310557,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:904",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e72d9c58ce6300807d1ca6343fa95f5fa34f3d7e29fc95a94b553ff4c66b36",
        "mints": 0,
        "number": 119776,
        "premine": 0,
        "spaced_rune": "NSZNPZDDFYCT",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 72959668,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:386",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "76e81c2a204074d61869f58ce86bf8ecfe66f1213bd444c4f22c6f638a401ef9",
        "mints": 0,
        "number": 119775,
        "premine": 0,
        "spaced_rune": "NTOOWMNTOOWMNTOOWM",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1000000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864334:4073",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "6c132c6b69ff19d3dbbd0165bcf2fb5db9bba717824a3ff93e94e976b7da5f9e",
        "mints": 0,
        "number": 119774,
        "premine": 0,
        "spaced_rune": "HIDDEN•SELDOM•DISEASE•WISE",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1127,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:4070",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "adcbc4dc91e0b354baacb37be52e187fab2cf619c43f0675b26c5e7d58ad1ded",
        "mints": 0,
        "number": 119773,
        "premine": 0,
        "spaced_rune": "TYDSJXISYECCOQYYSS",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 2361833545833,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:762",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "259fc5e99770c5d2ed0547571981ad191554282e6ab4b2a6eb4083c392edc1cb",
        "mints": 0,
        "number": 119772,
        "premine": 0,
        "spaced_rune": "BEGCOAJVXEHW",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 38385326,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:433",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "4d324233f38c0cbf36bf1a76e161cbe0ff9f0efb6ee78d94dffdd5f16ec7e8ba",
        "mints": 0,
        "number": 119771,
        "premine": 0,
        "spaced_rune": "BEDIALAMDARBEDIALAMDAR",
        "symbol": null,
        "terms": {
          "amount": 5,
          "cap": 100000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:432",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "f7b804462b33fd468ef3b171071094f3498968b0a488d08489e16058d470d809",
        "mints": 0,
        "number": 119770,
        "premine": 0,
        "spaced_rune": "RUTHMARTINRUTHMARTIN",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 999999,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:431",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "51ce542a9557a4894b0dfd705d13268682aa16c83e5eee9c5b1ba4d67113def8",
        "mints": 0,
        "number": 119769,
        "premine": 0,
        "spaced_rune": "ULTIVERSEULTIVERSE",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 7777777,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:182",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "195dc952cb7c9e8a5c370fe098b4aa1d8bba8225bb4706ee7243b8e3c43f2b32",
        "mints": 0,
        "number": 119768,
        "premine": 0,
        "spaced_rune": "NUQHRKVWSYEA",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 3063483,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864333:3461",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "65078629f16f0ce11a91da3de877a0ac5a25b5ed4c68d0ba3f6a8e75eab5f871",
        "mints": 0,
        "number": 119767,
        "premine": 0,
        "spaced_rune": "FMTJRFVGNHVZNUCB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 5541274870406,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3458",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "8471194b68cfab89a9d6112caf62f97819172d397e91674ec5413ad8f27b2828",
        "mints": 0,
        "number": 119766,
        "premine": 0,
        "spaced_rune": "WEELZZLGHGDRTO",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 507317119633,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3440",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "90d530d1daf7f1f6ece388a846fe8173a427f71b7e1c5cfc1c035dcd1fc0b017",
        "mints": 0,
        "number": 119765,
        "premine": 0,
        "spaced_rune": "MIIOBBPODENFJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 503174265447,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3437",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "5c0d2bbf9543cd50293fd6671d94502fa08c8c6d11431e0eee4ac3aedbdbc5bc",
        "mints": 0,
        "number": 119764,
        "premine": 0,
        "spaced_rune": "TASTE•RISING•FULL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 4812,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3434",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "1766810d3f53cfce81a4e0620c21e8e4643c7a40936dbafa6e88339c025fb5f6",
        "mints": 0,
        "number": 119763,
        "premine": 0,
        "spaced_rune": "REGION•MARK•LOW",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 2470,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3433",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "f2a6805462cebffc6eb5855d1205dedf9c7f746a7dfd420c153011bb572f58ba",
        "mints": 0,
        "number": 119762,
        "premine": 0,
        "spaced_rune": "QHKKEWPTDMNB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 53660832,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3432",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "5d2127d84533fc9d486eaec1a2b76b2d349fe63a06a9d14847b667d360af6e19",
        "mints": 0,
        "number": 119761,
        "premine": 0,
        "spaced_rune": "IWLUKGYIWMBP",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 94339731,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3431",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "ac4668d63f66c94515dbc2a74faa9152018758a75432cc085a7e7638a24cbc12",
        "mints": 0,
        "number": 119760,
        "premine": 0,
        "spaced_rune": "KWUFVEOJVKGQ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 196312580,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:2714",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "c642cd4cc7a075c61d3a32b949217990aa91dfc928f12a2cdba1f2f228c699c7",
        "mints": 26,
        "number": 119759,
        "premine": 210000,
        "spaced_rune": "BOUNCE•THE•BITCOIN•CAT",
        "symbol": "🐱",
        "terms": {
          "amount": 1000,
          "cap": 20790,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:2482",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "cc2415293c275bea4d73ff8f45f68f269686b819de447f50ec6988ac04a62d1b",
        "mints": 0,
        "number": 119758,
        "premine": 30000000,
        "spaced_rune": "BITCAT•IS•IN•CONTROL",
        "symbol": "🐈",
        "terms": {
          "amount": 5000,
          "cap": 194000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:2462",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a75f792be155a0b53691289433a6413c1efb1aeaf970f752ee70be3c6e755a06",
        "mints": 0,
        "number": 119757,
        "premine": 0,
        "spaced_rune": "FIRST•CAT•EATING•BITCOINER",
        "symbol": "🙀",
        "terms": {
          "amount": 1000,
          "cap": 21000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:1142",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "7488c2909e2bb5f39fb836ee1e18c23487d078e48e2420cc11776c8d7931fea5",
        "mints": 0,
        "number": 119756,
        "premine": 0,
        "spaced_rune": "AI•CRYPTO•AI•CRYPTO",
        "symbol": "A",
        "terms": {
          "amount": 1000,
          "cap": 1,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1140",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a024e2d4c4e15eab941376a954bb9176bc95990ba6b2a6d31e5b7c26cd8d7e7c",
        "mints": 0,
        "number": 119755,
        "premine": 0,
        "spaced_rune": "SACMKSOKCMPOKMWCLWMCLWCDWC",
        "symbol": "c",
        "terms": {
          "amount": 221,
          "cap": 2111111,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1136",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "d6358e0601130c5ebbdb535aa93bbe2e752fd7fd6eee8601fe5af29e7ff179e1",
        "mints": 0,
        "number": 119754,
        "premine": 0,
        "spaced_rune": "XQOFVAHHLCQR",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 94964916,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1135",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "62aa2bd48b0eb8a1c3bb090c6129bdc52a2348f3b8e25a2e2eeaa27313e242af",
        "mints": 0,
        "number": 119753,
        "premine": 0,
        "spaced_rune": "YEPWCVNODTII",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 39185064,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1134",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "e3e6a144d3ac57d35f7f141f79ea818bd26a78bf900c2d0aeaa2a95ce68f8c9e",
        "mints": 0,
        "number": 119752,
        "premine": 0,
        "spaced_rune": "SDFGJUJTYHTGRSFAD",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 5,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1133",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "97600a89179c0bfd4b7c69bc5f4e9fc2f206124fbc08d4872f18ac6be29a525e",
        "mints": 0,
        "number": 119751,
        "premine": 0,
        "spaced_rune": "XQEKAAGEYDXY",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 147617461,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1131",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "8ecabca3a2b1518c67c5ee41c93e7874d1117edfd0b36e46ea68eb83e6f9eaad",
        "mints": 0,
        "number": 119750,
        "premine": 0,
        "spaced_rune": "XFHSGMZJEUML",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1014672,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1130",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "77b1518d7ad77d89118eeb8eb92c120e1732d2e7ce9d6780cda180f5f4968df6",
        "mints": 0,
        "number": 119749,
        "premine": 0,
        "spaced_rune": "DJLNUHRYYTGR",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 146717679,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1129",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "4da09c158447950fabd281c7910c6e3f251b9b9a98ab7058e2f4b26304e332ee",
        "mints": 0,
        "number": 119748,
        "premine": 0,
        "spaced_rune": "CBAQVALKVMYP",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 181932658,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1128",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "b947075130f5a5f93a5cdfa9a216c76b761ff7cd2fb7ca677b3d00a3ca5d53e0",
        "mints": 0,
        "number": 119747,
        "premine": 0,
        "spaced_rune": "POJSRGWQBBWQ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 100105873,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1127",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a356dd06600bb163cb4d68bbe601f83d987c3c2cd456e3784616ab297d1843c0",
        "mints": 0,
        "number": 119746,
        "premine": 0,
        "spaced_rune": "FMPQPSLKENKY",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 82531312,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1126",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "b6ecdb27bb269949f58ace2ba162726483070e80c140dc60329b5fdbbd3e6395",
        "mints": 0,
        "number": 119745,
        "premine": 0,
        "spaced_rune": "GOARBTCEASGJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 99967467,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1125",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "abf680ed211d18428ddda208f164539fbf662705bd88d4041575c53e655ed794",
        "mints": 0,
        "number": 119744,
        "premine": 0,
        "spaced_rune": "MNBIUEEAKPBJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 168164931,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1124",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
          "height": [
            null,
            null
        "terms": {
          "amount": 1,
          "cap": 29807122,
          "height": [
            null,
            null
          "height": [
            null,
            null
            null
          ],
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ]
  ],
  "more": true,
  "prev": null,
  "next": 1
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/runes/&lt;PAGE&gt;</b></code>
  </summary>

### Description

Pagination allows you to specify which page of 100 runes you'd like to return.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/runes/0
```

```json
{
  "entries": [
    [
      "864348:823",
      {
        "block": 864348,
        "burned": 0,
        "divisibility": 0,
        "etching": "645431123f5ff8b92d057803f2ba786689fd04f2d968d8fb6a4162b63cabc4fd",
        "mints": 0,
        "number": 119793,
        "premine": 0,
        "spaced_rune": "ZKSKOOUGYPXB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 87187755,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728166072,
        "turbo": false
      }
    ],
    [
      "864348:822",
      {
        "block": 864348,
        "burned": 0,
        "divisibility": 0,
        "etching": "9d3a1200adfcb2e0ef07e4975120980befcc265cd85b9f2300bc12d4a1ab1beb",
        "mints": 0,
        "number": 119792,
        "premine": 0,
        "spaced_rune": "VEMRWZCGQRLL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 183543298,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728166072,
        "turbo": false
      }
    ],
    [
      "864346:427",
      {
        "block": 864346,
        "burned": 0,
        "divisibility": 0,
        "etching": "2acaba44a6dc31cc5f8a8f4ee3a10eb9ca74e47d62975709cb8e81723d91a20d",
        "mints": 0,
        "number": 119791,
        "premine": 0,
        "spaced_rune": "LBQPCHACURXD",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 12894945,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728165011,
        "turbo": false
      }
    ],
    [
      "864343:2413",
      {
        "block": 864343,
        "burned": 0,
        "divisibility": 0,
        "etching": "6698cd13f630107ccc4b3058cc09b1718aa435e8f9c4eba6b08eea5d13ee809b",
        "mints": 0,
        "number": 119790,
        "premine": 1000000000,
        "spaced_rune": "BABY•LEN•SASSAMAN",
        "symbol": "Ⱡ",
        "terms": {
          "amount": 100000,
          "cap": 11000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728162943,
        "turbo": false
      }
    ],
    [
      "864342:2591",
      {
        "block": 864342,
        "burned": 0,
        "divisibility": 1,
        "etching": "095513866c6e7aca84a39f403caac493eaa2f53eda848aaee3e96463571ec6d6",
        "mints": 0,
        "number": 119789,
        "premine": 30000,
        "spaced_rune": "COMPLETED•IT•MATE",
        "symbol": "⚽",
        "terms": {
          "amount": 100,
          "cap": 299999700,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728162376,
        "turbo": true
      }
    ],
    [
      "864338:4768",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "0d04505188efc69d4e2cb389607663ff556c062e1e2f8c890bfc598c637700ab",
        "mints": 0,
        "number": 119788,
        "premine": 0,
        "spaced_rune": "IJEIKMFKELRFRGRGRGEFREFGR",
        "symbol": "d",
        "terms": {
          "amount": 211,
          "cap": 554553,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864338:4767",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "e0490721505254c83a69ce1411b1659b6ecd0690751cf43ac45240ca7d3ab4fb",
        "mints": 0,
        "number": 119787,
        "premine": 0,
        "spaced_rune": "CQHMUFFTWWPF",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 14372222,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864338:4766",
      {
        "block": 864338,
        "burned": 0,
        "divisibility": 0,
        "etching": "ada836a0e9c834977161543ba7bace0b552e55f88da0398626b1c49a170502dd",
        "mints": 0,
        "number": 119786,
        "premine": 0,
        "spaced_rune": "KJMKPVMKREMVBVBFBVFD",
        "symbol": "3",
        "terms": {
          "amount": 332,
          "cap": 211222,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160156,
        "turbo": false
      }
    ],
    [
      "864337:4402",
      {
        "block": 864337,
        "burned": 0,
        "divisibility": 0,
        "etching": "ed45aaf2e9b82d55e35a8d0654d0bb044d1d3e2fdd3eb8787d572854316c53c2",
        "mints": 0,
        "number": 119785,
        "premine": 0,
        "spaced_rune": "JNJKMLKMNJCMPMCESCVDSV•DV",
        "symbol": "2",
        "terms": {
          "amount": 3222,
          "cap": 1111111,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728160097,
        "turbo": false
      }
    ],
    [
      "864335:913",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "435cc412c946ced0a5ae5a50ee41d2b541f06f09b6f587619507dfbcc61b8842",
        "mints": 0,
        "number": 119784,
        "premine": 0,
        "spaced_rune": "UOBYCVAGPLNO",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 194090811,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:912",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "79d77e44d66af6ec82ff7970eb3f15b9537408e3888ed0348a265810e99ddd3a",
        "mints": 0,
        "number": 119783,
        "premine": 0,
        "spaced_rune": "YNJMQPGPUGWN",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 71782828,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:910",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "b014db8f651ec05a1f261f3569c66973318787ad4c7410d6677fc6fcc45e5cfe",
        "mints": 0,
        "number": 119782,
        "premine": 0,
        "spaced_rune": "FDLQGMGRYAMF",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 135966360,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:909",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "bd649ba830b262ddcf24b0d6da5091f2dbf1276af26ad0809b65a95c42ddbec2",
        "mints": 0,
        "number": 119781,
        "premine": 0,
        "spaced_rune": "LBPOUDNUAIDK",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 128338720,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:908",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "4ee02e12ba76c8c85208510e078810efbb3843fdaa1323d4e84f40a753d97380",
        "mints": 0,
        "number": 119780,
        "premine": 0,
        "spaced_rune": "RNVHGUYHAUCM",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 3346818,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:907",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "c9b47a71a2a552450f6259262fc0c23c45148fccb52ee32cd5bb668a467a9f5d",
        "mints": 0,
        "number": 119779,
        "premine": 0,
        "spaced_rune": "RTSQQFKTEEBX",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 85692692,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:906",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "df772301fef3107549d200fea54f47e46d6aae197f85e93b0068749640028055",
        "mints": 0,
        "number": 119778,
        "premine": 0,
        "spaced_rune": "IWHXSPKPYQOX",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 166869547,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:905",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "186049ed6091d0a4d9e1abf6d436a6af7bc7603a33c71031b8bb0ba02f386b3a",
        "mints": 0,
        "number": 119777,
        "premine": 0,
        "spaced_rune": "OHDKZWZHYLVL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 189310557,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:904",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e72d9c58ce6300807d1ca6343fa95f5fa34f3d7e29fc95a94b553ff4c66b36",
        "mints": 0,
        "number": 119776,
        "premine": 0,
        "spaced_rune": "NSZNPZDDFYCT",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 72959668,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864335:386",
      {
        "block": 864335,
        "burned": 0,
        "divisibility": 0,
        "etching": "76e81c2a204074d61869f58ce86bf8ecfe66f1213bd444c4f22c6f638a401ef9",
        "mints": 0,
        "number": 119775,
        "premine": 0,
        "spaced_rune": "NTOOWMNTOOWMNTOOWM",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1000000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158372,
        "turbo": false
      }
    ],
    [
      "864334:4073",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "6c132c6b69ff19d3dbbd0165bcf2fb5db9bba717824a3ff93e94e976b7da5f9e",
        "mints": 0,
        "number": 119774,
        "premine": 0,
        "spaced_rune": "HIDDEN•SELDOM•DISEASE•WISE",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1127,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:4070",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "adcbc4dc91e0b354baacb37be52e187fab2cf619c43f0675b26c5e7d58ad1ded",
        "mints": 0,
        "number": 119773,
        "premine": 0,
        "spaced_rune": "TYDSJXISYECCOQYYSS",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 2361833545833,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:762",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "259fc5e99770c5d2ed0547571981ad191554282e6ab4b2a6eb4083c392edc1cb",
        "mints": 0,
        "number": 119772,
        "premine": 0,
        "spaced_rune": "BEGCOAJVXEHW",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 38385326,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:433",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "4d324233f38c0cbf36bf1a76e161cbe0ff9f0efb6ee78d94dffdd5f16ec7e8ba",
        "mints": 0,
        "number": 119771,
        "premine": 0,
        "spaced_rune": "BEDIALAMDARBEDIALAMDAR",
        "symbol": null,
        "terms": {
          "amount": 5,
          "cap": 100000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:432",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "f7b804462b33fd468ef3b171071094f3498968b0a488d08489e16058d470d809",
        "mints": 0,
        "number": 119770,
        "premine": 0,
        "spaced_rune": "RUTHMARTINRUTHMARTIN",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 999999,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:431",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "51ce542a9557a4894b0dfd705d13268682aa16c83e5eee9c5b1ba4d67113def8",
        "mints": 0,
        "number": 119769,
        "premine": 0,
        "spaced_rune": "ULTIVERSEULTIVERSE",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 7777777,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864334:182",
      {
        "block": 864334,
        "burned": 0,
        "divisibility": 0,
        "etching": "195dc952cb7c9e8a5c370fe098b4aa1d8bba8225bb4706ee7243b8e3c43f2b32",
        "mints": 0,
        "number": 119768,
        "premine": 0,
        "spaced_rune": "NUQHRKVWSYEA",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 3063483,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728158138,
        "turbo": false
      }
    ],
    [
      "864333:3461",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "65078629f16f0ce11a91da3de877a0ac5a25b5ed4c68d0ba3f6a8e75eab5f871",
        "mints": 0,
        "number": 119767,
        "premine": 0,
        "spaced_rune": "FMTJRFVGNHVZNUCB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 5541274870406,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3458",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "8471194b68cfab89a9d6112caf62f97819172d397e91674ec5413ad8f27b2828",
        "mints": 0,
        "number": 119766,
        "premine": 0,
        "spaced_rune": "WEELZZLGHGDRTO",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 507317119633,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3440",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "90d530d1daf7f1f6ece388a846fe8173a427f71b7e1c5cfc1c035dcd1fc0b017",
        "mints": 0,
        "number": 119765,
        "premine": 0,
        "spaced_rune": "MIIOBBPODENFJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 503174265447,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3437",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "5c0d2bbf9543cd50293fd6671d94502fa08c8c6d11431e0eee4ac3aedbdbc5bc",
        "mints": 0,
        "number": 119764,
        "premine": 0,
        "spaced_rune": "TASTE•RISING•FULL",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 4812,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3434",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "1766810d3f53cfce81a4e0620c21e8e4643c7a40936dbafa6e88339c025fb5f6",
        "mints": 0,
        "number": 119763,
        "premine": 0,
        "spaced_rune": "REGION•MARK•LOW",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 2470,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3433",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "f2a6805462cebffc6eb5855d1205dedf9c7f746a7dfd420c153011bb572f58ba",
        "mints": 0,
        "number": 119762,
        "premine": 0,
        "spaced_rune": "QHKKEWPTDMNB",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 53660832,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3432",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "5d2127d84533fc9d486eaec1a2b76b2d349fe63a06a9d14847b667d360af6e19",
        "mints": 0,
        "number": 119761,
        "premine": 0,
        "spaced_rune": "IWLUKGYIWMBP",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 94339731,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:3431",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "ac4668d63f66c94515dbc2a74faa9152018758a75432cc085a7e7638a24cbc12",
        "mints": 0,
        "number": 119760,
        "premine": 0,
        "spaced_rune": "KWUFVEOJVKGQ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 196312580,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:2714",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "c642cd4cc7a075c61d3a32b949217990aa91dfc928f12a2cdba1f2f228c699c7",
        "mints": 26,
        "number": 119759,
        "premine": 210000,
        "spaced_rune": "BOUNCE•THE•BITCOIN•CAT",
        "symbol": "🐱",
        "terms": {
          "amount": 1000,
          "cap": 20790,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:2482",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "cc2415293c275bea4d73ff8f45f68f269686b819de447f50ec6988ac04a62d1b",
        "mints": 0,
        "number": 119758,
        "premine": 30000000,
        "spaced_rune": "BITCAT•IS•IN•CONTROL",
        "symbol": "🐈",
        "terms": {
          "amount": 5000,
          "cap": 194000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:2462",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a75f792be155a0b53691289433a6413c1efb1aeaf970f752ee70be3c6e755a06",
        "mints": 0,
        "number": 119757,
        "premine": 0,
        "spaced_rune": "FIRST•CAT•EATING•BITCOINER",
        "symbol": "🙀",
        "terms": {
          "amount": 1000,
          "cap": 21000,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": true
      }
    ],
    [
      "864333:1142",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "7488c2909e2bb5f39fb836ee1e18c23487d078e48e2420cc11776c8d7931fea5",
        "mints": 0,
        "number": 119756,
        "premine": 0,
        "spaced_rune": "AI•CRYPTO•AI•CRYPTO",
        "symbol": "A",
        "terms": {
          "amount": 1000,
          "cap": 1,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1140",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a024e2d4c4e15eab941376a954bb9176bc95990ba6b2a6d31e5b7c26cd8d7e7c",
        "mints": 0,
        "number": 119755,
        "premine": 0,
        "spaced_rune": "SACMKSOKCMPOKMWCLWMCLWCDWC",
        "symbol": "c",
        "terms": {
          "amount": 221,
          "cap": 2111111,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1136",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "d6358e0601130c5ebbdb535aa93bbe2e752fd7fd6eee8601fe5af29e7ff179e1",
        "mints": 0,
        "number": 119754,
        "premine": 0,
        "spaced_rune": "XQOFVAHHLCQR",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 94964916,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1135",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "62aa2bd48b0eb8a1c3bb090c6129bdc52a2348f3b8e25a2e2eeaa27313e242af",
        "mints": 0,
        "number": 119753,
        "premine": 0,
        "spaced_rune": "YEPWCVNODTII",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 39185064,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1134",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "e3e6a144d3ac57d35f7f141f79ea818bd26a78bf900c2d0aeaa2a95ce68f8c9e",
        "mints": 0,
        "number": 119752,
        "premine": 0,
        "spaced_rune": "SDFGJUJTYHTGRSFAD",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 5,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1133",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "97600a89179c0bfd4b7c69bc5f4e9fc2f206124fbc08d4872f18ac6be29a525e",
        "mints": 0,
        "number": 119751,
        "premine": 0,
        "spaced_rune": "XQEKAAGEYDXY",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 147617461,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1131",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "8ecabca3a2b1518c67c5ee41c93e7874d1117edfd0b36e46ea68eb83e6f9eaad",
        "mints": 0,
        "number": 119750,
        "premine": 0,
        "spaced_rune": "XFHSGMZJEUML",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 1014672,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1130",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "77b1518d7ad77d89118eeb8eb92c120e1732d2e7ce9d6780cda180f5f4968df6",
        "mints": 0,
        "number": 119749,
        "premine": 0,
        "spaced_rune": "DJLNUHRYYTGR",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 146717679,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1129",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "4da09c158447950fabd281c7910c6e3f251b9b9a98ab7058e2f4b26304e332ee",
        "mints": 0,
        "number": 119748,
        "premine": 0,
        "spaced_rune": "CBAQVALKVMYP",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 181932658,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1128",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "b947075130f5a5f93a5cdfa9a216c76b761ff7cd2fb7ca677b3d00a3ca5d53e0",
        "mints": 0,
        "number": 119747,
        "premine": 0,
        "spaced_rune": "POJSRGWQBBWQ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 100105873,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1127",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "a356dd06600bb163cb4d68bbe601f83d987c3c2cd456e3784616ab297d1843c0",
        "mints": 0,
        "number": 119746,
        "premine": 0,
        "spaced_rune": "FMPQPSLKENKY",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 82531312,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1126",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "b6ecdb27bb269949f58ace2ba162726483070e80c140dc60329b5fdbbd3e6395",
        "mints": 0,
        "number": 119745,
        "premine": 0,
        "spaced_rune": "GOARBTCEASGJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 99967467,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1125",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "abf680ed211d18428ddda208f164539fbf662705bd88d4041575c53e655ed794",
        "mints": 0,
        "number": 119744,
        "premine": 0,
        "spaced_rune": "MNBIUEEAKPBJ",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 168164931,
          "height": [
            null,
            null
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ],
    [
      "864333:1124",
      {
        "block": 864333,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "burned": 0,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "divisibility": 0,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "etching": "74e290bc2ed6b39c887ab3b456f86d91edbadb829936c63bb166d42233527491",
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
        "mints": 0,
        "number": 119743,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
        "premine": 0,
        "spaced_rune": "CWTYCFSOTBSU",
        "symbol": null,
        "terms": {
          "amount": 1,
          "cap": 29807122,
          "height": [
            null,
            null
        "terms": {
          "amount": 1,
          "cap": 29807122,
          "height": [
            null,
            null
          "height": [
            null,
            null
            null
          ],
          ],
          "offset": [
            null,
            null
          ]
        },
        "timestamp": 1728157837,
        "turbo": false
      }
    ]
  ],
  "more": true,
  "prev": null,
  "next": 1
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/sat/&lt;SAT&gt;</b></code>
  </summary>

### Description

Returns details about a specific satoshi. Requires index with `--index-sats` flag.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/sat/2099994106992659
```

```json
{
  "block": 3891094,
  "charms": [],
  "cycle": 3,
  "decimal": "3891094.16797",
  "degree": "3°111094′214″16797‴",
  "epoch": 18,
  "inscriptions": [],
  "name": "satoshi",
  "number": 2099994106992659,
  "offset": 16797,
  "percentile": "99.99971949060254%",
  "period": 1930,
  "rarity": "common",
  "satpoint": null,
  "timestamp": 3544214021
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/status</b></code>
  </summary>

### Description

Returns details about the server installation and index.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/status
```

```json
{
  "address_index": true,
  "blessed_inscriptions": 76332641,
  "chain": "mainnet",
  "cursed_inscriptions": 472043,
  "height": 864351,
  "initial_sync_time": {
    "secs": 59213,
    "nanos": 979632000
  },
  "inscriptions": 76804684,
  "lost_sats": 0,
  "minimum_rune_for_next_block": "PVHGFEDCAZZ",
  "rune_index": true,
  "runes": 119811,
  "sat_index": false,
  "started": "2024-09-27T17:43:39.291876400Z",
  "transaction_index": false,
  "unrecoverably_reorged": false,
  "uptime": {
    "secs": 709843,
    "nanos": 910346200
  }
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/tx/&lt;TRANSACTION_ID&gt;</b></code>
  </summary>

### Description

Returns details about the specified transaction.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/tx/99811de396ff10152cdfc9588d9750d0151501f081df2e56071c42dc3532b743
```

```json
{
  "chain": "mainnet",
  "etching": null,
  "inscription_count": 1,
  "transaction": {
    "version": 2,
    "lock_time": 0,
    "input": [
      {
        "previous_output": "7d154f826f68e86370105641e3b5b1c6afc697613b8dfce48e4e40db01e8317a:1",
        "script_sig": "",
        "sequence": 4294967295,
        "witness": [
          "e68e67c60dfc06f570dfa8fe880cc09f1041a9b10b285743dd72b8f2e672987f7176ced40d46d279385c148a0c39b9914b91d9d503b7388791f6758884f0c2f4",
          "200c97fe0e7bb78d8dd7447bc098386c61248e1e9a7dfd263fd828c5373b945735ac0063036f7264010109696d6167652f706e67004d080289504e470d0a1a0a0000000d49484452000000360000003608060000008c456add000000017352474200aece1ce900000306494441546881ed9abf4e1b4110c63f477903a4b4a6a04186c21532a54de1c214a432051406d14471830405a2b25cb8701344e75c43a4d0514041112843a8281c642972111ec0cfb029ec3976d7b7e7fd670cd1fe24747b6b666ebf9bbdd9b9b581402010083c93f1e5a8130d18b5776b73defcdae26500a56a8df1e73fbe7f1d3acf6404ff2f29fe9d0f27c5fcba705cdbdcc1dae60ed8086028aad1aea0d1ae18f96612ba76ef8daea24131bf8edb874b004381fd6e0f8c3136bfb46aec4bb6fbfbfba7b6ad97881d1d6e6446471c1d6e089f2d2c2f627e69d56850c0b82853bc08234ad51a00e0e63c12fa49dcf1fe95961f5ed4d3e31d9e1eef70bc7f35f6cca6e155188f2c0e001aed4aaab84e34609d6820881af51b271b6f998932a32c88a20800fd6e2f6e2709e4138b8b28600ac94397b4ece82a0a98e25424f8082e2c2fc66d1abc0cf59fd64f9cd6ba99450c508bf3c1d423362bbc444c2ea9542465ca69613d879bad0b461506210d5cf09dcd1562f17bdb07906d937cb8240f2b233e42095150facce60a8c4f202a7c88339e8aa56a8d513d68220a1866452a942962fcda46647305e7c462953c14d348ebaede3e5ca68a22b2b9020060b73627bceee862953c6ece23a1a230b505c48a24293a24cc164d44012b610917d57e0664db490bb52dffed3a1684bd3582b0b78695b0848ca5bdced0abfe246692ee6dd730575b138c23d6eff650ccaf27550d13a346b60afb18bea4b2ad15ad8a60be524f98562a9f6c643bd13fefd35698ed3396d9db3e00301ca83458a6f88b074db60adf71db657bc069974ab53325fdcf589fce0be769fd049fbe7c7e5d7b1eb6627ce26b6b20b1df568c6bb4802944eca523a3c25a98bc81f35a0411b60b74da9e8792d3fa8970feebcf3700c0d9f5bdcd3052319ec726a23ad1403857a5eeadf20a0344812e3b5480a1b049a2642180d957b25be515e6539c16cdd6052b556bacd9ba10165efaeaa7130dd8e8cec7fd36d7e17db8f8d17ec66867898e141dfe8ed29472e1ecfa3e2347ce066d611f3fe49fdb29a5ce56790580dbaf02489cad7d20100804026f9d7f6be4942aeb43f7890000000049454e44ae42608268",
          "c10c97fe0e7bb78d8dd7447bc098386c61248e1e9a7dfd263fd828c5373b945735"
        ]
      }
    ],
    "output": [
      {
        "value": 546,
        "script_pubkey": "51208a2ade400b30af7cae07e30a9afa8ac49f54fb3ff7d0f42bbf4a66578a34c280"
      }
    ]
  },
  "txid": "99811de396ff10152cdfc9588d9750d0151501f081df2e56071c42dc3532b743"
}
```
</details>

## Recursive Endpoints

See [Recursion](../inscriptions/recursion.md).

ord/docs/src/guides/batch-inscribing.md


Batch Inscribing
================

Multiple inscriptions can be created at the same time using the
[pointer field](./../inscriptions/pointer.md). This is especially helpful for
collections, or other cases when multiple inscriptions should share the same
parent, since the parent can passed into a reveal transaction that creates
multiple children.

To create a batch inscription using a batchfile in `batch.yaml`, run the
following command:

```bash
ord wallet batch --fee-rate 21 --batch batch.yaml
```

Example `batch.yaml`
--------------------

```yaml
{{#include ../../../batch.yaml}}
```

ord/docs/src/guides/collecting.md


Collecting
==========

Currently, [ord](https://github.com/ordinals/ord/) is the only wallet supporting
sat-control and sat-selection, which are required to safely store and send rare
sats and inscriptions, hereafter ordinals.

The recommended way to send, receive, and store ordinals is with `ord`, but if
you are careful, it is possible to safely store, and in some cases send,
ordinals with other wallets.

As a general note, receiving ordinals in an unsupported wallet is not
dangerous. Ordinals can be sent to any bitcoin address, and are safe as long as
the UTXO that contains them is not spent. However, if that wallet is then used
to send bitcoin, it may select the UTXO containing the ordinal as an input, and
send the inscription or spend it to fees.

A [guide](./collecting/sparrow-wallet.md) to creating an `ord`-compatible wallet with [Sparrow Wallet](https://sparrowwallet.com/), is available
in this handbook.

Please note that if you follow this guide, you should not use the wallet you
create to send BTC, unless you perform manual coin-selection to avoid sending
ordinals.

ord/docs/src/guides/collecting/sparrow-wallet.md


Collecting Inscriptions and Ordinals with Sparrow Wallet
=====================

Users who cannot or have not yet set up the [ord](https://github.com/ordinals/ord) wallet can receive inscriptions and ordinals with alternative bitcoin wallets, as long as they are _very_ careful about how they spend from that wallet.

This guide gives some basic steps on how to create a wallet with [Sparrow Wallet](https://sparrowwallet.com/) which is compatible with `ord` and can be later imported into `ord`

## ⚠️⚠️ Warning!! ⚠️⚠️
As a general rule if you take this approach, you should use this wallet with the Sparrow software as a receive-only wallet.

Do not spend any satoshis from this wallet unless you are sure you know what you are doing. You could very easily inadvertently lose access to your ordinals and inscriptions if you don't heed this warning.

## Wallet Setup & Receiving

Download the Sparrow Wallet from the [releases page](https://sparrowwallet.com/download/) for your particular operating system.

Select `File -> New Wallet` and create a new wallet called `ord`.

![](images/wallet_setup_01.png)

Change the `Script Type` to `Taproot (P2TR)` and select the `New or Imported Software Wallet` option.

![](images/wallet_setup_02.png)

Select `Use 12 Words` and then click `Generate New`. Leave the passphrase blank.

![](images/wallet_setup_03.png)

A new 12 word [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) seed phrase will be generated for you. Write this down somewhere safe as this is your backup to get access to your wallet. NEVER share or show this seed phrase to anyone else.

Once you have written down the seed phrase click `Confirm Backup`.

![](images/wallet_setup_04.png)

Re-enter the seed phrase which you wrote down, and then click `Create Keystore`.

![](images/wallet_setup_05.png)

Click `Import Keystore`.

![](images/wallet_setup_06.png)

Click `Apply`. Add a password for the wallet if you want to.

![](images/wallet_setup_07.png)

You now have a wallet which is compatible with `ord`, and can be imported into `ord` using the [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) Seed Phrase. To receive ordinals or inscriptions, click on the `Receive` tab and copy a new address.

Each time you want to receive you should use a brand-new address, and not re-use existing addresses.

Note that bitcoin is different to some other blockchain wallets, in that this wallet can generate an unlimited number of new addresses. You can generate a new address by clicking on the `Get Next Address` button. You can see all of your addresses in the `Addresses` tab of the app.

You can add a label to each address, so you can keep track of what it was used for.

![](images/wallet_setup_08.png)

## Validating / Viewing Received Inscriptions

Once you have received an inscription you will see a new transaction in the `Transactions` tab of Sparrow, as well as a new UTXO in the `UTXOs` tab.

Initially this transaction may have an "Unconfirmed" status, and you will need to wait for it to be mined into a bitcoin block before it is fully received.

![](images/validating_viewing_01.png)

To track the status of your transaction you can right-click on it,  select `Copy Transaction ID` and then paste that transaction id into [mempool.space](https://mempool.space).

![](images/validating_viewing_02.png)

Once the transaction has confirmed, you can validate and view your inscription by heading over to the `UTXOs` tab, finding the UTXO you want to check, right-clicking on the `Output` and selecting `Copy Transaction Output`. This transaction output id can then be pasted into the [ordinals.com](https://ordinals.com) search.


## Freezing UTXO's
As explained above, each of your inscriptions is stored in an Unspent Transaction Output (UTXO). You want to be very careful not to accidentally spend your inscriptions, and one way to make it harder for this to happen is to freeze the UTXO.

To do this, go to the `UTXOs` tab, find the UTXO you want to freeze, right-click on the `Output` and select `Freeze UTXO`.

This UTXO (Inscription) is now un-spendable within the Sparrow Wallet until you unfreeze it.

## Importing into `ord` wallet

For details on setting up Bitcoin Core and the `ord` wallet check out the [Wallet Guide](../wallet.md)

When setting up `ord`, instead of running `ord wallet create` to create a brand-new wallet, you can import your existing wallet using `ord wallet restore "BIP39 SEED PHRASE"` using the seed phrase you generated with Sparrow Wallet.

There is currently a [bug](https://github.com/ordinals/ord/issues/1589) which causes an imported wallet to not be automatically rescanned against the blockchain. To work around this you will need to manually trigger a rescan using the bitcoin core cli:
`bitcoin-cli -rpcwallet=ord rescanblockchain 767430`

You can then check your wallet's inscriptions using `ord wallet inscriptions`

Note that if you have previously created a wallet with `ord`, then you will already have a wallet with the default name, and will need to give your imported wallet a different name. You can use the `--wallet` parameter in all `ord` commands to reference a different wallet, eg:

`ord wallet --name ord_from_sparrow wallet restore --from mnemonic`

`ord wallet --name ord_from_sparrow wallet inscriptions`

`bitcoin-cli -rpcwallet=ord_from_sparrow rescanblockchain 767430`

## Sending inscriptions with Sparrow Wallet

#### ⚠️⚠️ Warning ⚠️⚠️
While it is highly recommended that you set up a bitcoin core node and run the `ord` software, there are certain limited ways you can send inscriptions out of Sparrow Wallet in a safe way. Please note that this is not recommended, and you should only do this if you fully understand what you are doing.

Using the `ord` software will remove much of the complexity we are describing here, as it is able to automatically and safely handle sending inscriptions in an easy way.

#### ⚠️⚠️ Additional Warning ⚠️⚠️
Don't use your sparrow inscriptions wallet to do general sends of non-inscription bitcoin. You can setup a separate wallet in sparrow if you need to do normal bitcoin transactions, and keep your inscriptions wallet separate.

#### Bitcoin's UTXO model
Before sending any transaction it's important that you have a good mental model for bitcoin's Unspent Transaction Output (UTXO) system. The way Bitcoin works is fundamentally different to many other blockchains such as Ethereum. In Ethereum generally you have a single address in which you store ETH, and you cannot differentiate between any of the ETH -  it is just all a single value of the total amount in that address. Bitcoin works very differently in that we generate a new address in the wallet for each receive, and every time you receive sats to an address in your wallet you are creating a new UTXO. Each UTXO can be seen and managed individually. You can select specific UTXO's which you want to spend, and you can choose not to spend certain UTXO's.

Some Bitcoin wallets do not expose this level of detail, and they just show you a single summed up value of all the bitcoin in your wallet. However, when sending inscriptions it is important that you use a wallet like Sparrow which allows for UTXO control.

#### Inspecting your inscription before sending
Like we have previously described inscriptions are inscribed onto sats, and sats are stored within UTXOs. UTXO's are a collection of satoshis with some particular value of the number of satoshis (the output value). Usually (but not always) the inscription will be inscribed on the first satoshi in the UTXO.

When inspecting your inscription before sending the main thing you will want to check is which satoshi in the UTXO your inscription is inscribed on.

To do this, you can follow the [Validating / Viewing Received Inscriptions](./sparrow-wallet.md#validating--viewing-received-inscriptions) described above to find the inscription page for your inscription on ordinals.com

There you will find some metadata about your inscription which looks like the following:

![](images/sending_01.png)

There are a few of important things to check here:
* The `output` identifier matches the identifier of the UTXO you are going to send
* The `offset` of the inscription is `0` (this means that the inscription is located on the first sat in the UTXO)
* the `output_value` has enough sats to cover the transaction fee (postage) for sending the transaction. The exact amount you will need depends on the fee rate you will select for the transaction

If all of the above are true for your inscription, it should be safe for you to send it using the method below.

⚠️⚠️ Be very careful sending your inscription particularly if the `offset` value is not `0`. It is not recommended to use this method if that is the case, as doing so you could accidentally send your inscription to a bitcoin miner unless you know what you are doing.

#### Sending your inscription
To send an inscription navigate to the `UTXOs` tab, and find the UTXO which you previously validated contains your inscription.

If you previously froze the UTXO you will need to right-click on it and unfreeze it.

Select the UTXO you want to send, and ensure that is the _only_ UTXO is selected. You should see `UTXOs 1/1` in the interface. Once you are sure this is the case you can hit `Send Selected`.

![](images/sending_02.png)

You will then be presented with the transaction construction interface. There are a few things you need to check here to make sure that this is a safe send:

* The transaction should have only 1 input, and this should be the UTXO with the label you want to send
* The transaction should have only 1 output, which is the address/label where you want to send the inscription

If your transaction looks any different, for example you have multiple inputs, or multiple outputs then this may not be a safe transfer of your inscription, and you should abandon sending until you understand more, or can import into the `ord` wallet.

You should set an appropriate transaction fee, Sparrow will usually recommend a reasonable one, but you can also check [mempool.space](https://mempool.space) to see what the recommended fee rate is for sending a transaction.

You should add a label for the recipient address, a label like `alice address for inscription #123` would be ideal.

Once you have checked the transaction is a safe transaction using the checks above, and you are confident to send it you can click `Create Transaction`.

![](images/sending_03.png)

Here again you can double check that your transaction looks safe, and once you are confident you can click `Finalize Transaction for Signing`.

![](images/sending_04.png)

Here you can triple check everything before hitting `Sign`.

![](images/sending_05.png)

And then actually you get very very last chance to check everything before hitting `Broadcast Transaction`. Once you broadcast the transaction it is sent to the bitcoin network, and starts being propagated into the mempool.

![](images/sending_06.png)

If you want to track the status of your transaction you can copy the `Transaction Id (Txid)` and paste that into [mempool.space](https://mempool.space)

Once the transaction has confirmed you can check the inscription page on [ordinals.com](https://ordinals.com) to validate that it has moved to the new output location and address.

## Troubleshooting

#### Sparrow wallet is not showing a transaction/UTXO, but I can see it on mempool.space!

Make sure that your wallet is connected to a bitcoin node. To validate this, head into the `Preferences`-> `Server` settings, and click `Edit Existing Connection`.

![](images/troubleshooting_01.png)

From there you can select a node and click `Test Connection` to validate that Sparrow is able to connect successfully.

![](images/troubleshooting_02.png)

ord/docs/src/guides/explorer.md


Ordinal Explorer
================

The `ord` binary includes a block explorer. We host an instance of the block
explorer on mainnet at [ordinals.com](https://ordinals.com), on signet at
[signet.ordinals.com](https://signet.ordinals.com), and on testnet at
[testnet.ordinals.com](https://testnet.ordinals.com). As of version 0.16.0 the
wallet needs `ord server` running in the background. This is analogous to how
`bitcoin-cli` needs `bitcoind` running in the background.

### Running The Explorer
The server can be run locally with:

`ord server`

To specify a port add the `--http-port` flag:

`ord server --http-port 8080`

The JSON-API endpoints are enabled by default, to disable them add the
`--disable-json-api` flag (see [here](api.md) for more info):

`ord server --disable-json-api`

Search
------

The search box accepts a variety of object representations.

### Blocks

Blocks can be searched by hash, for example, the genesis block:

[000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f](https://ordinals.com/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f)

### Transactions

Transactions can be searched by hash, for example, the genesis block coinbase
transaction:

[4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b](https://ordinals.com/search/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b)

### Outputs

Transaction outputs can be searched by outpoint, for example, the only output of
the genesis block coinbase transaction:

[4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0](https://ordinals.com/search/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0)

### Sats

Sats can be searched by integer, their position within the entire bitcoin
supply:

[2099994106992659](https://ordinals.com/search/2099994106992659)

By decimal, their block and offset within that block:

[481824.0](https://ordinals.com/search/481824.0)

By degree, their cycle, blocks since the last halving, blocks since the last
difficulty adjustment, and offset within their block:

[1°0′0″0‴](https://ordinals.com/search/1°0′0″0‴)

By name, their base 26 representation using the letters "a" through "z":

[ahistorical](https://ordinals.com/search/ahistorical)

Or by percentile, the percentage of bitcoin's supply that has been or will have
been issued when they are mined:

[100%](https://ordinals.com/search/100%)

```

ord/docs/src/guides/moderation.md


Moderation
==========

`ord` includes a block explorer, which you can run locally with `ord server`.

The block explorer allows viewing inscriptions. Inscriptions are user-generated
content, which may be objectionable or unlawful.

It is the responsibility of each individual who runs an ordinal block explorer
instance to understand their responsibilities with respect to unlawful content,
and decide what moderation policy is appropriate for their instance.

In order to prevent particular inscriptions from being displayed on an `ord`
instance, they can be included in a YAML config file, which is loaded with the
`--config` option.

To hide inscriptions, first create a config file, with the inscription ID you
want to hide:

```yaml
hidden:
- 0000000000000000000000000000000000000000000000000000000000000000i0
```

The suggested name for `ord` config files is `ord.yaml`, but any filename can
be used.

Then pass the file to `--config` when starting the server:

`ord --config ord.yaml server`

Note that the `--config` option comes after `ord` but before the `server`
subcommand.

`ord` must be restarted in order to load changes to the config file.

`ordinals.com`
--------------

The `ordinals.com` instances use `systemd` to run the `ord server` service,
which is called `ord`, with a config file located at `/var/lib/ord/ord.yaml`.

To hide an inscription on `ordinals.com`:

1. SSH into the server
2. Add the inscription ID to `/var/lib/ord/ord.yaml`
3. Restart the service with `systemctl restart ord`
4. Monitor the restart with `journalctl -u ord`

Currently, `ord` is slow to restart, so the site will not come back online
immediately.

ord/docs/src/guides/reindexing.md


Reindexing
==========

Sometimes the `ord` database must be reindexed, which means deleting the
database and restarting the indexing process with either `ord index update` or
`ord server`. Reasons to reindex are:

1. A new major release of ord, which changes the database scheme
2. The database got corrupted somehow

The database `ord` uses is called [redb](https://github.com/cberner/redb),
so we give the index the default file name `index.redb`. By default we store this
file in different locations depending on your operating system.

|Platform | Value                                            | Example                                      |
| ------- | ------------------------------------------------ | -------------------------------------------- |
| Linux   | `$XDG_DATA_HOME`/ord or `$HOME`/.local/share/ord | /home/alice/.local/share/ord                 |
| macOS   | `$HOME`/Library/Application Support/ord          | /Users/Alice/Library/Application Support/ord |
| Windows | `{FOLDERID_RoamingAppData}`\ord                  | C:\Users\Alice\AppData\Roaming\ord           |

So to delete the database and reindex on MacOS you would have to run the following
commands in the terminal:

```bash
rm ~/Library/Application Support/ord/index.redb
ord index update
```

You can of course also set the location of the data directory yourself with `ord
--datadir <DIR> index update` or give it a specific filename and path with `ord
--index <FILENAME> index update`.

ord/docs/src/guides/sat-hunting.md


Sat Hunting
===========

Ordinal hunting is difficult but rewarding. The feeling of owning a wallet full
of UTXOs, redolent with the scent of rare and exotic sats, is beyond compare.

Ordinals are numbers for satoshis. Every satoshi has an ordinal number and
every ordinal number has a satoshi.

Preparation
-----------

There are a few things you'll need before you start.

1. First, you'll need a synced Bitcoin Core node with a transaction index. To
   turn on transaction indexing, pass `-txindex` on the command-line:

   ```sh
   bitcoind -txindex
   ```

   Or put the following in your [Bitcoin configuration
   file](https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md#configuration-file-path):

   ```
   txindex=1
   ```

   Launch it and wait for it to catch up to the chain tip, at which point the
   following command should print out the current block height:

   ```sh
   bitcoin-cli getblockcount
   ```

2. Second, you'll need a synced `ord` index.

   - Get a copy of `ord` from [the repo](https://github.com/ordinals/ord/).

   - Run `ord --index-sats server`. It should connect to your bitcoin core node and start indexing.
     
   - Once it has finished indexing, leave the server running and submit new `ord` commands in a separate terminal session.

3. Third, you'll need a wallet with UTXOs that you want to search.

Searching for Rare Ordinals
---------------------------

### Searching for Rare Ordinals in a Bitcoin Core Wallet

The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so
searching for rare ordinals in a Bitcoin Core wallet is Easy. Assuming your
wallet is named `foo`:

1. Load your wallet:

   ```sh
   bitcoin-cli loadwallet foo
   ```

2. Display any rare ordinals wallet `foo`'s UTXOs:

   ```sh
   ord --index-sats wallet --name foo sats
   ```

### Searching for Rare Ordinals in a Non-Bitcoin Core Wallet

The `ord wallet` command is just a wrapper around Bitcoin Core's RPC API, so to
search for rare ordinals in a non-Bitcoin Core wallet, you'll need to import
your wallet's descriptors into Bitcoin Core.

[Descriptors](https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md)
describe the ways that wallets generate private keys and public keys.

You should only import descriptors into Bitcoin Core for your wallet's public
keys, not its private keys.

If your wallet's public key descriptor is compromised, an attacker will be able
to see your wallet's addresses, but your funds will be safe.

If your wallet's private key descriptor is compromised, an attacker can drain
your wallet of funds.

1. Get the wallet descriptor from the wallet whose UTXOs you want to search for
   rare ordinals. It will look something like this:

   ```
   wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)#csvefu29
   ```

2. Create a watch-only wallet named `foo-watch-only`:

   ```sh
   bitcoin-cli createwallet foo-watch-only true true
   ```

   Feel free to give it a better name than `foo-watch-only`!

3. Load the `foo-watch-only` wallet:

   ```sh
   bitcoin-cli loadwallet foo-watch-only
   ```

4. Import your wallet descriptors into `foo-watch-only`:

   ```sh
   bitcoin-cli importdescriptors \
     '[{ "desc": "wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)#tpnxnxax", "timestamp":0 }]'
   ```

   If you know the Unix timestamp when your wallet first started receive
   transactions, you may use it for the value of `"timestamp"` instead of `0`.
   This will reduce the time it takes for Bitcoin Core to search for your
   wallet's UTXOs.

5. Check that everything worked:

   ```sh
   bitcoin-cli getwalletinfo
   ```

6. Display your wallet's rare ordinals:

   ```sh
   ord wallet sats
   ```

### Searching for Rare Ordinals in a Wallet that Exports Multi-path Descriptors

Some descriptors describe multiple paths in one descriptor using angle brackets,
e.g., `<0;1>`. Multi-path descriptors are not yet supported by Bitcoin Core, so
you'll first need to convert them into multiple descriptors, and then import
those multiple descriptors into Bitcoin Core.

1. First get the multi-path descriptor from your wallet. It will look something
   like this:

   ```
   wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/<0;1>/*)#fw76ulgt
   ```

2. Create a descriptor for the receive address path:

   ```
   wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)
   ```

   And the change address path:

   ```
   wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/*)
   ```

3. Get and note the checksum for the receive address descriptor, in this case
   `tpnxnxax`:

   ```sh
   bitcoin-cli getdescriptorinfo \
     'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)'
   ```

   ```json
   {
     "descriptor": "wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)#csvefu29",
     "checksum": "tpnxnxax",
     "isrange": true,
     "issolvable": true,
     "hasprivatekeys": false
   }
   ```

   And for the change address descriptor, in this case `64k8wnd7`:

   ```sh
   bitcoin-cli getdescriptorinfo \
     'wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/*)'
   ```

   ```json
   {
     "descriptor": "wpkh([bf1dd55e/84'/0'/0']xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/*)#fyfc5f6a",
     "checksum": "64k8wnd7",
     "isrange": true,
     "issolvable": true,
     "hasprivatekeys": false
   }
   ```

4. Load the wallet you want to import the descriptors into:

   ```sh
   bitcoin-cli loadwallet foo-watch-only
   ```

5. Now import the descriptors, with the correct checksums, into Bitcoin Core.

   ```sh
   bitcoin-cli \
    importdescriptors \
    '[
      {
        "desc": "wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/0/*)#tpnxnxax"
        "timestamp":0
      },
      {
        "desc": "wpkh([bf1dd55e/84h/0h/0h]xpub6CcJtWcvFQaMo39ANFi1MyXkEXM8T8ZhnxMtSjQAdPmVSTHYnc8Hwoc11VpuP8cb8JUTboZB5A7YYGDonYySij4XTawL6iNZvmZwdnSEEep/1/*)#64k8wnd7",
        "timestamp":0
      }
    ]'
   ```

   If you know the Unix timestamp when your wallet first started receive
   transactions, you may use it for the value of the `"timestamp"` fields
   instead of `0`. This will reduce the time it takes for Bitcoin Core to
   search for your wallet's UTXOs.

6. Check that everything worked:

   ```sh
   bitcoin-cli getwalletinfo
   ```

7. Display your wallet's rare ordinals:

   ```sh
   ord wallet sats
   ```

### Exporting Descriptors

#### Sparrow Wallet

Navigate to the `Settings` tab, then to `Script Policy`, and press the edit
button to display the descriptor.

### Transferring Ordinals

The `ord` wallet supports transferring specific satoshis by using the
name of the satoshi. To send the satoshi `zonefruits`, do:

```
ord wallet send <RECEIVING_ADDRESS> zonefruits --fee-rate 21
```

You can also use the `bitcoin-cli` commands `createrawtransaction`,
`signrawtransactionwithwallet`, and `sendrawtransaction`, but this
method can be complex and is outside the scope of this guide.

ord/docs/src/guides/satscards.md


Satscard
========

[Satscards](https://satscard.com/) are cards which can be used to store
bitcoin, inscriptions, and runes.

Slots
-----

Each satscard has ten slots containing private keys with corresponding bitcoin
addresses.

Initially, all slots are sealed and the private keys are stored only the
satscard.

Slots can be unsealed, which allows the corresponding private key to be
extracted.

Unsealing is permanent. If a satscard is sealed, you can have some confidence
that private key is not known to anyone. That taking physical ownership of a
satscard makes you the sole owner of assets in any sealed slots.

Lifespan
--------

Satscards are expected to have a usable lifetime of ten years. Do not use
satscards for long-term storage of valuable assets.


Viewing
-------

When placed on a smartphone, the satscard transmits a URL, beginning with
`https://satscard.com/start` or `https://getsatscard.com/start`, depending on
when it was manufactured.

This URL contains a signature which can be used to recover the address of the
current slot. This signature is made over a random nonce, so it changes every
time the satscard is tapped, and provides some confidence that the satscard
contains the private key.

`ord` supports viewing the contents of a satscard by entering the full URL into
the `ord` explorer search bar, or the input field on the `/satscard` page.

For `ordinals.com`, this is
[ordinals.com/satscard](https://ordinals.com/satscard).

Unsealing
---------

Satscard slots can be unsealed and the private keys extracted using the `cktap`
binary, available in the
[coinkite-tap-proto](https://github.com/coinkite/coinkite-tap-proto)
repository.

Sweeping
--------

After a satscard slot is unsealed, all assets should be swept from that slot to
another wallet, as the private key can now be read via NFC.

`ord` does not yet support sweeping assets from other wallets, so assets will
need to be transferred manually.

Be careful, and good luck!

ord/docs/src/guides/settings.md


Settings
========

`ord` can be configured with the command line, environment variables, a
configuration file, and default values.

The command line takes precedence over environment variables, which take
precedence over the configuration file, which takes precedence over defaults.

The path to the configuration file can be given with `--config <CONFIG_PATH>`.
`ord` will error if `<CONFIG_PATH>` doesn't exist.

The path to a directory containing a configuration file name named `ord.yaml`
can be given with `--config-dir <CONFIG_DIR_PATH>` or `--datadir
<DATA_DIR_PATH>` in which case the config path is `<CONFIG_DIR_PATH>/ord.yaml`
or `<DATA_DIR_PATH>/ord.yaml`. It is not an error if it does not exist.

If none of `--config`, `--config-dir`, or `--datadir` are given, and a file
named `ord.yaml` exists in the default data directory, it will be loaded.

For a setting named `--setting-name` on the command line, the environment
variable will be named `ORD_SETTING_NAME`, and the config file field will be
named `setting_name`. For example, the data directory can be configured with
`--datadir` on the command line, the `ORD_DATA_DIR` environment variable, or
`data_dir` in the config file.

See `ord --help` for documentation of all the settings.

`ord`'s current configuration can be viewed as JSON with the `ord settings`
command.

Example Configuration
---------------------

```yaml
{{#include ../../../ord.yaml}}
```

Hiding Inscription Content
--------------------------

Inscription content can be selectively prevented from being served by `ord
server`.

Unlike other settings, this can only be configured with the configuration file
or environment variables.

To hide inscriptions with an environment variable:

```
export ORD_HIDDEN='6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0'
```

Or with the configuration file:

```yaml
hidden:
- 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
- 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0
```

ord/docs/src/guides/splitting.md


Splitting
=========

Complex transactions can be created using the `ord wallet split` command.

The `split` command takes a YAML configuration file, which specifies any number
of outputs to be created, their bitcoin value, and their value of any number of
runes. It does not currently allow assigning inscriptions to outputs.

The `split` command can be used to split cardinal, bitcoin-only outputs for
transacting, distribute runes to large numbers of recipients in a single
transaction.

To send a split transaction using the configuration in `splits.yaml`, run the
following command:

```bash
ord wallet split --fee-rate 21 --splits split.yaml
```

Example `splits.yaml`
--------------------`

```yaml
{{#include ../../../splits.yaml}}
```

ord/docs/src/guides/teleburning.md


Teleburning
===========

Teleburn addresses can be used to burn assets on other blockchains, leaving
behind in the smoking rubble a sort of forwarding address pointing to an
inscription on Bitcoin.

Teleburning an asset means something like, "I'm out. Find me on Bitcoin."

Teleburn addresses are derived from inscription IDs. They have no corresponding
private key, so assets sent to a teleburn address are burned. Currently, only
Ethereum teleburn addresses are supported. Pull requests adding teleburn
addresses for other chains are welcome.

Ethereum
--------

Ethereum teleburn addresses are derived by taking the first 20 bytes of the
SHA-256 hash of the inscription ID, serialized as 36 bytes, with the first 32
bytes containing the transaction ID, and the last four bytes containing
big-endian inscription index, and interpreting it as an Ethereum address.

Example
-------

The ENS domain name [rodarmor.eth](https://app.ens.domains/rodarmor.eth), was
teleburned to [inscription
zero](https://ordinals.com/inscription/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0).

The inscription ID of inscription zero is
`6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0`.

Passing `6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0` to
the teleburn command:

```bash
$ ord teleburn 6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```

Returns:

```json
{
  "ethereum": "0xe43A06530BdF8A4e067581f48Fae3b535559dA9e"
}
```

Indicating that `0xe43A06530BdF8A4e067581f48Fae3b535559dA9e` is the Ethereum
teleburn address for inscription zero, which is, indeed, the current owner, on
Ethereum, of `rodarmor.eth`.

ord/docs/src/guides/testing.md


Testing
=======

Test Environment
----------------

`ord env <DIRECTORY>` creates a test environment in `<DIRECTORY>`, spins up
`bitcoind` and `ord server` instances, prints example commands for interacting
with the test `bitcoind` and `ord server` instances, waits for `CTRL-C`, and
then shuts down `bitcoind` and `ord server`.

`ord env` tries to use port 9000 for `bitcoind`'s RPC interface, and port
`9001` for `ord`'s RPC interface, but will fall back to random unused ports.

Inside of the env directory, `ord env` will write `bitcoind`'s configuration to
`bitcoin.conf`, `ord`'s configuration to `ord.yaml`, and the env configuration
to `env.json`.

`env.json` contains the commands needed to invoke `bitcoin-cli` and `ord
wallet`, as well as the ports `bitcoind` and `ord server` are listening on.

These can be extracted into shell commands using `jq`:

```shell
bitcoin=`jq -r '.bitcoin_cli_command | join(" ")' env/env.json`
$bitcoin listunspent

ord=`jq -r '.ord_wallet_command | join(" ")' env/env.json`
$ord outputs
```

If `ord` is in the `$PATH` and the env directory is `env`, the `bitcoin-cli`
command will be:

```
bitcoin-cli -datadir=env
```

And the `ord` will be:

```
ord --datadir env
```

Test Networks
-------------

Ord can be tested using the following flags to specify the test network. For more
information on running Bitcoin Core for testing, see [Bitcoin's developer documentation](https://developer.bitcoin.org/examples/testing.html).

Most `ord` commands in [wallet](wallet.md) and [explorer](explorer.md)
can be run with the following network flags:

| Network | Flag |
|---------|------|
| Testnet | `--testnet` or `-t` |
| Signet  | `--signet` or `-s` |
| Regtest | `--regtest` or `-r` |

Regtest doesn't require downloading the blockchain since you create your own
private blockchain, so indexing `ord` is almost instantaneous.

Example
-------

Run `bitcoind` in regtest with:

```
bitcoind -regtest -txindex
```

Run `ord server` in regtest with:

```
ord --regtest server
```

Create a wallet in regtest with:

```
ord --regtest wallet create
```

Get a regtest receive address with:

```
ord --regtest wallet receive
```

Mine 101 blocks (to unlock the coinbase) with:

```
bitcoin-cli -regtest generatetoaddress 101 <receive address>
```

Inscribe in regtest with:

```
ord --regtest wallet inscribe --fee-rate 1 --file <file>
```

Mine the inscription with:

```
bitcoin-cli -regtest generatetoaddress 1 <receive address>
```

By default, browsers don't support compression over HTTP. To test compressed
content over HTTP, use the `--decompress` flag:

```
ord --regtest server --decompress
```

Testing Recursion
-----------------

When testing out [recursion](../inscriptions/recursion.md), inscribe the
dependencies first (example with [p5.js](https://p5js.org)):

```
ord --regtest wallet inscribe --fee-rate 1 --file p5.js
```

This will return the inscription ID of the dependency which you can then
reference in your inscription.

However, inscription IDs differ between mainnet and test chains, so you must
change the inscription IDs in your inscription to the mainnet inscription IDs of
your dependencies before making the final inscription on mainnet.

Then you can inscribe your recursive inscription with:

```
ord --regtest wallet inscribe --fee-rate 1 --file recursive-inscription.html
```

Finally you will have to mine some blocks and start the server:

```
bitcoin-cli generatetoaddress 6 <receive address>
```

### Mainnet Dependencies

To avoid having to change dependency inscription IDs to mainnet inscription IDs,
you may utilize a content proxy when testing. `ord server` accepts a
`--proxy` option, which takes the URL of another `ord server`
instance. When making a request to `/content/<INSCRIPTION_ID>` when a content
proxy is set and the inscription is not found, `ord server` will forward the
request to the content proxy. This allows you to run a test `ord server`
instance with a mainnet content proxy. You can then use mainnet inscription IDs
in your test inscription, which will then return the content of the mainnet
inscriptions.

```
ord --regtest server --proxy https://ordinals.com
```

ord/docs/src/guides/wallet.md


Wallet
======

Individual sats can be inscribed with arbitrary content, creating
Bitcoin-native digital artifacts that can be held in a Bitcoin wallet and
transferred using Bitcoin transactions. Inscriptions are as durable, immutable,
secure, and decentralized as Bitcoin itself.

Working with inscriptions requires a Bitcoin full node, to give you a view of
the current state of the Bitcoin blockchain, and a wallet that can create
inscriptions and perform sat control when constructing transactions to send
inscriptions to another wallet.

Bitcoin Core provides both a Bitcoin full node and wallet. However, the Bitcoin
Core wallet cannot create inscriptions and does not perform sat control.

This requires [`ord`](https://github.com/ordinals/ord), the ordinal utility. `ord`
doesn't implement its own wallet, so `ord wallet` subcommands interact with
Bitcoin Core wallets.

This guide covers:

1. Installing Bitcoin Core
2. Syncing the Bitcoin blockchain
3. Creating a Bitcoin Core wallet
4. Using `ord wallet receive` to receive sats
5. Creating inscriptions with `ord wallet inscribe`
6. Sending inscriptions with `ord wallet send`
7. Receiving inscriptions with `ord wallet receive`
8. Batch inscribing with `ord wallet inscribe --batch`

Getting Help
------------

If you get stuck, try asking for help on the [Ordinals Discord
Server](https://discord.com/invite/87cjuz4FYg), or checking GitHub for relevant
[issues](https://github.com/ordinals/ord/issues) and
[discussions](https://github.com/ordinals/ord/discussions).

Installing Bitcoin Core
-----------------------

Bitcoin Core is available from [bitcoincore.org](https://bitcoincore.org/) on
the [download page](https://bitcoincore.org/en/download/).

Making inscriptions requires Bitcoin Core 28 or newer.

This guide does not cover installing Bitcoin Core in detail. Once Bitcoin Core
is installed, you should be able to run `bitcoind -version` successfully from
the command line. Do *NOT* use `bitcoin-qt`.

Configuring Bitcoin Core
------------------------

`ord` requires Bitcoin Core's transaction index and rest interface.

To configure your Bitcoin Core node to maintain a transaction
index, add the following to your `bitcoin.conf`:

```
txindex=1
```

Or, run `bitcoind` with `-txindex`:

```
bitcoind -txindex
```

Details on creating or modifying your `bitcoin.conf` file can be found
[here](https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md).

Syncing the Bitcoin Blockchain
------------------------------

To sync the chain, run:

```
bitcoind -txindex
```

…and leave it running until `getblockcount`:

```
bitcoin-cli getblockcount
```

agrees with the block count on a block explorer like [the mempool.space block
explorer](https://mempool.space/). `ord` interacts with `bitcoind`, so you
should leave `bitcoind` running in the background when you're using `ord`.

The blockchain takes about 600GB of disk space. If you have an external drive
you want to store blocks on, use the configuration option
`blocksdir=<external_drive_path>`. This is much simpler than using the
`datadir` option because the cookie file will still be in the default location
for `bitcoin-cli` and `ord` to find.

Troubleshooting
---------------

Make sure you can access `bitcoind` with `bitcoin-cli -getinfo` and that it is
fully synced.

If `bitcoin-cli -getinfo` returns `Could not connect to the server`, `bitcoind`
is not running.

Make sure `rpcuser`, `rpcpassword`, or `rpcauth` are *NOT* set in your
`bitcoin.conf` file. `ord` requires using cookie authentication. Make sure there
is a file `.cookie` in your bitcoin data directory.

If `bitcoin-cli -getinfo` returns `Could not locate RPC credentials`, then you
must specify the cookie file location.
If you are using a custom data directory (specifying the `datadir` option),
then you must specify the cookie location like
`bitcoin-cli -rpccookiefile=<your_bitcoin_datadir>/.cookie -getinfo`.
When running `ord` you must specify the cookie file location with
`--cookie-file=<your_bitcoin_datadir>/.cookie`.

Make sure you do *NOT* have `disablewallet=1` in your `bitcoin.conf` file. If
`bitcoin-cli listwallets` returns `Method not found` then the wallet is disabled
and you won't be able to use `ord`.

Make sure `txindex=1` is set. Run `bitcoin-cli getindexinfo` and it should
return something like
```json
{
  "txindex": {
    "synced": true,
    "best_block_height": 776546
  }
}
```
If it only returns `{}`, `txindex` is not set.
If it returns `"synced": false`, `bitcoind` is still creating the `txindex`.
Wait until `"synced": true` before using `ord`.

If you have `maxuploadtarget` set it can interfere with fetching blocks for
`ord` index. Either remove it or set `whitebind=127.0.0.1:8333`.

Installing `ord`
----------------

The `ord` utility is written in Rust and can be built from
[source](https://github.com/ordinals/ord). Pre-built binaries are available on the
[releases page](https://github.com/ordinals/ord/releases).

You can install the latest pre-built binary from the command line with:

```sh
curl --proto '=https' --tlsv1.2 -fsLS https://ordinals.com/install.sh | bash -s
```

Once `ord` is installed, you should be able to run:

```
ord --version
```

Which prints out `ord`'s version number.

Creating a Wallet
-----------------

`ord` uses `bitcoind` to manage private keys, sign transactions, and
broadcast transactions to the Bitcoin network. Additionally the `ord wallet`
requires [`ord server`](explorer.md) running in the background. Make sure these
programs are running:

```
bitcoind -txindex
```

```
ord server
```

To create a wallet named `ord`, the default, for use with `ord wallet`, run:

```
ord wallet create
```

This will print out your seed phrase mnemonic, store it somewhere safe.

```
{
  "mnemonic": "dignity buddy actor toast talk crisp city annual tourist orient similar federal",
  "passphrase": ""
}
```

If you want to specify a different name or use an `ord server` running on a
non-default URL you can set these options:

```
ord wallet --name foo --server-url http://127.0.0.1:8080 create
```

To see all available wallet options you can run:

```
ord wallet help
```

Restoring and Dumping Wallet
----------------------------

The `ord` wallet uses descriptors, so you can export the output descriptors and
import them into another descriptor-based wallet. To export the wallet
descriptors, which include your private keys:

```
$ ord wallet dump
==========================================
= THIS STRING CONTAINS YOUR PRIVATE KEYS =
=        DO NOT SHARE WITH ANYONE        =
==========================================
{
  "wallet_name": "ord",
  "descriptors": [
    {
      "desc": "tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/0/*)#uxn94yt5",
      "timestamp": 1296688602,
      "active": true,
      "internal": false,
      "range": [
        0,
        999
      ],
      "next": 0
    },
    {
      "desc": "tr([551ac972/86'/1'/0']tprv8h4xBhrfZwX9o1XtUMmz92yNiGRYjF9B1vkvQ858aN1UQcACZNqN9nFzj3vrYPa4jdPMfw4ooMuNBfR4gcYm7LmhKZNTaF4etbN29Tj7UcH/1/*)#djkyg3mv",
      "timestamp": 1296688602,
      "active": true,
      "internal": true,
      "range": [
        0,
        999
      ],
      "next": 0
    }
  ]
}
```

An `ord` wallet can be restored from a mnemonic:

```
ord wallet restore --from mnemonic
```

Type your mnemonic and press return.

To restore from a descriptor in `descriptor.json`:

```
cat descriptor.json | ord wallet restore --from descriptor
```

To restore from a descriptor in the clipboard:

```
ord wallet restore --from descriptor
```

Paste the descriptor into the terminal and press CTRL-D on unix and CTRL-Z
on Windows.

Receiving Sats
--------------

Inscriptions are made on individual sats, using normal Bitcoin transactions
that pay fees in sats, so your wallet will need some sats.

Get a new address from your `ord` wallet by running:

```
ord wallet receive
```

And send it some funds.

You can see pending transactions with:

```
ord wallet transactions
```

Once the transaction confirms, you should be able to see the transactions
outputs with `ord wallet outputs`.

Creating Inscription Content
----------------------------

Sats can be inscribed with any kind of content, but the `ord` wallet only
supports content types that can be displayed by the `ord` block explorer.

Additionally, inscriptions are included in transactions, so the larger the
content, the higher the fee that the inscription transaction must pay.

Inscription content is included in transaction witnesses, which receive the
witness discount. To calculate the approximate fee that an inscribe transaction
will pay, divide the content size by four and multiply by the fee rate.

Inscription transactions must be less than 400,000 weight units, or they will
not be relayed by Bitcoin Core. One byte of inscription content costs one
weight unit. Since an inscription transaction includes not just the inscription
content, limit inscription content to less than 400,000 weight units. 390,000
weight units should be safe.

Creating Inscriptions
---------------------

To create an inscription with the contents of `FILE`, run:

```
ord wallet inscribe --fee-rate FEE_RATE --file FILE
```

Ord will output two transactions IDs, one for the commit transaction, and one
for the reveal transaction, and the inscription ID. Inscription IDs are of the
form `TXIDiN`, where `TXID` is the transaction ID of the reveal transaction,
and `N` is the index of the inscription in the reveal transaction.

The commit transaction commits to a tapscript containing the content of the
inscription, and the reveal transaction spends from that tapscript, revealing
the content on chain and inscribing it on the first sat of the input that
contains the corresponding tapscript.

Wait for the reveal transaction to be mined. You can check the status of the
commit and reveal transactions using  [the mempool.space block
explorer](https://mempool.space/).

Once the reveal transaction has been mined, the inscription ID should be
printed when you run:

```
ord wallet inscriptions
```

Parent-Child Inscriptions
-------------------------

Parent-child inscriptions enable what is colloquially known as collections, see
[provenance](../inscriptions/provenance.md) for more information.

To make an inscription a child of another, the parent inscription has to be
inscribed and present in the wallet. To choose a parent run `ord wallet inscriptions`
and copy the inscription id (`<PARENT_INSCRIPTION_ID>`).

Now inscribe the child inscription and specify the parent like so:

```
ord wallet inscribe --fee-rate FEE_RATE --parent <PARENT_INSCRIPTION_ID> --file CHILD_FILE
```

This relationship cannot be added retroactively, the parent has to be
present at inception of the child.

Sending Inscriptions
--------------------

Ask the recipient to generate a new address by running:

```
ord wallet receive
```

Send the inscription by running:

```
ord wallet send --fee-rate <FEE_RATE> <ADDRESS> <INSCRIPTION_ID>
```

See the pending transaction with:

```
ord wallet transactions
```

Once the send transaction confirms, the recipient can confirm receipt by
running:

```
ord wallet inscriptions
```

Sending Runes
-------------

Ask the recipient to generate a new address by running:

```
ord wallet receive
```

Send the runes by running:

```
ord wallet send --fee-rate <FEE_RATE> <ADDRESS> <RUNES_AMOUNT>
```

Where `RUNES_AMOUNT` is the number of runes to send, a `:` character, and the
name of the rune. For example if you want to send 1000 of the EXAMPLE rune, you
would use `1000:EXAMPLE`.

```
ord wallet send --fee-rate 1 SOME_ADDRESS 1000:EXAMPLE
```

See the pending transaction with:

```
ord wallet transactions
```

Once the send transaction confirms, the recipient can confirm receipt with:

```
ord wallet balance
```

Receiving Inscriptions
----------------------

Generate a new receive address using:

```
ord wallet receive
```

The sender can transfer the inscription to your address using:

```
ord wallet send --fee-rate <FEE_RATE> ADDRESS INSCRIPTION_ID
```

See the pending transaction with:
```
ord wallet transactions
```

Once the send transaction confirms, you can confirm receipt by running:

```
ord wallet inscriptions
```

ord/docs/src/inscriptions.md


Inscriptions
============

Inscriptions inscribe sats with arbitrary content, creating bitcoin-native
digital artifacts, more commonly known as NFTs. Inscriptions do not require a
sidechain or separate token.

These inscribed sats can then be transferred using bitcoin transactions, sent
to bitcoin addresses, and held in bitcoin UTXOs. These transactions, addresses,
and UTXOs are normal bitcoin transactions, addresses, and UTXOS in all
respects, with the exception that in order to send individual sats,
transactions must control the order and value of inputs and outputs according
to ordinal theory.

The inscription content model is that of the web. An inscription consists of a
content type, also known as a MIME type, and the content itself, which is a
byte string. This allows inscription content to be returned from a web server,
and for creating HTML inscriptions that use and remix the content of other
inscriptions.

Inscription content is entirely on-chain, stored in taproot script-path spend
scripts. Taproot scripts have very few restrictions on their content, and
additionally receive the witness discount, making inscription content storage
relatively economical.

Since taproot script spends can only be made from existing taproot outputs,
inscriptions are made using a two-phase commit/reveal procedure. First, in the
commit transaction, a taproot output committing to a script containing the
inscription content is created. Second, in the reveal transaction, the output
created by the commit transaction is spent, revealing the inscription content
on-chain.

Inscription content is serialized using data pushes within unexecuted
conditionals, called "envelopes". Envelopes consist of an `OP_FALSE OP_IF …
OP_ENDIF` wrapping any number of data pushes. Because envelopes are effectively
no-ops, they do not change the semantics of the script in which they are
included, and can be combined with any other locking script.

A text inscription containing the string "Hello, world!" is serialized as
follows:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF
```

First the string `ord` is pushed, to disambiguate inscriptions from other uses
of envelopes.

`OP_PUSH 1` indicates that the next push contains the content type, and
`OP_PUSH 0`indicates that subsequent data pushes contain the content itself.
Multiple data pushes must be used for large inscriptions, as one of taproot's
few restrictions is that individual data pushes may not be larger than 520
bytes.

The inscription content is contained within the input of a reveal transaction,
and the inscription is made on the first sat of its input if it has no pointer
field. This sat can then be tracked using the familiar rules of ordinal
theory, allowing it to be transferred, bought, sold, lost to fees, and recovered.

Content
-------

The data model of inscriptions is that of a HTTP response, allowing inscription
content to be served by a web server and viewed in a web browser.

Fields
------

Inscriptions may include fields before an optional body. Each field consists of
two data pushes, a tag and a value.

Currently, there are six defined fields:

- `content_type`, with a tag of `1`, whose value is the MIME type of the body.
- `pointer`, with a tag of `2`, see [pointer docs](inscriptions/pointer.md).
- `parent`, with a tag of `3`, see [provenance](inscriptions/provenance.md).
- `metadata`, with a tag of `5`, see [metadata](inscriptions/metadata.md).
- `metaprotocol`, with a tag of `7`, whose value is the metaprotocol identifier.
- `content_encoding`, with a tag of `9`, whose value is the encoding of the body.
- `delegate`, with a tag of `11`, see [delegate](inscriptions/delegate.md).

The beginning of the body and end of fields is indicated with an empty data
push.

Unrecognized tags are interpreted differently depending on whether they are
even or odd, following the "it's okay to be odd" rule used by the Lightning
Network.

Even tags are used for fields which may affect creation, initial assignment, or
transfer of an inscription. Thus, inscriptions with unrecognized even fields
must be displayed as "unbound", that is, without a location.

Odd tags are used for fields which do not affect creation, initial assignment,
or transfer, such as additional metadata, and thus are safe to ignore.

Inscription IDs
---------------

The inscriptions are contained within the inputs of a reveal transaction. In
order to uniquely identify them they are assigned an ID of the form:

`521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0`

The part in front of the `i` is the transaction ID (`txid`) of the reveal
transaction. The number after the `i` defines the index (starting at 0) of new inscriptions
being inscribed in the reveal transaction.

Inscriptions can either be located in different inputs, within the same input or
a combination of both. In any case the ordering is clear, since a parser would
go through the inputs consecutively and look for all inscription `envelopes`.

| Input | Inscription Count | Indices    |
|:-----:|:-----------------:|:----------:|
| 0     | 2                 | i0, i1     |
| 1     | 1                 | i2         |
| 2     | 3                 | i3, i4, i5 |
| 3     | 0                 |            |
| 4     | 1                 | i6         |

Inscription Numbers
-------------------

Inscriptions are assigned inscription numbers starting at zero, first by the
order reveal transactions appear in blocks, and the order that reveal envelopes
appear in those transactions.

Due to a historical bug in `ord` which cannot be fixed without changing a great
many inscription numbers, inscriptions which are revealed and then immediately
spent to fees are numbered as if they appear last in the block in which they
are revealed.

Inscriptions which are cursed are numbered starting at negative one, counting
down. Cursed inscriptions on and after the jubilee at block 824544 are
vindicated, and are assigned positive inscription numbers.

Sandboxing
----------

HTML and SVG inscriptions are sandboxed in order to prevent references to
off-chain content, thus keeping inscriptions immutable and self-contained.

This is accomplished by loading HTML and SVG inscriptions inside `iframes` with
the `sandbox` attribute, as well as serving inscription content with
`Content-Security-Policy` headers.

Self-Reference
--------------

The content of the inscription with ID `INSCRIPTION_ID` must served from the
URL path `/content/<INSCRIPTION_ID>`.

This allows inscriptions to retrieve their own inscription ID with:

```js
let inscription_id = window.location.pathname.split("/").pop();
```

If an inscription with ID X delegates to an inscription with ID Y, that is to
say, if inscription X contains a delegate field with value Y, the content of
inscription X must be served from the URL path `/content/X`, *not*
`/content/Y`.

This allows delegating inscriptions to use their own inscription ID as a seed
for generative delegate content.

Reinscriptions
--------------

Previously inscribed sats can be reinscribed with the `--reinscribe` command if
the inscription is present in the wallet. This will only append an inscription to
a sat, not change the initial inscription.

Reinscribe with satpoint:
`ord wallet inscribe --fee-rate <FEE_RATE> --reinscribe --file <FILE> --satpoint <SATPOINT>`

Reinscribe on a sat (requires sat index):
`ord --index-sats wallet inscribe --fee-rate <FEE_RATE> --reinscribe --file <FILE> --sat <SAT>`

ord/docs/src/inscriptions/burning.md


Burning
=======

Inscriptions may be burned by constructing a transaction that spends them to a
script pubkey beginning with `OP_RETURN`.

Sending inscriptions to a so-called "burn address" is not recognized by `ord`.

Burned inscriptions receive the "burned" charm, recognized with 🔥 on the
inscription's `/inscription` page.

When burning inscriptions, CBOR metadata may be included in a data push
immediately following the `OP_RETURN`.

Burn metadata is unstructured, having no meaning to the underlying protocol,
and should be human readable. It is displayed on the burned inscription's
`/inscription` page, in the same manner as inscription metadata, under the
heading "burn metadata".

Use it, if you feel like it, to commemorate the inscription, celebrate the
closing of a collection, or for whatever other purposes you so desire.

Data pushes after the first are currently ignored by `ord`. However, they may
be given future meaning by the protocol, and should not be used.

For example, transaction
[b42f0d8a3277ce6a7e564fec8f5579f76bc19cb24f8eff565ebb81a4c2f94683](https://mempool.space/tx/b42f0d8a3277ce6a7e564fec8f5579f76bc19cb24f8eff565ebb81a4c2f94683)
burned inscription
[681b5373c03e3f819231afd9227f54101395299c9e58356bda278e2f32bef2cdi0](https://ordinals.com/inscription/681b5373c03e3f819231afd9227f54101395299c9e58356bda278e2f32bef2cdi0).

ord/docs/src/inscriptions/delegate.md


Delegate
========

Inscriptions may nominate a delegate inscription. Requests for the content of
an inscription with a delegate will instead return the content, content type 
and content encoding of the delegate. This can be used to cheaply create copies 
of an inscription.

### Specification

To create an inscription I with delegate inscription D:

- Create an inscription D. Note that inscription D does not have to exist when
  making inscription I. It may be inscribed later. Before inscription D is
  inscribed, requests for the content of inscription I will return a 404.
- Include tag `11`, i.e. `OP_PUSH 11`, in I, with the value of the serialized
  binary inscription ID of D, serialized as the 32-byte `TXID`, followed by the
  four-byte little-endian `INDEX`, with trailing zeroes omitted.

_NB_ The bytes of a bitcoin transaction ID are reversed in their text
representation, so the serialized transaction ID will be in the opposite order.

### Example

An example of an inscription which delegates to
`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0`:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 11
  OP_PUSH 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
OP_ENDIF
```

Note that the value of tag `11` is decimal, not hex.

The delegate field value uses the same encoding as the parent field. See
[provenance](provenance.md) for more examples of inscription ID encodings

See
[examples](examples.md#delegate) for on-chain examples of inscriptions that feature this functionality.

ord/docs/src/inscriptions/examples.md


Inscription Examples
====================

Delegate
--------

* The [first delegate inscription](https://ordinals.com/inscription/626127511953479e1933a448f020c76acd9086efe2f2b3f5e492c0c85d2ce051i0).
* The Oscillations * collection utilizes delegation, provenance, recursion, sat endpoint, and detects the kind of sat that each piece is inscribed on (sattribute-aware). Each piece is a delegate of [this inscription](https://ordinals.com/inscription/52b4ea10c2518c954c73594e403ccfb2d50044f5a3b09a224dfa3bf06dd1d499i0).
* [This inscription](https://ordinals.com/inscription/23a8f17fff4a73e2932dfc76e46d14d4f8975da96f5d5ae9a45898422056071ai0) was inscribed as a delegate of [this inscription](https://ordinals.com/inscription/9ff39db4c51f831225d41efbd29a399f2b16c758970ec4ab95a1a17e8be59088i0) and is also the parent inscription of a rune.

Metadata
--------

* Each [member](https://ordinals.com/inscription/ab924ff229beca227bf40221faf492a20b5e2ee4f084524c84a5f98b80fe527fi1) in the FUN collection has metadata that describes its attributes.
* [This inscription](https://ordinals.com/inscription/454700af2cffd3a872daeb89598a891f5fe9936225838a8c4522f491f246ca0ai0) uses its own metadata to draw the ordinal image.

Provenance
----------

* [Inscription 0](https://ordinals.com/inscription/0) is the parent inscription for Casey's sugar skull collection, a grandparent for the FUN! collection, and the grandparent for [the sleepiest rune](https://ordinals.com/rune/ZZZZZZZZZZZZZZZZZZZZZZZZZZ).
* With the [Rug Me](https://ordinals.com/inscription/f87a6b16a1224b65f9c8ea55e1718585b3075373581689e343860c12a70d15c7i41) collection, owners are able to change the background color by inscribing a child to it.
* This [Bitcoin Magazine Cover](https://ordinals.com/inscription/60902330) renders the children as part of the parent inscription.
* [The yellow_ord_bot](https://ordinals.com/inscription/17541f6adf6eb160d52bc6eb0a3546c7c1d2adfe607b1a3cddc72cc0619526adi0) has many different quotes as cursed children.
* The [Spellbound](https://ordinals.com/children/451accbce30177c23a8cd42ab85131312a449359ada3c054aa147b054136fa3bi0) collection from the Wizard of Ord utilizes recursion, delegation, metadata, provenance, postage, location, compression.

Recursion
---------

* [Inscription 12992](https://ordinals.com/inscription/bf637552f3758e20f733d1f250cbea7cbbb4bbf157a8d4a9c26132950a383415i0) was the first recursive inscription inscribed on mainnet.
* [OnChain Monkey Genesis (BTC)](https://ordinals.com/children/fb162a46943e5d7d31d72ee2c8c850e66c1ca5d0d453068aa63883528285ed21i0) was one of the earliest collections to use recursion to create its PFP art.
* [Blob](https://ordinals.com/inscription/67285791) is a recursive generative collection that seeds its generation with metadata and uses threeJS, React 3 Fiber and other libraries recursively.
* The [GPU Ordinals](https://ordinals.com/inscription/0b62d8790bb428e0278cd8c3dedd540e2495515198002a233a49ba7b21f8b2cei0) collection takes recursive content and transforms it before rendering, creating what is termed as 'super-recursion'.  Use Google Chrome and headphones to experience the spatial audio.
* The [Abstractii Genesis](https://ordinals.com/inscription/b2de70a5658d3659b74a9a349e02ed9396318eab844073b6f35e726dea9e103ei0) collection uses the inscriptions ID as a seed to generate its art.
* The [Abstractii Evolved](https://ordinals.com/inscription/c56795a454a30bb6866686770f14d015d53f3cf2ddfa5154e34a7cd1120a51efi0) generative collection uses the recursive blockheight endpoint as a seed to generate its art.
* [This code](http://ordinals.com/content/eafb859825cd843587d39552eb7a52f352e9621cd16b63b8702c1b8ea44faf1ci0) is called recursively in [this inscription](https://ordinals.com/inscription/60445754) to generate music.
* [This code](https://ordinals.com/content/e48af8aebe608656a3739393d1270d88285ab3051fb800743509d82bcf163623i0) is called recursively in [this inscription](https://ordinals.com/inscription/70940369), allowing it to function as a pixel art drawing program.

ord/docs/src/inscriptions/metadata.md


Metadata
========

Inscriptions may include [CBOR](https://cbor.io/) metadata, stored as data
pushes in fields with tag `5`. Since data pushes are limited to 520 bytes,
metadata longer than 520 bytes must be split into multiple tag `5` fields,
which will then be concatenated before decoding.

Metadata is human readable, and all metadata will be displayed to the user with
its inscription. Inscribers are encouraged to consider how metadata will be
displayed, and make metadata concise and attractive.

Metadata is rendered to HTML for display as follows:

- `null`, `true`, `false`, numbers, floats, and strings are rendered as plain
  text.
- Byte strings are rendered as uppercase hexadecimal.
- Arrays are rendered as `<ul>` tags, with every element wrapped in `<li>`
  tags.
- Maps are rendered as `<dl>` tags, with every key wrapped in `<dt>` tags, and
  every value wrapped in `<dd>` tags.
- Tags are rendered as the tag , enclosed in a `<sup>` tag, followed by the
  value.

CBOR is a complex spec with many different data types, and multiple ways of
representing the same data. Exotic data types, such as tags, floats, and
bignums, and encoding such as indefinite values, may fail to display correctly
or at all. Contributions to `ord` to remedy this are welcome.

Example
-------

Since CBOR is not human readable, in these examples it is represented as JSON.
Keep in mind that this is *only* for these examples, and JSON metadata will
*not* be displayed correctly.

The metadata `{"foo":"bar","baz":[null,true,false,0]}` would be included in an inscription as:

```
OP_FALSE
OP_IF
    ...
    OP_PUSH 0x05 OP_PUSH '{"foo":"bar","baz":[null,true,false,0]}'
    ...
OP_ENDIF
```

And rendered as:

```
<dl>
  ...
  <dt>metadata</dt>
  <dd>
    <dl>
      <dt>foo</dt>
      <dd>bar</dd>
      <dt>baz</dt>
      <dd>
        <ul>
          <li>null</li>
          <li>true</li>
          <li>false</li>
          <li>0</li>
        </ul>
      </dd>
    </dl>
  </dd>
  ...
</dl>
```

Metadata longer than 520 bytes must be split into multiple fields:

```
OP_FALSE
OP_IF
    ...
    OP_PUSH 0x05 OP_PUSH '{"very":"long","metadata":'
    OP_PUSH 0x05 OP_PUSH '"is","finally":"done"}'
    ...
OP_ENDIF
```

Which would then be concatenated into
`{"very":"long","metadata":"is","finally":"done"}`.


See
[examples](examples.md#metadata) for on-chain examples of inscriptions that feature this functionality.

ord/docs/src/inscriptions/pointer.md


Pointer
=======

In order to make an inscription on a sat other than the first of its input, a
zero-based integer, called the "pointer", can be provided with tag `2`, causing
the inscription to be made on the sat at the given position in the outputs. If
the pointer is equal to or greater than the number of total sats in the outputs
of the inscribe transaction, it is ignored, and the inscription is made as
usual. The value of the pointer field is a little endian integer, with trailing
zeroes ignored.

An even tag is used, so that old versions of `ord` consider the inscription to
be unbound, instead of assigning it, incorrectly, to the first sat.

This can be used to create multiple inscriptions in a single transaction on
different sats, when otherwise they would be made on the same sat.

Examples
--------

An inscription with pointer 255:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 2
  OP_PUSH 0xff
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF
```

An inscription with pointer 256:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 2
  OP_PUSH 0x0001
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF
```

An inscription with pointer 256, with trailing zeroes, which are ignored:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 2
  OP_PUSH 0x000100
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF
```

ord/docs/src/inscriptions/properties.md


Properties
==========

Inscriptions may include [CBOR](https://cbor.io/) properties, stored as data
pushes in fields with tag `17`. Since data pushes are limited to 520 bytes,
CBOR longer than 520 bytes must be split into multiple tag `17` fields, which
will then be concatenated before decoding.

Properties are a structured counterpart to [metadata](metadata.md). While
metadata may contain arbitrary CBOR which has no protocol-defined meaning and
is presented on `/inscription` as HTML, properties have protocol-defined
meaning and must conform to a strict schema.

Indefinite-length types are not supported. All maps, arrays, byte strings, and
text strings must be definite.

The non-normative [CDDL](https://datatracker.ietf.org/doc/html/rfc8610) schema
of the properties value is as follows:

```cddl
Properties = {
  ? 0: [*GalleryItem],
  * any => any,
}

GalleryItem = {
  ? 0: bstr .size (32..36),
  * any => any,
}
```

The above CDDL schema is provided as a convenience. As always, the ordinals
reference implementation `ord` is the normative specification of inscriptions,
and thus the properties field.

Fields matching the `* any => any` wildcard must be ignored, for compatibility
with future additions.

Galleries
=========

Inscriptions whose properties field contains `GalleryItem`s are galleries.

Galleries contain `GalleryItem`s, whose only defined key `0` contains a
serialized inscription ID. Inscription ID `TXIDiINDEX` is serialized as a byte
string containing the 32 byte TXID, concatenated with by the four-byte
little-endian `INDEX`. Trailing zeros may be removed from four-byte `INDEX`, so
IDs ending in `i0` may be serialized in 32 bytes.

Gallery items are displayed on the inscriptions `/inscription` page on the
explorer.

Galleries are similar to children, in that they provide a way to create
collections of inscriptions. However, galleries are permissionless. Anyone may
create a gallery including any inscriptions. Thus, inclusion in a gallery does
not imply provenance. Additionally, because of this, inclusion in a gallery
does not create a backlink from the gallery item's `/inscription` page to the
gallery.

Galleries may be created when batch inscribing with `ord wallet batch` by
including an array of string inscription IDs of under the `gallery` key of the
inscription entry in the batch file, or when using `ord wallet inscribe` using
the `--gallery` option.

ord/docs/src/inscriptions/provenance.md


Provenance
==========

The owner of an inscription can create child inscriptions, trustlessly
establishing the provenance of those children on-chain as having been created
by the owner of the parent inscription. This can be used for collections, with
the children of a parent inscription being members of the same collection.

Children can themselves have children, allowing for complex hierarchies. For
example, an artist might create an inscription representing themselves, with
sub inscriptions representing collections that they create, with the children
of those sub inscriptions being items in those collections.

### Specification

To create a child inscription C with parent inscription P:

- Create an inscribe transaction T as usual for C.
- Spend the parent P in one of the inputs of T.
- Include tag `3`, i.e. `OP_PUSH 3`, in C, with the value of the serialized
  binary inscription ID of P, serialized as the 32-byte `TXID`, followed by the
  four-byte little-endian `INDEX`, with trailing zeroes omitted.

_NB_ The bytes of a bitcoin transaction ID are reversed in their text
representation, so the serialized transaction ID will be in the opposite order.

### Example

An example of a child inscription of
`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0`:

```
OP_FALSE
OP_IF
  OP_PUSH "ord"
  OP_PUSH 1
  OP_PUSH "text/plain;charset=utf-8"
  OP_PUSH 3
  OP_PUSH 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
  OP_PUSH 0
  OP_PUSH "Hello, world!"
OP_ENDIF
```

Note that the value of tag `3` is binary, not hex, and that for the child
inscription to be recognized as a child,
`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi0` must be
spent as one of the inputs of the inscribe transaction.

Example encoding of inscription ID
`000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi255`:

```
OP_FALSE
OP_IF
  …
  OP_PUSH 3
  OP_PUSH 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100ff
  …
OP_ENDIF
```

And of inscription ID `000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1fi256`:

```
OP_FALSE
OP_IF
  …
  OP_PUSH 3
  OP_PUSH 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807060504030201000001
  …
OP_ENDIF
```

### Notes

The tag `3` is used because it is the first available odd tag. Unrecognized odd
tags do not make an inscription unbound, so child inscriptions would be
recognized and tracked by old versions of `ord`.

A collection can be closed by burning the collection's parent inscription,
which guarantees that no more items in the collection can be issued.


See
[examples](examples.md#provenance) for on-chain examples of inscriptions that feature this functionality.

ord/docs/src/inscriptions/recursion.md


Recursion
=========

An important exception to [sandboxing](../inscriptions.md#sandboxing) is
recursion. Recursive endpoints are whitelisted endpoints that allow access to
on-chain data, including the content of other inscriptions.

Since changes to recursive endpoints might break inscriptions that rely on
them, recursive endpoints have backwards-compatibility guarantees not shared by
`ord server`'s other endpoints. In particular:

- Recursive endpoints will not be removed
- Object fields returned by recursive endpoints will not be renamed or change types

However, additional object fields may be added or reordered, so inscriptions
must handle additional, unexpected fields, and must not expect fields to be
returned in a specific order.

Recursion has a number of interesting use-cases:

- Remixing the content of existing inscriptions.

- Publishing snippets of code, images, audio, or stylesheets as shared public
  resources.

- Generative art collections where an algorithm is inscribed as JavaScript,
  and instantiated from multiple inscriptions with unique seeds.

- Generative profile picture collections where accessories and attributes are
  inscribed as individual images, or in a shared texture atlas, and then
  combined, collage-style, in unique combinations in multiple inscriptions.

## Endpoints

<details>
  <summary>
    <code>GET</code>
    <code><b>/content/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

The content of the inscription with `<INSCRIPTION_ID>`.

### Example

```bash
curl -s \
  http://0.0.0.0:80/content/6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 > skull.jpg
```

<i>no terminal output, just file creation</i>
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/blockhash</b></code>
  </summary>

### Description
Latest block hash.

### Example
```bash
curl -s  \
  http://0.0.0.0:80/r/blockhash
```

```json
"00000000000000000002891b440944e0ce40b37b6ccaa138c280e9edfc319d5d"
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/blockhash/&lt;HEIGHT&gt;</b></code>
  </summary>

### Description

Block hash at given block height as JSON string.

### Example

```bash
curl -s  \
  http://0.0.0.0:80/r/blockhash/840000
```

```json
"0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5"
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/blockheight</b></code>
  </summary>

### Description

Latest block height.

### Example

```bash
curl -s  \
  http://0.0.0.0:80/r/blockheight
```

```json
866393
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/blockinfo/&lt;QUERY&gt;</b></code>
  </summary>

### Description

Block info. `<QUERY>` may be a block height or block hash.

### Example (blockheight)

```bash
curl -s \
  http://0.0.0.0:80/r/blockinfo/0
```

```json
{
  "average_fee": 0,
  "average_fee_rate": 0,
  "bits": 486604799,
  "chainwork": "0000000000000000000000000000000000000000000000000000000100010001",
  "confirmations": 866396,
  "difficulty": 1.0,
  "hash": "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
  "feerate_percentiles": [
    0,
    0,
    0,
    0,
    0
  ],
  "height": 0,
  "max_fee": 0,
  "max_fee_rate": 0,
  "max_tx_size": 0,
  "median_fee": 0,
  "median_time": 1231006505,
  "merkle_root": "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
  "min_fee": 0,
  "min_fee_rate": 0,
  "next_block": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048",
  "nonce": 2083236893,
  "previous_block": null,
  "subsidy": 5000000000,
  "target": "00000000ffff0000000000000000000000000000000000000000000000000000",
  "timestamp": 1231006505,
  "total_fee": 0,
  "total_size": 0,
  "total_weight": 0,
  "transaction_count": 1,
  "version": 1
}
```

### Example (blockhash)

```bash
curl -s \
  http://0.0.0.0:80/r/blockinfo/0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5
```

```json
{
  "average_fee": 1234031,
  "average_fee_rate": 3770,
  "bits": 386089497,
  "chainwork": "0000000000000000000000000000000000000000753bdab0e0d745453677442b",
  "confirmations": 26397,
  "difficulty": 86388558925171.02,
  "hash": "0000000000000000000320283a032748cef8227873ff4872689bf23f1cda83a5",
  "feerate_percentiles": [
    108,
    134,
    200,
    350,
    1063
  ],
  ],
  "height": 840000,
  "height": 840000,
  "max_fee": 799987800,
  "max_fee_rate": 3604819,
  "max_tx_size": 166989,
  "median_fee": 34800,
  "median_fee": 34800,
  "median_time": 1713570208,
  "merkle_root": "031b417c3a1828ddf3d6527fc210daafcc9218e81f98257f88d4d43bd7a5894f",
  "min_fee": 2060,
  "min_fee_rate": 15,
  "next_block": "00000000000000000001b48a75d5a3077913f3f441eb7e08c13c43f768db2463",
  "nonce": 3932395645,
  "previous_block": "0000000000000000000172014ba58d66455762add0512355ad651207918494ab",
  "subsidy": 312500000,
  "target": "0000000000000000000342190000000000000000000000000000000000000000",
  "timestamp": 1713571767,
  "total_fee": 3762561499,
  "total_size": 2325218,
  "total_weight": 3991793,
  "transaction_count": 3050,
  "version": 710926336
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/blocktime</b></code>
  </summary>

### Description

UNIX time stamp of latest block.

### Example

```bash
curl -s  \
  http://0.0.0.0:80/r/blocktime
```

```json
1729362253
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/children/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

The first 100 child inscription ids.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/children/e317a2a5d68bd1004ae15a06175a319272a10389ff125c98820389edef8b0a94i0
```

```json
{
  "ids": [
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei0",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei1",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei2",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei3",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei4",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei5",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei6",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei7",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei8",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei9",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei10",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei11",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei12",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei13",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei14",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei15",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei16",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei17",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei18",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei19",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei20",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei21",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei22",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei23",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei24",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei25",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei26",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei27",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei28",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei29",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei30",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei31",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei32",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei33",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei34",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei35",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei36",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei37",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei38",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei39",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei40",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei41",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei42",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei43",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei44",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei45",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei46",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei47",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei48",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei49",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei50",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei51",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei52",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei53",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei54",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei55",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei56",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei57",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei58",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei59",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei60",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei61",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei62",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei63",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei64",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei65",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei66",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei67",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei68",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei69",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei70",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei71",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei72",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei73",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei74",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei75",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei76",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei77",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei78",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei79",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei80",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei81",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei82",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei83",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei84",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei85",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei86",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei87",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei88",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei89",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei90",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei91",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei92",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei93",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei94",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei95",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei96",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei97",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei98",
    "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei99"
  ],
  "more": true,
  "page": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/children/&lt;INSCRIPTION_ID&gt;/&lt;PAGE&gt;</b></code>
  </summary>

### Description

The set of 100 child inscription ids on `<PAGE>`.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/children/e317a2a5d68bd1004ae15a06175a319272a10389ff125c98820389edef8b0a94i0/9
```

```json
{
  "ids": [
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci60",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci61",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci62",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci63",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci64",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci65",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci66",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci67",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci68",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci69",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci70",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci71",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci72",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci73",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci74",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci75",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci76",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci77",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci78",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci79",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci80",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci81",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci82",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci83",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci84",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci85",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci86",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci87",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci88",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci89",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci90",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci91",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci92",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci93",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci94",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci95",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci96",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci97",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci98",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci99",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci100",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci101",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci102",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci103",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci104",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci105",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci106",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci107",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci108",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci109",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci110",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci111",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci112",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci113",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci114",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci115",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci116",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci117",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci118",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci119",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci120",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci121",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci122",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci123",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci124",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci125",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci126",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci127",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci128",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci129",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci130",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci131",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci132",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci133",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci134",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci135",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci136",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci137",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci138",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci139",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci140",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci141",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci142",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci143",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci144",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci145",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci146",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci147",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci148",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci149",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci150",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci151",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci152",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci153",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci154",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci155",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci156",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci157",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci158",
    "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci159"
  ],
  "more": true,
  "page": 9
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/children/&lt;INSCRIPTION_ID&gt;/inscriptions</b></code>
  </summary>

### Description

Details of first 100 child inscriptions of `INSCRIPTION_ID`.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/children/e317a2a5d68bd1004ae15a06175a319272a10389ff125c98820389edef8b0a94i0/inscriptions
```

```json
{
  "children": [
    {
      "charms": [],
      "fee": 417,
      "height": 861224,
      "id": "89e4fb2e5ea5c6301b9ac915d1d05619776f5ca41fc02fb6e5dced16f2cabfdei0",
      "number": 75744297,
      "output": "236ce10d9cd3f9f7f824a07686f7d7bce0d64a400f0328ce5bb2191a60d15262:0",
      "sat": null,
      "satpoint": "236ce10d9cd3f9f7f824a07686f7d7bce0d64a400f0328ce5bb2191a60d15262:0:0",
      "timestamp": 1726282054
    },
    ...
  ],
  "more": true,
  "page": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/children/&lt;INSCRIPTION_ID&gt;/inscriptions/&lt;PAGE&gt;</b></code>
  </summary>

### Description

Details of 100 child inscriptions of `INSCRIPTION_ID` paginated by `PAGE`.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/children/e317a2a5d68bd1004ae15a06175a319272a10389ff125c98820389edef8b0a94i0/inscriptions/9
```

```json
{
  "children": [
    {
      "charms": [
        "vindicated"
      ],
      "fee": 418,
      "height": 861239,
      "id": "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci60",
      "number": 75750346,
      "output": "e8ebadbd9ce4e4372b1b9b30fd5cb831c1f48ff2d0f8f1d1de2e190a2f5bcbe8:1",
      "sat": null,
      "satpoint": "e8ebadbd9ce4e4372b1b9b30fd5cb831c1f48ff2d0f8f1d1de2e190a2f5bcbe8:1:0",
      "timestamp": 1726292222
    },
    {
      "charms": [
        "vindicated"
      ],
      "fee": 418,
      "height": 861239,
      "id": "b205c9d1dc054f24c13aeb886fba42d9dd0aac3cd9bdc4f034affc90f3a0bf3ci61",
      "number": 75750347,
      "output": "aa46f14bec8842edd7b7c1b79224cd186dda6c5577cd65196da77d7e27b00b0c:0",
      "sat": null,
      "satpoint": "aa46f14bec8842edd7b7c1b79224cd186dda6c5577cd65196da77d7e27b00b0c:0:0",
      "timestamp": 1726292222
    },
    ...
  ],
  "more": true,
  "page": 9
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/undelegated-content/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

Undelegated content of an inscription.

</details>


<details>
  <summary>
    <code>GET</code>
    <code><b>/r/inscription/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

Information about an inscription.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/inscriptions/13130e4b299ed361f2a734f6433844ef0f0211cd504e0ca8f4d4ab20f51b8127i0
```

```json
{
  "charms": [
    "vindicated"
  ],
  "content_type": "model/gltf-binary",
  "content_length": 3726620,
  "delegate": null,
  "fee": 7499396,
  "height": 866266,
  "id": "13130e4b299ed361f2a734f6433844ef0f0211cd504e0ca8f4d4ab20f51b8127i0",
  "number": 76545890,
  "output": "13130e4b299ed361f2a734f6433844ef0f0211cd504e0ca8f4d4ab20f51b8127:1",
  "sat": null,
  "satpoint": "13130e4b299ed361f2a734f6433844ef0f0211cd504e0ca8f4d4ab20f51b8127:1:0",
  "timestamp": 1729297535,
  "value": 1313,
  "address": "bc1phj8hgzeptthkur9se2jq5vex7vlyhc8ul689svxea0xsn6r43z7sekz6qh"
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/metadata/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

JSON string containing the hex-encoded CBOR metadata.

### Example
```bash
curl -s \
  http://0.0.0.0:80/r/metadata/b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0
```

```json
"ac6c50484f544f475241504845526a5041524b4552204441596643414d4552416c43414e4f4e20454f532d31566446494c4d6f4b4f44414b20454b54415220313030644c454e53781a5a4549535320504c414e415220542a2038354d4d20462f312e346d5348555454455220535045454465312f31323568415045525455524563462f38664d4f44454c5318646650484f544f531903e8684c4f434154494f4e774c4f5320414e47454c45532c2043414c49464f524e49416443524557a36a415353495354414e4345826e41524941532042555244454c4c49684e4153204e495858664d414b45555087754544454e2053594d4f4e45204c415454414e5a494f6a4d494d49204d455945526e53414d414e544841204c455052456f4c4953455454452053414e54414e416e4a45535349434120564552474f4e63504f4e724d415941204e414b415241205352554f4348644841495283694a414b4920494348556c4a4f43454c594e2056454741724a4546464552534f4e2054414e475241444966504154524f4e6e434153455920524f4441524d4f52674c4943454e534563434330"
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/parents/&lt;INSCRIPTION_ID&gt;</b></code>
  </summary>

### Description

The first 100 parent inscription ids.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/parents/b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0
```

```json
{
  "ids": [
    "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
  ],
  "more": false,
  "page_index": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/parents/&lt;INSCRIPTION_ID&gt;/&lt;PAGE&gt;</b></code>
  </summary>

### Description

The set of 100 parent inscription ids on `<PAGE>`.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/parents/b1ef66c2d1a047cbaa6260b74daac43813924378fe08ef8545da4cb79e8fcf00i0/9
```

```json
{
  "ids": [],
  "more": false,
  "page_index": 9
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/parents/&lt;INSCRIPTION_ID&gt;/inscriptions</b></code>
  </summary>

### Description

Details of the first 100 parent inscriptions.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/r/parents/4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019i0/inscriptions
```

```json
{
  "parents": [
    {
      "charms": [],
      "fee": 21730,
      "height": 775167,
      "id": "92c409fb749b1005fe9a1482d3a74a8e73936a72644f4979df8184aba473841di0",
      "number": 4573,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:13",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:13:0",
      "timestamp": 1675607405
    },
    {
      "charms": [],
      "fee": 14977,
      "height": 775167,
      "id": "c689cbcb8e31858c5e1476d04af4e7e7cedd1fb4fb9cae5bb62036936a08282di0",
      "number": 4576,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:14",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:14:0",
      "timestamp": 1675607405
    },
    {
      "charms": [],
      "fee": 12533,
      "height": 775167,
      "id": "982d15f6b3510307ef845f1cb3352b27e2b048616b7c0642367ebc05bbd36d3ai0",
      "number": 4578,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:12",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:12:0",
      "timestamp": 1675607405
    }
    ...
  ],
  "more": true,
  "page": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/parents/&lt;INSCRIPTION_ID&gt;/inscriptions/&lt;PAGE&gt;</b></code>
  </summary>

### Description

Details of the set of 100 parent inscriptions on &lt;PAGE&gt;.

### Example

```bash
curl -s -H "Accept: application/json" \
  http://0.0.0.0:80/r/parents/4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019i0/inscriptions/1
```

```json
{
  "parents": [
    {
      "charms": [],
      "fee": 65049,
      "height": 775443,
      "id": "972994a55c338e8458bfd156642f4aa56bdab54c68658d6b64d932fedef3c81fi0",
      "number": 10804,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:102",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:102:0",
      "timestamp": 1675780989
    },
    {
      "charms": [],
      "fee": 60111,
      "height": 775443,
      "id": "dbc21f2d3323df24a378fef3bdbe4e79c4947ce7da54968affcdefa7eda80d21i0",
      "number": 10805,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:110",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:110:0",
      "timestamp": 1675780989
    },
    {
      "charms": [],
      "fee": 49881,
      "height": 775443,
      "id": "97870f7cf65992a66d0413a7e6773190e686f185500f78c30f989f2d1f1ba922i0",
      "number": 10806,
      "output": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:101",
      "sat": null,
      "satpoint": "4a86d375a70a4ecc7ffcd910e05f5e0771ae6a50133543f1bf6b5651adbf0019:101:0",
      "timestamp": 1675780989
    }
    ...
  ],
  "more": false,
  "page": 1
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/sat/&lt;SAT_NUMBER&gt;</b></code>
  </summary>

### Description

The first 100 inscription ids on a sat. Requires index with `--index-sats` flag.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/sat/153899938226999
```

```json
{
  "ids": [
    "f4ad941ee3892598f34777c4b7b3e2ccccece58ab21aa4364d0d2066daf5b427i3",
    "a4bca99fba23122e113bfb9a8010095b2005c4d73fa5b5532de60752b768a3e5i0",
    "11b4097bc9ff238c930ed4df44a6a5943ac1b570d424d7e13425244e3f345db7i0",
    "488c32e4dfcdc0fa376c2c2af2d572a12f4d33d3245689d1a9f74167f1e14678i0"
  ],
  "more": false,
  "page": 0
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/sat/&lt;SAT_NUMBER&gt;/&lt;PAGE&gt;</b></code>
  </summary>

### Description

The set of 100 inscription ids on `<PAGE>`. Requires index with `--index-sats` flag.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/sat/1499676120331756/1
```

```json
{
  "ids": [
    "c18b2db646cd23b9745bd40a249fc84975b1105a637f3784aa4dbd46a839750fi0",
    "7d7c2db251779ea4147ed881daac210bfa416f39846b60e3e6813b713a393d9ai0",
    "f42913d8c95f055b586fa9a6c71d2432c7ac860a9a4524c0abf83b1dbe175383i0",
    "52fd615dc56a8efb241e4de141692cfa57b1af0ac5d65da7e9d5f12841c2c56ci0",
    "cd65b92b9d4080a850eaf2c67c8e0c40c61ecdebeea9ae03936947f981a7b54ai0",
    "708ac95fe35bcfef5403f13e5e32c927adb413ce39597abc20f8e8fa4fa1d005i0",
    "2399e57a8f598b4487dda149942404e5002321139997280c736dcd0c3a806672i0",
    "4a2b37c1e017646a9ba2aa13487ae55b8621972aac349426df680eaf66b90571i0",
    "2a7b8b23f2a36bcff7ab23013cd13b303b8797cfac75e88d4daf1a9ddcdbdc6ai0",
    "b4cac4e0c9a9ccf6428c1e3869bbbcc0e094d39d972094af21a3ca342a9afedbi0",
    "c5f4bb989cc8bca10079287272d07b77b562938eaad35b3dface018cb6ac1c38i0"
  ],
  "more": false,
  "page": 1
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/sat/&lt;SAT_NUMBER&gt;/at/&lt;INDEX&gt;</b></code>
  </summary>

### Description

The inscription id at `<INDEX>` of all inscriptions on a sat. `<INDEX>` may be
a negative number to index from the back. `0` being the first and `-1` being
the most recent for example. Requires index with `--index-sats` flag.

### Example

```bash
curl -s \
  http://0.0.0.0:80/r/sat/153899938226999/at/-1
```

```json
{
  "id": "488c32e4dfcdc0fa376c2c2af2d572a12f4d33d3245689d1a9f74167f1e14678i0"
}
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/sat/&lt;SAT_NUMBER&gt;/at/&lt;INDEX&gt;/content</b></code>
  </summary>

### Description

The content of the inscription at `<INDEX>` on a sat. `<INDEX>` may be a
negative number to index from the back. `0` being the first and `-1` being the
most recent. Requires index with `--index-sats` flag.

### Example

Fetch the content of the most recently created inscription on sat 289488340427831.

```bash
curl -s \
  http://0.0.0.0:80/r/sat/289488340427831/at/-1/content
```

```
Hello, world!
```

</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/tx/&lt;TRANSACTION_ID&gt;</b></code>
  </summary>

### Description

Get hex-encoded transaction with `<TRANSACTION_ID>`. In the event of a future
change to Bitcoin that changes transaction serialization in a
backwards-incompatible fashion, such as SegWit, this endpoint is guaranteed to
remain backwards compatible.

### Example

```bash
curl -s http://0.0.0.0:80/r/tx/60bcf821240064a9c55225c4f01711b0ebbcab39aa3fafeefe4299ab158536fa
```

```json
"0100000000010183572872dcb32bee57003d53c2b8dbb5bc5819ff6478052599911f7778d1c7bd0000000000fdffffff011027000000000000225120e41e0cba05c6ac797cf543ff9a6c619a91a53813e59146d1e32ea89747b111a603407aa50d93d6fc01265fd52d3edc93af4e009ccc1a704ce1b5cb8ede1412a5df31eba587d080b3dc903ceb9002ed9d921aad323fd44d7b4dc2a1ad2ea12d4360424d20c7a3a38df198a4fcde7d5dac5819ed19ff4d25bb893c9511f8e1f51d59326effac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800077072696d65730a6821c1c7a3a38df198a4fcde7d5dac5819ed19ff4d25bb893c9511f8e1f51d59326eff00000000"
```
</details>

<details>
  <summary>
    <code>GET</code>
    <code><b>/r/utxo/&lt;OUTPOINT&gt;</b></code>
  </summary>

### Description

Get assets held by an unspent transaction output.

### Examples

Unspent transaction output with server without any indices:

```bash
curl -s \
  http://0.0.0.0:80/r/utxo/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0
```

```json
{
  "inscriptions": null,
  "runes": null,
  "sat_ranges": null,
  "value": 5000000000
}
```

With rune, inscription, and sat index:

```bash
curl -s \
  http://0.0.0.0:80/r/utxo/626860df36c1047194866c6812f04c15ab84f3690e7cc06fd600c841f1943e05:0
```

```json
{
  "inscriptions": [
    "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
  ],
  "runes": {
    "UNCOMMON•GOODS": {
      "amount": 6845,
      "divisibility": 0,
      "symbol": "⧉"
    }
  },
  "sat_ranges": [
    [
      1905800627509113,
      1905800627509443
    ]
  ],
  "value": 330
}
```
</details>

&nbsp;
&nbsp;

Note: `<SAT_NUMBER>` only allows the actual number of a sat, not other sat
notations like degree, percentile or decimal. We may expand to allow those in
the future.

Responses from most of the above recursive endpoints are JSON. For backwards
compatibility, some endpoints are supported which only return
plain-text responses.

- `/blockheight`: latest block height.
- `/blockhash`: latest block hash.
- `/blockhash/<HEIGHT>`: block hash at given block height.
- `/blocktime`: UNIX time stamp of latest block.


See
[examples](examples.md#recursion) for on-chain examples of inscriptions that feature this functionality.

ord/docs/src/inscriptions/rendering.md


Rendering
=========

Aspect Ratio
------------

Inscriptions should be rendered with a square aspect ratio. Non-square aspect
ratio inscriptions should not be cropped, and should instead be centered and
resized to fit within their container.

Maximum Size
------------

The `ord` explorer, used by [ordinals.com](https://ordinals.com/), displays
inscription previews with a maximum size of 576 by 576 pixels, making it a
reasonable choice when choosing a maximum display size.

Image Rendering
---------------

The CSS `image-rendering` property controls how images are resampled when
upscaled and downscaled.

When downscaling image inscriptions, `image-rendering: auto`, should be used.
This is desirable even when downscaling pixel art.

When upscaling image inscriptions other than AVIF, `image-rendering: pixelated`
should be used. This is desirable when upscaling pixel art, since it preserves
the sharp edges of pixels. It is undesirable when upscaling non-pixel art, but
should still be used for visual compatibility with the `ord` explorer.

When upscaling AVIF and JPEG XL inscriptions, `image-rendering: auto` should be
used. This allows inscribers to opt-in to non-pixelated upscaling for non-pixel
art inscriptions. Until such time as JPEG XL is widely supported by browsers,
it is not a recommended image format.

ord/docs/src/inscriptions/uris.md


URIs
====

*This document is a draft. It should be considered provisional and subject to
change at any time. The `ord:` schema has not been registered with the IANA.*

Inscriptions content can be addressed with inscription URIs using the `ord:`
schema.

Inscription URIs consist of `ord:` followed by a target inscription ID. `ord:`
is not followed by `//`, since the schema-specific part of inscription URIs,
namely the target inscription ID, does not contain a hierarchical structure.

For example, the inscription URI of the genesis inscription is:

```
ord:6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0
```

Inscription URIs match the following verbose regular expression:

```
(?i)            # case-insensitive
ord:            # schema
[0-9a-f]{64}    # transaction ID
i               # separator
(0|[1-9][0-9]*) # inscription index
```

Inscription URIs are case-insensitive and can thus use the more compact
alphanumeric mode when encoded as QR codes. Lowercase is, however, the
preferred presentation style.

The referent of an inscription URI is an HTTP resource with the content,
content type, content encoding, and content length corresponding to the
inscription with the given ID.

The referent of an inscription URI is always the original content of the target
inscription, and not the content of the delegate, regardless of whether or not
the target inscription has a delegate.

ord/docs/src/introduction.md


Introduction
============

This handbook is a guide to ordinal theory. Ordinal theory concerns itself with
satoshis, giving them individual identities and allowing them to be tracked,
transferred, and imbued with meaning.

Satoshis, not bitcoin, are the atomic, native currency of the Bitcoin network.
One bitcoin can be sub-divided into 100,000,000 satoshis, but no further.

Ordinal theory does not require a sidechain or token aside from Bitcoin, and
can be used without any changes to the Bitcoin network. It works right now.

Ordinal theory imbues satoshis with numismatic value, allowing them to be
collected and traded as curios.

Individual satoshis can be inscribed with arbitrary content, creating unique
Bitcoin-native digital artifacts that can be held in Bitcoin wallets and
transferred using Bitcoin transactions. Inscriptions are as durable, immutable,
secure, and decentralized as Bitcoin itself.

Other, more unusual use-cases are possible: off-chain colored-coins, public key
infrastructure with key rotation, a decentralized replacement for the DNS. For
now though, such use-cases are speculative, and exist only in the minds of
fringe ordinal theorists.

For more details on ordinal theory, see the [overview](overview.md).

For more details on inscriptions, see [inscriptions](inscriptions.md).

When you're ready to get your hands dirty, a good place to start is with
[inscriptions](guides/wallet.md), a curious species of digital artifact
enabled by ordinal theory.


Authors
-------

- [Casey Rodarmor](https://github.com/casey)
- [Raph Japh](https://github.com/raphjaph)

Links
-----

- [GitHub](https://github.com/ordinals/ord/)
- [BIP](https://github.com/ordinals/ord/blob/master/bip.mediawiki)
- [Discord](https://discord.gg/ordinals)
- [Open Ordinals Institute Website](https://ordinals.org/)
- [Open Ordinals Institute X](https://x.com/ordinalsorg)
- [Mainnet Block Explorer](https://ordinals.com)
- [Signet Block Explorer](https://signet.ordinals.com)

Videos
------

- [Ordinal Theory Explained: Satoshi Serial Numbers and NFTs on Bitcoin](https://www.youtube.com/watch?v=rSS0O2KQpsI)
- [Ordinals Workshop with Rodarmor](https://www.youtube.com/watch?v=MC_haVa6N3I)

ord/docs/src/overview.md


Ordinal Theory Overview
=======================

Ordinals are a numbering scheme for satoshis that allows tracking and
transferring individual sats. These numbers are called [ordinal
numbers](https://ordinals.com). Satoshis are numbered in the order in which
they're mined, and transferred from transaction inputs to transaction outputs
first-in-first-out. Both the numbering scheme and the transfer scheme rely on
*order*, the numbering scheme on the *order* in which satoshis are mined, and
the transfer scheme on the *order* of transaction inputs and outputs. Thus the
name, *ordinals*.

Technical details are available in [the
BIP](https://github.com/ordinals/ord/blob/master/bip.mediawiki).

Ordinal theory does not require a separate token, another blockchain, or any
changes to Bitcoin. It works right now.

Ordinal numbers have a few different representations:

- *Integer notation*:
  [`2099994106992659`](https://ordinals.com/sat/2099994106992659) The
  ordinal number, assigned according to the order in which the satoshi was
  mined.

- *Decimal notation*:
  [`3891094.16797`](https://ordinals.com/sat/3891094.16797) The first
  number is the block height in which the satoshi was mined, the second the
  offset of the satoshi within the block.

- *Degree notation*:
  [`3°111094′214″16797‴`](https://ordinals.com/sat/3%C2%B0111094%E2%80%B2214%E2%80%B316797%E2%80%B4).
  We'll get to that in a moment.

- *Percentile notation*:
  [`99.99971949060254%`](https://ordinals.com/sat/99.99971949060254%25) .
  The satoshi's position in Bitcoin's supply, expressed as a percentage.

- *Name*: [`satoshi`](https://ordinals.com/sat/satoshi). An encoding of the
  ordinal number using the characters `a` through `z`.

Arbitrary assets, such as NFTs, security tokens, accounts, or stablecoins can
be attached to satoshis using ordinal numbers as stable identifiers.

Ordinals is an open-source project, developed [on
GitHub](https://github.com/ordinals/ord). The project consists of a BIP describing
the ordinal scheme, an index that communicates with a Bitcoin Core node to
track the location of all satoshis, a wallet that allows making ordinal-aware
transactions, a block explorer for interactive exploration of the blockchain,
functionality for inscribing satoshis with digital artifacts, and this manual.

Rarity
------

Humans are collectors, and since satoshis can now be tracked and transferred,
people will naturally want to collect them. Ordinal theorists can decide for
themselves which sats are rare and desirable, but there are some hints…

Bitcoin has periodic events, some frequent, some more uncommon, and these
naturally lend themselves to a system of rarity. These periodic events are:

- *Blocks*: A new block is mined approximately every 10 minutes, from now until
  the end of time.

- *Difficulty adjustments*: Every 2016 blocks, or approximately every two
  weeks, the Bitcoin network responds to changes in hashrate by adjusting the
  difficulty target which blocks must meet in order to be accepted.

- *Halvings*: Every 210,000 blocks, or roughly every four years, the amount of
  new sats created in every block is cut in half.

- *Cycles*: Every six halvings, something magical happens: the halving and the
  difficulty adjustment coincide. This is called a conjunction, and the time
  period between conjunctions a cycle. A conjunction occurs roughly every 24
  years. The first conjunction should happen sometime in 2032.

This gives us the following rarity levels:

- `common`: Any sat that is not the first sat of its block
- `uncommon`: The first sat of each block
- `rare`: The first sat of each difficulty adjustment period
- `epic`: The first sat of each halving epoch
- `legendary`: The first sat of each cycle
- `mythic`: The first sat of the genesis block

Which brings us to degree notation, which unambiguously represents an ordinal
number in a way that makes the rarity of a satoshi easy to see at a glance:

```
A°B′C″D‴
│ │ │ ╰─ Index of sat in the block
│ │ ╰─── Index of block in difficulty adjustment period
│ ╰───── Index of block in halving epoch
╰─────── Cycle, numbered starting from 0
```

Ordinal theorists often use the terms "hour", "minute", "second", and "third"
for *A*, *B*, *C*, and *D*, respectively.

Now for some examples. This satoshi is common:

```
0°0′0″1‴
│ │ │ ╰─ Not first sat in block
│ │ ╰─── Any block in difficulty adjustment period
│ ╰───── Any block in halving epoch
╰─────── Any cycle
```


This satoshi is uncommon:

```
0°1′1″0‴
│ │ │ ╰─ First sat in block
│ │ ╰─── Not first block in difficulty adjustment period
│ ╰───── Not first block in halving epoch
╰─────── Any cycle
```

This satoshi is rare:

```
0°2016′0″0‴
│    │ │ ╰─ First sat in block
│    │ ╰─── First block in difficulty adjustment period
│    ╰───── Not the first block in halving epoch
╰────────── Any cycle
```

This satoshi is epic:

```
0°0′336″0‴
│ │   │ ╰─ First sat in block
│ │   ╰─── Not first block in difficulty adjustment period
│ ╰─────── First block in halving epoch
╰───────── Any cycle
```

This satoshi is legendary:

```
1°0′0″0‴
│ │ │ ╰─ First sat in block
│ │ ╰─── First block in difficulty adjustment period
│ ╰───── First block in halving epoch
╰─────── Any cycle but first
```

And this satoshi is mythic:

```
0°0′0″0‴
│ │ │ ╰─ First sat in block
│ │ ╰─── First block in difficulty adjustment period
│ ╰───── First block in halving epoch
╰─────── First cycle
```

If the block offset is zero, it may be omitted. This is the uncommon satoshi
from above:

```
1°1′1″
│ │ ╰─ Not first block in difficulty adjustment period
│ ╰─── Not first block in halving epoch
╰───── Second cycle
```

Rare Satoshi Supply
-------------------

### Total Supply

- `common`: 2,099,999,990,760,000
- `uncommon`: 6,926,535
- `rare`: 3432
- `epic`: 27
- `legendary`: 5
- `mythic`: 1

### Current Supply

- `common`: ~1.98 quadrillion
- `uncommon`: ~880,000 (a new uncommon is mined roughly every ten minutes)
- `rare`: ~430 (a new rare is mined roughly every two weeks)
- `epic`: 3
- `legendary`: 0
- `mythic`: 1

At the moment, even uncommon satoshis are quite rare. As of this writing,
876,023 uncommon satoshis have been mined - one per 22.6 bitcoin in
circulation.

Names
-----

Each satoshi has a name, consisting of the letters *A* through *Z*, that get
shorter the further into the future the satoshi was mined. They could start
short and get longer, but then all the good, short names would be trapped in
the unspendable genesis block.

As an example, 1905530482684727°'s name is "iaiufjszmoba". The name of the last
satoshi to be mined is "a". Every combination of 10 characters or less is out
there, or will be out there, someday.

Exotics
-------

Satoshis may be prized for reasons other than their name or rarity. This might
be due to a quality of the number itself, like having an integer square or cube
root. Or it might be due to a connection to a historical event, such as
satoshis from block 477,120, the block in which SegWit activated, or
2099999997689999°, the last satoshi that will ever be mined.

Such satoshis are termed "exotic". Which satoshis are exotic and what makes
them so is subjective. Ordinal theorists are encouraged to seek out exotics
based on criteria of their own devising.

Inscriptions
------------

Satoshis can be inscribed with arbitrary content, creating Bitcoin-native
digital artifacts. Inscribing is done by sending the satoshi to be inscribed in
a transaction that reveals the inscription content on-chain. This content is
then inextricably linked to that satoshi, turning it into an immutable digital
artifact that can be tracked, transferred, hoarded, bought, sold, lost, and
rediscovered.

Archaeology
-----------

A lively community of archaeologists devoted to cataloging and collecting early
NFTs has sprung up. [Here's a great summary of historical NFTs by
Chainleft.](https://mirror.xyz/chainleft.eth/MzPWRsesC9mQflxlLo-N29oF4iwCgX3lacrvaG9Kjko)

A commonly accepted cut-off for early NFTs is March 19th, 2018, the date the
first ERC-721 contract, [SU SQUARES](https://tenthousandsu.com/), was deployed
on Ethereum.

Whether or not ordinals are of interest to NFT archaeologists is an open
question! In one sense, ordinals were created in early 2022, when the Ordinals
specification was finalized. In this sense, they are not of historical
interest.

In another sense though, ordinals were in fact created by Satoshi Nakamoto in
2009 when he mined the Bitcoin genesis block. In this sense, ordinals, and
especially early ordinals, are certainly of historical interest.

Many ordinal theorists favor the latter view. This is not least because the
ordinals were independently discovered on at least two separate occasions, long
before the era of modern NFTs began.

On August 21st, 2012, Charlie Lee [posted a proposal to add proof-of-stake to
Bitcoin to the Bitcoin Talk
forum](https://bitcointalk.org/index.php?topic=102355.0). This wasn't an asset
scheme, but did use the ordinal algorithm, and was implemented but never
deployed.

On October 8th, 2012, jl2012 [posted a scheme to the same
forum](https://bitcointalk.org/index.php?topic=117224.0) which uses decimal
notation and has all the important properties of ordinals. The scheme was
discussed but never implemented.

These independent inventions of ordinals indicate in some way that ordinals
were discovered, or rediscovered, and not invented. The ordinals are an
inevitability of the mathematics of Bitcoin, stemming not from their modern
documentation, but from their ancient genesis. They are the culmination of a
sequence of events set in motion with the mining of the first block, so many
years ago.

ord/docs/src/runes.md


Runes
=====

Runes allow Bitcoin transactions to etch, mint, and transfer Bitcoin-native
digital commodities.

Whereas every inscription is unique, every unit of a rune is the same. They are
interchangeable tokens, fit for a variety of purposes.

Runestones
----------

Rune protocol messages, called runestones, are stored in Bitcoin transaction
outputs.

A runestone output's script pubkey begins with an `OP_RETURN`, followed by
`OP_13`, followed by zero or more data pushes. These data pushes are
concatenated and decoded into a sequence of 128-bit integers, and finally
parsed into a runestone.

A transaction may have at most one runestone.

A runestone may etch a new rune, mint an existing rune, and transfer runes from
a transaction's inputs to its outputs.

A transaction output may hold balances of any number of runes.

Runes are identified by IDs, which consist of the block in which a rune was
etched and the index of the etching transaction within that block, represented
in text as `BLOCK:TX`. For example, the ID of the rune etched in the 20th
transaction of the 500th block is `500:20`.

Etching
-------

Runes come into existence by being etched. Etching creates a rune and sets its
properties. Once set, these properties are immutable, even to its etcher.

### Name

Names consist of the letters A through Z and are between one and twenty-six
letters long. For example `UNCOMMONGOODS` is a rune name.

Names may contain spacers, represented as bullets, to aid readability.
`UNCOMMONGOODS` might be etched as `UNCOMMON•GOODS`.

The uniqueness of a name does not depend on spacers. Thus, a rune may not be
etched with the same sequence of letters as an existing rune, even if it has
different spacers.

Spacers can only be placed between two letters. Finally, spacers do not
count towards the letter count.

### Divisibility

A rune's divisibility is how finely it may be divided into its atomic units.
Divisibility is expressed as the number of digits permissible after the decimal
point in an amount of runes. A rune with divisibility 0 may not be divided. A
unit of a rune with divisibility 1 may be divided into ten sub-units, a rune
with divisibility 2 may be divided into a hundred, and so on.

### Symbol

A rune's currency symbol is a single Unicode code point, for example `$`, `⧉`,
or `🧿`, displayed after quantities of that rune.

101 atomic units of a rune with divisibility 2 and symbol `🧿` would be
rendered as `1.01 🧿`.

If a rune does not have a symbol, the generic currency sign `¤`, also called a
scarab, should be used.

### Premine

The etcher of a rune may optionally allocate to themselves units of the rune
being etched. This allocation is called a premine.

### Terms

A rune may have an open mint, allowing anyone to create and allocate units of
that rune for themselves. An open mint is subject to terms, which are set upon
etching.

A mint is open while all terms of the mint are satisfied, and closed when any
of them are not. For example, a mint may be limited to a starting height, an
ending height, and a cap, and will be open between the starting height and
ending height, or until the cap is reached, whichever comes first.

#### Cap

The number of times a rune may be minted is its cap. A mint is closed once the
cap is reached.

#### Amount

Each mint transaction creates a fixed amount of new units of a rune.

#### Start Height

A mint is open starting in the block with the given start height.

#### End Height

A rune may not be minted in or after the block with the given end height.

#### Start Offset

A mint is open starting in the block whose height is equal to the start offset
plus the height of the block in which the rune was etched.

#### End Offset

A rune may not be minted in or after the block whose height is equal to the end
offset plus the height of the block in which the rune was etched.

Minting
-------

While a rune's mint is open, anyone may create a mint transaction that creates
a fixed amount of new units of that rune, subject to the terms of the mint.

Transferring
------------

When transaction inputs contain runes, or new runes are created by a premine or
mint, those runes are transferred to that transaction's outputs. A
transaction's runestone may change how input runes transfer to outputs.

### Edicts

A runestone may contain any number of edicts. Edicts consist of a rune ID, an
amount, and an output number. Edicts are processed in order, allocating
unallocated runes to outputs.

### Pointer

After all edicts are processed, remaining unallocated runes are transferred to
the transaction's first non-`OP_RETURN` output. A runestone may optionally
contain a pointer that specifies an alternative default output.

### Burning

Runes may be burned by transferring them to an `OP_RETURN` output with an edict
or pointer.

Cenotaphs
---------

Runestones may be malformed for a number of reasons, including non-pushdata
opcodes in the runestone `OP_RETURN`, invalid varints, or unrecognized
runestone fields.

Malformed runestones are termed
[cenotaphs](https://en.wikipedia.org/wiki/Cenotaph).

Runes input to a transaction with a cenotaph are burned. Runes etched in a
transaction with a cenotaph are set as unmintable. Mints in a transaction with
a cenotaph count towards the mint cap, but the minted runes are burned.

Cenotaphs are an upgrade mechanism, allowing runestones to be given new
semantics that change how runes are created and transferred, while not
misleading unupgraded clients as to the location of those runes, as unupgraded
clients will see those runes as having been burned.

ord/docs/src/runes/specification.md


Runes Does Not Have a Specification
===================================

The Runes reference implementation, `ord`, is the normative specification of
the Runes protocol.

Nothing you read here or elsewhere, aside from the code of `ord`, is a
specification. This prose description of the runes protocol is provided as a
guide to the behavior of `ord`, and the code of `ord` itself should always be
consulted to confirm the correctness of any prose description.

If, due to a bug in `ord`, this document diverges from the actual behavior of
`ord` and it is impractically disruptive to change `ord`'s behavior, this
document will be amended to agree with `ord`'s actual behavior.

Users of alternative implementations do so at their own risk, and services
wishing to integrate Runes are strongly encouraged to use `ord` itself to make
Runes transactions, and to determine the state of runes, mints, and balances.

Runestones
----------

Rune protocol messages are termed "runestones".

The Runes protocol activates on block 840,000. Runestones in earlier blocks are
ignored.

Abstractly, runestones contain the following fields:

```rust
struct Runestone {
  edicts: Vec<Edict>,
  etching: Option<Etching>,
  mint: Option<RuneId>,
  pointer: Option<u32>,
}
```

Runes are created by etchings:

```rust
struct Etching {
  divisibility: Option<u8>,
  premine: Option<u128>,
  rune: Option<Rune>,
  spacers: Option<u32>,
  symbol: Option<char>,
  terms: Option<Terms>,
}
```

Which may contain mint terms:

```rust
struct Terms {
  amount: Option<u128>,
  cap: Option<u128>,
  height: (Option<u64>, Option<u64>),
  offset: (Option<u64>, Option<u64>),
}
```

Runes are transferred by edict:

```rust
struct Edict {
  id: RuneId,
  amount: u128,
  output: u32,
}
```

Rune IDs are encoded as the block height and transaction index of the
transaction in which the rune was etched:

```rust
struct RuneId {
  block: u64,
  tx: u32,
}
```

Rune IDs are represented in text as `BLOCK:TX`.

Rune names are encoded as modified base-26 integers:

```rust
struct Rune(u128);
```

### Deciphering

Runestones are deciphered from transactions with the following steps:

1. Find the first transaction output whose script pubkey begins with `OP_RETURN
   OP_13`.

2. Concatenate all following data pushes into a payload buffer.

3. Decode a sequence 128-bit [LEB128](https://en.wikipedia.org/wiki/LEB128)
   integers from the payload buffer.

4. Parse the sequence of integers into an untyped message.

5. Parse the untyped message into a runestone.

Deciphering may produce a malformed runestone, termed a
[cenotaph](https://en.wikipedia.org/wiki/Cenotaph).

#### Locating the Runestone Output

Outputs are searched for the first script pubkey that beings with `OP_RETURN
OP_13`. If deciphering fails, later matching outputs are not considered.

#### Assembling the Payload Buffer

The payload buffer is assembled by concatenating data pushes, after `OP_13`, in
the matching script pubkey.

Data pushes are opcodes 0 through 78 inclusive. If a non-data push opcode is
encountered, i.e., any opcode equal to or greater than opcode 79, the
deciphered runestone is a cenotaph with no etching, mint, or edicts.

#### Decoding the Integer Sequence

A sequence of 128-bit integers are decoded from the payload as LEB128 varints.

LEB128 varints are encoded as sequence of bytes, each of which has the
most-significant bit set, except for the last.

If a LEB128 varint contains more than 18 bytes, would overflow a u128, or is
truncated, meaning that the end of the payload buffer is reached before
encountering a byte with the continuation bit not set, the decoded runestone is
a cenotaph with no etching, mint, or edicts.

#### Parsing the Message

The integer sequence is parsed into an untyped message:

```rust
struct Message {
  fields: Map<u128, Vec<u128>>,
  edicts: Vec<Edict>,
}
```

The integers are interpreted as a sequence of tag/value pairs, with duplicate
tags appending their value to the field value.

If a tag with value zero is encountered, all following integers are interpreted
as a series of four-integer edicts, each consisting of a rune ID block height,
rune ID transaction index, amount, and output.

```rust
struct Edict {
  id: RuneId,
  amount: u128,
  output: u32,
}
```

Rune ID block heights and transaction indices in edicts are delta encoded.

Edict rune ID decoding starts with a base block height and transaction index of
zero. When decoding each rune ID, first the encoded block height delta is added
to the base block height. If the block height delta is zero, the next integer
is a transaction index delta. If the block height delta is greater than zero,
the next integer is instead an absolute transaction index.

This implies that edicts must first be sorted by rune ID before being encoded
in a runestone.

For example, to encode the following edicts:

| block | TX | amount | output |
|-------|----|--------|--------|
| 10    | 5  | 5      | 1      |
| 50    | 1  | 25     | 4      |
| 10    | 7  | 1      | 8      |
| 10    | 5  | 10     | 3      |

They are first sorted by block height and transaction index:

| block | TX | amount | output |
|-------|----|--------|--------|
| 10    | 5  | 5      | 1      |
| 10    | 5  | 10     | 3      |
| 10    | 7  | 1      | 8      |
| 50    | 1  | 25     | 4      |

And then delta encoded as:

| block delta | TX delta | amount | output |
|-------------|----------|--------|--------|
| 10          | 5        | 5      | 1      |
| 0           | 0        | 10     | 3      |
| 0           | 2        | 1      | 8      |
| 40          | 1        | 25     | 4      |

If an edict output is greater than the number of outputs of the transaction, an
edict rune ID is encountered with block zero and nonzero transaction index, or
a field is truncated, meaning a tag is encountered without a value, the decoded
runestone is a cenotaph.

Note that if a cenotaph is produced here, the cenotaph is not empty, meaning
that it contains the fields and edicts, which may include an etching and mint.

#### Parsing the Runestone

The runestone:

```rust
struct Runestone {
  edicts: Vec<Edict>,
  etching: Option<Etching>,
  mint: Option<RuneId>,
  pointer: Option<u32>,
}
```

Is parsed from the unsigned message using the following tags:

```rust
enum Tag {
  Body = 0,
  Flags = 2,
  Rune = 4,
  Premine = 6,
  Cap = 8,
  Amount = 10,
  HeightStart = 12,
  HeightEnd = 14,
  OffsetStart = 16,
  OffsetEnd = 18,
  Mint = 20,
  Pointer = 22,
  Cenotaph = 126,

  Divisibility = 1,
  Spacers = 3,
  Symbol = 5,
  Nop = 127,
}
```

Note that tags are grouped by parity, i.e., whether they are even or odd.
Unrecognized odd tags are ignored. Unrecognized even tags produce a cenotaph.

All unused tags are reserved for use by the protocol, may be assigned at any
time, and should not be used.

##### Body

The `Body` tag marks the end of the runestone's fields, causing all following
integers to be interpreted as edicts.

##### Flags

The `Flag` field contains a bitmap of flags, whose position is `1 <<
FLAG_VALUE`:

```rust
enum Flag {
  Etching = 0,
  Terms = 1,
  Turbo = 2,
  Cenotaph = 127,
}
```

The `Etching` flag marks this transaction as containing an etching.

The `Terms` flag marks this transaction's etching as having open mint terms.

The `Turbo` flag marks this transaction's etching as opting into future
protocol changes. These protocol changes may increase light client validation
costs, or just be highly degenerate.

The `Cenotaph` flag is unrecognized.

If the value of the flags field after removing recognized flags is nonzero, the
runestone is a cenotaph.

##### Rune

The `Rune` field contains the name of the rune being etched. If the `Etching`
flag is set but the `Rune` field is omitted, a reserved rune name is
allocated.

##### Premine

The `Premine` field contains the amount of premined runes.

##### Cap

The `Cap` field contains the allowed number of mints.

##### Amount

The `Amount` field contains the amount of runes each mint transaction receives.

##### HeightStart and HeightEnd

The `HeightStart` and `HeightEnd` fields contain the mint's starting and ending
absolute block heights, respectively. The mint is open starting in the block
with height `HeightStart`, and closes in the block with height `HeightEnd`.

##### OffsetStart and OffsetEnd

The `OffsetStart` and `OffsetEnd` fields contain the mint's starting and ending
block heights, relative to the block in which the etching is mined. The mint is
open starting in the block with height `OffsetStart` + `ETCHING_HEIGHT`, and
closes in the block with height `OffsetEnd` + `ETCHING_HEIGHT`.

##### Mint

The `Mint` field contains the Rune ID of the rune to be minted in this
transaction.

##### Pointer

The `Pointer` field contains the index of the output to which runes unallocated
by edicts should be transferred. If the `Pointer` field is absent, unallocated
runes are transferred to the first non-`OP_RETURN` output. If the pointer is
greater than the number of outputs, the runestone is a cenotaph.

##### Cenotaph

The `Cenotaph` field is unrecognized.

##### Divisibility

The `Divisibility` field, raised to the power of ten, is the number of subunits
in a super unit of runes.

For example, the amount `1234` of different runes with divisibility 0 through 3
is displayed as follows:

| Divisibility | Display |
|--------------|---------|
| 0            | 1234    |
| 1            | 123.4   |
| 2            | 12.34   |
| 3            | 1.234   |

##### Spacers

The `Spacers` field is a bitfield of `•` spacers that should be displayed
between the letters of the rune's name.

The Nth field of the bitfield, starting from the least significant, determines
whether or not a spacer should be displayed between the Nth and N+1th
character, starting from the left of the rune's name.

For example, the rune name `AAAA` rendered with different spacers:

| Spacers | Display |
|---------|---------|
| 0b1     | A•AAA   |
| 0b11    | A•A•AA  |
| 0b10    | AA•AA   |
| 0b111   | A•A•A•A |

Trailing spacers are ignored.

##### Symbol

The `Symbol` field is the Unicode codepoint of the Rune's currency symbol,
which should be displayed after amounts of that rune. If a rune does not have a
currency symbol, the generic currency character `¤` should be used.

For example, if the `Symbol` is `#` and the divisibility is 2, the amount of
`1234` units should be displayed as `12.34 #`.

##### Nop

The `Nop` field is unrecognized.

#### Cenotaphs

Cenotaphs have the following effects:

- All runes input to a transaction containing a cenotaph are burned.

- If the runestone that produced the cenotaph contained an etching, the etched
  rune has supply zero and is unmintable.

- If the runestone that produced the cenotaph is a mint, the mint counts
  against the mint cap and the minted runes are burned.

Cenotaphs may be created if a runestone contains an unrecognized even tag, an
unrecognized flag, an edict with an output number greater than the number of
inputs, a rune ID with block zero and nonzero transaction index, a malformed
varint, a non-datapush instruction in the runestone output script pubkey, a tag
without a following value, or trailing integers not part of an edict.

#### Executing the Runestone

Runestones are executed in the order their transactions are included in blocks.

##### Etchings

A runestone may contain an etching:

```rust
struct Etching {
  divisibility: Option<u8>,
  premine: Option<u128>,
  rune: Option<Rune>,
  spacers: Option<u32>,
  symbol: Option<char>,
  terms: Option<Terms>,
}
```

`rune` is the name of the rune to be etched, encoded as modified base-26
integer.

Rune names consist of the letters A through Z, with the following encoding:

| Name | Encoding |
|------|----------|
| A    | 0        |
| B    | 1        |
| …    | …        |
| Y    | 24       |
| Z    | 25       |
| AA   | 26       |
| AB   | 27       |
| …    | …        |
| AY   | 50       |
| AZ   | 51       |
| BA   | 52       |

And so on and so on.

Rune names `AAAAAAAAAAAAAAAAAAAAAAAAAAA` and above are reserved.

If `rune` is omitted a reserved rune name is allocated as follows:

```rust
fn reserve(block: u64, tx: u32) -> Rune {
  Rune(
    6402364363415443603228541259936211926
    + (u128::from(block) << 32 | u128::from(tx))
  )
}
```

`6402364363415443603228541259936211926` corresponds to the rune name
`AAAAAAAAAAAAAAAAAAAAAAAAAAA`.

If `rune` is present, it must be unlocked as of the block in which the etching
appears.

Initially, all rune names of length thirteen and longer, up until the first
reserved rune name, are unlocked.

Runes begin unlocking in block 840,000, the block in which the runes protocol
activates.

Thereafter, every 17,500 block period, the next shortest length of rune names
is continuously unlocked. So, between block 840,000 and block 857,500, the
twelve-character rune names are unlocked, between block 857,500 and block
875,000 the eleven character rune names are unlocked, and so on and so on,
until the one-character rune names are unlocked between block 1,032,500 and
block 1,050,000. See the `ord` codebase for the precise unlocking schedule.

To prevent front running an etching that has been broadcast but not mined, if a
non-reserved rune name is being etched, the etching transaction must contain a
valid commitment to the name being etched.

A commitment consists of a data push of the rune name, encoded as a
little-endian integer with trailing zero bytes elided, present in an input
witness tapscript where the output being spent has at least six confirmations.

If a valid commitment is not present, the etching is ignored.

#### Minting

A runestone may mint a rune by including the rune's ID in the `Mint` field.

If the mint is open, the mint amount is added to the unallocated runes in the
transaction's inputs. These runes may be transferred using edicts, and will
otherwise be transferred to the first non-`OP_RETURN` output, or the output
designated by the `Pointer` field.

Mints may be made in any transaction after an etching, including in the same
block.

#### Transferring

Runes are transferred by edict:

```rust
struct Edict {
  id: RuneId,
  amount: u128,
  output: u32,
}
```

A runestone may contain any number of edicts, which are processed in sequence.

Before edicts are processed, input runes, as well as minted or premined runes,
if any, are unallocated.

Each edict decrements the unallocated balance of rune `id` and increments the
balance allocated to transaction outputs of rune `id`.

If an edict would allocate more runes than are currently unallocated, the
`amount` is reduced to the number of currently unallocated runes. In other
words, the edict allocates all remaining unallocated units of rune `id`.

Because the ID of an etched rune is not known before it is included in a block,
ID `0:0` is used to mean the rune being etched in this transaction, if any.

An edict with `amount` zero allocates all remaining units of rune `id`.

An edict with `output` equal to the number of transaction outputs allocates
`amount` runes to each non-`OP_RETURN` output in order.

An edict with `amount` zero and `output` equal to the number of transaction
outputs divides all unallocated units of rune `id` between each non-`OP_RETURN`
output. If the number of unallocated runes is not divisible by the number of
non-`OP_RETURN` outputs, 1 additional rune is assigned to the first `R`
non-`OP_RETURN` outputs, where `R` is the remainder after dividing the balance
of unallocated units of rune `id` by the number of non-`OP_RETURN` outputs.

If any edict in a runestone has a rune ID with `block` zero and `tx` greater
than zero, or `output` greater than the number of transaction outputs, the
runestone is a cenotaph.

Note that edicts in cenotaphs are not processed, and all input runes are
burned.

ord/docs/src/security.md


Security
========

Anyone can publish inscriptions, including arbitrary HTML, which `ord server`
will serve at `/content/<INSCRIPTION_ID>`,
`/r/undelegated-content/<INSCRIPTION_ID>`, and
`/r/sat/<SAT_NUMBER>/at/<INDEX>/content`.

This creates potential security vulnerabilities, including cross-site scripting
and spoofing attacks.

Without mitigations, a domain hosting an `ord server` explorer instance should
be considered to be completely untrusted.

### Cross-site Scripting

An attacker performs a cross-site scripting attack by injecting JavaScript into
a vulnerable website such that it is later served to a victim, causing the
malicious script to run in the victim's own browser with the same permissions
as any other script from that website.

Such a script could access private site resources or perform privileged actions
on the user's behalf, since it would be making requests on behalf of the user.

Cross-site scripting is *not* a concern for a vanilla `ord server` instance.
`ord server` does not serve any private resources or allow privileged actions
to be taken via the web interface, so although JavaScript can be injected using
an inscription, it does not run with any meaningful permissions.

However, if you serve requests to `ord server` from a domain which *also*
serves private resources or allow privileged actions, you may be vulnerable to
cross-site scripting attacks.

#### Example

- `ord server` is run under `https://example.com/ord`.

- A bitcoin exchange is run under `https://example.com/exchange`.

- Users can send bitcoin at `https://example.com/exchange/send`, with access
  controlled by a user's session cookie.

- An attacker publishes a malicious HTML inscription with ID `XYZ` which
  contains JavaScript that accesses `https://example.com/exchange/send` and
  sends them to an attacker-controlled address.

- When a user visits `https://example.com/ord/XYZ`, the malicious JavaScript
  requests `https://example.com/exchange/send`. Because the script executes on
  the user's behalf with the request including the user's session cookie, the
  server sends the user's bitcoin to the attacker.

To prevent this, do not make `ord server` available on the same domain and port
as another web service which is vulnerable to cross-site scripting.

### Spoofing

If `ord server` is run at a well known domain, for example, `ordinals.com`, an
attacker could publish a malicious inscription that attempts to trick users
into thinking that it represents the owners of the domain. For example, by
publishing a mint page to induce users to send bitcoin. Additionally, the
[History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) can
be used to change the path without triggering a page reload, so inscription
content can appear to the user under URL that suggests it was published by the
site owners.

#### Example

- `ord server` is run at `https://example.com`, by popular and well-respected
  Example Corporation.

- An attacker publishes a malicious HTML inscription with ID `XYZ` which
  displays a mint page, and changes the URL path using the History API to
  `/mint`.

- A user clicks a link to `https://example.com/inscription/XYZ` which displays
  the attacker's mint page, with the apparent URL of
  `https://example.com/mint`.

- The user thinks that this is a potentially lucrative mint run by popular and
  well-respected Example Corporation and sends bitcoin to the mint address.

To prevent this, avoid giving users the impression that a domain hosting an
`ord` explorer can be trusted for anything other than the content of the `ord`
explorer itself.

ord/src/api.rs


use {
  super::*,
  serde_hex::{SerHex, Strict},
};

pub use crate::{
  subcommand::decode::RawOutput as Decode,
  templates::{
    BlocksHtml as Blocks, RuneHtml as Rune, RunesHtml as Runes, StatusHtml as Status,
    TransactionHtml as Transaction,
  },
};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Block {
  pub best_height: u32,
  pub hash: BlockHash,
  pub height: u32,
  pub inscriptions: Vec<InscriptionId>,
  pub runes: Vec<SpacedRune>,
  pub target: BlockHash,
  pub transactions: Vec<bitcoin::blockdata::transaction::Transaction>,
}

impl Block {
  pub(crate) fn new(
    block: bitcoin::Block,
    height: Height,
    best_height: Height,
    inscriptions: Vec<InscriptionId>,
    runes: Vec<SpacedRune>,
  ) -> Self {
    Self {
      hash: block.header.block_hash(),
      target: target_as_block_hash(block.header.target()),
      height: height.0,
      best_height: best_height.0,
      inscriptions,
      runes,
      transactions: block.txdata,
    }
  }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockInfo {
  pub average_fee: u64,
  pub average_fee_rate: u64,
  pub bits: u32,
  #[serde(with = "SerHex::<Strict>")]
  pub chainwork: [u8; 32],
  pub confirmations: i32,
  pub difficulty: f64,
  pub hash: BlockHash,
  pub feerate_percentiles: [u64; 5],
  pub height: u32,
  pub max_fee: u64,
  pub max_fee_rate: u64,
  pub max_tx_size: u32,
  pub median_fee: u64,
  pub median_time: Option<u64>,
  pub merkle_root: TxMerkleNode,
  pub min_fee: u64,
  pub min_fee_rate: u64,
  pub next_block: Option<BlockHash>,
  pub nonce: u32,
  pub previous_block: Option<BlockHash>,
  pub subsidy: u64,
  pub target: BlockHash,
  pub timestamp: u64,
  pub total_fee: u64,
  pub total_size: usize,
  pub total_weight: usize,
  pub transaction_count: u64,
  pub version: u32,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Children {
  pub ids: Vec<InscriptionId>,
  pub more: bool,
  pub page: usize,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ChildInscriptions {
  pub children: Vec<RelativeInscriptionRecursive>,
  pub more: bool,
  pub page: usize,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct ParentInscriptions {
  pub parents: Vec<RelativeInscriptionRecursive>,
  pub more: bool,
  pub page: usize,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct Inscription {
  pub address: Option<String>,
  pub charms: Vec<Charm>,
  pub child_count: u64,
  pub children: Vec<InscriptionId>,
  pub content_length: Option<usize>,
  pub content_type: Option<String>,
  pub effective_content_type: Option<String>,
  pub fee: u64,
  pub height: u32,
  pub id: InscriptionId,
  pub next: Option<InscriptionId>,
  pub number: i32,
  pub parents: Vec<InscriptionId>,
  pub previous: Option<InscriptionId>,
  pub rune: Option<SpacedRune>,
  pub sat: Option<ordinals::Sat>,
  pub satpoint: SatPoint,
  pub timestamp: i64,
  pub value: Option<u64>,
  pub metaprotocol: Option<String>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct InscriptionRecursive {
  pub charms: Vec<Charm>,
  pub content_type: Option<String>,
  pub content_length: Option<usize>,
  pub delegate: Option<InscriptionId>,
  pub fee: u64,
  pub height: u32,
  pub id: InscriptionId,
  pub number: i32,
  pub output: OutPoint,
  pub sat: Option<ordinals::Sat>,
  pub satpoint: SatPoint,
  pub timestamp: i64,
  pub value: Option<u64>,
  pub address: Option<String>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct RelativeInscriptionRecursive {
  pub charms: Vec<Charm>,
  pub fee: u64,
  pub height: u32,
  pub id: InscriptionId,
  pub number: i32,
  pub output: OutPoint,
  pub sat: Option<ordinals::Sat>,
  pub satpoint: SatPoint,
  pub timestamp: i64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Inscriptions {
  pub ids: Vec<InscriptionId>,
  pub more: bool,
  pub page_index: u32,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct UtxoRecursive {
  pub inscriptions: Option<Vec<InscriptionId>>,
  pub runes: Option<BTreeMap<SpacedRune, Pile>>,
  pub sat_ranges: Option<Vec<(u64, u64)>>,
  pub value: u64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct Output {
  pub address: Option<Address<NetworkUnchecked>>,
  pub confirmations: u32,
  pub indexed: bool,
  pub inscriptions: Option<Vec<InscriptionId>>,
  pub outpoint: OutPoint,
  pub runes: Option<BTreeMap<SpacedRune, Pile>>,
  pub sat_ranges: Option<Vec<(u64, u64)>>,
  pub script_pubkey: ScriptBuf,
  pub spent: bool,
  pub transaction: Txid,
  pub value: u64,
}

impl Output {
  pub fn new(
    chain: Chain,
    confirmations: u32,
    inscriptions: Option<Vec<InscriptionId>>,
    outpoint: OutPoint,
    tx_out: TxOut,
    indexed: bool,
    runes: Option<BTreeMap<SpacedRune, Pile>>,
    sat_ranges: Option<Vec<(u64, u64)>>,
    spent: bool,
  ) -> Self {
    Self {
      address: chain
        .address_from_script(&tx_out.script_pubkey)
        .ok()
        .map(|address| uncheck(&address)),
      confirmations,
      indexed,
      inscriptions,
      outpoint,
      runes,
      sat_ranges,
      script_pubkey: tx_out.script_pubkey,
      spent,
      transaction: outpoint.txid,
      value: tx_out.value.to_sat(),
    }
  }
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Sat {
  pub address: Option<String>,
  pub block: u32,
  pub charms: Vec<Charm>,
  pub cycle: u32,
  pub decimal: String,
  pub degree: String,
  pub epoch: u32,
  pub inscriptions: Vec<InscriptionId>,
  pub name: String,
  pub number: u64,
  pub offset: u64,
  pub percentile: String,
  pub period: u32,
  pub rarity: Rarity,
  pub satpoint: Option<SatPoint>,
  pub timestamp: i64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SatInscription {
  pub id: Option<InscriptionId>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SatInscriptions {
  pub ids: Vec<InscriptionId>,
  pub more: bool,
  pub page: u64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct AddressInfo {
  pub outputs: Vec<OutPoint>,
  pub inscriptions: Option<Vec<InscriptionId>>,
  pub sat_balance: u64,
  pub runes_balances: Option<Vec<(SpacedRune, Decimal, Option<char>)>>,
}

ord/src/arguments.rs


use {
  super::*,
  clap::builder::styling::{AnsiColor, Effects, Styles},
};

#[derive(Debug, Parser)]
#[command(
  version,
  styles = Styles::styled()
    .error(AnsiColor::Red.on_default() | Effects::BOLD)
    .header(AnsiColor::Yellow.on_default() | Effects::BOLD)
    .invalid(AnsiColor::Red.on_default())
    .literal(AnsiColor::Blue.on_default())
    .placeholder(AnsiColor::Cyan.on_default())
    .usage(AnsiColor::Yellow.on_default() | Effects::BOLD)
    .valid(AnsiColor::Green.on_default()),
)]
pub(crate) struct Arguments {
  #[command(flatten)]
  pub(crate) options: Options,
  #[command(subcommand)]
  pub(crate) subcommand: Subcommand,
}

impl Arguments {
  pub(crate) fn run(self) -> SnafuResult<Option<Box<dyn subcommand::Output>>> {
    let mut env: BTreeMap<String, String> = BTreeMap::new();

    for (variable, value) in env::vars_os() {
      let Some(variable) = variable.to_str() else {
        continue;
      };

      let Some(key) = variable.strip_prefix("ORD_") else {
        continue;
      };

      env.insert(
        key.into(),
        value
          .into_string()
          .map_err(|value| SnafuError::EnvVarUnicode {
            backtrace: Backtrace::capture(),
            value,
            variable: variable.into(),
          })?,
      );
    }

    Ok(self.subcommand.run(Settings::load(self.options)?)?)
  }
}

ord/src/bin/main.rs


fn main() {
  ord::main();
}

ord/src/blocktime.rs


use super::*;

#[derive(Copy, Clone, Debug, PartialEq, Serialize)]
pub enum Blocktime {
  Confirmed(DateTime<Utc>),
  Expected(DateTime<Utc>),
}

impl Blocktime {
  pub(crate) fn confirmed(seconds: u32) -> Self {
    Self::Confirmed(timestamp(seconds.into()))
  }

  pub(crate) fn timestamp(self) -> DateTime<Utc> {
    match self {
      Self::Confirmed(timestamp) | Self::Expected(timestamp) => timestamp,
    }
  }

  pub(crate) fn unix_timestamp(self) -> i64 {
    match self {
      Self::Confirmed(timestamp) | Self::Expected(timestamp) => timestamp.timestamp(),
    }
  }

  pub(crate) fn suffix(self) -> &'static str {
    match self {
      Self::Confirmed(_) => "",
      Self::Expected(_) => " (expected)",
    }
  }
}

ord/src/chain.rs


use {super::*, clap::ValueEnum};

#[derive(Default, ValueEnum, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Chain {
  #[default]
  #[value(alias("main"))]
  Mainnet,
  Regtest,
  Signet,
  #[value(alias("test"))]
  Testnet,
  Testnet4,
}

impl Chain {
  pub(crate) fn network(self) -> Network {
    self.into()
  }

  pub(crate) fn bech32_hrp(self) -> KnownHrp {
    match self {
      Self::Mainnet => KnownHrp::Mainnet,
      Self::Regtest => KnownHrp::Regtest,
      Self::Signet | Self::Testnet | Self::Testnet4 => KnownHrp::Testnets,
    }
  }

  pub(crate) fn default_rpc_port(self) -> u16 {
    match self {
      Self::Mainnet => 8332,
      Self::Regtest => 18443,
      Self::Signet => 38332,
      Self::Testnet => 18332,
      Self::Testnet4 => 48332,
    }
  }

  pub(crate) fn inscription_content_size_limit(self) -> Option<usize> {
    match self {
      Self::Mainnet | Self::Regtest => None,
      Self::Testnet | Self::Testnet4 | Self::Signet => Some(1024),
    }
  }

  pub(crate) fn first_inscription_height(self) -> u32 {
    match self {
      Self::Mainnet => 767430,
      Self::Regtest => 0,
      Self::Signet => 112402,
      Self::Testnet => 2413343,
      Self::Testnet4 => 0,
    }
  }

  pub(crate) fn first_rune_height(self) -> u32 {
    Rune::first_rune_height(self.into())
  }

  pub(crate) fn jubilee_height(self) -> u32 {
    match self {
      Self::Mainnet => 824544,
      Self::Regtest => 110,
      Self::Signet => 175392,
      Self::Testnet => 2544192,
      Self::Testnet4 => 0,
    }
  }

  pub(crate) fn genesis_block(self) -> Block {
    bitcoin::blockdata::constants::genesis_block(self.network())
  }

  pub(crate) fn genesis_coinbase_outpoint(self) -> OutPoint {
    OutPoint {
      txid: self.genesis_block().coinbase().unwrap().compute_txid(),
      vout: 0,
    }
  }

  pub(crate) fn address_from_script(self, script: &Script) -> Result<Address, SnafuError> {
    Address::from_script(script, self.network()).snafu_context(error::AddressConversion)
  }

  pub(crate) fn join_with_data_dir(self, data_dir: impl AsRef<Path>) -> PathBuf {
    match self {
      Self::Mainnet => data_dir.as_ref().to_owned(),
      Self::Regtest => data_dir.as_ref().join("regtest"),
      Self::Signet => data_dir.as_ref().join("signet"),
      Self::Testnet => data_dir.as_ref().join("testnet3"),
      Self::Testnet4 => data_dir.as_ref().join("testnet4"),
    }
  }
}

impl From<Chain> for Network {
  fn from(chain: Chain) -> Network {
    match chain {
      Chain::Mainnet => Network::Bitcoin,
      Chain::Regtest => Network::Regtest,
      Chain::Signet => Network::Signet,
      Chain::Testnet => Network::Testnet,
      Chain::Testnet4 => Network::Testnet4,
    }
  }
}

impl Display for Chain {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(
      f,
      "{}",
      match self {
        Self::Mainnet => "mainnet",
        Self::Regtest => "regtest",
        Self::Signet => "signet",
        Self::Testnet => "testnet",
        Self::Testnet4 => "testnet4",
      }
    )
  }
}

impl FromStr for Chain {
  type Err = SnafuError;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    match s {
      "mainnet" => Ok(Self::Mainnet),
      "regtest" => Ok(Self::Regtest),
      "signet" => Ok(Self::Signet),
      "testnet" => Ok(Self::Testnet),
      "testnet4" => Ok(Self::Testnet4),
      _ => Err(SnafuError::InvalidChain {
        chain: s.to_string(),
      }),
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn from_str() {
    assert_eq!("mainnet".parse::<Chain>().unwrap(), Chain::Mainnet);
    assert_eq!("regtest".parse::<Chain>().unwrap(), Chain::Regtest);
    assert_eq!("signet".parse::<Chain>().unwrap(), Chain::Signet);
    assert_eq!("testnet".parse::<Chain>().unwrap(), Chain::Testnet);
    assert_eq!("testnet4".parse::<Chain>().unwrap(), Chain::Testnet4);
    assert_eq!(
      "foo".parse::<Chain>().unwrap_err().to_string(),
      "Invalid chain `foo`"
    );
  }
}

ord/src/decimal.rs


use super::*;

#[derive(Debug, PartialEq, Copy, Clone, Default, DeserializeFromStr, SerializeDisplay)]
pub struct Decimal {
  pub value: u128,
  pub scale: u8,
}

impl Decimal {
  pub fn to_integer(self, divisibility: u8) -> Result<u128> {
    match divisibility.checked_sub(self.scale) {
      Some(difference) => Ok(
        self
          .value
          .checked_mul(
            10u128
              .checked_pow(u32::from(difference))
              .context("divisibility out of range")?,
          )
          .context("amount out of range")?,
      ),
      None => bail!("excessive precision"),
    }
  }
}

impl Display for Decimal {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    let magnitude = 10u128.checked_pow(self.scale.into()).ok_or(fmt::Error)?;

    let integer = self.value / magnitude;
    let mut fraction = self.value % magnitude;

    write!(f, "{integer}")?;

    if fraction > 0 {
      let mut width = self.scale.into();

      while fraction % 10 == 0 {
        fraction /= 10;
        width -= 1;
      }

      write!(f, ".{fraction:0>width$}", width = width)?;
    }

    Ok(())
  }
}

impl FromStr for Decimal {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    if let Some((integer, decimal)) = s.split_once('.') {
      if integer.is_empty() && decimal.is_empty() {
        bail!("empty decimal");
      }

      let integer = if integer.is_empty() {
        0
      } else {
        integer.parse::<u128>()?
      };

      let (decimal, scale) = if decimal.is_empty() {
        (0, 0)
      } else {
        let trailing_zeros = decimal.chars().rev().take_while(|c| *c == '0').count();
        let significant_digits = decimal.chars().count() - trailing_zeros;
        let decimal = decimal.parse::<u128>()?
          / 10u128
            .checked_pow(u32::try_from(trailing_zeros).unwrap())
            .context("excessive trailing zeros")?;
        (decimal, u8::try_from(significant_digits).unwrap())
      };

      Ok(Self {
        value: integer * 10u128.pow(u32::from(scale)) + decimal,
        scale,
      })
    } else {
      Ok(Self {
        value: s.parse::<u128>()?,
        scale: 0,
      })
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn from_str() {
    #[track_caller]
    fn case(s: &str, value: u128, scale: u8) {
      assert_eq!(s.parse::<Decimal>().unwrap(), Decimal { value, scale });
    }

    assert_eq!(
      ".".parse::<Decimal>().unwrap_err().to_string(),
      "empty decimal",
    );

    assert_eq!(
      "a.b".parse::<Decimal>().unwrap_err().to_string(),
      "invalid digit found in string",
    );

    assert_eq!(
      " 0.1 ".parse::<Decimal>().unwrap_err().to_string(),
      "invalid digit found in string",
    );

    case("0", 0, 0);
    case("0.00000", 0, 0);
    case("1.0", 1, 0);
    case("1.1", 11, 1);
    case("1.11", 111, 2);
    case("1.", 1, 0);
    case(".1", 1, 1);
    case("1.10", 11, 1);
  }

  #[test]
  fn to_amount() {
    #[track_caller]
    fn case(s: &str, divisibility: u8, amount: u128) {
      assert_eq!(
        s.parse::<Decimal>()
          .unwrap()
          .to_integer(divisibility)
          .unwrap(),
        amount,
      );
    }

    assert_eq!(
      Decimal { value: 0, scale: 0 }
        .to_integer(255)
        .unwrap_err()
        .to_string(),
      "divisibility out of range"
    );

    assert_eq!(
      Decimal {
        value: u128::MAX,
        scale: 0,
      }
      .to_integer(1)
      .unwrap_err()
      .to_string(),
      "amount out of range",
    );

    assert_eq!(
      Decimal { value: 1, scale: 1 }
        .to_integer(0)
        .unwrap_err()
        .to_string(),
      "excessive precision",
    );

    case("1", 0, 1);
    case("1.0", 0, 1);
    case("1.0", 1, 10);
    case("1.2", 1, 12);
    case("1.2", 2, 120);
    case("123.456", 3, 123456);
    case("123.456", 6, 123456000);
  }

  #[test]
  fn to_string() {
    #[track_caller]
    fn case(decimal: Decimal, string: &str) {
      assert_eq!(decimal.to_string(), string);
      assert_eq!(decimal, string.parse::<Decimal>().unwrap());
    }

    case(Decimal { value: 0, scale: 0 }, "0");
    case(Decimal { value: 1, scale: 0 }, "1");
    case(Decimal { value: 1, scale: 1 }, "0.1");
    case(
      Decimal {
        value: 101,
        scale: 2,
      },
      "1.01",
    );
    case(
      Decimal {
        value: 1234,
        scale: 6,
      },
      "0.001234",
    );
    case(
      Decimal {
        value: 12,
        scale: 0,
      },
      "12",
    );
    case(
      Decimal {
        value: 12,
        scale: 1,
      },
      "1.2",
    );
    case(
      Decimal {
        value: 12,
        scale: 2,
      },
      "0.12",
    );
    case(
      Decimal {
        value: 123456,
        scale: 3,
      },
      "123.456",
    );
    case(
      Decimal {
        value: 123456789,
        scale: 6,
      },
      "123.456789",
    );
  }
}

ord/src/deserialize_from_str.rs


use super::*;

#[derive(Debug)]
pub struct DeserializeFromStr<T: FromStr>(pub T);

impl<'de, T: FromStr> Deserialize<'de> for DeserializeFromStr<T>
where
  T::Err: Display,
{
  fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
  where
    D: Deserializer<'de>,
  {
    Ok(Self(
      FromStr::from_str(&String::deserialize(deserializer)?).map_err(serde::de::Error::custom)?,
    ))
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn deserialize_from_str() {
    assert_eq!(
      serde_json::from_str::<DeserializeFromStr<u64>>("\"1\"")
        .unwrap()
        .0,
      1,
    );
  }
}

ord/src/error.rs


use super::*;

#[derive(Debug, Snafu)]
#[snafu(context(suffix(false)), visibility(pub(crate)))]
pub enum SnafuError {
  #[snafu(display("Failed to parse address `{}`", input))]
  AddressParse {
    source: bitcoin::address::error::ParseError,
    input: String,
  },
  #[snafu(display("Failed to parse hash `{}`", input))]
  HashParse {
    source: bitcoin::hex::HexToArrayError,
    input: String,
  },
  #[snafu(display("Failed to parse inscription ID `{}`", input))]
  InscriptionIdParse {
    source: inscriptions::inscription_id::ParseError,
    input: String,
  },
  #[snafu(display("Failed to parse integer `{}`", input))]
  IntegerParse {
    source: std::num::ParseIntError,
    input: String,
  },
  #[snafu(display("Failed to parse out point `{}`", input))]
  OutPointParse {
    source: bitcoin::transaction::ParseOutPointError,
    input: String,
  },
  #[snafu(display("Failed to parse rune `{}`", input))]
  RuneParse {
    source: ordinals::spaced_rune::Error,
    input: String,
  },
  #[snafu(display("Failed to parse sat `{}`", input))]
  SatParse {
    source: ordinals::sat::Error,
    input: String,
  },
  #[snafu(display("Failed to parse sat point `{}`", input))]
  SatPointParse {
    source: ordinals::sat_point::Error,
    input: String,
  },
  #[snafu(display("Unrecognized representation: `{}`", input))]
  UnrecognizedRepresentation { input: String },
  #[snafu(display("Unrecognized outgoing amount: `{}`", input))]
  AmountParse {
    source: <Amount as FromStr>::Err,
    input: String,
  },
  #[snafu(display("Unrecognized outgoing: `{}`", input))]
  OutgoingParse { input: String },
  #[snafu(display("Failed to parse decimal: {}", source))]
  RuneAmountParse { source: error::Error, input: String },
  #[snafu(display("Invalid chain `{}`", chain))]
  InvalidChain { chain: String },
  #[snafu(display("Failed to convert script to address: {}", source))]
  AddressConversion {
    source: bitcoin::address::FromScriptError,
  },
  #[snafu(display("{err}"))]
  Anyhow { err: anyhow::Error },
  #[snafu(display("environment variable `{variable}` not valid unicode: `{}`", value.to_string_lossy()))]
  EnvVarUnicode {
    backtrace: Backtrace,
    value: OsString,
    variable: String,
  },
  #[snafu(display("I/O error at `{}`", path.display()))]
  Io {
    backtrace: Backtrace,
    path: PathBuf,
    source: io::Error,
  },
  #[snafu(display("Unrecognized signer: `{}`", input))]
  SignerParse { input: String },
}

impl From<Error> for SnafuError {
  fn from(err: Error) -> SnafuError {
    Self::Anyhow { err }
  }
}

/// We currently use `anyhow` for error handling but are migrating to typed
/// errors using `snafu`. This trait exists to provide access to
/// `snafu::ResultExt::{context, with_context}`, which are otherwise shadowed
/// by `anyhow::Context::{context, with_context}`. Once the migration is
/// complete, this trait can be deleted, and `snafu::ResultExt` used directly.
pub(crate) trait ResultExt<T, E>: Sized {
  fn snafu_context<C, E2>(self, context: C) -> Result<T, E2>
  where
    C: snafu::IntoError<E2, Source = E>,
    E2: std::error::Error + snafu::ErrorCompat;

  #[allow(unused)]
  fn with_snafu_context<F, C, E2>(self, context: F) -> Result<T, E2>
  where
    F: FnOnce(&mut E) -> C,
    C: snafu::IntoError<E2, Source = E>,
    E2: std::error::Error + snafu::ErrorCompat;
}

impl<T, E> ResultExt<T, E> for std::result::Result<T, E> {
  fn snafu_context<C, E2>(self, context: C) -> Result<T, E2>
  where
    C: snafu::IntoError<E2, Source = E>,
    E2: std::error::Error + snafu::ErrorCompat,
  {
    use snafu::ResultExt;
    self.context(context)
  }

  fn with_snafu_context<F, C, E2>(self, context: F) -> Result<T, E2>
  where
    F: FnOnce(&mut E) -> C,
    C: snafu::IntoError<E2, Source = E>,
    E2: std::error::Error + snafu::ErrorCompat,
  {
    use snafu::ResultExt;
    self.with_context(context)
  }
}

ord/src/fee_rate.rs


use super::*;

#[derive(
  Debug, PartialEq, Clone, Copy, derive_more::Display, DeserializeFromStr, SerializeDisplay,
)]
pub struct FeeRate(f64);

impl FromStr for FeeRate {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    Self::try_from(f64::from_str(s)?)
  }
}

impl TryFrom<f64> for FeeRate {
  type Error = Error;

  fn try_from(rate: f64) -> Result<Self, Self::Error> {
    if rate.is_sign_negative() | rate.is_nan() | rate.is_infinite() {
      bail!("invalid fee rate: {rate}")
    }
    Ok(Self(rate))
  }
}

impl FeeRate {
  pub fn fee(&self, vsize: usize) -> Amount {
    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_sign_loss)]
    Amount::from_sat((self.0 * vsize as f64).round() as u64)
  }

  pub(crate) fn n(&self) -> f64 {
    self.0
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn parse() {
    assert_eq!("1.1".parse::<FeeRate>().unwrap().0, 1.1);
    assert_eq!("11.19".parse::<FeeRate>().unwrap().0, 11.19);
    assert_eq!("11.1111".parse::<FeeRate>().unwrap().0, 11.1111);
    assert!("-4.2".parse::<FeeRate>().is_err());
    assert!(FeeRate::try_from(f64::INFINITY).is_err());
    assert!(FeeRate::try_from(f64::NAN).is_err());
  }

  #[test]
  fn fee() {
    assert_eq!(
      "2.5".parse::<FeeRate>().unwrap().fee(100),
      Amount::from_sat(250)
    );
    assert_eq!(
      "2.0".parse::<FeeRate>().unwrap().fee(1024),
      Amount::from_sat(2048)
    );
    assert_eq!(
      "1.1".parse::<FeeRate>().unwrap().fee(100),
      Amount::from_sat(110)
    );
    assert_eq!(
      "1.0".parse::<FeeRate>().unwrap().fee(123456789),
      Amount::from_sat(123456789)
    );
  }
}

ord/src/index.rs


use {
  self::{
    entry::{
      Entry, HeaderValue, InscriptionEntry, InscriptionEntryValue, InscriptionIdValue,
      OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxidValue,
    },
    event::Event,
    lot::Lot,
    reorg::Reorg,
    updater::Updater,
    utxo_entry::{ParsedUtxoEntry, UtxoEntry, UtxoEntryBuf},
  },
  super::*,
  crate::{
    runes::MintError,
    subcommand::{find::FindRangeOutput, server::query},
    templates::StatusHtml,
  },
  bitcoin::block::Header,
  bitcoincore_rpc::{
    json::{
      GetBlockHeaderResult, GetBlockStatsResult, GetRawTransactionResult,
      GetRawTransactionResultVout, GetRawTransactionResultVoutScriptPubKey, GetTxOutResult,
    },
    Client,
  },
  chrono::SubsecRound,
  indicatif::{ProgressBar, ProgressStyle},
  log::log_enabled,
  redb::{
    Database, DatabaseError, MultimapTable, MultimapTableDefinition, MultimapTableHandle,
    ReadOnlyTable, ReadableMultimapTable, ReadableTable, ReadableTableMetadata, RepairSession,
    StorageError, Table, TableDefinition, TableHandle, TableStats, WriteTransaction,
  },
  std::{
    collections::HashMap,
    io::{BufWriter, Write},
    sync::Once,
  },
};

pub use self::entry::RuneEntry;

pub(crate) mod entry;
pub mod event;
mod fetcher;
mod lot;
mod reorg;
mod rtx;
mod updater;
mod utxo_entry;

#[cfg(test)]
pub(crate) mod testing;

const SCHEMA_VERSION: u64 = 30;

define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 }
define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 }
define_multimap_table! { SCRIPT_PUBKEY_TO_OUTPOINT, &[u8], OutPointValue }
define_table! { HEIGHT_TO_BLOCK_HEADER, u32, &HeaderValue }
define_table! { HEIGHT_TO_LAST_SEQUENCE_NUMBER, u32, u32 }
define_table! { HOME_INSCRIPTIONS, u32, InscriptionIdValue }
define_table! { INSCRIPTION_ID_TO_SEQUENCE_NUMBER, InscriptionIdValue, u32 }
define_table! { INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER, i32, u32 }
define_table! { OUTPOINT_TO_RUNE_BALANCES, &OutPointValue, &[u8] }
define_table! { OUTPOINT_TO_UTXO_ENTRY, &OutPointValue, &UtxoEntry }
define_table! { RUNE_ID_TO_RUNE_ENTRY, RuneIdValue, RuneEntryValue }
define_table! { RUNE_TO_RUNE_ID, u128, RuneIdValue }
define_table! { SAT_TO_SATPOINT, u64, &SatPointValue }
define_table! { SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, u32, InscriptionEntryValue }
define_table! { SEQUENCE_NUMBER_TO_RUNE_ID, u32, RuneIdValue }
define_table! { SEQUENCE_NUMBER_TO_SATPOINT, u32, &SatPointValue }
define_table! { STATISTIC_TO_COUNT, u64, u64 }
define_table! { TRANSACTION_ID_TO_RUNE, &TxidValue, u128 }
define_table! { TRANSACTION_ID_TO_TRANSACTION, &TxidValue, &[u8] }
define_table! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u32, u128 }

#[derive(Copy, Clone)]
pub(crate) enum Statistic {
  Schema = 0,
  BlessedInscriptions = 1,
  Commits = 2,
  CursedInscriptions = 3,
  IndexAddresses = 4,
  IndexInscriptions = 5,
  IndexRunes = 6,
  IndexSats = 7,
  IndexTransactions = 8,
  InitialSyncTime = 9,
  LostSats = 10,
  OutputsTraversed = 11,
  ReservedRunes = 12,
  Runes = 13,
  SatRanges = 14,
  UnboundInscriptions = 16,
  LastSavepointHeight = 17,
}

impl Statistic {
  fn key(self) -> u64 {
    self.into()
  }
}

impl From<Statistic> for u64 {
  fn from(statistic: Statistic) -> Self {
    statistic as u64
  }
}

#[derive(Serialize)]
pub struct Info {
  blocks_indexed: u32,
  branch_pages: u64,
  fragmented_bytes: u64,
  index_file_size: u64,
  index_path: PathBuf,
  leaf_pages: u64,
  metadata_bytes: u64,
  outputs_traversed: u64,
  page_size: usize,
  sat_ranges: u64,
  stored_bytes: u64,
  tables: BTreeMap<String, TableInfo>,
  total_bytes: u64,
  pub transactions: Vec<TransactionInfo>,
  tree_height: u32,
  utxos_indexed: u64,
}

#[derive(Serialize)]
pub(crate) struct TableInfo {
  branch_pages: u64,
  fragmented_bytes: u64,
  leaf_pages: u64,
  metadata_bytes: u64,
  proportion: f64,
  stored_bytes: u64,
  total_bytes: u64,
  tree_height: u32,
}

impl From<TableStats> for TableInfo {
  fn from(stats: TableStats) -> Self {
    Self {
      branch_pages: stats.branch_pages(),
      fragmented_bytes: stats.fragmented_bytes(),
      leaf_pages: stats.leaf_pages(),
      metadata_bytes: stats.metadata_bytes(),
      proportion: 0.0,
      stored_bytes: stats.stored_bytes(),
      total_bytes: stats.stored_bytes() + stats.metadata_bytes() + stats.fragmented_bytes(),
      tree_height: stats.tree_height(),
    }
  }
}

#[derive(Serialize)]
pub struct TransactionInfo {
  pub starting_block_count: u32,
  pub starting_timestamp: u128,
}

pub(crate) trait BitcoinCoreRpcResultExt<T> {
  fn into_option(self) -> Result<Option<T>>;
}

impl<T> BitcoinCoreRpcResultExt<T> for Result<T, bitcoincore_rpc::Error> {
  fn into_option(self) -> Result<Option<T>> {
    match self {
      Ok(ok) => Ok(Some(ok)),
      Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::error::Error::Rpc(
        bitcoincore_rpc::jsonrpc::error::RpcError { code: -8, .. },
      ))) => Ok(None),
      Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::error::Error::Rpc(
        bitcoincore_rpc::jsonrpc::error::RpcError {
          code: -5, message, ..
        },
      )))
        if message.starts_with("No such mempool or blockchain transaction") =>
      {
        Ok(None)
      }
      Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::error::Error::Rpc(
        bitcoincore_rpc::jsonrpc::error::RpcError { message, .. },
      )))
        if message.ends_with("not found") =>
      {
        Ok(None)
      }
      Err(err) => Err(err.into()),
    }
  }
}

pub struct Index {
  pub(crate) client: Client,
  database: Database,
  durability: redb::Durability,
  event_sender: Option<tokio::sync::mpsc::Sender<Event>>,
  genesis_block_coinbase_transaction: Transaction,
  genesis_block_coinbase_txid: Txid,
  height_limit: Option<u32>,
  index_addresses: bool,
  index_inscriptions: bool,
  index_runes: bool,
  index_sats: bool,
  index_transactions: bool,
  path: PathBuf,
  settings: Settings,
  started: DateTime<Utc>,
  first_index_height: u32,
  unrecoverably_reorged: AtomicBool,
}

impl Index {
  pub fn open(settings: &Settings) -> Result<Self> {
    Index::open_with_event_sender(settings, None)
  }

  pub fn open_with_event_sender(
    settings: &Settings,
    event_sender: Option<tokio::sync::mpsc::Sender<Event>>,
  ) -> Result<Self> {
    let client = settings.bitcoin_rpc_client(None)?;

    let path = settings.index().to_owned();

    let data_dir = path.parent().unwrap();

    fs::create_dir_all(data_dir).snafu_context(error::Io { path: data_dir })?;

    let index_cache_size = settings.index_cache_size();

    log::info!("Setting index cache size to {} bytes", index_cache_size);

    let durability = if cfg!(test) {
      redb::Durability::None
    } else {
      redb::Durability::Immediate
    };

    let index_path = path.clone();
    let once = Once::new();
    let progress_bar = Mutex::new(None);
    let integration_test = settings.integration_test();

    let repair_callback = move |progress: &mut RepairSession| {
      once.call_once(|| println!("Index file `{}` needs recovery. This can take a long time, especially for the --index-sats index.", index_path.display()));

      if !(cfg!(test) || log_enabled!(log::Level::Info) || integration_test) {
        let mut guard = progress_bar.lock().unwrap();

        let progress_bar = guard.get_or_insert_with(|| {
          let progress_bar = ProgressBar::new(100);
          progress_bar.set_style(
            ProgressStyle::with_template("[repairing database] {wide_bar} {pos}/{len}").unwrap(),
          );
          progress_bar
        });

        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
        progress_bar.set_position((progress.progress() * 100.0) as u64);
      }
    };

    let database = match Database::builder()
      .set_cache_size(index_cache_size)
      .set_repair_callback(repair_callback)
      .open(&path)
    {
      Ok(database) => {
        {
          let schema_version = database
            .begin_read()?
            .open_table(STATISTIC_TO_COUNT)?
            .get(&Statistic::Schema.key())?
            .map(|x| x.value())
            .unwrap_or(0);

          match schema_version.cmp(&SCHEMA_VERSION) {
            cmp::Ordering::Less =>
              bail!(
                "index at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}",
                path.display()
              ),
            cmp::Ordering::Greater =>
              bail!(
                "index at `{}` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {schema_version}, ord schema {SCHEMA_VERSION}",
                path.display()
              ),
            cmp::Ordering::Equal => {
            }
          }
        }

        database
      }
      Err(DatabaseError::Storage(StorageError::Io(error)))
        if error.kind() == io::ErrorKind::NotFound =>
      {
        let database = Database::builder()
          .set_cache_size(index_cache_size)
          .create(&path)?;

        let mut tx = database.begin_write()?;

        tx.set_durability(durability);
        tx.set_quick_repair(true);

        tx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?;
        tx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?;
        tx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?;
        tx.open_table(HEIGHT_TO_BLOCK_HEADER)?;
        tx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?;
        tx.open_table(HOME_INSCRIPTIONS)?;
        tx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?;
        tx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?;
        tx.open_table(OUTPOINT_TO_RUNE_BALANCES)?;
        tx.open_table(OUTPOINT_TO_UTXO_ENTRY)?;
        tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;
        tx.open_table(RUNE_TO_RUNE_ID)?;
        tx.open_table(SAT_TO_SATPOINT)?;
        tx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;
        tx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?;
        tx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?;
        tx.open_table(TRANSACTION_ID_TO_RUNE)?;
        tx.open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?;

        {
          let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?;

          Self::set_statistic(
            &mut statistics,
            Statistic::IndexAddresses,
            u64::from(settings.index_addresses_raw()),
          )?;

          Self::set_statistic(
            &mut statistics,
            Statistic::IndexInscriptions,
            u64::from(settings.index_inscriptions_raw()),
          )?;

          Self::set_statistic(
            &mut statistics,
            Statistic::IndexRunes,
            u64::from(settings.index_runes_raw()),
          )?;

          Self::set_statistic(
            &mut statistics,
            Statistic::IndexSats,
            u64::from(settings.index_sats_raw()),
          )?;

          Self::set_statistic(
            &mut statistics,
            Statistic::IndexTransactions,
            u64::from(settings.index_transactions_raw()),
          )?;

          Self::set_statistic(&mut statistics, Statistic::Schema, SCHEMA_VERSION)?;
        }

        if settings.index_runes_raw() && settings.chain() == Chain::Mainnet {
          let rune = Rune(2055900680524219742);

          let id = RuneId { block: 1, tx: 0 };
          let etching = Txid::all_zeros();

          tx.open_table(RUNE_TO_RUNE_ID)?
            .insert(rune.store(), id.store())?;

          let mut statistics = tx.open_table(STATISTIC_TO_COUNT)?;

          Self::set_statistic(&mut statistics, Statistic::Runes, 1)?;

          tx.open_table(RUNE_ID_TO_RUNE_ENTRY)?.insert(
            id.store(),
            RuneEntry {
              block: id.block,
              burned: 0,
              divisibility: 0,
              etching,
              terms: Some(Terms {
                amount: Some(1),
                cap: Some(u128::MAX),
                height: (
                  Some((SUBSIDY_HALVING_INTERVAL * 4).into()),
                  Some((SUBSIDY_HALVING_INTERVAL * 5).into()),
                ),
                offset: (None, None),
              }),
              mints: 0,
              number: 0,
              premine: 0,
              spaced_rune: SpacedRune { rune, spacers: 128 },
              symbol: Some('\u{29C9}'),
              timestamp: 0,
              turbo: true,
            }
            .store(),
          )?;

          tx.open_table(TRANSACTION_ID_TO_RUNE)?
            .insert(&etching.store(), rune.store())?;
        }

        tx.commit()?;

        database
      }
      Err(error) => bail!("failed to open index: {error}"),
    };

    let index_addresses;
    let index_runes;
    let index_sats;
    let index_transactions;
    let index_inscriptions;

    {
      let tx = database.begin_read()?;
      let statistics = tx.open_table(STATISTIC_TO_COUNT)?;
      index_addresses = Self::is_statistic_set(&statistics, Statistic::IndexAddresses)?;
      index_inscriptions = Self::is_statistic_set(&statistics, Statistic::IndexInscriptions)?;
      index_runes = Self::is_statistic_set(&statistics, Statistic::IndexRunes)?;
      index_sats = Self::is_statistic_set(&statistics, Statistic::IndexSats)?;
      index_transactions = Self::is_statistic_set(&statistics, Statistic::IndexTransactions)?;
    }

    let genesis_block_coinbase_transaction =
      settings.chain().genesis_block().coinbase().unwrap().clone();

    let first_index_height = if index_sats || index_addresses {
      0
    } else if index_inscriptions {
      settings.first_inscription_height()
    } else if index_runes {
      settings.first_rune_height()
    } else {
      u32::MAX
    };

    Ok(Self {
      genesis_block_coinbase_txid: genesis_block_coinbase_transaction.compute_txid(),
      client,
      database,
      durability,
      event_sender,
      first_index_height,
      genesis_block_coinbase_transaction,
      height_limit: settings.height_limit(),
      index_addresses,
      index_runes,
      index_sats,
      index_transactions,
      index_inscriptions,
      settings: settings.clone(),
      path,
      started: Utc::now(),
      unrecoverably_reorged: AtomicBool::new(false),
    })
  }

  #[cfg(test)]
  pub(crate) fn chain(&self) -> Chain {
    self.settings.chain()
  }

  pub fn have_full_utxo_index(&self) -> bool {
    self.first_index_height == 0
  }

  /// Unlike normal outpoints, which are added to index on creation and removed
  /// when spent, the UTXO entry for special outpoints may be updated.
  ///
  /// The special outpoints are the null outpoint, which receives lost sats,
  /// and the unbound outpoint, which receives unbound inscriptions.
  pub fn is_special_outpoint(outpoint: OutPoint) -> bool {
    outpoint == OutPoint::null() || outpoint == unbound_outpoint()
  }

  #[cfg(test)]
  fn set_durability(&mut self, durability: redb::Durability) {
    self.durability = durability;
  }

  pub fn contains_output(&self, output: &OutPoint) -> Result<bool> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(OUTPOINT_TO_UTXO_ENTRY)?
        .get(&output.store())?
        .is_some(),
    )
  }

  pub fn has_address_index(&self) -> bool {
    self.index_addresses
  }

  pub fn has_inscription_index(&self) -> bool {
    self.index_inscriptions
  }

  pub fn has_rune_index(&self) -> bool {
    self.index_runes
  }

  pub fn has_sat_index(&self) -> bool {
    self.index_sats
  }

  pub fn status(&self, json_api: bool) -> Result<StatusHtml> {
    let rtx = self.database.begin_read()?;

    let statistic_to_count = rtx.open_table(STATISTIC_TO_COUNT)?;

    let statistic = |statistic: Statistic| -> Result<u64> {
      Ok(
        statistic_to_count
          .get(statistic.key())?
          .map(|guard| guard.value())
          .unwrap_or_default(),
      )
    };

    let height = rtx
      .open_table(HEIGHT_TO_BLOCK_HEADER)?
      .range(0..)?
      .next_back()
      .transpose()?
      .map(|(height, _header)| height.value());

    let next_height = height.map(|height| height + 1).unwrap_or(0);

    let blessed_inscriptions = statistic(Statistic::BlessedInscriptions)?;
    let cursed_inscriptions = statistic(Statistic::CursedInscriptions)?;
    let initial_sync_time = statistic(Statistic::InitialSyncTime)?;

    Ok(StatusHtml {
      address_index: self.has_address_index(),
      blessed_inscriptions,
      chain: self.settings.chain(),
      cursed_inscriptions,
      height,
      initial_sync_time: Duration::from_micros(initial_sync_time),
      inscription_index: self.has_inscription_index(),
      inscriptions: blessed_inscriptions + cursed_inscriptions,
      json_api,
      lost_sats: statistic(Statistic::LostSats)?,
      minimum_rune_for_next_block: Rune::minimum_at_height(
        self.settings.chain().network(),
        Height(next_height),
      ),
      rune_index: self.has_rune_index(),
      runes: statistic(Statistic::Runes)?,
      sat_index: self.has_sat_index(),
      started: self.started,
      transaction_index: statistic(Statistic::IndexTransactions)? != 0,
      unrecoverably_reorged: self.unrecoverably_reorged.load(atomic::Ordering::Relaxed),
      uptime: (Utc::now() - self.started).to_std()?,
    })
  }

  pub fn info(&self) -> Result<Info> {
    let stats = self.database.begin_write()?.stats()?;

    let rtx = self.database.begin_read()?;

    let mut tables: BTreeMap<String, TableInfo> = BTreeMap::new();

    for handle in rtx.list_tables()? {
      let name = handle.name().into();
      let stats = rtx.open_untyped_table(handle)?.stats()?;
      tables.insert(name, stats.into());
    }

    for handle in rtx.list_multimap_tables()? {
      let name = handle.name().into();
      let stats = rtx.open_untyped_multimap_table(handle)?.stats()?;
      tables.insert(name, stats.into());
    }

    for table in rtx.list_tables()? {
      assert!(tables.contains_key(table.name()));
    }

    for table in rtx.list_multimap_tables()? {
      assert!(tables.contains_key(table.name()));
    }

    let total_bytes = tables
      .values()
      .map(|table_info| table_info.total_bytes)
      .sum();

    tables.values_mut().for_each(|table_info| {
      table_info.proportion = table_info.total_bytes as f64 / total_bytes as f64
    });

    let info = {
      let statistic_to_count = rtx.open_table(STATISTIC_TO_COUNT)?;
      let sat_ranges = statistic_to_count
        .get(&Statistic::SatRanges.key())?
        .map(|x| x.value())
        .unwrap_or(0);
      let outputs_traversed = statistic_to_count
        .get(&Statistic::OutputsTraversed.key())?
        .map(|x| x.value())
        .unwrap_or(0);
      Info {
        index_path: self.path.clone(),
        blocks_indexed: rtx
          .open_table(HEIGHT_TO_BLOCK_HEADER)?
          .range(0..)?
          .next_back()
          .transpose()?
          .map(|(height, _header)| height.value() + 1)
          .unwrap_or(0),
        branch_pages: stats.branch_pages(),
        fragmented_bytes: stats.fragmented_bytes(),
        index_file_size: fs::metadata(&self.path)?.len(),
        leaf_pages: stats.leaf_pages(),
        metadata_bytes: stats.metadata_bytes(),
        sat_ranges,
        outputs_traversed,
        page_size: stats.page_size(),
        stored_bytes: stats.stored_bytes(),
        total_bytes,
        tables,
        transactions: rtx
          .open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?
          .range(0..)?
          .flat_map(|result| {
            result.map(
              |(starting_block_count, starting_timestamp)| TransactionInfo {
                starting_block_count: starting_block_count.value(),
                starting_timestamp: starting_timestamp.value(),
              },
            )
          })
          .collect(),
        tree_height: stats.tree_height(),
        utxos_indexed: rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?.len()?,
      }
    };

    Ok(info)
  }

  pub fn update(&self) -> Result {
    loop {
      let wtx = self.begin_write()?;

      let mut updater = Updater {
        height: wtx
          .open_table(HEIGHT_TO_BLOCK_HEADER)?
          .range(0..)?
          .next_back()
          .transpose()?
          .map(|(height, _header)| height.value() + 1)
          .unwrap_or(0),
        index: self,
        outputs_cached: 0,
        outputs_traversed: 0,
        sat_ranges_since_flush: 0,
      };

      match updater.update_index(wtx) {
        Ok(ok) => return Ok(ok),
        Err(err) => {
          log::info!("{err}");

          match err.downcast_ref() {
            Some(&reorg::Error::Recoverable { height, depth }) => {
              Reorg::handle_reorg(self, height, depth)?;
            }
            Some(&reorg::Error::Unrecoverable) => {
              self
                .unrecoverably_reorged
                .store(true, atomic::Ordering::Relaxed);
              return Err(anyhow!(reorg::Error::Unrecoverable));
            }
            _ => return Err(err),
          };
        }
      }
    }
  }

  pub fn export(&self, filename: &String, include_addresses: bool) -> Result {
    let mut writer = BufWriter::new(File::create(filename)?);
    let rtx = self.database.begin_read()?;

    let blocks_indexed = rtx
      .open_table(HEIGHT_TO_BLOCK_HEADER)?
      .range(0..)?
      .next_back()
      .transpose()?
      .map(|(height, _header)| height.value() + 1)
      .unwrap_or(0);

    writeln!(writer, "# export at block height {}", blocks_indexed)?;

    log::info!("exporting database tables to {filename}");

    let sequence_number_to_satpoint = rtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?;
    let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?;

    for result in rtx
      .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?
      .iter()?
    {
      let entry = result?;
      let sequence_number = entry.0.value();
      let entry = InscriptionEntry::load(entry.1.value());
      let satpoint = SatPoint::load(
        *sequence_number_to_satpoint
          .get(sequence_number)?
          .unwrap()
          .value(),
      );

      write!(
        writer,
        "{}\t{}\t{}",
        entry.inscription_number, entry.id, satpoint
      )?;

      if include_addresses {
        let address = if satpoint.outpoint == unbound_outpoint() {
          "unbound".to_string()
        } else {
          let script_pubkey = if self.index_addresses {
            ScriptBuf::from_bytes(
              outpoint_to_utxo_entry
                .get(&satpoint.outpoint.store())?
                .unwrap()
                .value()
                .parse(self)
                .script_pubkey()
                .to_vec(),
            )
          } else {
            self
              .get_transaction(satpoint.outpoint.txid)?
              .unwrap()
              .output
              .into_iter()
              .nth(satpoint.outpoint.vout.try_into().unwrap())
              .unwrap()
              .script_pubkey
          };

          self
            .settings
            .chain()
            .address_from_script(&script_pubkey)
            .map(|address| address.to_string())
            .unwrap_or_else(|e| e.to_string())
        };
        write!(writer, "\t{}", address)?;
      }
      writeln!(writer)?;

      if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
        break;
      }
    }
    writer.flush()?;
    Ok(())
  }

  fn begin_read(&self) -> Result<rtx::Rtx> {
    Ok(rtx::Rtx(self.database.begin_read()?))
  }

  fn begin_write(&self) -> Result<WriteTransaction> {
    let mut tx = self.database.begin_write()?;
    tx.set_durability(self.durability);
    tx.set_quick_repair(true);
    Ok(tx)
  }

  fn increment_statistic(wtx: &WriteTransaction, statistic: Statistic, n: u64) -> Result {
    let mut statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?;
    let value = statistic_to_count
      .get(&(statistic.key()))?
      .map(|x| x.value())
      .unwrap_or_default()
      + n;
    statistic_to_count.insert(&statistic.key(), &value)?;
    Ok(())
  }

  pub(crate) fn set_statistic(
    statistics: &mut Table<u64, u64>,
    statistic: Statistic,
    value: u64,
  ) -> Result<()> {
    statistics.insert(&statistic.key(), &value)?;
    Ok(())
  }

  pub(crate) fn is_statistic_set(
    statistics: &ReadOnlyTable<u64, u64>,
    statistic: Statistic,
  ) -> Result<bool> {
    Ok(
      statistics
        .get(&statistic.key())?
        .map(|guard| guard.value())
        .unwrap_or_default()
        != 0,
    )
  }

  #[cfg(test)]
  pub(crate) fn statistic(&self, statistic: Statistic) -> u64 {
    self
      .database
      .begin_read()
      .unwrap()
      .open_table(STATISTIC_TO_COUNT)
      .unwrap()
      .get(&statistic.key())
      .unwrap()
      .map(|x| x.value())
      .unwrap_or_default()
  }

  #[cfg(test)]
  pub(crate) fn inscription_number(&self, inscription_id: InscriptionId) -> i32 {
    self
      .get_inscription_entry(inscription_id)
      .unwrap()
      .unwrap()
      .inscription_number
  }

  pub fn block_count(&self) -> Result<u32> {
    self.begin_read()?.block_count()
  }

  pub fn block_height(&self) -> Result<Option<Height>> {
    self.begin_read()?.block_height()
  }

  pub fn block_hash(&self, height: Option<u32>) -> Result<Option<BlockHash>> {
    self.begin_read()?.block_hash(height)
  }

  pub fn blocks(&self, take: usize) -> Result<Vec<(u32, BlockHash)>> {
    let rtx = self.begin_read()?;

    let block_count = rtx.block_count()?;

    let height_to_block_header = rtx.0.open_table(HEIGHT_TO_BLOCK_HEADER)?;

    let mut blocks = Vec::with_capacity(block_count.try_into().unwrap());

    for next in height_to_block_header
      .range(0..block_count)?
      .rev()
      .take(take)
    {
      let next = next?;
      blocks.push((next.0.value(), Header::load(*next.1.value()).block_hash()));
    }

    Ok(blocks)
  }

  pub fn rare_sat_satpoints(&self) -> Result<Vec<(Sat, SatPoint)>> {
    let rtx = self.database.begin_read()?;

    let sat_to_satpoint = rtx.open_table(SAT_TO_SATPOINT)?;

    let mut result = Vec::with_capacity(sat_to_satpoint.len()?.try_into().unwrap());

    for range in sat_to_satpoint.range(0..)? {
      let (sat, satpoint) = range?;
      result.push((Sat(sat.value()), Entry::load(*satpoint.value())));
    }

    Ok(result)
  }

  pub fn rare_sat_satpoint(&self, sat: Sat) -> Result<Option<SatPoint>> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(SAT_TO_SATPOINT)?
        .get(&sat.n())?
        .map(|satpoint| Entry::load(*satpoint.value())),
    )
  }

  pub fn get_rune_by_id(&self, id: RuneId) -> Result<Option<Rune>> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(RUNE_ID_TO_RUNE_ENTRY)?
        .get(&id.store())?
        .map(|entry| RuneEntry::load(entry.value()).spaced_rune.rune),
    )
  }

  pub fn get_rune_by_number(&self, number: usize) -> Result<Option<Rune>> {
    match self
      .database
      .begin_read()?
      .open_table(RUNE_ID_TO_RUNE_ENTRY)?
      .iter()?
      .nth(number)
    {
      Some(result) => {
        let rune_result =
          result.map(|(_id, entry)| RuneEntry::load(entry.value()).spaced_rune.rune);
        Ok(rune_result.ok())
      }
      None => Ok(None),
    }
  }

  pub fn rune(&self, rune: Rune) -> Result<Option<(RuneId, RuneEntry, Option<InscriptionId>)>> {
    let rtx = self.database.begin_read()?;

    let Some(id) = rtx
      .open_table(RUNE_TO_RUNE_ID)?
      .get(rune.0)?
      .map(|guard| guard.value())
    else {
      return Ok(None);
    };

    let entry = RuneEntry::load(
      rtx
        .open_table(RUNE_ID_TO_RUNE_ENTRY)?
        .get(id)?
        .unwrap()
        .value(),
    );

    let parent = InscriptionId {
      txid: entry.etching,
      index: 0,
    };

    let parent = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
      .get(&parent.store())?
      .is_some()
      .then_some(parent);

    Ok(Some((RuneId::load(id), entry, parent)))
  }

  pub fn runes(&self) -> Result<Vec<(RuneId, RuneEntry)>> {
    let mut entries = Vec::new();

    for result in self
      .database
      .begin_read()?
      .open_table(RUNE_ID_TO_RUNE_ENTRY)?
      .iter()?
    {
      let (id, entry) = result?;
      entries.push((RuneId::load(id.value()), RuneEntry::load(entry.value())));
    }

    Ok(entries)
  }

  pub fn runes_paginated(
    &self,
    page_size: usize,
    page_index: usize,
  ) -> Result<(Vec<(RuneId, RuneEntry)>, bool)> {
    let mut entries = Vec::new();

    for result in self
      .database
      .begin_read()?
      .open_table(RUNE_ID_TO_RUNE_ENTRY)?
      .iter()?
      .rev()
      .skip(page_index.saturating_mul(page_size))
      .take(page_size.saturating_add(1))
    {
      let (id, entry) = result?;
      entries.push((RuneId::load(id.value()), RuneEntry::load(entry.value())));
    }

    let more = entries.len() > page_size;

    Ok((entries, more))
  }

  pub fn encode_rune_balance(id: RuneId, balance: u128, buffer: &mut Vec<u8>) {
    varint::encode_to_vec(id.block.into(), buffer);
    varint::encode_to_vec(id.tx.into(), buffer);
    varint::encode_to_vec(balance, buffer);
  }

  pub fn decode_rune_balance(buffer: &[u8]) -> Result<((RuneId, u128), usize)> {
    let mut len = 0;
    let (block, block_len) = varint::decode(&buffer[len..])?;
    len += block_len;
    let (tx, tx_len) = varint::decode(&buffer[len..])?;
    len += tx_len;
    let id = RuneId {
      block: block.try_into()?,
      tx: tx.try_into()?,
    };
    let (balance, balance_len) = varint::decode(&buffer[len..])?;
    len += balance_len;
    Ok(((id, balance), len))
  }

  pub fn get_rune_balances_for_output(
    &self,
    outpoint: OutPoint,
  ) -> Result<Option<BTreeMap<SpacedRune, Pile>>> {
    if !self.index_runes {
      return Ok(None);
    }

    let rtx = self.database.begin_read()?;

    let outpoint_to_balances = rtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?;

    let id_to_rune_entries = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;

    let Some(balances) = outpoint_to_balances.get(&outpoint.store())? else {
      return Ok(Some(BTreeMap::new()));
    };

    let balances_buffer = balances.value();

    let mut balances = BTreeMap::new();
    let mut i = 0;
    while i < balances_buffer.len() {
      let ((id, amount), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap();
      i += length;

      let entry = RuneEntry::load(id_to_rune_entries.get(id.store())?.unwrap().value());

      balances.insert(
        entry.spaced_rune,
        Pile {
          amount,
          divisibility: entry.divisibility,
          symbol: entry.symbol,
        },
      );
    }

    Ok(Some(balances))
  }

  pub fn get_rune_balance_map(&self) -> Result<BTreeMap<SpacedRune, BTreeMap<OutPoint, Pile>>> {
    let outpoint_balances = self.get_rune_balances()?;

    let rtx = self.database.begin_read()?;

    let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;

    let mut rune_balances_by_id: BTreeMap<RuneId, BTreeMap<OutPoint, u128>> = BTreeMap::new();

    for (outpoint, balances) in outpoint_balances {
      for (rune_id, amount) in balances {
        *rune_balances_by_id
          .entry(rune_id)
          .or_default()
          .entry(outpoint)
          .or_default() += amount;
      }
    }

    let mut rune_balances = BTreeMap::new();

    for (rune_id, balances) in rune_balances_by_id {
      let RuneEntry {
        divisibility,
        spaced_rune,
        symbol,
        ..
      } = RuneEntry::load(
        rune_id_to_rune_entry
          .get(&rune_id.store())?
          .unwrap()
          .value(),
      );

      rune_balances.insert(
        spaced_rune,
        balances
          .into_iter()
          .map(|(outpoint, amount)| {
            (
              outpoint,
              Pile {
                amount,
                divisibility,
                symbol,
              },
            )
          })
          .collect(),
      );
    }

    Ok(rune_balances)
  }

  pub fn get_rune_balances(&self) -> Result<Vec<(OutPoint, Vec<(RuneId, u128)>)>> {
    let mut result = Vec::new();

    for entry in self
      .database
      .begin_read()?
      .open_table(OUTPOINT_TO_RUNE_BALANCES)?
      .iter()?
    {
      let (outpoint, balances_buffer) = entry?;
      let outpoint = OutPoint::load(*outpoint.value());
      let balances_buffer = balances_buffer.value();

      let mut balances = Vec::new();
      let mut i = 0;
      while i < balances_buffer.len() {
        let ((id, balance), length) = Index::decode_rune_balance(&balances_buffer[i..]).unwrap();
        i += length;
        balances.push((id, balance));
      }

      result.push((outpoint, balances));
    }

    Ok(result)
  }

  pub fn block_header(&self, hash: BlockHash) -> Result<Option<Header>> {
    self.client.get_block_header(&hash).into_option()
  }

  pub fn block_header_info(&self, hash: BlockHash) -> Result<Option<GetBlockHeaderResult>> {
    self.client.get_block_header_info(&hash).into_option()
  }

  pub fn block_stats(&self, height: u64) -> Result<Option<GetBlockStatsResult>> {
    self.client.get_block_stats(height).into_option()
  }

  pub fn get_block_by_height(&self, height: u32) -> Result<Option<Block>> {
    Ok(
      self
        .client
        .get_block_hash(height.into())
        .into_option()?
        .map(|hash| self.client.get_block(&hash))
        .transpose()?,
    )
  }

  pub fn get_block_by_hash(&self, hash: BlockHash) -> Result<Option<Block>> {
    self.client.get_block(&hash).into_option()
  }

  pub fn get_collections_paginated(
    &self,
    page_size: usize,
    page_index: usize,
  ) -> Result<(Vec<InscriptionId>, bool)> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let mut collections = rtx
      .open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?
      .iter()?
      .skip(page_index.saturating_mul(page_size))
      .take(page_size.saturating_add(1))
      .map(|result| {
        result
          .and_then(|(parent, _children)| {
            sequence_number_to_inscription_entry
              .get(parent.value())
              .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          })
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let more = collections.len() > page_size;

    if more {
      collections.pop();
    }

    Ok((collections, more))
  }

  #[cfg(test)]
  pub(crate) fn get_children_by_inscription_id(
    &self,
    inscription_id: InscriptionId,
  ) -> Result<Vec<InscriptionId>> {
    let rtx = self.database.begin_read()?;

    let Some(sequence_number) = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
      .get(&inscription_id.store())?
      .map(|sequence_number| sequence_number.value())
    else {
      return Ok(Vec::new());
    };

    self
      .get_children_by_sequence_number_paginated(sequence_number, usize::MAX, 0)
      .map(|(children, _more)| children)
  }

  #[cfg(test)]
  pub(crate) fn get_parents_by_inscription_id(
    &self,
    inscription_id: InscriptionId,
  ) -> Vec<InscriptionId> {
    let rtx = self.database.begin_read().unwrap();

    let sequence_number = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)
      .unwrap()
      .get(&inscription_id.store())
      .unwrap()
      .unwrap()
      .value();

    let sequence_number_to_inscription_entry = rtx
      .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)
      .unwrap();

    let parent_sequences = InscriptionEntry::load(
      sequence_number_to_inscription_entry
        .get(sequence_number)
        .unwrap()
        .unwrap()
        .value(),
    )
    .parents;

    parent_sequences
      .into_iter()
      .map(|parent_sequence_number| {
        InscriptionEntry::load(
          sequence_number_to_inscription_entry
            .get(parent_sequence_number)
            .unwrap()
            .unwrap()
            .value(),
        )
        .id
      })
      .collect()
  }

  pub fn get_children_by_sequence_number_paginated(
    &self,
    sequence_number: u32,
    page_size: usize,
    page_index: usize,
  ) -> Result<(Vec<InscriptionId>, bool)> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let mut children = rtx
      .open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?
      .get(sequence_number)?
      .skip(page_index * page_size)
      .take(page_size.saturating_add(1))
      .map(|result| {
        result
          .and_then(|sequence_number| {
            sequence_number_to_entry
              .get(sequence_number.value())
              .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          })
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let more = children.len() > page_size;

    if more {
      children.pop();
    }

    Ok((children, more))
  }

  pub fn get_parents_by_sequence_number_paginated(
    &self,
    parent_sequence_numbers: Vec<u32>,
    page_size: usize,
    page_index: usize,
  ) -> Result<(Vec<InscriptionId>, bool)> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_entry = rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let mut parents = parent_sequence_numbers
      .iter()
      .skip(page_index * page_size)
      .take(page_size.saturating_add(1))
      .map(|sequence_number| {
        sequence_number_to_entry
          .get(sequence_number)
          .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let more_parents = parents.len() > page_size;

    if more_parents {
      parents.pop();
    }

    Ok((parents, more_parents))
  }

  pub fn get_etching(&self, txid: Txid) -> Result<Option<SpacedRune>> {
    let rtx = self.database.begin_read()?;

    let transaction_id_to_rune = rtx.open_table(TRANSACTION_ID_TO_RUNE)?;
    let Some(rune) = transaction_id_to_rune.get(&txid.store())? else {
      return Ok(None);
    };

    let rune_to_rune_id = rtx.open_table(RUNE_TO_RUNE_ID)?;
    let id = rune_to_rune_id.get(rune.value())?.unwrap();

    let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;
    let entry = rune_id_to_rune_entry.get(&id.value())?.unwrap();

    Ok(Some(RuneEntry::load(entry.value()).spaced_rune))
  }

  pub fn get_inscription_ids_by_sat(&self, sat: Sat) -> Result<Vec<InscriptionId>> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let ids = rtx
      .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?
      .get(&sat.n())?
      .map(|result| {
        result
          .and_then(|sequence_number| {
            let sequence_number = sequence_number.value();
            sequence_number_to_inscription_entry
              .get(sequence_number)
              .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          })
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    Ok(ids)
  }

  pub fn get_inscription_ids_by_sat_paginated(
    &self,
    sat: Sat,
    page_size: u64,
    page_index: u64,
  ) -> Result<(Vec<InscriptionId>, bool)> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let mut ids = rtx
      .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?
      .get(&sat.n())?
      .skip(page_index.saturating_mul(page_size).try_into().unwrap())
      .take(page_size.saturating_add(1).try_into().unwrap())
      .map(|result| {
        result
          .and_then(|sequence_number| {
            let sequence_number = sequence_number.value();
            sequence_number_to_inscription_entry
              .get(sequence_number)
              .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          })
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let more = ids.len().into_u64() > page_size;

    if more {
      ids.pop();
    }

    Ok((ids, more))
  }

  pub fn get_inscription_id_by_sat_indexed(
    &self,
    sat: Sat,
    inscription_index: isize,
  ) -> Result<Option<InscriptionId>> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let sat_to_sequence_number = rtx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?;

    if inscription_index < 0 {
      sat_to_sequence_number
        .get(&sat.n())?
        .nth_back((inscription_index + 1).abs_diff(0))
    } else {
      sat_to_sequence_number
        .get(&sat.n())?
        .nth(inscription_index.abs_diff(0))
    }
    .map(|result| {
      result
        .and_then(|sequence_number| {
          let sequence_number = sequence_number.value();
          sequence_number_to_inscription_entry
            .get(sequence_number)
            .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
        })
        .map_err(|err| anyhow!(err.to_string()))
    })
    .transpose()
  }

  #[cfg(test)]
  pub(crate) fn get_inscription_id_by_inscription_number(
    &self,
    inscription_number: i32,
  ) -> Result<Option<InscriptionId>> {
    let rtx = self.database.begin_read()?;

    let Some(sequence_number) = rtx
      .open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?
      .get(inscription_number)?
      .map(|guard| guard.value())
    else {
      return Ok(None);
    };

    let inscription_id = rtx
      .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?
      .get(&sequence_number)?
      .map(|entry| InscriptionEntry::load(entry.value()).id);

    Ok(inscription_id)
  }

  pub fn get_inscription_satpoint_by_id(
    &self,
    inscription_id: InscriptionId,
  ) -> Result<Option<SatPoint>> {
    let rtx = self.database.begin_read()?;

    let Some(sequence_number) = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
      .get(&inscription_id.store())?
      .map(|guard| guard.value())
    else {
      return Ok(None);
    };

    let satpoint = rtx
      .open_table(SEQUENCE_NUMBER_TO_SATPOINT)?
      .get(sequence_number)?
      .map(|satpoint| Entry::load(*satpoint.value()));

    Ok(satpoint)
  }

  pub fn get_inscription_by_id(
    &self,
    inscription_id: InscriptionId,
  ) -> Result<Option<Inscription>> {
    if !self.inscription_exists(inscription_id)? {
      return Ok(None);
    }

    Ok(self.get_transaction(inscription_id.txid)?.and_then(|tx| {
      ParsedEnvelope::from_transaction(&tx)
        .into_iter()
        .nth(inscription_id.index as usize)
        .map(|envelope| envelope.payload)
    }))
  }

  pub fn inscription_count(&self, txid: Txid) -> Result<u32> {
    let start = InscriptionId { index: 0, txid };

    let end = InscriptionId {
      index: u32::MAX,
      txid,
    };

    Ok(
      self
        .database
        .begin_read()?
        .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
        .range::<&InscriptionIdValue>(&start.store()..&end.store())?
        .count()
        .try_into()
        .unwrap(),
    )
  }

  pub fn inscription_exists(&self, inscription_id: InscriptionId) -> Result<bool> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
        .get(&inscription_id.store())?
        .is_some(),
    )
  }

  pub fn get_inscriptions_on_output_with_satpoints(
    &self,
    outpoint: OutPoint,
  ) -> Result<Option<Vec<(SatPoint, InscriptionId)>>> {
    if !self.index_inscriptions {
      return Ok(None);
    }

    let rtx = self.database.begin_read()?;
    let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?;
    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    self.inscriptions_on_output(
      &outpoint_to_utxo_entry,
      &sequence_number_to_inscription_entry,
      outpoint,
    )
  }

  pub fn get_inscriptions_for_output(
    &self,
    outpoint: OutPoint,
  ) -> Result<Option<Vec<InscriptionId>>> {
    let Some(inscriptions) = self.get_inscriptions_on_output_with_satpoints(outpoint)? else {
      return Ok(None);
    };

    Ok(Some(
      inscriptions
        .iter()
        .map(|(_satpoint, inscription_id)| *inscription_id)
        .collect(),
    ))
  }

  pub fn get_inscriptions_for_outputs(
    &self,
    outpoints: &Vec<OutPoint>,
  ) -> Result<Option<Vec<InscriptionId>>> {
    let mut result = Vec::new();
    for outpoint in outpoints {
      let Some(inscriptions) = self.get_inscriptions_on_output_with_satpoints(*outpoint)? else {
        return Ok(None);
      };

      result.extend(
        inscriptions
          .iter()
          .map(|(_satpoint, inscription_id)| *inscription_id),
      );
    }

    Ok(Some(result))
  }

  pub fn get_unspent_or_unconfirmed_output(
    &self,
    txid: &Txid,
    vout: u32,
  ) -> Result<Option<GetTxOutResult>> {
    if txid == &self.genesis_block_coinbase_txid {
      let Some(output) = &self
        .genesis_block_coinbase_transaction
        .output
        .get(vout.into_usize())
      else {
        return Ok(None);
      };

      return Ok(Some(GetTxOutResult {
        bestblock: self.block_hash(None)?.unwrap(),
        coinbase: true,
        confirmations: self.block_count()?,
        script_pub_key: GetRawTransactionResultVoutScriptPubKey {
          address: None,
          addresses: Vec::new(),
          asm: output.script_pubkey.to_asm_string(),
          hex: output.script_pubkey.to_bytes(),
          req_sigs: Some(1),
          type_: Some(bitcoincore_rpc::json::ScriptPubkeyType::Pubkey),
        },
        value: output.value,
      }));
    }

    Ok(self.client.get_tx_out(txid, vout, Some(true))?)
  }

  pub fn get_transaction_info(&self, txid: &Txid) -> Result<Option<GetRawTransactionResult>> {
    if txid == &self.genesis_block_coinbase_txid {
      let tx = &self.genesis_block_coinbase_transaction;

      let block = bitcoin::blockdata::constants::genesis_block(self.settings.chain().network());
      let time = block.header.time.into_usize();

      return Ok(Some(GetRawTransactionResult {
        in_active_chain: Some(true),
        hex: consensus::encode::serialize(tx),
        txid: tx.compute_txid(),
        hash: tx.compute_wtxid(),
        size: tx.total_size(),
        vsize: tx.vsize(),
        #[allow(clippy::cast_sign_loss)]
        version: tx.version.0 as u32,
        locktime: 0,
        vin: Vec::new(),
        vout: tx
          .output
          .iter()
          .enumerate()
          .map(|(n, output)| GetRawTransactionResultVout {
            n: n.try_into().unwrap(),
            value: output.value,
            script_pub_key: GetRawTransactionResultVoutScriptPubKey {
              asm: output.script_pubkey.to_asm_string(),
              hex: output.script_pubkey.clone().into(),
              req_sigs: None,
              type_: None,
              addresses: Vec::new(),
              address: None,
            },
          })
          .collect(),
        blockhash: Some(block.block_hash()),
        confirmations: Some(self.block_count()?),
        time: Some(time),
        blocktime: Some(time),
      }));
    }

    self
      .client
      .get_raw_transaction_info(txid, None)
      .into_option()
  }

  pub fn get_transaction(&self, txid: Txid) -> Result<Option<Transaction>> {
    if txid == self.genesis_block_coinbase_txid {
      return Ok(Some(self.genesis_block_coinbase_transaction.clone()));
    }

    if self.index_transactions {
      if let Some(transaction) = self
        .database
        .begin_read()?
        .open_table(TRANSACTION_ID_TO_TRANSACTION)?
        .get(&txid.store())?
      {
        return Ok(Some(consensus::encode::deserialize(transaction.value())?));
      }
    }

    self.client.get_raw_transaction(&txid, None).into_option()
  }

  pub fn get_transaction_hex_recursive(&self, txid: Txid) -> Result<Option<String>> {
    if txid == self.genesis_block_coinbase_txid {
      return Ok(Some(consensus::encode::serialize_hex(
        &self.genesis_block_coinbase_transaction,
      )));
    }

    self
      .client
      .get_raw_transaction_hex(&txid, None)
      .into_option()
  }

  pub fn find(&self, sat: Sat) -> Result<Option<SatPoint>> {
    let sat = sat.0;
    let rtx = self.begin_read()?;

    if rtx.block_count()? <= Sat(sat).height().n() {
      return Ok(None);
    }

    let outpoint_to_utxo_entry = rtx.0.open_table(OUTPOINT_TO_UTXO_ENTRY)?;

    for entry in outpoint_to_utxo_entry.iter()? {
      let (outpoint, utxo_entry) = entry?;
      let sat_ranges = utxo_entry.value().parse(self).sat_ranges();

      let mut offset = 0;
      for chunk in sat_ranges.chunks_exact(11) {
        let (start, end) = SatRange::load(chunk.try_into().unwrap());
        if start <= sat && sat < end {
          return Ok(Some(SatPoint {
            outpoint: Entry::load(*outpoint.value()),
            offset: offset + sat - start,
          }));
        }
        offset += end - start;
      }
    }

    Ok(None)
  }

  pub fn find_range(
    &self,
    range_start: Sat,
    range_end: Sat,
  ) -> Result<Option<Vec<FindRangeOutput>>> {
    let range_start = range_start.0;
    let range_end = range_end.0;
    let rtx = self.begin_read()?;

    if rtx.block_count()? < Sat(range_end - 1).height().n() + 1 {
      return Ok(None);
    }

    let Some(mut remaining_sats) = range_end.checked_sub(range_start) else {
      return Err(anyhow!("range end is before range start"));
    };

    let outpoint_to_utxo_entry = rtx.0.open_table(OUTPOINT_TO_UTXO_ENTRY)?;

    let mut result = Vec::new();
    for entry in outpoint_to_utxo_entry.iter()? {
      let (outpoint, utxo_entry) = entry?;
      let sat_ranges = utxo_entry.value().parse(self).sat_ranges();

      let mut offset = 0;
      for sat_range in sat_ranges.chunks_exact(11) {
        let (start, end) = SatRange::load(sat_range.try_into().unwrap());

        if end > range_start && start < range_end {
          let overlap_start = start.max(range_start);
          let overlap_end = end.min(range_end);

          result.push(FindRangeOutput {
            start: overlap_start,
            size: overlap_end - overlap_start,
            satpoint: SatPoint {
              outpoint: Entry::load(*outpoint.value()),
              offset: offset + overlap_start - start,
            },
          });

          remaining_sats -= overlap_end - overlap_start;

          if remaining_sats == 0 {
            break;
          }
        }
        offset += end - start;
      }
    }

    Ok(Some(result))
  }

  pub fn list(&self, outpoint: OutPoint) -> Result<Option<Vec<(u64, u64)>>> {
    if !self.index_sats {
      return Ok(None);
    }

    Ok(
      self
        .database
        .begin_read()?
        .open_table(OUTPOINT_TO_UTXO_ENTRY)?
        .get(&outpoint.store())?
        .map(|utxo_entry| {
          utxo_entry
            .value()
            .parse(self)
            .sat_ranges()
            .chunks_exact(11)
            .map(|chunk| SatRange::load(chunk.try_into().unwrap()))
            .collect::<Vec<(u64, u64)>>()
        }),
    )
  }

  pub fn is_output_spent(&self, outpoint: OutPoint) -> Result<bool> {
    Ok(
      outpoint != OutPoint::null()
        && outpoint != self.settings.chain().genesis_coinbase_outpoint()
        && if self.have_full_utxo_index() {
          self
            .database
            .begin_read()?
            .open_table(OUTPOINT_TO_UTXO_ENTRY)?
            .get(&outpoint.store())?
            .is_none()
        } else {
          self
            .client
            .get_tx_out(&outpoint.txid, outpoint.vout, Some(true))?
            .is_none()
        },
    )
  }

  pub fn is_output_in_active_chain(&self, outpoint: OutPoint) -> Result<bool> {
    if outpoint == OutPoint::null() {
      return Ok(true);
    }

    if outpoint == self.settings.chain().genesis_coinbase_outpoint() {
      return Ok(true);
    }

    let Some(info) = self
      .client
      .get_raw_transaction_info(&outpoint.txid, None)
      .into_option()?
    else {
      return Ok(false);
    };

    if info.blockhash.is_none() {
      return Ok(false);
    }

    if outpoint.vout.into_usize() >= info.vout.len() {
      return Ok(false);
    }

    Ok(true)
  }

  pub fn block_time(&self, height: Height) -> Result<Blocktime> {
    let height = height.n();

    let rtx = self.database.begin_read()?;

    let height_to_block_header = rtx.open_table(HEIGHT_TO_BLOCK_HEADER)?;

    if let Some(guard) = height_to_block_header.get(height)? {
      return Ok(Blocktime::confirmed(Header::load(*guard.value()).time));
    }

    let current = height_to_block_header
      .range(0..)?
      .next_back()
      .transpose()?
      .map(|(height, _header)| height)
      .map(|x| x.value())
      .unwrap_or(0);

    let expected_blocks = height
      .checked_sub(current)
      .with_context(|| format!("current {current} height is greater than sat height {height}"))?;

    Ok(Blocktime::Expected(
      if self.settings.chain() == Chain::Regtest {
        DateTime::default()
      } else {
        Utc::now()
      }
      .round_subsecs(0)
      .checked_add_signed(
        chrono::Duration::try_seconds(10 * 60 * i64::from(expected_blocks))
          .context("timestamp out of range")?,
      )
      .context("timestamp out of range")?,
    ))
  }

  pub fn get_inscriptions_paginated(
    &self,
    page_size: u32,
    page_index: u32,
  ) -> Result<(Vec<InscriptionId>, bool)> {
    let rtx = self.database.begin_read()?;

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let last = sequence_number_to_inscription_entry
      .iter()?
      .next_back()
      .map(|result| result.map(|(number, _entry)| number.value()))
      .transpose()?
      .unwrap_or_default();

    let start = last.saturating_sub(page_size.saturating_mul(page_index));

    let end = start.saturating_sub(page_size);

    let mut inscriptions = sequence_number_to_inscription_entry
      .range(end..=start)?
      .rev()
      .map(|result| result.map(|(_number, entry)| InscriptionEntry::load(entry.value()).id))
      .collect::<Result<Vec<InscriptionId>, StorageError>>()?;

    let more = u32::try_from(inscriptions.len()).unwrap_or(u32::MAX) > page_size;

    if more {
      inscriptions.pop();
    }

    Ok((inscriptions, more))
  }

  pub fn get_inscriptions_in_block(&self, block_height: u32) -> Result<Vec<InscriptionId>> {
    let rtx = self.database.begin_read()?;

    let height_to_last_sequence_number = rtx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?;
    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let Some(newest_sequence_number) = height_to_last_sequence_number
      .get(&block_height)?
      .map(|ag| ag.value())
    else {
      return Ok(Vec::new());
    };

    let oldest_sequence_number = height_to_last_sequence_number
      .get(block_height.saturating_sub(1))?
      .map(|ag| ag.value())
      .unwrap_or(0);

    (oldest_sequence_number..newest_sequence_number)
      .map(|num| match sequence_number_to_inscription_entry.get(&num) {
        Ok(Some(inscription_id)) => Ok(InscriptionEntry::load(inscription_id.value()).id),
        Ok(None) => Err(anyhow!(
          "could not find inscription for inscription number {num}"
        )),
        Err(err) => Err(anyhow!(err)),
      })
      .collect::<Result<Vec<InscriptionId>>>()
  }

  pub fn get_runes_in_block(&self, block_height: u64) -> Result<Vec<SpacedRune>> {
    let rtx = self.database.begin_read()?;

    let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;

    let min_id = RuneId {
      block: block_height,
      tx: 0,
    };

    let max_id = RuneId {
      block: block_height,
      tx: u32::MAX,
    };

    let runes = rune_id_to_rune_entry
      .range(min_id.store()..=max_id.store())?
      .map(|result| result.map(|(_, entry)| RuneEntry::load(entry.value()).spaced_rune))
      .collect::<Result<Vec<SpacedRune>, StorageError>>()?;

    Ok(runes)
  }

  pub fn get_highest_paying_inscriptions_in_block(
    &self,
    block_height: u32,
    n: usize,
  ) -> Result<(Vec<InscriptionId>, usize)> {
    let inscription_ids = self.get_inscriptions_in_block(block_height)?;

    let mut inscription_to_fee: Vec<(InscriptionId, u64)> = Vec::new();
    for id in &inscription_ids {
      inscription_to_fee.push((
        *id,
        self
          .get_inscription_entry(*id)?
          .ok_or_else(|| anyhow!("could not get entry for inscription {id}"))?
          .fee,
      ));
    }

    inscription_to_fee.sort_by_key(|(_, fee)| *fee);

    Ok((
      inscription_to_fee
        .iter()
        .map(|(id, _)| *id)
        .rev()
        .take(n)
        .collect(),
      inscription_ids.len(),
    ))
  }

  pub fn get_home_inscriptions(&self) -> Result<Vec<InscriptionId>> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(HOME_INSCRIPTIONS)?
        .iter()?
        .rev()
        .flat_map(|result| result.map(|(_number, id)| InscriptionId::load(id.value())))
        .collect(),
    )
  }

  pub fn get_feed_inscriptions(&self, n: usize) -> Result<Vec<(u32, InscriptionId)>> {
    Ok(
      self
        .database
        .begin_read()?
        .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?
        .iter()?
        .rev()
        .take(n)
        .flat_map(|result| {
          result.map(|(number, entry)| (number.value(), InscriptionEntry::load(entry.value()).id))
        })
        .collect(),
    )
  }

  pub(crate) fn inscription_info(
    &self,
    query: query::Inscription,
    child: Option<usize>,
  ) -> Result<Option<(api::Inscription, Option<TxOut>, Inscription)>> {
    let rtx = self.database.begin_read()?;

    let sequence_number = match query {
      query::Inscription::Id(id) => rtx
        .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
        .get(&id.store())?
        .map(|guard| guard.value()),
      query::Inscription::Number(inscription_number) => rtx
        .open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?
        .get(inscription_number)?
        .map(|guard| guard.value()),
      query::Inscription::Sat(sat) => rtx
        .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?
        .get(sat.n())?
        .next()
        .transpose()?
        .map(|guard| guard.value()),
    };

    let Some(sequence_number) = sequence_number else {
      return Ok(None);
    };

    let sequence_number = if let Some(child) = child {
      let Some(child) = rtx
        .open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?
        .get(sequence_number)?
        .nth(child)
        .transpose()?
        .map(|child| child.value())
      else {
        return Ok(None);
      };

      child
    } else {
      sequence_number
    };

    let sequence_number_to_inscription_entry =
      rtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;

    let entry = InscriptionEntry::load(
      sequence_number_to_inscription_entry
        .get(&sequence_number)?
        .unwrap()
        .value(),
    );

    let Some(transaction) = self.get_transaction(entry.id.txid)? else {
      return Ok(None);
    };

    let Some(inscription) = ParsedEnvelope::from_transaction(&transaction)
      .into_iter()
      .nth(entry.id.index as usize)
      .map(|envelope| envelope.payload)
    else {
      return Ok(None);
    };

    let satpoint = SatPoint::load(
      *rtx
        .open_table(SEQUENCE_NUMBER_TO_SATPOINT)?
        .get(sequence_number)?
        .unwrap()
        .value(),
    );

    let output = if satpoint.outpoint == unbound_outpoint() || satpoint.outpoint == OutPoint::null()
    {
      None
    } else {
      let Some(transaction) = self.get_transaction(satpoint.outpoint.txid)? else {
        return Ok(None);
      };

      transaction
        .output
        .into_iter()
        .nth(satpoint.outpoint.vout.try_into().unwrap())
    };

    let previous = if let Some(n) = sequence_number.checked_sub(1) {
      Some(
        InscriptionEntry::load(
          sequence_number_to_inscription_entry
            .get(n)?
            .unwrap()
            .value(),
        )
        .id,
      )
    } else {
      None
    };

    let next = sequence_number_to_inscription_entry
      .get(sequence_number + 1)?
      .map(|guard| InscriptionEntry::load(guard.value()).id);

    let all_children = rtx
      .open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?
      .get(sequence_number)?;

    let child_count = all_children.len();

    let children = all_children
      .take(4)
      .map(|result| {
        result
          .and_then(|sequence_number| {
            sequence_number_to_inscription_entry
              .get(sequence_number.value())
              .map(|entry| InscriptionEntry::load(entry.unwrap().value()).id)
          })
          .map_err(|err| err.into())
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let rune = if let Some(rune_id) = rtx
      .open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?
      .get(sequence_number)?
    {
      let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;
      let entry = rune_id_to_rune_entry.get(&rune_id.value())?.unwrap();
      Some(RuneEntry::load(entry.value()).spaced_rune)
    } else {
      None
    };

    let parents = entry
      .parents
      .iter()
      .take(4)
      .map(|parent| {
        Ok(
          InscriptionEntry::load(
            sequence_number_to_inscription_entry
              .get(parent)?
              .unwrap()
              .value(),
          )
          .id,
        )
      })
      .collect::<Result<Vec<InscriptionId>>>()?;

    let mut charms = entry.charms;

    if satpoint.outpoint == OutPoint::null() {
      Charm::Lost.set(&mut charms);
    }

    let effective_mime_type = if let Some(delegate_id) = inscription.delegate() {
      let delegate_result = self.get_inscription_by_id(delegate_id);
      if let Ok(Some(delegate)) = delegate_result {
        delegate.content_type().map(str::to_string)
      } else {
        inscription.content_type().map(str::to_string)
      }
    } else {
      inscription.content_type().map(str::to_string)
    };

    Ok(Some((
      api::Inscription {
        address: output
          .as_ref()
          .and_then(|o| {
            self
              .settings
              .chain()
              .address_from_script(&o.script_pubkey)
              .ok()
          })
          .map(|address| address.to_string()),
        charms: Charm::charms(charms),
        child_count,
        children,
        content_length: inscription.content_length(),
        content_type: inscription.content_type().map(|s| s.to_string()),
        effective_content_type: effective_mime_type,
        fee: entry.fee,
        height: entry.height,
        id: entry.id,
        next,
        number: entry.inscription_number,
        parents,
        previous,
        rune,
        sat: entry.sat,
        satpoint,
        timestamp: timestamp(entry.timestamp.into()).timestamp(),
        value: output.as_ref().map(|o| o.value.to_sat()),
        metaprotocol: inscription.metaprotocol().map(|s| s.to_string()),
      },
      output,
      inscription,
    )))
  }

  pub fn get_inscription_entry(
    &self,
    inscription_id: InscriptionId,
  ) -> Result<Option<InscriptionEntry>> {
    let rtx = self.database.begin_read()?;

    let Some(sequence_number) = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?
      .get(&inscription_id.store())?
      .map(|guard| guard.value())
    else {
      return Ok(None);
    };

    let entry = rtx
      .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?
      .get(sequence_number)?
      .map(|value| InscriptionEntry::load(value.value()));

    Ok(entry)
  }

  #[cfg(test)]
  fn assert_inscription_location(
    &self,
    inscription_id: InscriptionId,
    satpoint: SatPoint,
    sat: Option<u64>,
  ) {
    let rtx = self.database.begin_read().unwrap();

    let outpoint_to_utxo_entry = rtx.open_table(OUTPOINT_TO_UTXO_ENTRY).unwrap();

    let sequence_number_to_satpoint = rtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT).unwrap();

    let sequence_number = rtx
      .open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)
      .unwrap()
      .get(&inscription_id.store())
      .unwrap()
      .unwrap()
      .value();

    assert_eq!(
      SatPoint::load(
        *sequence_number_to_satpoint
          .get(sequence_number)
          .unwrap()
          .unwrap()
          .value()
      ),
      satpoint,
    );

    let utxo_entry = outpoint_to_utxo_entry
      .get(&satpoint.outpoint.store())
      .unwrap()
      .unwrap();
    let parsed_inscriptions = utxo_entry.value().parse(self).parse_inscriptions();
    let satpoint_offsets: Vec<u64> = parsed_inscriptions
      .iter()
      .copied()
      .filter_map(|(seq, offset)| (seq == sequence_number).then_some(offset))
      .collect();
    assert!(satpoint_offsets == [satpoint.offset]);

    match sat {
      Some(sat) => {
        if self.index_sats {
          // unbound inscriptions should not be assigned to a sat
          assert_ne!(satpoint.outpoint, unbound_outpoint());

          assert!(rtx
            .open_multimap_table(SAT_TO_SEQUENCE_NUMBER)
            .unwrap()
            .get(&sat)
            .unwrap()
            .any(|entry| entry.unwrap().value() == sequence_number));

          // we do not track common sats (only the sat ranges)
          if !Sat(sat).common() {
            assert_eq!(
              SatPoint::load(
                *rtx
                  .open_table(SAT_TO_SATPOINT)
                  .unwrap()
                  .get(&sat)
                  .unwrap()
                  .unwrap()
                  .value()
              ),
              satpoint,
            );
          }
        }
      }
      None => {
        if self.index_sats {
          assert_eq!(satpoint.outpoint, unbound_outpoint())
        }
      }
    }
  }

  fn inscriptions_on_output<'a: 'tx, 'tx>(
    &self,
    outpoint_to_utxo_entry: &'a impl ReadableTable<&'static OutPointValue, &'static UtxoEntry>,
    sequence_number_to_inscription_entry: &'a impl ReadableTable<u32, InscriptionEntryValue>,
    outpoint: OutPoint,
  ) -> Result<Option<Vec<(SatPoint, InscriptionId)>>> {
    if !self.index_inscriptions {
      return Ok(None);
    }

    let Some(utxo_entry) = outpoint_to_utxo_entry.get(&outpoint.store())? else {
      return Ok(Some(Vec::new()));
    };

    let mut inscriptions = utxo_entry.value().parse(self).parse_inscriptions();

    inscriptions.sort_by_key(|(sequence_number, _)| *sequence_number);

    inscriptions
      .into_iter()
      .map(|(sequence_number, offset)| {
        let entry = sequence_number_to_inscription_entry
          .get(sequence_number)?
          .unwrap();

        let satpoint = SatPoint { outpoint, offset };

        Ok((satpoint, InscriptionEntry::load(entry.value()).id))
      })
      .collect::<Result<_>>()
      .map(Some)
  }

  pub fn get_address_info(&self, address: &Address) -> Result<Vec<OutPoint>> {
    self
      .database
      .begin_read()?
      .open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?
      .get(address.script_pubkey().as_bytes())?
      .map(|result| {
        result
          .map_err(|err| anyhow!(err))
          .map(|value| OutPoint::load(value.value()))
      })
      .collect()
  }

  pub(crate) fn get_aggregated_rune_balances_for_outputs(
    &self,
    outputs: &Vec<OutPoint>,
  ) -> Result<Option<Vec<(SpacedRune, Decimal, Option<char>)>>> {
    let mut runes = BTreeMap::new();

    for output in outputs {
      let Some(rune_balances) = self.get_rune_balances_for_output(*output)? else {
        return Ok(None);
      };

      for (spaced_rune, pile) in rune_balances {
        runes
          .entry(spaced_rune)
          .and_modify(|(decimal, _symbol): &mut (Decimal, Option<char>)| {
            assert_eq!(decimal.scale, pile.divisibility);
            decimal.value += pile.amount;
          })
          .or_insert((
            Decimal {
              value: pile.amount,
              scale: pile.divisibility,
            },
            pile.symbol,
          ));
      }
    }

    Ok(Some(
      runes
        .into_iter()
        .map(|(spaced_rune, (decimal, symbol))| (spaced_rune, decimal, symbol))
        .collect(),
    ))
  }

  pub(crate) fn get_sat_balances_for_outputs(&self, outputs: &Vec<OutPoint>) -> Result<u64> {
    let outpoint_to_utxo_entry = self
      .database
      .begin_read()?
      .open_table(OUTPOINT_TO_UTXO_ENTRY)?;

    let mut acc = 0;
    for output in outputs {
      if let Some(utxo_entry) = outpoint_to_utxo_entry.get(&output.store())? {
        acc += utxo_entry.value().parse(self).total_value();
      };
    }

    Ok(acc)
  }

  pub(crate) fn get_utxo_recursive(
    &self,
    outpoint: OutPoint,
  ) -> Result<Option<api::UtxoRecursive>> {
    let Some(utxo_entry) = self
      .database
      .begin_read()?
      .open_table(OUTPOINT_TO_UTXO_ENTRY)?
      .get(&outpoint.store())?
    else {
      return Ok(None);
    };

    Ok(Some(api::UtxoRecursive {
      inscriptions: self.get_inscriptions_for_output(outpoint)?,
      runes: self.get_rune_balances_for_output(outpoint)?,
      sat_ranges: self.list(outpoint)?,
      value: utxo_entry.value().parse(self).total_value(),
    }))
  }

  pub(crate) fn get_output_info(&self, outpoint: OutPoint) -> Result<Option<(api::Output, TxOut)>> {
    let sat_ranges = self.list(outpoint)?;

    let confirmations;
    let indexed;
    let spent;
    let txout;

    if outpoint == OutPoint::null() || outpoint == unbound_outpoint() {
      let mut value = 0;

      if let Some(ranges) = &sat_ranges {
        for (start, end) in ranges {
          value += end - start;
        }
      }

      confirmations = 0;
      indexed = true;
      spent = false;
      txout = TxOut {
        value: Amount::from_sat(value),
        script_pubkey: ScriptBuf::new(),
      };
    } else {
      indexed = self.contains_output(&outpoint)?;

      if let Some(result) = self.get_unspent_or_unconfirmed_output(&outpoint.txid, outpoint.vout)? {
        confirmations = result.confirmations;
        spent = false;
        txout = TxOut {
          value: result.value,
          script_pubkey: ScriptBuf::from_bytes(result.script_pub_key.hex),
        };
      } else {
        let Some(result) = self.get_transaction_info(&outpoint.txid)? else {
          return Ok(None);
        };

        let Some(output) = result.vout.into_iter().nth(outpoint.vout.into_usize()) else {
          return Ok(None);
        };

        confirmations = result.confirmations.unwrap_or_default();
        spent = true;
        txout = TxOut {
          value: output.value,
          script_pubkey: ScriptBuf::from_bytes(output.script_pub_key.hex),
        };
      }
    };

    let inscriptions = self.get_inscriptions_for_output(outpoint)?;

    let runes = self.get_rune_balances_for_output(outpoint)?;

    Ok(Some((
      api::Output::new(
        self.settings.chain(),
        confirmations,
        inscriptions,
        outpoint,
        txout.clone(),
        indexed,
        runes,
        sat_ranges,
        spent,
      ),
      txout,
    )))
  }
}

#[cfg(test)]
mod tests {
  use {super::*, crate::index::testing::Context};

  #[test]
  fn height_limit() {
    {
      let context = Context::builder().args(["--height-limit", "0"]).build();
      context.mine_blocks(1);
      assert_eq!(context.index.block_height().unwrap(), None);
      assert_eq!(context.index.block_count().unwrap(), 0);
    }

    {
      let context = Context::builder().args(["--height-limit", "1"]).build();
      context.mine_blocks(1);
      assert_eq!(context.index.block_height().unwrap(), Some(Height(0)));
      assert_eq!(context.index.block_count().unwrap(), 1);
    }

    {
      let context = Context::builder().args(["--height-limit", "2"]).build();
      context.mine_blocks(2);
      assert_eq!(context.index.block_height().unwrap(), Some(Height(1)));
      assert_eq!(context.index.block_count().unwrap(), 2);
    }
  }

  #[test]
  fn inscriptions_below_first_inscription_height_are_skipped() {
    let inscription = inscription("text/plain;charset=utf-8", "hello");
    let template = TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    };

    {
      let context = Context::builder().build();
      context.mine_blocks(1);
      let txid = context.core.broadcast_tx(template.clone());
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      assert_eq!(
        context.index.get_inscription_by_id(inscription_id).unwrap(),
        Some(inscription)
      );

      assert_eq!(
        context
          .index
          .get_inscription_satpoint_by_id(inscription_id)
          .unwrap(),
        Some(SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        })
      );
    }

    {
      let context = Context::builder().chain(Chain::Mainnet).build();
      context.mine_blocks(1);
      let txid = context.core.broadcast_tx(template);
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      assert_eq!(
        context
          .index
          .get_inscription_satpoint_by_id(inscription_id)
          .unwrap(),
        None,
      );
    }
  }

  #[test]
  fn inscriptions_are_not_indexed_if_no_index_inscriptions_flag_is_set() {
    let inscription = inscription("text/plain;charset=utf-8", "hello");
    let template = TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    };

    {
      let context = Context::builder().build();
      context.mine_blocks(1);
      let txid = context.core.broadcast_tx(template.clone());
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      assert_eq!(
        context.index.get_inscription_by_id(inscription_id).unwrap(),
        Some(inscription)
      );

      assert_eq!(
        context
          .index
          .get_inscription_satpoint_by_id(inscription_id)
          .unwrap(),
        Some(SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        })
      );
    }

    {
      let context = Context::builder().arg("--no-index-inscriptions").build();
      context.mine_blocks(1);
      let txid = context.core.broadcast_tx(template);
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      assert_eq!(
        context
          .index
          .get_inscription_satpoint_by_id(inscription_id)
          .unwrap(),
        None,
      );
    }
  }

  #[test]
  fn list_first_coinbase_transaction() {
    let context = Context::builder().arg("--index-sats").build();
    assert_eq!(
      context
        .index
        .list(
          "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0"
            .parse()
            .unwrap()
        )
        .unwrap()
        .unwrap(),
      &[(0, 50 * COIN_VALUE)],
    )
  }

  #[test]
  fn list_second_coinbase_transaction() {
    let context = Context::builder().arg("--index-sats").build();
    let txid = context.mine_blocks(1)[0].txdata[0].compute_txid();
    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[(50 * COIN_VALUE, 100 * COIN_VALUE)],
    )
  }

  #[test]
  fn list_split_ranges_are_tracked_correctly() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(1);
    let split_coinbase_output = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 0,
      ..default()
    };
    let txid = context.core.broadcast_tx(split_coinbase_output);

    context.mine_blocks(1);

    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[(50 * COIN_VALUE, 75 * COIN_VALUE)],
    );

    assert_eq!(
      context.index.list(OutPoint::new(txid, 1)).unwrap().unwrap(),
      &[(75 * COIN_VALUE, 100 * COIN_VALUE)],
    );
  }

  #[test]
  fn list_merge_ranges_are_tracked_correctly() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(2);
    let merge_coinbase_outputs = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default()), (2, 0, 0, Default::default())],
      fee: 0,
      ..default()
    };

    let txid = context.core.broadcast_tx(merge_coinbase_outputs);
    context.mine_blocks(1);

    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[
        (50 * COIN_VALUE, 100 * COIN_VALUE),
        (100 * COIN_VALUE, 150 * COIN_VALUE)
      ],
    );
  }

  #[test]
  fn list_fee_paying_transaction_range() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(1);
    let fee_paying_tx = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 10,
      ..default()
    };
    let txid = context.core.broadcast_tx(fee_paying_tx);
    let coinbase_txid = context.mine_blocks(1)[0].txdata[0].compute_txid();

    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[(50 * COIN_VALUE, 7499999995)],
    );

    assert_eq!(
      context.index.list(OutPoint::new(txid, 1)).unwrap().unwrap(),
      &[(7499999995, 9999999990)],
    );

    assert_eq!(
      context
        .index
        .list(OutPoint::new(coinbase_txid, 0))
        .unwrap()
        .unwrap(),
      &[(10000000000, 15000000000), (9999999990, 10000000000)],
    );
  }

  #[test]
  fn list_two_fee_paying_transaction_range() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(2);
    let first_fee_paying_tx = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 10,
      ..default()
    };
    let second_fee_paying_tx = TransactionTemplate {
      inputs: &[(2, 0, 0, Default::default())],
      fee: 10,
      ..default()
    };
    context.core.broadcast_tx(first_fee_paying_tx);
    context.core.broadcast_tx(second_fee_paying_tx);

    let coinbase_txid = context.mine_blocks(1)[0].txdata[0].compute_txid();

    assert_eq!(
      context
        .index
        .list(OutPoint::new(coinbase_txid, 0))
        .unwrap()
        .unwrap(),
      &[
        (15000000000, 20000000000),
        (9999999990, 10000000000),
        (14999999990, 15000000000)
      ],
    );
  }

  #[test]
  fn list_null_output() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(1);
    let no_value_output = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 50 * COIN_VALUE,
      ..default()
    };
    let txid = context.core.broadcast_tx(no_value_output);
    context.mine_blocks(1);

    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[],
    );
  }

  #[test]
  fn list_null_input() {
    let context = Context::builder().arg("--index-sats").build();

    context.mine_blocks(1);
    let no_value_output = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 50 * COIN_VALUE,
      ..default()
    };
    context.core.broadcast_tx(no_value_output);
    context.mine_blocks(1);

    let no_value_input = TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 0,
      ..default()
    };
    let txid = context.core.broadcast_tx(no_value_input);
    context.mine_blocks(1);

    assert_eq!(
      context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(),
      &[],
    );
  }

  #[test]
  fn list_spent_output() {
    let context = Context::builder().arg("--index-sats").build();
    context.mine_blocks(1);
    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 0,
      ..default()
    });
    context.mine_blocks(1);
    let txid = context.core.tx(1, 0).compute_txid();
    assert_matches!(context.index.list(OutPoint::new(txid, 0)).unwrap(), None);
  }

  #[test]
  fn list_unknown_output() {
    let context = Context::builder().arg("--index-sats").build();

    assert_eq!(
      context
        .index
        .list(
          "0000000000000000000000000000000000000000000000000000000000000000:0"
            .parse()
            .unwrap()
        )
        .unwrap(),
      None
    );
  }

  #[test]
  fn find_first_sat() {
    let context = Context::builder().arg("--index-sats").build();
    assert_eq!(
      context.index.find(Sat(0)).unwrap().unwrap(),
      SatPoint {
        outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0"
          .parse()
          .unwrap(),
        offset: 0,
      }
    )
  }

  #[test]
  fn find_second_sat() {
    let context = Context::builder().arg("--index-sats").build();
    assert_eq!(
      context.index.find(Sat(1)).unwrap().unwrap(),
      SatPoint {
        outpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0"
          .parse()
          .unwrap(),
        offset: 1,
      }
    )
  }

  #[test]
  fn find_first_sat_of_second_block() {
    let context = Context::builder().arg("--index-sats").build();
    context.mine_blocks(1);
    let tx = context.core.tx(1, 0);
    assert_eq!(
      context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(),
      SatPoint {
        outpoint: OutPoint {
          txid: tx.compute_txid(),
          vout: 0,
        },
        offset: 0,
      }
    )
  }

  #[test]
  fn find_unmined_sat() {
    let context = Context::builder().arg("--index-sats").build();
    assert_eq!(context.index.find(Sat(50 * COIN_VALUE)).unwrap(), None);
  }

  #[test]
  fn find_first_sat_spent_in_second_block() {
    let context = Context::builder().arg("--index-sats").build();
    context.mine_blocks(1);
    let spend_txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 0,
      ..default()
    });
    context.mine_blocks(1);
    assert_eq!(
      context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(),
      SatPoint {
        outpoint: OutPoint::new(spend_txid, 0),
        offset: 0,
      }
    )
  }

  #[test]
  fn inscriptions_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscriptions_without_sats_are_unbound() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, Default::default())],
        fee: 50 * 100_000_000,
        ..default()
      });

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0,
        },
        None,
      );

      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(4, 0, 0, Default::default())],
        fee: 50 * 100_000_000,
        ..default()
      });

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(5, 1, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 1,
        },
        None,
      );
    }
  }

  #[test]
  fn unaligned_inscriptions_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      let send_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())],
        ..default()
      });

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: send_txid,
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn merged_inscriptions_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(2);

      let first_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let first_inscription_id = InscriptionId {
        txid: first_txid,
        index: 0,
      };

      let second_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/png", [1; 100]).to_witness())],
        ..default()
      });
      let second_inscription_id = InscriptionId {
        txid: second_txid,
        index: 0,
      };

      context.mine_blocks(1);

      let merged_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 1, 0, Default::default()), (3, 2, 0, Default::default())],
        ..default()
      });

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        first_inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: merged_txid,
            vout: 0,
          },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second_inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: merged_txid,
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(100 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscriptions_that_are_sent_to_second_output_are_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      let send_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())],
        outputs: 2,
        ..default()
      });

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: send_txid,
            vout: 1,
          },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn one_input_fee_spent_inscriptions_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, Default::default())],
        fee: 50 * COIN_VALUE,
        ..default()
      });

      let coinbase_tx = context.mine_blocks(1)[0].txdata[0].compute_txid();

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: coinbase_tx,
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn two_input_fee_spent_inscriptions_are_tracked_correctly() {
    for context in Context::configurations() {
      context.mine_blocks(2);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, Default::default()), (3, 1, 0, Default::default())],
        fee: 50 * COIN_VALUE,
        ..default()
      });

      let coinbase_tx = context.mine_blocks(1)[0].txdata[0].compute_txid();

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: coinbase_tx,
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscription_can_be_fee_spent_in_first_transaction() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      let coinbase_tx = context.mine_blocks(1)[0].txdata[0].compute_txid();

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: coinbase_tx,
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn lost_inscriptions() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks_with_subsidy(1, 0);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint::null(),
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn multiple_inscriptions_can_be_lost() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let first_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      let first_inscription_id = InscriptionId {
        txid: first_txid,
        index: 0,
      };

      context.mine_blocks_with_subsidy(1, 0);
      context.mine_blocks(1);

      let second_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      let second_inscription_id = InscriptionId {
        txid: second_txid,
        index: 0,
      };

      context.mine_blocks_with_subsidy(1, 0);

      context.index.assert_inscription_location(
        first_inscription_id,
        SatPoint {
          outpoint: OutPoint::null(),
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second_inscription_id,
        SatPoint {
          outpoint: OutPoint::null(),
          offset: 50 * COIN_VALUE,
        },
        Some(150 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn lost_sats_are_tracked_correctly() {
    let context = Context::builder().args(["--index-sats"]).build();
    assert_eq!(context.index.statistic(Statistic::LostSats), 0);

    context.mine_blocks(1);
    assert_eq!(context.index.statistic(Statistic::LostSats), 0);

    context.mine_blocks_with_subsidy(1, 0);
    assert_eq!(
      context.index.statistic(Statistic::LostSats),
      50 * COIN_VALUE
    );

    context.mine_blocks_with_subsidy(1, 0);
    assert_eq!(
      context.index.statistic(Statistic::LostSats),
      100 * COIN_VALUE
    );

    context.mine_blocks(1);
    assert_eq!(
      context.index.statistic(Statistic::LostSats),
      100 * COIN_VALUE
    );
  }

  #[test]
  fn lost_sat_ranges_are_tracked_correctly() {
    let context = Context::builder().args(["--index-sats"]).build();

    let null_ranges = || {
      context
        .index
        .list(OutPoint::null())
        .unwrap()
        .unwrap_or_default()
    };

    assert!(null_ranges().is_empty());

    context.mine_blocks(1);

    assert!(null_ranges().is_empty());

    context.mine_blocks_with_subsidy(1, 0);

    assert_eq!(null_ranges(), [(100 * COIN_VALUE, 150 * COIN_VALUE)]);

    context.mine_blocks_with_subsidy(1, 0);

    assert_eq!(
      null_ranges(),
      [
        (100 * COIN_VALUE, 150 * COIN_VALUE),
        (150 * COIN_VALUE, 200 * COIN_VALUE)
      ]
    );

    context.mine_blocks(1);

    assert_eq!(
      null_ranges(),
      [
        (100 * COIN_VALUE, 150 * COIN_VALUE),
        (150 * COIN_VALUE, 200 * COIN_VALUE)
      ]
    );

    context.mine_blocks_with_subsidy(1, 0);

    assert_eq!(
      null_ranges(),
      [
        (100 * COIN_VALUE, 150 * COIN_VALUE),
        (150 * COIN_VALUE, 200 * COIN_VALUE),
        (250 * COIN_VALUE, 300 * COIN_VALUE)
      ]
    );
  }

  #[test]
  fn lost_inscriptions_get_lost_satpoints() {
    for context in Context::configurations() {
      context.mine_blocks_with_subsidy(1, 0);
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
        outputs: 2,
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 1, 1, Default::default()), (3, 1, 0, Default::default())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      context.mine_blocks_with_subsidy(1, 0);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint::null(),
          offset: 75 * COIN_VALUE,
        },
        Some(100 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscription_skips_zero_value_first_output_of_inscribe_transaction() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        outputs: 2,
        output_values: &[0, 50 * COIN_VALUE],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 1 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscription_can_be_lost_in_first_transaction() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };
      context.mine_blocks_with_subsidy(1, 0);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint::null(),
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn lost_rare_sats_are_tracked() {
    let context = Context::builder().arg("--index-sats").build();
    context.mine_blocks_with_subsidy(1, 0);
    context.mine_blocks_with_subsidy(1, 0);

    assert_eq!(
      context
        .index
        .rare_sat_satpoint(Sat(50 * COIN_VALUE))
        .unwrap()
        .unwrap(),
      SatPoint {
        outpoint: OutPoint::null(),
        offset: 0,
      },
    );

    assert_eq!(
      context
        .index
        .rare_sat_satpoint(Sat(100 * COIN_VALUE))
        .unwrap()
        .unwrap(),
      SatPoint {
        outpoint: OutPoint::null(),
        offset: 50 * COIN_VALUE,
      },
    );
  }

  #[test]
  fn old_schema_gives_correct_error() {
    let tempdir = {
      let context = Context::builder().build();

      let wtx = context.index.database.begin_write().unwrap();

      wtx
        .open_table(STATISTIC_TO_COUNT)
        .unwrap()
        .insert(&Statistic::Schema.key(), &0)
        .unwrap();

      wtx.commit().unwrap();

      context.tempdir
    };

    let path = tempdir.path().to_owned();

    let delimiter = if cfg!(windows) { '\\' } else { '/' };

    assert_eq!(
      Context::builder().tempdir(tempdir).try_build().err().unwrap().to_string(),
      format!("index at `{}{delimiter}regtest{delimiter}index.redb` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema 0, ord schema {SCHEMA_VERSION}", path.display()));
  }

  #[test]
  fn new_schema_gives_correct_error() {
    let tempdir = {
      let context = Context::builder().build();

      let wtx = context.index.database.begin_write().unwrap();

      wtx
        .open_table(STATISTIC_TO_COUNT)
        .unwrap()
        .insert(&Statistic::Schema.key(), &u64::MAX)
        .unwrap();

      wtx.commit().unwrap();

      context.tempdir
    };

    let path = tempdir.path().to_owned();

    let delimiter = if cfg!(windows) { '\\' } else { '/' };

    assert_eq!(
      Context::builder().tempdir(tempdir).try_build().err().unwrap().to_string(),
      format!("index at `{}{delimiter}regtest{delimiter}index.redb` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {}, ord schema {SCHEMA_VERSION}", path.display(), u64::MAX));
  }

  #[test]
  fn inscriptions_on_output() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context
          .index
          .get_inscriptions_for_output(OutPoint { txid, vout: 0 })
          .unwrap()
          .unwrap_or_default(),
        []
      );

      context.mine_blocks(1);

      assert_eq!(
        context
          .index
          .get_inscriptions_for_output(OutPoint { txid, vout: 0 })
          .unwrap()
          .unwrap_or_default(),
        [inscription_id]
      );

      let send_id = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, Default::default())],
        ..default()
      });

      context.mine_blocks(1);

      assert_eq!(
        context
          .index
          .get_inscriptions_for_output(OutPoint { txid, vout: 0 })
          .unwrap()
          .unwrap_or_default(),
        []
      );

      assert_eq!(
        context
          .index
          .get_inscriptions_for_output(OutPoint {
            txid: send_id,
            vout: 0,
          })
          .unwrap()
          .unwrap_or_default(),
        [inscription_id]
      );
    }
  }

  #[test]
  fn inscriptions_on_same_sat_after_the_first_are_not_unbound() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let first = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId {
        txid: first,
        index: 0,
      };

      assert_eq!(
        context
          .index
          .get_inscriptions_for_output(OutPoint {
            txid: first,
            vout: 0
          })
          .unwrap()
          .unwrap_or_default(),
        [inscription_id]
      );

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: first,
            vout: 0,
          },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      let second = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let inscription_id = InscriptionId {
        txid: second,
        index: 0,
      };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: second,
            vout: 0,
          },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      assert!(context
        .index
        .get_inscription_by_id(InscriptionId {
          txid: second,
          index: 0
        })
        .unwrap()
        .is_some());

      assert!(context
        .index
        .get_inscription_by_id(InscriptionId {
          txid: second,
          index: 0
        })
        .unwrap()
        .is_some());
    }
  }

  #[test]
  fn get_latest_inscriptions_with_no_more() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      let (inscriptions, more) = context.index.get_inscriptions_paginated(100, 0).unwrap();
      assert_eq!(inscriptions, &[inscription_id]);
      assert!(!more);
    }
  }

  #[test]
  fn get_latest_inscriptions_with_more() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let mut ids = Vec::new();

      for i in 0..101 {
        let txid = context.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
          ..default()
        });
        context.mine_blocks(1);
        ids.push(InscriptionId { txid, index: 0 });
      }

      ids.reverse();
      ids.pop();

      assert_eq!(ids.len(), 100);

      let (inscriptions, more) = context.index.get_inscriptions_paginated(100, 0).unwrap();
      assert_eq!(inscriptions, ids);
      assert!(more);
    }
  }

  #[test]
  fn unrecognized_even_field_inscriptions_are_cursed_and_unbound() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[2],
        b"bar",
        &[4],
        b"ord",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0,
        },
        None,
      );

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  #[test]
  fn unrecognized_even_field_inscriptions_are_unbound_after_jubilee() {
    for context in Context::configurations() {
      context.mine_blocks(109);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[2],
        b"bar",
        &[4],
        b"ord",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0,
        },
        None,
      );

      assert_eq!(context.index.inscription_number(inscription_id), 0);
    }
  }

  #[test]
  fn inscriptions_are_uncursed_after_jubilee() {
    for context in Context::configurations() {
      context.mine_blocks(108);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[1],
        b"text/plain;charset=utf-8",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness.clone())],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.core.height(), 109);

      assert_eq!(context.index.inscription_number(inscription_id), -1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.core.height(), 110);

      assert_eq!(context.index.inscription_number(inscription_id), 0);
    }
  }

  #[test]
  fn duplicate_field_inscriptions_are_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[1],
        b"text/plain;charset=utf-8",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  #[test]
  fn incomplete_field_inscriptions_are_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let witness = envelope(&[b"ord", &[1]]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  #[test]
  fn inscriptions_with_pushnum_opcodes_are_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([])
        .push_opcode(opcodes::all::OP_PUSHNUM_1)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  #[test]
  fn inscriptions_with_stutter_are_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([])
        .push_opcode(opcodes::all::OP_PUSHNUM_1)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  // https://github.com/ordinals/ord/issues/2062
  #[test]
  fn zero_value_transaction_inscription_not_cursed_but_unbound() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, Default::default())],
        fee: 50 * 100_000_000,
        ..default()
      });

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0,
        },
        None,
      );

      assert_eq!(context.index.inscription_number(inscription_id), 0);
    }
  }

  #[test]
  fn transaction_with_inscription_inside_zero_value_2nd_input_should_be_unbound_and_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      // create zero value input
      context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, Default::default())],
        fee: 50 * 100_000_000,
        ..default()
      });

      context.mine_blocks(1);

      let witness = inscription("text/plain", "hello").to_witness();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, witness.clone()), (2, 1, 0, witness.clone())],
        ..default()
      });

      let second_inscription_id = InscriptionId { txid, index: 1 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        second_inscription_id,
        SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0,
        },
        None,
      );

      assert_eq!(context.index.inscription_number(second_inscription_id), -1);
    }
  }

  #[test]
  fn multiple_inscriptions_in_same_tx_all_but_first_input_are_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);
      context.mine_blocks(1);
      context.mine_blocks(1);

      let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (1, 0, 0, witness.clone()),
          (2, 0, 0, witness.clone()),
          (3, 0, 0, witness.clone()),
        ],
        ..default()
      });

      let first = InscriptionId { txid, index: 0 };
      let second = InscriptionId { txid, index: 1 };
      let third = InscriptionId { txid, index: 2 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        first,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 50 * COIN_VALUE,
        },
        Some(100 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        third,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 100 * COIN_VALUE,
        },
        Some(150 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(first), 0);
      assert_eq!(context.index.inscription_number(second), -1);
      assert_eq!(context.index.inscription_number(third), -2);
    }
  }

  #[test]
  fn multiple_inscriptions_same_input_are_cursed_reinscriptions() {
    for context in Context::configurations() {
      context.core.mine_blocks(1);

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"foo")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"bar")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"qix")
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let first = InscriptionId { txid, index: 0 };
      let second = InscriptionId { txid, index: 1 };
      let third = InscriptionId { txid, index: 2 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        first,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        third,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(first), 0);
      assert_eq!(context.index.inscription_number(second), -1);
      assert_eq!(context.index.inscription_number(third), -2);
    }
  }

  #[test]
  fn multiple_inscriptions_different_inputs_and_same_inputs() {
    for context in Context::configurations() {
      context.core.mine_blocks(1);
      context.core.mine_blocks(1);
      context.core.mine_blocks(1);

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"foo")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"bar")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"qix")
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (1, 0, 0, witness.clone()),
          (2, 0, 0, witness.clone()),
          (3, 0, 0, witness.clone()),
        ],
        ..default()
      });

      let first = InscriptionId { txid, index: 0 }; // normal
      let second = InscriptionId { txid, index: 1 }; // cursed reinscription
      let fourth = InscriptionId { txid, index: 3 }; // cursed but bound
      let ninth = InscriptionId { txid, index: 8 }; // cursed reinscription

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        first,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        fourth,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 50 * COIN_VALUE,
        },
        Some(100 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        ninth,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 100 * COIN_VALUE,
        },
        Some(150 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(first), 0);

      assert_eq!(
        context
          .index
          .get_inscription_id_by_inscription_number(-3)
          .unwrap()
          .unwrap(),
        fourth
      );

      assert_eq!(context.index.inscription_number(fourth), -3);

      assert_eq!(context.index.inscription_number(ninth), -8);
    }
  }

  #[test]
  fn inscription_fee_distributed_evenly() {
    for context in Context::configurations() {
      context.core.mine_blocks(1);

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"foo")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"bar")
        .push_opcode(opcodes::all::OP_ENDIF)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([1])
        .push_slice(b"text/plain;charset=utf-8")
        .push_slice([])
        .push_slice(b"qix")
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        fee: 33,
        ..default()
      });

      let first = InscriptionId { txid, index: 0 };
      let second = InscriptionId { txid, index: 1 };

      context.mine_blocks(1);

      assert_eq!(
        context
          .index
          .get_inscription_entry(first)
          .unwrap()
          .unwrap()
          .fee,
        11
      );

      assert_eq!(
        context
          .index
          .get_inscription_entry(second)
          .unwrap()
          .unwrap()
          .fee,
        11
      );
    }
  }

  #[test]
  fn reinscription_on_cursed_inscription_is_not_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);
      context.mine_blocks(1);

      let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]);

      let cursed_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness.clone()), (2, 0, 0, witness.clone())],
        outputs: 2,
        ..default()
      });

      let cursed = InscriptionId {
        txid: cursed_txid,
        index: 1,
      };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        cursed,
        SatPoint {
          outpoint: OutPoint {
            txid: cursed_txid,
            vout: 1,
          },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(cursed), -1);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[],
        b"reinscription on cursed",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 1, 1, witness)],
        ..default()
      });

      let reinscription_on_cursed = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        reinscription_on_cursed,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(reinscription_on_cursed), 1);
    }
  }

  #[test]
  fn second_reinscription_on_cursed_inscription_is_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);
      context.mine_blocks(1);

      let witness = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]);

      let cursed_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness.clone()), (2, 0, 0, witness.clone())],
        outputs: 2,
        ..default()
      });

      let cursed = InscriptionId {
        txid: cursed_txid,
        index: 1,
      };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        cursed,
        SatPoint {
          outpoint: OutPoint {
            txid: cursed_txid,
            vout: 1,
          },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(cursed), -1);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[],
        b"reinscription on cursed",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 1, 1, witness)],
        ..default()
      });

      let reinscription_on_cursed = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        reinscription_on_cursed,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(reinscription_on_cursed), 1);

      let witness = envelope(&[
        b"ord",
        &[1],
        b"text/plain;charset=utf-8",
        &[],
        b"second reinscription on cursed",
      ]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(4, 1, 0, witness)],
        ..default()
      });

      let second_reinscription_on_cursed = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      context.index.assert_inscription_location(
        second_reinscription_on_cursed,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      assert_eq!(
        context
          .index
          .inscription_number(second_reinscription_on_cursed),
        -2
      );

      assert_eq!(
        vec![
          cursed,
          reinscription_on_cursed,
          second_reinscription_on_cursed
        ],
        context
          .index
          .get_inscriptions_on_output_with_satpoints(OutPoint { txid, vout: 0 })
          .unwrap()
          .unwrap_or_default()
          .iter()
          .map(|(_satpoint, inscription_id)| *inscription_id)
          .collect::<Vec<InscriptionId>>()
      )
    }
  }

  #[test]
  fn reinscriptions_on_output_correctly_ordered_and_transferred() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          1,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });

      let first = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          1,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });

      let second = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);
      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          3,
          1,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });

      let third = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      let location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      assert_eq!(
        vec![(location, first), (location, second), (location, third)],
        context
          .index
          .get_inscriptions_on_output_with_satpoints(OutPoint { txid, vout: 0 })
          .unwrap()
          .unwrap_or_default()
      )
    }
  }

  #[test]
  fn reinscriptions_are_ordered_correctly_for_many_outpoints() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let mut inscription_ids = Vec::new();
      for i in 1..=21 {
        let txid = context.core.broadcast_tx(TransactionTemplate {
          inputs: &[(
            i,
            if i == 1 { 0 } else { 1 },
            0,
            inscription("text/plain;charset=utf-8", format!("hello {i}")).to_witness(),
          )], // for the first inscription use coinbase, otherwise use the previous tx
          ..default()
        });

        inscription_ids.push(InscriptionId { txid, index: 0 });

        context.mine_blocks(1);
      }

      let final_txid = inscription_ids.last().unwrap().txid;
      let location = SatPoint {
        outpoint: OutPoint {
          txid: final_txid,
          vout: 0,
        },
        offset: 0,
      };

      let expected_result = inscription_ids
        .iter()
        .map(|id| (location, *id))
        .collect::<Vec<(SatPoint, InscriptionId)>>();

      assert_eq!(
        expected_result,
        context
          .index
          .get_inscriptions_on_output_with_satpoints(OutPoint {
            txid: final_txid,
            vout: 0
          })
          .unwrap()
          .unwrap_or_default()
      )
    }
  }

  #[test]
  fn recover_from_reorg() {
    for mut context in Context::configurations() {
      context.index.set_durability(redb::Durability::Immediate);

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          1,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });
      let first_id = InscriptionId { txid, index: 0 };
      let first_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      context.mine_blocks(6);

      context
        .index
        .assert_inscription_location(first_id, first_location, Some(50 * COIN_VALUE));

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });
      let second_id = InscriptionId { txid, index: 0 };
      let second_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      context.mine_blocks(1);

      context
        .index
        .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE));

      context.core.invalidate_tip();
      context.mine_blocks(2);

      context
        .index
        .assert_inscription_location(first_id, first_location, Some(50 * COIN_VALUE));

      assert!(!context.index.inscription_exists(second_id).unwrap());
    }
  }

  #[test]
  fn recover_from_3_block_deep_and_consecutive_reorg() {
    for mut context in Context::configurations() {
      context.index.set_durability(redb::Durability::Immediate);

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          1,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });
      let first_id = InscriptionId { txid, index: 0 };
      let first_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      context.mine_blocks(10);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });
      let second_id = InscriptionId { txid, index: 0 };
      let second_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      context.mine_blocks(1);

      context
        .index
        .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE));

      context.core.invalidate_tip();
      context.core.invalidate_tip();
      context.core.invalidate_tip();

      context.mine_blocks(4);

      assert!(!context.index.inscription_exists(second_id).unwrap());

      context.core.invalidate_tip();

      context.mine_blocks(2);

      context
        .index
        .assert_inscription_location(first_id, first_location, Some(50 * COIN_VALUE));
    }
  }

  #[test]
  fn recover_from_very_unlikely_7_block_deep_reorg() {
    for mut context in Context::configurations() {
      context.index.set_durability(redb::Durability::Immediate);

      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          1,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(11);

      let first_id = InscriptionId { txid, index: 0 };
      let first_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          0,
          0,
          inscription("text/plain;charset=utf-8", "hello").to_witness(),
        )],
        ..default()
      });

      let second_id = InscriptionId { txid, index: 0 };
      let second_location = SatPoint {
        outpoint: OutPoint { txid, vout: 0 },
        offset: 0,
      };

      context.mine_blocks(7);

      context
        .index
        .assert_inscription_location(second_id, second_location, Some(100 * COIN_VALUE));

      for _ in 0..7 {
        context.core.invalidate_tip();
      }

      context.mine_blocks(9);

      assert!(!context.index.inscription_exists(second_id).unwrap());

      context
        .index
        .assert_inscription_location(first_id, first_location, Some(50 * COIN_VALUE));
    }
  }

  #[test]
  fn inscription_without_parent_tag_has_no_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert!(context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap()
        .parents
        .is_empty());
    }
  }

  #[test]
  fn inscription_with_parent_tag_without_parent_has_no_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert!(context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap()
        .parents
        .is_empty());
    }
  }

  #[test]
  fn inscription_with_parent_tag_and_parent_has_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          1,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_two_parent_tags_and_parents_has_parent_entries() {
    for context in Context::configurations() {
      context.mine_blocks(2);

      let parent_txid_a = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let parent_txid_b = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id_a = InscriptionId {
        txid: parent_txid_a,
        index: 0,
      };
      let parent_inscription_id_b = InscriptionId {
        txid: parent_txid_b,
        index: 0,
      };

      let multi_parent_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![
          parent_inscription_id_a.value(),
          parent_inscription_id_b.value(),
        ],
        ..default()
      };
      let multi_parent_witness = multi_parent_inscription.to_witness();

      let revelation_input = (3, 1, 0, multi_parent_witness);

      let parent_b_input = (3, 2, 0, Witness::new());

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[revelation_input, parent_b_input],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id_a, parent_inscription_id_b]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_a)
          .unwrap(),
        vec![inscription_id]
      );
      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_b)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_repeated_parent_tags_and_parents_has_singular_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          1,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value(), parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_distinct_parent_tag_encodings_for_same_parent_has_singular_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let trailing_zero_inscription_id: Vec<u8> = parent_inscription_id
        .value()
        .into_iter()
        .chain(vec![0, 0, 0, 0])
        .collect();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          1,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value(), trailing_zero_inscription_id],
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_three_parent_tags_and_two_parents_has_two_parent_entries() {
    for context in Context::configurations() {
      context.mine_blocks(3);

      let parent_txid_a = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let parent_txid_b = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())],
        ..default()
      });
      let parent_txid_c = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id_a = InscriptionId {
        txid: parent_txid_a,
        index: 0,
      };
      let parent_inscription_id_b = InscriptionId {
        txid: parent_txid_b,
        index: 0,
      };
      let parent_inscription_id_c = InscriptionId {
        txid: parent_txid_c,
        index: 0,
      };

      let multi_parent_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![
          parent_inscription_id_a.value(),
          parent_inscription_id_b.value(),
          parent_inscription_id_c.value(),
        ],
        ..default()
      };
      let multi_parent_witness = multi_parent_inscription.to_witness();

      let revealing_parent_a_input = (4, 1, 0, multi_parent_witness);

      let parent_c_input = (4, 3, 0, Witness::new());

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[revealing_parent_a_input, parent_c_input],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id_a, parent_inscription_id_c]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_a)
          .unwrap(),
        vec![inscription_id]
      );
      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_b)
          .unwrap(),
        Vec::new()
      );
      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_c)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_valid_and_malformed_parent_tags_only_lists_valid_entries() {
    for context in Context::configurations() {
      context.mine_blocks(3);

      let parent_txid_a = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });
      let parent_txid_b = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/plain", "world").to_witness())],
        ..default()
      });
      let parent_txid_c = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 0, 0, inscription("text/plain", "wazzup").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id_a = InscriptionId {
        txid: parent_txid_a,
        index: 0,
      };
      let parent_inscription_id_b = InscriptionId {
        txid: parent_txid_b,
        index: 0,
      };
      let parent_inscription_id_c = InscriptionId {
        txid: parent_txid_c,
        index: 0,
      };

      let malformed_inscription_id_b = parent_inscription_id_b
        .value()
        .into_iter()
        .chain(iter::once(0))
        .collect();

      let multi_parent_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![
          parent_inscription_id_a.value(),
          malformed_inscription_id_b,
          parent_inscription_id_c.value(),
        ],
        ..default()
      };
      let multi_parent_witness = multi_parent_inscription.to_witness();

      let revealing_parent_a_input = (4, 1, 0, multi_parent_witness);
      let parent_b_input = (4, 2, 0, Witness::new());
      let parent_c_input = (4, 3, 0, Witness::new());

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[revealing_parent_a_input, parent_b_input, parent_c_input],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id_a, parent_inscription_id_c]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_a)
          .unwrap(),
        vec![inscription_id]
      );
      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_b)
          .unwrap(),
        Vec::new()
      );
      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id_c)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn parents_can_be_in_preceding_input() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(2);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (2, 1, 0, Default::default()),
          (
            3,
            0,
            0,
            Inscription {
              content_type: Some("text/plain".into()),
              body: Some("hello".into()),
              parents: vec![parent_inscription_id.value()],
              ..default()
            }
            .to_witness(),
          ),
        ],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn parents_can_be_in_following_input() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(2);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (
            3,
            0,
            0,
            Inscription {
              content_type: Some("text/plain".into()),
              body: Some("hello".into()),
              parents: vec![parent_inscription_id.value()],
              ..default()
            }
            .to_witness(),
          ),
          (2, 1, 0, Default::default()),
        ],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert_eq!(
        context.index.get_parents_by_inscription_id(inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![inscription_id]
      );
    }
  }

  #[test]
  fn inscription_with_invalid_parent_tag_and_parent_has_no_parent_entry() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(
          2,
          1,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id
              .value()
              .into_iter()
              .chain(iter::once(0))
              .collect()],
            ..default()
          }
          .to_witness(),
        )],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      assert!(context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap()
        .parents
        .is_empty());
    }
  }

  #[test]
  fn inscription_with_pointer() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        pointer: Some(100u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 100,
        },
        Some(50 * COIN_VALUE + 100),
      );
    }
  }

  #[test]
  fn inscription_with_pointer_greater_than_output_value_assigned_default() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscription_with_pointer_into_fee_ignored_and_assigned_default_location() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        pointer: Some((25 * COIN_VALUE).to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        fee: 25 * COIN_VALUE,
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscription_with_pointer_is_cursed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("pointer-child".into()),
        pointer: Some(0u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(inscription_id), -1);
    }
  }

  #[test]
  fn inscription_with_pointer_to_parent_is_cursed_reinscription() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let parent_txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "parent").to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let parent_inscription_id = InscriptionId {
        txid: parent_txid,
        index: 0,
      };

      let child_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("pointer-child".into()),
        parents: vec![parent_inscription_id.value()],
        pointer: Some(0u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, child_inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let child_inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        parent_inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        child_inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(child_inscription_id), -1);

      assert_eq!(
        context
          .index
          .get_parents_by_inscription_id(child_inscription_id),
        vec![parent_inscription_id]
      );

      assert_eq!(
        context
          .index
          .get_children_by_inscription_id(parent_inscription_id)
          .unwrap(),
        vec![child_inscription_id]
      );
    }
  }

  #[test]
  fn inscriptions_in_same_input_with_pointers_to_same_output() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let builder = script::Builder::new();

      let builder = Inscription {
        pointer: Some(100u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let builder = Inscription {
        pointer: Some(300_000u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let builder = Inscription {
        pointer: Some(1_000_000u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      context.mine_blocks(1);

      let first = InscriptionId { txid, index: 0 };
      let second = InscriptionId { txid, index: 1 };
      let third = InscriptionId { txid, index: 2 };

      context.index.assert_inscription_location(
        first,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 100,
        },
        Some(50 * COIN_VALUE + 100),
      );

      context.index.assert_inscription_location(
        second,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 300_000,
        },
        Some(50 * COIN_VALUE + 300_000),
      );

      context.index.assert_inscription_location(
        third,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 1_000_000,
        },
        Some(50 * COIN_VALUE + 1_000_000),
      );
    }
  }

  #[test]
  fn inscriptions_in_same_input_with_pointers_to_different_outputs() {
    for context in Context::configurations() {
      context.mine_blocks_with_subsidy(1, 300_000);

      let builder = script::Builder::new();

      let builder = Inscription {
        pointer: Some(100u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let builder = Inscription {
        pointer: Some(100_111u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let builder = Inscription {
        pointer: Some(299_999u64.to_le_bytes().to_vec()),
        ..default()
      }
      .append_reveal_script_to_builder(builder);

      let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        outputs: 3,
        ..default()
      });

      context.mine_blocks(1);

      let first = InscriptionId { txid, index: 0 };
      let second = InscriptionId { txid, index: 1 };
      let third = InscriptionId { txid, index: 2 };

      context.index.assert_inscription_location(
        first,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 100,
        },
        Some(50 * COIN_VALUE + 100),
      );

      context.index.assert_inscription_location(
        second,
        SatPoint {
          outpoint: OutPoint { txid, vout: 1 },
          offset: 111,
        },
        Some(50 * COIN_VALUE + 100_111),
      );

      context.index.assert_inscription_location(
        third,
        SatPoint {
          outpoint: OutPoint { txid, vout: 2 },
          offset: 99_999,
        },
        Some(50 * COIN_VALUE + 299_999),
      );
    }
  }

  #[test]
  fn inscriptions_in_different_inputs_with_pointers_to_different_outputs() {
    for context in Context::configurations() {
      context.mine_blocks(3);

      let inscription_for_second_output = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello jupiter".into()),
        pointer: Some((50 * COIN_VALUE).to_le_bytes().to_vec()),
        ..default()
      };

      let inscription_for_third_output = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello mars".into()),
        pointer: Some((100 * COIN_VALUE).to_le_bytes().to_vec()),
        ..default()
      };

      let inscription_for_first_output = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello world".into()),
        pointer: Some(0u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (1, 0, 0, inscription_for_second_output.to_witness()),
          (2, 0, 0, inscription_for_third_output.to_witness()),
          (3, 0, 0, inscription_for_first_output.to_witness()),
        ],
        outputs: 3,
        ..default()
      });

      context.mine_blocks(1);

      let inscription_for_second_output = InscriptionId { txid, index: 0 };
      let inscription_for_third_output = InscriptionId { txid, index: 1 };
      let inscription_for_first_output = InscriptionId { txid, index: 2 };

      context.index.assert_inscription_location(
        inscription_for_second_output,
        SatPoint {
          outpoint: OutPoint { txid, vout: 1 },
          offset: 0,
        },
        Some(100 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        inscription_for_third_output,
        SatPoint {
          outpoint: OutPoint { txid, vout: 2 },
          offset: 0,
        },
        Some(150 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        inscription_for_first_output,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscriptions_in_different_inputs_with_pointers_to_same_output() {
    for context in Context::configurations() {
      context.mine_blocks(3);

      let first_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello jupiter".into()),
        ..default()
      };

      let second_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello mars".into()),
        pointer: Some(1u64.to_le_bytes().to_vec()),
        ..default()
      };

      let third_inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello world".into()),
        pointer: Some(2u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (1, 0, 0, first_inscription.to_witness()),
          (2, 0, 0, second_inscription.to_witness()),
          (3, 0, 0, third_inscription.to_witness()),
        ],
        outputs: 1,
        ..default()
      });

      context.mine_blocks(1);

      let first_inscription_id = InscriptionId { txid, index: 0 };
      let second_inscription_id = InscriptionId { txid, index: 1 };
      let third_inscription_id = InscriptionId { txid, index: 2 };

      context.index.assert_inscription_location(
        first_inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        second_inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 1,
        },
        Some(50 * COIN_VALUE + 1),
      );

      context.index.assert_inscription_location(
        third_inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 2,
        },
        Some(50 * COIN_VALUE + 2),
      );
    }
  }

  #[test]
  fn inscriptions_with_pointers_to_same_sat_one_becomes_cursed_reinscriptions() {
    for context in Context::configurations() {
      context.mine_blocks(2);

      let inscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello jupiter".into()),
        ..default()
      };

      let cursed_reinscription = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello mars".into()),
        pointer: Some(0u64.to_le_bytes().to_vec()),
        ..default()
      };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (1, 0, 0, inscription.to_witness()),
          (2, 0, 0, cursed_reinscription.to_witness()),
        ],
        outputs: 2,
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };
      let cursed_reinscription_id = InscriptionId { txid, index: 1 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      context.index.assert_inscription_location(
        cursed_reinscription_id,
        SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0,
        },
        Some(50 * COIN_VALUE),
      );

      assert_eq!(context.index.inscription_number(inscription_id), 0);

      assert_eq!(
        context.index.inscription_number(cursed_reinscription_id),
        -1
      );
    }
  }

  #[test]
  fn inscribe_into_fee() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });

      let blocks = context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: blocks[0].txdata[0].compute_txid(),
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn inscribe_into_fee_with_reduced_subsidy() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription.to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });

      let blocks = context.mine_blocks_with_subsidy(1, 25 * COIN_VALUE);

      let inscription_id = InscriptionId { txid, index: 0 };

      context.index.assert_inscription_location(
        inscription_id,
        SatPoint {
          outpoint: OutPoint {
            txid: blocks[0].txdata[0].compute_txid(),
            vout: 0,
          },
          offset: 50 * COIN_VALUE,
        },
        Some(50 * COIN_VALUE),
      );
    }
  }

  #[test]
  fn pre_jubilee_first_reinscription_after_cursed_inscription_is_blessed() {
    for context in Context::configurations() {
      context.mine_blocks(1);

      // Before the jubilee, an inscription on a sat using a pushnum opcode is
      // cursed and not vindicated.

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([])
        .push_opcode(opcodes::all::OP_PUSHNUM_1)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert!(Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(!Charm::charms(entry.charms).contains(&Charm::Vindicated));

      let sat = entry.sat;

      assert_eq!(entry.inscription_number, -1);

      // Before the jubilee, reinscription on the same sat is not cursed and
      // not vindicated.

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 1, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert_eq!(entry.inscription_number, 0);

      assert!(!Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(!Charm::charms(entry.charms).contains(&Charm::Vindicated));

      assert_eq!(sat, entry.sat);

      // Before the jubilee, a third reinscription on the same sat is cursed
      // and not vindicated.

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(3, 1, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert!(Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(!Charm::charms(entry.charms).contains(&Charm::Vindicated));

      assert_eq!(entry.inscription_number, -2);

      assert_eq!(sat, entry.sat);
    }
  }

  #[test]
  fn post_jubilee_first_reinscription_after_vindicated_inscription_not_vindicated() {
    for context in Context::configurations() {
      context.mine_blocks(110);
      // After the jubilee, an inscription on a sat using a pushnum opcode is
      // vindicated and not cursed.

      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(b"ord")
        .push_slice([])
        .push_opcode(opcodes::all::OP_PUSHNUM_1)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, witness)],
        ..default()
      });

      let inscription_id = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert!(!Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(Charm::charms(entry.charms).contains(&Charm::Vindicated));

      let sat = entry.sat;

      assert_eq!(entry.inscription_number, 0);

      // After the jubilee, a reinscription on the same is not cursed and not
      // vindicated.

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(111, 1, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert!(!Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(!Charm::charms(entry.charms).contains(&Charm::Vindicated));

      assert_eq!(entry.inscription_number, 1);

      assert_eq!(sat, entry.sat);

      // After the jubilee, a third reinscription on the same is vindicated and
      // not cursed.

      let inscription = Inscription::default();

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(112, 1, 0, inscription.to_witness())],
        ..default()
      });

      context.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      let entry = context
        .index
        .get_inscription_entry(inscription_id)
        .unwrap()
        .unwrap();

      assert!(!Charm::charms(entry.charms).contains(&Charm::Cursed));

      assert!(Charm::charms(entry.charms).contains(&Charm::Vindicated));

      assert_eq!(entry.inscription_number, 2);

      assert_eq!(sat, entry.sat);
    }
  }

  #[test]
  fn is_output_spent() {
    let context = Context::builder().build();

    assert!(!context.index.is_output_spent(OutPoint::null()).unwrap());
    assert!(!context
      .index
      .is_output_spent(Chain::Mainnet.genesis_coinbase_outpoint())
      .unwrap());

    context.mine_blocks(1);

    assert!(!context
      .index
      .is_output_spent(OutPoint {
        txid: context.core.tx(1, 0).compute_txid(),
        vout: 0,
      })
      .unwrap());

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      ..default()
    });

    context.mine_blocks(1);

    assert!(context
      .index
      .is_output_spent(OutPoint {
        txid: context.core.tx(1, 0).compute_txid(),
        vout: 0,
      })
      .unwrap());
  }

  #[test]
  fn is_output_in_active_chain() {
    let context = Context::builder().build();

    assert!(context
      .index
      .is_output_in_active_chain(OutPoint::null())
      .unwrap());

    assert!(context
      .index
      .is_output_in_active_chain(Chain::Mainnet.genesis_coinbase_outpoint())
      .unwrap());

    context.mine_blocks(1);

    assert!(context
      .index
      .is_output_in_active_chain(OutPoint {
        txid: context.core.tx(1, 0).compute_txid(),
        vout: 0,
      })
      .unwrap());

    assert!(!context
      .index
      .is_output_in_active_chain(OutPoint {
        txid: context.core.tx(1, 0).compute_txid(),
        vout: 1,
      })
      .unwrap());

    assert!(!context
      .index
      .is_output_in_active_chain(OutPoint {
        txid: Txid::all_zeros(),
        vout: 0,
      })
      .unwrap());
  }

  #[test]
  fn output_addresses_are_updated() {
    let context = Context::builder()
      .arg("--index-addresses")
      .arg("--index-sats")
      .build();

    context.mine_blocks(2);

    let txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Witness::new()), (2, 0, 0, Witness::new())],
      outputs: 2,
      ..Default::default()
    });

    context.mine_blocks(1);

    let transaction = context.index.get_transaction(txid).unwrap().unwrap();

    let first_address = context
      .index
      .settings
      .chain()
      .address_from_script(&transaction.output[0].script_pubkey)
      .unwrap();

    let first_address_second_output = OutPoint {
      txid: transaction.compute_txid(),
      vout: 1,
    };

    assert_eq!(
      context.index.get_address_info(&first_address).unwrap(),
      [
        OutPoint {
          txid: transaction.compute_txid(),
          vout: 0
        },
        first_address_second_output
      ]
    );

    let txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 1, 0, Witness::new())],
      p2tr: true,
      ..Default::default()
    });

    context.mine_blocks(1);

    let transaction = context.index.get_transaction(txid).unwrap().unwrap();

    let second_address = context
      .index
      .settings
      .chain()
      .address_from_script(&transaction.output[0].script_pubkey)
      .unwrap();

    assert_eq!(
      context.index.get_address_info(&first_address).unwrap(),
      [first_address_second_output]
    );

    assert_eq!(
      context.index.get_address_info(&second_address).unwrap(),
      [OutPoint {
        txid: transaction.compute_txid(),
        vout: 0
      }]
    );
  }

  #[test]
  fn fee_spent_inscriptions_are_numbered_last_in_block() {
    for context in Context::configurations() {
      context.mine_blocks(2);

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        fee: 50 * COIN_VALUE,
        ..default()
      });

      let a = InscriptionId { txid, index: 0 };

      let txid = context.core.broadcast_tx(TransactionTemplate {
        inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      let b = InscriptionId { txid, index: 0 };

      context.mine_blocks(1);

      assert_eq!(context.index.inscription_number(a), 1);
      assert_eq!(context.index.inscription_number(b), 0);
    }
  }

  #[test]
  fn inscription_event_sender_channel() {
    let (event_sender, mut event_receiver) = tokio::sync::mpsc::channel(1024);
    let context = Context::builder().event_sender(event_sender).build();

    context.mine_blocks(1);

    let inscription = Inscription::default();
    let create_txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      fee: 0,
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    let inscription_id = InscriptionId {
      txid: create_txid,
      index: 0,
    };
    let create_event = event_receiver.blocking_recv().unwrap();
    let expected_charms = if context.index.index_sats { 513 } else { 0 };
    assert_eq!(
      create_event,
      Event::InscriptionCreated {
        inscription_id,
        location: Some(SatPoint {
          outpoint: OutPoint {
            txid: create_txid,
            vout: 0
          },
          offset: 0
        }),
        sequence_number: 0,
        block_height: 2,
        charms: expected_charms,
        parent_inscription_ids: Vec::new(),
      }
    );

    // Transfer inscription
    let transfer_txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 0,
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    let transfer_event = event_receiver.blocking_recv().unwrap();
    assert_eq!(
      transfer_event,
      Event::InscriptionTransferred {
        block_height: 3,
        inscription_id,
        new_location: SatPoint {
          outpoint: OutPoint {
            txid: transfer_txid,
            vout: 0
          },
          offset: 0
        },
        old_location: SatPoint {
          outpoint: OutPoint {
            txid: create_txid,
            vout: 0
          },
          offset: 0
        },
        sequence_number: 0,
      }
    );
  }

  #[test]
  fn rune_event_sender_channel() {
    const RUNE: u128 = 99246114928149462;

    let (event_sender, mut event_receiver) = tokio::sync::mpsc::channel(1024);
    let context = Context::builder()
      .arg("--index-runes")
      .event_sender(event_sender)
      .build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          mints: 0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    assert_eq!(
      event_receiver.blocking_recv().unwrap(),
      Event::RuneEtched {
        block_height: 8,
        txid: txid0,
        rune_id: id,
      }
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 1,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    assert_eq!(
      event_receiver.blocking_recv().unwrap(),
      Event::RuneMinted {
        block_height: 9,
        txid: txid1,
        rune_id: id,
        amount: 1000,
      }
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(9, 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          ..Default::default()
        }
        .encipher(),
      ),
      ..Default::default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: 8,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            ..default()
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..Default::default()
          }),
          timestamp: 8,
          mints: 1,
          ..Default::default()
        },
      )],
      [(
        OutPoint {
          txid: txid2,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    event_receiver.blocking_recv().unwrap();

    pretty_assert_eq!(
      event_receiver.blocking_recv().unwrap(),
      Event::RuneTransferred {
        block_height: 10,
        txid: txid2,
        rune_id: id,
        amount: 1000,
        outpoint: OutPoint {
          txid: txid2,
          vout: 0,
        },
      }
    );

    let txid3 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(10, 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 111,
            output: 0,
          }],
          ..Default::default()
        }
        .encipher(),
      ),
      op_return_index: Some(0),
      ..Default::default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: 8,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            ..default()
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..Default::default()
          }),
          timestamp: 8,
          mints: 1,
          burned: 111,
          ..Default::default()
        },
      )],
      [(
        OutPoint {
          txid: txid3,
          vout: 1,
        },
        vec![(id, 889)],
      )],
    );

    event_receiver.blocking_recv().unwrap();

    pretty_assert_eq!(
      event_receiver.blocking_recv().unwrap(),
      Event::RuneBurned {
        block_height: 11,
        txid: txid3,
        amount: 111,
        rune_id: id,
      }
    );
  }

  #[test]
  fn assert_schema_statistic_key_is_zero() {
    // other schema statistic keys may change when the schema changes, but for
    // good error messages in older versions, the schema statistic key must be
    // zero
    assert_eq!(Statistic::Schema.key(), 0);
  }
}

ord/src/index/entry.rs


use super::*;

pub(crate) trait Entry: Sized {
  type Value;

  fn load(value: Self::Value) -> Self;

  fn store(self) -> Self::Value;
}

pub(super) type HeaderValue = [u8; 80];

impl Entry for Header {
  type Value = HeaderValue;

  fn load(value: Self::Value) -> Self {
    consensus::encode::deserialize(&value).unwrap()
  }

  fn store(self) -> Self::Value {
    let mut buffer = [0; 80];
    let len = self
      .consensus_encode(&mut buffer.as_mut_slice())
      .expect("in-memory writers don't error");
    debug_assert_eq!(len, buffer.len());
    buffer
  }
}

impl Entry for Rune {
  type Value = u128;

  fn load(value: Self::Value) -> Self {
    Self(value)
  }

  fn store(self) -> Self::Value {
    self.0
  }
}

#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub struct RuneEntry {
  pub block: u64,
  pub burned: u128,
  pub divisibility: u8,
  pub etching: Txid,
  pub mints: u128,
  pub number: u64,
  pub premine: u128,
  pub spaced_rune: SpacedRune,
  pub symbol: Option<char>,
  pub terms: Option<Terms>,
  pub timestamp: u64,
  pub turbo: bool,
}

impl RuneEntry {
  pub fn mintable(&self, height: u64) -> Result<u128, MintError> {
    let Some(terms) = self.terms else {
      return Err(MintError::Unmintable);
    };

    if let Some(start) = self.start() {
      if height < start {
        return Err(MintError::Start(start));
      }
    }

    if let Some(end) = self.end() {
      if height >= end {
        return Err(MintError::End(end));
      }
    }

    let cap = terms.cap.unwrap_or_default();

    if self.mints >= cap {
      return Err(MintError::Cap(cap));
    }

    Ok(terms.amount.unwrap_or_default())
  }

  pub fn supply(&self) -> u128 {
    self.premine
      + self.mints
        * self
          .terms
          .and_then(|terms| terms.amount)
          .unwrap_or_default()
  }

  pub fn max_supply(&self) -> u128 {
    self.premine
      + self.terms.and_then(|terms| terms.cap).unwrap_or_default()
        * self
          .terms
          .and_then(|terms| terms.amount)
          .unwrap_or_default()
  }

  pub fn pile(&self, amount: u128) -> Pile {
    Pile {
      amount,
      divisibility: self.divisibility,
      symbol: self.symbol,
    }
  }

  pub fn start(&self) -> Option<u64> {
    let terms = self.terms?;

    let relative = terms
      .offset
      .0
      .map(|offset| self.block.saturating_add(offset));

    let absolute = terms.height.0;

    relative
      .zip(absolute)
      .map(|(relative, absolute)| relative.max(absolute))
      .or(relative)
      .or(absolute)
  }

  pub fn end(&self) -> Option<u64> {
    let terms = self.terms?;

    let relative = terms
      .offset
      .1
      .map(|offset| self.block.saturating_add(offset));

    let absolute = terms.height.1;

    relative
      .zip(absolute)
      .map(|(relative, absolute)| relative.min(absolute))
      .or(relative)
      .or(absolute)
  }
}

type TermsEntryValue = (
  Option<u128>,               // cap
  (Option<u64>, Option<u64>), // height
  Option<u128>,               // amount
  (Option<u64>, Option<u64>), // offset
);

pub(super) type RuneEntryValue = (
  u64,                     // block
  u128,                    // burned
  u8,                      // divisibility
  (u128, u128),            // etching
  u128,                    // mints
  u64,                     // number
  u128,                    // premine
  (u128, u32),             // spaced rune
  Option<char>,            // symbol
  Option<TermsEntryValue>, // terms
  u64,                     // timestamp
  bool,                    // turbo
);

impl Default for RuneEntry {
  fn default() -> Self {
    Self {
      block: 0,
      burned: 0,
      divisibility: 0,
      etching: Txid::all_zeros(),
      mints: 0,
      number: 0,
      premine: 0,
      spaced_rune: SpacedRune::default(),
      symbol: None,
      terms: None,
      timestamp: 0,
      turbo: false,
    }
  }
}

impl Entry for RuneEntry {
  type Value = RuneEntryValue;

  fn load(
    (
      block,
      burned,
      divisibility,
      etching,
      mints,
      number,
      premine,
      (rune, spacers),
      symbol,
      terms,
      timestamp,
      turbo,
    ): RuneEntryValue,
  ) -> Self {
    Self {
      block,
      burned,
      divisibility,
      etching: {
        let low = etching.0.to_le_bytes();
        let high = etching.1.to_le_bytes();
        Txid::from_byte_array([
          low[0], low[1], low[2], low[3], low[4], low[5], low[6], low[7], low[8], low[9], low[10],
          low[11], low[12], low[13], low[14], low[15], high[0], high[1], high[2], high[3], high[4],
          high[5], high[6], high[7], high[8], high[9], high[10], high[11], high[12], high[13],
          high[14], high[15],
        ])
      },
      mints,
      number,
      premine,
      spaced_rune: SpacedRune {
        rune: Rune(rune),
        spacers,
      },
      symbol,
      terms: terms.map(|(cap, height, amount, offset)| Terms {
        cap,
        height,
        amount,
        offset,
      }),
      timestamp,
      turbo,
    }
  }

  fn store(self) -> Self::Value {
    (
      self.block,
      self.burned,
      self.divisibility,
      {
        let bytes = self.etching.to_byte_array();
        (
          u128::from_le_bytes([
            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
          ]),
          u128::from_le_bytes([
            bytes[16], bytes[17], bytes[18], bytes[19], bytes[20], bytes[21], bytes[22], bytes[23],
            bytes[24], bytes[25], bytes[26], bytes[27], bytes[28], bytes[29], bytes[30], bytes[31],
          ]),
        )
      },
      self.mints,
      self.number,
      self.premine,
      (self.spaced_rune.rune.0, self.spaced_rune.spacers),
      self.symbol,
      self.terms.map(
        |Terms {
           cap,
           height,
           amount,
           offset,
         }| (cap, height, amount, offset),
      ),
      self.timestamp,
      self.turbo,
    )
  }
}

pub(super) type RuneIdValue = (u64, u32);

impl Entry for RuneId {
  type Value = RuneIdValue;

  fn load((block, tx): Self::Value) -> Self {
    Self { block, tx }
  }

  fn store(self) -> Self::Value {
    (self.block, self.tx)
  }
}

#[derive(Debug, Eq, PartialEq, Clone)]
pub struct InscriptionEntry {
  pub charms: u16,
  pub fee: u64,
  pub height: u32,
  pub id: InscriptionId,
  pub inscription_number: i32,
  pub parents: Vec<u32>,
  pub sat: Option<Sat>,
  pub sequence_number: u32,
  pub timestamp: u32,
}

pub(crate) type InscriptionEntryValue = (
  u16,                // charms
  u64,                // fee
  u32,                // height
  InscriptionIdValue, // inscription id
  i32,                // inscription number
  Vec<u32>,           // parents
  Option<u64>,        // sat
  u32,                // sequence number
  u32,                // timestamp
);

impl Entry for InscriptionEntry {
  type Value = InscriptionEntryValue;

  #[rustfmt::skip]
  fn load(
    (
      charms,
      fee,
      height,
      id,
      inscription_number,
      parents,
      sat,
      sequence_number,
      timestamp,
    ): InscriptionEntryValue,
  ) -> Self {
    Self {
      charms,
      fee,
      height,
      id: InscriptionId::load(id),
      inscription_number,
      parents,
      sat: sat.map(Sat),
      sequence_number,
      timestamp,
    }
  }

  fn store(self) -> Self::Value {
    (
      self.charms,
      self.fee,
      self.height,
      self.id.store(),
      self.inscription_number,
      self.parents,
      self.sat.map(Sat::n),
      self.sequence_number,
      self.timestamp,
    )
  }
}

pub(crate) type InscriptionIdValue = (u128, u128, u32);

impl Entry for InscriptionId {
  type Value = InscriptionIdValue;

  fn load(value: Self::Value) -> Self {
    let (head, tail, index) = value;
    let head_array = head.to_le_bytes();
    let tail_array = tail.to_le_bytes();
    let array = [
      head_array[0],
      head_array[1],
      head_array[2],
      head_array[3],
      head_array[4],
      head_array[5],
      head_array[6],
      head_array[7],
      head_array[8],
      head_array[9],
      head_array[10],
      head_array[11],
      head_array[12],
      head_array[13],
      head_array[14],
      head_array[15],
      tail_array[0],
      tail_array[1],
      tail_array[2],
      tail_array[3],
      tail_array[4],
      tail_array[5],
      tail_array[6],
      tail_array[7],
      tail_array[8],
      tail_array[9],
      tail_array[10],
      tail_array[11],
      tail_array[12],
      tail_array[13],
      tail_array[14],
      tail_array[15],
    ];

    Self {
      txid: Txid::from_byte_array(array),
      index,
    }
  }

  fn store(self) -> Self::Value {
    let txid_entry = self.txid.store();
    let little_end = u128::from_le_bytes(txid_entry[..16].try_into().unwrap());
    let big_end = u128::from_le_bytes(txid_entry[16..].try_into().unwrap());
    (little_end, big_end, self.index)
  }
}

pub(super) type OutPointValue = [u8; 36];

impl Entry for OutPoint {
  type Value = OutPointValue;

  fn load(value: Self::Value) -> Self {
    Decodable::consensus_decode(&mut bitcoin::io::Cursor::new(value)).unwrap()
  }

  fn store(self) -> Self::Value {
    let mut value = [0; 36];
    self.consensus_encode(&mut value.as_mut_slice()).unwrap();
    value
  }
}

pub(super) type SatPointValue = [u8; 44];

impl Entry for SatPoint {
  type Value = SatPointValue;

  fn load(value: Self::Value) -> Self {
    Decodable::consensus_decode(&mut bitcoin::io::Cursor::new(value)).unwrap()
  }

  fn store(self) -> Self::Value {
    let mut value = [0; 44];
    self.consensus_encode(&mut value.as_mut_slice()).unwrap();
    value
  }
}

pub(super) type SatRange = (u64, u64);

impl Entry for SatRange {
  type Value = [u8; 11];

  fn load([b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10]: Self::Value) -> Self {
    let raw_base = u64::from_le_bytes([b0, b1, b2, b3, b4, b5, b6, 0]);

    // 51 bit base
    let base = raw_base & ((1 << 51) - 1);

    let raw_delta = u64::from_le_bytes([b6, b7, b8, b9, b10, 0, 0, 0]);

    // 33 bit delta
    let delta = raw_delta >> 3;

    (base, base + delta)
  }

  fn store(self) -> Self::Value {
    let base = self.0;
    let delta = self.1 - self.0;
    let n = u128::from(base) | (u128::from(delta) << 51);
    n.to_le_bytes()[0..11].try_into().unwrap()
  }
}

pub(super) type TxidValue = [u8; 32];

impl Entry for Txid {
  type Value = TxidValue;

  fn load(value: Self::Value) -> Self {
    Txid::from_byte_array(value)
  }

  fn store(self) -> Self::Value {
    Txid::to_byte_array(self)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn inscription_entry() {
    let id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0"
      .parse::<InscriptionId>()
      .unwrap();

    let entry = InscriptionEntry {
      charms: 0,
      fee: 1,
      height: 2,
      id,
      inscription_number: 3,
      parents: vec![4, 5, 6],
      sat: Some(Sat(7)),
      sequence_number: 8,
      timestamp: 9,
    };

    let value = (0, 1, 2, id.store(), 3, vec![4, 5, 6], Some(7), 8, 9);

    assert_eq!(entry.clone().store(), value);
    assert_eq!(InscriptionEntry::load(value), entry);
  }

  #[test]
  fn inscription_id_entry() {
    let inscription_id = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi0"
      .parse::<InscriptionId>()
      .unwrap();

    assert_eq!(
      inscription_id.store(),
      (
        0x0123456789abcdef0123456789abcdef,
        0x0123456789abcdef0123456789abcdef,
        0
      )
    );

    assert_eq!(
      InscriptionId::load((
        0x0123456789abcdef0123456789abcdef,
        0x0123456789abcdef0123456789abcdef,
        0
      )),
      inscription_id
    );
  }

  #[test]
  fn parent_entry_index() {
    let inscription_id = "0000000000000000000000000000000000000000000000000000000000000000i1"
      .parse::<InscriptionId>()
      .unwrap();

    assert_eq!(inscription_id.store(), (0, 0, 1));

    assert_eq!(InscriptionId::load((0, 0, 1)), inscription_id);

    let inscription_id = "0000000000000000000000000000000000000000000000000000000000000000i256"
      .parse::<InscriptionId>()
      .unwrap();

    assert_eq!(inscription_id.store(), (0, 0, 256));

    assert_eq!(InscriptionId::load((0, 0, 256)), inscription_id);
  }

  #[test]
  fn rune_entry() {
    let entry = RuneEntry {
      block: 12,
      burned: 1,
      divisibility: 3,
      etching: Txid::from_byte_array([
        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
        0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
        0x1E, 0x1F,
      ]),
      terms: Some(Terms {
        cap: Some(1),
        height: (Some(2), Some(3)),
        amount: Some(4),
        offset: (Some(5), Some(6)),
      }),
      mints: 11,
      number: 6,
      premine: 12,
      spaced_rune: SpacedRune {
        rune: Rune(7),
        spacers: 8,
      },
      symbol: Some('a'),
      timestamp: 10,
      turbo: true,
    };

    let value = (
      12,
      1,
      3,
      (
        0x0F0E0D0C0B0A09080706050403020100,
        0x1F1E1D1C1B1A19181716151413121110,
      ),
      11,
      6,
      12,
      (7, 8),
      Some('a'),
      Some((Some(1), (Some(2), Some(3)), Some(4), (Some(5), Some(6)))),
      10,
      true,
    );

    assert_eq!(entry.store(), value);
    assert_eq!(RuneEntry::load(value), entry);
  }

  #[test]
  fn rune_id_entry() {
    assert_eq!(RuneId { block: 1, tx: 2 }.store(), (1, 2),);

    assert_eq!(RuneId { block: 1, tx: 2 }, RuneId::load((1, 2)),);
  }

  #[test]
  fn header() {
    let expected = [
      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
      26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48,
      49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
      72, 73, 74, 75, 76, 77, 78, 79,
    ];

    let header = Header::load(expected);
    let actual = header.store();

    assert_eq!(actual, expected);
  }

  #[test]
  fn mintable_default() {
    assert_eq!(RuneEntry::default().mintable(0), Err(MintError::Unmintable));
  }

  #[test]
  fn mintable_cap() {
    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(0),
      Ok(1000),
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          ..default()
        }),
        mints: 1,
        ..default()
      }
      .mintable(0),
      Err(MintError::Cap(1)),
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: None,
          amount: Some(1000),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(0),
      Err(MintError::Cap(0)),
    );
  }

  #[test]
  fn mintable_offset_start() {
    assert_eq!(
      RuneEntry {
        block: 1,
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          offset: (Some(1), None),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(1),
      Err(MintError::Start(2)),
    );

    assert_eq!(
      RuneEntry {
        block: 1,
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          offset: (Some(1), None),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(2),
      Ok(1000),
    );
  }

  #[test]
  fn mintable_offset_end() {
    assert_eq!(
      RuneEntry {
        block: 1,
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          offset: (None, Some(1)),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(1),
      Ok(1000),
    );

    assert_eq!(
      RuneEntry {
        block: 1,
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          offset: (None, Some(1)),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(2),
      Err(MintError::End(2)),
    );
  }

  #[test]
  fn mintable_height_start() {
    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          height: (Some(1), None),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(0),
      Err(MintError::Start(1)),
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          height: (Some(1), None),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(1),
      Ok(1000),
    );
  }

  #[test]
  fn mintable_height_end() {
    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          height: (None, Some(1)),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(0),
      Ok(1000),
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          cap: Some(1),
          amount: Some(1000),
          height: (None, Some(1)),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .mintable(1),
      Err(MintError::End(1)),
    );
  }

  #[test]
  fn mintable_multiple_terms() {
    let entry = RuneEntry {
      terms: Some(Terms {
        cap: Some(1),
        amount: Some(1000),
        height: (Some(10), Some(20)),
        offset: (Some(0), Some(10)),
      }),
      block: 10,
      mints: 0,
      ..default()
    };

    assert_eq!(entry.mintable(10), Ok(1000));

    {
      let mut entry = entry;
      entry.terms.as_mut().unwrap().cap = None;
      assert_eq!(entry.mintable(10), Err(MintError::Cap(0)));
    }

    {
      let mut entry = entry;
      entry.terms.as_mut().unwrap().height.0 = Some(11);
      assert_eq!(entry.mintable(10), Err(MintError::Start(11)));
    }

    {
      let mut entry = entry;
      entry.terms.as_mut().unwrap().height.1 = Some(10);
      assert_eq!(entry.mintable(10), Err(MintError::End(10)));
    }

    {
      let mut entry = entry;
      entry.terms.as_mut().unwrap().offset.0 = Some(1);
      assert_eq!(entry.mintable(10), Err(MintError::Start(11)));
    }

    {
      let mut entry = entry;
      entry.terms.as_mut().unwrap().offset.1 = Some(0);
      assert_eq!(entry.mintable(10), Err(MintError::End(10)));
    }
  }

  #[test]
  fn supply() {
    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        mints: 0,
        ..default()
      }
      .supply(),
      0
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        mints: 1,
        ..default()
      }
      .supply(),
      1000
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        mints: 0,
        premine: 1,
        ..default()
      }
      .supply(),
      1
    );

    assert_eq!(
      RuneEntry {
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        mints: 1,
        premine: 1,
        ..default()
      }
      .supply(),
      1001
    );
  }
}

ord/src/index/event.rs


use super::*;

#[derive(Debug, Clone, PartialEq)]
pub enum Event {
  InscriptionCreated {
    block_height: u32,
    charms: u16,
    inscription_id: InscriptionId,
    location: Option<SatPoint>,
    parent_inscription_ids: Vec<InscriptionId>,
    sequence_number: u32,
  },
  InscriptionTransferred {
    block_height: u32,
    inscription_id: InscriptionId,
    new_location: SatPoint,
    old_location: SatPoint,
    sequence_number: u32,
  },
  RuneBurned {
    amount: u128,
    block_height: u32,
    rune_id: RuneId,
    txid: Txid,
  },
  RuneEtched {
    block_height: u32,
    rune_id: RuneId,
    txid: Txid,
  },
  RuneMinted {
    amount: u128,
    block_height: u32,
    rune_id: RuneId,
    txid: Txid,
  },
  RuneTransferred {
    amount: u128,
    block_height: u32,
    outpoint: OutPoint,
    rune_id: RuneId,
    txid: Txid,
  },
}

ord/src/index/fetcher.rs


use {
  super::*,
  http_body_util::{BodyExt, Full},
  hyper::{body::Bytes, Method, Request, Uri},
  hyper_util::{
    client::legacy::{connect::HttpConnector, Client},
    rt::TokioExecutor,
  },
  serde_json::{json, Value},
};

pub(crate) struct Fetcher {
  auth: String,
  client: Client<HttpConnector, Full<Bytes>>,
  url: Uri,
}

#[derive(Deserialize, Debug)]
struct JsonResponse<T> {
  error: Option<JsonError>,
  id: usize,
  result: Option<T>,
}

#[derive(Deserialize, Debug)]
struct JsonError {
  code: i32,
  message: String,
}

impl Fetcher {
  pub(crate) fn new(settings: &Settings) -> Result<Self> {
    let client = Client::builder(TokioExecutor::new()).build_http();

    let url = if settings.bitcoin_rpc_url(None).starts_with("http://") {
      settings.bitcoin_rpc_url(None)
    } else {
      "http://".to_string() + &settings.bitcoin_rpc_url(None)
    };

    let url = Uri::try_from(&url).map_err(|e| anyhow!("Invalid rpc url {url}: {e}"))?;

    let (user, password) = settings.bitcoin_credentials()?.get_user_pass()?;
    let auth = format!("{}:{}", user.unwrap(), password.unwrap());
    let auth = format!("Basic {}", &base64_encode(auth.as_bytes()));
    Ok(Fetcher { client, url, auth })
  }

  pub(crate) async fn get_transactions(&self, txids: Vec<Txid>) -> Result<Vec<Transaction>> {
    if txids.is_empty() {
      return Ok(Vec::new());
    }

    let mut reqs = Vec::with_capacity(txids.len());
    for (i, txid) in txids.iter().enumerate() {
      let req = json!({
        "jsonrpc": "2.0",
        "id": i, // Use the index as id, so we can quickly sort the response
        "method": "getrawtransaction",
        "params": [ txid ]
      });
      reqs.push(req);
    }

    let body = Value::Array(reqs).to_string();

    let mut results: Vec<JsonResponse<String>>;
    let mut retries = 0;

    loop {
      results = match self.try_get_transactions(body.clone()).await {
        Ok(results) => results,
        Err(error) => {
          if retries >= 5 {
            return Err(anyhow!(
              "failed to fetch raw transactions after 5 retries: {}",
              error
            ));
          }

          log::info!("failed to fetch raw transactions, retrying: {}", error);

          tokio::time::sleep(Duration::from_millis(100 * u64::pow(2, retries))).await;
          retries += 1;
          continue;
        }
      };
      break;
    }

    // Return early on any error, because we need all results to proceed
    if let Some(err) = results.iter().find_map(|res| res.error.as_ref()) {
      return Err(anyhow!(
        "failed to fetch raw transaction: code {} message {}",
        err.code,
        err.message
      ));
    }

    // Results from batched JSON-RPC requests can come back in any order, so we must sort them by id
    results.sort_by(|a, b| a.id.cmp(&b.id));

    let txs = results
      .into_iter()
      .map(|res| {
        res
          .result
          .ok_or_else(|| anyhow!("Missing result for batched JSON-RPC response"))
          .and_then(|str| {
            hex::decode(str)
              .map_err(|e| anyhow!("Result for batched JSON-RPC response not valid hex: {e}"))
          })
          .and_then(|hex| {
            consensus::deserialize(&hex).map_err(|e| {
              anyhow!("Result for batched JSON-RPC response not valid bitcoin tx: {e}")
            })
          })
      })
      .collect::<Result<Vec<Transaction>>>()?;
    Ok(txs)
  }

  async fn try_get_transactions(&self, body: String) -> Result<Vec<JsonResponse<String>>> {
    let req = Request::builder()
      .method(Method::POST)
      .uri(&self.url)
      .header(hyper::header::AUTHORIZATION, &self.auth)
      .header(hyper::header::CONTENT_TYPE, "application/json")
      .body(Full::new(Bytes::from(body)))?;

    let response = self.client.request(req).await?;

    let buf = response.into_body().collect().await?.to_bytes();

    let results: Vec<JsonResponse<String>> = match serde_json::from_slice(&buf) {
      Ok(results) => results,
      Err(e) => {
        return Err(anyhow!(
          "failed to parse JSON-RPC response: {e}. response: {response}",
          e = e,
          response = String::from_utf8_lossy(&buf)
        ))
      }
    };

    Ok(results)
  }
}

ord/src/index/lot.rs


use {
  super::*,
  std::{
    cmp::{PartialEq, PartialOrd},
    ops::{Add, AddAssign, Div, Rem, Sub, SubAssign},
  },
};

#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Default, Serialize, Deserialize)]
pub struct Lot(pub u128);

impl Lot {
  #[cfg(test)]
  const MAX: Self = Self(u128::MAX);

  pub(super) fn n(self) -> u128 {
    self.0
  }

  fn checked_add(self, rhs: Self) -> Option<Self> {
    Some(Self(self.0.checked_add(rhs.0)?))
  }

  fn checked_sub(self, rhs: Self) -> Option<Self> {
    Some(Self(self.0.checked_sub(rhs.0)?))
  }
}

impl TryFrom<Lot> for usize {
  type Error = <usize as TryFrom<u128>>::Error;
  fn try_from(lot: Lot) -> Result<Self, Self::Error> {
    usize::try_from(lot.0)
  }
}

impl Add for Lot {
  type Output = Self;
  fn add(self, other: Self) -> Self::Output {
    self.checked_add(other).expect("lot overflow")
  }
}

impl AddAssign for Lot {
  fn add_assign(&mut self, other: Self) {
    *self = *self + other;
  }
}

impl Add<u128> for Lot {
  type Output = Self;
  fn add(self, other: u128) -> Self::Output {
    self + Lot(other)
  }
}

impl AddAssign<u128> for Lot {
  fn add_assign(&mut self, other: u128) {
    *self += Lot(other);
  }
}

impl Sub for Lot {
  type Output = Self;
  fn sub(self, other: Self) -> Self::Output {
    self.checked_sub(other).expect("lot underflow")
  }
}

impl SubAssign for Lot {
  fn sub_assign(&mut self, other: Self) {
    *self = *self - other;
  }
}

impl Div<u128> for Lot {
  type Output = Self;
  fn div(self, other: u128) -> Self::Output {
    Lot(self.0 / other)
  }
}

impl Rem<u128> for Lot {
  type Output = Self;
  fn rem(self, other: u128) -> Self::Output {
    Lot(self.0 % other)
  }
}

impl PartialEq<u128> for Lot {
  fn eq(&self, other: &u128) -> bool {
    self.0 == *other
  }
}

impl PartialOrd<u128> for Lot {
  fn partial_cmp(&self, other: &u128) -> Option<std::cmp::Ordering> {
    self.0.partial_cmp(other)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  #[should_panic(expected = "lot overflow")]
  fn add() {
    let _ = Lot::MAX + 1;
  }

  #[test]
  #[should_panic(expected = "lot overflow")]
  fn add_assign() {
    let mut l = Lot::MAX;
    l += Lot(1);
  }

  #[test]
  #[should_panic(expected = "lot overflow")]
  fn add_u128() {
    let _ = Lot::MAX + 1;
  }

  #[test]
  #[should_panic(expected = "lot overflow")]
  fn add_assign_u128() {
    let mut l = Lot::MAX;
    l += 1;
  }

  #[test]
  #[should_panic(expected = "lot underflow")]
  fn sub() {
    let _ = Lot(0) - Lot(1);
  }

  #[test]
  #[should_panic(expected = "lot underflow")]
  fn sub_assign() {
    let mut l = Lot(0);
    l -= Lot(1);
  }

  #[test]
  fn div() {
    assert_eq!(Lot(100) / 2, Lot(50));
  }

  #[test]
  fn rem() {
    assert_eq!(Lot(77) % 8, Lot(5));
  }

  #[test]
  fn partial_eq() {
    assert_eq!(Lot(100), 100);
  }

  #[test]
  fn partial_ord() {
    assert!(Lot(100) > 10);
  }
}

ord/src/index/reorg.rs


use {super::*, updater::BlockData};

#[derive(Debug, PartialEq)]
pub(crate) enum Error {
  Recoverable { height: u32, depth: u32 },
  Unrecoverable,
}

impl Display for Error {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Recoverable { height, depth } => {
        write!(f, "{depth} block deep reorg detected at height {height}")
      }
      Self::Unrecoverable => write!(f, "unrecoverable reorg detected"),
    }
  }
}

impl std::error::Error for Error {}

pub(crate) struct Reorg {}

impl Reorg {
  pub(crate) fn detect_reorg(block: &BlockData, height: u32, index: &Index) -> Result {
    let bitcoind_prev_blockhash = block.header.prev_blockhash;

    match index.block_hash(height.checked_sub(1))? {
      Some(index_prev_blockhash) if index_prev_blockhash == bitcoind_prev_blockhash => Ok(()),
      Some(index_prev_blockhash) if index_prev_blockhash != bitcoind_prev_blockhash => {
        let savepoint_interval = u32::try_from(index.settings.savepoint_interval()).unwrap();
        let max_savepoints = u32::try_from(index.settings.max_savepoints()).unwrap();
        let max_recoverable_reorg_depth =
          (max_savepoints - 1) * savepoint_interval + height % savepoint_interval;

        for depth in 1..max_recoverable_reorg_depth {
          let index_block_hash = index.block_hash(height.checked_sub(depth))?;
          let bitcoind_block_hash = index
            .client
            .get_block_hash(u64::from(height.saturating_sub(depth)))
            .into_option()?;

          if index_block_hash == bitcoind_block_hash {
            return Err(anyhow!(reorg::Error::Recoverable { height, depth }));
          }
        }

        Err(anyhow!(reorg::Error::Unrecoverable))
      }
      _ => Ok(()),
    }
  }

  pub(crate) fn handle_reorg(index: &Index, height: u32, depth: u32) -> Result {
    log::info!("rolling back database after reorg of depth {depth} at height {height}");

    if let redb::Durability::None = index.durability {
      panic!("set index durability to `Durability::Immediate` to test reorg handling");
    }

    let mut wtx = index.begin_write()?;

    let oldest_savepoint =
      wtx.get_persistent_savepoint(wtx.list_persistent_savepoints()?.min().unwrap())?;

    wtx.restore_savepoint(&oldest_savepoint)?;

    Index::increment_statistic(&wtx, Statistic::Commits, 1)?;
    wtx.commit()?;

    log::info!(
      "successfully rolled back database to height {}",
      index.begin_read()?.block_count()?
    );

    Ok(())
  }

  pub(crate) fn is_savepoint_required(index: &Index, height: u32) -> Result<bool> {
    if let redb::Durability::None = index.durability {
      return Ok(false);
    }

    let height = u64::from(height);

    let last_savepoint_height = index
      .begin_read()?
      .0
      .open_table(STATISTIC_TO_COUNT)?
      .get(&Statistic::LastSavepointHeight.key())?
      .map(|last_savepoint_height| last_savepoint_height.value())
      .unwrap_or(0);

    let blocks = index.client.get_blockchain_info()?.headers;

    let savepoint_interval = u64::try_from(index.settings.savepoint_interval()).unwrap();
    let max_savepoints = u64::try_from(index.settings.max_savepoints()).unwrap();

    let result = (height < savepoint_interval
      || height.saturating_sub(last_savepoint_height) >= savepoint_interval)
      && blocks.saturating_sub(height) <= savepoint_interval * max_savepoints + 1;

    log::trace!(
      "is_savepoint_required={}: height={}, last_savepoint_height={}, blocks={}",
      result,
      height,
      last_savepoint_height,
      blocks
    );

    Ok(result)
  }

  pub(crate) fn update_savepoints(index: &Index, height: u32) -> Result {
    if let redb::Durability::None = index.durability {
      return Ok(());
    }

    if Self::is_savepoint_required(index, height)? {
      let wtx = index.begin_write()?;

      let savepoints = wtx.list_persistent_savepoints()?.collect::<Vec<u64>>();

      if savepoints.len() >= index.settings.max_savepoints() {
        log::info!(
          "Cleaning up savepoints, keeping max {}",
          index.settings.max_savepoints()
        );
        wtx.delete_persistent_savepoint(savepoints.into_iter().min().unwrap())?;
      }

      Index::increment_statistic(&wtx, Statistic::Commits, 1)?;
      wtx.commit()?;

      let wtx = index.begin_write()?;

      log::info!("Creating savepoint at height {}", height);

      wtx.persistent_savepoint()?;

      wtx
        .open_table(STATISTIC_TO_COUNT)?
        .insert(&Statistic::LastSavepointHeight.key(), &height.into())?;

      Index::increment_statistic(&wtx, Statistic::Commits, 1)?;
      wtx.commit()?;
    }

    Ok(())
  }
}

ord/src/index/rtx.rs


use super::*;

pub(crate) struct Rtx(pub(crate) redb::ReadTransaction);

impl Rtx {
  pub(crate) fn block_height(&self) -> Result<Option<Height>> {
    Ok(
      self
        .0
        .open_table(HEIGHT_TO_BLOCK_HEADER)?
        .range(0..)?
        .next_back()
        .transpose()?
        .map(|(height, _header)| Height(height.value())),
    )
  }

  pub(crate) fn block_count(&self) -> Result<u32> {
    Ok(
      self
        .0
        .open_table(HEIGHT_TO_BLOCK_HEADER)?
        .range(0..)?
        .next_back()
        .transpose()?
        .map(|(height, _header)| height.value() + 1)
        .unwrap_or(0),
    )
  }

  pub(crate) fn block_hash(&self, height: Option<u32>) -> Result<Option<BlockHash>> {
    let height_to_block_header = self.0.open_table(HEIGHT_TO_BLOCK_HEADER)?;

    Ok(
      match height {
        Some(height) => height_to_block_header.get(height)?,
        None => height_to_block_header
          .range(0..)?
          .next_back()
          .transpose()?
          .map(|(_height, header)| header),
      }
      .map(|header| Header::load(*header.value()).block_hash()),
    )
  }
}

ord/src/index/testing.rs


use {super::*, bitcoin::script::PushBytes, std::ffi::OsString, tempfile::TempDir};

pub(crate) struct ContextBuilder {
  args: Vec<OsString>,
  chain: Chain,
  event_sender: Option<tokio::sync::mpsc::Sender<Event>>,
  tempdir: Option<TempDir>,
}

impl ContextBuilder {
  pub(crate) fn build(self) -> Context {
    self.try_build().unwrap()
  }

  pub(crate) fn try_build(self) -> Result<Context> {
    let core = mockcore::builder().network(self.chain.network()).build();

    let tempdir = self.tempdir.unwrap_or_else(|| TempDir::new().unwrap());
    let cookie_file = tempdir.path().join("cookie");
    fs::write(&cookie_file, "username:password").unwrap();

    let command: Vec<OsString> = vec![
      "ord".into(),
      "--bitcoin-rpc-url".into(),
      core.url().into(),
      "--datadir".into(),
      tempdir.path().into(),
      "--cookie-file".into(),
      cookie_file.into(),
      format!("--chain={}", self.chain).into(),
    ];

    let options = Options::try_parse_from(command.into_iter().chain(self.args)).unwrap();

    let index = Index::open_with_event_sender(
      &Settings::from_options(options).or_defaults().unwrap(),
      self.event_sender,
    )?;

    index.update().unwrap();

    Ok(Context {
      index,
      core,
      tempdir,
    })
  }

  pub(crate) fn arg(mut self, arg: impl Into<OsString>) -> Self {
    self.args.push(arg.into());
    self
  }

  pub(crate) fn args<T: Into<OsString>, I: IntoIterator<Item = T>>(mut self, args: I) -> Self {
    self.args.extend(args.into_iter().map(|arg| arg.into()));
    self
  }

  pub(crate) fn chain(mut self, chain: Chain) -> Self {
    self.chain = chain;
    self
  }

  pub(crate) fn tempdir(mut self, tempdir: TempDir) -> Self {
    self.tempdir = Some(tempdir);
    self
  }

  pub(crate) fn event_sender(mut self, sender: tokio::sync::mpsc::Sender<Event>) -> Self {
    self.event_sender = Some(sender);
    self
  }
}

pub(crate) struct Context {
  pub(crate) index: Index,
  pub(crate) core: mockcore::Handle,
  #[allow(unused)]
  pub(crate) tempdir: TempDir,
}

impl Context {
  pub(crate) fn builder() -> ContextBuilder {
    ContextBuilder {
      args: Vec::new(),
      chain: Chain::Regtest,
      event_sender: None,
      tempdir: None,
    }
  }

  #[track_caller]
  pub(crate) fn mine_blocks(&self, n: u64) -> Vec<Block> {
    self.mine_blocks_with_update(n, true)
  }

  #[track_caller]
  pub(crate) fn mine_blocks_with_update(&self, n: u64, update: bool) -> Vec<Block> {
    let blocks = self.core.mine_blocks(n);
    if update {
      self.index.update().unwrap();
    }
    blocks
  }

  pub(crate) fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec<Block> {
    let blocks = self.core.mine_blocks_with_subsidy(n, subsidy);
    self.index.update().unwrap();
    blocks
  }

  pub(crate) fn configurations() -> Vec<Context> {
    vec![
      Context::builder().build(),
      Context::builder().arg("--index-sats").build(),
    ]
  }

  #[track_caller]
  pub(crate) fn assert_runes(
    &self,
    mut runes: impl AsMut<[(RuneId, RuneEntry)]>,
    mut balances: impl AsMut<[(OutPoint, Vec<(RuneId, u128)>)]>,
  ) {
    let runes = runes.as_mut();
    runes.sort_by_key(|(id, _)| *id);

    let balances = balances.as_mut();
    balances.sort_by_key(|(outpoint, _)| *outpoint);

    for (_, balances) in balances.iter_mut() {
      balances.sort_by_key(|(id, _)| *id);
    }

    pretty_assert_eq!(runes, self.index.runes().unwrap());

    pretty_assert_eq!(balances, self.index.get_rune_balances().unwrap());

    let mut outstanding: HashMap<RuneId, u128> = HashMap::new();

    for (_, balances) in balances {
      for (id, balance) in balances {
        *outstanding.entry(*id).or_default() += *balance;
      }
    }

    for (id, entry) in runes {
      pretty_assert_eq!(
        outstanding.get(id).copied().unwrap_or_default(),
        entry.supply() - entry.burned
      );
    }
  }

  pub(crate) fn etch(&self, runestone: Runestone, outputs: usize) -> (Txid, RuneId) {
    let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap();

    self.mine_blocks(1);

    self.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count, 0, 0, Witness::new())],
      p2tr: true,
      ..default()
    });

    self.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into());

    let mut witness = Witness::new();

    if let Some(etching) = runestone.etching {
      let tapscript = script::Builder::new()
        .push_slice::<&PushBytes>(
          etching
            .rune
            .unwrap()
            .commitment()
            .as_slice()
            .try_into()
            .unwrap(),
        )
        .into_script();

      witness.push(tapscript);
    } else {
      witness.push(ScriptBuf::new());
    }

    witness.push([]);

    let txid = self.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count + 1, 1, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs,
      ..default()
    });

    self.mine_blocks(1);

    (
      txid,
      RuneId {
        block: u64::try_from(block_count + usize::from(Runestone::COMMIT_CONFIRMATIONS) + 1)
          .unwrap(),
        tx: 1,
      },
    )
  }
}

ord/src/index/updater.rs


use {
  self::{inscription_updater::InscriptionUpdater, rune_updater::RuneUpdater},
  super::{fetcher::Fetcher, *},
  futures::future::try_join_all,
  tokio::sync::{
    broadcast::{self, error::TryRecvError},
    mpsc::{self},
  },
};

mod inscription_updater;
mod rune_updater;

pub(crate) struct BlockData {
  pub(crate) header: Header,
  pub(crate) txdata: Vec<(Transaction, Txid)>,
}

impl From<Block> for BlockData {
  fn from(block: Block) -> Self {
    BlockData {
      header: block.header,
      txdata: block
        .txdata
        .into_iter()
        .map(|transaction| {
          let txid = transaction.compute_txid();
          (transaction, txid)
        })
        .collect(),
    }
  }
}

pub(crate) struct Updater<'index> {
  pub(super) height: u32,
  pub(super) index: &'index Index,
  pub(super) outputs_cached: u64,
  pub(super) outputs_traversed: u64,
  pub(super) sat_ranges_since_flush: u64,
}

impl Updater<'_> {
  pub(crate) fn update_index(&mut self, mut wtx: WriteTransaction) -> Result {
    let start = Instant::now();
    let starting_height = u32::try_from(self.index.client.get_block_count()?).unwrap() + 1;
    let starting_index_height = self.height;

    wtx
      .open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?
      .insert(
        &self.height,
        &SystemTime::now()
          .duration_since(SystemTime::UNIX_EPOCH)
          .map(|duration| duration.as_millis())
          .unwrap_or(0),
      )?;

    let mut progress_bar = if cfg!(test)
      || log_enabled!(log::Level::Info)
      || starting_height <= self.height
      || self.index.settings.integration_test()
    {
      None
    } else {
      let progress_bar = ProgressBar::new(starting_height.into());
      progress_bar.set_position(self.height.into());
      progress_bar.set_style(
        ProgressStyle::with_template("[indexing blocks] {wide_bar} {pos}/{len}").unwrap(),
      );
      Some(progress_bar)
    };

    let rx = Self::fetch_blocks_from(self.index, self.height)?;

    let (mut output_sender, mut txout_receiver) = Self::spawn_fetcher(self.index)?;

    let mut uncommitted = 0;
    let mut utxo_cache = HashMap::new();
    while let Ok(block) = rx.recv() {
      self.index_block(
        &mut output_sender,
        &mut txout_receiver,
        &mut wtx,
        block,
        &mut utxo_cache,
      )?;

      if let Some(progress_bar) = &mut progress_bar {
        progress_bar.inc(1);

        if progress_bar.position() > progress_bar.length().unwrap() {
          if let Ok(count) = self.index.client.get_block_count() {
            progress_bar.set_length(count + 1);
          } else {
            log::warn!("Failed to fetch latest block height");
          }
        }
      }

      uncommitted += 1;

      if uncommitted == self.index.settings.commit_interval()
        || (!self.index.settings.integration_test()
          && Reorg::is_savepoint_required(self.index, self.height)?)
      {
        self.commit(wtx, utxo_cache)?;
        utxo_cache = HashMap::new();
        uncommitted = 0;
        wtx = self.index.begin_write()?;
        let height = wtx
          .open_table(HEIGHT_TO_BLOCK_HEADER)?
          .range(0..)?
          .next_back()
          .transpose()?
          .map(|(height, _hash)| height.value() + 1)
          .unwrap_or(0);
        if height != self.height {
          // another update has run between committing and beginning the new
          // write transaction
          break;
        }
        wtx
          .open_table(WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP)?
          .insert(
            &self.height,
            &SystemTime::now()
              .duration_since(SystemTime::UNIX_EPOCH)?
              .as_millis(),
          )?;
      }

      if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
        break;
      }
    }

    if starting_index_height == 0 && self.height > 0 {
      wtx.open_table(STATISTIC_TO_COUNT)?.insert(
        Statistic::InitialSyncTime.key(),
        &u64::try_from(start.elapsed().as_micros())?,
      )?;
    }

    if uncommitted > 0 {
      self.commit(wtx, utxo_cache)?;
    }

    if let Some(progress_bar) = &mut progress_bar {
      progress_bar.finish_and_clear();
    }

    Ok(())
  }

  fn fetch_blocks_from(
    index: &Index,
    mut height: u32,
  ) -> Result<std::sync::mpsc::Receiver<BlockData>> {
    let (tx, rx) = std::sync::mpsc::sync_channel(32);

    let first_index_height = index.first_index_height;

    let height_limit = index.height_limit;

    let client = index.settings.bitcoin_rpc_client(None)?;

    thread::spawn(move || loop {
      if let Some(height_limit) = height_limit {
        if height >= height_limit {
          break;
        }
      }

      match Self::get_block_with_retries(&client, height, first_index_height) {
        Ok(Some(block)) => {
          if let Err(err) = tx.send(block.into()) {
            log::info!("Block receiver disconnected: {err}");
            break;
          }
          height += 1;
        }
        Ok(None) => break,
        Err(err) => {
          log::error!("failed to fetch block {height}: {err}");
          break;
        }
      }
    });

    Ok(rx)
  }

  fn get_block_with_retries(
    client: &Client,
    height: u32,
    first_index_height: u32,
  ) -> Result<Option<Block>> {
    let mut errors = 0;
    loop {
      match client
        .get_block_hash(height.into())
        .into_option()
        .and_then(|option| {
          option
            .map(|hash| {
              if height >= first_index_height {
                Ok(client.get_block(&hash)?)
              } else {
                Ok(Block {
                  header: client.get_block_header(&hash)?,
                  txdata: Vec::new(),
                })
              }
            })
            .transpose()
        }) {
        Err(err) => {
          if cfg!(test) {
            return Err(err);
          }

          errors += 1;
          let seconds = 1 << errors;
          log::warn!("failed to fetch block {height}, retrying in {seconds}s: {err}");

          if seconds > 120 {
            log::error!("would sleep for more than 120s, giving up");
            return Err(err);
          }

          thread::sleep(Duration::from_secs(seconds));
        }
        Ok(result) => return Ok(result),
      }
    }
  }

  fn spawn_fetcher(index: &Index) -> Result<(mpsc::Sender<OutPoint>, broadcast::Receiver<TxOut>)> {
    let fetcher = Fetcher::new(&index.settings)?;

    // A block probably has no more than 20k inputs
    const CHANNEL_BUFFER_SIZE: usize = 20_000;

    // Batch 2048 missing inputs at a time, arbitrarily chosen size
    const BATCH_SIZE: usize = 2048;

    let (outpoint_sender, mut outpoint_receiver) = mpsc::channel::<OutPoint>(CHANNEL_BUFFER_SIZE);

    let (txout_sender, txout_receiver) = broadcast::channel::<TxOut>(CHANNEL_BUFFER_SIZE);

    // Default rpcworkqueue in bitcoind is 16, meaning more than 16 concurrent requests will be rejected.
    // Since we are already requesting blocks on a separate thread, and we don't want to break if anything
    // else runs a request, we keep this to 12.
    let parallel_requests: usize = index.settings.bitcoin_rpc_limit().try_into().unwrap();

    thread::spawn(move || {
      let rt = tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap();
      rt.block_on(async move {
        loop {
          let Some(outpoint) = outpoint_receiver.recv().await else {
            log::debug!("Outpoint channel closed");
            return;
          };

          // There's no try_iter on tokio::sync::mpsc::Receiver like std::sync::mpsc::Receiver.
          // So we just loop until BATCH_SIZE doing try_recv until it returns None.
          let mut outpoints = vec![outpoint];
          for _ in 0..BATCH_SIZE - 1 {
            let Ok(outpoint) = outpoint_receiver.try_recv() else {
              break;
            };
            outpoints.push(outpoint);
          }

          // Break outputs into chunks for parallel requests
          let chunk_size = (outpoints.len() / parallel_requests) + 1;
          let mut futs = Vec::with_capacity(parallel_requests);
          for chunk in outpoints.chunks(chunk_size) {
            let txids = chunk.iter().map(|outpoint| outpoint.txid).collect();
            let fut = fetcher.get_transactions(txids);
            futs.push(fut);
          }

          let txs = match try_join_all(futs).await {
            Ok(txs) => txs,
            Err(e) => {
              log::error!("Couldn't receive txs {e}");
              return;
            }
          };

          // Send all tx outputs back in order
          for (i, tx) in txs.iter().flatten().enumerate() {
            let Ok(_) =
              txout_sender.send(tx.output[usize::try_from(outpoints[i].vout).unwrap()].clone())
            else {
              log::error!("Value channel closed unexpectedly");
              return;
            };
          }
        }
      })
    });

    Ok((outpoint_sender, txout_receiver))
  }

  fn index_block(
    &mut self,
    output_sender: &mut mpsc::Sender<OutPoint>,
    txout_receiver: &mut broadcast::Receiver<TxOut>,
    wtx: &mut WriteTransaction,
    block: BlockData,
    utxo_cache: &mut HashMap<OutPoint, UtxoEntryBuf>,
  ) -> Result<()> {
    Reorg::detect_reorg(&block, self.height, self.index)?;

    let start = Instant::now();
    let mut sat_ranges_written = 0;
    let mut outputs_in_block = 0;

    log::info!(
      "Block {} at {} with {} transactions…",
      self.height,
      timestamp(block.header.time.into()),
      block.txdata.len()
    );

    let mut height_to_block_header = wtx.open_table(HEIGHT_TO_BLOCK_HEADER)?;
    let mut inscription_id_to_sequence_number =
      wtx.open_table(INSCRIPTION_ID_TO_SEQUENCE_NUMBER)?;
    let mut statistic_to_count = wtx.open_table(STATISTIC_TO_COUNT)?;

    if self.index.index_inscriptions || self.index.index_addresses || self.index.index_sats {
      self.index_utxo_entries(
        &block,
        txout_receiver,
        output_sender,
        utxo_cache,
        wtx,
        &mut inscription_id_to_sequence_number,
        &mut statistic_to_count,
        &mut sat_ranges_written,
        &mut outputs_in_block,
      )?;
    }

    if self.index.index_runes && self.height >= self.index.settings.first_rune_height() {
      let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?;
      let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;
      let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?;
      let mut sequence_number_to_rune_id = wtx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?;
      let mut transaction_id_to_rune = wtx.open_table(TRANSACTION_ID_TO_RUNE)?;

      let runes = statistic_to_count
        .get(&Statistic::Runes.into())?
        .map(|x| x.value())
        .unwrap_or(0);

      let mut rune_updater = RuneUpdater {
        event_sender: self.index.event_sender.as_ref(),
        block_time: block.header.time,
        burned: HashMap::new(),
        client: &self.index.client,
        height: self.height,
        id_to_entry: &mut rune_id_to_rune_entry,
        inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number,
        minimum: Rune::minimum_at_height(
          self.index.settings.chain().network(),
          Height(self.height),
        ),
        outpoint_to_balances: &mut outpoint_to_rune_balances,
        rune_to_id: &mut rune_to_rune_id,
        runes,
        sequence_number_to_rune_id: &mut sequence_number_to_rune_id,
        statistic_to_count: &mut statistic_to_count,
        transaction_id_to_rune: &mut transaction_id_to_rune,
      };

      for (i, (tx, txid)) in block.txdata.iter().enumerate() {
        rune_updater.index_runes(u32::try_from(i).unwrap(), tx, *txid)?;
      }

      rune_updater.update()?;
    }

    height_to_block_header.insert(&self.height, &block.header.store())?;

    self.height += 1;
    self.outputs_traversed += outputs_in_block;

    log::info!(
      "Wrote {sat_ranges_written} sat ranges from {outputs_in_block} outputs in {} ms",
      (Instant::now() - start).as_millis(),
    );

    Ok(())
  }

  fn index_utxo_entries<'wtx>(
    &mut self,
    block: &BlockData,
    txout_receiver: &mut broadcast::Receiver<TxOut>,
    output_sender: &mut mpsc::Sender<OutPoint>,
    utxo_cache: &mut HashMap<OutPoint, UtxoEntryBuf>,
    wtx: &'wtx WriteTransaction,
    inscription_id_to_sequence_number: &mut Table<'wtx, (u128, u128, u32), u32>,
    statistic_to_count: &mut Table<'wtx, u64, u64>,
    sat_ranges_written: &mut u64,
    outputs_in_block: &mut u64,
  ) -> Result<(), Error> {
    let mut height_to_last_sequence_number = wtx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?;
    let mut home_inscriptions = wtx.open_table(HOME_INSCRIPTIONS)?;
    let mut inscription_number_to_sequence_number =
      wtx.open_table(INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER)?;
    let mut outpoint_to_utxo_entry = wtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?;
    let mut sat_to_satpoint = wtx.open_table(SAT_TO_SATPOINT)?;
    let mut sat_to_sequence_number = wtx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?;
    let mut script_pubkey_to_outpoint = wtx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?;
    let mut sequence_number_to_children = wtx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?;
    let mut sequence_number_to_inscription_entry =
      wtx.open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY)?;
    let mut transaction_id_to_transaction = wtx.open_table(TRANSACTION_ID_TO_TRANSACTION)?;

    let index_inscriptions = self.height >= self.index.settings.first_inscription_height()
      && self.index.index_inscriptions;

    // If the receiver still has inputs something went wrong in the last
    // block and we shouldn't recover from this and commit the last block
    if index_inscriptions {
      assert!(
        matches!(txout_receiver.try_recv(), Err(TryRecvError::Empty)),
        "Previous block did not consume all inputs"
      );
    }

    if !self.index.have_full_utxo_index() {
      // Send all missing input outpoints to be fetched
      let txids = block
        .txdata
        .iter()
        .map(|(_, txid)| txid)
        .collect::<HashSet<_>>();

      for (tx, _) in &block.txdata {
        for input in &tx.input {
          let prev_output = input.previous_output;
          // We don't need coinbase inputs
          if prev_output.is_null() {
            continue;
          }
          // We don't need inputs from txs earlier in the block, since
          // they'll be added to cache when the tx is indexed
          if txids.contains(&prev_output.txid) {
            continue;
          }
          // We don't need inputs we already have in our cache from earlier blocks
          if utxo_cache.contains_key(&prev_output) {
            continue;
          }
          // We don't need inputs we already have in our database
          if outpoint_to_utxo_entry.get(&prev_output.store())?.is_some() {
            continue;
          }
          // Send this outpoint to background thread to be fetched
          output_sender.blocking_send(prev_output)?;
        }
      }
    }

    let mut lost_sats = statistic_to_count
      .get(&Statistic::LostSats.key())?
      .map(|lost_sats| lost_sats.value())
      .unwrap_or(0);

    let cursed_inscription_count = statistic_to_count
      .get(&Statistic::CursedInscriptions.key())?
      .map(|count| count.value())
      .unwrap_or(0);

    let blessed_inscription_count = statistic_to_count
      .get(&Statistic::BlessedInscriptions.key())?
      .map(|count| count.value())
      .unwrap_or(0);

    let unbound_inscriptions = statistic_to_count
      .get(&Statistic::UnboundInscriptions.key())?
      .map(|unbound_inscriptions| unbound_inscriptions.value())
      .unwrap_or(0);

    let next_sequence_number = sequence_number_to_inscription_entry
      .iter()?
      .next_back()
      .transpose()?
      .map(|(number, _id)| number.value() + 1)
      .unwrap_or(0);

    let home_inscription_count = home_inscriptions.len()?;

    let mut inscription_updater = InscriptionUpdater {
      blessed_inscription_count,
      cursed_inscription_count,
      flotsam: Vec::new(),
      height: self.height,
      home_inscription_count,
      home_inscriptions: &mut home_inscriptions,
      id_to_sequence_number: inscription_id_to_sequence_number,
      inscription_number_to_sequence_number: &mut inscription_number_to_sequence_number,
      lost_sats,
      next_sequence_number,
      reward: Height(self.height).subsidy(),
      sat_to_sequence_number: &mut sat_to_sequence_number,
      sequence_number_to_children: &mut sequence_number_to_children,
      sequence_number_to_entry: &mut sequence_number_to_inscription_entry,
      timestamp: block.header.time,
      transaction_buffer: Vec::new(),
      transaction_id_to_transaction: &mut transaction_id_to_transaction,
      unbound_inscriptions,
    };

    let mut coinbase_inputs = Vec::new();
    let mut lost_sat_ranges = Vec::new();

    if self.index.index_sats {
      let h = Height(self.height);
      if h.subsidy() > 0 {
        let start = h.starting_sat();
        coinbase_inputs.extend(SatRange::store((start.n(), (start + h.subsidy()).n())));
        self.sat_ranges_since_flush += 1;
      }
    }

    for (tx_offset, (tx, txid)) in block
      .txdata
      .iter()
      .enumerate()
      .skip(1)
      .chain(block.txdata.iter().enumerate().take(1))
    {
      log::trace!("Indexing transaction {tx_offset}…");

      let input_utxo_entries = if tx_offset == 0 {
        Vec::new()
      } else {
        tx.input
          .iter()
          .map(|input| {
            let outpoint = input.previous_output.store();

            let entry = if let Some(entry) = utxo_cache.remove(&OutPoint::load(outpoint)) {
              self.outputs_cached += 1;
              entry
            } else if let Some(entry) = outpoint_to_utxo_entry.remove(&outpoint)? {
              if self.index.index_addresses {
                let script_pubkey = entry.value().parse(self.index).script_pubkey();
                if !script_pubkey_to_outpoint.remove(script_pubkey, outpoint)? {
                  panic!("script pubkey entry ({script_pubkey:?}, {outpoint:?}) not found");
                }
              }

              entry.value().to_buf()
            } else {
              assert!(!self.index.have_full_utxo_index());
              let txout = txout_receiver.blocking_recv().map_err(|err| {
                anyhow!(
                  "failed to get transaction for {}: {err}",
                  input.previous_output
                )
              })?;

              let mut entry = UtxoEntryBuf::new();
              entry.push_value(txout.value.to_sat(), self.index);
              if self.index.index_addresses {
                entry.push_script_pubkey(txout.script_pubkey.as_bytes(), self.index);
              }

              entry
            };

            Ok(entry)
          })
          .collect::<Result<Vec<UtxoEntryBuf>>>()?
      };

      let input_utxo_entries = input_utxo_entries
        .iter()
        .map(|entry| entry.parse(self.index))
        .collect::<Vec<ParsedUtxoEntry>>();

      let mut output_utxo_entries = tx
        .output
        .iter()
        .map(|_| UtxoEntryBuf::new())
        .collect::<Vec<UtxoEntryBuf>>();

      let input_sat_ranges;
      if self.index.index_sats {
        let leftover_sat_ranges;

        if tx_offset == 0 {
          input_sat_ranges = Some(vec![coinbase_inputs.as_slice()]);
          leftover_sat_ranges = &mut lost_sat_ranges;
        } else {
          input_sat_ranges = Some(
            input_utxo_entries
              .iter()
              .map(|entry| entry.sat_ranges())
              .collect(),
          );
          leftover_sat_ranges = &mut coinbase_inputs;
        }

        self.index_transaction_sats(
          tx,
          *txid,
          &mut sat_to_satpoint,
          &mut output_utxo_entries,
          input_sat_ranges.as_ref().unwrap(),
          leftover_sat_ranges,
          sat_ranges_written,
          outputs_in_block,
        )?;
      } else {
        input_sat_ranges = None;

        for (vout, txout) in tx.output.iter().enumerate() {
          output_utxo_entries[vout].push_value(txout.value.to_sat(), self.index);
        }
      }

      if self.index.index_addresses {
        self.index_transaction_output_script_pubkeys(tx, &mut output_utxo_entries);
      }

      if index_inscriptions {
        inscription_updater.index_inscriptions(
          tx,
          *txid,
          &input_utxo_entries,
          &mut output_utxo_entries,
          utxo_cache,
          self.index,
          input_sat_ranges.as_ref(),
        )?;
      }

      for (vout, output_utxo_entry) in output_utxo_entries.into_iter().enumerate() {
        let vout = u32::try_from(vout).unwrap();
        utxo_cache.insert(OutPoint { txid: *txid, vout }, output_utxo_entry);
      }
    }

    if index_inscriptions {
      height_to_last_sequence_number
        .insert(&self.height, inscription_updater.next_sequence_number)?;
    }

    if !lost_sat_ranges.is_empty() {
      // Note that the lost-sats outpoint is special, because (unlike real
      // outputs) it gets written to more than once.  commit() will merge
      // our new entry with any existing one.
      let utxo_entry = utxo_cache
        .entry(OutPoint::null())
        .or_insert(UtxoEntryBuf::empty(self.index));

      for chunk in lost_sat_ranges.chunks_exact(11) {
        let (start, end) = SatRange::load(chunk.try_into().unwrap());
        if !Sat(start).common() {
          sat_to_satpoint.insert(
            &start,
            &SatPoint {
              outpoint: OutPoint::null(),
              offset: lost_sats,
            }
            .store(),
          )?;
        }

        lost_sats += end - start;
      }

      let mut new_utxo_entry = UtxoEntryBuf::new();
      new_utxo_entry.push_sat_ranges(&lost_sat_ranges, self.index);
      if self.index.index_addresses {
        new_utxo_entry.push_script_pubkey(&[], self.index);
      }

      *utxo_entry = UtxoEntryBuf::merged(utxo_entry, &new_utxo_entry, self.index);
    }

    statistic_to_count.insert(
      &Statistic::LostSats.key(),
      &if self.index.index_sats {
        lost_sats
      } else {
        inscription_updater.lost_sats
      },
    )?;

    statistic_to_count.insert(
      &Statistic::CursedInscriptions.key(),
      &inscription_updater.cursed_inscription_count,
    )?;

    statistic_to_count.insert(
      &Statistic::BlessedInscriptions.key(),
      &inscription_updater.blessed_inscription_count,
    )?;

    statistic_to_count.insert(
      &Statistic::UnboundInscriptions.key(),
      &inscription_updater.unbound_inscriptions,
    )?;

    Ok(())
  }

  fn index_transaction_output_script_pubkeys(
    &mut self,
    tx: &Transaction,
    output_utxo_entries: &mut [UtxoEntryBuf],
  ) {
    for (vout, txout) in tx.output.iter().enumerate() {
      output_utxo_entries[vout].push_script_pubkey(txout.script_pubkey.as_bytes(), self.index);
    }
  }

  fn index_transaction_sats(
    &mut self,
    tx: &Transaction,
    txid: Txid,
    sat_to_satpoint: &mut Table<u64, &SatPointValue>,
    output_utxo_entries: &mut [UtxoEntryBuf],
    input_sat_ranges: &[&[u8]],
    leftover_sat_ranges: &mut Vec<u8>,
    sat_ranges_written: &mut u64,
    outputs_traversed: &mut u64,
  ) -> Result {
    let mut pending_input_sat_range = None;
    let mut input_sat_ranges_iter = input_sat_ranges
      .iter()
      .flat_map(|slice| slice.chunks_exact(11));

    // Preallocate our temporary array, sized to hold the combined
    // sat ranges from our inputs.  We'll never need more than that
    // for a single output, even if we end up splitting some ranges.
    let mut sats = Vec::with_capacity(
      input_sat_ranges
        .iter()
        .map(|slice| slice.len())
        .sum::<usize>(),
    );

    for (vout, output) in tx.output.iter().enumerate() {
      let outpoint = OutPoint {
        vout: vout.try_into().unwrap(),
        txid,
      };

      let mut remaining = output.value.to_sat();
      while remaining > 0 {
        let range = pending_input_sat_range.take().unwrap_or_else(|| {
          SatRange::load(
            input_sat_ranges_iter
              .next()
              .expect("insufficient inputs for transaction outputs")
              .try_into()
              .unwrap(),
          )
        });

        if !Sat(range.0).common() {
          sat_to_satpoint.insert(
            &range.0,
            &SatPoint {
              outpoint,
              offset: output.value.to_sat() - remaining,
            }
            .store(),
          )?;
        }

        let count = range.1 - range.0;

        let assigned = if count > remaining {
          self.sat_ranges_since_flush += 1;
          let middle = range.0 + remaining;
          pending_input_sat_range = Some((middle, range.1));
          (range.0, middle)
        } else {
          range
        };

        sats.extend_from_slice(&assigned.store());

        remaining -= assigned.1 - assigned.0;

        *sat_ranges_written += 1;
      }

      *outputs_traversed += 1;

      output_utxo_entries[vout].push_sat_ranges(&sats, self.index);
      sats.clear();
    }

    if let Some(range) = pending_input_sat_range {
      leftover_sat_ranges.extend(&range.store());
    }
    leftover_sat_ranges.extend(input_sat_ranges_iter.flatten());

    Ok(())
  }

  fn commit(
    &mut self,
    wtx: WriteTransaction,
    utxo_cache: HashMap<OutPoint, UtxoEntryBuf>,
  ) -> Result {
    log::info!(
      "Committing at block height {}, {} outputs traversed, {} in map, {} cached",
      self.height,
      self.outputs_traversed,
      utxo_cache.len(),
      self.outputs_cached
    );

    {
      let mut outpoint_to_utxo_entry = wtx.open_table(OUTPOINT_TO_UTXO_ENTRY)?;
      let mut script_pubkey_to_outpoint = wtx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?;
      let mut sequence_number_to_satpoint = wtx.open_table(SEQUENCE_NUMBER_TO_SATPOINT)?;

      for (outpoint, mut utxo_entry) in utxo_cache {
        if Index::is_special_outpoint(outpoint) {
          if let Some(old_entry) = outpoint_to_utxo_entry.get(&outpoint.store())? {
            utxo_entry = UtxoEntryBuf::merged(old_entry.value(), &utxo_entry, self.index);
          }
        }

        outpoint_to_utxo_entry.insert(&outpoint.store(), utxo_entry.as_ref())?;

        let utxo_entry = utxo_entry.parse(self.index);
        if self.index.index_addresses {
          let script_pubkey = utxo_entry.script_pubkey();
          script_pubkey_to_outpoint.insert(script_pubkey, &outpoint.store())?;
        }

        if self.index.index_inscriptions {
          for (sequence_number, offset) in utxo_entry.parse_inscriptions() {
            let satpoint = SatPoint { outpoint, offset };
            sequence_number_to_satpoint.insert(sequence_number, &satpoint.store())?;
          }
        }
      }
    }

    Index::increment_statistic(&wtx, Statistic::OutputsTraversed, self.outputs_traversed)?;
    self.outputs_traversed = 0;
    Index::increment_statistic(&wtx, Statistic::SatRanges, self.sat_ranges_since_flush)?;
    self.sat_ranges_since_flush = 0;
    Index::increment_statistic(&wtx, Statistic::Commits, 1)?;
    wtx.commit()?;

    // Commit twice since due to a bug redb will only reuse pages freed in the
    // transaction before last.
    self.index.begin_write()?.commit()?;

    Reorg::update_savepoints(self.index, self.height)?;

    Ok(())
  }
}

ord/src/index/updater/inscription_updater.rs


use super::*;

#[derive(Debug, PartialEq, Copy, Clone)]
enum Curse {
  DuplicateField,
  IncompleteField,
  NotAtOffsetZero,
  NotInFirstInput,
  Pointer,
  Pushnum,
  Reinscription,
  Stutter,
  UnrecognizedEvenField,
}

#[derive(Debug, Clone)]
pub(super) struct Flotsam {
  inscription_id: InscriptionId,
  offset: u64,
  origin: Origin,
}

#[derive(Debug, Clone)]
enum Origin {
  New {
    cursed: bool,
    fee: u64,
    hidden: bool,
    parents: Vec<InscriptionId>,
    reinscription: bool,
    unbound: bool,
    vindicated: bool,
  },
  Old {
    sequence_number: u32,
    old_satpoint: SatPoint,
  },
}

pub(super) struct InscriptionUpdater<'a, 'tx> {
  pub(super) blessed_inscription_count: u64,
  pub(super) cursed_inscription_count: u64,
  pub(super) flotsam: Vec<Flotsam>,
  pub(super) height: u32,
  pub(super) home_inscription_count: u64,
  pub(super) home_inscriptions: &'a mut Table<'tx, u32, InscriptionIdValue>,
  pub(super) id_to_sequence_number: &'a mut Table<'tx, InscriptionIdValue, u32>,
  pub(super) inscription_number_to_sequence_number: &'a mut Table<'tx, i32, u32>,
  pub(super) lost_sats: u64,
  pub(super) next_sequence_number: u32,
  pub(super) reward: u64,
  pub(super) transaction_buffer: Vec<u8>,
  pub(super) transaction_id_to_transaction: &'a mut Table<'tx, &'static TxidValue, &'static [u8]>,
  pub(super) sat_to_sequence_number: &'a mut MultimapTable<'tx, u64, u32>,
  pub(super) sequence_number_to_children: &'a mut MultimapTable<'tx, u32, u32>,
  pub(super) sequence_number_to_entry: &'a mut Table<'tx, u32, InscriptionEntryValue>,
  pub(super) timestamp: u32,
  pub(super) unbound_inscriptions: u64,
}

impl InscriptionUpdater<'_, '_> {
  pub(super) fn index_inscriptions(
    &mut self,
    tx: &Transaction,
    txid: Txid,
    input_utxo_entries: &[ParsedUtxoEntry],
    output_utxo_entries: &mut [UtxoEntryBuf],
    utxo_cache: &mut HashMap<OutPoint, UtxoEntryBuf>,
    index: &Index,
    input_sat_ranges: Option<&Vec<&[u8]>>,
  ) -> Result {
    let mut floating_inscriptions = Vec::new();
    let mut id_counter = 0;
    let mut inscribed_offsets = BTreeMap::new();
    let jubilant = self.height >= index.settings.chain().jubilee_height();
    let mut total_input_value = 0;
    let total_output_value = tx
      .output
      .iter()
      .map(|txout| txout.value.to_sat())
      .sum::<u64>();

    let envelopes = ParsedEnvelope::from_transaction(tx);
    let has_new_inscriptions = !envelopes.is_empty();
    let mut envelopes = envelopes.into_iter().peekable();

    for (input_index, txin) in tx.input.iter().enumerate() {
      // skip subsidy since no inscriptions possible
      if txin.previous_output.is_null() {
        total_input_value += Height(self.height).subsidy();
        continue;
      }

      let mut transferred_inscriptions = input_utxo_entries[input_index].parse_inscriptions();

      transferred_inscriptions.sort_by_key(|(sequence_number, _)| *sequence_number);

      for (sequence_number, old_satpoint_offset) in transferred_inscriptions {
        let old_satpoint = SatPoint {
          outpoint: txin.previous_output,
          offset: old_satpoint_offset,
        };

        let inscription_id = InscriptionEntry::load(
          self
            .sequence_number_to_entry
            .get(sequence_number)?
            .unwrap()
            .value(),
        )
        .id;

        let offset = total_input_value + old_satpoint_offset;
        floating_inscriptions.push(Flotsam {
          offset,
          inscription_id,
          origin: Origin::Old {
            sequence_number,
            old_satpoint,
          },
        });

        inscribed_offsets
          .entry(offset)
          .or_insert((inscription_id, 0))
          .1 += 1;
      }

      let offset = total_input_value;

      let input_value = input_utxo_entries[input_index].total_value();
      total_input_value += input_value;

      // go through all inscriptions in this input
      while let Some(inscription) = envelopes.peek() {
        if inscription.input != u32::try_from(input_index).unwrap() {
          break;
        }

        let inscription_id = InscriptionId {
          txid,
          index: id_counter,
        };

        let curse = if inscription.payload.unrecognized_even_field {
          Some(Curse::UnrecognizedEvenField)
        } else if inscription.payload.duplicate_field {
          Some(Curse::DuplicateField)
        } else if inscription.payload.incomplete_field {
          Some(Curse::IncompleteField)
        } else if inscription.input != 0 {
          Some(Curse::NotInFirstInput)
        } else if inscription.offset != 0 {
          Some(Curse::NotAtOffsetZero)
        } else if inscription.payload.pointer.is_some() {
          Some(Curse::Pointer)
        } else if inscription.pushnum {
          Some(Curse::Pushnum)
        } else if inscription.stutter {
          Some(Curse::Stutter)
        } else if let Some((id, count)) = inscribed_offsets.get(&offset) {
          if *count > 1 {
            Some(Curse::Reinscription)
          } else {
            let initial_inscription_sequence_number =
              self.id_to_sequence_number.get(id.store())?.unwrap().value();

            let entry = InscriptionEntry::load(
              self
                .sequence_number_to_entry
                .get(initial_inscription_sequence_number)?
                .unwrap()
                .value(),
            );

            let initial_inscription_was_cursed_or_vindicated =
              entry.inscription_number < 0 || Charm::Vindicated.is_set(entry.charms);

            if initial_inscription_was_cursed_or_vindicated {
              None
            } else {
              Some(Curse::Reinscription)
            }
          }
        } else {
          None
        };

        let offset = inscription
          .payload
          .pointer()
          .filter(|&pointer| pointer < total_output_value)
          .unwrap_or(offset);

        floating_inscriptions.push(Flotsam {
          inscription_id,
          offset,
          origin: Origin::New {
            cursed: curse.is_some() && !jubilant,
            fee: 0,
            hidden: inscription.payload.hidden(),
            parents: inscription.payload.parents(),
            reinscription: inscribed_offsets.contains_key(&offset),
            unbound: input_value == 0
              || curse == Some(Curse::UnrecognizedEvenField)
              || inscription.payload.unrecognized_even_field,
            vindicated: curse.is_some() && jubilant,
          },
        });

        inscribed_offsets
          .entry(offset)
          .or_insert((inscription_id, 0))
          .1 += 1;

        envelopes.next();
        id_counter += 1;
      }
    }

    if index.index_transactions && has_new_inscriptions {
      tx.consensus_encode(&mut self.transaction_buffer)
        .expect("in-memory writers don't error");

      self
        .transaction_id_to_transaction
        .insert(&txid.store(), self.transaction_buffer.as_slice())?;

      self.transaction_buffer.clear();
    }

    let potential_parents = floating_inscriptions
      .iter()
      .map(|flotsam| flotsam.inscription_id)
      .collect::<HashSet<InscriptionId>>();

    for flotsam in &mut floating_inscriptions {
      if let Flotsam {
        origin: Origin::New {
          parents: purported_parents,
          ..
        },
        ..
      } = flotsam
      {
        let mut seen = HashSet::new();
        purported_parents
          .retain(|parent| seen.insert(*parent) && potential_parents.contains(parent));
      }
    }

    // still have to normalize over inscription size
    for flotsam in &mut floating_inscriptions {
      if let Flotsam {
        origin: Origin::New { fee, .. },
        ..
      } = flotsam
      {
        *fee = (total_input_value - total_output_value) / u64::from(id_counter);
      }
    }

    let is_coinbase = tx
      .input
      .first()
      .map(|tx_in| tx_in.previous_output.is_null())
      .unwrap_or_default();

    if is_coinbase {
      floating_inscriptions.append(&mut self.flotsam);
    }

    floating_inscriptions.sort_by_key(|flotsam| flotsam.offset);
    let mut inscriptions = floating_inscriptions.into_iter().peekable();

    let mut new_locations = Vec::new();
    let mut output_value = 0;
    for (vout, txout) in tx.output.iter().enumerate() {
      let end = output_value + txout.value.to_sat();

      while let Some(flotsam) = inscriptions.peek() {
        if flotsam.offset >= end {
          break;
        }

        let new_satpoint = SatPoint {
          outpoint: OutPoint {
            txid,
            vout: vout.try_into().unwrap(),
          },
          offset: flotsam.offset - output_value,
        };

        new_locations.push((
          new_satpoint,
          inscriptions.next().unwrap(),
          txout.script_pubkey.is_op_return(),
        ));
      }

      output_value = end;
    }

    for (new_satpoint, flotsam, op_return) in new_locations.into_iter() {
      let output_utxo_entry =
        &mut output_utxo_entries[usize::try_from(new_satpoint.outpoint.vout).unwrap()];

      self.update_inscription_location(
        input_sat_ranges,
        flotsam,
        new_satpoint,
        op_return,
        Some(output_utxo_entry),
        utxo_cache,
        index,
      )?;
    }

    if is_coinbase {
      for flotsam in inscriptions {
        let new_satpoint = SatPoint {
          outpoint: OutPoint::null(),
          offset: self.lost_sats + flotsam.offset - output_value,
        };
        self.update_inscription_location(
          input_sat_ranges,
          flotsam,
          new_satpoint,
          false,
          None,
          utxo_cache,
          index,
        )?;
      }
      self.lost_sats += self.reward - output_value;
      Ok(())
    } else {
      self.flotsam.extend(inscriptions.map(|flotsam| Flotsam {
        offset: self.reward + flotsam.offset - output_value,
        ..flotsam
      }));
      self.reward += total_input_value - output_value;
      Ok(())
    }
  }

  fn calculate_sat(input_sat_ranges: Option<&Vec<&[u8]>>, input_offset: u64) -> Option<Sat> {
    let input_sat_ranges = input_sat_ranges?;

    let mut offset = 0;
    for chunk in input_sat_ranges
      .iter()
      .flat_map(|slice| slice.chunks_exact(11))
    {
      let (start, end) = SatRange::load(chunk.try_into().unwrap());
      let size = end - start;
      if offset + size > input_offset {
        let n = start + input_offset - offset;
        return Some(Sat(n));
      }
      offset += size;
    }

    unreachable!()
  }

  fn update_inscription_location(
    &mut self,
    input_sat_ranges: Option<&Vec<&[u8]>>,
    flotsam: Flotsam,
    new_satpoint: SatPoint,
    op_return: bool,
    mut normal_output_utxo_entry: Option<&mut UtxoEntryBuf>,
    utxo_cache: &mut HashMap<OutPoint, UtxoEntryBuf>,
    index: &Index,
  ) -> Result {
    let inscription_id = flotsam.inscription_id;
    let (unbound, sequence_number) = match flotsam.origin {
      Origin::Old {
        sequence_number,
        old_satpoint,
      } => {
        if op_return {
          let entry = InscriptionEntry::load(
            self
              .sequence_number_to_entry
              .get(&sequence_number)?
              .unwrap()
              .value(),
          );

          let mut charms = entry.charms;
          Charm::Burned.set(&mut charms);

          self.sequence_number_to_entry.insert(
            sequence_number,
            &InscriptionEntry { charms, ..entry }.store(),
          )?;
        }

        if let Some(ref sender) = index.event_sender {
          sender.blocking_send(Event::InscriptionTransferred {
            block_height: self.height,
            inscription_id,
            new_location: new_satpoint,
            old_location: old_satpoint,
            sequence_number,
          })?;
        }

        (false, sequence_number)
      }
      Origin::New {
        cursed,
        fee,
        hidden,
        parents,
        reinscription,
        unbound,
        vindicated,
      } => {
        let inscription_number = if cursed {
          let number: i32 = self.cursed_inscription_count.try_into().unwrap();
          self.cursed_inscription_count += 1;
          -(number + 1)
        } else {
          let number: i32 = self.blessed_inscription_count.try_into().unwrap();
          self.blessed_inscription_count += 1;
          number
        };

        let sequence_number = self.next_sequence_number;
        self.next_sequence_number += 1;

        self
          .inscription_number_to_sequence_number
          .insert(inscription_number, sequence_number)?;

        let sat = if unbound {
          None
        } else {
          Self::calculate_sat(input_sat_ranges, flotsam.offset)
        };

        let mut charms = 0;

        if cursed {
          Charm::Cursed.set(&mut charms);
        }

        if reinscription {
          Charm::Reinscription.set(&mut charms);
        }

        if let Some(sat) = sat {
          charms |= sat.charms();
        }

        if op_return {
          Charm::Burned.set(&mut charms);
        }

        if new_satpoint.outpoint == OutPoint::null() {
          Charm::Lost.set(&mut charms);
        }

        if unbound {
          Charm::Unbound.set(&mut charms);
        }

        if vindicated {
          Charm::Vindicated.set(&mut charms);
        }

        if let Some(Sat(n)) = sat {
          self.sat_to_sequence_number.insert(&n, &sequence_number)?;
        }

        let parent_sequence_numbers = parents
          .iter()
          .map(|parent| {
            let parent_sequence_number = self
              .id_to_sequence_number
              .get(&parent.store())?
              .unwrap()
              .value();

            self
              .sequence_number_to_children
              .insert(parent_sequence_number, sequence_number)?;

            Ok(parent_sequence_number)
          })
          .collect::<Result<Vec<u32>>>()?;

        if let Some(ref sender) = index.event_sender {
          sender.blocking_send(Event::InscriptionCreated {
            block_height: self.height,
            charms,
            inscription_id,
            location: (!unbound).then_some(new_satpoint),
            parent_inscription_ids: parents,
            sequence_number,
          })?;
        }

        self.sequence_number_to_entry.insert(
          sequence_number,
          &InscriptionEntry {
            charms,
            fee,
            height: self.height,
            id: inscription_id,
            inscription_number,
            parents: parent_sequence_numbers,
            sat,
            sequence_number,
            timestamp: self.timestamp,
          }
          .store(),
        )?;

        self
          .id_to_sequence_number
          .insert(&inscription_id.store(), sequence_number)?;

        if !hidden {
          self
            .home_inscriptions
            .insert(&sequence_number, inscription_id.store())?;

          if self.home_inscription_count == 100 {
            self.home_inscriptions.pop_first()?;
          } else {
            self.home_inscription_count += 1;
          }
        }

        (unbound, sequence_number)
      }
    };

    let satpoint = if unbound {
      let new_unbound_satpoint = SatPoint {
        outpoint: unbound_outpoint(),
        offset: self.unbound_inscriptions,
      };
      self.unbound_inscriptions += 1;
      normal_output_utxo_entry = None;
      new_unbound_satpoint
    } else {
      new_satpoint
    };

    // The special outpoints, i.e., the null outpoint and the unbound outpoint,
    // don't follow the normal rules. Unlike real outputs they get written to
    // more than once. So we create a new UTXO entry here and commit() will
    // merge it with any existing entry.
    let output_utxo_entry = normal_output_utxo_entry.unwrap_or_else(|| {
      assert!(Index::is_special_outpoint(satpoint.outpoint));
      utxo_cache
        .entry(satpoint.outpoint)
        .or_insert(UtxoEntryBuf::empty(index))
    });

    output_utxo_entry.push_inscription(sequence_number, satpoint.offset, index);

    Ok(())
  }
}

ord/src/index/updater/rune_updater.rs


use super::*;

pub(super) struct RuneUpdater<'a, 'tx, 'client> {
  pub(super) block_time: u32,
  pub(super) burned: HashMap<RuneId, Lot>,
  pub(super) client: &'client Client,
  pub(super) event_sender: Option<&'a mpsc::Sender<Event>>,
  pub(super) height: u32,
  pub(super) id_to_entry: &'a mut Table<'tx, RuneIdValue, RuneEntryValue>,
  pub(super) inscription_id_to_sequence_number: &'a Table<'tx, InscriptionIdValue, u32>,
  pub(super) minimum: Rune,
  pub(super) outpoint_to_balances: &'a mut Table<'tx, &'static OutPointValue, &'static [u8]>,
  pub(super) rune_to_id: &'a mut Table<'tx, u128, RuneIdValue>,
  pub(super) runes: u64,
  pub(super) sequence_number_to_rune_id: &'a mut Table<'tx, u32, RuneIdValue>,
  pub(super) statistic_to_count: &'a mut Table<'tx, u64, u64>,
  pub(super) transaction_id_to_rune: &'a mut Table<'tx, &'static TxidValue, u128>,
}

impl RuneUpdater<'_, '_, '_> {
  pub(super) fn index_runes(&mut self, tx_index: u32, tx: &Transaction, txid: Txid) -> Result<()> {
    let artifact = Runestone::decipher(tx);

    let mut unallocated = self.unallocated(tx)?;

    let mut allocated: Vec<HashMap<RuneId, Lot>> = vec![HashMap::new(); tx.output.len()];

    if let Some(artifact) = &artifact {
      if let Some(id) = artifact.mint() {
        if let Some(amount) = self.mint(id)? {
          *unallocated.entry(id).or_default() += amount;

          if let Some(sender) = self.event_sender {
            sender.blocking_send(Event::RuneMinted {
              block_height: self.height,
              txid,
              rune_id: id,
              amount: amount.n(),
            })?;
          }
        }
      }

      let etched = self.etched(tx_index, tx, artifact)?;

      if let Artifact::Runestone(runestone) = artifact {
        if let Some((id, ..)) = etched {
          *unallocated.entry(id).or_default() +=
            runestone.etching.unwrap().premine.unwrap_or_default();
        }

        for Edict { id, amount, output } in runestone.edicts.iter().copied() {
          let amount = Lot(amount);

          // edicts with output values greater than the number of outputs
          // should never be produced by the edict parser
          let output = usize::try_from(output).unwrap();
          assert!(output <= tx.output.len());

          let id = if id == RuneId::default() {
            let Some((id, ..)) = etched else {
              continue;
            };

            id
          } else {
            id
          };

          let Some(balance) = unallocated.get_mut(&id) else {
            continue;
          };

          let mut allocate = |balance: &mut Lot, amount: Lot, output: usize| {
            if amount > 0 {
              *balance -= amount;
              *allocated[output].entry(id).or_default() += amount;
            }
          };

          if output == tx.output.len() {
            // find non-OP_RETURN outputs
            let destinations = tx
              .output
              .iter()
              .enumerate()
              .filter_map(|(output, tx_out)| {
                (!tx_out.script_pubkey.is_op_return()).then_some(output)
              })
              .collect::<Vec<usize>>();

            if !destinations.is_empty() {
              if amount == 0 {
                // if amount is zero, divide balance between eligible outputs
                let amount = *balance / destinations.len() as u128;
                let remainder = usize::try_from(*balance % destinations.len() as u128).unwrap();

                for (i, output) in destinations.iter().enumerate() {
                  allocate(
                    balance,
                    if i < remainder { amount + 1 } else { amount },
                    *output,
                  );
                }
              } else {
                // if amount is non-zero, distribute amount to eligible outputs
                for output in destinations {
                  allocate(balance, amount.min(*balance), output);
                }
              }
            }
          } else {
            // Get the allocatable amount
            let amount = if amount == 0 {
              *balance
            } else {
              amount.min(*balance)
            };

            allocate(balance, amount, output);
          }
        }
      }

      if let Some((id, rune)) = etched {
        self.create_rune_entry(txid, artifact, id, rune)?;
      }
    }

    let mut burned: HashMap<RuneId, Lot> = HashMap::new();

    if let Some(Artifact::Cenotaph(_)) = artifact {
      for (id, balance) in unallocated {
        *burned.entry(id).or_default() += balance;
      }
    } else {
      let pointer = artifact
        .map(|artifact| match artifact {
          Artifact::Runestone(runestone) => runestone.pointer,
          Artifact::Cenotaph(_) => unreachable!(),
        })
        .unwrap_or_default();

      // assign all un-allocated runes to the default output, or the first non
      // OP_RETURN output if there is no default
      if let Some(vout) = pointer
        .map(|pointer| pointer.into_usize())
        .inspect(|&pointer| assert!(pointer < allocated.len()))
        .or_else(|| {
          tx.output
            .iter()
            .enumerate()
            .find(|(_vout, tx_out)| !tx_out.script_pubkey.is_op_return())
            .map(|(vout, _tx_out)| vout)
        })
      {
        for (id, balance) in unallocated {
          if balance > 0 {
            *allocated[vout].entry(id).or_default() += balance;
          }
        }
      } else {
        for (id, balance) in unallocated {
          if balance > 0 {
            *burned.entry(id).or_default() += balance;
          }
        }
      }
    }

    // update outpoint balances
    let mut buffer: Vec<u8> = Vec::new();
    for (vout, balances) in allocated.into_iter().enumerate() {
      if balances.is_empty() {
        continue;
      }

      // increment burned balances
      if tx.output[vout].script_pubkey.is_op_return() {
        for (id, balance) in &balances {
          *burned.entry(*id).or_default() += *balance;
        }
        continue;
      }

      buffer.clear();

      let mut balances = balances.into_iter().collect::<Vec<(RuneId, Lot)>>();

      // Sort balances by id so tests can assert balances in a fixed order
      balances.sort();

      let outpoint = OutPoint {
        txid,
        vout: vout.try_into().unwrap(),
      };

      for (id, balance) in balances {
        Index::encode_rune_balance(id, balance.n(), &mut buffer);

        if let Some(sender) = self.event_sender {
          sender.blocking_send(Event::RuneTransferred {
            outpoint,
            block_height: self.height,
            txid,
            rune_id: id,
            amount: balance.0,
          })?;
        }
      }

      self
        .outpoint_to_balances
        .insert(&outpoint.store(), buffer.as_slice())?;
    }

    // increment entries with burned runes
    for (id, amount) in burned {
      *self.burned.entry(id).or_default() += amount;

      if let Some(sender) = self.event_sender {
        sender.blocking_send(Event::RuneBurned {
          block_height: self.height,
          txid,
          rune_id: id,
          amount: amount.n(),
        })?;
      }
    }

    Ok(())
  }

  pub(super) fn update(self) -> Result {
    for (rune_id, burned) in self.burned {
      let mut entry = RuneEntry::load(self.id_to_entry.get(&rune_id.store())?.unwrap().value());
      entry.burned = entry.burned.checked_add(burned.n()).unwrap();
      self.id_to_entry.insert(&rune_id.store(), entry.store())?;
    }

    Ok(())
  }

  fn create_rune_entry(
    &mut self,
    txid: Txid,
    artifact: &Artifact,
    id: RuneId,
    rune: Rune,
  ) -> Result {
    self.rune_to_id.insert(rune.store(), id.store())?;
    self
      .transaction_id_to_rune
      .insert(&txid.store(), rune.store())?;

    let number = self.runes;
    self.runes += 1;

    self
      .statistic_to_count
      .insert(&Statistic::Runes.into(), self.runes)?;

    let entry = match artifact {
      Artifact::Cenotaph(_) => RuneEntry {
        block: id.block,
        burned: 0,
        divisibility: 0,
        etching: txid,
        terms: None,
        mints: 0,
        number,
        premine: 0,
        spaced_rune: SpacedRune { rune, spacers: 0 },
        symbol: None,
        timestamp: self.block_time.into(),
        turbo: false,
      },
      Artifact::Runestone(Runestone { etching, .. }) => {
        let Etching {
          divisibility,
          terms,
          premine,
          spacers,
          symbol,
          turbo,
          ..
        } = etching.unwrap();

        RuneEntry {
          block: id.block,
          burned: 0,
          divisibility: divisibility.unwrap_or_default(),
          etching: txid,
          terms,
          mints: 0,
          number,
          premine: premine.unwrap_or_default(),
          spaced_rune: SpacedRune {
            rune,
            spacers: spacers.unwrap_or_default(),
          },
          symbol,
          timestamp: self.block_time.into(),
          turbo,
        }
      }
    };

    self.id_to_entry.insert(id.store(), entry.store())?;

    if let Some(sender) = self.event_sender {
      sender.blocking_send(Event::RuneEtched {
        block_height: self.height,
        txid,
        rune_id: id,
      })?;
    }

    let inscription_id = InscriptionId { txid, index: 0 };

    if let Some(sequence_number) = self
      .inscription_id_to_sequence_number
      .get(&inscription_id.store())?
    {
      self
        .sequence_number_to_rune_id
        .insert(sequence_number.value(), id.store())?;
    }

    Ok(())
  }

  fn etched(
    &mut self,
    tx_index: u32,
    tx: &Transaction,
    artifact: &Artifact,
  ) -> Result<Option<(RuneId, Rune)>> {
    let rune = match artifact {
      Artifact::Runestone(runestone) => match runestone.etching {
        Some(etching) => etching.rune,
        None => return Ok(None),
      },
      Artifact::Cenotaph(cenotaph) => match cenotaph.etching {
        Some(rune) => Some(rune),
        None => return Ok(None),
      },
    };

    let rune = if let Some(rune) = rune {
      if rune < self.minimum
        || rune.is_reserved()
        || self.rune_to_id.get(rune.0)?.is_some()
        || !self.tx_commits_to_rune(tx, rune)?
      {
        return Ok(None);
      }
      rune
    } else {
      let reserved_runes = self
        .statistic_to_count
        .get(&Statistic::ReservedRunes.into())?
        .map(|entry| entry.value())
        .unwrap_or_default();

      self
        .statistic_to_count
        .insert(&Statistic::ReservedRunes.into(), reserved_runes + 1)?;

      Rune::reserved(self.height.into(), tx_index)
    };

    Ok(Some((
      RuneId {
        block: self.height.into(),
        tx: tx_index,
      },
      rune,
    )))
  }

  fn mint(&mut self, id: RuneId) -> Result<Option<Lot>> {
    let Some(entry) = self.id_to_entry.get(&id.store())? else {
      return Ok(None);
    };

    let mut rune_entry = RuneEntry::load(entry.value());

    let Ok(amount) = rune_entry.mintable(self.height.into()) else {
      return Ok(None);
    };

    drop(entry);

    rune_entry.mints += 1;

    self.id_to_entry.insert(&id.store(), rune_entry.store())?;

    Ok(Some(Lot(amount)))
  }

  fn tx_commits_to_rune(&self, tx: &Transaction, rune: Rune) -> Result<bool> {
    let commitment = rune.commitment();

    for input in &tx.input {
      // extracting a tapscript does not indicate that the input being spent
      // was actually a taproot output. this is checked below, when we load the
      // output's entry from the database
      #[allow(deprecated)]
      let Some(tapscript) = input.witness.tapscript() else {
        continue;
      };

      for instruction in tapscript.instructions() {
        // ignore errors, since the extracted script may not be valid
        let Ok(instruction) = instruction else {
          break;
        };

        let Some(pushbytes) = instruction.push_bytes() else {
          continue;
        };

        if pushbytes.as_bytes() != commitment {
          continue;
        }

        let Some(tx_info) = self
          .client
          .get_raw_transaction_info(&input.previous_output.txid, None)
          .into_option()?
        else {
          panic!(
            "can't get input transaction: {}",
            input.previous_output.txid
          );
        };

        let taproot = tx_info.vout[input.previous_output.vout.into_usize()]
          .script_pub_key
          .script()?
          .is_p2tr();

        if !taproot {
          continue;
        }

        let commit_tx_height = self
          .client
          .get_block_header_info(&tx_info.blockhash.unwrap())
          .into_option()?
          .unwrap()
          .height;

        let confirmations = self
          .height
          .checked_sub(commit_tx_height.try_into().unwrap())
          .unwrap()
          + 1;

        if confirmations >= u32::from(Runestone::COMMIT_CONFIRMATIONS) {
          return Ok(true);
        }
      }
    }

    Ok(false)
  }

  fn unallocated(&mut self, tx: &Transaction) -> Result<HashMap<RuneId, Lot>> {
    // map of rune ID to un-allocated balance of that rune
    let mut unallocated: HashMap<RuneId, Lot> = HashMap::new();

    // increment unallocated runes with the runes in tx inputs
    for input in &tx.input {
      if let Some(guard) = self
        .outpoint_to_balances
        .remove(&input.previous_output.store())?
      {
        let buffer = guard.value();
        let mut i = 0;
        while i < buffer.len() {
          let ((id, balance), len) = Index::decode_rune_balance(&buffer[i..]).unwrap();
          i += len;
          *unallocated.entry(id).or_default() += balance;
        }
      }
    }

    Ok(unallocated)
  }
}

ord/src/index/utxo_entry.rs


use {
  super::{
    entry::{Entry, SatRange},
    Index,
  },
  ordinals::varint,
  redb::TypeName,
  ref_cast::RefCast,
  std::ops::Deref,
};

enum Sats<'a> {
  Ranges(&'a [u8]),
  Value(u64),
}

/// A `UtxoEntry` stores the following information about an unspent transaction
/// output, depending on the indexing options:
///
/// If `--index-sats`, the full list of sat ranges, stored as a varint followed
/// by that many 11-byte sat range entries, otherwise the total output value
/// stored as a varint.
///
/// If `--index-addresses`, the script pubkey stored as a varint followed by
/// that many bytes of data.
///
/// If `--index-inscriptions`, the list of inscriptions stored as
/// `(sequence_number, offset)`, with the sequence number stored as a u32 and
/// the offset as a varint.
///
/// Note that the list of inscriptions doesn't need an explicit length, it
/// continues until the end of the array.
///
/// A `UtxoEntry` is the read-only value stored in redb as a byte string. A
/// `UtxoEntryBuf` is the writeable version, used for constructing new
/// `UtxoEntry`s. A `ParsedUtxoEntry` is the parsed value.
#[derive(Debug, RefCast)]
#[repr(transparent)]
pub struct UtxoEntry {
  bytes: [u8],
}

impl UtxoEntry {
  pub fn parse(&self, index: &Index) -> ParsedUtxoEntry {
    let sats;
    let mut script_pubkey = None;
    let mut inscriptions = None;

    let mut offset = 0;
    if index.index_sats {
      let (num_sat_ranges, varint_len) = varint::decode(&self.bytes).unwrap();
      offset += varint_len;

      let num_sat_ranges: usize = num_sat_ranges.try_into().unwrap();
      let sat_ranges_len = num_sat_ranges * 11;
      sats = Sats::Ranges(&self.bytes[offset..offset + sat_ranges_len]);
      offset += sat_ranges_len;
    } else {
      let (value, varint_len) = varint::decode(&self.bytes).unwrap();
      sats = Sats::Value(value.try_into().unwrap());
      offset += varint_len;
    };

    if index.index_addresses {
      let (script_pubkey_len, varint_len) = varint::decode(&self.bytes[offset..]).unwrap();
      offset += varint_len;

      let script_pubkey_len: usize = script_pubkey_len.try_into().unwrap();
      script_pubkey = Some(&self.bytes[offset..offset + script_pubkey_len]);
      offset += script_pubkey_len;
    }

    if index.index_inscriptions {
      inscriptions = Some(&self.bytes[offset..self.bytes.len()]);
    }

    ParsedUtxoEntry {
      sats,
      script_pubkey,
      inscriptions,
    }
  }

  pub fn to_buf(&self) -> UtxoEntryBuf {
    UtxoEntryBuf {
      vec: self.bytes.to_vec(),
      #[cfg(debug_assertions)]
      state: State::Valid,
    }
  }
}

impl redb::Value for &UtxoEntry {
  type SelfType<'a>
    = &'a UtxoEntry
  where
    Self: 'a;

  type AsBytes<'a>
    = &'a [u8]
  where
    Self: 'a;

  fn fixed_width() -> Option<usize> {
    None
  }

  fn from_bytes<'a>(data: &'a [u8]) -> Self::SelfType<'a>
  where
    Self: 'a,
  {
    UtxoEntry::ref_cast(data)
  }

  fn as_bytes<'a, 'b: 'a>(value: &'a Self::SelfType<'b>) -> Self::AsBytes<'a>
  where
    Self: 'a,
    Self: 'b,
  {
    &value.bytes
  }

  fn type_name() -> TypeName {
    TypeName::new("ord::UtxoEntry")
  }
}

pub struct ParsedUtxoEntry<'a> {
  sats: Sats<'a>,
  script_pubkey: Option<&'a [u8]>,
  inscriptions: Option<&'a [u8]>,
}

impl<'a> ParsedUtxoEntry<'a> {
  pub fn total_value(&self) -> u64 {
    match self.sats {
      Sats::Value(value) => value,
      Sats::Ranges(ranges) => {
        let mut value = 0;
        for chunk in ranges.chunks_exact(11) {
          let range = SatRange::load(chunk.try_into().unwrap());
          value += range.1 - range.0;
        }
        value
      }
    }
  }

  pub fn sat_ranges(&self) -> &'a [u8] {
    let Sats::Ranges(ranges) = self.sats else {
      panic!("sat ranges are missing");
    };
    ranges
  }

  pub fn script_pubkey(&self) -> &'a [u8] {
    self.script_pubkey.unwrap()
  }

  pub fn inscriptions(&self) -> &'a [u8] {
    self.inscriptions.unwrap()
  }

  pub fn parse_inscriptions(&self) -> Vec<(u32, u64)> {
    let inscriptions = self.inscriptions.unwrap();
    let mut byte_offset = 0;
    let mut parsed_inscriptions = Vec::new();

    while byte_offset < inscriptions.len() {
      let sequence_number = u32::from_le_bytes(
        inscriptions[byte_offset..byte_offset + 4]
          .try_into()
          .unwrap(),
      );
      byte_offset += 4;

      let (satpoint_offset, varint_len) = varint::decode(&inscriptions[byte_offset..]).unwrap();
      let satpoint_offset = u64::try_from(satpoint_offset).unwrap();
      byte_offset += varint_len;

      parsed_inscriptions.push((sequence_number, satpoint_offset));
    }

    parsed_inscriptions
  }
}

#[cfg(debug_assertions)]
#[derive(Debug, Eq, PartialEq)]
enum State {
  NeedSats,
  NeedScriptPubkey,
  Valid,
}

#[derive(Debug)]
pub struct UtxoEntryBuf {
  vec: Vec<u8>,
  #[cfg(debug_assertions)]
  state: State,
}

impl UtxoEntryBuf {
  pub fn new() -> Self {
    Self {
      vec: Vec::new(),
      #[cfg(debug_assertions)]
      state: State::NeedSats,
    }
  }

  pub fn push_value(&mut self, value: u64, index: &Index) {
    assert!(!index.index_sats);
    varint::encode_to_vec(value.into(), &mut self.vec);

    #[cfg(debug_assertions)]
    self.advance_state(State::NeedSats, State::NeedScriptPubkey, index);
  }

  pub fn push_sat_ranges(&mut self, sat_ranges: &[u8], index: &Index) {
    assert!(index.index_sats);
    let num_sat_ranges = sat_ranges.len() / 11;
    assert!(num_sat_ranges * 11 == sat_ranges.len());
    varint::encode_to_vec(num_sat_ranges.try_into().unwrap(), &mut self.vec);
    self.vec.extend(sat_ranges);

    #[cfg(debug_assertions)]
    self.advance_state(State::NeedSats, State::NeedScriptPubkey, index);
  }

  pub fn push_script_pubkey(&mut self, script_pubkey: &[u8], index: &Index) {
    assert!(index.index_addresses);
    varint::encode_to_vec(script_pubkey.len().try_into().unwrap(), &mut self.vec);
    self.vec.extend(script_pubkey);

    #[cfg(debug_assertions)]
    self.advance_state(State::NeedScriptPubkey, State::Valid, index);
  }

  pub fn push_inscriptions(&mut self, inscriptions: &[u8], index: &Index) {
    assert!(index.index_inscriptions);
    self.vec.extend(inscriptions);

    #[cfg(debug_assertions)]
    self.advance_state(State::Valid, State::Valid, index);
  }

  pub fn push_inscription(&mut self, sequence_number: u32, satpoint_offset: u64, index: &Index) {
    assert!(index.index_inscriptions);
    self.vec.extend(sequence_number.to_le_bytes());
    varint::encode_to_vec(satpoint_offset.into(), &mut self.vec);

    #[cfg(debug_assertions)]
    self.advance_state(State::Valid, State::Valid, index);
  }

  #[cfg(debug_assertions)]
  fn advance_state(&mut self, expected_state: State, new_state: State, index: &Index) {
    assert!(self.state == expected_state);
    self.state = new_state;

    if self.state == State::NeedScriptPubkey && !index.index_addresses {
      self.state = State::Valid;
    }
  }

  pub fn merged(a: &UtxoEntry, b: &UtxoEntry, index: &Index) -> Self {
    let a_parsed = a.parse(index);
    let b_parsed = b.parse(index);
    let mut merged = Self::new();

    if index.index_sats {
      let sat_ranges = [a_parsed.sat_ranges(), b_parsed.sat_ranges()].concat();
      merged.push_sat_ranges(&sat_ranges, index);
    } else {
      assert!(a_parsed.total_value() == 0);
      assert!(b_parsed.total_value() == 0);
      merged.push_value(0, index);
    }

    if index.index_addresses {
      assert!(a_parsed.script_pubkey().is_empty());
      assert!(b_parsed.script_pubkey().is_empty());
      merged.push_script_pubkey(&[], index);
    }

    if index.index_inscriptions {
      merged.push_inscriptions(a_parsed.inscriptions(), index);
      merged.push_inscriptions(b_parsed.inscriptions(), index);
    }

    merged
  }

  pub fn empty(index: &Index) -> Self {
    let mut utxo_entry = Self::new();

    if index.index_sats {
      utxo_entry.push_sat_ranges(&[], index);
    } else {
      utxo_entry.push_value(0, index);
    }

    if index.index_addresses {
      utxo_entry.push_script_pubkey(&[], index);
    }

    utxo_entry
  }

  pub fn as_ref(&self) -> &UtxoEntry {
    #[cfg(debug_assertions)]
    assert!(self.state == State::Valid);
    UtxoEntry::ref_cast(&self.vec)
  }
}

impl Default for UtxoEntryBuf {
  fn default() -> Self {
    Self::new()
  }
}

impl Deref for UtxoEntryBuf {
  type Target = UtxoEntry;

  fn deref(&self) -> &UtxoEntry {
    self.as_ref()
  }
}

ord/src/inscriptions.rs


use super::*;

use tag::Tag;

pub(crate) use self::media::Media;

pub use self::{
  envelope::Envelope, envelope::ParsedEnvelope, envelope::RawEnvelope, inscription::Inscription,
  inscription_id::InscriptionId,
};

mod envelope;
mod inscription;
pub(crate) mod inscription_id;
pub(crate) mod media;
mod tag;
pub(crate) mod teleburn;

ord/src/inscriptions/envelope.rs


use {
  super::*,
  bitcoin::blockdata::{
    opcodes,
    script::{
      Instruction::{self, Op, PushBytes},
      Instructions,
    },
  },
  std::iter::Peekable,
};

pub(crate) const PROTOCOL_ID: [u8; 3] = *b"ord";
pub(crate) const BODY_TAG: [u8; 0] = [];

type Result<T> = std::result::Result<T, script::Error>;
pub type RawEnvelope = Envelope<Vec<Vec<u8>>>;
pub type ParsedEnvelope = Envelope<Inscription>;

#[derive(Default, PartialEq, Clone, Serialize, Deserialize, Debug, Eq)]
pub struct Envelope<T> {
  pub input: u32,
  pub offset: u32,
  pub payload: T,
  pub pushnum: bool,
  pub stutter: bool,
}

impl From<RawEnvelope> for ParsedEnvelope {
  fn from(envelope: RawEnvelope) -> Self {
    let body = envelope
      .payload
      .iter()
      .enumerate()
      .position(|(i, push)| i % 2 == 0 && push.is_empty());

    let mut fields: BTreeMap<&[u8], Vec<&[u8]>> = BTreeMap::new();

    let mut incomplete_field = false;

    for item in envelope.payload[..body.unwrap_or(envelope.payload.len())].chunks(2) {
      match item {
        [key, value] => fields.entry(key).or_default().push(value),
        _ => incomplete_field = true,
      }
    }

    let duplicate_field = fields.iter().any(|(_key, values)| values.len() > 1);

    let content_encoding = Tag::ContentEncoding.take(&mut fields);
    let content_type = Tag::ContentType.take(&mut fields);
    let delegate = Tag::Delegate.take(&mut fields);
    let metadata = Tag::Metadata.take(&mut fields);
    let metaprotocol = Tag::Metaprotocol.take(&mut fields);
    let parents = Tag::Parent.take_array(&mut fields);
    let pointer = Tag::Pointer.take(&mut fields);
    let properties = Tag::Properties.take(&mut fields);
    let rune = Tag::Rune.take(&mut fields);

    let unrecognized_even_field = fields
      .keys()
      .any(|tag| tag.first().map(|lsb| lsb % 2 == 0).unwrap_or_default());

    Self {
      payload: Inscription {
        body: body.map(|i| {
          envelope.payload[i + 1..]
            .iter()
            .flatten()
            .cloned()
            .collect()
        }),
        content_encoding,
        content_type,
        delegate,
        duplicate_field,
        incomplete_field,
        metadata,
        metaprotocol,
        parents,
        pointer,
        properties,
        rune,
        unrecognized_even_field,
      },
      input: envelope.input,
      offset: envelope.offset,
      pushnum: envelope.pushnum,
      stutter: envelope.stutter,
    }
  }
}

impl ParsedEnvelope {
  pub fn from_transaction(transaction: &Transaction) -> Vec<Self> {
    RawEnvelope::from_transaction(transaction)
      .into_iter()
      .map(|envelope| envelope.into())
      .collect()
  }
}

impl RawEnvelope {
  pub fn from_transaction(transaction: &Transaction) -> Vec<Self> {
    let mut envelopes = Vec::new();

    for (i, input) in transaction.input.iter().enumerate() {
      #[allow(deprecated)]
      if let Some(tapscript) = input.witness.tapscript() {
        if let Ok(input_envelopes) = Self::from_tapscript(tapscript, i) {
          envelopes.extend(input_envelopes);
        }
      }
    }

    envelopes
  }

  fn from_tapscript(tapscript: &Script, input: usize) -> Result<Vec<Self>> {
    let mut envelopes = Vec::new();

    let mut instructions = tapscript.instructions().peekable();

    let mut stuttered = false;
    while let Some(instruction) = instructions.next().transpose()? {
      if instruction == PushBytes((&[]).into()) {
        let (stutter, envelope) =
          Self::from_instructions(&mut instructions, input, envelopes.len(), stuttered)?;
        if let Some(envelope) = envelope {
          envelopes.push(envelope);
        } else {
          stuttered = stutter;
        }
      }
    }

    Ok(envelopes)
  }

  fn accept(instructions: &mut Peekable<Instructions>, instruction: Instruction) -> Result<bool> {
    if instructions.peek() == Some(&Ok(instruction)) {
      instructions.next().transpose()?;
      Ok(true)
    } else {
      Ok(false)
    }
  }

  fn from_instructions(
    instructions: &mut Peekable<Instructions>,
    input: usize,
    offset: usize,
    stutter: bool,
  ) -> Result<(bool, Option<Self>)> {
    if !Self::accept(instructions, Op(opcodes::all::OP_IF))? {
      let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into())));
      return Ok((stutter, None));
    }

    if !Self::accept(instructions, PushBytes((&PROTOCOL_ID).into()))? {
      let stutter = instructions.peek() == Some(&Ok(PushBytes((&[]).into())));
      return Ok((stutter, None));
    }

    let mut pushnum = false;

    let mut payload = Vec::new();

    loop {
      match instructions.next().transpose()? {
        None => return Ok((false, None)),
        Some(Op(opcodes::all::OP_ENDIF)) => {
          return Ok((
            false,
            Some(Envelope {
              input: input.try_into().unwrap(),
              offset: offset.try_into().unwrap(),
              payload,
              pushnum,
              stutter,
            }),
          ));
        }
        Some(Op(opcodes::all::OP_PUSHNUM_NEG1)) => {
          pushnum = true;
          payload.push(vec![0x81]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_1)) => {
          pushnum = true;
          payload.push(vec![1]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_2)) => {
          pushnum = true;
          payload.push(vec![2]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_3)) => {
          pushnum = true;
          payload.push(vec![3]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_4)) => {
          pushnum = true;
          payload.push(vec![4]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_5)) => {
          pushnum = true;
          payload.push(vec![5]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_6)) => {
          pushnum = true;
          payload.push(vec![6]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_7)) => {
          pushnum = true;
          payload.push(vec![7]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_8)) => {
          pushnum = true;
          payload.push(vec![8]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_9)) => {
          pushnum = true;
          payload.push(vec![9]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_10)) => {
          pushnum = true;
          payload.push(vec![10]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_11)) => {
          pushnum = true;
          payload.push(vec![11]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_12)) => {
          pushnum = true;
          payload.push(vec![12]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_13)) => {
          pushnum = true;
          payload.push(vec![13]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_14)) => {
          pushnum = true;
          payload.push(vec![14]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_15)) => {
          pushnum = true;
          payload.push(vec![15]);
        }
        Some(Op(opcodes::all::OP_PUSHNUM_16)) => {
          pushnum = true;
          payload.push(vec![16]);
        }
        Some(PushBytes(push)) => {
          payload.push(push.as_bytes().to_vec());
        }
        Some(_) => return Ok((false, None)),
      }
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  fn parse(witnesses: &[Witness]) -> Vec<ParsedEnvelope> {
    ParsedEnvelope::from_transaction(&Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: witnesses
        .iter()
        .map(|witness| TxIn {
          previous_output: OutPoint::null(),
          script_sig: ScriptBuf::new(),
          sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
          witness: witness.clone(),
        })
        .collect(),
      output: Vec::new(),
    })
  }

  #[test]
  fn empty() {
    assert_eq!(parse(&[Witness::new()]), Vec::new())
  }

  #[test]
  fn ignore_key_path_spends() {
    assert_eq!(
      parse(&[Witness::from_slice(&[script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(PROTOCOL_ID)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script()
        .into_bytes()])]),
      Vec::new()
    );
  }

  #[test]
  fn ignore_key_path_spends_with_annex() {
    assert_eq!(
      parse(&[Witness::from_slice(&[
        script::Builder::new()
          .push_opcode(opcodes::OP_FALSE)
          .push_opcode(opcodes::all::OP_IF)
          .push_slice(PROTOCOL_ID)
          .push_opcode(opcodes::all::OP_ENDIF)
          .into_script()
          .into_bytes(),
        vec![0x50]
      ])]),
      Vec::new()
    );
  }

  #[test]
  fn parse_from_tapscript() {
    assert_eq!(
      parse(&[Witness::from_slice(&[
        script::Builder::new()
          .push_opcode(opcodes::OP_FALSE)
          .push_opcode(opcodes::all::OP_IF)
          .push_slice(PROTOCOL_ID)
          .push_opcode(opcodes::all::OP_ENDIF)
          .into_script()
          .into_bytes(),
        Vec::new()
      ])]),
      vec![ParsedEnvelope { ..default() }]
    );
  }

  #[test]
  fn ignore_unparsable_scripts() {
    let mut script_bytes = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script()
      .into_bytes();
    script_bytes.push(0x01);

    assert_eq!(
      parse(&[Witness::from_slice(&[script_bytes, Vec::new()])]),
      Vec::new()
    );
  }

  #[test]
  fn no_inscription() {
    assert_eq!(
      parse(&[Witness::from_slice(&[
        ScriptBuf::new().into_bytes(),
        Vec::new()
      ])]),
      Vec::new()
    );
  }

  #[test]
  fn duplicate_field() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        Tag::Nop.bytes().as_slice(),
        &[],
        &Tag::Nop.bytes(),
        &[]
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          duplicate_field: true,
          ..default()
        },
        ..default()
      }]
    );
  }

  #[test]
  fn with_content_type() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[],
        b"ord",
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "ord"),
        ..default()
      }]
    );
  }

  #[test]
  fn with_content_encoding() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[9],
        b"br",
        &[],
        b"ord",
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          content_encoding: Some("br".as_bytes().to_vec()),
          ..inscription("text/plain;charset=utf-8", "ord")
        },
        ..default()
      }]
    );
  }

  #[test]
  fn with_unknown_tag() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        Tag::Nop.bytes().as_slice(),
        b"bar",
        &[],
        b"ord",
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "ord"),
        ..default()
      }]
    );
  }

  #[test]
  fn no_body() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8"
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          content_type: Some(b"text/plain;charset=utf-8".to_vec()),
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn no_content_type() {
    assert_eq!(
      parse(&[envelope(&[b"ord", &[], b"foo"])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          body: Some(b"foo".to_vec()),
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn valid_body_in_multiple_pushes() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[],
        b"foo",
        b"bar"
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "foobar"),
        ..default()
      }],
    );
  }

  #[test]
  fn valid_body_in_zero_pushes() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[]
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", ""),
        ..default()
      }]
    );
  }

  #[test]
  fn valid_body_in_multiple_empty_pushes() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[],
        &[],
        &[],
        &[],
        &[],
        &[],
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", ""),
        ..default()
      }],
    );
  }

  #[test]
  fn valid_ignore_trailing() {
    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"ord")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::all::OP_CHECKSIG)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "ord"),
        ..default()
      }],
    );
  }

  #[test]
  fn valid_ignore_preceding() {
    let script = script::Builder::new()
      .push_opcode(opcodes::all::OP_CHECKSIG)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"ord")
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "ord"),
        ..default()
      }],
    );
  }

  #[test]
  fn multiple_inscriptions_in_a_single_witness() {
    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"foo")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"bar")
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![
        ParsedEnvelope {
          payload: inscription("text/plain;charset=utf-8", "foo"),
          ..default()
        },
        ParsedEnvelope {
          payload: inscription("text/plain;charset=utf-8", "bar"),
          offset: 1,
          ..default()
        },
      ],
    );
  }

  #[test]
  fn invalid_utf8_does_not_render_inscription_invalid() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[],
        &[0b10000000]
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", [0b10000000]),
        ..default()
      },],
    );
  }

  #[test]
  fn no_endif() {
    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      Vec::new(),
    );
  }

  #[test]
  fn no_op_false() {
    let script = script::Builder::new()
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      Vec::new(),
    );
  }

  #[test]
  fn empty_envelope() {
    assert_eq!(parse(&[envelope(&[])]), Vec::new());
  }

  #[test]
  fn wrong_protocol_identifier() {
    assert_eq!(parse(&[envelope(&[b"foo"])]), Vec::new());
  }

  #[test]
  fn extract_from_transaction() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"text/plain;charset=utf-8",
        &[],
        b"ord"
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("text/plain;charset=utf-8", "ord"),
        ..default()
      }],
    );
  }

  #[test]
  fn extract_from_second_input() {
    assert_eq!(
      parse(&[Witness::new(), inscription("foo", [1; 1040]).to_witness()]),
      vec![ParsedEnvelope {
        payload: inscription("foo", [1; 1040]),
        input: 1,
        ..default()
      }]
    );
  }

  #[test]
  fn extract_from_second_envelope() {
    let mut builder = script::Builder::new();
    builder = inscription("foo", [1; 100]).append_reveal_script_to_builder(builder);
    builder = inscription("bar", [1; 100]).append_reveal_script_to_builder(builder);

    assert_eq!(
      parse(&[Witness::from_slice(&[
        builder.into_script().into_bytes(),
        Vec::new()
      ])]),
      vec![
        ParsedEnvelope {
          payload: inscription("foo", [1; 100]),
          ..default()
        },
        ParsedEnvelope {
          payload: inscription("bar", [1; 100]),
          offset: 1,
          ..default()
        }
      ]
    );
  }

  #[test]
  fn inscribe_png() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::ContentType.bytes(),
        b"image/png",
        &[],
        &[1; 100]
      ])]),
      vec![ParsedEnvelope {
        payload: inscription("image/png", [1; 100]),
        ..default()
      }]
    );
  }

  #[test]
  fn chunked_data_is_parsable() {
    let mut witness = Witness::new();

    witness.push(inscription("foo", [1; 1040]).append_reveal_script(script::Builder::new()));

    witness.push([]);

    assert_eq!(
      parse(&[witness]),
      vec![ParsedEnvelope {
        payload: inscription("foo", [1; 1040]),
        ..default()
      }]
    );
  }

  #[test]
  fn round_trip_with_no_fields() {
    let mut witness = Witness::new();

    witness.push(Inscription::default().append_reveal_script(script::Builder::new()));

    witness.push([]);

    assert_eq!(
      parse(&[witness]),
      vec![ParsedEnvelope {
        payload: Inscription::default(),
        ..default()
      }],
    );
  }

  #[test]
  fn unknown_odd_fields_are_ignored() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &Tag::Nop.bytes(), &[0]])]),
      vec![ParsedEnvelope {
        payload: Inscription::default(),
        ..default()
      }],
    );
  }

  #[test]
  fn unknown_even_fields() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &[22], &[0]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          unrecognized_even_field: true,
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn pointer_field_is_recognized() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &[2], &[1]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          pointer: Some(vec![1]),
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn duplicate_pointer_field_makes_inscription_unbound() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &[2], &[1], &[2], &[0]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          pointer: Some(vec![1]),
          duplicate_field: true,
          unrecognized_even_field: true,
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn tag_66_makes_inscriptions_unbound() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &Tag::Unbound.bytes(), &[1]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          unrecognized_even_field: true,
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn incomplete_field() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &[99]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          incomplete_field: true,
          ..default()
        },
        ..default()
      }],
    );
  }

  #[test]
  fn metadata_is_parsed_correctly() {
    assert_eq!(
      parse(&[envelope(&[&PROTOCOL_ID, &Tag::Metadata.bytes(), &[]])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          metadata: Some(Vec::new()),
          ..default()
        },
        ..default()
      }]
    );
  }

  #[test]
  fn metadata_is_parsed_correctly_from_chunks() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::Metadata.bytes(),
        &[0],
        &Tag::Metadata.bytes(),
        &[1]
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          metadata: Some(vec![0, 1]),
          duplicate_field: true,
          ..default()
        },
        ..default()
      }]
    );
  }

  #[test]
  fn properties_are_parsed_correctly() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::Properties.bytes(),
        &[1, 2, 3]
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          properties: Some(vec![1, 2, 3]),
          ..default()
        },
        ..default()
      }]
    );
  }

  #[test]
  fn properties_are_parsed_correctly_from_chunks() {
    assert_eq!(
      parse(&[envelope(&[
        &PROTOCOL_ID,
        &Tag::Properties.bytes(),
        &[0],
        &Tag::Properties.bytes(),
        &[1]
      ])]),
      vec![ParsedEnvelope {
        payload: Inscription {
          properties: Some(vec![0, 1]),
          duplicate_field: true,
          ..default()
        },
        ..default()
      }]
    );
  }

  #[test]
  fn pushnum_opcodes_are_parsed_correctly() {
    const PUSHNUMS: &[(opcodes::Opcode, u8)] = &[
      (opcodes::all::OP_PUSHNUM_NEG1, 0x81),
      (opcodes::all::OP_PUSHNUM_1, 1),
      (opcodes::all::OP_PUSHNUM_2, 2),
      (opcodes::all::OP_PUSHNUM_3, 3),
      (opcodes::all::OP_PUSHNUM_4, 4),
      (opcodes::all::OP_PUSHNUM_5, 5),
      (opcodes::all::OP_PUSHNUM_6, 6),
      (opcodes::all::OP_PUSHNUM_7, 7),
      (opcodes::all::OP_PUSHNUM_8, 8),
      (opcodes::all::OP_PUSHNUM_9, 9),
      (opcodes::all::OP_PUSHNUM_10, 10),
      (opcodes::all::OP_PUSHNUM_11, 11),
      (opcodes::all::OP_PUSHNUM_12, 12),
      (opcodes::all::OP_PUSHNUM_13, 13),
      (opcodes::all::OP_PUSHNUM_14, 14),
      (opcodes::all::OP_PUSHNUM_15, 15),
      (opcodes::all::OP_PUSHNUM_16, 16),
    ];

    for &(op, value) in PUSHNUMS {
      let script = script::Builder::new()
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(opcodes::all::OP_IF)
        .push_slice(PROTOCOL_ID)
        .push_opcode(opcodes::OP_FALSE)
        .push_opcode(op)
        .push_opcode(opcodes::all::OP_ENDIF)
        .into_script();

      assert_eq!(
        parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
        vec![ParsedEnvelope {
          payload: Inscription {
            body: Some(vec![value]),
            ..default()
          },
          pushnum: true,
          ..default()
        }],
      );
    }
  }

  #[test]
  fn stuttering() {
    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: Default::default(),
        stutter: true,
        ..default()
      }],
    );

    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: Default::default(),
        stutter: true,
        ..default()
      }],
    );

    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: Default::default(),
        stutter: true,
        ..default()
      }],
    );

    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_AND)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(PROTOCOL_ID)
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    assert_eq!(
      parse(&[Witness::from_slice(&[script.into_bytes(), Vec::new()])]),
      vec![ParsedEnvelope {
        payload: Default::default(),
        stutter: false,
        ..default()
      }],
    );
  }
}

ord/src/inscriptions/inscription.rs


use {
  super::*,
  anyhow::ensure,
  axum::http::header::HeaderValue,
  bitcoin::blockdata::opcodes,
  brotli::enc::{writer::CompressorWriter, BrotliEncoderParams},
  io::Write,
  std::str,
};

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Eq, Default)]
pub struct Inscription {
  pub body: Option<Vec<u8>>,
  pub content_encoding: Option<Vec<u8>>,
  pub content_type: Option<Vec<u8>>,
  pub delegate: Option<Vec<u8>>,
  pub duplicate_field: bool,
  pub incomplete_field: bool,
  pub metadata: Option<Vec<u8>>,
  pub metaprotocol: Option<Vec<u8>>,
  pub parents: Vec<Vec<u8>>,
  pub pointer: Option<Vec<u8>>,
  pub properties: Option<Vec<u8>>,
  pub rune: Option<Vec<u8>>,
  pub unrecognized_even_field: bool,
}

impl Inscription {
  pub fn new(
    chain: Chain,
    compress: bool,
    delegate: Option<InscriptionId>,
    metadata: Option<Vec<u8>>,
    metaprotocol: Option<String>,
    parents: Vec<InscriptionId>,
    path: Option<PathBuf>,
    pointer: Option<u64>,
    properties: Properties,
    rune: Option<Rune>,
  ) -> Result<Self, Error> {
    let path = path.as_ref();

    let (body, content_type, content_encoding) = if let Some(path) = path {
      let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?;

      let content_type = Media::content_type_for_path(path)?.0;

      let (body, content_encoding) = if compress {
        let compression_mode = Media::content_type_for_path(path)?.1;
        let mut compressed = Vec::new();

        {
          CompressorWriter::with_params(
            &mut compressed,
            body.len(),
            &BrotliEncoderParams {
              lgblock: 24,
              lgwin: 24,
              mode: compression_mode,
              quality: 11,
              size_hint: body.len(),
              ..default()
            },
          )
          .write_all(&body)?;

          let mut decompressor = brotli::Decompressor::new(compressed.as_slice(), compressed.len());

          let mut decompressed = Vec::new();

          decompressor.read_to_end(&mut decompressed)?;

          ensure!(decompressed == body, "decompression roundtrip failed");
        }

        if compressed.len() < body.len() {
          (compressed, Some("br".as_bytes().to_vec()))
        } else {
          (body, None)
        }
      } else {
        (body, None)
      };

      if let Some(limit) = chain.inscription_content_size_limit() {
        let len = body.len();
        if len > limit {
          bail!("content size of {len} bytes exceeds {limit} byte limit for {chain} inscriptions");
        }
      }

      (Some(body), Some(content_type), content_encoding)
    } else {
      (None, None, None)
    };

    Ok(Self {
      body,
      content_encoding,
      content_type: content_type.map(|content_type| content_type.into()),
      delegate: delegate.map(|delegate| delegate.value()),
      metadata,
      metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()),
      parents: parents.iter().map(|parent| parent.value()).collect(),
      pointer: pointer.map(Self::pointer_value),
      rune: rune.map(|rune| rune.commitment()),
      properties: properties.to_cbor(),
      ..default()
    })
  }

  pub fn pointer_value(pointer: u64) -> Vec<u8> {
    let mut bytes = pointer.to_le_bytes().to_vec();

    while bytes.last().copied() == Some(0) {
      bytes.pop();
    }

    bytes
  }

  pub fn append_reveal_script_to_builder(&self, mut builder: script::Builder) -> script::Builder {
    builder = builder
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(envelope::PROTOCOL_ID);

    Tag::ContentType.append(&mut builder, &self.content_type);
    Tag::ContentEncoding.append(&mut builder, &self.content_encoding);
    Tag::Metaprotocol.append(&mut builder, &self.metaprotocol);
    Tag::Parent.append_array(&mut builder, &self.parents);
    Tag::Delegate.append(&mut builder, &self.delegate);
    Tag::Pointer.append(&mut builder, &self.pointer);
    Tag::Metadata.append(&mut builder, &self.metadata);
    Tag::Rune.append(&mut builder, &self.rune);
    Tag::Properties.append(&mut builder, &self.properties);

    if let Some(body) = &self.body {
      builder = builder.push_slice(envelope::BODY_TAG);
      for chunk in body.chunks(MAX_SCRIPT_ELEMENT_SIZE) {
        builder = builder.push_slice::<&script::PushBytes>(chunk.try_into().unwrap());
      }
    }

    builder.push_opcode(opcodes::all::OP_ENDIF)
  }

  #[cfg(test)]
  pub(crate) fn append_reveal_script(&self, builder: script::Builder) -> ScriptBuf {
    self.append_reveal_script_to_builder(builder).into_script()
  }

  pub fn append_batch_reveal_script_to_builder(
    inscriptions: &[Inscription],
    mut builder: script::Builder,
  ) -> script::Builder {
    for inscription in inscriptions {
      builder = inscription.append_reveal_script_to_builder(builder);
    }

    builder
  }

  pub fn append_batch_reveal_script(
    inscriptions: &[Inscription],
    builder: script::Builder,
  ) -> ScriptBuf {
    Inscription::append_batch_reveal_script_to_builder(inscriptions, builder).into_script()
  }

  pub fn media(&self) -> Media {
    if self.body.is_none() {
      return Media::Unknown;
    }

    let Some(content_type) = self.content_type() else {
      return Media::Unknown;
    };

    content_type.parse().unwrap_or(Media::Unknown)
  }

  pub fn body(&self) -> Option<&[u8]> {
    Some(self.body.as_ref()?)
  }

  pub fn into_body(self) -> Option<Vec<u8>> {
    self.body
  }

  pub fn content_length(&self) -> Option<usize> {
    Some(self.body()?.len())
  }

  pub fn content_type(&self) -> Option<&str> {
    str::from_utf8(self.content_type.as_ref()?).ok()
  }

  pub fn content_encoding(&self) -> Option<HeaderValue> {
    HeaderValue::from_str(str::from_utf8(self.content_encoding.as_ref()?).unwrap_or_default()).ok()
  }

  pub fn delegate(&self) -> Option<InscriptionId> {
    InscriptionId::from_value(self.delegate.as_deref()?)
  }

  pub fn metadata(&self) -> Option<Value> {
    ciborium::from_reader(Cursor::new(self.metadata.as_ref()?)).ok()
  }

  pub fn metaprotocol(&self) -> Option<&str> {
    str::from_utf8(self.metaprotocol.as_ref()?).ok()
  }

  pub fn parents(&self) -> Vec<InscriptionId> {
    self
      .parents
      .iter()
      .filter_map(|parent| InscriptionId::from_value(parent))
      .collect()
  }

  pub fn pointer(&self) -> Option<u64> {
    let value = self.pointer.as_ref()?;

    if value.iter().skip(8).copied().any(|byte| byte != 0) {
      return None;
    }

    let pointer = [
      value.first().copied().unwrap_or(0),
      value.get(1).copied().unwrap_or(0),
      value.get(2).copied().unwrap_or(0),
      value.get(3).copied().unwrap_or(0),
      value.get(4).copied().unwrap_or(0),
      value.get(5).copied().unwrap_or(0),
      value.get(6).copied().unwrap_or(0),
      value.get(7).copied().unwrap_or(0),
    ];

    Some(u64::from_le_bytes(pointer))
  }

  #[cfg(test)]
  pub(crate) fn to_witness(&self) -> Witness {
    let builder = script::Builder::new();

    let script = self.append_reveal_script(builder);

    let mut witness = Witness::new();

    witness.push(script);
    witness.push([]);

    witness
  }

  pub fn hidden(&self) -> bool {
    use regex::bytes::Regex;

    const BVM_NETWORK: &[u8] = b"<body style=\"background:#F61;color:#fff;\">\
                        <h1 style=\"height:100%\">bvm.network</h1></body>";

    lazy_static! {
      static ref BRC_420: Regex = Regex::new(r"^\s*/content/[[:xdigit:]]{64}i\d+\s*$").unwrap();
    }

    self
      .body()
      .map(|body| BRC_420.is_match(body) || body.starts_with(BVM_NETWORK))
      .unwrap_or_default()
      || self.metaprotocol.is_some()
      || matches!(self.media(), Media::Code(_) | Media::Text | Media::Unknown)
  }

  pub(crate) fn gallery(&self) -> Vec<InscriptionId> {
    self
      .properties
      .as_ref()
      .map(|cbor| Properties::from_cbor(cbor))
      .unwrap_or_default()
      .gallery
  }
}

#[cfg(test)]
mod tests {
  use {super::*, std::io::Write};

  #[test]
  fn reveal_script_chunks_body() {
    assert_eq!(
      inscription("foo", [])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      7
    );

    assert_eq!(
      inscription("foo", [0; 1])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      8
    );

    assert_eq!(
      inscription("foo", [0; 520])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      8
    );

    assert_eq!(
      inscription("foo", [0; 521])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      9
    );

    assert_eq!(
      inscription("foo", [0; 1040])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      9
    );

    assert_eq!(
      inscription("foo", [0; 1041])
        .append_reveal_script(script::Builder::new())
        .instructions()
        .count(),
      10
    );
  }

  #[test]
  fn reveal_script_chunks_metadata() {
    assert_eq!(
      Inscription {
        metadata: None,
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        metadata: Some(Vec::new()),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 1]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 520]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        metadata: Some(vec![0; 521]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      8
    );
  }

  #[test]
  fn reveal_script_chunks_properties() {
    assert_eq!(
      Inscription {
        properties: None,
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        properties: Some(Vec::new()),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      4
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 1]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 520]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      6
    );

    assert_eq!(
      Inscription {
        properties: Some(vec![0; 521]),
        ..default()
      }
      .append_reveal_script(script::Builder::new())
      .instructions()
      .count(),
      8
    );
  }

  #[test]
  fn inscription_with_no_parent_field_has_no_parent() {
    assert!(Inscription {
      parents: Vec::new(),
      ..default()
    }
    .parents()
    .is_empty());
  }

  #[test]
  fn inscription_with_parent_field_shorter_than_txid_length_has_no_parent() {
    assert!(Inscription {
      parents: vec![Vec::new()],
      ..default()
    }
    .parents()
    .is_empty());
  }

  #[test]
  fn inscription_with_parent_field_longer_than_txid_and_index_has_no_parent() {
    assert!(Inscription {
      parents: vec![vec![1; 37]],
      ..default()
    }
    .parents()
    .is_empty());
  }

  #[test]
  fn inscription_with_parent_field_index_with_trailing_zeroes_and_fixed_length_has_parent() {
    let mut parent = vec![1; 36];

    parent[35] = 0;

    assert!(!Inscription {
      parents: vec![parent],
      ..default()
    }
    .parents()
    .is_empty());
  }

  #[test]
  fn inscription_with_parent_field_index_with_trailing_zeroes_and_variable_length_has_no_parent() {
    let mut parent = vec![1; 35];

    parent[34] = 0;

    assert!(Inscription {
      parents: vec![parent],
      ..default()
    }
    .parents()
    .is_empty());
  }

  #[test]
  fn inscription_delegate_txid_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        delegate: Some(vec![
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
          0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
          0x1e, 0x1f,
        ]),
        ..default()
      }
      .delegate()
      .unwrap()
      .txid,
      "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
        .parse()
        .unwrap()
    );
  }

  #[test]
  fn inscription_parent_txid_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
          0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
          0x1e, 0x1f,
        ]],
        ..default()
      }
      .parents(),
      [
        "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100i0"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_zero_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![1; 32]],
        ..default()
      }
      .parents(),
      [
        "0101010101010101010101010101010101010101010101010101010101010101i0"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_one_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi1"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_two_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi513"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_three_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02, 0x03
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi197121"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_with_four_byte_index_field_is_deserialized_correctly() {
    assert_eq!(
      Inscription {
        parents: vec![vec![
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
          0xff, 0xff, 0x01, 0x02, 0x03, 0x04,
        ]],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn inscription_parent_returns_multiple_parents() {
    assert_eq!(
      Inscription {
        parents: vec![
          vec![
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0x01, 0x02, 0x03, 0x04,
          ],
          vec![
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
            0xff, 0xff, 0xff, 0xff, 0x00, 0x02, 0x03, 0x04,
          ]
        ],
        ..default()
      }
      .parents(),
      [
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305985"
          .parse()
          .unwrap(),
        "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffi67305984"
          .parse()
          .unwrap()
      ],
    );
  }

  #[test]
  fn metadata_function_decodes_metadata() {
    assert_eq!(
      Inscription {
        metadata: Some(vec![0x44, 0, 1, 2, 3]),
        ..default()
      }
      .metadata()
      .unwrap(),
      Value::Bytes(vec![0, 1, 2, 3]),
    );
  }

  #[test]
  fn metadata_function_returns_none_if_no_metadata() {
    assert_eq!(
      Inscription {
        metadata: None,
        ..default()
      }
      .metadata(),
      None,
    );
  }

  #[test]
  fn metadata_function_returns_none_if_metadata_fails_to_parse() {
    assert_eq!(
      Inscription {
        metadata: Some(vec![0x44]),
        ..default()
      }
      .metadata(),
      None,
    );
  }

  #[test]
  fn pointer_decode() {
    assert_eq!(
      Inscription {
        pointer: None,
        ..default()
      }
      .pointer(),
      None
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![0]),
        ..default()
      }
      .pointer(),
      Some(0),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8]),
        ..default()
      }
      .pointer(),
      Some(0x0807060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6]),
        ..default()
      }
      .pointer(),
      Some(0x0000060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0]),
        ..default()
      }
      .pointer(),
      Some(0x0807060504030201),
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 1]),
        ..default()
      }
      .pointer(),
      None,
    );
    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 1]),
        ..default()
      }
      .pointer(),
      None,
    );
  }

  #[test]
  fn pointer_encode() {
    assert_eq!(
      Inscription {
        pointer: None,
        ..default()
      }
      .to_witness(),
      envelope(&[b"ord"]),
    );

    assert_eq!(
      Inscription {
        pointer: Some(vec![1, 2, 3]),
        ..default()
      }
      .to_witness(),
      envelope(&[b"ord", &[2], &[1, 2, 3]]),
    );
  }

  #[test]
  fn pointer_value() {
    let mut file = tempfile::Builder::new().suffix(".txt").tempfile().unwrap();

    write!(file, "foo").unwrap();

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      None,
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, None);

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(0),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(Vec::new()));

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(1),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(vec![1]));

    let inscription = Inscription::new(
      Chain::Mainnet,
      false,
      None,
      None,
      None,
      Vec::new(),
      Some(file.path().to_path_buf()),
      Some(256),
      Properties::default(),
      None,
    )
    .unwrap();

    assert_eq!(inscription.pointer, Some(vec![0, 1]));
  }

  #[test]
  fn hidden() {
    #[track_caller]
    fn case(content_type: Option<&str>, body: Option<&str>, expected: bool) {
      assert_eq!(
        Inscription {
          content_type: content_type.map(|content_type| content_type.as_bytes().into()),
          body: body.map(|content_type| content_type.as_bytes().into()),
          ..default()
        }
        .hidden(),
        expected
      );
    }

    case(None, None, true);
    case(Some("foo"), Some(""), true);
    case(Some("text/plain"), None, true);
    case(
      Some("text/plain"),
      Some("The fox jumped. The cow danced."),
      true,
    );
    case(Some("text/plain;charset=utf-8"), Some("foo"), true);
    case(Some("text/plain;charset=cn-big5"), Some("foo"), true);
    case(Some("application/json"), Some("foo"), true);
    case(
      Some("text/markdown"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(
      Some("text/html"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(Some("application/yaml"), Some(""), true);
    case(
      Some("text/html;charset=utf-8"),
      Some("/content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0"),
      true,
    );
    case(
      Some("text/html"),
      Some("  /content/09a8d837ec0bcaec668ecf405e696a16bee5990863659c224ff888fb6f8f45e7i0  \n"),
      true,
    );
    case(
      Some("text/html"),
      Some(
        r#"<body style="background:#F61;color:#fff;"><h1 style="height:100%">bvm.network</h1></body>"#,
      ),
      true,
    );
    case(
      Some("text/html"),
      Some(
        r#"<body style="background:#F61;color:#fff;"><h1 style="height:100%">bvm.network</h1></body>foo"#,
      ),
      true,
    );

    assert!(Inscription {
      content_type: Some("text/plain".as_bytes().into()),
      body: Some(b"{\xc3\x28}".as_slice().into()),
      ..default()
    }
    .hidden());

    assert!(Inscription {
      content_type: Some("text/html".as_bytes().into()),
      body: Some("hello".as_bytes().into()),
      metaprotocol: Some(Vec::new()),
      ..default()
    }
    .hidden());
  }
}

ord/src/inscriptions/inscription_id.rs


use super::*;

#[derive(
  Debug, PartialEq, Copy, Clone, Hash, Eq, PartialOrd, Ord, DeserializeFromStr, SerializeDisplay,
)]
pub struct InscriptionId {
  pub txid: Txid,
  pub index: u32,
}

impl Default for InscriptionId {
  fn default() -> Self {
    Self {
      txid: Txid::all_zeros(),
      index: 0,
    }
  }
}

impl InscriptionId {
  pub(crate) fn from_value(value: &[u8]) -> Option<Self> {
    if value.len() < Txid::LEN {
      return None;
    }

    if value.len() > Txid::LEN + 4 {
      return None;
    }

    let (txid, index) = value.split_at(Txid::LEN);

    if let Some(last) = index.last() {
      // Accept fixed length encoding with 4 bytes (with potential trailing zeroes)
      // or variable length (no trailing zeroes)
      if index.len() != 4 && *last == 0 {
        return None;
      }
    }

    let txid = Txid::from_slice(txid).unwrap();

    let index = [
      index.first().copied().unwrap_or_default(),
      index.get(1).copied().unwrap_or_default(),
      index.get(2).copied().unwrap_or_default(),
      index.get(3).copied().unwrap_or_default(),
    ];

    let index = u32::from_le_bytes(index);

    Some(Self { txid, index })
  }

  pub(crate) fn value(self) -> Vec<u8> {
    let index = self.index.to_le_bytes();
    let mut index_slice = index.as_slice();

    while index_slice.last().copied() == Some(0) {
      index_slice = &index_slice[0..index_slice.len() - 1];
    }

    self
      .txid
      .to_byte_array()
      .iter()
      .chain(index_slice)
      .copied()
      .collect()
  }
}

impl Display for InscriptionId {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(f, "{}i{}", self.txid, self.index)
  }
}

#[derive(Debug)]
pub enum ParseError {
  Character(char),
  Length(usize),
  Separator(char),
  Txid(bitcoin::hex::HexToArrayError),
  Index(std::num::ParseIntError),
}

impl Display for ParseError {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Character(c) => write!(f, "invalid character: '{c}'"),
      Self::Length(len) => write!(f, "invalid length: {len}"),
      Self::Separator(c) => write!(f, "invalid separator: `{c}`"),
      Self::Txid(err) => write!(f, "invalid txid: {err}"),
      Self::Index(err) => write!(f, "invalid index: {err}"),
    }
  }
}

impl std::error::Error for ParseError {}

impl FromStr for InscriptionId {
  type Err = ParseError;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    if let Some(char) = s.chars().find(|char| !char.is_ascii()) {
      return Err(ParseError::Character(char));
    }

    const TXID_LEN: usize = 64;
    const MIN_LEN: usize = TXID_LEN + 2;

    if s.len() < MIN_LEN {
      return Err(ParseError::Length(s.len()));
    }

    let txid = &s[..TXID_LEN];

    let separator = s.chars().nth(TXID_LEN).unwrap();

    if separator != 'i' {
      return Err(ParseError::Separator(separator));
    }

    let vout = &s[TXID_LEN + 1..];

    Ok(Self {
      txid: txid.parse().map_err(ParseError::Txid)?,
      index: vout.parse().map_err(ParseError::Index)?,
    })
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn display() {
    assert_eq!(
      inscription_id(1).to_string(),
      "1111111111111111111111111111111111111111111111111111111111111111i1",
    );
    assert_eq!(
      InscriptionId {
        txid: txid(1),
        index: 0,
      }
      .to_string(),
      "1111111111111111111111111111111111111111111111111111111111111111i0",
    );
    assert_eq!(
      InscriptionId {
        txid: txid(1),
        index: 0xFFFFFFFF,
      }
      .to_string(),
      "1111111111111111111111111111111111111111111111111111111111111111i4294967295",
    );
  }

  #[test]
  fn from_str() {
    assert_eq!(
      "1111111111111111111111111111111111111111111111111111111111111111i1"
        .parse::<InscriptionId>()
        .unwrap(),
      inscription_id(1),
    );
    assert_eq!(
      "1111111111111111111111111111111111111111111111111111111111111111i4294967295"
        .parse::<InscriptionId>()
        .unwrap(),
      InscriptionId {
        txid: txid(1),
        index: 0xFFFFFFFF,
      },
    );
    assert_eq!(
      "1111111111111111111111111111111111111111111111111111111111111111i4294967295"
        .parse::<InscriptionId>()
        .unwrap(),
      InscriptionId {
        txid: txid(1),
        index: 0xFFFFFFFF,
      },
    );
  }

  #[test]
  fn from_str_bad_character() {
    assert_matches!(
      "→".parse::<InscriptionId>(),
      Err(ParseError::Character('→')),
    );
  }

  #[test]
  fn from_str_bad_length() {
    assert_matches!("foo".parse::<InscriptionId>(), Err(ParseError::Length(3)));
  }

  #[test]
  fn from_str_bad_separator() {
    assert_matches!(
      "0000000000000000000000000000000000000000000000000000000000000000x0".parse::<InscriptionId>(),
      Err(ParseError::Separator('x')),
    );
  }

  #[test]
  fn from_str_bad_index() {
    assert_matches!(
      "0000000000000000000000000000000000000000000000000000000000000000ifoo"
        .parse::<InscriptionId>(),
      Err(ParseError::Index(_)),
    );
  }

  #[test]
  fn from_str_bad_txid() {
    assert_matches!(
      "x000000000000000000000000000000000000000000000000000000000000000i0".parse::<InscriptionId>(),
      Err(ParseError::Txid(_)),
    );
  }
}

ord/src/inscriptions/media.rs


use {
  self::{ImageRendering::*, Language::*, Media::*},
  super::*,
  brotli::enc::backward_references::BrotliEncoderMode::{
    self, BROTLI_MODE_FONT as FONT, BROTLI_MODE_GENERIC as GENERIC, BROTLI_MODE_TEXT as TEXT,
  },
  mp4::{MediaType, Mp4Reader, TrackType},
};

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Media {
  Audio,
  Code(Language),
  Font,
  Iframe,
  Image(ImageRendering),
  Markdown,
  Model,
  Pdf,
  Text,
  Unknown,
  Video,
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Language {
  Css,
  JavaScript,
  Json,
  Python,
  Yaml,
}

impl Display for Language {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(
      f,
      "{}",
      match self {
        Self::Css => "css",
        Self::JavaScript => "javascript",
        Self::Json => "json",
        Self::Python => "python",
        Self::Yaml => "yaml",
      }
    )
  }
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum ImageRendering {
  Auto,
  Pixelated,
}

impl Display for ImageRendering {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(
      f,
      "{}",
      match self {
        Self::Auto => "auto",
        Self::Pixelated => "pixelated",
      }
    )
  }
}

impl Media {
  #[rustfmt::skip]
  const TABLE: &'static [(&'static str, BrotliEncoderMode, Media, &'static [&'static str])] = &[
    ("application/cbor",            GENERIC, Unknown,          &["cbor"]),
    ("application/json",            TEXT,    Code(Json),       &["json"]),
    ("application/octet-stream",    GENERIC, Unknown,          &["bin"]),
    ("application/pdf",             GENERIC, Pdf,              &["pdf"]),
    ("application/pgp-signature",   TEXT,    Text,             &["asc"]),
    ("application/protobuf",        GENERIC, Unknown,          &["binpb"]),
    ("application/x-bittorrent",    GENERIC, Unknown,          &["torrent"]),
    ("application/x-javascript",    TEXT,    Code(JavaScript), &[]),
    ("application/yaml",            TEXT,    Code(Yaml),       &["yaml", "yml"]),
    ("audio/flac",                  GENERIC, Audio,            &["flac"]),
    ("audio/mpeg",                  GENERIC, Audio,            &["mp3"]),
    ("audio/ogg",                   GENERIC, Audio,            &[]),
    ("audio/ogg;codecs=opus",       GENERIC, Audio,            &["opus"]),
    ("audio/wav",                   GENERIC, Audio,            &["wav"]),
    ("font/otf",                    GENERIC, Font,             &["otf"]),
    ("font/ttf",                    GENERIC, Font,             &["ttf"]),
    ("font/woff",                   GENERIC, Font,             &["woff"]),
    ("font/woff2",                  FONT,    Font,             &["woff2"]),
    ("image/apng",                  GENERIC, Image(Pixelated), &["apng"]),
    ("image/avif",                  GENERIC, Image(Auto),      &["avif"]),
    ("image/gif",                   GENERIC, Image(Pixelated), &["gif"]),
    ("image/jpeg",                  GENERIC, Image(Pixelated), &["jpg", "jpeg"]),
    ("image/jxl",                   GENERIC, Image(Auto),      &["jxl"]),
    ("image/png",                   GENERIC, Image(Pixelated), &["png"]),
    ("image/svg+xml",               TEXT,    Iframe,           &["svg"]),
    ("image/webp",                  GENERIC, Image(Pixelated), &["webp"]),
    ("model/gltf+json",             TEXT,    Model,            &["gltf"]),
    ("model/gltf-binary",           GENERIC, Model,            &["glb"]),
    ("model/stl",                   GENERIC, Unknown,          &["stl"]),
    ("text/css",                    TEXT,    Code(Css),        &["css"]),
    ("text/html",                   TEXT,    Iframe,           &[]),
    ("text/html;charset=utf-8",     TEXT,    Iframe,           &["html"]),
    ("text/javascript",             TEXT,    Code(JavaScript), &["js", "mjs"]),
    ("text/markdown",               TEXT,    Markdown,         &[]),
    ("text/markdown;charset=utf-8", TEXT,    Markdown,         &["md"]),
    ("text/plain",                  TEXT,    Text,             &[]),
    ("text/plain;charset=utf-8",    TEXT,    Text,             &["txt"]),
    ("text/x-python",               TEXT,    Code(Python),     &["py"]),
    ("video/mp4",                   GENERIC, Video,            &["mp4"]),
    ("video/webm",                  GENERIC, Video,            &["webm"]),
  ];

  pub(crate) fn content_type_for_path(
    path: &Path,
  ) -> Result<(&'static str, BrotliEncoderMode), Error> {
    let extension = path
      .extension()
      .ok_or_else(|| anyhow!("file must have extension"))?
      .to_str()
      .ok_or_else(|| anyhow!("unrecognized extension"))?;

    let extension = extension.to_lowercase();

    if extension == "mp4" {
      Media::check_mp4_codec(path)?;
    }

    for (content_type, mode, _, extensions) in Self::TABLE {
      if extensions.contains(&extension.as_str()) {
        return Ok((*content_type, *mode));
      }
    }

    let mut extensions = Self::TABLE
      .iter()
      .flat_map(|(_, _, _, extensions)| extensions.first().cloned())
      .collect::<Vec<&str>>();

    extensions.sort();

    Err(anyhow!(
      "unsupported file extension `.{extension}`, supported extensions: {}",
      extensions.join(" "),
    ))
  }

  pub(crate) fn check_mp4_codec(path: &Path) -> Result<(), Error> {
    let f = File::open(path)?;
    let size = f.metadata()?.len();
    let reader = BufReader::new(f);

    let mp4 = Mp4Reader::read_header(reader, size)?;

    for track in mp4.tracks().values() {
      if let TrackType::Video = track.track_type()? {
        let media_type = track.media_type()?;
        if media_type != MediaType::H264 {
          return Err(anyhow!(
            "Unsupported video codec, only H.264 is supported in MP4: {media_type}"
          ));
        }
      }
    }

    Ok(())
  }
}

impl FromStr for Media {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    for entry in Self::TABLE {
      if entry.0 == s {
        return Ok(entry.2);
      }
    }

    Err(anyhow!("unknown content type: {s}"))
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn for_extension() {
    assert_eq!(
      Media::content_type_for_path(Path::new("pepe.jpg")).unwrap(),
      ("image/jpeg", BrotliEncoderMode::BROTLI_MODE_GENERIC)
    );
    assert_eq!(
      Media::content_type_for_path(Path::new("pepe.jpeg")).unwrap(),
      ("image/jpeg", BrotliEncoderMode::BROTLI_MODE_GENERIC)
    );
    assert_eq!(
      Media::content_type_for_path(Path::new("pepe.JPG")).unwrap(),
      ("image/jpeg", BrotliEncoderMode::BROTLI_MODE_GENERIC)
    );
    assert_eq!(
      Media::content_type_for_path(Path::new("pepe.txt")).unwrap(),
      (
        "text/plain;charset=utf-8",
        BrotliEncoderMode::BROTLI_MODE_TEXT
      )
    );
    assert_regex_match!(
      Media::content_type_for_path(Path::new("pepe.foo")).unwrap_err(),
      r"unsupported file extension `\.foo`, supported extensions: apng .*"
    );
  }

  #[test]
  fn h264_in_mp4_is_allowed() {
    assert!(Media::check_mp4_codec(Path::new("examples/h264.mp4")).is_ok(),);
  }

  #[test]
  fn av1_in_mp4_is_rejected() {
    assert!(Media::check_mp4_codec(Path::new("examples/av1.mp4")).is_err(),);
  }

  #[test]
  fn no_duplicate_extensions() {
    let mut set = HashSet::new();
    for (_, _, _, extensions) in Media::TABLE {
      for extension in *extensions {
        assert!(set.insert(extension), "duplicate extension `{extension}`");
      }
    }
  }
}

ord/src/inscriptions/tag.rs


use super::*;

#[derive(Copy, Clone)]
#[repr(u8)]
pub(crate) enum Tag {
  Pointer = 2,
  #[allow(unused)]
  Unbound = 66,

  ContentType = 1,
  Parent = 3,
  Metadata = 5,
  Metaprotocol = 7,
  ContentEncoding = 9,
  Delegate = 11,
  Rune = 13,
  #[allow(unused)]
  Note = 15,
  Properties = 17,
  #[allow(unused)]
  Nop = 255,
}

impl Tag {
  fn chunked(self) -> bool {
    matches!(self, Self::Metadata | Self::Properties)
  }

  pub(crate) fn bytes(self) -> [u8; 1] {
    [self as u8]
  }

  pub(crate) fn append(self, builder: &mut script::Builder, value: &Option<Vec<u8>>) {
    if let Some(value) = value {
      let mut tmp = script::Builder::new();
      mem::swap(&mut tmp, builder);

      if self.chunked() {
        for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) {
          tmp = tmp
            .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap())
            .push_slice::<&script::PushBytes>(chunk.try_into().unwrap());
        }
      } else {
        tmp = tmp
          .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap())
          .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap());
      }

      mem::swap(&mut tmp, builder);
    }
  }

  pub(crate) fn append_array(self, builder: &mut script::Builder, values: &Vec<Vec<u8>>) {
    let mut tmp = script::Builder::new();
    mem::swap(&mut tmp, builder);

    for value in values {
      tmp = tmp
        .push_slice::<&script::PushBytes>(self.bytes().as_slice().try_into().unwrap())
        .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap());
    }

    mem::swap(&mut tmp, builder);
  }

  pub(crate) fn take(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Option<Vec<u8>> {
    if self.chunked() {
      let value = fields.remove(self.bytes().as_slice())?;

      if value.is_empty() {
        None
      } else {
        Some(value.into_iter().flatten().cloned().collect())
      }
    } else {
      let values = fields.get_mut(self.bytes().as_slice())?;

      if values.is_empty() {
        None
      } else {
        let value = values.remove(0).to_vec();

        if values.is_empty() {
          fields.remove(self.bytes().as_slice());
        }

        Some(value)
      }
    }
  }

  pub(crate) fn take_array(self, fields: &mut BTreeMap<&[u8], Vec<&[u8]>>) -> Vec<Vec<u8>> {
    fields
      .remove(self.bytes().as_slice())
      .unwrap_or_default()
      .into_iter()
      .map(|v| v.to_vec())
      .collect()
  }
}

ord/src/inscriptions/teleburn.rs


use {super::*, sha3::Digest, sha3::Keccak256};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Ethereum(String);

impl From<InscriptionId> for Ethereum {
  fn from(inscription_id: InscriptionId) -> Self {
    let mut array = [0; 36];
    let (txid, index) = array.split_at_mut(32);
    txid.copy_from_slice(inscription_id.txid.as_ref());
    index.copy_from_slice(&inscription_id.index.to_be_bytes());
    let digest = bitcoin::hashes::sha256::Hash::hash(&array);
    Self(create_address_with_checksum(&hex::encode(&digest[0..20])))
  }
}

impl Display for Ethereum {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    write!(f, "{}", self.0)
  }
}

/// Given the hex digits of an Ethereum address, return that address with a
/// checksum as per https://eips.ethereum.org/EIPS/eip-55
fn create_address_with_checksum(address: &str) -> String {
  assert_eq!(address.len(), 40);
  assert!(address
    .chars()
    .all(|c| c.is_ascii_hexdigit() && (!c.is_alphabetic() || c.is_lowercase())));

  let hash = hex::encode(&Keccak256::digest(address.as_bytes())[..20]);
  assert_eq!(hash.len(), 40);

  "0x"
    .chars()
    .chain(address.chars().zip(hash.chars()).map(|(a, h)| match h {
      '0'..='7' => a,
      '8'..='9' | 'a'..='f' => a.to_ascii_uppercase(),
      _ => unreachable!(),
    }))
    .collect()
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_eth_checksum_generation() {
    // test addresses from https://eips.ethereum.org/EIPS/eip-55
    for addr in &[
      "0x27b1fdb04752bbc536007a920d24acb045561c26",
      "0x52908400098527886E0F7030069857D2E4169EE7",
      "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
      "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
      "0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb",
      "0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB",
      "0xde709f2102306220921060314715629080e2fb77",
      "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
    ] {
      let lowercased = String::from(&addr[2..]).to_ascii_lowercase();
      assert_eq!(addr.to_string(), create_address_with_checksum(&lowercased));
    }
  }

  #[test]
  fn test_inscription_id_to_teleburn_address() {
    for (inscription_id, addr) in &[
      (
        InscriptionId {
          txid: Txid::all_zeros(),
          index: 0,
        },
        "0x6db65fD59fd356F6729140571B5BCd6bB3b83492",
      ),
      (
        InscriptionId::from_str(
          "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i7",
        )
        .unwrap(),
        "0xEb26fEFA572a25F0ED7B41C5249bCba2Ca976475",
      ),
      (
        InscriptionId::from_str(
          "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0",
        )
        .unwrap(),
        "0xe43A06530BdF8A4e067581f48Fae3b535559dA9e",
      ),
    ] {
      assert_eq!(*addr, Ethereum::from(*inscription_id).0);
    }
  }
}

ord/src/into_u64.rs


pub(crate) trait IntoU64 {
  fn into_u64(self) -> u64;
}

impl IntoU64 for usize {
  fn into_u64(self) -> u64 {
    self.try_into().unwrap()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn into_u64() {
    usize::MAX.into_u64();
  }
}

ord/src/into_usize.rs


pub(crate) trait IntoUsize {
  fn into_usize(self) -> usize;
}

impl IntoUsize for u32 {
  fn into_usize(self) -> usize {
    self.try_into().unwrap()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn into_usize() {
    u32::MAX.into_usize();
  }
}

ord/src/lib.rs


#![allow(
  clippy::large_enum_variant,
  clippy::result_large_err,
  clippy::too_many_arguments,
  clippy::type_complexity
)]
#![deny(
  clippy::cast_lossless,
  clippy::cast_possible_truncation,
  clippy::cast_possible_wrap,
  clippy::cast_sign_loss
)]

use {
  self::{
    arguments::Arguments,
    blocktime::Blocktime,
    decimal::Decimal,
    deserialize_from_str::DeserializeFromStr,
    index::BitcoinCoreRpcResultExt,
    inscriptions::{
      inscription_id,
      media::{self, ImageRendering, Media},
      teleburn,
    },
    into_u64::IntoU64,
    into_usize::IntoUsize,
    option_ext::OptionExt,
    outgoing::Outgoing,
    properties::Properties,
    representation::Representation,
    satscard::Satscard,
    settings::Settings,
    signer::Signer,
    subcommand::{OutputFormat, Subcommand, SubcommandResult},
    tally::Tally,
  },
  anyhow::{anyhow, bail, ensure, Context, Error},
  bip39::Mnemonic,
  bitcoin::{
    address::{Address, NetworkUnchecked},
    blockdata::{
      constants::{DIFFCHANGE_INTERVAL, MAX_SCRIPT_ELEMENT_SIZE, SUBSIDY_HALVING_INTERVAL},
      locktime::absolute::LockTime,
    },
    consensus::{self, Decodable, Encodable},
    hash_types::{BlockHash, TxMerkleNode},
    hashes::Hash,
    policy::MAX_STANDARD_TX_WEIGHT,
    script, secp256k1,
    transaction::Version,
    Amount, Block, KnownHrp, Network, OutPoint, Script, ScriptBuf, Sequence, SignedAmount,
    Transaction, TxIn, TxOut, Txid, Witness,
  },
  bitcoincore_rpc::{Client, RpcApi},
  chrono::{DateTime, TimeZone, Utc},
  ciborium::Value,
  clap::{ArgGroup, Parser},
  error::{ResultExt, SnafuError},
  html_escaper::{Escape, Trusted},
  lazy_static::lazy_static,
  ordinals::{
    varint, Artifact, Charm, Edict, Epoch, Etching, Height, Pile, Rarity, Rune, RuneId, Runestone,
    Sat, SatPoint, SpacedRune, Terms,
  },
  regex::Regex,
  reqwest::{header::HeaderMap, StatusCode, Url},
  serde::{Deserialize, Deserializer, Serialize},
  serde_with::{DeserializeFromStr, SerializeDisplay},
  snafu::{Backtrace, ErrorCompat, Snafu},
  std::{
    backtrace::BacktraceStatus,
    cmp,
    collections::{BTreeMap, BTreeSet, HashSet},
    env,
    ffi::OsString,
    fmt::{self, Display, Formatter},
    fs::{self, File},
    io::{self, BufReader, Cursor, Read},
    mem,
    net::ToSocketAddrs,
    path::{Path, PathBuf},
    process::{self, Command, Stdio},
    str::FromStr,
    sync::{
      atomic::{self, AtomicBool},
      Arc, LazyLock, Mutex,
    },
    thread,
    time::{Duration, Instant, SystemTime},
  },
  sysinfo::System,
  tokio::{runtime::Runtime, task},
};

pub use self::{
  chain::Chain,
  fee_rate::FeeRate,
  index::{Index, RuneEntry},
  inscriptions::{Envelope, Inscription, InscriptionId, ParsedEnvelope, RawEnvelope},
  object::Object,
  options::Options,
  wallet::transaction_builder::{Target, TransactionBuilder},
};

#[cfg(test)]
#[macro_use]
mod test;

#[cfg(test)]
use self::test::*;

pub mod api;
pub mod arguments;
mod blocktime;
pub mod chain;
pub mod decimal;
mod deserialize_from_str;
mod error;
mod fee_rate;
pub mod index;
mod inscriptions;
mod into_u64;
mod into_usize;
mod macros;
mod object;
mod option_ext;
pub mod options;
pub mod outgoing;
mod properties;
mod re;
mod representation;
pub mod runes;
mod satscard;
pub mod settings;
mod signer;
pub mod subcommand;
mod tally;
pub mod templates;
pub mod wallet;

type Result<T = (), E = Error> = std::result::Result<T, E>;
type SnafuResult<T = (), E = SnafuError> = std::result::Result<T, E>;

const MAX_STANDARD_OP_RETURN_SIZE: usize = 83;
const TARGET_POSTAGE: Amount = Amount::from_sat(10_000);

static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false);
static LISTENERS: Mutex<Vec<axum_server::Handle>> = Mutex::new(Vec::new());
static INDEXER: Mutex<Option<thread::JoinHandle<()>>> = Mutex::new(None);

#[doc(hidden)]
#[derive(Deserialize, Serialize)]
pub struct SimulateRawTransactionResult {
  #[serde(with = "bitcoin::amount::serde::as_btc")]
  pub balance_change: SignedAmount,
}

#[doc(hidden)]
#[derive(Deserialize, Serialize)]
pub struct SimulateRawTransactionOptions {
  include_watchonly: bool,
}

#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn fund_raw_transaction(
  client: &Client,
  fee_rate: FeeRate,
  unfunded_transaction: &Transaction,
) -> Result<Vec<u8>> {
  let mut buffer = Vec::new();

  {
    unfunded_transaction.version.consensus_encode(&mut buffer)?;
    unfunded_transaction.input.consensus_encode(&mut buffer)?;
    unfunded_transaction.output.consensus_encode(&mut buffer)?;
    unfunded_transaction
      .lock_time
      .consensus_encode(&mut buffer)?;
  }

  Ok(
    client
      .fund_raw_transaction(
        &buffer,
        Some(&bitcoincore_rpc::json::FundRawTransactionOptions {
          // NB. This is `fundrawtransaction`'s `feeRate`, which is fee per kvB
          // and *not* fee per vB. So, we multiply the fee rate given by the user
          // by 1000.
          fee_rate: Some(Amount::from_sat((fee_rate.n() * 1000.0).ceil() as u64)),
          change_position: Some(unfunded_transaction.output.len().try_into()?),
          ..default()
        }),
        Some(false),
      )
      .map_err(|err| {
        if matches!(
          err,
          bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(
            bitcoincore_rpc::jsonrpc::error::RpcError { code: -6, .. }
          ))
        ) {
          anyhow!("not enough cardinal utxos")
        } else {
          err.into()
        }
      })?
      .hex,
  )
}

pub fn timestamp(seconds: u64) -> DateTime<Utc> {
  Utc
    .timestamp_opt(seconds.try_into().unwrap_or(i64::MAX), 0)
    .unwrap()
}

fn target_as_block_hash(target: bitcoin::Target) -> BlockHash {
  BlockHash::from_raw_hash(Hash::from_byte_array(target.to_le_bytes()))
}

pub fn unbound_outpoint() -> OutPoint {
  OutPoint {
    txid: Hash::all_zeros(),
    vout: 0,
  }
}

fn uncheck(address: &Address) -> Address<NetworkUnchecked> {
  address.to_string().parse().unwrap()
}

pub fn base64_encode(data: &[u8]) -> String {
  use base64::Engine;
  base64::engine::general_purpose::STANDARD.encode(data)
}

pub fn base64_decode(s: &str) -> Result<Vec<u8>> {
  use base64::Engine;
  Ok(base64::engine::general_purpose::STANDARD.decode(s)?)
}

fn default<T: Default>() -> T {
  Default::default()
}

pub fn parse_ord_server_args(args: &str) -> (Settings, subcommand::server::Server) {
  match Arguments::try_parse_from(args.split_whitespace()) {
    Ok(arguments) => match arguments.subcommand {
      Subcommand::Server(server) => (
        Settings::merge(
          arguments.options,
          vec![("INTEGRATION_TEST".into(), "1".into())]
            .into_iter()
            .collect(),
        )
        .unwrap(),
        server,
      ),
      subcommand => panic!("unexpected subcommand: {subcommand:?}"),
    },
    Err(err) => panic!("error parsing arguments: {err}"),
  }
}

pub fn cancel_shutdown() {
  SHUTTING_DOWN.store(false, atomic::Ordering::Relaxed);
}

pub fn shut_down() {
  SHUTTING_DOWN.store(true, atomic::Ordering::Relaxed);
}

fn gracefully_shut_down_indexer() {
  if let Some(indexer) = INDEXER.lock().unwrap().take() {
    shut_down();
    log::info!("Waiting for index thread to finish...");
    if indexer.join().is_err() {
      log::warn!("Index thread panicked; join failed");
    }
  }
}

pub fn main() {
  env_logger::init();

  ctrlc::set_handler(move || {
    if SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Relaxed) {
      process::exit(1);
    }

    eprintln!("Shutting down gracefully. Press <CTRL-C> again to shutdown immediately.");

    LISTENERS
      .lock()
      .unwrap()
      .iter()
      .for_each(|handle| handle.graceful_shutdown(Some(Duration::from_millis(100))));

    gracefully_shut_down_indexer();
  })
  .expect("Error setting <CTRL-C> handler");

  let args = Arguments::parse();

  let format = args.options.format;

  match args.run() {
    Err(err) => {
      eprintln!("error: {err}");

      if let SnafuError::Anyhow { err } = err {
        for (i, err) in err.chain().skip(1).enumerate() {
          if i == 0 {
            eprintln!();
            eprintln!("because:");
          }

          eprintln!("- {err}");
        }

        if env::var_os("RUST_BACKTRACE")
          .map(|val| val == "1")
          .unwrap_or_default()
        {
          eprintln!("{}", err.backtrace());
        }
      } else {
        for (i, err) in err.iter_chain().skip(1).enumerate() {
          if i == 0 {
            eprintln!();
            eprintln!("because:");
          }

          eprintln!("- {err}");
        }

        if let Some(backtrace) = err.backtrace() {
          if backtrace.status() == BacktraceStatus::Captured {
            eprintln!("backtrace:");
            eprintln!("{backtrace}");
          }
        }
      }

      gracefully_shut_down_indexer();

      process::exit(1);
    }
    Ok(output) => {
      if let Some(output) = output {
        output.print(format.unwrap_or_default());
      }
      gracefully_shut_down_indexer();
    }
  }
}

ord/src/macros.rs


#[macro_export]
macro_rules! define_table {
  ($name:ident, $key:ty, $value:ty) => {
    const $name: TableDefinition<$key, $value> = TableDefinition::new(stringify!($name));
  };
}

#[macro_export]
macro_rules! define_multimap_table {
  ($name:ident, $key:ty, $value:ty) => {
    const $name: MultimapTableDefinition<$key, $value> =
      MultimapTableDefinition::new(stringify!($name));
  };
}

#[macro_export]
macro_rules! tprintln {
  ($($arg:tt)*) => {
    if cfg!(test) {
      eprint!("==> ");
      eprintln!($($arg)*);
    }
  };
}

#[macro_export]
macro_rules! assert_regex_match {
  ($value:expr, $pattern:expr $(,)?) => {
    let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap();
    let string = $value.to_string();

    if !regex.is_match(string.as_ref()) {
      eprintln!("Regex did not match:");
      pretty_assert_eq!(regex.as_str(), string);
    }
  };
}

#[macro_export]
macro_rules! assert_matches {
  ($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
    match $expression {
      $( $pattern )|+ $( if $guard )? => {}
      left => panic!(
        "assertion failed: (left ~= right)\n  left: `{:?}`\n right: `{}`",
        left,
        stringify!($($pattern)|+ $(if $guard)?)
      ),
    }
  }
}

ord/src/object.rs


use super::*;

#[derive(Debug, PartialEq, Clone, DeserializeFromStr, SerializeDisplay)]
pub enum Object {
  Address(Address<NetworkUnchecked>),
  Hash([u8; 32]),
  InscriptionId(InscriptionId),
  Integer(u128),
  OutPoint(OutPoint),
  Rune(SpacedRune),
  Sat(Sat),
  SatPoint(SatPoint),
}

impl FromStr for Object {
  type Err = SnafuError;

  fn from_str(input: &str) -> Result<Self, Self::Err> {
    use Representation::*;

    match input.parse::<Representation>()? {
      Address => Ok(Self::Address(
        input.parse().snafu_context(error::AddressParse { input })?,
      )),
      Decimal | Degree | Percentile | Name => Ok(Self::Sat(
        input.parse().snafu_context(error::SatParse { input })?,
      )),
      Hash => Ok(Self::Hash(
        bitcoin::hashes::sha256::Hash::from_str(input)
          .snafu_context(error::HashParse { input })?
          .to_byte_array(),
      )),
      InscriptionId => Ok(Self::InscriptionId(
        input
          .parse()
          .snafu_context(error::InscriptionIdParse { input })?,
      )),
      Integer => Ok(Self::Integer(
        input.parse().snafu_context(error::IntegerParse { input })?,
      )),
      OutPoint => Ok(Self::OutPoint(
        input
          .parse()
          .snafu_context(error::OutPointParse { input })?,
      )),
      Rune => Ok(Self::Rune(
        input.parse().snafu_context(error::RuneParse { input })?,
      )),
      SatPoint => Ok(Self::SatPoint(
        input
          .parse()
          .snafu_context(error::SatPointParse { input })?,
      )),
    }
  }
}

impl Display for Object {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Address(address) => write!(f, "{}", address.clone().assume_checked()),
      Self::Hash(hash) => {
        for byte in hash {
          write!(f, "{byte:02x}")?;
        }
        Ok(())
      }
      Self::InscriptionId(inscription_id) => write!(f, "{inscription_id}"),
      Self::Integer(integer) => write!(f, "{integer}"),
      Self::OutPoint(outpoint) => write!(f, "{outpoint}"),
      Self::Rune(rune) => write!(f, "{rune}"),
      Self::Sat(sat) => write!(f, "{sat}"),
      Self::SatPoint(satpoint) => write!(f, "{satpoint}"),
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn from_str() {
    #[track_caller]
    fn case(s: &str, expected: Object) {
      let actual = s.parse::<Object>().unwrap();
      assert_eq!(actual, expected);
      let round_trip = actual.to_string().parse::<Object>().unwrap();
      assert_eq!(round_trip, expected);
    }

    assert_eq!(
      "nvtdijuwxlp".parse::<Object>().unwrap(),
      Object::Sat(Sat(0))
    );
    assert_eq!("a".parse::<Object>().unwrap(), Object::Sat(Sat::LAST));
    assert_eq!(
      "1.1".parse::<Object>().unwrap(),
      Object::Sat(Sat(50 * COIN_VALUE + 1))
    );
    assert_eq!(
      "1°0′0″0‴".parse::<Object>().unwrap(),
      Object::Sat(Sat(2067187500000000))
    );
    assert_eq!("0%".parse::<Object>().unwrap(), Object::Sat(Sat(0)));

    case("0", Object::Integer(0));

    case(
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi1",
      Object::InscriptionId(
        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdefi1"
          .parse()
          .unwrap(),
      ),
    );

    case(
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
      Object::Hash([
        0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd,
        0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab,
        0xcd, 0xef,
      ]),
    );
    case(
      "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
      Object::Address(
        "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4",
      Object::Address(
        "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy",
      Object::Address(
        "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "TB1QQQQQP399ET2XYGDJ5XREQHJJVCMZHXW4AYWXECJDZEW6HYLGVSESRXH6HY",
      Object::Address(
        "TB1QQQQQP399ET2XYGDJ5XREQHJJVCMZHXW4AYWXECJDZEW6HYLGVSESRXH6HY"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw",
      Object::Address(
        "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "BCRT1QS758URSH4Q9Z627KT3PP5YYSM78DDNY6TXAQGW",
      Object::Address(
        "BCRT1QS758URSH4Q9Z627KT3PP5YYSM78DDNY6TXAQGW"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123",
      Object::OutPoint(
        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF:123",
      Object::OutPoint(
        "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF:123"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123:456",
      Object::SatPoint(
        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123:456"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF:123:456",
      Object::SatPoint(
        "0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF:123:456"
          .parse()
          .unwrap(),
      ),
    );
    case(
      "A",
      Object::Rune(SpacedRune {
        rune: Rune(0),
        spacers: 0,
      }),
    );
    case(
      "A•A",
      Object::Rune(SpacedRune {
        rune: Rune(26),
        spacers: 1,
      }),
    );
  }
}

ord/src/option_ext.rs


use super::*;

/// We currently use `anyhow` for error handling but are migrating to typed
/// errors using `snafu`. This trait exists to provide access to
/// `snafu::OptionExt::{context, with_context}`, which are otherwise shadowed
/// by `anyhow::Context::{context, with_context}`. Once the migration is
/// complete, this trait can be deleted, and `snafu::OptionExt` used directly.
pub trait OptionExt<T>: Sized {
  fn snafu_context<C, E>(self, context: C) -> Result<T, E>
  where
    C: snafu::IntoError<E, Source = snafu::NoneError>,
    E: std::error::Error + snafu::ErrorCompat;

  #[allow(unused)]
  fn with_snafu_context<F, C, E>(self, context: F) -> Result<T, E>
  where
    F: FnOnce() -> C,
    C: snafu::IntoError<E, Source = snafu::NoneError>,
    E: std::error::Error + snafu::ErrorCompat;
}

impl<T> OptionExt<T> for Option<T> {
  fn snafu_context<C, E>(self, context: C) -> Result<T, E>
  where
    C: snafu::IntoError<E, Source = snafu::NoneError>,
    E: std::error::Error + snafu::ErrorCompat,
  {
    snafu::OptionExt::context(self, context)
  }

  fn with_snafu_context<F, C, E>(self, context: F) -> Result<T, E>
  where
    F: FnOnce() -> C,
    C: snafu::IntoError<E, Source = snafu::NoneError>,
    E: std::error::Error + snafu::ErrorCompat,
  {
    snafu::OptionExt::with_context(self, context)
  }
}

ord/src/options.rs


use super::*;

#[derive(Clone, Default, Debug, Parser)]
#[command(group(
  ArgGroup::new("chains")
    .required(false)
    .args(&["chain_argument", "signet", "regtest", "testnet", "testnet4"]),
))]
pub struct Options {
  #[arg(long, help = "Load Bitcoin Core data dir from <BITCOIN_DATA_DIR>.")]
  pub(crate) bitcoin_data_dir: Option<PathBuf>,
  #[arg(
    long,
    help = "Authenticate to Bitcoin Core RPC with <BITCOIN_RPC_PASSWORD>."
  )]
  pub(crate) bitcoin_rpc_password: Option<String>,
  #[arg(long, help = "Connect to Bitcoin Core RPC at <BITCOIN_RPC_URL>.")]
  pub(crate) bitcoin_rpc_url: Option<String>,
  #[arg(
    long,
    help = "Authenticate to Bitcoin Core RPC as <BITCOIN_RPC_USERNAME>."
  )]
  pub(crate) bitcoin_rpc_username: Option<String>,
  #[arg(long, help = "Max <N> requests in flight. [default: 12]")]
  pub(crate) bitcoin_rpc_limit: Option<u32>,
  #[arg(long = "chain", value_enum, help = "Use <CHAIN>. [default: mainnet]")]
  pub(crate) chain_argument: Option<Chain>,
  #[arg(
    long,
    help = "Commit to index every <COMMIT_INTERVAL> blocks. [default: 5000]"
  )]
  pub(crate) commit_interval: Option<usize>,
  #[arg(
    long,
    help = "Create a savepoint every <SAVEPOINT_INTERVAL> blocks. [default: 10]"
  )]
  pub(crate) savepoint_interval: Option<usize>,
  #[arg(long, help = "Store maximum <MAX_SAVEPOINTS> blocks. [default: 2]")]
  pub(crate) max_savepoints: Option<usize>,
  #[arg(long, help = "Load configuration from <CONFIG>.")]
  pub(crate) config: Option<PathBuf>,
  #[arg(long, help = "Load configuration from <CONFIG_DIR>.")]
  pub(crate) config_dir: Option<PathBuf>,
  #[arg(long, help = "Load Bitcoin Core RPC cookie file from <COOKIE_FILE>.")]
  pub(crate) cookie_file: Option<PathBuf>,
  #[arg(long, alias = "datadir", help = "Store index in <DATA_DIR>.")]
  pub(crate) data_dir: Option<PathBuf>,
  #[arg(long, help = "Limit index to <HEIGHT_LIMIT> blocks.")]
  pub(crate) height_limit: Option<u32>,
  #[arg(long, help = "Use index at <INDEX>.")]
  pub(crate) index: Option<PathBuf>,
  #[arg(long, help = "Track unspent output addresses.")]
  pub(crate) index_addresses: bool,
  #[arg(
    long,
    help = "Set index cache size to <INDEX_CACHE_SIZE> bytes. [default: 1/4 available RAM]"
  )]
  pub(crate) index_cache_size: Option<usize>,
  #[arg(long, help = "Track location of runes.")]
  pub(crate) index_runes: bool,
  #[arg(long, help = "Track location of all satoshis.")]
  pub(crate) index_sats: bool,
  #[arg(long, help = "Store transactions in index.")]
  pub(crate) index_transactions: bool,
  #[arg(long, help = "Run in integration test mode.")]
  pub(crate) integration_test: bool,
  #[clap(long, short, long, help = "Specify output format. [default: json]")]
  pub(crate) format: Option<OutputFormat>,
  #[arg(
    long,
    short,
    alias = "noindex_inscriptions",
    help = "Do not index inscriptions."
  )]
  pub(crate) no_index_inscriptions: bool,
  #[arg(
    long,
    help = "Require basic HTTP authentication with <SERVER_PASSWORD>. Credentials are sent in cleartext. Consider using authentication in conjunction with HTTPS."
  )]
  pub(crate) server_password: Option<String>,
  #[arg(
    long,
    help = "Require basic HTTP authentication with <SERVER_USERNAME>. Credentials are sent in cleartext. Consider using authentication in conjunction with HTTPS."
  )]
  pub(crate) server_username: Option<String>,
  #[arg(long, short, help = "Use regtest. Equivalent to `--chain regtest`.")]
  pub(crate) regtest: bool,
  #[arg(long, short, help = "Use signet. Equivalent to `--chain signet`.")]
  pub(crate) signet: bool,
  #[arg(long, short, help = "Use testnet. Equivalent to `--chain testnet`.")]
  pub(crate) testnet: bool,
  #[arg(long, help = "Use testnet4. Equivalent to `--chain testnet4`.")]
  pub(crate) testnet4: bool,
}

ord/src/outgoing.rs


use super::*;

#[derive(Debug, PartialEq, Clone, DeserializeFromStr, SerializeDisplay)]
pub enum Outgoing {
  Amount(Amount),
  InscriptionId(InscriptionId),
  Rune { decimal: Decimal, rune: SpacedRune },
  Sat(Sat),
  SatPoint(SatPoint),
}

impl Display for Outgoing {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Amount(amount) => write!(f, "{}", amount.to_string().to_lowercase()),
      Self::InscriptionId(inscription_id) => inscription_id.fmt(f),
      Self::Rune { decimal, rune } => write!(f, "{decimal}:{rune}"),
      Self::Sat(sat) => write!(f, "{}", sat.name()),
      Self::SatPoint(satpoint) => satpoint.fmt(f),
    }
  }
}

impl FromStr for Outgoing {
  type Err = SnafuError;

  fn from_str(input: &str) -> Result<Self, Self::Err> {
    lazy_static! {
      static ref AMOUNT: Regex = Regex::new(
        r"(?x)
        ^
        (
          \d+
          |
          \.\d+
          |
          \d+\.\d+
        )
        \ ?
        (bit|btc|cbtc|mbtc|msat|nbtc|pbtc|sat|satoshi|ubtc)
        (s)?
        $
        "
      )
      .unwrap();
      static ref RUNE: Regex = Regex::new(
        r"(?x)
        ^
        (
          \d+
          |
          \.\d+
          |
          \d+\.\d+
        )
        \s*:\s*
        (
          [A-Z•.]+
        )
        $
        "
      )
      .unwrap();
    }

    if re::SAT_NAME.is_match(input) {
      Ok(Outgoing::Sat(
        input.parse().snafu_context(error::SatParse { input })?,
      ))
    } else if re::SATPOINT.is_match(input) {
      Ok(Outgoing::SatPoint(
        input
          .parse()
          .snafu_context(error::SatPointParse { input })?,
      ))
    } else if re::INSCRIPTION_ID.is_match(input) {
      Ok(Outgoing::InscriptionId(
        input
          .parse()
          .snafu_context(error::InscriptionIdParse { input })?,
      ))
    } else if AMOUNT.is_match(input) {
      Ok(Outgoing::Amount(
        input.parse().snafu_context(error::AmountParse { input })?,
      ))
    } else if let Some(captures) = RUNE.captures(input) {
      let decimal = captures[1]
        .parse::<Decimal>()
        .snafu_context(error::RuneAmountParse { input })?;
      let rune = captures[2]
        .parse()
        .snafu_context(error::RuneParse { input })?;
      Ok(Self::Rune { decimal, rune })
    } else {
      Err(SnafuError::OutgoingParse {
        input: input.to_string(),
      })
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn from_str() {
    #[track_caller]
    fn case(s: &str, outgoing: Outgoing) {
      assert_eq!(s.parse::<Outgoing>().unwrap(), outgoing);
    }

    case("nvtdijuwxlp", Outgoing::Sat("nvtdijuwxlp".parse().unwrap()));
    case("a", Outgoing::Sat("a".parse().unwrap()));

    case(
      "0000000000000000000000000000000000000000000000000000000000000000i0",
      Outgoing::InscriptionId(
        "0000000000000000000000000000000000000000000000000000000000000000i0"
          .parse()
          .unwrap(),
      ),
    );

    case(
      "0000000000000000000000000000000000000000000000000000000000000000:0:0",
      Outgoing::SatPoint(
        "0000000000000000000000000000000000000000000000000000000000000000:0:0"
          .parse()
          .unwrap(),
      ),
    );

    case("0 btc", Outgoing::Amount("0 btc".parse().unwrap()));
    case("0btc", Outgoing::Amount("0 btc".parse().unwrap()));
    case("0.0btc", Outgoing::Amount("0 btc".parse().unwrap()));
    case(".0btc", Outgoing::Amount("0 btc".parse().unwrap()));

    case(
      "0  : XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: "0".parse().unwrap(),
      },
    );

    case(
      "0:XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: "0".parse().unwrap(),
      },
    );

    case(
      "0.0:XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: "0.0".parse().unwrap(),
      },
    );

    case(
      ".0:XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: ".0".parse().unwrap(),
      },
    );

    case(
      "1.1:XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: "1.1".parse().unwrap(),
      },
    );

    case(
      "1.1:X.Y.Z",
      Outgoing::Rune {
        rune: "X.Y.Z".parse().unwrap(),
        decimal: "1.1".parse().unwrap(),
      },
    );
  }

  #[test]
  fn roundtrip() {
    #[track_caller]
    fn case(s: &str, outgoing: Outgoing) {
      assert_eq!(s.parse::<Outgoing>().unwrap(), outgoing);
      assert_eq!(s, outgoing.to_string());
    }

    case("nvtdijuwxlp", Outgoing::Sat("nvtdijuwxlp".parse().unwrap()));
    case("a", Outgoing::Sat("a".parse().unwrap()));

    case(
      "0000000000000000000000000000000000000000000000000000000000000000i0",
      Outgoing::InscriptionId(
        "0000000000000000000000000000000000000000000000000000000000000000i0"
          .parse()
          .unwrap(),
      ),
    );

    case(
      "0000000000000000000000000000000000000000000000000000000000000000:0:0",
      Outgoing::SatPoint(
        "0000000000000000000000000000000000000000000000000000000000000000:0:0"
          .parse()
          .unwrap(),
      ),
    );

    case("0 btc", Outgoing::Amount("0 btc".parse().unwrap()));
    case(
      "1.20000000 btc",
      Outgoing::Amount("1.2 btc".parse().unwrap()),
    );

    case(
      "0:XY•Z",
      Outgoing::Rune {
        rune: "XY•Z".parse().unwrap(),
        decimal: "0".parse().unwrap(),
      },
    );

    case(
      "1.1:XYZ",
      Outgoing::Rune {
        rune: "XYZ".parse().unwrap(),
        decimal: "1.1".parse().unwrap(),
      },
    );
  }

  #[test]
  fn serde() {
    #[track_caller]
    fn case(s: &str, j: &str, o: Outgoing) {
      assert_eq!(s.parse::<Outgoing>().unwrap(), o);
      assert_eq!(serde_json::to_string(&o).unwrap(), j);
      assert_eq!(serde_json::from_str::<Outgoing>(j).unwrap(), o);
    }

    case(
      "nvtdijuwxlp",
      "\"nvtdijuwxlp\"",
      Outgoing::Sat("nvtdijuwxlp".parse().unwrap()),
    );
    case("a", "\"a\"", Outgoing::Sat("a".parse().unwrap()));

    case(
      "0000000000000000000000000000000000000000000000000000000000000000i0",
      "\"0000000000000000000000000000000000000000000000000000000000000000i0\"",
      Outgoing::InscriptionId(
        "0000000000000000000000000000000000000000000000000000000000000000i0"
          .parse()
          .unwrap(),
      ),
    );

    case(
      "0000000000000000000000000000000000000000000000000000000000000000:0:0",
      "\"0000000000000000000000000000000000000000000000000000000000000000:0:0\"",
      Outgoing::SatPoint(
        "0000000000000000000000000000000000000000000000000000000000000000:0:0"
          .parse()
          .unwrap(),
      ),
    );

    case(
      "3 btc",
      "\"3 btc\"",
      Outgoing::Amount(Amount::from_sat(3 * COIN_VALUE)),
    );

    case(
      "6.66:HELL.MONEY",
      "\"6.66:HELL•MONEY\"",
      Outgoing::Rune {
        rune: "HELL•MONEY".parse().unwrap(),
        decimal: "6.66".parse().unwrap(),
      },
    );
  }
}

ord/src/properties.rs


use {
  super::*,
  minicbor::{decode, encode, Decode, Decoder, Encode, Encoder},
};

#[derive(Debug, Default, PartialEq)]
pub struct Properties {
  pub(crate) gallery: Vec<InscriptionId>,
}

impl Properties {
  pub(crate) fn from_cbor(cbor: &[u8]) -> Self {
    let Ok(raw) = decode::<RawProperties>(cbor) else {
      return Self::default();
    };

    Self {
      gallery: raw
        .gallery
        .and_then(|gallery| {
          let mut items = Vec::new();

          for item in gallery {
            items.push(item.id?);
          }

          Some(items)
        })
        .unwrap_or_default(),
    }
  }

  pub(crate) fn to_cbor(&self) -> Option<Vec<u8>> {
    if *self == Self::default() {
      return None;
    }

    Some(
      minicbor::to_vec(RawProperties {
        gallery: Some(
          self
            .gallery
            .iter()
            .copied()
            .map(|item| GalleryItem { id: Some(item) })
            .collect(),
        ),
      })
      .unwrap(),
    )
  }
}

#[derive(Decode, Encode)]
#[cbor(map)]
pub(crate) struct GalleryItem {
  #[n(0)]
  pub(crate) id: Option<InscriptionId>,
}

#[derive(Decode, Encode)]
#[cbor(map)]
pub(crate) struct RawProperties {
  #[n(0)]
  pub(crate) gallery: Option<Vec<GalleryItem>>,
}

#[derive(Debug, Snafu)]
#[snafu(context(suffix(Error)))]
enum DecodeError {
  #[snafu(display("invalid inscription ID length {len}"))]
  InscriptionId { len: usize },
}

impl<'a, T> Decode<'a, T> for InscriptionId {
  fn decode(decoder: &mut Decoder<'a>, _: &mut T) -> Result<Self, decode::Error> {
    let bytes = decoder.bytes()?;

    Self::from_value(bytes)
      .ok_or_else(|| decode::Error::custom(InscriptionIdError { len: bytes.len() }.build()))
  }
}

impl<T> Encode<T> for InscriptionId {
  fn encode<W>(&self, encoder: &mut Encoder<W>, _: &mut T) -> Result<(), encode::Error<W::Error>>
  where
    W: encode::Write,
  {
    encoder.bytes(&self.value()).map(|_| ())
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn decode() {
    assert_eq!(Properties::from_cbor(&[]), Properties::default());
  }

  #[test]
  fn encode() {
    assert_eq!(Properties::default().to_cbor(), None);

    let mut buffer = Vec::new();

    {
      Encoder::new(&mut buffer)
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .array(2)
        .unwrap()
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .bytes(&inscription_id(0).value())
        .unwrap()
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .bytes(&inscription_id(1).value())
        .unwrap();
    }

    let expected = Properties {
      gallery: vec![inscription_id(0), inscription_id(1)],
    };

    assert_eq!(expected.to_cbor(), Some(buffer.clone()));

    assert_eq!(Properties::from_cbor(&buffer), expected);
  }

  #[test]
  fn invalid_gallery_item_produces_empty_gallery() {
    let mut buffer = Vec::new();

    {
      Encoder::new(&mut buffer)
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .array(2)
        .unwrap()
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .bytes(&inscription_id(0).value())
        .unwrap()
        .map(1)
        .unwrap()
        .u8(0)
        .unwrap()
        .bytes(&[1, 2, 3])
        .unwrap();
    }

    assert_eq!(Properties::from_cbor(&buffer), Properties::default());
  }
}

ord/src/re.rs


use super::*;

fn re(s: &'static str) -> Regex {
  Regex::new(&format!("^{s}$")).unwrap()
}

lazy_static! {
  pub(crate) static ref ADDRESS: Regex = re(
    r"((bc1|tb1|bcrt1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{39,60}|[123][a-km-zA-HJ-NP-Z1-9]{25,34})"
  );
  pub(crate) static ref HASH: Regex = re(r"[[:xdigit:]]{64}");
  pub(crate) static ref INSCRIPTION_ID: Regex = re(r"[[:xdigit:]]{64}i\d+");
  pub(crate) static ref INSCRIPTION_NUMBER: Regex = re(r"-?[0-9]+");
  pub(crate) static ref OUTPOINT: Regex = re(r"[[:xdigit:]]{64}:\d+");
  pub(crate) static ref RUNE_ID: Regex = re(r"[0-9]+:[0-9]+");
  pub(crate) static ref RUNE_NUMBER: Regex = re(r"-?[0-9]+");
  pub(crate) static ref SATPOINT: Regex = re(r"[[:xdigit:]]{64}:\d+:\d+");
  pub(crate) static ref SATSCARD_URL: Regex =
    re(r"https://(get)?satscard.com/start#(?<parameters>.*)");
  pub(crate) static ref SAT_NAME: Regex = re(r"[a-z]{1,11}");
  pub(crate) static ref SPACED_RUNE: Regex = re(r"[A-Z•.]+");
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn sat_name() {
    assert!(SAT_NAME.is_match(&Sat(0).name()));
    assert!(SAT_NAME.is_match(&Sat::LAST.name()));
  }
}

ord/src/representation.rs


use {super::*, regex::RegexSet};

#[derive(Debug, Copy, Clone)]
pub(crate) enum Representation {
  Address,
  Decimal,
  Degree,
  Hash,
  InscriptionId,
  Integer,
  Name,
  OutPoint,
  Percentile,
  Rune,
  SatPoint,
}

impl Representation {
  const fn pattern(self) -> (Self, &'static str) {
    (
      self,
      match self {
        Self::Address => r"^(bc|BC|tb|TB|bcrt|BCRT)1.*$",
        Self::Decimal => r"^.*\..*$",
        Self::Degree => r"^.*°.*′.*″(.*‴)?$",
        Self::Hash => r"^[[:xdigit:]]{64}$",
        Self::InscriptionId => r"^[[:xdigit:]]{64}i\d+$",
        Self::Integer => r"^[0-9]*$",
        Self::Name => r"^[a-z]{1,11}$",
        Self::OutPoint => r"^[[:xdigit:]]{64}:\d+$",
        Self::Percentile => r"^.*%$",
        Self::Rune => r"^[A-Z•.]+$",
        Self::SatPoint => r"^[[:xdigit:]]{64}:\d+:\d+$",
      },
    )
  }
}

impl FromStr for Representation {
  type Err = SnafuError;

  fn from_str(input: &str) -> Result<Self, Self::Err> {
    if let Some(i) = REGEX_SET.matches(input).into_iter().next() {
      Ok(PATTERNS[i].0)
    } else {
      Err(error::UnrecognizedRepresentation { input }.build())
    }
  }
}

const PATTERNS: &[(Representation, &str)] = &[
  Representation::Address.pattern(),
  Representation::Decimal.pattern(),
  Representation::Degree.pattern(),
  Representation::Hash.pattern(),
  Representation::InscriptionId.pattern(),
  Representation::Integer.pattern(),
  Representation::Name.pattern(),
  Representation::OutPoint.pattern(),
  Representation::Percentile.pattern(),
  Representation::Rune.pattern(),
  Representation::SatPoint.pattern(),
];

lazy_static! {
  static ref REGEX_SET: RegexSet =
    RegexSet::new(PATTERNS.iter().map(|(_representation, pattern)| pattern),).unwrap();
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn all_patterns_are_anchored() {
    assert!(PATTERNS
      .iter()
      .all(|(_representation, pattern)| pattern.starts_with('^') && pattern.ends_with('$')));
  }
}

ord/src/runes.rs


use super::*;

#[derive(Debug, PartialEq)]
pub enum MintError {
  Cap(u128),
  End(u64),
  Start(u64),
  Unmintable,
}

impl Display for MintError {
  fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
    match self {
      MintError::Cap(cap) => write!(f, "limited to {cap} mints"),
      MintError::End(end) => write!(f, "mint ended on block {end}"),
      MintError::Start(start) => write!(f, "mint starts on block {start}"),
      MintError::Unmintable => write!(f, "not mintable"),
    }
  }
}

#[cfg(test)]
mod tests {
  use {super::*, crate::index::testing::Context};

  const RUNE: u128 = 99246114928149462;

  #[test]
  fn index_starts_with_no_runes() {
    let context = Context::builder().arg("--index-runes").build();
    context.assert_runes([], []);
  }

  #[test]
  fn default_index_does_not_index_runes() {
    let context = Context::builder().build();

    context.mine_blocks(1);

    context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes([], []);
  }

  #[test]
  fn empty_runestone_does_not_create_rune() {
    let context = Context::builder().arg("--index-runes").build();

    context.mine_blocks(1);

    context.etch(Default::default(), 1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Witness::new())],
      op_return: Some(Runestone::default().encipher()),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn etching_with_no_edicts_creates_rune() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn etching_with_edict_creates_rune() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn runes_must_be_greater_than_or_equal_to_minimum_for_height() {
    let minimum = Rune::minimum_at_height(
      Chain::Regtest.network(),
      Height((Runestone::COMMIT_CONFIRMATIONS + 2).into()),
    )
    .0;

    {
      let context = Context::builder()
        .chain(Chain::Regtest)
        .arg("--index-runes")
        .build();

      context.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(Rune(minimum - 1)),
            premine: Some(u128::MAX),
            ..default()
          }),
          ..default()
        },
        1,
      );

      context.assert_runes([], []);
    }

    {
      let context = Context::builder()
        .chain(Chain::Regtest)
        .arg("--index-runes")
        .build();

      let (txid, id) = context.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(Rune(minimum)),
            premine: Some(u128::MAX),
            ..default()
          }),
          ..default()
        },
        1,
      );

      context.assert_runes(
        [(
          id,
          RuneEntry {
            block: id.block,
            etching: txid,
            spaced_rune: SpacedRune {
              rune: Rune(minimum),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id.block,
            ..default()
          },
        )],
        [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
      );
    }
  }

  #[test]
  fn etching_cannot_specify_reserved_rune() {
    {
      let context = Context::builder().arg("--index-runes").build();

      context.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(Rune::reserved(0, 0)),
            ..default()
          }),
          ..default()
        },
        1,
      );

      context.assert_runes([], []);
    }

    {
      let context = Context::builder().arg("--index-runes").build();

      let (txid, id) = context.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(Rune(Rune::reserved(0, 0).n() - 1)),
            premine: Some(u128::MAX),
            ..default()
          }),
          ..default()
        },
        1,
      );

      context.assert_runes(
        [(
          id,
          RuneEntry {
            block: id.block,
            etching: txid,
            spaced_rune: SpacedRune {
              rune: Rune(Rune::reserved(0, 0).n() - 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id.block,
            ..default()
          },
        )],
        [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
      );
    }
  }

  #[test]
  fn reserved_runes_may_be_etched() {
    let context = Context::builder().arg("--index-runes").build();

    context.mine_blocks(1);

    let txid0 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: None,
            premine: Some(u128::MAX),
            ..default()
          }),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    let id0 = RuneId { block: 2, tx: 1 };

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id0,
        RuneEntry {
          block: id0.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune::reserved(id0.block, id0.tx),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: 2,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id0, u128::MAX)],
      )],
    );

    context.mine_blocks(1);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            premine: Some(u128::MAX),
            rune: None,
            ..default()
          }),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    let id1 = RuneId { block: 4, tx: 1 };

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune::reserved(id0.block, id0.tx),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: 2,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune::reserved(id1.block, id0.tx),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: 4,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );
  }

  #[test]
  fn etching_with_non_zero_divisibility_and_rune() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          divisibility: Some(1),
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          etching: txid,
          divisibility: 1,
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn allocations_over_max_supply_are_ignored() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn allocations_partially_over_max_supply_are_honored() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: u128::MAX / 2,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          symbol: None,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn etching_may_allocate_less_than_max_supply() {
    let context = Context::builder().arg("--index-runes").build();

    context.mine_blocks(1);

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 100,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(100),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 100,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 100)])],
    );
  }

  #[test]
  fn etching_may_allocate_to_multiple_outputs() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: 100,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: 100,
            output: 1,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(200),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          burned: 100,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 200,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 100)])],
    );
  }

  #[test]
  fn allocations_to_invalid_outputs_produce_cenotaph() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: 100,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: 100,
            output: 3,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn input_runes_may_be_allocated() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: u128::MAX,
            output: 0,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn etched_rune_is_allocated_with_zero_supply_for_cenotaph() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          ..default()
        }),
        pointer: Some(10),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn etched_rune_parameters_are_unset_for_cenotaph() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          premine: Some(u128::MAX),
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            cap: Some(1),
            amount: Some(1),
            offset: (Some(1), Some(1)),
            height: (None, None),
          }),
          divisibility: Some(1),
          symbol: Some('$'),
          spacers: Some(1),
          turbo: true,
        }),
        pointer: Some(10),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          burned: 0,
          divisibility: 0,
          etching: txid0,
          terms: None,
          mints: 0,
          number: 0,
          premine: 0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          symbol: None,
          timestamp: id.block,
          turbo: false,
        },
      )],
      [],
    );
  }

  #[test]
  fn reserved_runes_are_not_allocated_in_cenotaph() {
    let context = Context::builder().arg("--index-runes").build();

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching::default()),
          pointer: Some(10),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn input_runes_are_burned_if_an_unrecognized_even_tag_is_encountered() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          pointer: Some(10),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          burned: u128::MAX,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn unallocated_runes_are_assigned_to_first_non_op_return_output() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(Runestone::default().encipher()),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn unallocated_runes_are_burned_if_no_non_op_return_output_is_present() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(Runestone::default().encipher()),
      outputs: 0,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          burned: u128::MAX,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn unallocated_runes_are_assigned_to_default_output() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          pointer: Some(1),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 1,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn unallocated_runes_are_burned_if_default_output_is_op_return() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          pointer: Some(2),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          burned: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn unallocated_runes_in_transactions_with_no_runestone_are_assigned_to_first_non_op_return_output(
  ) {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: None,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn duplicate_runes_are_forbidden() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );

    context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn output_may_hold_multiple_runes() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id0) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id0,
        RuneEntry {
          block: id0.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id0.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id0, u128::MAX)],
      )],
    );

    let (txid1, id1) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE + 1)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (id0.block.try_into().unwrap(), 1, 0, Witness::new()),
        (id1.block.try_into().unwrap(), 1, 0, Witness::new()),
      ],
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [(
        OutPoint {
          txid: txid2,
          vout: 0,
        },
        vec![(id0, u128::MAX), (id1, u128::MAX)],
      )],
    );
  }

  #[test]
  fn multiple_input_runes_on_the_same_input_may_be_allocated() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id0) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id0,
        RuneEntry {
          block: id0.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id0.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id0, u128::MAX)],
      )],
    );

    let (txid1, id1) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE + 1)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (id0.block.try_into().unwrap(), 1, 0, Witness::new()),
        (id1.block.try_into().unwrap(), 1, 0, Witness::new()),
      ],
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [(
        OutPoint {
          txid: txid2,
          vout: 0,
        },
        vec![(id0, u128::MAX), (id1, u128::MAX)],
      )],
    );

    let txid3 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[((id1.block + 1).try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id: id0,
              amount: u128::MAX / 2,
              output: 1,
            },
            Edict {
              id: id1,
              amount: u128::MAX / 2,
              output: 1,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid3,
            vout: 0,
          },
          vec![(id0, u128::MAX / 2 + 1), (id1, u128::MAX / 2 + 1)],
        ),
        (
          OutPoint {
            txid: txid3,
            vout: 1,
          },
          vec![(id0, u128::MAX / 2), (id1, u128::MAX / 2)],
        ),
      ],
    );
  }

  #[test]
  fn multiple_input_runes_on_different_inputs_may_be_allocated() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id0) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id0,
        RuneEntry {
          block: id0.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id0.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id0, u128::MAX)],
      )],
    );

    let (txid1, id1) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE + 1)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (id0.block.try_into().unwrap(), 1, 0, Witness::new()),
        (id1.block.try_into().unwrap(), 1, 0, Witness::new()),
      ],
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id: id0,
              amount: u128::MAX,
              output: 0,
            },
            Edict {
              id: id1,
              amount: u128::MAX,
              output: 0,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [(
        OutPoint {
          txid: txid2,
          vout: 0,
        },
        vec![(id0, u128::MAX), (id1, u128::MAX)],
      )],
    );
  }

  #[test]
  fn unallocated_runes_are_assigned_to_first_non_op_return_output_when_op_return_is_not_last_output(
  ) {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        script::Builder::new()
          .push_opcode(opcodes::all::OP_RETURN)
          .into_script(),
      ),
      op_return_index: Some(0),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 1 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn multiple_runes_may_be_etched_in_one_block() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id0) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let (txid1, id1) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE + 1)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );
  }

  #[test]
  fn edicts_with_id_zero_are_skipped() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id: RuneId::default(),
              amount: 100,
              output: 0,
            },
            Edict {
              id,
              amount: u128::MAX,
              output: 0,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn edicts_which_refer_to_input_rune_with_no_balance_are_skipped() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id0) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id0,
        RuneEntry {
          block: id0.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id0.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id0, u128::MAX)],
      )],
    );

    let (txid1, id1) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE + 1)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid0,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
      ],
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id0.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id: id0,
              amount: u128::MAX,
              output: 0,
            },
            Edict {
              id: id1,
              amount: u128::MAX,
              output: 0,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [
        (
          id0,
          RuneEntry {
            block: id0.block,
            etching: txid0,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id0.block,
            ..default()
          },
        ),
        (
          id1,
          RuneEntry {
            block: id1.block,
            etching: txid1,
            spaced_rune: SpacedRune {
              rune: Rune(RUNE + 1),
              spacers: 0,
            },
            premine: u128::MAX,
            timestamp: id1.block,
            number: 1,
            ..default()
          },
        ),
      ],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id1, u128::MAX)],
        ),
        (
          OutPoint {
            txid: txid2,
            vout: 0,
          },
          vec![(id0, u128::MAX)],
        ),
      ],
    );
  }

  #[test]
  fn edicts_over_max_inputs_are_ignored() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX / 2,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX / 2),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX / 2,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX / 2)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: u128::MAX,
            output: 0,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX / 2,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, u128::MAX / 2)],
      )],
    );
  }

  #[test]
  fn edicts_may_transfer_runes_to_op_return_outputs() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 1,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          burned: u128::MAX,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn outputs_with_no_runes_have_no_balance() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn edicts_which_transfer_no_runes_to_output_create_no_balance_entry() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: 0,
            output: 1,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn split_in_etching() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 0,
          output: 5,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (OutPoint { txid, vout: 0 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 1 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 2 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 3 }, vec![(id, u128::MAX / 4)]),
      ],
    );
  }

  #[test]
  fn split_in_etching_with_preceding_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: 1000,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: 0,
            output: 5,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint { txid, vout: 0 },
          vec![(id, 1000 + (u128::MAX - 1000) / 4 + 1)],
        ),
        (
          OutPoint { txid, vout: 1 },
          vec![(id, (u128::MAX - 1000) / 4 + 1)],
        ),
        (
          OutPoint { txid, vout: 2 },
          vec![(id, (u128::MAX - 1000) / 4 + 1)],
        ),
        (
          OutPoint { txid, vout: 3 },
          vec![(id, (u128::MAX - 1000) / 4)],
        ),
      ],
    );
  }

  #[test]
  fn split_in_etching_with_following_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: 0,
            output: 5,
          },
          Edict {
            id: RuneId::default(),
            amount: 1000,
            output: 0,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (OutPoint { txid, vout: 0 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 1 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 2 }, vec![(id, u128::MAX / 4 + 1)]),
        (OutPoint { txid, vout: 3 }, vec![(id, u128::MAX / 4)]),
      ],
    );
  }

  #[test]
  fn split_with_amount_in_etching() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 1000,
          output: 5,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(4000),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 4000,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (OutPoint { txid, vout: 0 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 1 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 2 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 3 }, vec![(id, 1000)]),
      ],
    );
  }

  #[test]
  fn split_in_etching_with_amount_with_preceding_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: u128::MAX - 3000,
            output: 0,
          },
          Edict {
            id: RuneId::default(),
            amount: 1000,
            output: 5,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (OutPoint { txid, vout: 0 }, vec![(id, u128::MAX - 2000)]),
        (OutPoint { txid, vout: 1 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 2 }, vec![(id, 1000)]),
      ],
    );
  }

  #[test]
  fn split_in_etching_with_amount_with_following_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![
          Edict {
            id: RuneId::default(),
            amount: 1000,
            output: 5,
          },
          Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          },
        ],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      4,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint { txid, vout: 0 },
          vec![(id, u128::MAX - 4000 + 1000)],
        ),
        (OutPoint { txid, vout: 1 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 2 }, vec![(id, 1000)]),
        (OutPoint { txid, vout: 3 }, vec![(id, 1000)]),
      ],
    );
  }

  #[test]
  fn split() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 0,
            output: 3,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, u128::MAX / 2 + 1)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, u128::MAX / 2)],
        ),
      ],
    );
  }

  #[test]
  fn split_with_preceding_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id,
              amount: 1000,
              output: 0,
            },
            Edict {
              id,
              amount: 0,
              output: 3,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 1000 + (u128::MAX - 1000) / 2 + 1)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, (u128::MAX - 1000) / 2)],
        ),
      ],
    );
  }

  #[test]
  fn split_with_following_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id,
              amount: 0,
              output: 3,
            },
            Edict {
              id,
              amount: 1000,
              output: 1,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, u128::MAX / 2 + 1)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, u128::MAX / 2)],
        ),
      ],
    );
  }

  #[test]
  fn split_with_amount() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 3,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, u128::MAX - 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, 1000)],
        ),
      ],
    );
  }

  #[test]
  fn split_with_amount_with_preceding_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 4,
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id,
              amount: u128::MAX - 2000,
              output: 0,
            },
            Edict {
              id,
              amount: 1000,
              output: 5,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, u128::MAX - 2000 + 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, 1000)],
        ),
      ],
    );
  }

  #[test]
  fn split_with_amount_with_following_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 4,
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id,
              amount: 1000,
              output: 5,
            },
            Edict {
              id,
              amount: u128::MAX,
              output: 0,
            },
          ],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, u128::MAX - 4000 + 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 2,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 3,
          },
          vec![(id, 1000)],
        ),
      ],
    );
  }

  #[test]
  fn etching_may_specify_symbol() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          symbol: Some('$'),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          symbol: Some('$'),
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn allocate_all_remaining_runes_in_etching() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 0,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])],
    );
  }

  #[test]
  fn allocate_all_remaining_runes_in_inputs() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 0,
            output: 1,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 1,
        },
        vec![(id, u128::MAX)],
      )],
    );
  }

  #[test]
  fn rune_can_be_minted_without_edict() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          mints: 0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 1,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn rune_cannot_be_minted_less_than_limit_amount() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          mints: 0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          mint: Some(id),
          edicts: vec![Edict {
            id,
            amount: 111,
            output: 0,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 1,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn etching_with_amount_can_be_minted() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            cap: Some(100),
            amount: Some(1000),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          premine: 0,
          mints: 0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 1,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    // claim the rune
    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(4, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 2,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid2,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
      ],
    );

    // claim the rune in a burn runestone
    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(5, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          pointer: Some(10),
          mint: Some(id),
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          burned: 1000,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 3,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: 0,
          timestamp: id.block,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid2,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
      ],
    );
  }

  #[test]
  fn open_mints_can_be_limited_with_offset_end() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            offset: (None, Some(2)),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        offset: (None, Some(2)),
        cap: Some(100),
        ..default()
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_can_be_limited_with_offset_start() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            offset: (Some(2), None),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        offset: (Some(2), None),
        cap: Some(100),
        ..default()
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_can_be_limited_with_height_start() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (Some(10), None),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (Some(10), None),
        cap: Some(100),
        ..default()
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_can_be_limited_with_height_end() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (None, Some(10)),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (None, Some(10)),
        cap: Some(100),
        ..default()
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_must_be_ended_with_etched_height_plus_offset_end() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (None, Some(100)),
            offset: (None, Some(2)),
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (None, Some(100)),
        offset: (None, Some(2)),
        cap: Some(100),
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);
    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_must_be_ended_with_height_end() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (None, Some(10)),
            offset: (None, Some(100)),
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (None, Some(10)),
        offset: (None, Some(100)),
        cap: Some(100),
      }),
      timestamp: id.block,
      ..default()
    };

    context.assert_runes([(id, entry)], []);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);
    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_must_be_started_with_height_start() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (Some(11), None),
            offset: (Some(1), None),
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry0 = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (Some(11), None),
        offset: (Some(1), None),
        cap: Some(100),
      }),
      timestamp: id.block,
      ..default()
    };

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([(id, entry0)], []);

    context.mine_blocks(1);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry0.mints += 1;

    context.assert_runes(
      [(id, entry0)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_must_be_started_with_etched_height_plus_offset_start() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            height: (Some(9), None),
            offset: (Some(3), None),
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let mut entry = RuneEntry {
      block: id.block,
      etching: txid0,
      spaced_rune: SpacedRune {
        rune: Rune(RUNE),
        spacers: 0,
      },
      terms: Some(Terms {
        amount: Some(1000),
        height: (Some(9), None),
        offset: (Some(3), None),
        cap: Some(100),
      }),
      timestamp: id.block,
      ..default()
    };

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([(id, entry)], []);

    context.mine_blocks(1);

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    entry.mints += 1;

    context.assert_runes(
      [(id, entry)],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn open_mints_with_offset_end_zero_can_be_premined() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 1111,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(1111),
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(0)),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(0)),
            ..default()
          }),
          timestamp: id.block,
          premine: 1111,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(0)),
            ..default()
          }),
          premine: 1111,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 1111)])],
    );
  }

  #[test]
  fn open_mints_can_be_limited_to_cap() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(2),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(2),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          mints: 1,
          etching: txid0,
          terms: Some(Terms {
            cap: Some(2),
            amount: Some(1000),
            ..default()
          }),
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );

    let txid2 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(2),
            ..default()
          }),
          mints: 2,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid2,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
      ],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(4, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(2),
            ..default()
          }),
          mints: 2,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
        (
          OutPoint {
            txid: txid2,
            vout: 0,
          },
          vec![(id, 1000)],
        ),
      ],
    );
  }

  #[test]
  fn open_mints_without_a_cap_are_unmintable() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(2)),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(2)),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 1000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          mints: 0,
          etching: txid0,
          terms: Some(Terms {
            amount: Some(1000),
            offset: (None, Some(2)),
            ..default()
          }),
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn open_mint_claims_can_use_split() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, Witness::new())],
      outputs: 2,
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 0,
            output: 3,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          timestamp: id.block,
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          mints: 1,
          ..default()
        },
      )],
      [
        (
          OutPoint {
            txid: txid1,
            vout: 0,
          },
          vec![(id, 500)],
        ),
        (
          OutPoint {
            txid: txid1,
            vout: 1,
          },
          vec![(id, 500)],
        ),
      ],
    );
  }

  #[test]
  fn runes_can_be_etched_and_premined_in_the_same_transaction() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(2000),
          terms: Some(Terms {
            amount: Some(1000),
            ..default()
          }),
          ..default()
        }),
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 2000,
          output: 0,
        }],
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            ..default()
          }),
          timestamp: id.block,
          premine: 2000,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 2000)])],
    );
  }

  #[test]
  fn omitted_edicts_defaults_to_mint_amount() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            offset: (None, Some(1)),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: None,
            offset: (None, Some(1)),
            ..default()
          }),
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn premines_can_claim_over_mint_amount() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(2000),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(1),
            ..default()
          }),
          ..default()
        }),
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: 2000,
          output: 0,
        }],
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(1),
            ..default()
          }),
          timestamp: id.block,
          premine: 2000,
          mints: 0,
          ..default()
        },
      )],
      [(OutPoint { txid, vout: 0 }, vec![(id, 2000)])],
    );
  }

  #[test]
  fn transactions_cannot_claim_more_than_mint_amount() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 2000,
            output: 0,
          }],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          timestamp: id.block,
          mints: 1,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn multiple_edicts_in_one_transaction_may_claim_open_mint() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );

    let txid1 = context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![
            Edict {
              id,
              amount: 500,
              output: 0,
            },
            Edict {
              id,
              amount: 500,
              output: 0,
            },
            Edict {
              id,
              amount: 500,
              output: 0,
            },
          ],
          mint: Some(id),
          ..default()
        }
        .encipher(),
      ),
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          terms: Some(Terms {
            amount: Some(1000),
            cap: Some(100),
            ..default()
          }),
          timestamp: id.block,
          mints: 1,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid1,
          vout: 0,
        },
        vec![(id, 1000)],
      )],
    );
  }

  #[test]
  fn commits_are_not_valid_in_non_taproot_witnesses() {
    let context = Context::builder().arg("--index-runes").build();

    let block_count = context.index.block_count().unwrap().into_usize();

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count, 0, 0, Witness::new())],
      p2tr: false,
      ..default()
    });

    context.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into());

    let mut witness = Witness::new();

    let runestone = Runestone {
      etching: Some(Etching {
        rune: Some(Rune(RUNE)),
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        ..default()
      }),
      ..default()
    };

    let tapscript = script::Builder::new()
      .push_slice::<&PushBytes>(
        runestone
          .etching
          .unwrap()
          .rune
          .unwrap()
          .commitment()
          .as_slice()
          .try_into()
          .unwrap(),
      )
      .into_script();

    witness.push(tapscript);

    witness.push([]);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count + 1, 1, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn immature_commits_are_not_valid() {
    let context = Context::builder().arg("--index-runes").build();

    let block_count = context.index.block_count().unwrap().into_usize();

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count, 0, 0, Witness::new())],
      p2tr: true,
      ..default()
    });

    context.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 2).into());

    let mut witness = Witness::new();

    let runestone = Runestone {
      etching: Some(Etching {
        rune: Some(Rune(RUNE)),
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        ..default()
      }),
      ..default()
    };

    let tapscript = script::Builder::new()
      .push_slice::<&PushBytes>(
        runestone
          .etching
          .unwrap()
          .rune
          .unwrap()
          .commitment()
          .as_slice()
          .try_into()
          .unwrap(),
      )
      .into_script();

    witness.push(tapscript);

    witness.push([]);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count + 1, 1, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn immature_commits_are_not_valid_even_when_bitcoind_is_ahead() {
    let context = Context::builder().arg("--index-runes").build();

    let block_count = context.index.block_count().unwrap().into_usize();

    context.mine_blocks_with_update(1, false);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count, 0, 0, Witness::new())],
      p2tr: true,
      ..default()
    });

    context.mine_blocks_with_update((Runestone::COMMIT_CONFIRMATIONS - 2).into(), false);

    let mut witness = Witness::new();

    let runestone = Runestone {
      etching: Some(Etching {
        rune: Some(Rune(RUNE)),
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        ..default()
      }),
      ..default()
    };

    let tapscript = script::Builder::new()
      .push_slice::<&PushBytes>(
        runestone
          .etching
          .unwrap()
          .rune
          .unwrap()
          .commitment()
          .as_slice()
          .try_into()
          .unwrap(),
      )
      .into_script();

    witness.push(tapscript);

    witness.push([]);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count + 1, 1, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs: 1,
      ..default()
    });

    context.mine_blocks_with_update(2, false);

    context.mine_blocks_with_update(1, true);

    context.assert_runes([], []);
  }

  #[test]
  fn etchings_are_not_valid_without_commitment() {
    let context = Context::builder().arg("--index-runes").build();

    let block_count = context.index.block_count().unwrap().into_usize();

    context.mine_blocks(1);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count, 0, 0, Witness::new())],
      p2tr: true,
      ..default()
    });

    context.mine_blocks(Runestone::COMMIT_CONFIRMATIONS.into());

    let mut witness = Witness::new();

    let runestone = Runestone {
      etching: Some(Etching {
        rune: Some(Rune(RUNE)),
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        ..default()
      }),
      ..default()
    };

    let tapscript = script::Builder::new()
      .push_slice::<&PushBytes>([].as_slice().try_into().unwrap())
      .into_script();

    witness.push(tapscript);

    witness.push([]);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(block_count + 1, 1, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn tx_commits_to_rune_ignores_invalid_script() {
    let context = Context::builder().arg("--index-runes").build();

    context.mine_blocks(1);

    let runestone = Runestone {
      etching: Some(Etching {
        rune: Some(Rune(RUNE)),
        terms: Some(Terms {
          amount: Some(1000),
          ..default()
        }),
        ..default()
      }),
      ..default()
    };

    let mut witness = Witness::new();

    witness.push([opcodes::all::OP_PUSHDATA4.to_u8()]);
    witness.push([]);

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, witness)],
      op_return: Some(runestone.encipher()),
      outputs: 1,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes([], []);
  }

  #[test]
  fn edict_with_amount_zero_and_no_destinations_is_ignored() {
    let context = Context::builder().arg("--index-runes").build();

    let (txid0, id) = context.etch(
      Runestone {
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
    );

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [(
        OutPoint {
          txid: txid0,
          vout: 0,
        },
        vec![(id, u128::MAX)],
      )],
    );

    context.core.broadcast_tx(TransactionTemplate {
      inputs: &[(id.block.try_into().unwrap(), 1, 0, Witness::new())],
      op_return: Some(
        Runestone {
          edicts: vec![Edict {
            id,
            amount: 0,
            output: 1,
          }],
          ..default()
        }
        .encipher(),
      ),
      outputs: 0,
      ..default()
    });

    context.mine_blocks(1);

    context.assert_runes(
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid0,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0,
          },
          premine: u128::MAX,
          burned: u128::MAX,
          timestamp: id.block,
          ..default()
        },
      )],
      [],
    );
  }

  #[test]
  fn genesis_rune() {
    assert_eq!(
      Chain::Mainnet.first_rune_height(),
      SUBSIDY_HALVING_INTERVAL * 4,
    );

    Context::builder()
      .chain(Chain::Mainnet)
      .arg("--index-runes")
      .build()
      .assert_runes(
        [(
          RuneId { block: 1, tx: 0 },
          RuneEntry {
            block: 1,
            burned: 0,
            divisibility: 0,
            etching: Txid::all_zeros(),
            mints: 0,
            number: 0,
            premine: 0,
            spaced_rune: SpacedRune {
              rune: Rune(2055900680524219742),
              spacers: 128,
            },
            symbol: Some('\u{29C9}'),
            terms: Some(Terms {
              amount: Some(1),
              cap: Some(u128::MAX),
              height: (
                Some((SUBSIDY_HALVING_INTERVAL * 4).into()),
                Some((SUBSIDY_HALVING_INTERVAL * 5).into()),
              ),
              offset: (None, None),
            }),
            timestamp: 0,
            turbo: true,
          },
        )],
        [],
      );
  }
}

ord/src/satscard.rs


use super::*;

#[derive(Debug, PartialEq)]
pub(crate) enum State {
  Error,
  Sealed,
  Unsealed,
}

impl Display for State {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Error => write!(f, "error"),
      Self::Sealed => write!(f, "sealed"),
      Self::Unsealed => write!(f, "unsealed"),
    }
  }
}

#[derive(Debug, Snafu)]
#[snafu(context(suffix(Error)))]
pub(crate) enum Error {
  #[snafu(display("address recovery failed"))]
  AddressRecovery,
  #[snafu(display("duplicate key `{key}`"))]
  DuplicateKey { key: String },
  #[snafu(display("parameter {parameter} has no value"))]
  ParameterValueMissing { parameter: String },
  #[snafu(display("unrecognized state {value}"))]
  State { value: String },
  #[snafu(display("invalid slot `{value}`: {source}"))]
  Slot {
    value: String,
    source: std::num::ParseIntError,
  },
  #[snafu(display("missing address suffix"))]
  MissingAddressSuffix,
  #[snafu(display("missing nonce"))]
  MissingNonce,
  #[snafu(display("missing signature"))]
  MissingSignature,
  #[snafu(display("missing slot"))]
  MissingSlot,
  #[snafu(display("missing state"))]
  MissingState,
  #[snafu(display("invalid nonce `{value}`: {source}"))]
  Nonce {
    value: String,
    source: hex::FromHexError,
  },
  #[snafu(display("invalid nonce length {}, expected 16 hex digits", nonce.len()))]
  NonceLength { nonce: Vec<u8> },
  #[snafu(display("hex decoding signature `{value}` failed: {source}"))]
  SignatureHex {
    value: String,
    source: hex::FromHexError,
  },
  #[snafu(display("decoding signature failed: {source}"))]
  SignatureDecode { source: secp256k1::Error },
  #[snafu(display("unknown key `{key}`"))]
  UnknownKey { key: String },
}

#[derive(Debug, PartialEq)]
pub(crate) struct Satscard {
  pub(crate) address: Address,
  pub(crate) nonce: [u8; 8],
  pub(crate) query_parameters: String,
  pub(crate) slot: u8,
  pub(crate) state: State,
}

impl Satscard {
  pub(crate) fn from_query_parameters(chain: Chain, query_parameters: &str) -> Result<Self, Error> {
    let mut address_suffix = None;
    let mut nonce = Option::<[u8; 8]>::None;
    let mut signature = None;
    let mut slot = None;
    let mut state = None;

    let mut keys = BTreeSet::new();
    for parameter in query_parameters.split('&') {
      let (key, value) = parameter
        .split_once('=')
        .snafu_context(ParameterValueMissingError { parameter })?;

      if !keys.insert(key) {
        return Err(DuplicateKeyError { key }.build());
      }

      match key {
        "u" => {
          state = Some(match value {
            "S" => State::Sealed,
            "E" => State::Error,
            "U" => State::Unsealed,
            _ => {
              return Err(StateError { value }.build());
            }
          })
        }
        "o" => slot = Some(value.parse::<u8>().snafu_context(SlotError { value })?),
        "r" => address_suffix = Some(value),
        "n" => {
          nonce = Some({
            let nonce = hex::decode(value).snafu_context(NonceError { value })?;
            nonce
              .as_slice()
              .try_into()
              .ok()
              .snafu_context(NonceLengthError { nonce })?
          })
        }
        "s" => {
          signature = Some({
            let signature = hex::decode(value).snafu_context(SignatureHexError { value })?;
            secp256k1::ecdsa::Signature::from_compact(&signature)
              .snafu_context(SignatureDecodeError)?
          });
        }
        _ => return Err(UnknownKeyError { key }.build()),
      }
    }

    let address_suffix = address_suffix.snafu_context(MissingAddressSuffixError)?;
    let nonce = nonce.snafu_context(MissingNonceError)?;
    let signature = signature.snafu_context(MissingSignatureError)?;
    let slot = slot.snafu_context(MissingSlotError)?;
    let state = state.snafu_context(MissingStateError)?;

    let message = &query_parameters[0..query_parameters.rfind('=').unwrap() + 1];

    let address = Self::recover_address(address_suffix, chain, message, &signature)?;

    Ok(Self {
      address,
      nonce,
      query_parameters: query_parameters.into(),
      slot,
      state,
    })
  }

  fn recover_address(
    address_suffix: &str,
    chain: Chain,
    message: &str,
    signature: &secp256k1::ecdsa::Signature,
  ) -> Result<Address, Error> {
    use bitcoin::{
      key::PublicKey,
      secp256k1::{
        ecdsa::{RecoverableSignature, RecoveryId},
        hashes::sha256::Hash,
        Message,
      },
      CompressedPublicKey,
    };

    let signature_compact = signature.serialize_compact();

    let message = Message::from_digest(*Hash::hash(message.as_bytes()).as_ref());

    for i in 0.. {
      let Ok(id) = RecoveryId::from_i32(i) else {
        break;
      };

      let recoverable_signature =
        RecoverableSignature::from_compact(&signature_compact, id).unwrap();

      let Ok(public_key) = recoverable_signature.recover(&message) else {
        continue;
      };

      signature.verify(&message, &public_key).unwrap();

      let public_key = PublicKey::new(public_key);

      let public_key = CompressedPublicKey::try_from(public_key).unwrap();

      let address = Address::p2wpkh(&public_key, chain.bech32_hrp());

      if address.to_string().ends_with(&address_suffix) {
        return Ok(address);
      }
    }

    Err(Error::AddressRecovery)
  }
}

#[cfg(test)]
pub(crate) mod tests {
  use super::*;

  pub(crate) const URL: &str = concat!(
    "https://satscard.com/start",
    "#u=S",
    "&o=0",
    "&r=a5x2tplf",
    "&n=7664168a4ef7b8e8",
    "&s=",
    "42b209c86ab90be6418d36b0accc3a53c11901861b55be95b763799842d403dc",
    "17cd1b74695a7ffe2d78965535d6fe7f6aafc77f6143912a163cb65862e8fb53",
  );

  pub(crate) fn query_parameters() -> &'static str {
    URL.split_once('#').unwrap().1
  }

  pub(crate) fn satscard() -> Satscard {
    Satscard::from_query_parameters(Chain::Mainnet, query_parameters()).unwrap()
  }

  pub(crate) fn address() -> Address {
    "bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf"
      .parse::<Address<NetworkUnchecked>>()
      .unwrap()
      .require_network(Network::Bitcoin)
      .unwrap()
  }

  #[test]
  fn query_from_coinkite_url() {
    assert_eq!(
      satscard(),
      Satscard {
        address: address(),
        nonce: [0x76, 0x64, 0x16, 0x8a, 0x4e, 0xf7, 0xb8, 0xe8],
        slot: 0,
        state: State::Sealed,
        query_parameters: query_parameters().into(),
      }
    );
  }
}

ord/src/settings.rs


use {super::*, bitcoincore_rpc::Auth};

#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
  bitcoin_data_dir: Option<PathBuf>,
  bitcoin_rpc_limit: Option<u32>,
  bitcoin_rpc_password: Option<String>,
  bitcoin_rpc_url: Option<String>,
  bitcoin_rpc_username: Option<String>,
  chain: Option<Chain>,
  commit_interval: Option<usize>,
  savepoint_interval: Option<usize>,
  max_savepoints: Option<usize>,
  config: Option<PathBuf>,
  config_dir: Option<PathBuf>,
  cookie_file: Option<PathBuf>,
  data_dir: Option<PathBuf>,
  height_limit: Option<u32>,
  hidden: Option<HashSet<InscriptionId>>,
  http_port: Option<u16>,
  index: Option<PathBuf>,
  index_addresses: bool,
  index_cache_size: Option<usize>,
  index_runes: bool,
  index_sats: bool,
  index_transactions: bool,
  integration_test: bool,
  no_index_inscriptions: bool,
  server_password: Option<String>,
  server_url: Option<String>,
  server_username: Option<String>,
}

impl Settings {
  pub fn load(options: Options) -> Result<Settings> {
    let mut env = BTreeMap::<String, String>::new();

    for (var, value) in env::vars_os() {
      let Some(var) = var.to_str() else {
        continue;
      };

      let Some(key) = var.strip_prefix("ORD_") else {
        continue;
      };

      env.insert(
        key.into(),
        value.into_string().map_err(|value| {
          anyhow!(
            "environment variable `{var}` not valid unicode: `{}`",
            value.to_string_lossy()
          )
        })?,
      );
    }

    Self::merge(options, env)
  }

  pub fn merge(options: Options, env: BTreeMap<String, String>) -> Result<Self> {
    let settings = Settings::from_options(options).or(Settings::from_env(env)?);

    let config_path = if let Some(path) = &settings.config {
      Some(path.into())
    } else {
      let path = if let Some(dir) = settings.config_dir.clone().or(settings.data_dir.clone()) {
        dir
      } else {
        Self::default_data_dir()?
      }
      .join("ord.yaml");

      path.exists().then_some(path)
    };

    let config = if let Some(config_path) = config_path {
      serde_yaml::from_reader(File::open(&config_path).context(anyhow!(
        "failed to open config file `{}`",
        config_path.display()
      ))?)
      .context(anyhow!(
        "failed to deserialize config file `{}`",
        config_path.display()
      ))?
    } else {
      Settings::default()
    };

    let settings = settings.or(config).or_defaults()?;

    match (
      &settings.bitcoin_rpc_username,
      &settings.bitcoin_rpc_password,
    ) {
      (None, Some(_rpc_pass)) => bail!("no bitcoin RPC username specified"),
      (Some(_rpc_user), None) => bail!("no bitcoin RPC password specified"),
      _ => {}
    };

    match (&settings.server_username, &settings.server_password) {
      (None, Some(_rpc_pass)) => bail!("no username specified"),
      (Some(_rpc_user), None) => bail!("no password specified"),
      _ => {}
    };

    Ok(settings)
  }

  pub fn or(self, source: Settings) -> Self {
    Self {
      bitcoin_data_dir: self.bitcoin_data_dir.or(source.bitcoin_data_dir),
      bitcoin_rpc_limit: self.bitcoin_rpc_limit.or(source.bitcoin_rpc_limit),
      bitcoin_rpc_password: self.bitcoin_rpc_password.or(source.bitcoin_rpc_password),
      bitcoin_rpc_url: self.bitcoin_rpc_url.or(source.bitcoin_rpc_url),
      bitcoin_rpc_username: self.bitcoin_rpc_username.or(source.bitcoin_rpc_username),
      chain: self.chain.or(source.chain),
      commit_interval: self.commit_interval.or(source.commit_interval),
      savepoint_interval: self.savepoint_interval.or(source.savepoint_interval),
      max_savepoints: self.max_savepoints.or(source.max_savepoints),
      config: self.config.or(source.config),
      config_dir: self.config_dir.or(source.config_dir),
      cookie_file: self.cookie_file.or(source.cookie_file),
      data_dir: self.data_dir.or(source.data_dir),
      height_limit: self.height_limit.or(source.height_limit),
      hidden: Some(
        self
          .hidden
          .iter()
          .flatten()
          .chain(source.hidden.iter().flatten())
          .cloned()
          .collect(),
      ),
      http_port: self.http_port.or(source.http_port),
      index: self.index.or(source.index),
      index_addresses: self.index_addresses || source.index_addresses,
      index_cache_size: self.index_cache_size.or(source.index_cache_size),
      index_runes: self.index_runes || source.index_runes,
      index_sats: self.index_sats || source.index_sats,
      index_transactions: self.index_transactions || source.index_transactions,
      integration_test: self.integration_test || source.integration_test,
      no_index_inscriptions: self.no_index_inscriptions || source.no_index_inscriptions,
      server_password: self.server_password.or(source.server_password),
      server_url: self.server_url.or(source.server_url),
      server_username: self.server_username.or(source.server_username),
    }
  }

  pub fn from_options(options: Options) -> Self {
    Self {
      bitcoin_data_dir: options.bitcoin_data_dir,
      bitcoin_rpc_limit: options.bitcoin_rpc_limit,
      bitcoin_rpc_password: options.bitcoin_rpc_password,
      bitcoin_rpc_url: options.bitcoin_rpc_url,
      bitcoin_rpc_username: options.bitcoin_rpc_username,
      chain: options
        .signet
        .then_some(Chain::Signet)
        .or(options.regtest.then_some(Chain::Regtest))
        .or(options.testnet.then_some(Chain::Testnet))
        .or(options.testnet4.then_some(Chain::Testnet4))
        .or(options.chain_argument),
      commit_interval: options.commit_interval,
      savepoint_interval: options.savepoint_interval,
      max_savepoints: options.max_savepoints,
      config: options.config,
      config_dir: options.config_dir,
      cookie_file: options.cookie_file,
      data_dir: options.data_dir,
      height_limit: options.height_limit,
      hidden: None,
      http_port: None,
      index: options.index,
      index_addresses: options.index_addresses,
      index_cache_size: options.index_cache_size,
      index_runes: options.index_runes,
      index_sats: options.index_sats,
      index_transactions: options.index_transactions,
      integration_test: options.integration_test,
      no_index_inscriptions: options.no_index_inscriptions,
      server_password: options.server_password,
      server_url: None,
      server_username: options.server_username,
    }
  }

  pub fn from_env(env: BTreeMap<String, String>) -> Result<Self> {
    let get_bool = |key| {
      env
        .get(key)
        .map(|value| !value.is_empty())
        .unwrap_or_default()
    };

    let get_string = |key| env.get(key).cloned();

    let get_path = |key| env.get(key).map(PathBuf::from);

    let get_chain = |key| {
      env
        .get(key)
        .map(|chain| chain.parse::<Chain>())
        .transpose()
        .with_context(|| format!("failed to parse environment variable ORD_{key} as chain"))
    };

    let inscriptions = |key| {
      env
        .get(key)
        .map(|inscriptions| {
          inscriptions
            .split_whitespace()
            .map(|inscription_id| inscription_id.parse::<InscriptionId>())
            .collect::<Result<HashSet<InscriptionId>, inscription_id::ParseError>>()
        })
        .transpose()
        .with_context(|| {
          format!("failed to parse environment variable ORD_{key} as inscription list")
        })
    };

    let get_u16 = |key| {
      env
        .get(key)
        .map(|int| int.parse::<u16>())
        .transpose()
        .with_context(|| format!("failed to parse environment variable ORD_{key} as u16"))
    };

    let get_u32 = |key| {
      env
        .get(key)
        .map(|int| int.parse::<u32>())
        .transpose()
        .with_context(|| format!("failed to parse environment variable ORD_{key} as u32"))
    };

    let get_usize = |key| {
      env
        .get(key)
        .map(|int| int.parse::<usize>())
        .transpose()
        .with_context(|| format!("failed to parse environment variable ORD_{key} as usize"))
    };

    Ok(Self {
      bitcoin_data_dir: get_path("BITCOIN_DATA_DIR"),
      bitcoin_rpc_limit: get_u32("BITCOIN_RPC_LIMIT")?,
      bitcoin_rpc_password: get_string("BITCOIN_RPC_PASSWORD"),
      bitcoin_rpc_url: get_string("BITCOIN_RPC_URL"),
      bitcoin_rpc_username: get_string("BITCOIN_RPC_USERNAME"),
      chain: get_chain("CHAIN")?,
      commit_interval: get_usize("COMMIT_INTERVAL")?,
      savepoint_interval: get_usize("SAVEPOINT_INTERVAL")?,
      max_savepoints: get_usize("MAX_SAVEPOINTS")?,
      config: get_path("CONFIG"),
      config_dir: get_path("CONFIG_DIR"),
      cookie_file: get_path("COOKIE_FILE"),
      data_dir: get_path("DATA_DIR"),
      height_limit: get_u32("HEIGHT_LIMIT")?,
      hidden: inscriptions("HIDDEN")?,
      http_port: get_u16("HTTP_PORT")?,
      index: get_path("INDEX"),
      index_addresses: get_bool("INDEX_ADDRESSES"),
      index_cache_size: get_usize("INDEX_CACHE_SIZE")?,
      index_runes: get_bool("INDEX_RUNES"),
      index_sats: get_bool("INDEX_SATS"),
      index_transactions: get_bool("INDEX_TRANSACTIONS"),
      integration_test: get_bool("INTEGRATION_TEST"),
      no_index_inscriptions: get_bool("NO_INDEX_INSCRIPTIONS"),
      server_password: get_string("SERVER_PASSWORD"),
      server_url: get_string("SERVER_URL"),
      server_username: get_string("SERVER_USERNAME"),
    })
  }

  pub fn for_env(dir: &Path, rpc_url: &str, server_url: &str) -> Self {
    Self {
      bitcoin_data_dir: Some(dir.into()),
      bitcoin_rpc_password: None,
      bitcoin_rpc_url: Some(rpc_url.into()),
      bitcoin_rpc_username: None,
      bitcoin_rpc_limit: None,
      chain: Some(Chain::Regtest),
      commit_interval: None,
      savepoint_interval: None,
      max_savepoints: None,
      config: None,
      config_dir: None,
      cookie_file: None,
      data_dir: Some(dir.into()),
      height_limit: None,
      hidden: None,
      http_port: None,
      index: None,
      index_addresses: true,
      index_cache_size: None,
      index_runes: true,
      index_sats: true,
      index_transactions: false,
      integration_test: false,
      no_index_inscriptions: false,
      server_password: None,
      server_url: Some(server_url.into()),
      server_username: None,
    }
  }

  pub fn or_defaults(self) -> Result<Self> {
    let chain = self.chain.unwrap_or_default();

    let bitcoin_data_dir = match &self.bitcoin_data_dir {
      Some(bitcoin_data_dir) => bitcoin_data_dir.clone(),
      None => {
        if cfg!(target_os = "linux") {
          dirs::home_dir()
            .ok_or_else(|| anyhow!("failed to get cookie file path: could not get home dir"))?
            .join(".bitcoin")
        } else {
          dirs::data_dir()
            .ok_or_else(|| anyhow!("failed to get cookie file path: could not get data dir"))?
            .join("Bitcoin")
        }
      }
    };

    let cookie_file = match self.cookie_file {
      Some(cookie_file) => cookie_file,
      None => chain.join_with_data_dir(&bitcoin_data_dir).join(".cookie"),
    };

    let data_dir = chain.join_with_data_dir(match &self.data_dir {
      Some(data_dir) => data_dir.clone(),
      None => Self::default_data_dir()?,
    });

    let index = match &self.index {
      Some(path) => path.clone(),
      None => data_dir.join("index.redb"),
    };

    Ok(Self {
      bitcoin_data_dir: Some(bitcoin_data_dir),
      bitcoin_rpc_limit: Some(self.bitcoin_rpc_limit.unwrap_or(12)),
      bitcoin_rpc_password: self.bitcoin_rpc_password,
      bitcoin_rpc_url: Some(
        self
          .bitcoin_rpc_url
          .clone()
          .unwrap_or_else(|| format!("127.0.0.1:{}", chain.default_rpc_port())),
      ),
      bitcoin_rpc_username: self.bitcoin_rpc_username,
      chain: Some(chain),
      commit_interval: Some(self.commit_interval.unwrap_or(5000)),
      savepoint_interval: Some(self.savepoint_interval.unwrap_or(10)),
      max_savepoints: Some(self.max_savepoints.unwrap_or(2)),
      config: None,
      config_dir: None,
      cookie_file: Some(cookie_file),
      data_dir: Some(data_dir),
      height_limit: self.height_limit,
      hidden: self.hidden,
      http_port: self.http_port,
      index: Some(index),
      index_addresses: self.index_addresses,
      index_cache_size: Some(match self.index_cache_size {
        Some(index_cache_size) => index_cache_size,
        None => {
          let mut sys = System::new();
          sys.refresh_memory();
          usize::try_from(sys.total_memory() / 4)?
        }
      }),
      index_runes: self.index_runes,
      index_sats: self.index_sats,
      index_transactions: self.index_transactions,
      integration_test: self.integration_test,
      no_index_inscriptions: self.no_index_inscriptions,
      server_password: self.server_password,
      server_url: self.server_url,
      server_username: self.server_username,
    })
  }

  pub fn default_data_dir() -> Result<PathBuf> {
    Ok(
      dirs::data_dir()
        .context("could not get data dir")?
        .join("ord"),
    )
  }

  pub fn bitcoin_credentials(&self) -> Result<Auth> {
    if let Some((user, pass)) = &self
      .bitcoin_rpc_username
      .as_ref()
      .zip(self.bitcoin_rpc_password.as_ref())
    {
      Ok(Auth::UserPass((*user).clone(), (*pass).clone()))
    } else {
      Ok(Auth::CookieFile(self.cookie_file()?))
    }
  }

  pub fn bitcoin_rpc_client(&self, wallet: Option<String>) -> Result<Client> {
    let rpc_url = self.bitcoin_rpc_url(wallet);

    let bitcoin_credentials = self.bitcoin_credentials()?;

    log::trace!(
      "Connecting to Bitcoin Core at {}",
      self.bitcoin_rpc_url(None)
    );

    if let Auth::CookieFile(cookie_file) = &bitcoin_credentials {
      log::trace!(
        "Using credentials from cookie file at `{}`",
        cookie_file.display()
      );

      ensure!(
        cookie_file.is_file(),
        "cookie file `{}` does not exist",
        cookie_file.display()
      );
    }

    let client = Client::new(&rpc_url, bitcoin_credentials.clone()).with_context(|| {
      format!(
        "failed to connect to Bitcoin Core RPC at `{rpc_url}` with {}",
        match bitcoin_credentials {
          Auth::None => "no credentials".into(),
          Auth::UserPass(_, _) => "username and password".into(),
          Auth::CookieFile(cookie_file) => format!("cookie file at {}", cookie_file.display()),
        }
      )
    })?;

    let mut checks = 0;
    let rpc_chain = loop {
      match client.get_blockchain_info() {
        Ok(blockchain_info) => {
          break match blockchain_info.chain.to_string().as_str() {
            "bitcoin" => Chain::Mainnet,
            "regtest" => Chain::Regtest,
            "signet" => Chain::Signet,
            "testnet" => Chain::Testnet,
            "testnet4" => Chain::Testnet4,
            other => bail!("Bitcoin RPC server on unknown chain: {other}"),
          }
        }
        Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
          if err.code == -28 => {}
        Err(err) if err.to_string().contains("Resource temporarily unavailable") => {}
        Err(err) => bail!("Failed to connect to Bitcoin Core RPC at `{rpc_url}`:  {err}"),
      }

      ensure! {
        checks < 100,
        "Failed to connect to Bitcoin Core RPC at `{rpc_url}`",
      }

      checks += 1;
      thread::sleep(Duration::from_millis(100));
    };

    let ord_chain = self.chain();

    if rpc_chain != ord_chain {
      bail!("Bitcoin RPC server is on {rpc_chain} but ord is on {ord_chain}");
    }

    Ok(client)
  }

  pub fn chain(&self) -> Chain {
    self.chain.unwrap()
  }

  pub fn commit_interval(&self) -> usize {
    self.commit_interval.unwrap()
  }

  pub fn savepoint_interval(&self) -> usize {
    self.savepoint_interval.unwrap()
  }

  pub fn max_savepoints(&self) -> usize {
    self.max_savepoints.unwrap()
  }

  pub fn cookie_file(&self) -> Result<PathBuf> {
    if let Some(cookie_file) = &self.cookie_file {
      return Ok(cookie_file.clone());
    }

    let path = if let Some(bitcoin_data_dir) = &self.bitcoin_data_dir {
      bitcoin_data_dir.clone()
    } else if cfg!(target_os = "linux") {
      dirs::home_dir()
        .ok_or_else(|| anyhow!("failed to get cookie file path: could not get home dir"))?
        .join(".bitcoin")
    } else {
      dirs::data_dir()
        .ok_or_else(|| anyhow!("failed to get cookie file path: could not get data dir"))?
        .join("Bitcoin")
    };

    let path = self.chain().join_with_data_dir(path);

    Ok(path.join(".cookie"))
  }

  pub fn credentials(&self) -> Option<(&str, &str)> {
    self
      .server_username
      .as_deref()
      .zip(self.server_password.as_deref())
  }

  pub fn data_dir(&self) -> PathBuf {
    self.data_dir.as_ref().unwrap().into()
  }

  pub fn first_inscription_height(&self) -> u32 {
    if self.integration_test {
      0
    } else {
      self.chain.unwrap().first_inscription_height()
    }
  }

  pub fn first_rune_height(&self) -> u32 {
    if self.integration_test {
      0
    } else {
      self.chain.unwrap().first_rune_height()
    }
  }

  pub fn height_limit(&self) -> Option<u32> {
    self.height_limit
  }

  pub fn index(&self) -> &Path {
    self.index.as_ref().unwrap()
  }

  pub fn index_addresses_raw(&self) -> bool {
    self.index_addresses
  }

  pub fn index_inscriptions_raw(&self) -> bool {
    !self.no_index_inscriptions
  }

  pub fn index_runes_raw(&self) -> bool {
    self.index_runes
  }

  pub fn index_cache_size(&self) -> usize {
    self.index_cache_size.unwrap()
  }

  pub fn index_sats_raw(&self) -> bool {
    self.index_sats
  }

  pub fn index_transactions_raw(&self) -> bool {
    self.index_transactions
  }

  pub fn integration_test(&self) -> bool {
    self.integration_test
  }

  pub fn is_hidden(&self, inscription_id: InscriptionId) -> bool {
    self
      .hidden
      .as_ref()
      .map(|hidden| hidden.contains(&inscription_id))
      .unwrap_or_default()
  }

  pub fn bitcoin_rpc_url(&self, wallet_name: Option<String>) -> String {
    let base_url = self.bitcoin_rpc_url.as_ref().unwrap();
    match wallet_name {
      Some(wallet_name) => format!("{base_url}/wallet/{wallet_name}"),
      None => format!("{base_url}/"),
    }
  }

  pub fn bitcoin_rpc_limit(&self) -> u32 {
    self.bitcoin_rpc_limit.unwrap()
  }

  pub fn server_url(&self) -> Option<&str> {
    self.server_url.as_deref()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  fn parse(args: &[&str]) -> Settings {
    let args = iter::once("ord")
      .chain(args.iter().copied())
      .collect::<Vec<&str>>();
    Settings::from_options(Options::try_parse_from(args).unwrap())
      .or_defaults()
      .unwrap()
  }

  fn wallet(args: &str) -> (Settings, subcommand::wallet::WalletCommand) {
    match Arguments::try_parse_from(args.split_whitespace()) {
      Ok(arguments) => match arguments.subcommand {
        Subcommand::Wallet(wallet) => (
          Settings::from_options(arguments.options)
            .or_defaults()
            .unwrap(),
          wallet,
        ),
        subcommand => panic!("unexpected subcommand: {subcommand:?}"),
      },
      Err(err) => panic!("error parsing arguments: {err}"),
    }
  }

  #[test]
  fn auth_missing_rpc_pass_is_an_error() {
    assert_eq!(
      Settings::merge(
        Options {
          bitcoin_rpc_username: Some("foo".into()),
          ..default()
        },
        Default::default(),
      )
      .unwrap_err()
      .to_string(),
      "no bitcoin RPC password specified"
    );
  }

  #[test]
  fn auth_missing_rpc_user_is_an_error() {
    assert_eq!(
      Settings::merge(
        Options {
          bitcoin_rpc_password: Some("foo".into()),
          ..default()
        },
        Default::default(),
      )
      .unwrap_err()
      .to_string(),
      "no bitcoin RPC username specified"
    );
  }

  #[test]
  fn auth_with_user_and_pass() {
    assert_eq!(
      parse(&["--bitcoin-rpc-username=foo", "--bitcoin-rpc-password=bar"])
        .bitcoin_credentials()
        .unwrap(),
      Auth::UserPass("foo".into(), "bar".into())
    );
  }

  #[test]
  fn auth_with_cookie_file() {
    assert_eq!(
      parse(&["--cookie-file=/var/lib/Bitcoin/.cookie"])
        .bitcoin_credentials()
        .unwrap(),
      Auth::CookieFile("/var/lib/Bitcoin/.cookie".into())
    );
  }

  #[test]
  fn cookie_file_does_not_exist_error() {
    assert_eq!(
      parse(&["--cookie-file=/foo/bar/baz/qux/.cookie"])
        .bitcoin_rpc_client(None)
        .err()
        .unwrap()
        .to_string(),
      "cookie file `/foo/bar/baz/qux/.cookie` does not exist"
    );
  }

  #[test]
  fn rpc_server_chain_must_match() {
    let core = mockcore::builder().network(Network::Testnet).build();

    let settings = parse(&[
      "--cookie-file",
      core.cookie_file().to_str().unwrap(),
      "--bitcoin-rpc-url",
      &core.url(),
    ]);

    assert_eq!(
      settings.bitcoin_rpc_client(None).unwrap_err().to_string(),
      "Bitcoin RPC server is on testnet but ord is on mainnet"
    );
  }

  #[test]
  fn rpc_url_overrides_network() {
    assert_eq!(
      parse(&["--bitcoin-rpc-url=127.0.0.1:1234", "--chain=signet"]).bitcoin_rpc_url(None),
      "127.0.0.1:1234/"
    );
  }

  #[test]
  fn cookie_file_overrides_network() {
    assert_eq!(
      parse(&["--cookie-file=/foo/bar", "--chain=signet"])
        .cookie_file()
        .unwrap(),
      Path::new("/foo/bar")
    );
  }

  #[test]
  fn use_default_network() {
    let settings = parse(&[]);

    assert_eq!(settings.bitcoin_rpc_url(None), "127.0.0.1:8332/");

    assert!(settings.cookie_file().unwrap().ends_with(".cookie"));
  }

  #[test]
  fn uses_network_defaults() {
    let settings = parse(&["--chain=signet"]);

    assert_eq!(settings.bitcoin_rpc_url(None), "127.0.0.1:38332/");

    assert!(settings
      .cookie_file()
      .unwrap()
      .display()
      .to_string()
      .ends_with(if cfg!(windows) {
        r"\signet\.cookie"
      } else {
        "/signet/.cookie"
      }));
  }

  #[test]
  fn mainnet_cookie_file_path() {
    let cookie_file = parse(&[]).cookie_file().unwrap().display().to_string();

    assert!(cookie_file.ends_with(if cfg!(target_os = "linux") {
      "/.bitcoin/.cookie"
    } else if cfg!(windows) {
      r"\Bitcoin\.cookie"
    } else {
      "/Bitcoin/.cookie"
    }))
  }

  #[test]
  fn othernet_cookie_file_path() {
    let cookie_file = parse(&["--chain=signet"])
      .cookie_file()
      .unwrap()
      .display()
      .to_string();

    assert!(cookie_file.ends_with(if cfg!(target_os = "linux") {
      "/.bitcoin/signet/.cookie"
    } else if cfg!(windows) {
      r"\Bitcoin\signet\.cookie"
    } else {
      "/Bitcoin/signet/.cookie"
    }));

    let cookie_file = parse(&["--testnet4"])
      .cookie_file()
      .unwrap()
      .display()
      .to_string();

    assert!(cookie_file.ends_with(if cfg!(target_os = "linux") {
      "/.bitcoin/testnet4/.cookie"
    } else if cfg!(windows) {
      r"\Bitcoin\testnet4\.cookie"
    } else {
      "/Bitcoin/testnet4/.cookie"
    }));
  }

  #[test]
  fn cookie_file_defaults_to_bitcoin_data_dir() {
    let cookie_file = parse(&["--bitcoin-data-dir=foo", "--chain=signet"])
      .cookie_file()
      .unwrap()
      .display()
      .to_string();

    assert!(cookie_file.ends_with(if cfg!(windows) {
      r"foo\signet\.cookie"
    } else {
      "foo/signet/.cookie"
    }));
  }

  #[test]
  fn mainnet_data_dir() {
    let data_dir = parse(&[]).data_dir().display().to_string();
    assert!(
      data_dir.ends_with(if cfg!(windows) { r"\ord" } else { "/ord" }),
      "{data_dir}"
    );
  }

  #[test]
  fn othernet_data_dir() {
    let data_dir = parse(&["--chain=signet"]).data_dir().display().to_string();
    assert!(
      data_dir.ends_with(if cfg!(windows) {
        r"\ord\signet"
      } else {
        "/ord/signet"
      }),
      "{data_dir}"
    );
  }

  #[test]
  fn network_is_joined_with_data_dir() {
    let data_dir = parse(&["--chain=signet", "--datadir=foo"])
      .data_dir()
      .display()
      .to_string();
    assert!(
      data_dir.ends_with(if cfg!(windows) {
        r"foo\signet"
      } else {
        "foo/signet"
      }),
      "{data_dir}"
    );
  }

  #[test]
  fn network_accepts_aliases() {
    #[track_caller]
    fn check_network_alias(alias: &str, suffix: &str) {
      let data_dir = parse(&["--chain", alias]).data_dir().display().to_string();

      assert!(data_dir.ends_with(suffix), "{data_dir}");
    }

    check_network_alias("main", "ord");
    check_network_alias("mainnet", "ord");
    check_network_alias(
      "regtest",
      if cfg!(windows) {
        r"ord\regtest"
      } else {
        "ord/regtest"
      },
    );
    check_network_alias(
      "signet",
      if cfg!(windows) {
        r"ord\signet"
      } else {
        "ord/signet"
      },
    );
    check_network_alias(
      "test",
      if cfg!(windows) {
        r"ord\testnet3"
      } else {
        "ord/testnet3"
      },
    );
    check_network_alias(
      "testnet",
      if cfg!(windows) {
        r"ord\testnet3"
      } else {
        "ord/testnet3"
      },
    );
    check_network_alias(
      "testnet4",
      if cfg!(windows) {
        r"ord\testnet4"
      } else {
        "ord/testnet4"
      },
    );
  }

  #[test]
  fn chain_flags() {
    Arguments::try_parse_from(["ord", "--signet", "--chain", "signet", "index", "update"])
      .unwrap_err();
    assert_eq!(parse(&["--signet"]).chain(), Chain::Signet);
    assert_eq!(parse(&["-s"]).chain(), Chain::Signet);

    Arguments::try_parse_from(["ord", "--regtest", "--chain", "signet", "index", "update"])
      .unwrap_err();
    assert_eq!(parse(&["--regtest"]).chain(), Chain::Regtest);
    assert_eq!(parse(&["-r"]).chain(), Chain::Regtest);

    Arguments::try_parse_from(["ord", "--testnet", "--chain", "signet", "index", "update"])
      .unwrap_err();
    assert_eq!(parse(&["--testnet"]).chain(), Chain::Testnet);
    assert_eq!(parse(&["-t"]).chain(), Chain::Testnet);
  }

  #[test]
  fn wallet_flag_overrides_default_name() {
    assert_eq!(wallet("ord wallet create").1.name, "ord");
    assert_eq!(wallet("ord wallet --name foo create").1.name, "foo")
  }

  #[test]
  fn uses_wallet_rpc() {
    let (settings, _) = wallet("ord wallet --name foo balance");

    assert_eq!(
      settings.bitcoin_rpc_url(Some("foo".into())),
      "127.0.0.1:8332/wallet/foo"
    );
  }

  #[test]
  fn setting_index_cache_size() {
    assert_eq!(
      parse(&["--index-cache-size=16000000000",]).index_cache_size(),
      16000000000
    );
  }

  #[test]
  fn setting_commit_interval() {
    let arguments =
      Arguments::try_parse_from(["ord", "--commit-interval", "500", "index", "update"]).unwrap();
    assert_eq!(arguments.options.commit_interval, Some(500));
  }

  #[test]
  fn setting_savepoint_interval() {
    let arguments =
      Arguments::try_parse_from(["ord", "--savepoint-interval", "500", "index", "update"]).unwrap();
    assert_eq!(arguments.options.savepoint_interval, Some(500));
  }

  #[test]
  fn setting_max_savepoints() {
    let arguments =
      Arguments::try_parse_from(["ord", "--max-savepoints", "10", "index", "update"]).unwrap();
    assert_eq!(arguments.options.max_savepoints, Some(10));
  }

  #[test]
  fn index_runes() {
    assert!(parse(&["--chain=signet", "--index-runes"]).index_runes_raw());
    assert!(parse(&["--index-runes"]).index_runes_raw());
    assert!(!parse(&[]).index_runes_raw());
  }

  #[test]
  fn bitcoin_rpc_and_pass_setting() {
    let config = Settings {
      bitcoin_rpc_username: Some("config_user".into()),
      bitcoin_rpc_password: Some("config_pass".into()),
      ..default()
    };

    let tempdir = TempDir::new().unwrap();

    let config_path = tempdir.path().join("ord.yaml");

    fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap();

    assert_eq!(
      Settings::merge(
        Options {
          bitcoin_rpc_username: Some("option_user".into()),
          bitcoin_rpc_password: Some("option_pass".into()),
          config: Some(config_path.clone()),
          ..default()
        },
        vec![
          ("BITCOIN_RPC_USERNAME".into(), "env_user".into()),
          ("BITCOIN_RPC_PASSWORD".into(), "env_pass".into()),
        ]
        .into_iter()
        .collect(),
      )
      .unwrap()
      .bitcoin_credentials()
      .unwrap(),
      Auth::UserPass("option_user".into(), "option_pass".into()),
    );

    assert_eq!(
      Settings::merge(
        Options {
          config: Some(config_path.clone()),
          ..default()
        },
        vec![
          ("BITCOIN_RPC_USERNAME".into(), "env_user".into()),
          ("BITCOIN_RPC_PASSWORD".into(), "env_pass".into()),
        ]
        .into_iter()
        .collect(),
      )
      .unwrap()
      .bitcoin_credentials()
      .unwrap(),
      Auth::UserPass("env_user".into(), "env_pass".into()),
    );

    assert_eq!(
      Settings::merge(
        Options {
          config: Some(config_path),
          ..default()
        },
        Default::default(),
      )
      .unwrap()
      .bitcoin_credentials()
      .unwrap(),
      Auth::UserPass("config_user".into(), "config_pass".into()),
    );

    assert_matches!(
      Settings::merge(Default::default(), Default::default())
        .unwrap()
        .bitcoin_credentials()
        .unwrap(),
      Auth::CookieFile(_),
    );
  }

  #[test]
  fn example_config_file_is_valid() {
    let _: Settings = serde_yaml::from_reader(File::open("ord.yaml").unwrap()).unwrap();
  }

  #[test]
  fn from_env() {
    let env = vec![
      ("BITCOIN_DATA_DIR", "/bitcoin/data/dir"),
      ("BITCOIN_RPC_LIMIT", "12"),
      ("BITCOIN_RPC_PASSWORD", "bitcoin password"),
      ("BITCOIN_RPC_URL", "url"),
      ("BITCOIN_RPC_USERNAME", "bitcoin username"),
      ("CHAIN", "signet"),
      ("COMMIT_INTERVAL", "1"),
      ("SAVEPOINT_INTERVAL", "10"),
      ("MAX_SAVEPOINTS", "2"),
      ("CONFIG", "config"),
      ("CONFIG_DIR", "config dir"),
      ("COOKIE_FILE", "cookie file"),
      ("DATA_DIR", "/data/dir"),
      ("HEIGHT_LIMIT", "3"),
      ("HIDDEN", "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0 703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0"),
    ("HTTP_PORT", "8080"),
      ("INDEX", "index"),
      ("INDEX_CACHE_SIZE", "4"),
      ("INDEX_ADDRESSES", "1"),
      ("INDEX_RUNES", "1"),
      ("INDEX_SATS", "1"),
      ("INDEX_TRANSACTIONS", "1"),
      ("INTEGRATION_TEST", "1"),
      ("NO_INDEX_INSCRIPTIONS", "1"),
      ("SERVER_PASSWORD", "server password"),
      ("SERVER_URL", "server url"),
      ("SERVER_USERNAME", "server username"),
    ]
    .into_iter()
    .map(|(key, value)| (key.into(), value.into()))
    .collect::<BTreeMap<String, String>>();

    pretty_assert_eq!(
      Settings::from_env(env).unwrap(),
      Settings {
        bitcoin_data_dir: Some("/bitcoin/data/dir".into()),
        bitcoin_rpc_limit: Some(12),
        bitcoin_rpc_password: Some("bitcoin password".into()),
        bitcoin_rpc_url: Some("url".into()),
        bitcoin_rpc_username: Some("bitcoin username".into()),
        chain: Some(Chain::Signet),
        commit_interval: Some(1),
        savepoint_interval: Some(10),
        max_savepoints: Some(2),
        config: Some("config".into()),
        config_dir: Some("config dir".into()),
        cookie_file: Some("cookie file".into()),
        data_dir: Some("/data/dir".into()),
        height_limit: Some(3),
        hidden: Some(
          vec![
            "6fb976ab49dcec017f1e201e84395983204ae1a7c2abf7ced0a85d692e442799i0"
              .parse()
              .unwrap(),
            "703e5f7c49d82aab99e605af306b9a30e991e57d42f982908a962a81ac439832i0"
              .parse()
              .unwrap()
          ]
          .into_iter()
          .collect()
        ),
        http_port: Some(8080),
        index: Some("index".into()),
        index_addresses: true,
        index_cache_size: Some(4),
        index_runes: true,
        index_sats: true,
        index_transactions: true,
        integration_test: true,
        no_index_inscriptions: true,
        server_password: Some("server password".into()),
        server_url: Some("server url".into()),
        server_username: Some("server username".into()),
      }
    );
  }

  #[test]
  fn from_options() {
    pretty_assert_eq!(
      Settings::from_options(
        Options::try_parse_from([
          "ord",
          "--bitcoin-data-dir=/bitcoin/data/dir",
          "--bitcoin-rpc-limit=12",
          "--bitcoin-rpc-password=bitcoin password",
          "--bitcoin-rpc-url=url",
          "--bitcoin-rpc-username=bitcoin username",
          "--chain=signet",
          "--commit-interval=1",
          "--savepoint-interval=10",
          "--max-savepoints=2",
          "--config=config",
          "--config-dir=config dir",
          "--cookie-file=cookie file",
          "--datadir=/data/dir",
          "--height-limit=3",
          "--index-addresses",
          "--index-cache-size=4",
          "--index-runes",
          "--index-sats",
          "--index-transactions",
          "--index=index",
          "--integration-test",
          "--no-index-inscriptions",
          "--server-password=server password",
          "--server-username=server username",
        ])
        .unwrap()
      ),
      Settings {
        bitcoin_data_dir: Some("/bitcoin/data/dir".into()),
        bitcoin_rpc_limit: Some(12),
        bitcoin_rpc_password: Some("bitcoin password".into()),
        bitcoin_rpc_url: Some("url".into()),
        bitcoin_rpc_username: Some("bitcoin username".into()),
        chain: Some(Chain::Signet),
        commit_interval: Some(1),
        savepoint_interval: Some(10),
        max_savepoints: Some(2),
        config: Some("config".into()),
        config_dir: Some("config dir".into()),
        cookie_file: Some("cookie file".into()),
        data_dir: Some("/data/dir".into()),
        height_limit: Some(3),
        hidden: None,
        http_port: None,
        index: Some("index".into()),
        index_addresses: true,
        index_cache_size: Some(4),
        index_runes: true,
        index_sats: true,
        index_transactions: true,
        integration_test: true,
        no_index_inscriptions: true,
        server_password: Some("server password".into()),
        server_url: None,
        server_username: Some("server username".into()),
      }
    );
  }

  #[test]
  fn merge() {
    let env = vec![("INDEX", "env")]
      .into_iter()
      .map(|(key, value)| (key.into(), value.into()))
      .collect::<BTreeMap<String, String>>();

    let config = Settings {
      index: Some("config".into()),
      ..default()
    };

    let tempdir = TempDir::new().unwrap();

    let config_path = tempdir.path().join("ord.yaml");

    fs::write(&config_path, serde_yaml::to_string(&config).unwrap()).unwrap();

    let options =
      Options::try_parse_from(["ord", "--config", config_path.to_str().unwrap()]).unwrap();

    pretty_assert_eq!(
      Settings::merge(options.clone(), Default::default())
        .unwrap()
        .index,
      Some("config".into()),
    );

    pretty_assert_eq!(
      Settings::merge(options, env.clone()).unwrap().index,
      Some("env".into()),
    );

    let options = Options::try_parse_from([
      "ord",
      "--index=option",
      "--config",
      config_path.to_str().unwrap(),
    ])
    .unwrap();

    pretty_assert_eq!(
      Settings::merge(options, env).unwrap().index,
      Some("option".into()),
    );
  }
}

ord/src/signer.rs


use super::*;

#[derive(Debug, PartialEq, Clone, DeserializeFromStr)]
pub(crate) enum Signer {
  Address(Address<NetworkUnchecked>),
  Inscription(InscriptionId),
  Output(OutPoint),
}

impl FromStr for Signer {
  type Err = SnafuError;

  fn from_str(input: &str) -> Result<Self, Self::Err> {
    if re::ADDRESS.is_match(input) {
      Ok(Signer::Address(
        input.parse().snafu_context(error::AddressParse { input })?,
      ))
    } else if re::OUTPOINT.is_match(input) {
      Ok(Signer::Output(
        input
          .parse()
          .snafu_context(error::OutPointParse { input })?,
      ))
    } else if re::INSCRIPTION_ID.is_match(input) {
      Ok(Signer::Inscription(
        input
          .parse()
          .snafu_context(error::InscriptionIdParse { input })?,
      ))
    } else {
      Err(SnafuError::SignerParse {
        input: input.to_string(),
      })
    }
  }
}

ord/src/subcommand.rs


use super::*;

pub mod balances;
pub mod decode;
pub mod env;
pub mod epochs;
pub mod find;
pub mod index;
pub mod list;
pub mod parse;
pub mod runes;
pub mod server;
mod settings;
pub mod subsidy;
pub mod supply;
pub mod teleburn;
pub mod traits;
pub mod verify;
pub mod wallet;
pub mod wallets;

#[derive(Debug, Parser)]
pub(crate) enum Subcommand {
  #[command(about = "List all rune balances")]
  Balances,
  #[command(about = "Decode a transaction")]
  Decode(decode::Decode),
  #[command(about = "Start a regtest ord and bitcoind instance")]
  Env(env::Env),
  #[command(about = "List the first satoshis of each reward epoch")]
  Epochs,
  #[command(about = "Find a satoshi's current location")]
  Find(find::Find),
  #[command(subcommand, about = "Index commands")]
  Index(index::IndexSubcommand),
  #[command(about = "List the satoshis in an output")]
  List(list::List),
  #[command(about = "Parse a satoshi from ordinal notation")]
  Parse(parse::Parse),
  #[command(about = "List all runes")]
  Runes,
  #[command(about = "Run the explorer server")]
  Server(server::Server),
  #[command(about = "Display settings")]
  Settings,
  #[command(about = "Display information about a block's subsidy")]
  Subsidy(subsidy::Subsidy),
  #[command(about = "Display Bitcoin supply information")]
  Supply,
  #[command(about = "Generate teleburn addresses")]
  Teleburn(teleburn::Teleburn),
  #[command(about = "Display satoshi traits")]
  Traits(traits::Traits),
  #[command(about = "Verify BIP322 signature")]
  Verify(verify::Verify),
  #[command(about = "Wallet commands")]
  Wallet(wallet::WalletCommand),
  #[command(about = "List all Bitcoin Core wallets")]
  Wallets,
}

impl Subcommand {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    match self {
      Self::Balances => balances::run(settings),
      Self::Decode(decode) => decode.run(settings),
      Self::Env(env) => env.run(),
      Self::Epochs => epochs::run(),
      Self::Find(find) => find.run(settings),
      Self::Index(index) => index.run(settings),
      Self::List(list) => list.run(settings),
      Self::Parse(parse) => parse.run(),
      Self::Runes => runes::run(settings),
      Self::Server(server) => {
        let index = Arc::new(Index::open(&settings)?);
        let handle = axum_server::Handle::new();
        LISTENERS.lock().unwrap().push(handle.clone());
        server.run(settings, index, handle)
      }
      Self::Settings => settings::run(settings),
      Self::Subsidy(subsidy) => subsidy.run(),
      Self::Supply => supply::run(),
      Self::Teleburn(teleburn) => teleburn.run(),
      Self::Traits(traits) => traits.run(),
      Self::Verify(verify) => verify.run(),
      Self::Wallet(wallet) => wallet.run(settings),
      Self::Wallets => wallets::run(settings),
    }
  }
}

#[derive(clap::ValueEnum, Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub enum OutputFormat {
  #[default]
  Json,
  Yaml,
  Minify,
}

pub trait Output: Send {
  fn print(&self, format: OutputFormat);
}

impl<T> Output for T
where
  T: Serialize + Send,
{
  fn print(&self, format: OutputFormat) {
    match format {
      OutputFormat::Json => serde_json::to_writer_pretty(io::stdout(), self).ok(),
      OutputFormat::Yaml => serde_yaml::to_writer(io::stdout(), self).ok(),
      OutputFormat::Minify => serde_json::to_writer(io::stdout(), self).ok(),
    };
    println!();
  }
}

pub(crate) type SubcommandResult = Result<Option<Box<dyn Output>>>;

ord/src/subcommand/balances.rs


use super::*;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub runes: BTreeMap<SpacedRune, BTreeMap<OutPoint, Pile>>,
}

pub(crate) fn run(settings: Settings) -> SubcommandResult {
  let index = Index::open(&settings)?;

  ensure!(
    index.has_rune_index(),
    "`ord balances` requires index created with `--index-runes` flag",
  );

  index.update()?;

  Ok(Some(Box::new(Output {
    runes: index.get_rune_balance_map()?,
  })))
}

ord/src/subcommand/decode.rs


use super::*;

#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct CompactOutput {
  pub inscriptions: Vec<CompactInscription>,
  pub runestone: Option<Artifact>,
}

#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct RawOutput {
  pub inscriptions: Vec<ParsedEnvelope>,
  pub runestone: Option<Artifact>,
}

#[serde_with::skip_serializing_none]
#[derive(Serialize, Eq, PartialEq, Deserialize, Debug)]
pub struct CompactInscription {
  pub body: Option<String>,
  pub content_encoding: Option<String>,
  pub content_type: Option<String>,
  #[serde(default, skip_serializing_if = "std::ops::Not::not")]
  pub duplicate_field: bool,
  #[serde(default, skip_serializing_if = "std::ops::Not::not")]
  pub incomplete_field: bool,
  pub metadata: Option<String>,
  pub metaprotocol: Option<String>,
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub parents: Vec<InscriptionId>,
  pub pointer: Option<u64>,
  #[serde(default, skip_serializing_if = "std::ops::Not::not")]
  pub unrecognized_even_field: bool,
}

impl TryFrom<Inscription> for CompactInscription {
  type Error = Error;

  fn try_from(inscription: Inscription) -> Result<Self> {
    Ok(Self {
      content_encoding: inscription
        .content_encoding()
        .map(|header_value| header_value.to_str().map(str::to_string))
        .transpose()?,
      content_type: inscription.content_type().map(str::to_string),
      metaprotocol: inscription.metaprotocol().map(str::to_string),
      parents: inscription.parents(),
      pointer: inscription.pointer(),
      body: inscription.body.map(hex::encode),
      duplicate_field: inscription.duplicate_field,
      incomplete_field: inscription.incomplete_field,
      metadata: inscription.metadata.map(hex::encode),
      unrecognized_even_field: inscription.unrecognized_even_field,
    })
  }
}

#[derive(Debug, Parser)]
pub(crate) struct Decode {
  #[arg(
    long,
    conflicts_with = "file",
    help = "Fetch transaction with <TXID> from Bitcoin Core."
  )]
  txid: Option<Txid>,
  #[arg(long, conflicts_with = "txid", help = "Load transaction from <FILE>.")]
  file: Option<PathBuf>,
  #[arg(
    long,
    help = "Serialize inscriptions in a compact, human-readable format."
  )]
  compact: bool,
}

impl Decode {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    let transaction = if let Some(txid) = self.txid {
      settings
        .bitcoin_rpc_client(None)?
        .get_raw_transaction(&txid, None)?
    } else if let Some(file) = self.file {
      Transaction::consensus_decode(&mut io::BufReader::new(File::open(file)?))?
    } else {
      Transaction::consensus_decode(&mut io::BufReader::new(io::stdin()))?
    };

    let inscriptions = ParsedEnvelope::from_transaction(&transaction);

    let runestone = Runestone::decipher(&transaction);

    if self.compact {
      Ok(Some(Box::new(CompactOutput {
        inscriptions: inscriptions
          .clone()
          .into_iter()
          .map(|inscription| inscription.payload.try_into())
          .collect::<Result<Vec<CompactInscription>>>()?,
        runestone,
      })))
    } else {
      Ok(Some(Box::new(RawOutput {
        inscriptions,
        runestone,
      })))
    }
  }
}

ord/src/subcommand/env.rs


use {super::*, crate::wallet::batch, colored::Colorize, std::net::TcpListener};

struct KillOnDrop(process::Child);

impl Drop for KillOnDrop {
  fn drop(&mut self) {
    let _ = Command::new("kill").arg(self.0.id().to_string()).status();

    let _ = self.0.kill();

    let _ = self.0.wait();
  }
}

#[derive(Debug, Parser)]
pub(crate) struct Env {
  #[arg(default_value = "env", help = "Create env in <DIRECTORY>.")]
  directory: PathBuf,
  #[arg(
    long,
    help = "Decompress encoded content. Currently only supports brotli. Be careful using this on production instances. A decompressed inscription may be arbitrarily large, making decompression a DoS vector."
  )]
  pub(crate) decompress: bool,
  #[arg(
    long,
    help = "Proxy `/content/INSCRIPTION_ID` and other recursive endpoints to `<PROXY>` if the inscription is not present on current chain."
  )]
  pub(crate) proxy: Option<Url>,
}

#[derive(Serialize)]
struct Info {
  bitcoin_cli_command: Vec<String>,
  bitcoind_port: u16,
  ord_port: u16,
  ord_wallet_command: Vec<String>,
}

impl Env {
  pub(crate) fn run(self) -> SubcommandResult {
    let bitcoind_port = TcpListener::bind("127.0.0.1:9000")
      .ok()
      .map(|listener| listener.local_addr().unwrap().port());

    let ord_port = TcpListener::bind("127.0.0.1:9001")
      .ok()
      .map(|listener| listener.local_addr().unwrap().port());

    let (bitcoind_port, ord_port) = (
      bitcoind_port.unwrap_or(TcpListener::bind("127.0.0.1:0")?.local_addr()?.port()),
      ord_port.unwrap_or(TcpListener::bind("127.0.0.1:0")?.local_addr()?.port()),
    );

    let relative = self.directory.to_str().unwrap().to_string();
    let absolute = std::env::current_dir()?.join(&self.directory);
    let absolute_str = absolute
      .to_str()
      .with_context(|| format!("directory `{}` is not valid unicode", absolute.display()))?;

    fs::create_dir_all(&absolute)?;

    let bitcoin_conf = absolute.join("bitcoin.conf");

    if !bitcoin_conf.try_exists()? {
      fs::write(
        bitcoin_conf,
        format!(
          "datacarriersize=1000000
regtest=1
datadir={absolute_str}
listen=0
txindex=1
[regtest]
rpcport={bitcoind_port}
",
        ),
      )?;
    }

    fs::write(absolute.join("inscription.txt"), "FOO")?;

    let yaml = serde_yaml::to_string(&batch::File {
      etching: Some(batch::Etching {
        divisibility: 0,
        rune: "FOO".parse::<SpacedRune>().unwrap(),
        supply: "2000".parse().unwrap(),
        premine: "1000".parse().unwrap(),
        symbol: '¢',
        terms: Some(batch::Terms {
          amount: "1000".parse().unwrap(),
          cap: 1,
          ..default()
        }),
        turbo: false,
      }),
      inscriptions: vec![batch::Entry {
        file: Some("env/inscription.txt".into()),
        ..default()
      }],
      ..default()
    })
    .unwrap();

    let batch_yaml = absolute.join("batch.yaml");

    if !batch_yaml.try_exists()? {
      fs::write(absolute.join("batch.yaml"), yaml)?;
    }

    let _bitcoind = KillOnDrop(
      Command::new("bitcoind")
        .arg(format!("-conf={}", absolute.join("bitcoin.conf").display()))
        .stdout(Stdio::null())
        .spawn()
        .expect("failed to start bitcoind"),
    );

    loop {
      if absolute.join("regtest/.cookie").try_exists()? {
        break;
      }
    }

    let rpc_url = format!("http://localhost:{bitcoind_port}");

    let server_url = format!("http://127.0.0.1:{ord_port}");

    let config = absolute.join("ord.yaml");

    if !config.try_exists()? {
      fs::write(
        config,
        serde_yaml::to_string(&Settings::for_env(&absolute, &rpc_url, &server_url))?,
      )?;
    }

    let ord = std::env::current_exe()?;

    let decompress = self.decompress;
    let proxy = self.proxy.map(|url| url.to_string());

    let mut command = Command::new(&ord);
    let ord_server = command
      .arg("--datadir")
      .arg(&absolute)
      .arg("server")
      .arg("--polling-interval=100ms")
      .arg("--http-port")
      .arg(ord_port.to_string());

    if decompress {
      ord_server.arg("--decompress");
    }

    if let Some(proxy) = proxy {
      ord_server.arg("--proxy").arg(proxy);
    }

    let _ord = KillOnDrop(ord_server.spawn()?);

    thread::sleep(Duration::from_millis(250));

    if !absolute.join("regtest/wallets/ord").try_exists()? {
      let status = Command::new(&ord)
        .arg("--datadir")
        .arg(&absolute)
        .arg("wallet")
        .arg("create")
        .status()?;

      ensure!(status.success(), "failed to create wallet: {status}");

      let output = Command::new(&ord)
        .arg("--datadir")
        .arg(&absolute)
        .arg("wallet")
        .arg("receive")
        .output()?;

      ensure!(
        output.status.success(),
        "failed to generate receive address: {status}"
      );

      let receive = serde_json::from_slice::<wallet::receive::Output>(&output.stdout)?;

      let status = Command::new("bitcoin-cli")
        .arg(format!("-datadir={relative}"))
        .arg("generatetoaddress")
        .arg("200")
        .arg(
          receive
            .addresses
            .first()
            .cloned()
            .unwrap()
            .require_network(Network::Regtest)?
            .to_string(),
        )
        .status()?;

      ensure!(status.success(), "failed to create wallet: {status}");
    }

    serde_json::to_writer_pretty(
      File::create(self.directory.join("env.json"))?,
      &Info {
        bitcoind_port,
        ord_port,
        bitcoin_cli_command: vec!["bitcoin-cli".into(), format!("-datadir={relative}")],
        ord_wallet_command: vec![
          ord.to_str().unwrap().into(),
          "--datadir".into(),
          absolute.to_str().unwrap().into(),
          "wallet".into(),
        ],
      },
    )?;

    let datadir = if relative
      .chars()
      .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
    {
      relative
    } else {
      format!("'{relative}'")
    };

    eprintln!(
      "{}
{server_url}
{}
bitcoin-cli -datadir={datadir} getblockchaininfo
{}
{} --datadir {datadir} wallet balance",
      "`ord` server URL:".blue().bold(),
      "Example `bitcoin-cli` command:".blue().bold(),
      "Example `ord` command:".blue().bold(),
      ord.display(),
    );

    loop {
      if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
        break;
      }

      thread::sleep(Duration::from_millis(100));
    }

    thread::sleep(Duration::from_secs(5));

    Ok(None)
  }
}

ord/src/subcommand/epochs.rs


use super::*;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub starting_sats: Vec<Sat>,
}

pub(crate) fn run() -> SubcommandResult {
  let mut starting_sats = Vec::new();
  for sat in Epoch::STARTING_SATS {
    starting_sats.push(sat);
  }

  Ok(Some(Box::new(Output { starting_sats })))
}

ord/src/subcommand/find.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Find {
  #[arg(help = "Find output and offset of <SAT>.")]
  sat: Sat,
  #[clap(help = "Find output and offset of all sats in the range [<SAT>, <END>).")]
  end: Option<Sat>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub satpoint: SatPoint,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct FindRangeOutput {
  pub start: u64,
  pub size: u64,
  pub satpoint: SatPoint,
}

impl Find {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    let index = Index::open(&settings)?;

    if !index.has_sat_index() {
      bail!("find requires index created with `--index-sats` flag");
    }

    index.update()?;

    match self.end {
      Some(end) => match index.find_range(self.sat, end)? {
        Some(mut results) => {
          results.sort_by_key(|find_range_output| find_range_output.start);
          Ok(Some(Box::new(results)))
        }
        None => Err(anyhow!("range has not been mined as of index height")),
      },
      None => match index.find(self.sat)? {
        Some(satpoint) => Ok(Some(Box::new(Output { satpoint }))),
        None => Err(anyhow!("sat has not been mined as of index height")),
      },
    }
  }
}

ord/src/subcommand/index.rs


use super::*;

mod export;
pub mod info;
mod update;

#[derive(Debug, Parser)]
pub(crate) enum IndexSubcommand {
  #[command(about = "Write inscription numbers and ids to a tab-separated file")]
  Export(export::Export),
  #[command(about = "Print index statistics")]
  Info(info::Info),
  #[command(about = "Update the index", alias = "run")]
  Update,
}

impl IndexSubcommand {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    match self {
      Self::Export(export) => export.run(settings),
      Self::Info(info) => info.run(settings),
      Self::Update => update::run(settings),
    }
  }
}

ord/src/subcommand/index/export.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Export {
  #[arg(long, help = "Include addresses in export")]
  include_addresses: bool,
  #[arg(long, help = "Write export to <TSV>")]
  tsv: String,
}

impl Export {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    let index = Index::open(&settings)?;

    index.update()?;
    index.export(&self.tsv, self.include_addresses)?;

    Ok(None)
  }
}

ord/src/subcommand/index/info.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Info {
  #[arg(long)]
  transactions: bool,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TransactionsOutput {
  pub start: u32,
  pub end: u32,
  pub count: u32,
  pub elapsed: f64,
}

impl Info {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    let index = Index::open(&settings)?;

    let info = index.info()?;

    if self.transactions {
      let mut output = Vec::new();
      for window in info.transactions.windows(2) {
        let start = &window[0];
        let end = &window[1];
        output.push(TransactionsOutput {
          start: start.starting_block_count,
          end: end.starting_block_count,
          count: end.starting_block_count - start.starting_block_count,
          elapsed: (end.starting_timestamp - start.starting_timestamp) as f64 / 1000.0 / 60.0,
        });
      }
      Ok(Some(Box::new(output)))
    } else {
      Ok(Some(Box::new(info)))
    }
  }
}

ord/src/subcommand/index/update.rs


use super::*;

pub(crate) fn run(settings: Settings) -> SubcommandResult {
  let index = Index::open(&settings)?;

  index.update()?;

  Ok(None)
}

ord/src/subcommand/list.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct List {
  #[arg(help = "List information for <OUTPOINT>.")]
  outpoint: OutPoint,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub address: Option<Address<NetworkUnchecked>>,
  pub indexed: bool,
  pub inscriptions: Option<Vec<InscriptionId>>,
  pub runes: Option<BTreeMap<SpacedRune, Pile>>,
  pub sat_ranges: Option<Vec<Range>>,
  pub script_pubkey: String,
  pub spent: bool,
  pub transaction: String,
  pub value: u64,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Range {
  pub start: u64,
  pub name: String,
  pub offset: u64,
  pub rarity: Rarity,
  pub end: u64,
  pub size: u64,
}

impl List {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    let index = Index::open(&settings)?;

    if !index.has_sat_index() {
      bail!("list requires index created with `--index-sats` flag");
    }

    index.update()?;

    ensure! {
      index.is_output_in_active_chain(self.outpoint)?,
      "output not found"
    }

    let (list, _txout) = match index.get_output_info(self.outpoint)? {
      Some((output, txout)) => (output, txout),
      None => return Ok(None),
    };

    Ok(Some(Box::new(Output {
      address: list.address,
      indexed: list.indexed,
      inscriptions: list.inscriptions,
      runes: list.runes,
      sat_ranges: list.sat_ranges.map(output_ranges),
      script_pubkey: list.script_pubkey.to_asm_string(),
      spent: list.spent,
      transaction: list.transaction.to_string(),
      value: list.value,
    })))
  }
}

fn output_ranges(ranges: Vec<(u64, u64)>) -> Vec<Range> {
  let mut offset = 0;
  ranges
    .into_iter()
    .map(|(start, end)| {
      let size = end - start;
      let output = Range {
        end,
        name: Sat(start).name(),
        offset,
        rarity: Sat(start).rarity(),
        size,
        start,
      };

      offset += size;

      output
    })
    .collect()
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn list_ranges() {
    assert_eq!(
      output_ranges(vec![
        (50 * COIN_VALUE, 55 * COIN_VALUE),
        (10, 100),
        (1050000000000000, 1150000000000000),
      ]),
      vec![
        Range {
          end: 55 * COIN_VALUE,
          name: "nvtcsezkbth".to_string(),
          offset: 0,
          rarity: Rarity::Uncommon,
          size: 5 * COIN_VALUE,
          start: 50 * COIN_VALUE,
        },
        Range {
          end: 100,
          name: "nvtdijuwxlf".to_string(),
          offset: 5 * COIN_VALUE,
          rarity: Rarity::Common,
          size: 90,
          start: 10,
        },
        Range {
          end: 1150000000000000,
          name: "gkjbdrhkfqf".to_string(),
          offset: 5 * COIN_VALUE + 90,
          rarity: Rarity::Epic,
          size: 100000000000000,
          start: 1050000000000000,
        }
      ]
    )
  }
}

ord/src/subcommand/parse.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Parse {
  #[arg(help = "Parse <OBJECT>.")]
  object: Object,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub object: Object,
}

impl Parse {
  pub(crate) fn run(self) -> SubcommandResult {
    Ok(Some(Box::new(Output {
      object: self.object,
    })))
  }
}

ord/src/subcommand/runes.rs


use super::*;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub runes: BTreeMap<Rune, RuneInfo>,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct RuneInfo {
  pub block: u64,
  pub burned: u128,
  pub divisibility: u8,
  pub etching: Txid,
  pub id: RuneId,
  pub mints: u128,
  pub number: u64,
  pub premine: u128,
  pub rune: SpacedRune,
  pub supply: u128,
  pub symbol: Option<char>,
  pub terms: Option<Terms>,
  pub timestamp: DateTime<Utc>,
  pub turbo: bool,
  pub tx: u32,
}

pub(crate) fn run(settings: Settings) -> SubcommandResult {
  let index = Index::open(&settings)?;

  ensure!(
    index.has_rune_index(),
    "`ord runes` requires index created with `--index-runes` flag",
  );

  index.update()?;

  Ok(Some(Box::new(Output {
    runes: index
      .runes()?
      .into_iter()
      .map(
        |(
          id,
          entry @ RuneEntry {
            block,
            burned,
            divisibility,
            etching,
            mints,
            number,
            premine,
            spaced_rune,
            symbol,
            terms,
            timestamp,
            turbo,
          },
        )| {
          (
            spaced_rune.rune,
            RuneInfo {
              block,
              burned,
              divisibility,
              etching,
              id,
              mints,
              number,
              premine,
              rune: spaced_rune,
              supply: entry.supply(),
              symbol,
              terms,
              timestamp: crate::timestamp(timestamp),
              turbo,
              tx: id.tx,
            },
          )
        },
      )
      .collect::<BTreeMap<Rune, RuneInfo>>(),
  })))
}

ord/src/subcommand/server.rs


use {
  self::{
    accept_encoding::AcceptEncoding,
    accept_json::AcceptJson,
    error::{OptionExt, ServerError, ServerResult},
  },
  super::*,
  crate::templates::{
    AddressHtml, BlockHtml, BlocksHtml, ChildrenHtml, ClockSvg, CollectionsHtml, HomeHtml,
    InputHtml, InscriptionHtml, InscriptionsBlockHtml, InscriptionsHtml, OutputHtml, PageContent,
    PageHtml, ParentsHtml, PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml,
    PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml,
    PreviewVideoHtml, RareTxt, RuneHtml, RuneNotFoundHtml, RunesHtml, SatHtml, SatscardHtml,
    TransactionHtml,
  },
  axum::{
    extract::{DefaultBodyLimit, Extension, Json, Path, Query},
    http::{self, header, HeaderMap, HeaderName, HeaderValue, StatusCode, Uri},
    response::{IntoResponse, Redirect, Response},
    routing::{get, post},
    Router,
  },
  axum_server::Handle,
  brotli::Decompressor,
  rust_embed::RustEmbed,
  rustls_acme::{
    acme::{LETS_ENCRYPT_PRODUCTION_DIRECTORY, LETS_ENCRYPT_STAGING_DIRECTORY},
    axum::AxumAcceptor,
    caches::DirCache,
    AcmeConfig,
  },
  std::{str, sync::Arc},
  tokio_stream::StreamExt,
  tower_http::{
    compression::CompressionLayer,
    cors::{Any, CorsLayer},
    set_header::SetResponseHeaderLayer,
    validate_request::ValidateRequestHeaderLayer,
  },
};

pub use server_config::ServerConfig;

mod accept_encoding;
mod accept_json;
mod error;
pub mod query;
mod r;
mod server_config;

enum SpawnConfig {
  Https(AxumAcceptor),
  Http,
  Redirect(String),
}

#[derive(Deserialize)]
pub(crate) struct OutputsQuery {
  #[serde(rename = "type")]
  pub(crate) ty: Option<OutputType>,
}

#[derive(Clone, Copy, Deserialize, Default, PartialEq)]
#[serde(rename_all = "lowercase")]
pub(crate) enum OutputType {
  #[default]
  Any,
  Cardinal,
  Inscribed,
  Runic,
}

#[derive(Deserialize)]
struct Search {
  query: String,
}

#[derive(RustEmbed)]
#[folder = "static"]
struct StaticAssets;

lazy_static! {
  static ref SAT_AT_INDEX_PATH: Regex = Regex::new(r"^/r/sat/[^/]+/at/[^/]+$").unwrap();
}

#[derive(Debug, Parser, Clone)]
pub struct Server {
  #[arg(
    long,
    help = "Listen on <ADDRESS> for incoming requests. [default: 0.0.0.0]"
  )]
  pub(crate) address: Option<String>,
  #[arg(
    long,
    help = "Request ACME TLS certificate for <ACME_DOMAIN>. This ord instance must be reachable at <ACME_DOMAIN>:443 to respond to Let's Encrypt ACME challenges."
  )]
  pub(crate) acme_domain: Vec<String>,
  #[arg(
    long,
    help = "Use <CSP_ORIGIN> in Content-Security-Policy header. Set this to the public-facing URL of your ord instance."
  )]
  pub(crate) csp_origin: Option<String>,
  #[arg(
    long,
    help = "Decompress encoded content. Currently only supports brotli. Be careful using this on production instances. A decompressed inscription may be arbitrarily large, making decompression a DoS vector."
  )]
  pub(crate) decompress: bool,
  #[arg(long, env = "ORD_SERVER_DISABLE_JSON_API", help = "Disable JSON API.")]
  pub(crate) disable_json_api: bool,
  #[arg(
    long,
    help = "Listen on <HTTP_PORT> for incoming HTTP requests. [default: 80]"
  )]
  pub(crate) http_port: Option<u16>,
  #[arg(
    long,
    group = "port",
    help = "Listen on <HTTPS_PORT> for incoming HTTPS requests. [default: 443]"
  )]
  pub(crate) https_port: Option<u16>,
  #[arg(long, help = "Store ACME TLS certificates in <ACME_CACHE>.")]
  pub(crate) acme_cache: Option<PathBuf>,
  #[arg(long, help = "Provide ACME contact <ACME_CONTACT>.")]
  pub(crate) acme_contact: Vec<String>,
  #[arg(long, help = "Serve HTTP traffic on <HTTP_PORT>.")]
  pub(crate) http: bool,
  #[arg(long, help = "Serve HTTPS traffic on <HTTPS_PORT>.")]
  pub(crate) https: bool,
  #[arg(long, help = "Redirect HTTP traffic to HTTPS.")]
  pub(crate) redirect_http_to_https: bool,
  #[arg(long, alias = "nosync", help = "Do not update the index.")]
  pub(crate) no_sync: bool,
  #[arg(
    long,
    help = "Proxy `/content/INSCRIPTION_ID` and other recursive endpoints to `<PROXY>` if the inscription is not present on current chain."
  )]
  pub(crate) proxy: Option<Url>,
  #[arg(
    long,
    default_value = "5s",
    help = "Poll Bitcoin Core every <POLLING_INTERVAL>."
  )]
  pub(crate) polling_interval: humantime::Duration,
}

impl Server {
  pub fn run(self, settings: Settings, index: Arc<Index>, handle: Handle) -> SubcommandResult {
    Runtime::new()?.block_on(async {
      let index_clone = index.clone();
      let integration_test = settings.integration_test();

      let index_thread = thread::spawn(move || loop {
        if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
          break;
        }

        if !self.no_sync {
          if let Err(error) = index_clone.update() {
            log::warn!("Updating index: {error}");
          }
        }

        thread::sleep(if integration_test {
          Duration::from_millis(100)
        } else {
          self.polling_interval.into()
        });
      });

      INDEXER.lock().unwrap().replace(index_thread);

      let settings = Arc::new(settings);
      let acme_domains = self.acme_domains()?;

      let server_config = Arc::new(ServerConfig {
        chain: settings.chain(),
        csp_origin: self.csp_origin.clone(),
        decompress: self.decompress,
        domain: acme_domains.first().cloned(),
        index_sats: index.has_sat_index(),
        json_api_enabled: !self.disable_json_api,
        proxy: self.proxy.clone(),
      });

      // non-recursive endpoints
      let router = Router::new()
        .route("/", get(Self::home))
        .route("/address/{address}", get(Self::address))
        .route("/block/{query}", get(Self::block))
        .route("/blockcount", get(Self::block_count))
        .route("/blocks", get(Self::blocks))
        .route("/bounties", get(Self::bounties))
        .route("/children/{inscription_id}", get(Self::children))
        .route(
          "/children/{inscription_id}/{page}",
          get(Self::children_paginated),
        )
        .route("/clock", get(Self::clock))
        .route("/collections", get(Self::collections))
        .route("/collections/{page}", get(Self::collections_paginated))
        .route("/decode/{txid}", get(Self::decode))
        .route("/faq", get(Self::faq))
        .route("/favicon.ico", get(Self::favicon))
        .route("/feed.xml", get(Self::feed))
        .route("/input/{block}/{transaction}/{input}", get(Self::input))
        .route("/inscription/{inscription_query}", get(Self::inscription))
        .route(
          "/inscription/{inscription_query}/{child}",
          get(Self::inscription_child),
        )
        .route("/inscriptions", get(Self::inscriptions))
        .route("/inscriptions", post(Self::inscriptions_json))
        .route(
          "/inscriptions/block/{height}",
          get(Self::inscriptions_in_block),
        )
        .route(
          "/inscriptions/block/{height}/{page}",
          get(Self::inscriptions_in_block_paginated),
        )
        .route("/inscriptions/{page}", get(Self::inscriptions_paginated))
        .route("/install.sh", get(Self::install_script))
        .route("/ordinal/{sat}", get(Self::ordinal))
        .route("/output/{output}", get(Self::output))
        .route("/outputs", post(Self::outputs))
        .route("/outputs/{address}", get(Self::outputs_address))
        .route("/parents/{inscription_id}", get(Self::parents))
        .route(
          "/parents/{inscription_id}/{page}",
          get(Self::parents_paginated),
        )
        .route("/preview/{inscription_id}", get(Self::preview))
        .route("/rare.txt", get(Self::rare_txt))
        .route("/rune/{rune}", get(Self::rune))
        .route("/runes", get(Self::runes))
        .route("/runes/{page}", get(Self::runes_paginated))
        .route("/sat/{sat}", get(Self::sat))
        .route("/satpoint/{satpoint}", get(Self::satpoint))
        .route("/satscard", get(Self::satscard))
        .route("/search", get(Self::search_by_query))
        .route("/search/{*query}", get(Self::search_by_path))
        .route("/static/{*path}", get(Self::static_asset))
        .route("/status", get(Self::status))
        .route("/tx/{txid}", get(Self::transaction))
        .route("/update", get(Self::update));

      // recursive endpoints
      let router = router
        .route("/blockhash", get(r::blockhash_string))
        .route("/blockhash/{height}", get(r::block_hash_from_height_string))
        .route("/blockheight", get(r::blockheight_string))
        .route("/blocktime", get(r::blocktime_string))
        .route("/r/blockhash", get(r::blockhash))
        .route("/r/blockhash/{height}", get(r::blockhash_at_height))
        .route("/r/blockheight", get(r::blockheight_string))
        .route("/r/blockinfo/{query}", get(r::blockinfo))
        .route("/r/blocktime", get(r::blocktime_string))
        .route(
          "/r/children/{inscription_id}/inscriptions",
          get(r::children_inscriptions),
        )
        .route(
          "/r/children/{inscription_id}/inscriptions/{page}",
          get(r::children_inscriptions_paginated),
        )
        .route("/r/parents/{inscription_id}", get(r::parents))
        .route(
          "/r/parents/{inscription_id}/{page}",
          get(r::parents_paginated),
        )
        .route(
          "/r/parents/{inscription_id}/inscriptions",
          get(r::parent_inscriptions),
        )
        .route(
          "/r/parents/{inscription_id}/inscriptions/{page}",
          get(r::parent_inscriptions_paginated),
        )
        .route("/r/sat/{sat_number}", get(r::sat))
        .route("/r/sat/{sat_number}/{page}", get(r::sat_paginated))
        .route("/r/tx/{txid}", get(r::tx))
        .route(
          "/r/undelegated-content/{inscription_id}",
          get(r::undelegated_content),
        )
        .route("/r/utxo/{outpoint}", get(r::utxo));

      let proxiable_routes = Router::new()
        .route("/content/{inscription_id}", get(r::content))
        .route("/r/children/{inscription_id}", get(r::children))
        .route(
          "/r/children/{inscription_id}/{page}",
          get(r::children_paginated),
        )
        .route("/r/inscription/{inscription_id}", get(r::inscription))
        .route("/r/metadata/{inscription_id}", get(r::metadata))
        .route("/r/sat/{sat_number}/at/{index}", get(r::sat_at_index))
        .route(
          "/r/sat/{sat_number}/at/{index}/content",
          get(r::sat_at_index_content),
        )
        .layer(axum::middleware::from_fn(Self::proxy_layer));

      let router = router.merge(proxiable_routes);

      let router = router
        .fallback(Self::fallback)
        .layer(Extension(index))
        .layer(Extension(server_config.clone()))
        .layer(Extension(settings.clone()))
        .layer(SetResponseHeaderLayer::if_not_present(
          header::CONTENT_SECURITY_POLICY,
          HeaderValue::from_static("default-src 'self'"),
        ))
        .layer(SetResponseHeaderLayer::overriding(
          header::STRICT_TRANSPORT_SECURITY,
          HeaderValue::from_static("max-age=31536000; includeSubDomains; preload"),
        ))
        .layer(
          CorsLayer::new()
            .allow_methods([http::Method::GET, http::Method::POST])
            .allow_headers([http::header::CONTENT_TYPE])
            .allow_origin(Any),
        )
        .layer(CompressionLayer::new())
        .with_state(server_config.clone());

      let router = if server_config.json_api_enabled {
        router.layer(DefaultBodyLimit::disable())
      } else {
        router
      };

      let router = if let Some((username, password)) = settings.credentials() {
        router.layer(ValidateRequestHeaderLayer::basic(username, password))
      } else {
        router
      };

      match (self.http_port(), self.https_port()) {
        (Some(http_port), None) => {
          self
            .spawn(&settings, router, handle, http_port, SpawnConfig::Http)?
            .await??
        }
        (None, Some(https_port)) => {
          self
            .spawn(
              &settings,
              router,
              handle,
              https_port,
              SpawnConfig::Https(self.acceptor(&settings)?),
            )?
            .await??
        }
        (Some(http_port), Some(https_port)) => {
          let http_spawn_config = if self.redirect_http_to_https {
            SpawnConfig::Redirect(if https_port == 443 {
              format!("https://{}", acme_domains[0])
            } else {
              format!("https://{}:{https_port}", acme_domains[0])
            })
          } else {
            SpawnConfig::Http
          };

          let (http_result, https_result) = tokio::join!(
            self.spawn(
              &settings,
              router.clone(),
              handle.clone(),
              http_port,
              http_spawn_config
            )?,
            self.spawn(
              &settings,
              router,
              handle,
              https_port,
              SpawnConfig::Https(self.acceptor(&settings)?),
            )?
          );
          http_result.and(https_result)??;
        }
        (None, None) => unreachable!(),
      }

      Ok(None)
    })
  }

  fn spawn(
    &self,
    settings: &Settings,
    router: Router,
    handle: Handle,
    port: u16,
    config: SpawnConfig,
  ) -> Result<task::JoinHandle<io::Result<()>>> {
    let address = match &self.address {
      Some(address) => address.as_str(),
      None => {
        if cfg!(test) || settings.integration_test() {
          "127.0.0.1"
        } else {
          "0.0.0.0"
        }
      }
    };

    let addr = (address, port)
      .to_socket_addrs()?
      .next()
      .ok_or_else(|| anyhow!("failed to get socket addrs"))?;

    if !settings.integration_test() && !cfg!(test) {
      eprintln!(
        "Listening on {}://{addr}",
        match config {
          SpawnConfig::Https(_) => "https",
          _ => "http",
        }
      );
    }

    Ok(tokio::spawn(async move {
      match config {
        SpawnConfig::Https(acceptor) => {
          axum_server::Server::bind(addr)
            .handle(handle)
            .acceptor(acceptor)
            .serve(router.into_make_service())
            .await
        }
        SpawnConfig::Redirect(destination) => {
          axum_server::Server::bind(addr)
            .handle(handle)
            .serve(
              Router::new()
                .fallback(Self::redirect_http_to_https)
                .layer(Extension(destination))
                .into_make_service(),
            )
            .await
        }
        SpawnConfig::Http => {
          axum_server::Server::bind(addr)
            .handle(handle)
            .serve(router.into_make_service())
            .await
        }
      }
    }))
  }

  fn acme_cache(acme_cache: Option<&PathBuf>, settings: &Settings) -> PathBuf {
    match acme_cache {
      Some(acme_cache) => acme_cache.clone(),
      None => settings.data_dir().join("acme-cache"),
    }
  }

  fn acme_domains(&self) -> Result<Vec<String>> {
    if !self.acme_domain.is_empty() {
      Ok(self.acme_domain.clone())
    } else {
      Ok(vec![
        System::host_name().ok_or(anyhow!("no hostname found"))?
      ])
    }
  }

  fn http_port(&self) -> Option<u16> {
    if self.http || self.http_port.is_some() || (self.https_port.is_none() && !self.https) {
      Some(self.http_port.unwrap_or(80))
    } else {
      None
    }
  }

  fn https_port(&self) -> Option<u16> {
    if self.https || self.https_port.is_some() {
      Some(self.https_port.unwrap_or(443))
    } else {
      None
    }
  }

  fn acceptor(&self, settings: &Settings) -> Result<AxumAcceptor> {
    static RUSTLS_PROVIDER_INSTALLED: LazyLock<bool> = LazyLock::new(|| {
      rustls::crypto::ring::default_provider()
        .install_default()
        .is_ok()
    });

    let config = AcmeConfig::new(self.acme_domains()?)
      .contact(&self.acme_contact)
      .cache_option(Some(DirCache::new(Self::acme_cache(
        self.acme_cache.as_ref(),
        settings,
      ))))
      .directory(if cfg!(test) {
        LETS_ENCRYPT_STAGING_DIRECTORY
      } else {
        LETS_ENCRYPT_PRODUCTION_DIRECTORY
      });

    let mut state = config.state();

    ensure! {
      *RUSTLS_PROVIDER_INSTALLED,
      "failed to install rustls ring crypto provider",
    }

    let mut server_config = rustls::ServerConfig::builder()
      .with_no_client_auth()
      .with_cert_resolver(state.resolver());

    server_config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];

    let acceptor = state.axum_acceptor(Arc::new(server_config));

    tokio::spawn(async move {
      while let Some(result) = state.next().await {
        match result {
          Ok(ok) => log::info!("ACME event: {:?}", ok),
          Err(err) => log::error!("ACME error: {:?}", err),
        }
      }
    });

    Ok(acceptor)
  }

  async fn proxy_layer(
    server_config: Extension<Arc<ServerConfig>>,
    request: http::Request<axum::body::Body>,
    next: axum::middleware::Next,
  ) -> ServerResult {
    let path = request.uri().path().to_owned();

    let response = next.run(request).await;

    if let Some(proxy) = &server_config.proxy {
      if response.status() == StatusCode::NOT_FOUND {
        return task::block_in_place(|| Server::proxy(proxy, &path));
      }

      // `/r/sat/<SAT_NUMBER>/at/<INDEX>` does not return a 404 when no
      // inscription is present, so we must deserialize and check the body.
      if SAT_AT_INDEX_PATH.is_match(&path) {
        let (parts, body) = response.into_parts();

        let bytes = axum::body::to_bytes(body, usize::MAX)
          .await
          .map_err(|err| anyhow!(err))?;

        if let Ok(api::SatInscription { id: None }) =
          serde_json::from_slice::<api::SatInscription>(&bytes)
        {
          return task::block_in_place(|| Server::proxy(proxy, &path));
        }

        return Ok(Response::from_parts(parts, axum::body::Body::from(bytes)));
      }
    }

    Ok(response)
  }

  fn index_height(index: &Index) -> ServerResult<Height> {
    index.block_height()?.ok_or_not_found(|| "genesis block")
  }

  async fn clock(Extension(index): Extension<Arc<Index>>) -> ServerResult {
    task::block_in_place(|| {
      Ok(
        (
          [(
            header::CONTENT_SECURITY_POLICY,
            HeaderValue::from_static("default-src 'unsafe-inline'"),
          )],
          ClockSvg::new(Self::index_height(&index)?),
        )
          .into_response(),
      )
    })
  }

  async fn fallback(Extension(index): Extension<Arc<Index>>, uri: Uri) -> ServerResult<Response> {
    task::block_in_place(|| {
      let path = urlencoding::decode(uri.path().trim_matches('/'))
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) {
        "inscription"
      } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) {
        "rune"
      } else if re::OUTPOINT.is_match(&path) {
        "output"
      } else if re::SATPOINT.is_match(&path) {
        "satpoint"
      } else if re::HASH.is_match(&path) {
        if index.block_header(path.parse().unwrap())?.is_some() {
          "block"
        } else {
          "tx"
        }
      } else if re::ADDRESS.is_match(&path) {
        "address"
      } else {
        return Ok(StatusCode::NOT_FOUND.into_response());
      };

      Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response())
    })
  }

  async fn satscard(
    Extension(settings): Extension<Arc<Settings>>,
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    uri: Uri,
  ) -> ServerResult<Response> {
    #[derive(Debug, Deserialize)]
    struct Form {
      url: DeserializeFromStr<Url>,
    }

    if let Ok(form) = Query::<Form>::try_from_uri(&uri) {
      return if let Some(fragment) = form.url.0.fragment() {
        Ok(Redirect::to(&format!("/satscard?{}", fragment)).into_response())
      } else {
        Err(ServerError::BadRequest(
          "satscard URL missing fragment".into(),
        ))
      };
    }

    let satscard = if let Some(query) = uri.query().filter(|query| !query.is_empty()) {
      let satscard = Satscard::from_query_parameters(settings.chain(), query).map_err(|err| {
        ServerError::BadRequest(format!("invalid satscard query parameters: {err}"))
      })?;

      let address_info = Self::address_info(&index, &satscard.address)?.map(
        |api::AddressInfo {
           outputs,
           inscriptions,
           sat_balance,
           runes_balances,
         }| AddressHtml {
          address: satscard.address.clone(),
          header: false,
          inscriptions,
          outputs,
          runes_balances,
          sat_balance,
        },
      );

      Some((satscard, address_info))
    } else {
      None
    };

    Ok(
      SatscardHtml { satscard }
        .page(server_config)
        .into_response(),
    )
  }

  async fn sat(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(sat)): Path<DeserializeFromStr<Sat>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let inscriptions = index.get_inscription_ids_by_sat(sat)?;
      let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| {
        inscriptions.first().and_then(|&first_inscription_id| {
          index
            .get_inscription_satpoint_by_id(first_inscription_id)
            .ok()
            .flatten()
        })
      });
      let blocktime = index.block_time(sat.height())?;

      let charms = sat.charms();

      let address = if let Some(satpoint) = satpoint {
        if satpoint.outpoint == unbound_outpoint() {
          None
        } else {
          let tx = index
            .get_transaction(satpoint.outpoint.txid)?
            .context("could not get transaction for sat")?;

          let tx_out = tx
            .output
            .get::<usize>(satpoint.outpoint.vout.try_into().unwrap())
            .context("could not get vout for sat")?;

          server_config
            .chain
            .address_from_script(&tx_out.script_pubkey)
            .ok()
        }
      } else {
        None
      };

      Ok(if accept_json {
        Json(api::Sat {
          address: address.map(|address| address.to_string()),
          block: sat.height().0,
          charms: Charm::charms(charms),
          cycle: sat.cycle(),
          decimal: sat.decimal().to_string(),
          degree: sat.degree().to_string(),
          epoch: sat.epoch().0,
          inscriptions,
          name: sat.name(),
          number: sat.0,
          offset: sat.third(),
          percentile: sat.percentile(),
          period: sat.period(),
          rarity: sat.rarity(),
          satpoint,
          timestamp: blocktime.timestamp().timestamp(),
        })
        .into_response()
      } else {
        SatHtml {
          address,
          blocktime,
          inscriptions,
          sat,
          satpoint,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn ordinal(Path(sat): Path<String>) -> Redirect {
    Redirect::to(&format!("/sat/{sat}"))
  }

  async fn output(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(outpoint): Path<OutPoint>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (output_info, txout) = index
        .get_output_info(outpoint)?
        .ok_or_not_found(|| format!("output {outpoint}"))?;

      Ok(if accept_json {
        Json(output_info).into_response()
      } else {
        OutputHtml {
          chain: server_config.chain,
          confirmations: output_info.confirmations,
          inscriptions: output_info.inscriptions,
          outpoint,
          output: txout,
          runes: output_info.runes,
          sat_ranges: output_info.sat_ranges,
          spent: output_info.spent,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn satpoint(
    Extension(index): Extension<Arc<Index>>,
    Path(satpoint): Path<SatPoint>,
  ) -> ServerResult<Redirect> {
    task::block_in_place(|| {
      let (output_info, _) = index
        .get_output_info(satpoint.outpoint)?
        .ok_or_not_found(|| format!("satpoint {satpoint}"))?;

      let Some(ranges) = output_info.sat_ranges else {
        return Err(ServerError::NotFound("sat index required".into()));
      };

      let mut total = 0;
      for (start, end) in ranges {
        let size = end - start;
        if satpoint.offset < total + size {
          let sat = start + satpoint.offset - total;

          return Ok(Redirect::to(&format!("/sat/{sat}")));
        }
        total += size;
      }

      Err(ServerError::NotFound(format!(
        "satpoint {satpoint} not found"
      )))
    })
  }

  async fn outputs(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Json(outputs): Json<Vec<OutPoint>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        let mut response = Vec::new();
        for outpoint in outputs {
          let (output_info, _) = index
            .get_output_info(outpoint)?
            .ok_or_not_found(|| format!("output {outpoint}"))?;

          response.push(output_info);
        }
        Json(response).into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn outputs_address(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path(address): Path<Address<NetworkUnchecked>>,
    Query(query): Query<OutputsQuery>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if !index.has_address_index() {
        return Err(ServerError::NotFound(
          "this server has no address index".to_string(),
        ));
      }

      if !accept_json {
        return Ok(StatusCode::NOT_FOUND.into_response());
      }

      let output_type = query.ty.unwrap_or_default();

      if output_type != OutputType::Any {
        if !index.has_rune_index() {
          return Err(ServerError::BadRequest(
            "this server has no runes index".to_string(),
          ));
        }

        if !index.has_inscription_index() {
          return Err(ServerError::BadRequest(
            "this server has no inscriptions index".to_string(),
          ));
        }
      }

      let address = address
        .require_network(server_config.chain.network())
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let outputs = index.get_address_info(&address)?;

      let mut response = Vec::new();
      for output in outputs.into_iter() {
        let include = match output_type {
          OutputType::Any => true,
          OutputType::Cardinal => {
            index
              .get_inscriptions_on_output_with_satpoints(output)?
              .unwrap_or_default()
              .is_empty()
              && index
                .get_rune_balances_for_output(output)?
                .unwrap_or_default()
                .is_empty()
          }
          OutputType::Inscribed => !index
            .get_inscriptions_on_output_with_satpoints(output)?
            .unwrap_or_default()
            .is_empty(),
          OutputType::Runic => !index
            .get_rune_balances_for_output(output)?
            .unwrap_or_default()
            .is_empty(),
        };

        if include {
          let (output_info, _) = index
            .get_output_info(output)?
            .ok_or_not_found(|| format!("output {output}"))?;

          response.push(output_info);
        }
      }

      Ok(Json(response).into_response())
    })
  }

  async fn rare_txt(Extension(index): Extension<Arc<Index>>) -> ServerResult<RareTxt> {
    task::block_in_place(|| Ok(RareTxt(index.rare_sat_satpoints()?)))
  }

  async fn rune(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(rune_query)): Path<DeserializeFromStr<query::Rune>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      if !index.has_rune_index() {
        return Err(ServerError::NotFound(
          "this server has no rune index".to_string(),
        ));
      }

      let rune = match rune_query {
        query::Rune::Spaced(spaced_rune) => spaced_rune.rune,
        query::Rune::Id(rune_id) => index
          .get_rune_by_id(rune_id)?
          .ok_or_not_found(|| format!("rune {rune_id}"))?,
        query::Rune::Number(number) => index
          .get_rune_by_number(usize::try_from(number).unwrap())?
          .ok_or_not_found(|| format!("rune number {number}"))?,
      };

      let Some((id, entry, parent)) = index.rune(rune)? else {
        return Ok(if accept_json {
          StatusCode::NOT_FOUND.into_response()
        } else {
          let unlock = if let Some(height) = rune.unlock_height(server_config.chain.network()) {
            Some((height, index.block_time(height)?))
          } else {
            None
          };

          (
            StatusCode::NOT_FOUND,
            RuneNotFoundHtml { rune, unlock }.page(server_config),
          )
            .into_response()
        });
      };

      let block_height = index.block_height()?.unwrap_or(Height(0));

      let mintable = entry.mintable((block_height.n() + 1).into()).is_ok();

      Ok(if accept_json {
        Json(api::Rune {
          entry,
          id,
          mintable,
          parent,
        })
        .into_response()
      } else {
        RuneHtml {
          entry,
          id,
          mintable,
          parent,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn runes(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    accept_json: AcceptJson,
  ) -> ServerResult<Response> {
    Self::runes_paginated(
      Extension(server_config),
      Extension(index),
      Path(0),
      accept_json,
    )
    .await
  }

  async fn runes_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<usize>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (entries, more) = index.runes_paginated(50, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more.then_some(page_index + 1);

      Ok(if accept_json {
        Json(RunesHtml {
          entries,
          more,
          prev,
          next,
        })
        .into_response()
      } else {
        RunesHtml {
          entries,
          more,
          prev,
          next,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn home(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult<PageHtml<HomeHtml>> {
    task::block_in_place(|| {
      Ok(
        HomeHtml {
          inscriptions: index.get_home_inscriptions()?,
        }
        .page(server_config),
      )
    })
  }

  async fn blocks(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let blocks = index.blocks(100)?;
      let mut featured_blocks = BTreeMap::new();
      for (height, hash) in blocks.iter().take(5) {
        let (inscriptions, _total_num) =
          index.get_highest_paying_inscriptions_in_block(*height, 8)?;

        featured_blocks.insert(*hash, inscriptions);
      }

      Ok(if accept_json {
        Json(api::Blocks::new(blocks, featured_blocks)).into_response()
      } else {
        BlocksHtml::new(blocks, featured_blocks)
          .page(server_config)
          .into_response()
      })
    })
  }

  async fn install_script() -> Redirect {
    Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh")
  }

  async fn address(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(address): Path<Address<NetworkUnchecked>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let address = address
        .require_network(server_config.chain.network())
        .map_err(|err| ServerError::BadRequest(err.to_string()))?;

      let Some(info) = Self::address_info(&index, &address)? else {
        return Err(ServerError::NotFound(
          "this server has no address index".to_string(),
        ));
      };

      Ok(if accept_json {
        Json(info).into_response()
      } else {
        let api::AddressInfo {
          sat_balance,
          outputs,
          inscriptions,
          runes_balances,
        } = info;

        AddressHtml {
          address,
          header: true,
          inscriptions,
          outputs,
          runes_balances,
          sat_balance,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  fn address_info(index: &Index, address: &Address) -> ServerResult<Option<api::AddressInfo>> {
    if !index.has_address_index() {
      return Ok(None);
    }

    let mut outputs = index.get_address_info(address)?;

    outputs.sort();

    let sat_balance = index.get_sat_balances_for_outputs(&outputs)?;

    let inscriptions = index.get_inscriptions_for_outputs(&outputs)?;

    let runes_balances = index.get_aggregated_rune_balances_for_outputs(&outputs)?;

    Ok(Some(api::AddressInfo {
      sat_balance,
      outputs,
      inscriptions,
      runes_balances,
    }))
  }

  async fn block(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (block, height) = match query {
        query::Block::Height(height) => {
          let block = index
            .get_block_by_height(height)?
            .ok_or_not_found(|| format!("block {height}"))?;

          (block, height)
        }
        query::Block::Hash(hash) => {
          let info = index
            .block_header_info(hash)?
            .ok_or_not_found(|| format!("block {hash}"))?;

          let block = index
            .get_block_by_hash(hash)?
            .ok_or_not_found(|| format!("block {hash}"))?;

          (block, u32::try_from(info.height).unwrap())
        }
      };

      let runes = index.get_runes_in_block(u64::from(height))?;
      Ok(if accept_json {
        let inscriptions = index.get_inscriptions_in_block(height)?;
        Json(api::Block::new(
          block,
          Height(height),
          Self::index_height(&index)?,
          inscriptions,
          runes,
        ))
        .into_response()
      } else {
        let (featured_inscriptions, total_num) =
          index.get_highest_paying_inscriptions_in_block(height, 8)?;
        BlockHtml::new(
          block,
          Height(height),
          Self::index_height(&index)?,
          total_num,
          featured_inscriptions,
          runes,
        )
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn transaction(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(txid): Path<Txid>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let transaction = index
        .get_transaction(txid)?
        .ok_or_not_found(|| format!("transaction {txid}"))?;

      let inscription_count = index.inscription_count(txid)?;

      Ok(if accept_json {
        Json(api::Transaction {
          chain: server_config.chain,
          etching: index.get_etching(txid)?,
          inscription_count,
          transaction,
          txid,
        })
        .into_response()
      } else {
        TransactionHtml {
          chain: server_config.chain,
          etching: index.get_etching(txid)?,
          inscription_count,
          transaction,
          txid,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn decode(
    Extension(index): Extension<Arc<Index>>,
    Path(txid): Path<Txid>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let transaction = index
        .get_transaction(txid)?
        .ok_or_not_found(|| format!("transaction {txid}"))?;

      let inscriptions = ParsedEnvelope::from_transaction(&transaction);
      let runestone = Runestone::decipher(&transaction);

      Ok(if accept_json {
        Json(api::Decode {
          inscriptions,
          runestone,
        })
        .into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn update(
    Extension(settings): Extension<Arc<Settings>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if settings.integration_test() {
        index.update()?;
        Ok(index.block_count()?.to_string().into_response())
      } else {
        Ok(StatusCode::NOT_FOUND.into_response())
      }
    })
  }

  async fn status(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        Json(index.status(server_config.json_api_enabled)?).into_response()
      } else {
        index
          .status(server_config.json_api_enabled)?
          .page(server_config)
          .into_response()
      })
    })
  }

  async fn search_by_query(
    Extension(index): Extension<Arc<Index>>,
    Query(search): Query<Search>,
  ) -> ServerResult<Redirect> {
    Self::search(index, search.query).await
  }

  async fn search_by_path(
    Extension(index): Extension<Arc<Index>>,
    Path(search): Path<Search>,
  ) -> ServerResult<Redirect> {
    Self::search(index, search.query).await
  }

  async fn search(index: Arc<Index>, query: String) -> ServerResult<Redirect> {
    task::block_in_place(|| {
      let query = query.trim();

      if re::HASH.is_match(query) {
        if index.block_header(query.parse().unwrap())?.is_some() {
          Ok(Redirect::to(&format!("/block/{query}")))
        } else {
          Ok(Redirect::to(&format!("/tx/{query}")))
        }
      } else if re::OUTPOINT.is_match(query) {
        Ok(Redirect::to(&format!("/output/{query}")))
      } else if re::INSCRIPTION_ID.is_match(query) || re::INSCRIPTION_NUMBER.is_match(query) {
        Ok(Redirect::to(&format!("/inscription/{query}")))
      } else if let Some(captures) = re::SATSCARD_URL.captures(query) {
        Ok(Redirect::to(&format!(
          "/satscard?{}",
          &captures["parameters"]
        )))
      } else if re::SPACED_RUNE.is_match(query) {
        Ok(Redirect::to(&format!("/rune/{query}")))
      } else if re::RUNE_ID.is_match(query) {
        let id = query
          .parse::<RuneId>()
          .map_err(|err| ServerError::BadRequest(err.to_string()))?;

        let rune = index.get_rune_by_id(id)?.ok_or_not_found(|| "rune ID")?;

        Ok(Redirect::to(&format!("/rune/{rune}")))
      } else if re::ADDRESS.is_match(query) {
        Ok(Redirect::to(&format!("/address/{query}")))
      } else if re::SATPOINT.is_match(query) {
        Ok(Redirect::to(&format!("/satpoint/{query}")))
      } else {
        Ok(Redirect::to(&format!("/sat/{query}")))
      }
    })
  }

  async fn favicon() -> ServerResult {
    Ok(
      Self::static_asset(Path("/favicon.png".to_string()))
        .await
        .into_response(),
    )
  }

  async fn feed(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      let mut builder = rss::ChannelBuilder::default();

      let chain = server_config.chain;
      match chain {
        Chain::Mainnet => builder.title("Inscriptions".to_string()),
        _ => builder.title(format!("Inscriptions – {chain:?}")),
      };

      builder.generator(Some("ord".to_string()));

      for (number, id) in index.get_feed_inscriptions(300)? {
        builder.item(
          rss::ItemBuilder::default()
            .title(Some(format!("Inscription {number}")))
            .link(Some(format!("/inscription/{id}")))
            .guid(Some(rss::Guid {
              value: format!("/inscription/{id}"),
              permalink: true,
            }))
            .build(),
        );
      }

      Ok(
        (
          [
            (header::CONTENT_TYPE, "application/rss+xml"),
            (
              header::CONTENT_SECURITY_POLICY,
              "default-src 'unsafe-inline'",
            ),
          ],
          builder.build().to_string(),
        )
          .into_response(),
      )
    })
  }

  async fn static_asset(Path(path): Path<String>) -> ServerResult {
    let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') {
      stripped
    } else {
      &path
    })
    .ok_or_not_found(|| format!("asset {path}"))?;

    let mime = mime_guess::from_path(path).first_or_octet_stream();

    Ok(
      Response::builder()
        .header(header::CONTENT_TYPE, mime.as_ref())
        .body(content.data.into())
        .unwrap(),
    )
  }

  async fn block_count(Extension(index): Extension<Arc<Index>>) -> ServerResult<String> {
    task::block_in_place(|| Ok(index.block_count()?.to_string()))
  }

  async fn input(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(path): Path<(u32, usize, usize)>,
  ) -> ServerResult<PageHtml<InputHtml>> {
    task::block_in_place(|| {
      let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2);

      let block = index
        .get_block_by_height(path.0)?
        .ok_or_not_found(not_found)?;

      let transaction = block
        .txdata
        .into_iter()
        .nth(path.1)
        .ok_or_not_found(not_found)?;

      let input = transaction
        .input
        .into_iter()
        .nth(path.2)
        .ok_or_not_found(not_found)?;

      Ok(InputHtml { path, input }.page(server_config))
    })
  }

  async fn faq() -> Redirect {
    Redirect::to("https://docs.ordinals.com/faq")
  }

  async fn bounties() -> Redirect {
    Redirect::to("https://docs.ordinals.com/bounties")
  }

  async fn preview(
    Extension(index): Extension<Arc<Index>>,
    Extension(settings): Extension<Arc<Settings>>,
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Path(inscription_id): Path<InscriptionId>,
    accept_encoding: AcceptEncoding,
  ) -> ServerResult {
    task::block_in_place(|| {
      if settings.is_hidden(inscription_id) {
        return Ok(PreviewUnknownHtml.into_response());
      }

      let mut inscription = index
        .get_inscription_by_id(inscription_id)?
        .ok_or_not_found(|| format!("inscription {inscription_id}"))?;

      let inscription_number = index
        .get_inscription_entry(inscription_id)?
        .ok_or_not_found(|| format!("inscription {inscription_id}"))?
        .inscription_number;

      if let Some(delegate) = inscription.delegate() {
        inscription = index
          .get_inscription_by_id(delegate)?
          .ok_or_not_found(|| format!("delegate {inscription_id}"))?
      }

      let media = inscription.media();

      if let Media::Iframe = media {
        return Ok(
          r::content_response(inscription, accept_encoding, &server_config)?
            .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
            .into_response(),
        );
      }

      let content_security_policy = server_config.preview_content_security_policy(media)?;

      match media {
        Media::Audio => Ok(
          (
            content_security_policy,
            PreviewAudioHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Code(language) => Ok(
          (
            content_security_policy,
            PreviewCodeHtml {
              inscription_id,
              language,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Font => Ok(
          (
            content_security_policy,
            PreviewFontHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Iframe => unreachable!(),
        Media::Image(image_rendering) => Ok(
          (
            content_security_policy,
            PreviewImageHtml {
              image_rendering,
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Markdown => Ok(
          (
            content_security_policy,
            PreviewMarkdownHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Model => Ok(
          (
            content_security_policy,
            PreviewModelHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Pdf => Ok(
          (
            content_security_policy,
            PreviewPdfHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Text => Ok(
          (
            content_security_policy,
            PreviewTextHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
        Media::Unknown => Ok((content_security_policy, PreviewUnknownHtml).into_response()),
        Media::Video => Ok(
          (
            content_security_policy,
            PreviewVideoHtml {
              inscription_id,
              inscription_number,
            },
          )
            .into_response(),
        ),
      }
    })
  }

  async fn inscription(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Inscription>>,
  ) -> ServerResult {
    Self::inscription_inner(server_config, &index, accept_json, query, None).await
  }

  async fn inscription_child(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Path((DeserializeFromStr(query), child)): Path<(DeserializeFromStr<query::Inscription>, usize)>,
  ) -> ServerResult {
    Self::inscription_inner(server_config, &index, accept_json, query, Some(child)).await
  }

  async fn inscription_inner(
    server_config: Arc<ServerConfig>,
    index: &Index,
    accept_json: bool,
    query: query::Inscription,
    child: Option<usize>,
  ) -> ServerResult {
    task::block_in_place(|| {
      if let query::Inscription::Sat(_) = query {
        if !index.has_sat_index() {
          return Err(ServerError::NotFound("sat index required".into()));
        }
      }

      let inscription_info = index.inscription_info(query, child)?;

      Ok(if accept_json {
        let status_code = if inscription_info.is_none() {
          StatusCode::NOT_FOUND
        } else {
          StatusCode::OK
        };

        (status_code, Json(inscription_info.map(|info| info.0))).into_response()
      } else {
        let (info, txout, inscription) =
          inscription_info.ok_or_not_found(|| format!("inscription {query}"))?;

        InscriptionHtml {
          chain: server_config.chain,
          charms: Charm::Vindicated.unset(info.charms.iter().fold(0, |mut acc, charm| {
            charm.set(&mut acc);
            acc
          })),
          child_count: info.child_count,
          children: info.children,
          fee: info.fee,
          height: info.height,
          inscription,
          id: info.id,
          number: info.number,
          next: info.next,
          output: txout,
          parents: info.parents,
          previous: info.previous,
          rune: info.rune,
          sat: info.sat,
          satpoint: info.satpoint,
          timestamp: Utc.timestamp_opt(info.timestamp, 0).unwrap(),
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn inscriptions_json(
    Extension(index): Extension<Arc<Index>>,
    AcceptJson(accept_json): AcceptJson,
    Json(inscriptions): Json<Vec<InscriptionId>>,
  ) -> ServerResult {
    task::block_in_place(|| {
      Ok(if accept_json {
        let mut response = Vec::new();
        for inscription in inscriptions {
          let query = query::Inscription::Id(inscription);
          let (info, _, _) = index
            .inscription_info(query, None)?
            .ok_or_not_found(|| format!("inscription {query}"))?;

          response.push(info);
        }

        Json(response).into_response()
      } else {
        StatusCode::NOT_FOUND.into_response()
      })
    })
  }

  async fn collections(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
  ) -> ServerResult {
    Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await
  }

  async fn collections_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<usize>,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (collections, more_collections) = index.get_collections_paginated(100, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more_collections.then_some(page_index + 1);

      Ok(
        CollectionsHtml {
          inscriptions: collections,
          prev,
          next,
        }
        .page(server_config)
        .into_response(),
      )
    })
  }

  async fn children(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(inscription_id): Path<InscriptionId>,
  ) -> ServerResult {
    Self::children_paginated(
      Extension(server_config),
      Extension(index),
      Path((inscription_id, 0)),
    )
    .await
  }

  async fn children_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((parent, page)): Path<(InscriptionId, usize)>,
  ) -> ServerResult {
    task::block_in_place(|| {
      let entry = index
        .get_inscription_entry(parent)?
        .ok_or_not_found(|| format!("inscription {parent}"))?;

      let parent_number = entry.inscription_number;

      let (children, more_children) =
        index.get_children_by_sequence_number_paginated(entry.sequence_number, 100, page)?;

      let prev_page = page.checked_sub(1);

      let next_page = more_children.then_some(page + 1);

      Ok(
        ChildrenHtml {
          parent,
          parent_number,
          children,
          prev_page,
          next_page,
        }
        .page(server_config)
        .into_response(),
      )
    })
  }

  async fn inscriptions(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    accept_json: AcceptJson,
  ) -> ServerResult {
    Self::inscriptions_paginated(
      Extension(server_config),
      Extension(index),
      Path(0),
      accept_json,
    )
    .await
  }

  async fn inscriptions_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(page_index): Path<u32>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?;

      let prev = page_index.checked_sub(1);

      let next = more.then_some(page_index + 1);

      Ok(if accept_json {
        Json(api::Inscriptions {
          ids: inscriptions,
          page_index,
          more,
        })
        .into_response()
      } else {
        InscriptionsHtml {
          inscriptions,
          next,
          prev,
        }
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn inscriptions_in_block(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(block_height): Path<u32>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    Self::inscriptions_in_block_paginated(
      Extension(server_config),
      Extension(index),
      Path((block_height, 0)),
      AcceptJson(accept_json),
    )
    .await
  }

  async fn inscriptions_in_block_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((block_height, page_index)): Path<(u32, u32)>,
    AcceptJson(accept_json): AcceptJson,
  ) -> ServerResult {
    task::block_in_place(|| {
      let page_size = 100;

      let page_index_usize = usize::try_from(page_index).unwrap_or(usize::MAX);
      let page_size_usize = usize::try_from(page_size).unwrap_or(usize::MAX);

      let mut inscriptions = index
        .get_inscriptions_in_block(block_height)?
        .into_iter()
        .skip(page_index_usize.saturating_mul(page_size_usize))
        .take(page_size_usize.saturating_add(1))
        .collect::<Vec<InscriptionId>>();

      let more = inscriptions.len() > page_size_usize;

      if more {
        inscriptions.pop();
      }

      Ok(if accept_json {
        Json(api::Inscriptions {
          ids: inscriptions,
          page_index,
          more,
        })
        .into_response()
      } else {
        InscriptionsBlockHtml::new(
          block_height,
          index.block_height()?.unwrap_or(Height(0)).n(),
          inscriptions,
          more,
          page_index,
        )?
        .page(server_config)
        .into_response()
      })
    })
  }

  async fn parents(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path(inscription_id): Path<InscriptionId>,
  ) -> ServerResult<Response> {
    Self::parents_paginated(
      Extension(server_config),
      Extension(index),
      Path((inscription_id, 0)),
    )
    .await
  }

  async fn parents_paginated(
    Extension(server_config): Extension<Arc<ServerConfig>>,
    Extension(index): Extension<Arc<Index>>,
    Path((id, page)): Path<(InscriptionId, usize)>,
  ) -> ServerResult<Response> {
    task::block_in_place(|| {
      let child = index
        .get_inscription_entry(id)?
        .ok_or_not_found(|| format!("inscription {id}"))?;

      let (parents, more) =
        index.get_parents_by_sequence_number_paginated(child.parents, 100, page)?;

      let prev_page = page.checked_sub(1);

      let next_page = more.then_some(page + 1);

      Ok(
        ParentsHtml {
          id,
          number: child.inscription_number,
          parents,
          prev_page,
          next_page,
        }
        .page(server_config)
        .into_response(),
      )
    })
  }

  fn proxy(proxy: &Url, path: &str) -> ServerResult<Response> {
    let response = reqwest::blocking::Client::new()
      .get(format!("{}{}", proxy, &path[1..]))
      .send()
      .map_err(|err| anyhow!(err))?;

    let status = response.status();

    let mut headers = response.headers().clone();

    headers.insert(
      header::CONTENT_SECURITY_POLICY,
      HeaderValue::from_str(&format!(
        "default-src 'self' {proxy} 'unsafe-eval' 'unsafe-inline' data: blob:"
      ))
      .map_err(|err| ServerError::Internal(Error::from(err)))?,
    );

    Ok(
      (
        status,
        headers,
        response.bytes().map_err(|err| anyhow!(err))?,
      )
        .into_response(),
    )
  }

  async fn redirect_http_to_https(
    Extension(mut destination): Extension<String>,
    uri: Uri,
  ) -> Redirect {
    if let Some(path_and_query) = uri.path_and_query() {
      destination.push_str(path_and_query.as_str());
    }

    Redirect::to(&destination)
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    reqwest::{
      header::{self, HeaderMap},
      StatusCode, Url,
    },
    serde::de::DeserializeOwned,
    std::net::TcpListener,
    tempfile::TempDir,
  };

  const RUNE: u128 = 99246114928149462;

  #[derive(Default)]
  struct Builder {
    core: Option<mockcore::Handle>,
    config: String,
    ord_args: BTreeMap<String, Option<String>>,
    server_args: BTreeMap<String, Option<String>>,
  }

  impl Builder {
    fn core(self, core: mockcore::Handle) -> Self {
      Self {
        core: Some(core),
        ..self
      }
    }

    fn ord_option(mut self, option: &str, value: &str) -> Self {
      self.ord_args.insert(option.into(), Some(value.into()));
      self
    }

    fn ord_flag(mut self, flag: &str) -> Self {
      self.ord_args.insert(flag.into(), None);
      self
    }

    fn server_option(mut self, option: &str, value: &str) -> Self {
      self.server_args.insert(option.into(), Some(value.into()));
      self
    }

    fn server_flag(mut self, flag: &str) -> Self {
      self.server_args.insert(flag.into(), None);
      self
    }

    fn chain(self, chain: Chain) -> Self {
      self.ord_option("--chain", &chain.to_string())
    }

    fn config(self, config: &str) -> Self {
      Self {
        config: config.into(),
        ..self
      }
    }

    fn build(self) -> TestServer {
      let core = self.core.unwrap_or_else(|| {
        mockcore::builder()
          .network(
            self
              .ord_args
              .get("--chain")
              .map(|chain| chain.as_ref().unwrap().parse::<Chain>().unwrap())
              .unwrap_or_default()
              .network(),
          )
          .build()
      });

      let tempdir = TempDir::new().unwrap();

      let cookiefile = tempdir.path().join("cookie");

      fs::write(&cookiefile, "username:password").unwrap();

      let port = TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port();

      let mut args = vec!["ord".to_string()];

      args.push("--bitcoin-rpc-url".into());
      args.push(core.url());

      args.push("--cookie-file".into());
      args.push(cookiefile.to_str().unwrap().into());

      args.push("--datadir".into());
      args.push(tempdir.path().to_str().unwrap().into());

      if !self.ord_args.contains_key("--chain") {
        args.push("--chain".into());
        args.push(core.network());
      }

      for (arg, value) in self.ord_args {
        args.push(arg);

        if let Some(value) = value {
          args.push(value);
        }
      }

      args.push("server".into());

      args.push("--address".into());
      args.push("127.0.0.1".into());

      args.push("--http-port".into());
      args.push(port.to_string());

      args.push("--polling-interval".into());
      args.push("100ms".into());

      for (arg, value) in self.server_args {
        args.push(arg);

        if let Some(value) = value {
          args.push(value);
        }
      }

      let arguments = Arguments::try_parse_from(args).unwrap();

      let Subcommand::Server(server) = arguments.subcommand else {
        panic!("unexpected subcommand: {:?}", arguments.subcommand);
      };

      let settings = Settings::from_options(arguments.options)
        .or(serde_yaml::from_str::<Settings>(&self.config).unwrap())
        .or_defaults()
        .unwrap();

      let index = Arc::new(Index::open(&settings).unwrap());
      let ord_server_handle = Handle::new();

      {
        let index = index.clone();
        let ord_server_handle = ord_server_handle.clone();
        thread::spawn(|| server.run(settings, index, ord_server_handle).unwrap());
      }

      while index.statistic(crate::index::Statistic::Commits) == 0 {
        thread::sleep(Duration::from_millis(50));
      }

      let client = reqwest::blocking::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .unwrap();

      for i in 0.. {
        match client.get(format!("http://127.0.0.1:{port}/status")).send() {
          Ok(_) => break,
          Err(err) => {
            if i == 400 {
              panic!("ord server failed to start: {err}");
            }
          }
        }

        thread::sleep(Duration::from_millis(50));
      }

      TestServer {
        core,
        index,
        ord_server_handle,
        tempdir,
        url: Url::parse(&format!("http://127.0.0.1:{port}")).unwrap(),
      }
    }

    fn https(self) -> Self {
      self.server_flag("--https")
    }

    fn index_addresses(self) -> Self {
      self.ord_flag("--index-addresses")
    }

    fn index_runes(self) -> Self {
      self.ord_flag("--index-runes")
    }

    fn index_sats(self) -> Self {
      self.ord_flag("--index-sats")
    }

    fn redirect_http_to_https(self) -> Self {
      self.server_flag("--redirect-http-to-https")
    }
  }

  struct TestServer {
    core: mockcore::Handle,
    index: Arc<Index>,
    ord_server_handle: Handle,
    #[allow(unused)]
    tempdir: TempDir,
    url: Url,
  }

  impl TestServer {
    fn builder() -> Builder {
      Default::default()
    }

    fn new() -> Self {
      Builder::default().build()
    }

    #[track_caller]
    pub(crate) fn etch(
      &self,
      runestone: Runestone,
      outputs: usize,
      witness: Option<Witness>,
    ) -> (Txid, RuneId) {
      let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap();

      self.mine_blocks(1);

      self.core.broadcast_tx(TransactionTemplate {
        inputs: &[(block_count, 0, 0, Default::default())],
        p2tr: true,
        ..default()
      });

      self.mine_blocks((Runestone::COMMIT_CONFIRMATIONS - 1).into());

      let witness = witness.unwrap_or_else(|| {
        let tapscript = script::Builder::new()
          .push_slice::<&PushBytes>(
            runestone
              .etching
              .unwrap()
              .rune
              .unwrap()
              .commitment()
              .as_slice()
              .try_into()
              .unwrap(),
          )
          .into_script();
        let mut witness = Witness::default();
        witness.push(tapscript);
        witness.push([]);
        witness
      });

      let txid = self.core.broadcast_tx(TransactionTemplate {
        inputs: &[(block_count + 1, 1, 0, witness)],
        op_return: Some(runestone.encipher()),
        outputs,
        ..default()
      });

      self.mine_blocks(1);

      (
        txid,
        RuneId {
          block: (self.index.block_count().unwrap() - 1).into(),
          tx: 1,
        },
      )
    }

    #[track_caller]
    fn get(&self, path: impl AsRef<str>) -> reqwest::blocking::Response {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }
      reqwest::blocking::get(self.join_url(path.as_ref())).unwrap()
    }

    #[track_caller]
    pub(crate) fn get_json<T: DeserializeOwned>(&self, path: impl AsRef<str>) -> T {
      if let Err(error) = self.index.update() {
        log::error!("{error}");
      }

      let client = reqwest::blocking::Client::new();

      let response = client
        .get(self.join_url(path.as_ref()))
        .header(header::ACCEPT, "application/json")
        .send()
        .unwrap();

      assert_eq!(response.status(), StatusCode::OK);

      response.json().unwrap()
    }

    fn join_url(&self, url: &str) -> Url {
      self.url.join(url).unwrap()
    }

    #[track_caller]
    fn assert_response(&self, path: impl AsRef<str>, status: StatusCode, expected_response: &str) {
      let response = self.get(path);
      assert_eq!(response.status(), status, "{}", response.text().unwrap());
      pretty_assert_eq!(response.text().unwrap(), expected_response);
    }

    #[track_caller]
    fn assert_response_regex(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      regex: impl AsRef<str>,
    ) {
      let response = self.get(path);
      assert_eq!(
        response.status(),
        status,
        "response: {}",
        response.text().unwrap()
      );
      assert_regex_match!(response.text().unwrap(), regex.as_ref());
    }

    #[track_caller]
    fn assert_html(&self, path: impl AsRef<str>, content: impl PageContent) {
      self.assert_html_status(path, StatusCode::OK, content);
    }

    #[track_caller]
    fn assert_html_status(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      content: impl PageContent,
    ) {
      let response = self.get(path);

      assert_eq!(response.status(), status, "{}", response.text().unwrap());

      let expected_response = PageHtml::new(
        content,
        Arc::new(ServerConfig {
          chain: self.index.chain(),
          domain: Some(System::host_name().unwrap()),
          ..Default::default()
        }),
      )
      .to_string();

      pretty_assert_eq!(response.text().unwrap(), expected_response);
    }

    fn assert_response_csp(
      &self,
      path: impl AsRef<str>,
      status: StatusCode,
      content_security_policy: &str,
      regex: impl AsRef<str>,
    ) {
      let response = self.get(path);
      assert_eq!(response.status(), status);
      assert_eq!(
        response
          .headers()
          .get(header::CONTENT_SECURITY_POLICY,)
          .unwrap(),
        content_security_policy
      );
      assert_regex_match!(response.text().unwrap(), regex.as_ref());
    }

    #[track_caller]
    fn assert_redirect(&self, path: &str, location: &str) {
      let response = reqwest::blocking::Client::builder()
        .redirect(reqwest::redirect::Policy::none())
        .build()
        .unwrap()
        .get(self.join_url(path))
        .send()
        .unwrap();

      assert_eq!(response.status(), StatusCode::SEE_OTHER);
      assert_eq!(response.headers().get(header::LOCATION).unwrap(), location);
    }

    #[track_caller]
    fn mine_blocks(&self, n: u64) -> Vec<Block> {
      let blocks = self.core.mine_blocks(n);
      self.index.update().unwrap();
      blocks
    }

    fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec<Block> {
      let blocks = self.core.mine_blocks_with_subsidy(n, subsidy);
      self.index.update().unwrap();
      blocks
    }
  }

  impl Drop for TestServer {
    fn drop(&mut self) {
      self.ord_server_handle.shutdown();
    }
  }

  fn parse_server_args(args: &str) -> (Settings, Server) {
    match Arguments::try_parse_from(args.split_whitespace()) {
      Ok(arguments) => match arguments.subcommand {
        Subcommand::Server(server) => (
          Settings::from_options(arguments.options)
            .or_defaults()
            .unwrap(),
          server,
        ),
        subcommand => panic!("unexpected subcommand: {subcommand:?}"),
      },
      Err(err) => panic!("error parsing arguments: {err}"),
    }
  }

  #[test]
  fn http_and_https_port_dont_conflict() {
    parse_server_args(
      "ord server --http-port 0 --https-port 0 --acme-cache foo --acme-contact bar --acme-domain baz",
    );
  }

  #[test]
  fn http_port_defaults_to_80() {
    assert_eq!(parse_server_args("ord server").1.http_port(), Some(80));
  }

  #[test]
  fn https_port_defaults_to_none() {
    assert_eq!(parse_server_args("ord server").1.https_port(), None);
  }

  #[test]
  fn https_sets_https_port_to_443() {
    assert_eq!(
      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
        .1
        .https_port(),
      Some(443)
    );
  }

  #[test]
  fn https_disables_http() {
    assert_eq!(
      parse_server_args("ord server --https --acme-cache foo --acme-contact bar --acme-domain baz")
        .1
        .http_port(),
      None
    );
  }

  #[test]
  fn https_port_disables_http() {
    assert_eq!(
      parse_server_args(
        "ord server --https-port 433 --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .http_port(),
      None
    );
  }

  #[test]
  fn https_port_sets_https_port() {
    assert_eq!(
      parse_server_args(
        "ord server --https-port 1000 --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .https_port(),
      Some(1000)
    );
  }

  #[test]
  fn http_with_https_leaves_http_enabled() {
    assert_eq!(
      parse_server_args(
        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .http_port(),
      Some(80)
    );
  }

  #[test]
  fn http_with_https_leaves_https_enabled() {
    assert_eq!(
      parse_server_args(
        "ord server --https --http --acme-cache foo --acme-contact bar --acme-domain baz"
      )
      .1
      .https_port(),
      Some(443)
    );
  }

  #[test]
  fn acme_contact_accepts_multiple_values() {
    assert!(Arguments::try_parse_from([
      "ord",
      "server",
      "--address",
      "127.0.0.1",
      "--http-port",
      "0",
      "--acme-contact",
      "foo",
      "--acme-contact",
      "bar"
    ])
    .is_ok());
  }

  #[test]
  fn acme_domain_accepts_multiple_values() {
    assert!(Arguments::try_parse_from([
      "ord",
      "server",
      "--address",
      "127.0.0.1",
      "--http-port",
      "0",
      "--acme-domain",
      "foo",
      "--acme-domain",
      "bar"
    ])
    .is_ok());
  }

  #[test]
  fn acme_cache_defaults_to_data_dir() {
    let arguments = Arguments::try_parse_from(["ord", "--datadir", "foo", "server"]).unwrap();

    let settings = Settings::from_options(arguments.options)
      .or_defaults()
      .unwrap();

    let acme_cache = Server::acme_cache(None, &settings).display().to_string();
    assert!(
      acme_cache.contains(if cfg!(windows) {
        r"foo\acme-cache"
      } else {
        "foo/acme-cache"
      }),
      "{acme_cache}"
    )
  }

  #[test]
  fn acme_cache_flag_is_respected() {
    let arguments =
      Arguments::try_parse_from(["ord", "--datadir", "foo", "server", "--acme-cache", "bar"])
        .unwrap();

    let settings = Settings::from_options(arguments.options)
      .or_defaults()
      .unwrap();

    let acme_cache = Server::acme_cache(Some(&"bar".into()), &settings)
      .display()
      .to_string();
    assert_eq!(acme_cache, "bar")
  }

  #[test]
  fn acme_domain_defaults_to_hostname() {
    let (_, server) = parse_server_args("ord server");
    assert_eq!(
      server.acme_domains().unwrap(),
      &[System::host_name().unwrap()]
    );
  }

  #[test]
  fn acme_domain_flag_is_respected() {
    let (_, server) = parse_server_args("ord server --acme-domain example.com");
    assert_eq!(server.acme_domains().unwrap(), &["example.com"]);
  }

  #[test]
  fn install_sh_redirects_to_github() {
    TestServer::new().assert_redirect(
      "/install.sh",
      "https://raw.githubusercontent.com/ordinals/ord/master/install.sh",
    );
  }

  #[test]
  fn ordinal_redirects_to_sat() {
    TestServer::new().assert_redirect("/ordinal/0", "/sat/0");
  }

  #[test]
  fn bounties_redirects_to_docs_site() {
    TestServer::new().assert_redirect("/bounties", "https://docs.ordinals.com/bounties");
  }

  #[test]
  fn faq_redirects_to_docs_site() {
    TestServer::new().assert_redirect("/faq", "https://docs.ordinals.com/faq");
  }

  #[test]
  fn search_by_query_returns_rune() {
    TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD");
  }

  #[test]
  fn search_by_query_returns_spaced_rune() {
    TestServer::new().assert_redirect("/search?query=AB•CD", "/rune/AB•CD");
  }

  #[test]
  fn search_by_query_returns_satscard() {
    TestServer::new().assert_redirect(
      "/search?query=https://satscard.com/start%23foo",
      "/satscard?foo",
    );
    TestServer::new().assert_redirect(
      "/search?query=https://getsatscard.com/start%23foo",
      "/satscard?foo",
    );
  }

  #[test]
  fn search_by_query_returns_inscription() {
    TestServer::new().assert_redirect(
      "/search?query=0000000000000000000000000000000000000000000000000000000000000000i0",
      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
    );
  }

  #[test]
  fn search_by_query_returns_inscription_by_number() {
    TestServer::new().assert_redirect("/search?query=0", "/inscription/0");
  }

  #[test]
  fn search_is_whitespace_insensitive() {
    TestServer::new().assert_redirect("/search/ abc ", "/sat/abc");
  }

  #[test]
  fn search_by_path_returns_sat() {
    TestServer::new().assert_redirect("/search/abc", "/sat/abc");
  }

  #[test]
  fn search_for_blockhash_returns_block() {
    TestServer::new().assert_redirect(
      "/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    );
  }

  #[test]
  fn search_for_txid_returns_transaction() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000",
      "/tx/0000000000000000000000000000000000000000000000000000000000000000",
    );
  }

  #[test]
  fn search_for_outpoint_returns_output() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000:0",
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
    );
  }

  #[test]
  fn search_for_inscription_id_returns_inscription() {
    TestServer::new().assert_redirect(
      "/search/0000000000000000000000000000000000000000000000000000000000000000i0",
      "/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
    );
  }

  #[test]
  fn search_by_path_returns_rune() {
    TestServer::new().assert_redirect("/search/ABCD", "/rune/ABCD");
  }

  #[test]
  fn search_by_path_returns_spaced_rune() {
    TestServer::new().assert_redirect("/search/AB•CD", "/rune/AB•CD");
  }

  #[test]
  fn search_by_rune_id_returns_rune() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    server.mine_blocks(1);

    server.assert_redirect("/search/8:1", "/rune/AAAAAAAAAAAAA");
    server.assert_redirect("/search?query=8:1", "/rune/AAAAAAAAAAAAA");

    server.assert_response_regex(
      "/search/100000000000000000000:200000000000000000",
      StatusCode::BAD_REQUEST,
      ".*",
    );
  }

  #[test]
  fn search_by_satpoint_returns_sat() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    let txid = server.mine_blocks(1)[0].txdata[0].compute_txid();

    server.assert_redirect(
      &format!("/search/{txid}:0:0"),
      &format!("/satpoint/{txid}:0:0"),
    );

    server.assert_redirect(
      &format!("/search?query={txid}:0:0"),
      &format!("/satpoint/{txid}:0:0"),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:0"),
      &format!("/sat/{}", 50 * COIN_VALUE),
    );

    server.assert_response_regex("/search/1:2:3", StatusCode::BAD_REQUEST, ".*");
  }

  #[test]
  fn satpoint_returns_sat_in_multiple_ranges() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    let split = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 0,
      ..default()
    };

    server.core.broadcast_tx(split);

    server.mine_blocks(1);

    let merge = TransactionTemplate {
      inputs: &[(2, 0, 0, Default::default()), (2, 1, 0, Default::default())],
      fee: 0,
      ..default()
    };

    let txid = server.core.broadcast_tx(merge);

    server.mine_blocks(1);

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:0"),
      &format!("/sat/{}", 100 * COIN_VALUE),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE),
      &format!("/sat/{}", 50 * COIN_VALUE),
    );

    server.assert_redirect(
      &format!("/satpoint/{txid}:0:{}", 50 * COIN_VALUE - 1),
      &format!("/sat/{}", 150 * COIN_VALUE - 1),
    );
  }

  #[test]
  fn fallback() {
    let server = TestServer::new();

    server.assert_redirect("/0", "/inscription/0");
    server.assert_redirect("/0/", "/inscription/0");
    server.assert_redirect("/0//", "/inscription/0");
    server.assert_redirect(
      "/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
      "/inscription/521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0",
    );
    server.assert_redirect("/-1", "/inscription/-1");
    server.assert_redirect("/FOO", "/rune/FOO");
    server.assert_redirect("/FO.O", "/rune/FO.O");
    server.assert_redirect("/FO•O", "/rune/FO•O");
    server.assert_redirect("/0:0", "/rune/0:0");
    server.assert_redirect(
      "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
      "/output/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0",
    );
    server.assert_redirect(
      "/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
      "/satpoint/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0",
    );
    server.assert_redirect(
      "/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
      "/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
    );
    server.assert_redirect(
      "/000000000000000000000000000000000000000000000000000000000000000f",
      "/tx/000000000000000000000000000000000000000000000000000000000000000f",
    );
    server.assert_redirect(
      "/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
      "/address/bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297",
    );
    server.assert_redirect(
      "/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
      "/address/bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
    );
    server.assert_redirect(
      "/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
      "/address/1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
    );

    server.assert_response_regex("/hello", StatusCode::NOT_FOUND, "");

    server.assert_response_regex(
      "/%C3%28",
      StatusCode::BAD_REQUEST,
      "invalid utf-8 sequence of 1 bytes from index 0",
    );
  }

  #[test]
  fn runes_can_be_queried_by_rune_id() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*");

    server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    server.mine_blocks(1);

    server.assert_response_regex(
      "/rune/8:1",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
    );
  }

  #[test]
  fn runes_can_be_queried_by_rune_number() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_response_regex("/rune/0", StatusCode::NOT_FOUND, ".*");

    for i in 0..10 {
      let rune = Rune(RUNE + i);
      server.etch(
        Runestone {
          edicts: vec![Edict {
            id: RuneId::default(),
            amount: u128::MAX,
            output: 0,
          }],
          etching: Some(Etching {
            rune: Some(rune),
            ..default()
          }),
          ..default()
        },
        1,
        None,
      );

      server.mine_blocks(1);
    }

    server.assert_response_regex(
      "/rune/0",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAA</title>.*",
    );

    for i in 1..6 {
      server.assert_response_regex(
        format!("/rune/{}", i),
        StatusCode::OK,
        ".*<title>Rune AAAAAAAAAAAA.*</title>.*",
      );
    }

    server.assert_response_regex(
      "/rune/9",
      StatusCode::OK,
      ".*<title>Rune AAAAAAAAAAAAJ</title>.*",
    );
  }

  #[test]
  fn rune_not_etched_shows_unlock_height() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html_status(
      "/rune/A",
      StatusCode::NOT_FOUND,
      RuneNotFoundHtml {
        rune: Rune(0),
        unlock: Some((
          Height(209999),
          Blocktime::Expected(DateTime::from_timestamp(125998800, 0).unwrap()),
        )),
      },
    );
  }

  #[test]
  fn reserved_rune_not_etched_shows_reserved_status() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html_status(
      format!("/rune/{}", Rune(Rune::RESERVED)),
      StatusCode::NOT_FOUND,
      RuneNotFoundHtml {
        rune: Rune(Rune::RESERVED),
        unlock: None,
      },
    );
  }

  #[test]
  fn runes_are_displayed_on_runes_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_html(
      "/runes",
      RunesHtml {
        entries: Vec::new(),
        more: false,
        prev: None,
        next: None,
      },
    );

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          symbol: Some('%'),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      Default::default(),
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0
          },
          premine: u128::MAX,
          timestamp: id.block,
          symbol: Some('%'),
          ..default()
        }
      )]
    );

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_html(
      "/runes",
      RunesHtml {
        entries: vec![(
          RuneId::default(),
          RuneEntry {
            spaced_rune: SpacedRune {
              rune: Rune(RUNE),
              spacers: 0,
            },
            ..default()
          },
        )],
        more: false,
        prev: None,
        next: None,
      },
    );
  }

  #[test]
  fn runes_are_displayed_on_rune_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          symbol: Some('%'),
          premine: Some(u128::MAX),
          turbo: true,
          ..default()
        }),
        ..default()
      },
      1,
      Some(
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          rune: Some(rune.commitment()),
          ..default()
        }
        .to_witness(),
      ),
    );

    let entry = RuneEntry {
      block: id.block,
      etching: txid,
      spaced_rune: SpacedRune { rune, spacers: 0 },
      premine: u128::MAX,
      symbol: Some('%'),
      timestamp: id.block,
      turbo: true,
      ..default()
    };

    assert_eq!(server.index.runes().unwrap(), [(id, entry)]);

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    let parent = InscriptionId { txid, index: 0 };

    server.assert_html(
      format!("/rune/{rune}"),
      RuneHtml {
        id,
        entry,
        mintable: false,
        parent: Some(parent),
      },
    );

    server.assert_response_regex(
      format!("/inscription/{parent}"),
      StatusCode::OK,
      ".*
<dl>
  <dt>rune</dt>
  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
  .*
</dl>
.*",
    );
  }

  #[test]
  fn etched_runes_are_displayed_on_block_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune0 = Rune(RUNE);

    let (_txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune0),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    assert_eq!(
      server.index.get_runes_in_block(id.block - 1).unwrap().len(),
      0
    );
    assert_eq!(server.index.get_runes_in_block(id.block).unwrap().len(), 1);
    assert_eq!(
      server.index.get_runes_in_block(id.block + 1).unwrap().len(),
      0
    );

    server.assert_response_regex(
      format!("/block/{}", id.block),
      StatusCode::OK,
      format!(".*<h2>1 Rune</h2>.*<li><a href=/rune/{rune0}>{rune0}</a></li>.*"),
    );
  }

  #[test]
  fn runes_are_spaced() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(rune),
          symbol: Some('%'),
          spacers: Some(1),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      Some(
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          rune: Some(rune.commitment()),
          ..default()
        }
        .to_witness(),
      ),
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 1 },
          premine: u128::MAX,
          symbol: Some('%'),
          timestamp: id.block,
          ..default()
        }
      )]
    );

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/rune/{rune}"),
      StatusCode::OK,
      r".*<title>Rune A•AAAAAAAAAAAA</title>.*<h1>A•AAAAAAAAAAAA</h1>.*",
    );

    server.assert_response_regex(
      format!("/inscription/{txid}i0"),
      StatusCode::OK,
      ".*<dt>rune</dt>.*<dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>.*",
    );

    server.assert_response_regex(
      "/runes",
      StatusCode::OK,
      ".*<li><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></li>.*",
    );

    server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      ".*
  <dt>etching</dt>
  <dd><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></dd>
.*",
    );

    server.assert_response_regex(
      format!("/output/{txid}:0"),
      StatusCode::OK,
      ".*<tr>
        <td><a href=/rune/A•AAAAAAAAAAAA>A•AAAAAAAAAAAA</a></td>
        <td>340282366920938463463374607431768211455\u{A0}%</td>
      </tr>.*",
    );
  }

  #[test]
  fn transactions_link_to_etching() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    server.assert_response_regex(
      "/runes",
      StatusCode::OK,
      ".*<title>Runes</title>.*<h1>Runes</h1>\n<ul>\n</ul>.*",
    );

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          rune: Some(Rune(RUNE)),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          etching: txid,
          spaced_rune: SpacedRune {
            rune: Rune(RUNE),
            spacers: 0
          },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    pretty_assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      ".*
  <dt>etching</dt>
  <dd><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></dd>
.*",
    );
  }

  #[test]
  fn runes_are_displayed_on_output_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_runes()
      .build();

    server.mine_blocks(1);

    let rune = Rune(RUNE);

    server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          divisibility: Some(1),
          rune: Some(rune),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          divisibility: 1,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 0 },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    let output = OutPoint { txid, vout: 0 };

    assert_eq!(
      server.index.get_rune_balances().unwrap(),
      [(output, vec![(id, u128::MAX)])]
    );

    server.assert_response_regex(
      format!("/output/{output}"),
      StatusCode::OK,
      format!(
        ".*<title>Output {output}</title>.*<h1>Output <span class=monospace>{output}</span></h1>.*
  <dt>runes</dt>
  <dd>
    <table>
      <tr>
        <th>rune</th>
        <th>balance</th>
      </tr>
      <tr>
        <td><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></td>
        <td>34028236692093846346337460743176821145.5\u{A0}¤</td>
      </tr>
    </table>
  </dd>
.*"
      ),
    );

    let address = default_address(Chain::Regtest);

    pretty_assert_eq!(
      server.get_json::<api::Output>(format!("/output/{output}")),
      api::Output {
        value: 5000000000,
        script_pubkey: address.script_pubkey(),
        address: Some(uncheck(&address)),
        confirmations: 1,
        transaction: txid,
        sat_ranges: None,
        indexed: true,
        inscriptions: Some(Vec::new()),
        outpoint: output,
        runes: Some(
          vec![(
            SpacedRune {
              rune: Rune(RUNE),
              spacers: 0
            },
            Pile {
              amount: 340282366920938463463374607431768211455,
              divisibility: 1,
              symbol: None,
            }
          )]
          .into_iter()
          .collect()
        ),
        spent: false,
      }
    );
  }

  #[test]
  fn http_to_https_redirect_with_path() {
    TestServer::builder()
      .redirect_http_to_https()
      .https()
      .build()
      .assert_redirect(
        "/sat/0",
        &format!("https://{}/sat/0", System::host_name().unwrap()),
      );
  }

  #[test]
  fn http_to_https_redirect_with_empty() {
    TestServer::builder()
      .redirect_http_to_https()
      .https()
      .build()
      .assert_redirect("/", &format!("https://{}/", System::host_name().unwrap()));
  }

  #[test]
  fn status() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(3);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        3,
        0,
        0,
        Inscription {
          content_type: None,
          body: Some("hello".as_bytes().into()),
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<h1>Status</h1>
<dl>
  <dt>chain</dt>
  <dd>regtest</dd>
  <dt>height</dt>
  <dd><a href=/block/4>4</a></dd>
  <dt>inscriptions</dt>
  <dd><a href=/inscriptions>3</a></dd>
  <dt>blessed inscriptions</dt>
  <dd>3</dd>
  <dt>cursed inscriptions</dt>
  <dd>0</dd>
  <dt>runes</dt>
  <dd><a href=/runes>0</a></dd>
  <dt>lost sats</dt>
  <dd>.*</dd>
  <dt>started</dt>
  <dd>.*</dd>
  <dt>uptime</dt>
  <dd>.*</dd>
  <dt>minimum rune for next block</dt>
  <dd>.*</dd>
  <dt>version</dt>
  <dd>.*</dd>
  <dt>unrecoverably reorged</dt>
  <dd>false</dd>
  <dt>address index</dt>
  <dd>false</dd>
  <dt>inscription index</dt>
  <dd>true</dd>
  <dt>rune index</dt>
  <dd>false</dd>
  <dt>sat index</dt>
  <dd>false</dd>
  <dt>transaction index</dt>
  <dd>false</dd>
  <dt>json api</dt>
  <dd>true</dd>
  <dt>git branch</dt>
  <dd>.*</dd>
  <dt>git commit</dt>
  <dd>
    <a class=collapse href=https://github.com/ordinals/ord/commit/[[:xdigit:]]{40}>
      [[:xdigit:]]{40}
    </a>
  </dd>
</dl>
.*",
    );
  }

  #[test]
  fn block_count_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockcount");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "1");

    test_server.mine_blocks(1);

    let response = test_server.get("/blockcount");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "2");
  }

  #[test]
  fn block_height_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockheight");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "0");

    test_server.mine_blocks(2);

    let response = test_server.get("/blockheight");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "2");
  }

  #[test]
  fn block_hash_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockhash");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.text().unwrap(),
      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    );
  }

  #[test]
  fn block_hash_from_height_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blockhash/0");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.text().unwrap(),
      "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
    );
  }

  #[test]
  fn block_time_endpoint() {
    let test_server = TestServer::new();

    let response = test_server.get("/blocktime");

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(response.text().unwrap(), "1231006505");
  }

  #[test]
  fn sat_number() {
    TestServer::new().assert_response_regex("/sat/0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_decimal() {
    TestServer::new().assert_response_regex("/sat/0.0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_degree() {
    TestServer::new().assert_response_regex("/sat/0°0′0″0‴", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
  }

  #[test]
  fn sat_name() {
    TestServer::new().assert_response_regex(
      "/sat/nvtdijuwxlp",
      StatusCode::OK,
      ".*<h1>Sat 0</h1>.*",
    );
  }

  #[test]
  fn sat() {
    TestServer::new().assert_response_regex(
      "/sat/0",
      StatusCode::OK,
      ".*<title>Sat 0</title>.*<h1>Sat 0</h1>.*",
    );
  }

  #[test]
  fn block() {
    TestServer::new().assert_response_regex(
      "/block/0",
      StatusCode::OK,
      ".*<title>Block 0</title>.*<h1>Block 0</h1>.*",
    );
  }

  #[test]
  fn sat_out_of_range() {
    TestServer::new().assert_response(
      "/sat/2099999997690000",
      StatusCode::BAD_REQUEST,
      "Invalid URL: failed to parse sat `2099999997690000`: invalid integer range",
    );
  }

  #[test]
  fn invalid_outpoint_hash_returns_400() {
    TestServer::new().assert_response(
      "/output/foo:0",
      StatusCode::BAD_REQUEST,
      "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
    );
  }

  #[test]
  fn output_with_sat_index() {
    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        format!("/output/{txid}:0"),
        StatusCode::OK,
        format!(
          ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>1</dd>
  <dt>spent</dt><dd>false</dd>
</dl>
<h2>1 Sat Range</h2>
<ul class=monospace>
  <li><a href=/sat/0 class=mythic>0</a>-<a href=/sat/4999999999 class=common>4999999999</a> \\(5000000000 sats\\)</li>
</ul>.*"
        ),
      );
  }

  #[test]
  fn output_without_sat_index() {
    let txid = "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b";
    TestServer::new().assert_response_regex(
      format!("/output/{txid}:0"),
      StatusCode::OK,
      format!(
        ".*<title>Output {txid}:0</title>.*<h1>Output <span class=monospace>{txid}:0</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace>OP_PUSHBYTES_65 [[:xdigit:]]{{130}} OP_CHECKSIG</dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>1</dd>
  <dt>spent</dt><dd>false</dd>
</dl>.*"
      ),
    );
  }

  #[test]
  fn null_output_receives_lost_sats() {
    let server = TestServer::builder().index_sats().build();

    server.mine_blocks_with_subsidy(1, 0);

    let txid = "0000000000000000000000000000000000000000000000000000000000000000";

    server.assert_response_regex(
      format!("/output/{txid}:4294967295"),
      StatusCode::OK,
      format!(
        ".*<title>Output {txid}:4294967295</title>.*<h1>Output <span class=monospace>{txid}:4294967295</span></h1>
<dl>
  <dt>value</dt><dd>5000000000</dd>
  <dt>script pubkey</dt><dd class=monospace></dd>
  <dt>transaction</dt><dd><a class=collapse href=/tx/{txid}>{txid}</a></dd>
  <dt>confirmations</dt><dd>0</dd>
  <dt>spent</dt><dd>false</dd>
</dl>
<h2>1 Sat Range</h2>
<ul class=monospace>
  <li><a href=/sat/5000000000 class=uncommon>5000000000</a>-<a href=/sat/9999999999 class=common>9999999999</a> \\(5000000000 sats\\)</li>
</ul>.*"
      ),
    );
  }

  #[test]
  fn unbound_output_receives_unbound_inscriptions() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 50 * 100_000_000,
      ..default()
    });

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        1,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{}", inscription_id),
      StatusCode::OK,
      format!(
        ".*<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*<dt>output</dt>
  <dd><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:0>0000000000000000000000000000000000000000000000000000000000000000:0</a></dd>.*"
      ),
    );

    server.assert_response_regex(
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
      StatusCode::OK,
      ".*<h1>Output <span class=monospace>0000000000000000000000000000000000000000000000000000000000000000:0</span></h1>
<dl>
  <dt>inscriptions</dt>
  <dd class=thumbnails>
    <a href=/inscription/.*><iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/.*></iframe></a>
  </dd>.*",
    );
  }

  #[test]
  fn unbound_output_returns_200() {
    TestServer::new().assert_response_regex(
      "/output/0000000000000000000000000000000000000000000000000000000000000000:0",
      StatusCode::OK,
      ".*",
    );
  }

  #[test]
  fn invalid_output_returns_400() {
    TestServer::new().assert_response(
      "/output/foo:0",
      StatusCode::BAD_REQUEST,
      "Invalid URL: Cannot parse `output` with value `foo:0`: error parsing TXID",
    );
  }

  #[test]
  fn home() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut ids = Vec::new();

    for i in 0..101 {
      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("image/png", "hello").to_witness())],
        ..default()
      });
      ids.push(InscriptionId { txid, index: 0 });
      server.mine_blocks(1);
    }

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/",
      StatusCode::OK,
      format!(
        r".*<title>Ordinals</title>.*
<h1>Latest Inscriptions</h1>
<div class=thumbnails>
  <a href=/inscription/{}>.*</a>
  (<a href=/inscription/[[:xdigit:]]{{64}}i0>.*</a>\s*){{99}}
</div>
.*
",
        ids[100]
      ),
    );
  }

  #[test]
  fn blocks() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);

    test_server.assert_response_regex(
      "/blocks",
      StatusCode::OK,
      ".*<title>Blocks</title>.*
<h1>Blocks</h1>
<div class=block>
  <h2><a href=/block/1>Block 1</a></h2>
  <div class=thumbnails>
  </div>
</div>
<div class=block>
  <h2><a href=/block/0>Block 0</a></h2>
  <div class=thumbnails>
  </div>
</div>
</ol>.*",
    );
  }

  #[test]
  fn nav_displays_chain() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response_regex(
        "/",
        StatusCode::OK,
        ".*<a href=/ title=home>Ordinals<sup>regtest</sup></a>.*",
      );
  }

  #[test]
  fn blocks_block_limit() {
    let test_server = TestServer::new();

    test_server.mine_blocks(101);

    test_server.assert_response_regex(
      "/blocks",
      StatusCode::OK,
      ".*<ol start=96 reversed class=block-list>\n(  <li><a class=collapse href=/block/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>\n){95}</ol>.*"
    );
  }

  #[test]
  fn block_not_found() {
    TestServer::new().assert_response(
      "/block/467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16",
      StatusCode::NOT_FOUND,
      "block 467a86f0642b1d284376d13a98ef58310caa49502b0f9a560ee222e0a122fe16 not found",
    );
  }

  #[test]
  fn unmined_sat() {
    TestServer::new().assert_response_regex(
      "/sat/0",
      StatusCode::OK,
      ".*<dt>timestamp</dt><dd><time>2009-01-03 18:15:05 UTC</time></dd>.*",
    );
  }

  #[test]
  fn mined_sat() {
    TestServer::new().assert_response_regex(
      "/sat/5000000000",
      StatusCode::OK,
      ".*<dt>timestamp</dt><dd><time>.*</time> \\(expected\\)</dd>.*",
    );
  }

  #[test]
  fn static_asset() {
    TestServer::new().assert_response_regex(
      "/static/index.css",
      StatusCode::OK,
      r".*\.rare \{
  background-color: var\(--rare\);
}.*",
    );
  }

  #[test]
  fn favicon() {
    TestServer::new().assert_response_regex("/favicon.ico", StatusCode::OK, r".*");
  }

  #[test]
  fn clock_updates() {
    let test_server = TestServer::new();
    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>0</text>.*");
    test_server.mine_blocks(1);
    test_server.assert_response_regex("/clock", StatusCode::OK, ".*<text.*>1</text>.*");
  }

  #[test]
  fn block_by_hash() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);
    let transaction = TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      fee: 0,
      ..default()
    };
    test_server.core.broadcast_tx(transaction);
    let block_hash = test_server.mine_blocks(1)[0].block_hash();

    test_server.assert_response_regex(
      format!("/block/{block_hash}"),
      StatusCode::OK,
      ".*<h1>Block 2</h1>.*",
    );
  }

  #[test]
  fn block_by_height() {
    let test_server = TestServer::new();

    test_server.assert_response_regex("/block/0", StatusCode::OK, ".*<h1>Block 0</h1>.*");
  }

  #[test]
  fn transaction() {
    let test_server = TestServer::new();

    let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
    let txid = coinbase_tx.compute_txid();

    test_server.assert_response_regex(
      format!("/tx/{txid}"),
      StatusCode::OK,
      format!(
        ".*<title>Transaction {txid}</title>.*<h1>Transaction <span class=monospace>{txid}</span></h1>
<dl>
</dl>
<h2>1 Input</h2>
<ul>
  <li><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:4294967295>0000000000000000000000000000000000000000000000000000000000000000:4294967295</a></li>
</ul>
<h2>1 Output</h2>
<ul class=monospace>
  <li>
    <a href=/output/{txid}:0 class=collapse>
      {txid}:0
    </a>
    <dl>
      <dt>value</dt><dd>5000000000</dd>
      <dt>script pubkey</dt><dd class=monospace>.*</dd>
    </dl>
  </li>
</ul>.*"
      ),
    );
  }

  #[test]
  fn recursive_transaction_hex_endpoint() {
    let test_server = TestServer::new();

    let coinbase_tx = test_server.mine_blocks(1)[0].txdata[0].clone();
    let txid = coinbase_tx.compute_txid();

    test_server.assert_response(
      format!("/r/tx/{txid}"),
      StatusCode::OK,
      "\"02000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0151ffffffff0100f2052a01000000225120be7cbbe9ca06a7d7b2a17c6b4ff4b85b362cbcd7ee1970daa66dfaa834df59a000000000\""
    );
  }

  #[test]
  fn recursive_transaction_hex_endpoint_for_genesis_transaction() {
    let test_server = TestServer::new();

    test_server.mine_blocks(1);

    test_server.assert_response(
      "/r/tx/4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b",
      StatusCode::OK,
      "\"01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000\""
    );
  }

  #[test]
  fn detect_unrecoverable_reorg() {
    let test_server = TestServer::new();

    test_server.mine_blocks(21);

    test_server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<dt>unrecoverably reorged</dt>\n  <dd>false</dd>.*",
    );

    for _ in 0..15 {
      test_server.core.invalidate_tip();
    }

    test_server.core.mine_blocks(21);

    test_server.assert_response_regex(
      "/status",
      StatusCode::OK,
      ".*<dt>unrecoverably reorged</dt>\n  <dd>true</dd>.*",
    );
  }

  #[test]
  fn rare_with_sat_index() {
    TestServer::builder().index_sats().build().assert_response(
      "/rare.txt",
      StatusCode::OK,
      "sat\tsatpoint
0\t4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0
",
    );
  }

  #[test]
  fn rare_without_sat_index() {
    TestServer::new().assert_response(
      "/rare.txt",
      StatusCode::OK,
      "sat\tsatpoint
",
    );
  }

  #[test]
  fn show_rare_txt_in_header_with_sat_index() {
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        "/",
        StatusCode::OK,
        ".*
      <a href=/clock title=clock>.*</a>
      <a href=/rare.txt title=rare>.*</a>.*",
      );
  }

  #[test]
  fn rare_sat_location() {
    TestServer::builder()
      .index_sats()
      .build()
      .assert_response_regex(
        "/sat/0",
        StatusCode::OK,
        ".*>4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0<.*",
      );
  }

  #[test]
  fn dont_show_rare_txt_in_header_without_sat_index() {
    TestServer::new().assert_response_regex(
      "/",
      StatusCode::OK,
      ".*
      <a href=/clock title=clock>.*</a>
      <a href=https://docs.ordinals.com/.*",
    );
  }

  #[test]
  fn input() {
    TestServer::new().assert_response_regex(
      "/input/0/0/0",
      StatusCode::OK,
      ".*<title>Input /0/0/0</title>.*<h1>Input /0/0/0</h1>.*<dt>text</dt><dd>.*The Times 03/Jan/2009 Chancellor on brink of second bailout for banks</dd>.*",
    );
  }

  #[test]
  fn input_missing() {
    TestServer::new().assert_response(
      "/input/1/1/1",
      StatusCode::NOT_FOUND,
      "input /1/1/1 not found",
    );
  }

  #[test]
  fn commits_are_tracked() {
    let server = TestServer::new();

    thread::sleep(Duration::from_millis(100));
    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 1);
    assert_eq!(info.transactions[0].starting_block_count, 0);

    server.index.update().unwrap();

    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 1);
    assert_eq!(info.transactions[0].starting_block_count, 0);

    server.mine_blocks(1);

    thread::sleep(Duration::from_millis(10));
    server.index.update().unwrap();

    assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2);

    let info = server.index.info().unwrap();
    assert_eq!(info.transactions.len(), 2);
    assert_eq!(info.transactions[0].starting_block_count, 0);
    assert_eq!(info.transactions[1].starting_block_count, 1);
    assert!(
      info.transactions[1].starting_timestamp - info.transactions[0].starting_timestamp >= 10
    );
  }

  #[test]
  fn outputs_traversed_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      1
    );

    server.index.update().unwrap();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      1
    );

    server.mine_blocks(2);

    server.index.update().unwrap();

    assert_eq!(
      server
        .index
        .statistic(crate::index::Statistic::OutputsTraversed),
      3
    );
  }

  #[test]
  fn coinbase_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      2
    );

    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      3
    );
  }

  #[test]
  fn split_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);
    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 0,
      ..default()
    });
    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      4,
    );
  }

  #[test]
  fn fee_sat_ranges_are_tracked() {
    let server = TestServer::builder().index_sats().build();

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      1
    );

    server.mine_blocks(1);
    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, Default::default())],
      outputs: 2,
      fee: 2,
      ..default()
    });
    server.mine_blocks(1);

    assert_eq!(
      server.index.statistic(crate::index::Statistic::SatRanges),
      5,
    );
  }

  #[test]
  fn content_response_no_content() {
    assert_eq!(
      r::content_response(
        Inscription {
          content_type: Some("text/plain".as_bytes().to_vec()),
          body: None,
          ..default()
        },
        AcceptEncoding::default(),
        &ServerConfig::default(),
      )
      .unwrap(),
      None
    );
  }

  #[test]
  fn content_response_with_content() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "text/plain");
    assert_eq!(body, vec![1, 2, 3]);
  }

  #[test]
  fn content_security_policy_no_origin() {
    let (headers, _) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
    )
    .unwrap()
    .unwrap();

    assert_eq!(
      headers["content-security-policy"],
      HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:")
    );
  }

  #[test]
  fn content_security_policy_with_origin() {
    let (headers, _) = r::content_response(
      Inscription {
        content_type: Some("text/plain".as_bytes().to_vec()),
        body: Some(vec![1, 2, 3]),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig {
        csp_origin: Some("https://ordinals.com".into()),
        ..default()
      },
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-security-policy"], HeaderValue::from_static("default-src https://ordinals.com/content/ https://ordinals.com/blockheight https://ordinals.com/blockhash https://ordinals.com/blockhash/ https://ordinals.com/blocktime https://ordinals.com/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"));
  }

  #[test]
  fn preview_content_security_policy() {
    {
      let server = TestServer::builder().chain(Chain::Regtest).build();

      server.mine_blocks(1);

      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      server.assert_response_csp(
        format!("/preview/{}", inscription_id),
        StatusCode::OK,
        "default-src 'self'",
        format!(
          ".*<html lang=en data-inscription={}>.*<title>Inscription 0 Preview</title>.*",
          inscription_id
        ),
      );
    }

    {
      let server = TestServer::builder()
        .chain(Chain::Regtest)
        .server_option("--csp-origin", "https://ordinals.com")
        .build();

      server.mine_blocks(1);

      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      let inscription_id = InscriptionId { txid, index: 0 };

      server.assert_response_csp(
        format!("/preview/{}", inscription_id),
        StatusCode::OK,
        "default-src https://ordinals.com",
        format!(".*<html lang=en data-inscription={}>.*", inscription_id),
      );
    }
  }

  #[test]
  fn code_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/javascript", "hello").to_witness(),
      )],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<html lang=en data-inscription={inscription_id} data-language=javascript>.*"),
    );
  }

  #[test]
  fn content_response_no_content_type() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: None,
        body: Some(Vec::new()),
        ..default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "application/octet-stream");
    assert!(body.is_empty());
  }

  #[test]
  fn content_response_bad_content_type() {
    let (headers, body) = r::content_response(
      Inscription {
        content_type: Some("\n".as_bytes().to_vec()),
        body: Some(Vec::new()),
        ..Default::default()
      },
      AcceptEncoding::default(),
      &ServerConfig::default(),
    )
    .unwrap()
    .unwrap();

    assert_eq!(headers["content-type"], "application/octet-stream");
    assert!(body.is_empty());
  }

  #[test]
  fn text_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{}", inscription_id),
      StatusCode::OK,
      "default-src 'self'",
      format!(".*<html lang=en data-inscription={}>.*", inscription_id),
    );
  }

  #[test]
  fn audio_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("audio/flac", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<audio .*>\s*<source src=/content/{inscription_id}>.*"),
    );
  }

  #[test]
  fn font_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("font/ttf", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*src: url\(/content/{inscription_id}\).*"),
    );
  }

  #[test]
  fn pdf_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("application/pdf", "hello").to_witness(),
      )],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<canvas data-inscription={inscription_id}></canvas>.*"),
    );
  }

  #[test]
  fn markdown_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/markdown", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<html lang=en data-inscription={inscription_id}>.*"),
    );
  }

  #[test]
  fn image_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("image/png", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      "default-src 'self' 'unsafe-inline'",
      format!(r".*background-image: url\(/content/{inscription_id}\);.*"),
    );
  }

  #[test]
  fn iframe_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/html;charset=utf-8", "hello").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
      "hello",
    );
  }

  #[test]
  fn unknown_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_csp(
      format!("/preview/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      "default-src 'self'",
      fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn video_preview() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("video/webm", "hello").to_witness())],
      ..default()
    });
    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      format!(r".*<video .*>\s*<source src=/content/{inscription_id}>.*"),
    );
  }

  #[test]
  fn inscription_page_title() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      ".*<title>Inscription 0</title>.*",
    );
  }

  #[test]
  fn inscription_page_has_sat_when_sats_are_tracked() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      r".*<dt>sat</dt>\s*<dd><a href=/sat/5000000000>5000000000</a></dd>\s*<dt>sat name</dt>\s*<dd><a href=/sat/nvtcsezkbth>nvtcsezkbth</a></dd>\s*<dt>preview</dt>.*",
    );
  }

  #[test]
  fn inscriptions_can_be_looked_up_by_sat_name() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", Sat(5000000000).name()),
      StatusCode::OK,
      ".*<title>Inscription 0</title.*",
    );
  }

  #[test]
  fn inscriptions_can_be_looked_up_by_sat_name_with_letter_i() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.assert_response_regex("/inscription/i", StatusCode::NOT_FOUND, ".*");
  }

  #[test]
  fn inscription_page_does_not_have_sat_when_sats_are_not_tracked() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{}", InscriptionId { txid, index: 0 }),
      StatusCode::OK,
      r".*<dt>value</dt>\s*<dd>5000000000</dd>\s*<dt>preview</dt>.*",
    );
  }

  #[test]
  fn strict_transport_security_header_is_set() {
    assert_eq!(
      TestServer::new()
        .get("/status")
        .headers()
        .get(header::STRICT_TRANSPORT_SECURITY)
        .unwrap(),
      "max-age=31536000; includeSubDomains; preload",
    );
  }

  #[test]
  fn feed() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      "/feed.xml",
      StatusCode::OK,
      ".*<title>Inscription 0</title>.*",
    );
  }

  #[test]
  fn inscription_with_unknown_type_and_no_body_has_unknown_preview() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        Inscription {
          content_type: Some("foo/bar".as_bytes().to_vec()),
          body: None,
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn inscription_with_known_type_and_no_body_has_unknown_preview() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        Inscription {
          content_type: Some("image/png".as_bytes().to_vec()),
          body: None,
          ..default()
        }
        .to_witness(),
      )],
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response(
      format!("/preview/{inscription_id}"),
      StatusCode::OK,
      &fs::read_to_string("templates/preview-unknown.html").unwrap(),
    );
  }

  #[test]
  fn content_responses_have_cache_control_headers() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let response = server.get(format!("/content/{}", InscriptionId { txid, index: 0 }));

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
      response.headers().get(header::CACHE_CONTROL).unwrap(),
      "public, max-age=1209600, immutable"
    );
  }

  #[test]
  fn error_content_responses_have_max_age_zero_cache_control_headers() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    let response =
      server.get("/content/6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0");

    assert_eq!(response.status(), 404);
    assert_eq!(
      response.headers().get(header::CACHE_CONTROL).unwrap(),
      "no-store"
    );
  }

  #[test]
  fn inscriptions_page_with_no_prev_or_next() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build()
      .assert_response_regex("/inscriptions", StatusCode::OK, ".*prev\nnext.*");
  }

  #[test]
  fn inscriptions_page_with_no_next() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for i in 0..101 {
      server.mine_blocks(1);
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/1",
      StatusCode::OK,
      ".*<a class=prev href=/inscriptions/0>prev</a>\nnext.*",
    );
  }

  #[test]
  fn inscriptions_page_with_no_prev() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for i in 0..101 {
      server.mine_blocks(1);
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/0",
      StatusCode::OK,
      ".*prev\n<a class=next href=/inscriptions/1>next</a>.*",
    );
  }

  #[test]
  fn collections_page_prev_and_next() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    let mut parent_ids = Vec::new();

    for i in 0..101 {
      server.mine_blocks(1);

      parent_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
          ..default()
        }),
        index: 0,
      });
    }

    for (i, parent_id) in parent_ids.iter().enumerate().take(101) {
      server.mine_blocks(1);

      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[
          (i + 2, 1, 0, Default::default()),
          (
            i + 102,
            0,
            0,
            Inscription {
              content_type: Some("text/plain".into()),
              body: Some("hello".into()),
              parents: vec![parent_id.value()],
              ..default()
            }
            .to_witness(),
          ),
        ],
        outputs: 2,
        output_values: &[50 * COIN_VALUE, 50 * COIN_VALUE],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/collections",
      StatusCode::OK,
      r".*
<h1>Collections</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
  (<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>\s*){99}
</div>
<div class=center>
prev
<a class=next href=/collections/1>next</a>
</div>.*"
        .to_string()
        .unindent(),
    );

    server.assert_response_regex(
      "/collections/1",
      StatusCode::OK,
      ".*
<h1>Collections</h1>
<div class=thumbnails>
  <a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>
</div>
<div class=center>
<a class=prev href=/collections/0>prev</a>
next
</div>.*"
        .unindent(),
    );
  }

  #[test]
  fn responses_are_gzipped() {
    let server = TestServer::new();

    let mut headers = HeaderMap::new();

    headers.insert(header::ACCEPT_ENCODING, "gzip".parse().unwrap());

    let response = reqwest::blocking::Client::builder()
      .default_headers(headers)
      .build()
      .unwrap()
      .get(server.join_url("/"))
      .send()
      .unwrap();

    assert_eq!(
      response.headers().get(header::CONTENT_ENCODING).unwrap(),
      "gzip"
    );
  }

  #[test]
  fn responses_are_brotlied() {
    let server = TestServer::new();

    let mut headers = HeaderMap::new();

    headers.insert(header::ACCEPT_ENCODING, "br".parse().unwrap());

    let response = reqwest::blocking::Client::builder()
      .default_headers(headers)
      .brotli(false)
      .build()
      .unwrap()
      .get(server.join_url("/"))
      .send()
      .unwrap();

    assert_eq!(
      response.headers().get(header::CONTENT_ENCODING).unwrap(),
      "br"
    );
  }

  #[test]
  fn inscription_links_to_parent() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 1</title>.*<dt>parents</dt>.*<div class=thumbnails>.**<a href=/inscription/{parent_inscription_id}><iframe .* src=/preview/{parent_inscription_id}></iframe></a>.*"),
    );
    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 0</title>.*<dt>children</dt>.*<a href=/inscription/{inscription_id}>.*</a>.*"),
    );

    assert_eq!(
      server
        .get_json::<api::Inscription>(format!("/inscription/{inscription_id}"))
        .parents,
      vec![parent_inscription_id],
    );

    assert_eq!(
      server
        .get_json::<api::Inscription>(format!("/inscription/{parent_inscription_id}"))
        .children,
      [inscription_id],
    );
  }

  #[test]
  fn inscription_with_and_without_children_page() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/children/{parent_inscription_id}"),
      StatusCode::OK,
      ".*<h3>No children</h3>.*",
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/children/{parent_inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription 0 Children</title>.*<h1><a href=/inscription/{parent_inscription_id}>Inscription 0</a> Children</h1>.*<div class=thumbnails>.*<a href=/inscription/{inscription_id}><iframe .* src=/preview/{inscription_id}></iframe></a>.*"),
    );
  }

  #[test]
  fn inscriptions_page_shows_max_four_children() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(6);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let _txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          4,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          5,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          6,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription 0</title>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*
    <div class=center>
      <a href=/children/{parent_inscription_id}>all \\(5\\)</a>
    </div>.*"
      ),
    );
  }

  #[test]
  fn inscription_child() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(2);

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    let child_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          2,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![parent_inscription_id.value()],
            ..default()
          }
          .to_witness(),
        ),
        (2, 1, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let child0 = InscriptionId {
      txid: child_txid,
      index: 0,
    };

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}/0"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription 1</title>.*
.*<dt>id</dt>
.*<dd class=collapse>{child0}</dd>.*"
      ),
    );

    let child1 = InscriptionId {
      txid: child_txid,
      index: 1,
    };

    server.assert_response_regex(
      format!("/inscription/{parent_inscription_id}/1"),
      StatusCode::OK,
      format!(
        ".*<title>Inscription -1</title>.*
.*<dt>id</dt>
.*<dd class=collapse>{child1}</dd>.*"
      ),
    );
  }

  #[test]
  fn inscription_with_parent_page() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(2);

    let parent_a_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_b_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let parent_a_inscription_id = InscriptionId {
      txid: parent_a_txid,
      index: 0,
    };

    let parent_b_inscription_id = InscriptionId {
      txid: parent_b_txid,
      index: 0,
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (
          3,
          0,
          0,
          Inscription {
            content_type: Some("text/plain".into()),
            body: Some("hello".into()),
            parents: vec![
              parent_a_inscription_id.value(),
              parent_b_inscription_id.value(),
            ],
            ..default()
          }
          .to_witness(),
        ),
        (3, 1, 0, Default::default()),
        (3, 2, 0, Default::default()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/parents/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>.*<a href=/inscription/{parent_a_inscription_id}><iframe .* src=/preview/{parent_b_inscription_id}></iframe></a>.*"),
    );
  }

  #[test]
  fn inscription_parent_page_pagination() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut parent_ids = Vec::new();
    let mut inputs = Vec::new();
    for i in 0..101 {
      parent_ids.push(
        InscriptionId {
          txid: server.core.broadcast_tx(TransactionTemplate {
            inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
            ..default()
          }),
          index: 0,
        }
        .value(),
      );

      inputs.push((i + 2, 1, 0, Witness::default()));

      server.mine_blocks(1);
    }

    inputs.insert(
      0,
      (
        102,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          parents: parent_ids,
          ..default()
        }
        .to_witness(),
      ),
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &inputs,
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/parents/{inscription_id}"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{100}}.*"),
    );

    server.assert_response_regex(
      format!("/parents/{inscription_id}/1"),
      StatusCode::OK,
      format!(".*<title>Inscription -1 Parents</title>.*<h1><a href=/inscription/{inscription_id}>Inscription -1</a> Parents</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){{1}}.*"),
    );

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      ".*<title>Inscription -1</title>.*<h1>Inscription -1</h1>.*<div class=thumbnails>(.*<a href=/inscription/.*><iframe .* src=/preview/.*></iframe></a>.*){4}.*",
    );
  }

  #[test]
  fn inscription_number_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, inscription("text/plain", "hello").to_witness()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let inscription_id = InscriptionId { txid, index: 0 };
    let cursed_inscription_id = InscriptionId { txid, index: 1 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{inscription_id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*"
      ),
    );
    server.assert_response_regex(
      "/inscription/0",
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{inscription_id}</dd>.*"
      ),
    );

    server.assert_response_regex(
      "/inscription/-1",
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{cursed_inscription_id}</dd>.*"
      ),
    )
  }

  #[test]
  fn charm_cursed() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, Witness::default()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=cursed>👹</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_vindicated() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(110);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, Witness::default()),
        (2, 0, 0, inscription("text/plain", "cursed").to_witness()),
      ],
      outputs: 2,
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_coin() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=coin>🪙</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_uncommon() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(2);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=uncommon>🌱</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_nineball() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    server.mine_blocks(9);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>.*<span title=nineball>9️⃣</span>.*</dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_reinscription() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, inscription("text/plain", "bar").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_reinscription_in_same_tx_input() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let script = script::Builder::new()
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"foo")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"bar")
      .push_opcode(opcodes::all::OP_ENDIF)
      .push_opcode(opcodes::OP_FALSE)
      .push_opcode(opcodes::all::OP_IF)
      .push_slice(b"ord")
      .push_slice([1])
      .push_slice(b"text/plain;charset=utf-8")
      .push_slice([])
      .push_slice(b"qix")
      .push_opcode(opcodes::all::OP_ENDIF)
      .into_script();

    let witness = Witness::from_slice(&[script.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, witness)],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );

    let id = InscriptionId { txid, index: 1 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );

    let id = InscriptionId { txid, index: 2 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );
  }

  #[test]
  fn charm_reinscription_in_same_tx_with_pointer() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(3);

    let cursed_inscription = inscription("text/plain", "bar");
    let reinscription: Inscription = InscriptionTemplate {
      pointer: Some(0),
      ..default()
    }
    .into();

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (1, 0, 0, inscription("text/plain", "foo").to_witness()),
        (2, 0, 0, cursed_inscription.to_witness()),
        (3, 0, 0, reinscription.to_witness()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  .*
</dl>
.*
"
      ),
    );

    let id = InscriptionId { txid, index: 1 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=cursed>👹</span>.*",
    );

    let id = InscriptionId { txid, index: 2 };
    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      ".*
    <span title=reinscription>♻️</span>
    <span title=cursed>👹</span>.*",
    );
  }

  #[test]
  fn charm_unbound() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, envelope(&[b"ord", &[128], &[0]]))],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription -1</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=cursed>👹</span>
    <span title=unbound>🔓</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn charm_lost() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    let id = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  .*
  <dt>value</dt>
  <dd>5000000000</dd>
  .*
</dl>
.*
"
      ),
    );

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 50 * COIN_VALUE,
      ..default()
    });

    server.mine_blocks_with_subsidy(1, 0);

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 0</h1>.*
<dl>
  <dt>id</dt>
  <dd class=collapse>{id}</dd>
  <dt>charms</dt>
  <dd>
    <span title=lost>🤔</span>
  </dd>
  .*
</dl>
.*
"
      ),
    );
  }

  #[test]
  fn utxo_recursive_endpoint_all() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .index_runes()
      .build();

    let rune = Rune(RUNE);

    let (txid, id) = server.etch(
      Runestone {
        edicts: vec![Edict {
          id: RuneId::default(),
          amount: u128::MAX,
          output: 0,
        }],
        etching: Some(Etching {
          divisibility: Some(1),
          rune: Some(rune),
          premine: Some(u128::MAX),
          ..default()
        }),
        ..default()
      },
      1,
      None,
    );

    pretty_assert_eq!(
      server.index.runes().unwrap(),
      [(
        id,
        RuneEntry {
          block: id.block,
          divisibility: 1,
          etching: txid,
          spaced_rune: SpacedRune { rune, spacers: 0 },
          premine: u128::MAX,
          timestamp: id.block,
          ..default()
        }
      )]
    );

    server.mine_blocks(1);

    // merge rune with two inscriptions
    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[
        (6, 0, 0, inscription("text/plain", "foo").to_witness()),
        (7, 0, 0, inscription("text/plain", "bar").to_witness()),
        (7, 1, 0, Witness::new()),
      ],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };
    let second_inscription_id = InscriptionId { txid, index: 1 };
    let outpoint: OutPoint = OutPoint { txid, vout: 0 };

    let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{}", outpoint));

    pretty_assert_eq!(
      utxo_recursive,
      api::UtxoRecursive {
        inscriptions: Some(vec![inscription_id, second_inscription_id]),
        runes: Some(
          [(
            SpacedRune { rune, spacers: 0 },
            Pile {
              amount: u128::MAX,
              divisibility: 1,
              symbol: None
            }
          )]
          .into_iter()
          .collect()
        ),
        sat_ranges: Some(vec![
          (6 * 50 * COIN_VALUE, 7 * 50 * COIN_VALUE),
          (7 * 50 * COIN_VALUE, 8 * 50 * COIN_VALUE),
          (50 * COIN_VALUE, 2 * 50 * COIN_VALUE)
        ]),
        value: 150 * COIN_VALUE,
      }
    );
  }

  #[test]
  fn utxo_recursive_endpoint_only_inscriptions() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };
    let outpoint: OutPoint = OutPoint { txid, vout: 0 };

    let utxo_recursive = server.get_json::<api::UtxoRecursive>(format!("/r/utxo/{}", outpoint));

    pretty_assert_eq!(
      utxo_recursive,
      api::UtxoRecursive {
        inscriptions: Some(vec![inscription_id]),
        runes: None,
        sat_ranges: None,
        value: 50 * COIN_VALUE,
      }
    );
  }

  #[test]
  fn sat_recursive_endpoints() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    assert_eq!(
      server.get_json::<api::SatInscriptions>("/r/sat/5000000000"),
      api::SatInscriptions {
        ids: Vec::new(),
        page: 0,
        more: false
      }
    );

    assert_eq!(
      server.get_json::<api::SatInscription>("/r/sat/5000000000/at/0"),
      api::SatInscription { id: None }
    );

    server.mine_blocks(1);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let mut ids = Vec::new();
    ids.push(InscriptionId { txid, index: 0 });

    for i in 1..111 {
      let txid = server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 1, 0, inscription("text/plain", "foo").to_witness())],
        ..default()
      });

      server.mine_blocks(1);

      ids.push(InscriptionId { txid, index: 0 });
    }

    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000");

    let equivalent_paginated_response =
      server.get_json::<api::SatInscriptions>("/r/sat/5000000000/0");

    assert_eq!(paginated_response.ids.len(), 100);
    assert!(paginated_response.more);
    assert_eq!(paginated_response.page, 0);

    assert_eq!(
      paginated_response.ids.len(),
      equivalent_paginated_response.ids.len()
    );
    assert_eq!(paginated_response.more, equivalent_paginated_response.more);
    assert_eq!(paginated_response.page, equivalent_paginated_response.page);

    let paginated_response = server.get_json::<api::SatInscriptions>("/r/sat/5000000000/1");

    assert_eq!(paginated_response.ids.len(), 11);
    assert!(!paginated_response.more);
    assert_eq!(paginated_response.page, 1);

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/0")
        .id,
      Some(ids[0])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-111")
        .id,
      Some(ids[0])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/110")
        .id,
      Some(ids[110])
    );

    assert_eq!(
      server
        .get_json::<api::SatInscription>("/r/sat/5000000000/at/-1")
        .id,
      Some(ids[110])
    );

    assert!(server
      .get_json::<api::SatInscription>("/r/sat/5000000000/at/111")
      .id
      .is_none());
  }

  #[test]
  fn children_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_inscription_id}"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));
    assert_eq!(children_json.ids.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_inscription_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_inscription_id = InscriptionId { txid, index: 0 };
    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}"));

    assert_eq!(children_json.ids.len(), 100);
    assert_eq!(children_json.ids[0], first_child_inscription_id);
    assert_eq!(children_json.ids[99], hundredth_child_inscription_id);
    assert!(children_json.more);
    assert_eq!(children_json.page, 0);

    let children_json =
      server.get_json::<api::Children>(format!("/r/children/{parent_inscription_id}/1"));

    assert_eq!(children_json.ids.len(), 11);
    assert_eq!(children_json.ids[0], hundred_first_child_inscription_id);
    assert_eq!(children_json.ids[10], hundred_eleventh_child_inscription_id);
    assert!(!children_json.more);
    assert_eq!(children_json.page, 1);
  }

  #[test]
  fn parents_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let mut parent_ids = Vec::new();
    let mut inputs = Vec::new();
    for i in 0..111 {
      parent_ids.push(InscriptionId {
        txid: server.core.broadcast_tx(TransactionTemplate {
          inputs: &[(i + 1, 0, 0, inscription("text/plain", "hello").to_witness())],
          ..default()
        }),
        index: 0,
      });

      inputs.push((i + 2, 1, 0, Witness::default()));

      server.mine_blocks(1);
    }

    inputs.insert(
      0,
      (
        112,
        0,
        0,
        Inscription {
          content_type: Some("text/plain".into()),
          body: Some("hello".into()),
          parents: parent_ids.iter().map(|id| id.value()).collect(),
          ..default()
        }
        .to_witness(),
      ),
    );

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &inputs,
      ..default()
    });

    server.mine_blocks(1);

    let inscription_id = InscriptionId { txid, index: 0 };

    let first_parent_inscription_id = parent_ids[0];
    let hundredth_parent_inscription_id = parent_ids[99];
    let hundred_first_parent_inscription_id = parent_ids[100];
    let hundred_eleventh_parent_inscription_id = parent_ids[110];

    let parents_json = server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}"));

    assert_eq!(parents_json.ids.len(), 100);
    assert_eq!(parents_json.ids[0], first_parent_inscription_id);
    assert_eq!(parents_json.ids[99], hundredth_parent_inscription_id);
    assert!(parents_json.more);
    assert_eq!(parents_json.page_index, 0);

    let parents_json =
      server.get_json::<api::Inscriptions>(format!("/r/parents/{inscription_id}/1"));

    assert_eq!(parents_json.ids.len(), 11);
    assert_eq!(parents_json.ids[0], hundred_first_parent_inscription_id);
    assert_eq!(parents_json.ids[10], hundred_eleventh_parent_inscription_id);
    assert!(!parents_json.more);
    assert_eq!(parents_json.page_index, 1);
  }

  #[test]
  fn child_inscriptions_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_inscription_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_inscription_id}/inscriptions"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions"
    ));
    assert_eq!(child_inscriptions_json.children.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_inscription_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_inscription_id = InscriptionId { txid, index: 0 };
    let hundredth_child_inscription_id = InscriptionId { txid, index: 99 };
    let hundred_first_child_inscription_id = InscriptionId { txid, index: 100 };
    let hundred_eleventh_child_inscription_id = InscriptionId { txid, index: 110 };

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions"
    ));

    assert_eq!(child_inscriptions_json.children.len(), 100);

    assert_eq!(
      child_inscriptions_json.children[0].id,
      first_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[0].number, 1); // parent is #0, 1st child is #1

    assert_eq!(
      child_inscriptions_json.children[99].id,
      hundredth_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[99].number, -99); // all but 1st child are cursed

    assert!(child_inscriptions_json.more);
    assert_eq!(child_inscriptions_json.page, 0);

    let child_inscriptions_json = server.get_json::<api::ChildInscriptions>(format!(
      "/r/children/{parent_inscription_id}/inscriptions/1"
    ));

    assert_eq!(child_inscriptions_json.children.len(), 11);

    assert_eq!(
      child_inscriptions_json.children[0].id,
      hundred_first_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[0].number, -100);

    assert_eq!(
      child_inscriptions_json.children[10].id,
      hundred_eleventh_child_inscription_id
    );
    assert_eq!(child_inscriptions_json.children[10].number, -110);

    assert!(!child_inscriptions_json.more);
    assert_eq!(child_inscriptions_json.page, 1);
  }

  #[test]
  fn parent_inscriptions_recursive_endpoint() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.mine_blocks(1);

    let mut builder = script::Builder::new();
    for _ in 0..111 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let parents_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, witness)],
      ..default()
    });

    server.mine_blocks(1);

    let mut builder = script::Builder::new();
    builder = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("hello".into()),
      parents: (0..111)
        .map(|i| {
          InscriptionId {
            txid: parents_txid,
            index: i,
          }
          .value()
        })
        .collect(),
      unrecognized_even_field: false,
      ..default()
    }
    .append_reveal_script_to_builder(builder);

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let child_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    let child_inscription_id = InscriptionId {
      txid: child_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/parents/{child_inscription_id}/inscriptions"),
      StatusCode::NOT_FOUND,
      &format!("inscription {child_inscription_id} not found"),
    );

    server.mine_blocks(1);

    let first_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 0,
    };
    let hundredth_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 99,
    };
    let hundred_first_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 100,
    };
    let hundred_eleventh_parent_inscription_id = InscriptionId {
      txid: parents_txid,
      index: 110,
    };

    let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
      "/r/parents/{child_inscription_id}/inscriptions"
    ));

    assert_eq!(parent_inscriptions_json.parents.len(), 100);

    assert_eq!(
      parent_inscriptions_json.parents[0].id,
      first_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[0].number, 0); // parents are #0 and -1 to -110, child is #1

    assert_eq!(
      parent_inscriptions_json.parents[99].id,
      hundredth_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[99].number, -99); // all but 1st parent are cursed

    assert!(parent_inscriptions_json.more);
    assert_eq!(parent_inscriptions_json.page, 0);

    let parent_inscriptions_json = server.get_json::<api::ParentInscriptions>(format!(
      "/r/parents/{child_inscription_id}/inscriptions/1"
    ));

    assert_eq!(parent_inscriptions_json.parents.len(), 11);

    assert_eq!(
      parent_inscriptions_json.parents[0].id,
      hundred_first_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[0].number, -100);

    assert_eq!(
      parent_inscriptions_json.parents[10].id,
      hundred_eleventh_parent_inscription_id
    );
    assert_eq!(parent_inscriptions_json.parents[10].number, -110);

    assert!(!parent_inscriptions_json.more);
    assert_eq!(parent_inscriptions_json.page, 1);
  }

  #[test]
  fn inscriptions_in_block_page() {
    let server = TestServer::builder()
      .chain(Chain::Regtest)
      .index_sats()
      .build();

    for _ in 0..101 {
      server.mine_blocks(1);
    }

    for i in 0..101 {
      server.core.broadcast_tx(TransactionTemplate {
        inputs: &[(i + 1, 0, 0, inscription("text/foo", "hello").to_witness())],
        ..default()
      });
    }

    server.mine_blocks(1);

    server.assert_response_regex(
      "/inscriptions/block/102",
      StatusCode::OK,
      r".*(<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*){100}.*",
    );

    server.assert_response_regex(
      "/inscriptions/block/102/1",
      StatusCode::OK,
      r".*<a href=/inscription/[[:xdigit:]]{64}i0>.*</a>.*",
    );
  }

  #[test]
  fn inscription_query_display() {
    assert_eq!(
      query::Inscription::Id(inscription_id(1)).to_string(),
      "1111111111111111111111111111111111111111111111111111111111111111i1"
    );
    assert_eq!(query::Inscription::Number(1).to_string(), "1")
  }

  #[test]
  fn inscription_not_found() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response(
        "/inscription/0",
        StatusCode::NOT_FOUND,
        "inscription 0 not found",
      );
  }

  #[test]
  fn looking_up_inscription_by_sat_requires_sat_index() {
    TestServer::builder()
      .chain(Chain::Regtest)
      .build()
      .assert_response(
        "/inscription/abcd",
        StatusCode::NOT_FOUND,
        "sat index required",
      );
  }

  #[test]
  fn delegate() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let delegate = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, delegate.to_witness())],
      ..default()
    });

    let delegate = InscriptionId { txid, index: 0 };

    server.mine_blocks(1);

    let inscription = Inscription {
      delegate: Some(delegate.value()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response_regex(
      format!("/inscription/{id}"),
      StatusCode::OK,
      format!(
        ".*<h1>Inscription 1</h1>.*
        <dl>
          <dt>id</dt>
          <dd class=collapse>{id}</dd>
          .*
          <dt>delegate</dt>
          <dd><a href=/inscription/{delegate}>{delegate}</a></dd>
          .*
        </dl>.*"
      )
      .unindent(),
    );

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    server.assert_response(format!("/preview/{id}"), StatusCode::OK, "foo");

    assert_eq!(
      server
        .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}"))
        .delegate,
      Some(delegate)
    );
  }

  #[test]
  fn undelegated_content() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let delegate = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("foo".into()),
      ..default()
    };

    let delegate_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, delegate.to_witness())],
      ..default()
    });

    let delegate_id = InscriptionId {
      txid: delegate_txid,
      index: 0,
    };

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("bar".into()),
      delegate: Some(delegate_id.value()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(
      format!("/r/undelegated-content/{id}"),
      StatusCode::OK,
      "bar",
    );

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    // Test normal inscription without delegate
    let normal_inscription = Inscription {
      content_type: Some("text/plain".into()),
      body: Some("baz".into()),
      ..default()
    };

    let normal_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(3, 0, 0, normal_inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let normal_id = InscriptionId {
      txid: normal_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/undelegated-content/{normal_id}"),
      StatusCode::OK,
      "baz",
    );
    server.assert_response(format!("/content/{normal_id}"), StatusCode::OK, "baz");
  }

  #[test]
  fn content_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    server.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
    server_with_proxy.assert_response(format!("/content/{id}"), StatusCode::OK, "foo");
  }

  #[test]
  fn metadata_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let mut metadata = Vec::new();
    ciborium::into_writer("bar", &mut metadata).unwrap();

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      metadata: Some(metadata.clone()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    server.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    server.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );

    server_with_proxy.assert_response(
      format!("/r/metadata/{id}"),
      StatusCode::OK,
      &format!("\"{}\"", hex::encode(metadata.clone())),
    );
  }

  #[test]
  fn children_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let parent_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
      ..default()
    });

    let parent_id = InscriptionId {
      txid: parent_txid,
      index: 0,
    };

    server.assert_response(
      format!("/r/children/{parent_id}"),
      StatusCode::NOT_FOUND,
      &format!("inscription {parent_id} not found"),
    );

    server.mine_blocks(1);

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 0);

    let mut builder = script::Builder::new();
    for _ in 0..11 {
      builder = Inscription {
        content_type: Some("text/plain".into()),
        body: Some("hello".into()),
        parents: vec![parent_id.value()],
        unrecognized_even_field: false,
        ..default()
      }
      .append_reveal_script_to_builder(builder);
    }

    let witness = Witness::from_slice(&[builder.into_bytes(), Vec::new()]);

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 0, 0, witness), (2, 1, 0, Default::default())],
      ..default()
    });

    server.mine_blocks(1);

    let first_child_id = InscriptionId { txid, index: 0 };

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    let children = server.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);

    let children = server_with_proxy.get_json::<api::Children>(format!("/r/children/{parent_id}"));

    assert_eq!(children.ids.len(), 11);
    assert_eq!(first_child_id, children.ids[0]);
  }

  #[test]
  fn inscription_proxy() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    assert_eq!(
      server_with_proxy.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: Vec::new(),
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );
  }

  #[test]
  fn sat_at_index_proxy() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    let ordinal: u64 = 5000000000;

    pretty_assert_eq!(
      server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();
    let sat_indexed_server_with_proxy = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);
    sat_indexed_server_with_proxy.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    pretty_assert_eq!(
      server_with_proxy.get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );

    pretty_assert_eq!(
      sat_indexed_server_with_proxy
        .get_json::<api::SatInscription>(format!("/r/sat/{ordinal}/at/-1")),
      api::SatInscription { id: Some(id) }
    );
  }

  #[test]
  fn sat_at_index_content_proxy() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };
    let ordinal: u64 = 5000000000;

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Coin, Charm::Uncommon],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: Some(Sat(ordinal)),
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    server.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );

    let server_with_proxy = TestServer::builder()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();
    let sat_indexed_server_with_proxy = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .server_option("--proxy", server.url.as_ref())
      .build();

    server_with_proxy.mine_blocks(1);
    sat_indexed_server_with_proxy.mine_blocks(1);

    server.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
    server_with_proxy.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
    sat_indexed_server_with_proxy.assert_response(
      format!("/r/sat/{ordinal}/at/-1/content"),
      StatusCode::OK,
      "foo",
    );
  }

  #[test]
  fn block_info() {
    let server = TestServer::new();

    pretty_assert_eq!(
      server.get_json::<api::BlockInfo>("/r/blockinfo/0"),
      api::BlockInfo {
        average_fee: 0,
        average_fee_rate: 0,
        bits: 486604799,
        chainwork: [0; 32],
        confirmations: 0,
        difficulty: 0.0,
        hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
          .parse()
          .unwrap(),
        feerate_percentiles: [0, 0, 0, 0, 0],
        height: 0,
        max_fee: 0,
        max_fee_rate: 0,
        max_tx_size: 0,
        median_fee: 0,
        median_time: None,
        merkle_root: TxMerkleNode::all_zeros(),
        min_fee: 0,
        min_fee_rate: 0,
        next_block: None,
        nonce: 0,
        previous_block: None,
        subsidy: 0,
        target: "00000000ffff0000000000000000000000000000000000000000000000000000"
          .parse()
          .unwrap(),
        timestamp: 0,
        total_fee: 0,
        total_size: 0,
        total_weight: 0,
        transaction_count: 0,
        version: 1,
      },
    );

    server.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::BlockInfo>("/r/blockinfo/1"),
      api::BlockInfo {
        average_fee: 0,
        average_fee_rate: 0,
        bits: 0,
        chainwork: [0; 32],
        confirmations: 0,
        difficulty: 0.0,
        hash: "56d05060a0280d0712d113f25321158747310ece87ea9e299bde06cf385b8d85"
          .parse()
          .unwrap(),
        feerate_percentiles: [0, 0, 0, 0, 0],
        height: 1,
        max_fee: 0,
        max_fee_rate: 0,
        max_tx_size: 0,
        median_fee: 0,
        median_time: None,
        merkle_root: TxMerkleNode::all_zeros(),
        min_fee: 0,
        min_fee_rate: 0,
        next_block: None,
        nonce: 0,
        previous_block: None,
        subsidy: 0,
        target: BlockHash::all_zeros(),
        timestamp: 0,
        total_fee: 0,
        total_size: 0,
        total_weight: 0,
        transaction_count: 0,
        version: 1,
      },
    )
  }

  #[test]
  fn authentication_requires_username_and_password() {
    assert!(Arguments::try_parse_from(["ord", "--server-username", "server", "foo"]).is_err());
    assert!(Arguments::try_parse_from(["ord", "--server-password", "server", "bar"]).is_err());
    assert!(Arguments::try_parse_from([
      "ord",
      "--server-username",
      "foo",
      "--server-password",
      "bar",
      "server"
    ])
    .is_ok());
  }

  #[test]
  fn inscriptions_can_be_hidden_with_config() {
    let core = mockcore::builder()
      .network(Chain::Regtest.network())
      .build();

    core.mine_blocks(1);

    let txid = core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription("text/foo", "hello").to_witness())],
      ..default()
    });

    core.mine_blocks(1);

    let inscription = InscriptionId { txid, index: 0 };

    let server = TestServer::builder()
      .core(core)
      .config(&format!("hidden: [{inscription}]"))
      .build();

    server.assert_response_regex(format!("/inscription/{inscription}"), StatusCode::OK, ".*");

    server.assert_response_regex(
      format!("/content/{inscription}"),
      StatusCode::OK,
      PreviewUnknownHtml.to_string(),
    );
  }

  #[test]
  fn update_endpoint_is_not_available_when_not_in_integration_test_mode() {
    let server = TestServer::builder().build();
    server.assert_response("/update", StatusCode::NOT_FOUND, "");
  }

  #[test]
  fn burned_charm() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      outputs: 0,
      op_return_index: Some(0),
      op_return_value: Some(50 * COIN_VALUE),
      op_return: Some(
        script::Builder::new()
          .push_opcode(opcodes::all::OP_RETURN)
          .into_script(),
      ),
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId { txid, index: 0 };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Burned],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint { txid, vout: 0 },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint { txid, vout: 0 },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: None
      }
    );
  }

  #[test]
  fn burned_charm_on_transfer() {
    let server = TestServer::builder().chain(Chain::Regtest).build();

    server.mine_blocks(1);

    let inscription = Inscription {
      content_type: Some("text/html".into()),
      body: Some("foo".into()),
      ..default()
    };

    let create_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(1, 0, 0, inscription.to_witness())],
      outputs: 1,
      ..default()
    });

    server.mine_blocks(1);

    let id = InscriptionId {
      txid: create_txid,
      index: 0,
    };

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint {
          txid: create_txid,
          vout: 0
        },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint {
            txid: create_txid,
            vout: 0
          },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: Some("bcrt1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqdku202".to_string())
      }
    );

    let transfer_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(2, 1, 0, Default::default())],
      fee: 0,
      outputs: 0,
      op_return_index: Some(0),
      op_return_value: Some(50 * COIN_VALUE),
      op_return: Some(
        script::Builder::new()
          .push_opcode(opcodes::all::OP_RETURN)
          .into_script(),
      ),
      ..default()
    });

    server.mine_blocks(1);

    pretty_assert_eq!(
      server.get_json::<api::InscriptionRecursive>(format!("/r/inscription/{id}")),
      api::InscriptionRecursive {
        charms: vec![Charm::Burned],
        content_type: Some("text/html".into()),
        content_length: Some(3),
        delegate: None,
        fee: 0,
        height: 2,
        id,
        number: 0,
        output: OutPoint {
          txid: transfer_txid,
          vout: 0
        },
        sat: None,
        satpoint: SatPoint {
          outpoint: OutPoint {
            txid: transfer_txid,
            vout: 0
          },
          offset: 0
        },
        timestamp: 2,
        value: Some(50 * COIN_VALUE),
        address: None
      }
    );
  }

  #[test]
  fn unknown_output_returns_404() {
    let server = TestServer::builder().chain(Chain::Regtest).build();
    server.assert_response(
      "/output/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123",
      StatusCode::NOT_FOUND,
      "output 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef:123 not found",
    );
  }

  #[test]
  fn satscard_form_redirects_to_query() {
    TestServer::new().assert_redirect(
      &format!(
        "/satscard?url={}",
        urlencoding::encode(satscard::tests::URL)
      ),
      &format!("/satscard?{}", satscard::tests::query_parameters()),
    );
  }

  #[test]
  fn satscard_missing_form_query_is_error() {
    TestServer::new().assert_response(
      "/satscard?url=https://foo.com",
      StatusCode::BAD_REQUEST,
      "satscard URL missing fragment",
    );
  }

  #[test]
  fn satscard_invalid_query_parameters() {
    TestServer::new().assert_response(
      "/satscard?foo=bar",
      StatusCode::BAD_REQUEST,
      "invalid satscard query parameters: unknown key `foo`",
    );
  }

  #[test]
  fn satscard_empty_query_parameters_are_allowed() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .build()
      .assert_html("/satscard?", SatscardHtml { satscard: None });
  }

  #[test]
  fn satscard_display_without_address_index() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .build()
      .assert_html(
        format!("/satscard?{}", satscard::tests::query_parameters()),
        SatscardHtml {
          satscard: Some((satscard::tests::satscard(), None)),
        },
      );
  }

  #[test]
  fn satscard_display_with_address_index_empty() {
    TestServer::builder()
      .chain(Chain::Mainnet)
      .index_addresses()
      .build()
      .assert_html(
        format!("/satscard?{}", satscard::tests::query_parameters()),
        SatscardHtml {
          satscard: Some((
            satscard::tests::satscard(),
            Some(AddressHtml {
              address: satscard::tests::address(),
              header: false,
              inscriptions: Some(Vec::new()),
              outputs: Vec::new(),
              runes_balances: None,
              sat_balance: 0,
            }),
          )),
        },
      );
  }

  #[test]
  fn satscard_address_recovery_fails_on_wrong_chain() {
    TestServer::builder()
      .chain(Chain::Testnet)
      .build()
      .assert_response(
        format!("/satscard?{}", satscard::tests::query_parameters()),
        StatusCode::BAD_REQUEST,
        "invalid satscard query parameters: address recovery failed",
      );
  }

  #[test]
  fn sat_inscription_at_index_content_endpoint() {
    let server = TestServer::builder()
      .index_sats()
      .chain(Chain::Regtest)
      .build();

    server.mine_blocks(1);

    let first_txid = server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        1,
        0,
        0,
        inscription("text/plain;charset=utf-8", "foo").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    let first_inscription_id = InscriptionId {
      txid: first_txid,
      index: 0,
    };

    let first_inscription = server
      .get_json::<api::InscriptionRecursive>(format!("/r/inscription/{first_inscription_id}"));

    let sat = first_inscription.sat.unwrap();

    server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");

    server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "foo");

    server.core.broadcast_tx(TransactionTemplate {
      inputs: &[(
        2,
        1,
        first_inscription.satpoint.outpoint.vout.try_into().unwrap(),
        inscription("text/plain;charset=utf-8", "bar").to_witness(),
      )],
      ..default()
    });

    server.mine_blocks(1);

    server.assert_response(format!("/r/sat/{sat}/at/0/content"), StatusCode::OK, "foo");

    server.assert_response(format!("/r/sat/{sat}/at/1/content"), StatusCode::OK, "bar");

    server.assert_response(format!("/r/sat/{sat}/at/-1/content"), StatusCode::OK, "bar");

    server.assert_response(
      "/r/sat/0/at/0/content",
      StatusCode::NOT_FOUND,
      "inscription on sat 0 not found",
    );

    let server = TestServer::new();

    server.assert_response(
      "/r/sat/0/at/0/content",
      StatusCode::NOT_FOUND,
      "this server has no sat index",
    );
  }
}

ord/src/subcommand/server/accept_encoding.rs


use {super::*, axum::extract::FromRef};

#[derive(Default, Debug)]
pub(crate) struct AcceptEncoding(pub(crate) Option<String>);

impl<S> axum::extract::FromRequestParts<S> for AcceptEncoding
where
  Arc<ServerConfig>: FromRef<S>,
  S: Send + Sync,
{
  type Rejection = (StatusCode, &'static str);

  async fn from_request_parts(
    parts: &mut http::request::Parts,
    _state: &S,
  ) -> Result<Self, Self::Rejection> {
    Ok(Self(
      parts
        .headers
        .get("accept-encoding")
        .map(|value| value.to_str().unwrap_or_default().to_owned()),
    ))
  }
}

impl AcceptEncoding {
  pub(crate) fn is_acceptable(&self, encoding: &HeaderValue) -> bool {
    let Ok(encoding) = encoding.to_str() else {
      return false;
    };

    self
      .0
      .clone()
      .unwrap_or_default()
      .split(',')
      .any(|value| value.split(';').next().unwrap_or_default().trim() == encoding)
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    axum::{extract::FromRequestParts, http::Request},
    http::header::ACCEPT_ENCODING,
  };

  #[tokio::test]
  async fn single_encoding() {
    let req = Request::builder()
      .header(ACCEPT_ENCODING, "gzip")
      .body(())
      .unwrap();

    let encodings = AcceptEncoding::from_request_parts(
      &mut req.into_parts().0,
      &Arc::new(ServerConfig {
        json_api_enabled: false,
        decompress: false,
        ..default()
      }),
    )
    .await
    .unwrap();

    assert_eq!(encodings.0, Some("gzip".to_string()));
  }

  #[tokio::test]
  async fn accepts_encoding_with_qvalues() {
    let req = Request::builder()
      .header(ACCEPT_ENCODING, "deflate;q=0.5, gzip;q=1.0, br;q=0.8")
      .body(())
      .unwrap();

    let encodings = AcceptEncoding::from_request_parts(
      &mut req.into_parts().0,
      &Arc::new(ServerConfig {
        json_api_enabled: false,
        decompress: false,
        ..default()
      }),
    )
    .await
    .unwrap();

    assert_eq!(
      encodings.0,
      Some("deflate;q=0.5, gzip;q=1.0, br;q=0.8".to_string())
    );

    assert!(encodings.is_acceptable(&HeaderValue::from_static("deflate")));
    assert!(encodings.is_acceptable(&HeaderValue::from_static("gzip")));
    assert!(encodings.is_acceptable(&HeaderValue::from_static("br")));
    assert!(!encodings.is_acceptable(&HeaderValue::from_static("bzip2")));
  }

  #[tokio::test]
  async fn accepts_encoding_without_qvalues() {
    let req = Request::builder()
      .header(ACCEPT_ENCODING, "gzip, deflate, br")
      .body(())
      .unwrap();

    let encodings = AcceptEncoding::from_request_parts(
      &mut req.into_parts().0,
      &Arc::new(ServerConfig {
        json_api_enabled: false,
        decompress: false,
        ..default()
      }),
    )
    .await
    .unwrap();

    assert_eq!(encodings.0, Some("gzip, deflate, br".to_string()));

    assert!(encodings.is_acceptable(&HeaderValue::from_static("deflate")));
    assert!(encodings.is_acceptable(&HeaderValue::from_static("gzip")));
    assert!(encodings.is_acceptable(&HeaderValue::from_static("br")));
    assert!(!encodings.is_acceptable(&HeaderValue::from_static("bzip2")));
  }
}

ord/src/subcommand/server/accept_json.rs


use {super::*, axum::extract::FromRef};

pub(crate) struct AcceptJson(pub(crate) bool);

impl<S> axum::extract::FromRequestParts<S> for AcceptJson
where
  Arc<ServerConfig>: FromRef<S>,
  S: Send + Sync,
{
  type Rejection = (StatusCode, &'static str);

  async fn from_request_parts(
    parts: &mut http::request::Parts,
    state: &S,
  ) -> Result<Self, Self::Rejection> {
    let state = Arc::from_ref(state);
    let json_api_enabled = state.json_api_enabled;
    let json_header = parts
      .headers
      .get("accept")
      .map(|value| value == "application/json")
      .unwrap_or_default();
    if json_header && json_api_enabled {
      Ok(Self(true))
    } else if json_header && !json_api_enabled {
      Err((StatusCode::NOT_ACCEPTABLE, "JSON API disabled"))
    } else {
      Ok(Self(false))
    }
  }
}

ord/src/subcommand/server/error.rs


use {super::*, std::fmt::Write};

#[derive(Debug)]
pub(super) enum ServerError {
  BadRequest(String),
  Internal(Error),
  NotAcceptable {
    accept_encoding: AcceptEncoding,
    content_encoding: HeaderValue,
  },
  NotFound(String),
}

pub(super) type ServerResult<T = Response> = Result<T, ServerError>;

impl IntoResponse for ServerError {
  fn into_response(self) -> Response {
    match self {
      Self::BadRequest(message) => (StatusCode::BAD_REQUEST, message).into_response(),
      Self::Internal(error) => {
        eprintln!("error serving request: {error}");
        (
          StatusCode::INTERNAL_SERVER_ERROR,
          StatusCode::INTERNAL_SERVER_ERROR
            .canonical_reason()
            .unwrap_or_default(),
        )
          .into_response()
      }
      Self::NotAcceptable {
        accept_encoding,
        content_encoding,
      } => {
        let mut message = format!(
          "inscription content encoding `{}` is not acceptable.",
          String::from_utf8_lossy(content_encoding.as_bytes())
        );

        if let Some(accept_encoding) = accept_encoding.0 {
          write!(message, " `Accept-Encoding` header: `{accept_encoding}`").unwrap();
        } else {
          write!(message, " `Accept-Encoding` header not present").unwrap();
        };

        (StatusCode::NOT_ACCEPTABLE, message).into_response()
      }
      Self::NotFound(message) => (
        StatusCode::NOT_FOUND,
        [(header::CACHE_CONTROL, HeaderValue::from_static("no-store"))],
        message,
      )
        .into_response(),
    }
  }
}

pub(super) trait OptionExt<T> {
  fn ok_or_not_found<F: FnOnce() -> S, S: Into<String>>(self, f: F) -> ServerResult<T>;
}

impl<T> OptionExt<T> for Option<T> {
  fn ok_or_not_found<F: FnOnce() -> S, S: Into<String>>(self, f: F) -> ServerResult<T> {
    match self {
      Some(value) => Ok(value),
      None => Err(ServerError::NotFound(f().into() + " not found")),
    }
  }
}

impl From<Error> for ServerError {
  fn from(error: Error) -> Self {
    Self::Internal(error)
  }
}

ord/src/subcommand/server/query.rs


use super::*;

pub(super) enum Block {
  Height(u32),
  Hash(BlockHash),
}

impl FromStr for Block {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    Ok(if s.len() == 64 {
      Self::Hash(s.parse()?)
    } else {
      Self::Height(s.parse()?)
    })
  }
}

#[derive(Copy, Clone, Debug)]
pub(crate) enum Inscription {
  Id(InscriptionId),
  Number(i32),
  Sat(Sat),
}

impl FromStr for Inscription {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    if re::INSCRIPTION_ID.is_match(s) {
      Ok(Self::Id(s.parse()?))
    } else if re::INSCRIPTION_NUMBER.is_match(s) {
      Ok(Self::Number(s.parse()?))
    } else if re::SAT_NAME.is_match(s) {
      Ok(Self::Sat(s.parse()?))
    } else {
      Err(anyhow!("bad inscription query {s}"))
    }
  }
}

impl Display for Inscription {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::Id(id) => write!(f, "{id}"),
      Self::Number(number) => write!(f, "{number}"),
      Self::Sat(sat) => write!(f, "on sat {}", sat.name()),
    }
  }
}

#[derive(Debug)]
pub(super) enum Rune {
  Spaced(SpacedRune),
  Id(RuneId),
  Number(u64),
}

impl FromStr for Rune {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    if s.contains(':') {
      Ok(Self::Id(s.parse()?))
    } else if re::RUNE_NUMBER.is_match(s) {
      Ok(Self::Number(s.parse()?))
    } else {
      Ok(Self::Spaced(s.parse()?))
    }
  }
}

ord/src/subcommand/server/r.rs


use super::*;

pub(super) async fn blockhash(
  Extension(index): Extension<Arc<Index>>,
) -> ServerResult<Json<String>> {
  task::block_in_place(|| {
    Ok(Json(
      index
        .block_hash(None)?
        .ok_or_not_found(|| "blockhash")?
        .to_string(),
    ))
  })
}

pub(super) async fn blockhash_at_height(
  Extension(index): Extension<Arc<Index>>,
  Path(height): Path<u32>,
) -> ServerResult<Json<String>> {
  task::block_in_place(|| {
    Ok(Json(
      index
        .block_hash(Some(height))?
        .ok_or_not_found(|| "blockhash")?
        .to_string(),
    ))
  })
}

pub(super) async fn block_hash_from_height_string(
  Extension(index): Extension<Arc<Index>>,
  Path(height): Path<u32>,
) -> ServerResult<String> {
  task::block_in_place(|| {
    Ok(
      index
        .block_hash(Some(height))?
        .ok_or_not_found(|| "blockhash")?
        .to_string(),
    )
  })
}

pub(super) async fn blockhash_string(
  Extension(index): Extension<Arc<Index>>,
) -> ServerResult<String> {
  task::block_in_place(|| {
    Ok(
      index
        .block_hash(None)?
        .ok_or_not_found(|| "blockhash")?
        .to_string(),
    )
  })
}

pub(super) async fn blockheight_string(
  Extension(index): Extension<Arc<Index>>,
) -> ServerResult<String> {
  task::block_in_place(|| {
    Ok(
      index
        .block_height()?
        .ok_or_not_found(|| "blockheight")?
        .to_string(),
    )
  })
}

pub(super) async fn blockinfo(
  Extension(index): Extension<Arc<Index>>,
  Path(DeserializeFromStr(query)): Path<DeserializeFromStr<query::Block>>,
) -> ServerResult<Json<api::BlockInfo>> {
  task::block_in_place(|| {
    let hash = match query {
      query::Block::Hash(hash) => hash,
      query::Block::Height(height) => index
        .block_hash(Some(height))?
        .ok_or_not_found(|| format!("block {height}"))?,
    };

    let header = index
      .block_header(hash)?
      .ok_or_not_found(|| format!("block {hash}"))?;

    let info = index
      .block_header_info(hash)?
      .ok_or_not_found(|| format!("block {hash}"))?;

    let stats = index
      .block_stats(info.height.try_into().unwrap())?
      .ok_or_not_found(|| format!("block {hash}"))?;

    Ok(Json(api::BlockInfo {
      average_fee: stats.avg_fee.to_sat(),
      average_fee_rate: stats.avg_fee_rate.to_sat(),
      bits: header.bits.to_consensus(),
      chainwork: info.chainwork.try_into().unwrap(),
      confirmations: info.confirmations,
      difficulty: info.difficulty,
      hash,
      feerate_percentiles: [
        stats.fee_rate_percentiles.fr_10th.to_sat(),
        stats.fee_rate_percentiles.fr_25th.to_sat(),
        stats.fee_rate_percentiles.fr_50th.to_sat(),
        stats.fee_rate_percentiles.fr_75th.to_sat(),
        stats.fee_rate_percentiles.fr_90th.to_sat(),
      ],
      height: info.height.try_into().unwrap(),
      max_fee: stats.max_fee.to_sat(),
      max_fee_rate: stats.max_fee_rate.to_sat(),
      max_tx_size: stats.max_tx_size,
      median_fee: stats.median_fee.to_sat(),
      median_time: info
        .median_time
        .map(|median_time| median_time.try_into().unwrap()),
      merkle_root: info.merkle_root,
      min_fee: stats.min_fee.to_sat(),
      min_fee_rate: stats.min_fee_rate.to_sat(),
      next_block: info.next_block_hash,
      nonce: info.nonce,
      previous_block: info.previous_block_hash,
      subsidy: stats.subsidy.to_sat(),
      target: target_as_block_hash(header.target()),
      timestamp: info.time.try_into().unwrap(),
      total_fee: stats.total_fee.to_sat(),
      total_size: stats.total_size,
      total_weight: stats.total_weight,
      transaction_count: info.n_tx.try_into().unwrap(),
      #[allow(clippy::cast_sign_loss)]
      version: info.version.to_consensus() as u32,
    }))
  })
}

pub(super) async fn blocktime_string(
  Extension(index): Extension<Arc<Index>>,
) -> ServerResult<String> {
  task::block_in_place(|| {
    Ok(
      index
        .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)?
        .unix_timestamp()
        .to_string(),
    )
  })
}

pub(super) async fn children(
  Extension(index): Extension<Arc<Index>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  children_paginated(Extension(index), Path((inscription_id, 0))).await
}

pub(super) async fn children_inscriptions(
  Extension(index): Extension<Arc<Index>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  children_inscriptions_paginated(Extension(index), Path((inscription_id, 0))).await
}

pub(super) async fn children_inscriptions_paginated(
  Extension(index): Extension<Arc<Index>>,
  Path((parent, page)): Path<(InscriptionId, usize)>,
) -> ServerResult {
  task::block_in_place(|| {
    let parent_sequence_number = index
      .get_inscription_entry(parent)?
      .ok_or_not_found(|| format!("inscription {parent}"))?
      .sequence_number;

    let (ids, more) =
      index.get_children_by_sequence_number_paginated(parent_sequence_number, 100, page)?;

    let children = ids
      .into_iter()
      .map(|inscription_id| get_relative_inscription(&index, inscription_id))
      .collect::<ServerResult<Vec<api::RelativeInscriptionRecursive>>>()?;

    Ok(
      Json(api::ChildInscriptions {
        children,
        more,
        page,
      })
      .into_response(),
    )
  })
}

pub(super) async fn children_paginated(
  Extension(index): Extension<Arc<Index>>,
  Path((parent, page)): Path<(InscriptionId, usize)>,
) -> ServerResult {
  task::block_in_place(|| {
    let Some(parent) = index.get_inscription_entry(parent)? else {
      return Err(ServerError::NotFound(format!(
        "inscription {} not found",
        parent
      )));
    };

    let parent_sequence_number = parent.sequence_number;

    let (ids, more) =
      index.get_children_by_sequence_number_paginated(parent_sequence_number, 100, page)?;

    Ok(Json(api::Children { ids, more, page }).into_response())
  })
}

pub(super) async fn content(
  Extension(index): Extension<Arc<Index>>,
  Extension(settings): Extension<Arc<Settings>>,
  Extension(server_config): Extension<Arc<ServerConfig>>,
  Path(inscription_id): Path<InscriptionId>,
  accept_encoding: AcceptEncoding,
) -> ServerResult {
  task::block_in_place(|| {
    if settings.is_hidden(inscription_id) {
      return Ok(PreviewUnknownHtml.into_response());
    }

    let Some(mut inscription) = index.get_inscription_by_id(inscription_id)? else {
      return Err(ServerError::NotFound(format!(
        "inscription {inscription_id} not found"
      )));
    };

    if let Some(delegate) = inscription.delegate() {
      inscription = index
        .get_inscription_by_id(delegate)?
        .ok_or_not_found(|| format!("delegate {inscription_id}"))?
    }

    Ok(
      content_response(inscription, accept_encoding, &server_config)?
        .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
        .into_response(),
    )
  })
}

pub(super) fn content_response(
  inscription: Inscription,
  accept_encoding: AcceptEncoding,
  server_config: &ServerConfig,
) -> ServerResult<Option<(HeaderMap, Vec<u8>)>> {
  let mut headers = HeaderMap::new();

  match &server_config.csp_origin {
    None => {
      headers.insert(
        header::CONTENT_SECURITY_POLICY,
        HeaderValue::from_static("default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:"),
      );
      headers.append(
          header::CONTENT_SECURITY_POLICY,
          HeaderValue::from_static("default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:"),
        );
    }
    Some(origin) => {
      let csp = format!("default-src {origin}/content/ {origin}/blockheight {origin}/blockhash {origin}/blockhash/ {origin}/blocktime {origin}/r/ 'unsafe-eval' 'unsafe-inline' data: blob:");
      headers.insert(
        header::CONTENT_SECURITY_POLICY,
        HeaderValue::from_str(&csp).map_err(|err| ServerError::Internal(Error::from(err)))?,
      );
    }
  }

  headers.insert(
    header::CACHE_CONTROL,
    HeaderValue::from_static("public, max-age=1209600, immutable"),
  );

  headers.insert(
    header::CONTENT_TYPE,
    inscription
      .content_type()
      .and_then(|content_type| content_type.parse().ok())
      .unwrap_or(HeaderValue::from_static("application/octet-stream")),
  );

  if let Some(content_encoding) = inscription.content_encoding() {
    if accept_encoding.is_acceptable(&content_encoding) {
      headers.insert(header::CONTENT_ENCODING, content_encoding);
    } else if server_config.decompress && content_encoding == "br" {
      let Some(body) = inscription.into_body() else {
        return Ok(None);
      };

      let mut decompressed = Vec::new();

      Decompressor::new(body.as_slice(), 4096)
        .read_to_end(&mut decompressed)
        .map_err(|err| ServerError::Internal(err.into()))?;

      return Ok(Some((headers, decompressed)));
    } else {
      return Err(ServerError::NotAcceptable {
        accept_encoding,
        content_encoding,
      });
    }
  }

  let Some(body) = inscription.into_body() else {
    return Ok(None);
  };

  Ok(Some((headers, body)))
}

pub(super) async fn inscription(
  Extension(index): Extension<Arc<Index>>,
  Extension(server_config): Extension<Arc<ServerConfig>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  task::block_in_place(|| {
    let Some(inscription) = index.get_inscription_by_id(inscription_id)? else {
      return Err(ServerError::NotFound(format!(
        "inscription {} not found",
        inscription_id
      )));
    };

    let entry = index
      .get_inscription_entry(inscription_id)
      .unwrap()
      .unwrap();

    let satpoint = index
      .get_inscription_satpoint_by_id(inscription_id)
      .ok()
      .flatten()
      .unwrap();

    let output = if satpoint.outpoint == unbound_outpoint() {
      None
    } else {
      Some(
        index
          .get_transaction(satpoint.outpoint.txid)?
          .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))?
          .output
          .into_iter()
          .nth(satpoint.outpoint.vout.try_into().unwrap())
          .ok_or_not_found(|| format!("inscription {inscription_id} current transaction output"))?,
      )
    };

    let address = output.as_ref().and_then(|output| {
      server_config
        .chain
        .address_from_script(&output.script_pubkey)
        .ok()
        .map(|address| address.to_string())
    });

    Ok(
      Json(api::InscriptionRecursive {
        charms: Charm::charms(entry.charms),
        content_type: inscription.content_type().map(|s| s.to_string()),
        content_length: inscription.content_length(),
        delegate: inscription.delegate(),
        fee: entry.fee,
        height: entry.height,
        id: inscription_id,
        number: entry.inscription_number,
        output: satpoint.outpoint,
        value: output.as_ref().map(|o| o.value.to_sat()),
        sat: entry.sat,
        satpoint,
        timestamp: timestamp(entry.timestamp.into()).timestamp(),
        address,
      })
      .into_response(),
    )
  })
}

pub(super) async fn metadata(
  Extension(index): Extension<Arc<Index>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  task::block_in_place(|| {
    let Some(inscription) = index.get_inscription_by_id(inscription_id)? else {
      return Err(ServerError::NotFound(format!(
        "inscription {} not found",
        inscription_id
      )));
    };

    let metadata = inscription
      .metadata
      .ok_or_not_found(|| format!("inscription {inscription_id} metadata"))?;

    Ok(Json(hex::encode(metadata)).into_response())
  })
}

pub(super) async fn parents(
  Extension(index): Extension<Arc<Index>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  parents_paginated(Extension(index), Path((inscription_id, 0))).await
}

pub async fn parent_inscriptions(
  Extension(index): Extension<Arc<Index>>,
  Path(inscription_id): Path<InscriptionId>,
) -> ServerResult {
  parent_inscriptions_paginated(Extension(index), Path((inscription_id, 0))).await
}

pub async fn parent_inscriptions_paginated(
  Extension(index): Extension<Arc<Index>>,
  Path((child, page)): Path<(InscriptionId, usize)>,
) -> ServerResult {
  task::block_in_place(|| {
    let entry = index
      .get_inscription_entry(child)?
      .ok_or_not_found(|| format!("inscription {child}"))?;

    let (ids, more) = index.get_parents_by_sequence_number_paginated(entry.parents, 100, page)?;

    let parents = ids
      .into_iter()
      .map(|inscription_id| get_relative_inscription(&index, inscription_id))
      .collect::<ServerResult<Vec<api::RelativeInscriptionRecursive>>>()?;

    Ok(
      Json(api::ParentInscriptions {
        parents,
        more,
        page,
      })
      .into_response(),
    )
  })
}

pub(super) async fn parents_paginated(
  Extension(index): Extension<Arc<Index>>,
  Path((inscription_id, page)): Path<(InscriptionId, usize)>,
) -> ServerResult {
  task::block_in_place(|| {
    let child = index
      .get_inscription_entry(inscription_id)?
      .ok_or_not_found(|| format!("inscription {inscription_id}"))?;

    let (ids, more) = index.get_parents_by_sequence_number_paginated(child.parents, 100, page)?;

    let page_index =
      u32::try_from(page).map_err(|_| anyhow!("page index {} out of range", page))?;

    Ok(
      Json(api::Inscriptions {
        ids,
        more,
        page_index,
      })
      .into_response(),
    )
  })
}

pub(super) async fn sat(
  Extension(index): Extension<Arc<Index>>,
  Path(sat): Path<u64>,
) -> ServerResult<Json<api::SatInscriptions>> {
  sat_paginated(Extension(index), Path((sat, 0))).await
}

pub(super) async fn sat_at_index(
  Extension(index): Extension<Arc<Index>>,
  Path((DeserializeFromStr(sat), inscription_index)): Path<(DeserializeFromStr<Sat>, isize)>,
) -> ServerResult<Json<api::SatInscription>> {
  task::block_in_place(|| {
    if !index.has_sat_index() {
      return Err(ServerError::NotFound(
        "this server has no sat index".to_string(),
      ));
    }

    let id = index.get_inscription_id_by_sat_indexed(sat, inscription_index)?;

    Ok(Json(api::SatInscription { id }))
  })
}

pub(super) async fn sat_paginated(
  Extension(index): Extension<Arc<Index>>,
  Path((sat, page)): Path<(u64, u64)>,
) -> ServerResult<Json<api::SatInscriptions>> {
  task::block_in_place(|| {
    if !index.has_sat_index() {
      return Err(ServerError::NotFound("this server has no sat index".into()));
    }

    let (ids, more) = index.get_inscription_ids_by_sat_paginated(Sat(sat), 100, page)?;

    Ok(Json(api::SatInscriptions { ids, more, page }))
  })
}

pub(super) async fn sat_at_index_content(
  index: Extension<Arc<Index>>,
  settings: Extension<Arc<Settings>>,
  server_config: Extension<Arc<ServerConfig>>,
  Path((DeserializeFromStr(sat), inscription_index)): Path<(DeserializeFromStr<Sat>, isize)>,
  accept_encoding: AcceptEncoding,
) -> ServerResult {
  let inscription_id = task::block_in_place(|| {
    if !index.has_sat_index() {
      return Err(ServerError::NotFound("this server has no sat index".into()));
    }

    index
      .get_inscription_id_by_sat_indexed(sat, inscription_index)?
      .ok_or_not_found(|| format!("inscription on sat {sat}"))
  })?;

  content(
    index,
    settings,
    server_config,
    Path(inscription_id),
    accept_encoding,
  )
  .await
}

fn get_relative_inscription(
  index: &Index,
  id: InscriptionId,
) -> ServerResult<api::RelativeInscriptionRecursive> {
  let entry = index
    .get_inscription_entry(id)?
    .ok_or_not_found(|| format!("inscription {id}"))?;

  let satpoint = index
    .get_inscription_satpoint_by_id(id)?
    .ok_or_not_found(|| format!("satpoint for inscription {id}"))?;

  Ok(api::RelativeInscriptionRecursive {
    charms: Charm::charms(entry.charms),
    fee: entry.fee,
    height: entry.height,
    id,
    number: entry.inscription_number,
    output: satpoint.outpoint,
    sat: entry.sat,
    satpoint,
    timestamp: timestamp(entry.timestamp.into()).timestamp(),
  })
}

pub(super) async fn tx(
  Extension(index): Extension<Arc<Index>>,
  Path(txid): Path<Txid>,
) -> ServerResult<Json<String>> {
  task::block_in_place(|| {
    Ok(Json(
      index
        .get_transaction_hex_recursive(txid)?
        .ok_or_not_found(|| format!("transaction {txid}"))?,
    ))
  })
}

pub(super) async fn undelegated_content(
  Extension(index): Extension<Arc<Index>>,
  Extension(settings): Extension<Arc<Settings>>,
  Extension(server_config): Extension<Arc<ServerConfig>>,
  Path(inscription_id): Path<InscriptionId>,
  accept_encoding: AcceptEncoding,
) -> ServerResult {
  task::block_in_place(|| {
    if settings.is_hidden(inscription_id) {
      return Ok(PreviewUnknownHtml.into_response());
    }

    let inscription = index
      .get_inscription_by_id(inscription_id)?
      .ok_or_not_found(|| format!("inscription {inscription_id}"))?;

    Ok(
      r::content_response(inscription, accept_encoding, &server_config)?
        .ok_or_not_found(|| format!("inscription {inscription_id} content"))?
        .into_response(),
    )
  })
}

pub(super) async fn utxo(
  Extension(index): Extension<Arc<Index>>,
  Path(outpoint): Path<OutPoint>,
) -> ServerResult {
  task::block_in_place(|| {
    Ok(
      Json(
        index
          .get_utxo_recursive(outpoint)?
          .ok_or_not_found(|| format!("output {outpoint}"))?,
      )
      .into_response(),
    )
  })
}

ord/src/subcommand/server/server_config.rs


use super::*;

#[derive(Default)]
pub struct ServerConfig {
  pub chain: Chain,
  pub csp_origin: Option<String>,
  pub decompress: bool,
  pub domain: Option<String>,
  pub index_sats: bool,
  pub json_api_enabled: bool,
  pub proxy: Option<Url>,
}

impl ServerConfig {
  pub(super) fn preview_content_security_policy(
    &self,
    media: Media,
  ) -> ServerResult<[(HeaderName, HeaderValue); 1]> {
    let default = match media {
      Media::Audio => "default-src 'self'",
      Media::Code(_) => "script-src-elem 'self' https://cdn.jsdelivr.net",
      Media::Font => "script-src-elem 'self'; style-src 'self' 'unsafe-inline'",
      Media::Iframe => {
        return Err(
          anyhow!("preview_content_security_policy cannot be called with Media::Iframe").into(),
        )
      }
      Media::Image(_) => "default-src 'self' 'unsafe-inline'",
      Media::Markdown => "script-src-elem 'self' https://cdn.jsdelivr.net",
      Media::Model => "script-src-elem 'self' https://ajax.googleapis.com",
      Media::Pdf => "script-src-elem 'self' https://cdn.jsdelivr.net",
      Media::Text => "default-src 'self'",
      Media::Unknown => "default-src 'self'",
      Media::Video => "default-src 'self'",
    };

    let value = if let Some(csp_origin) = &self.csp_origin {
      default
        .replace("'self'", csp_origin)
        .parse()
        .map_err(|err| anyhow!("invalid content-security-policy origin `{csp_origin}`: {err}"))?
    } else {
      HeaderValue::from_static(default)
    };

    Ok([(header::CONTENT_SECURITY_POLICY, value)])
  }
}

ord/src/subcommand/settings.rs


use super::*;

pub(crate) fn run(settings: Settings) -> SubcommandResult {
  Ok(Some(Box::new(settings)))
}

ord/src/subcommand/subsidy.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Subsidy {
  #[arg(help = "List sats in subsidy at <HEIGHT>.")]
  height: Height,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub first: u64,
  pub subsidy: u64,
  pub name: String,
}

impl Subsidy {
  pub(crate) fn run(self) -> SubcommandResult {
    let first = self.height.starting_sat();

    let subsidy = self.height.subsidy();

    if subsidy == 0 {
      bail!("block {} has no subsidy", self.height);
    }

    Ok(Some(Box::new(Output {
      first: first.0,
      subsidy,
      name: first.name(),
    })))
  }
}

ord/src/subcommand/supply.rs


use super::*;

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub supply: u64,
  pub first: u64,
  pub last: u64,
  pub last_mined_in_block: u32,
}

pub(crate) fn run() -> SubcommandResult {
  let mut last = 0;

  loop {
    if Height(last + 1).subsidy() == 0 {
      break;
    }
    last += 1;
  }

  Ok(Some(Box::new(Output {
    supply: Sat::SUPPLY,
    first: 0,
    last: Sat::SUPPLY - 1,
    last_mined_in_block: last,
  })))
}

ord/src/subcommand/teleburn.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Teleburn {
  #[arg(help = "Generate teleburn addresses for inscription <DESTINATION>.")]
  destination: InscriptionId,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub ethereum: crate::teleburn::Ethereum,
}

impl Teleburn {
  pub(crate) fn run(self) -> SubcommandResult {
    Ok(Some(Box::new(Output {
      ethereum: self.destination.into(),
    })))
  }
}

ord/src/subcommand/traits.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Traits {
  #[arg(help = "Show traits for <SAT>.")]
  sat: Sat,
}

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub number: u64,
  pub decimal: String,
  pub degree: String,
  pub name: String,
  pub height: u32,
  pub cycle: u32,
  pub epoch: u32,
  pub period: u32,
  pub offset: u64,
  pub rarity: Rarity,
}

impl Traits {
  pub(crate) fn run(self) -> SubcommandResult {
    Ok(Some(Box::new(Output {
      number: self.sat.n(),
      decimal: self.sat.decimal().to_string(),
      degree: self.sat.degree().to_string(),
      name: self.sat.name(),
      height: self.sat.height().0,
      cycle: self.sat.cycle(),
      epoch: self.sat.epoch().0,
      period: self.sat.period(),
      offset: self.sat.third(),
      rarity: self.sat.rarity(),
    })))
  }
}

ord/src/subcommand/verify.rs


use {
  super::*,
  base64::{engine::general_purpose, Engine},
};

#[derive(Debug, Parser)]
#[clap(
group(
  ArgGroup::new("input")
    .required(true)
    .args(&["text", "file"])),
group(
  ArgGroup::new("signature")
    .required(true)
    .args(&["transaction", "witness"]))
)]
pub(crate) struct Verify {
  #[arg(long, help = "Verify signature made by <ADDRESS>.")]
  address: Address<NetworkUnchecked>,
  #[arg(long, help = "Verify signature over <TEXT>.")]
  text: Option<String>,
  #[arg(long, help = "Verify signature over contents of <FILE>.")]
  file: Option<PathBuf>,
  #[arg(long, help = "Verify base64-encoded <WITNESS>.")]
  witness: Option<String>,
  #[arg(long, help = "Verify base64-encoded <TRANSACTION>.")]
  transaction: Option<String>,
}

impl Verify {
  pub(crate) fn run(self) -> SubcommandResult {
    let message = if let Some(text) = &self.text {
      text.as_bytes()
    } else if let Some(file) = &self.file {
      &fs::read(file)?
    } else {
      unreachable!()
    };

    if let Some(witness) = self.witness {
      let mut cursor = bitcoin::io::Cursor::new(general_purpose::STANDARD.decode(witness)?);
      let witness = Witness::consensus_decode_from_finite_reader(&mut cursor)?;
      bip322::verify_simple(&self.address.assume_checked(), message, witness)?;
    } else if let Some(transaction) = self.transaction {
      let mut cursor = bitcoin::io::Cursor::new(general_purpose::STANDARD.decode(transaction)?);
      let transaction = Transaction::consensus_decode_from_finite_reader(&mut cursor)?;
      bip322::verify_full(&self.address.assume_checked(), message, transaction)?;
    } else {
      unreachable!();
    }

    Ok(None)
  }
}

ord/src/subcommand/wallet.rs


use {
  super::*,
  crate::wallet::{batch, wallet_constructor::WalletConstructor, ListDescriptorsResult, Wallet},
  bitcoin::Psbt,
  shared_args::SharedArgs,
};

pub mod addresses;
pub mod balance;
mod batch_command;
pub mod burn;
pub mod cardinals;
pub mod create;
pub mod dump;
pub mod inscribe;
pub mod inscriptions;
mod label;
pub mod mint;
pub mod offer;
pub mod outputs;
pub mod pending;
pub mod receive;
pub mod restore;
pub mod resume;
pub mod runics;
pub mod sats;
pub mod send;
mod shared_args;
pub mod sign;
pub mod split;
pub mod transactions;

#[derive(Debug, Parser)]
pub(crate) struct WalletCommand {
  #[arg(long, default_value = "ord", help = "Use wallet named <WALLET>.")]
  pub(crate) name: String,
  #[arg(long, alias = "nosync", help = "Do not update index.")]
  pub(crate) no_sync: bool,
  #[arg(
    long,
    help = "Use ord running at <SERVER_URL>. [default: http://localhost:80]"
  )]
  pub(crate) server_url: Option<Url>,
  #[command(subcommand)]
  pub(crate) subcommand: Subcommand,
}

#[derive(Debug, Parser)]
#[allow(clippy::large_enum_variant)]
pub(crate) enum Subcommand {
  #[command(about = "Get wallet addresses")]
  Addresses,
  #[command(about = "Get wallet balance")]
  Balance,
  #[command(about = "Create inscriptions and runes")]
  Batch(batch_command::Batch),
  #[command(about = "Burn an inscription")]
  Burn(burn::Burn),
  #[command(about = "List unspent cardinal outputs in wallet")]
  Cardinals,
  #[command(about = "Create new wallet")]
  Create(create::Create),
  #[command(about = "Dump wallet descriptors")]
  Dump,
  #[command(about = "Create inscription")]
  Inscribe(inscribe::Inscribe),
  #[command(about = "List wallet inscriptions")]
  Inscriptions,
  #[command(about = "Export output labels")]
  Label,
  #[command(about = "Mint a rune")]
  Mint(mint::Mint),
  #[command(subcommand, about = "Offer commands")]
  Offer(offer::Offer),
  #[command(about = "List all unspent outputs in wallet")]
  Outputs(outputs::Outputs),
  #[command(about = "List pending etchings")]
  Pending(pending::Pending),
  #[command(about = "Generate receive address")]
  Receive(receive::Receive),
  #[command(about = "Restore wallet")]
  Restore(restore::Restore),
  #[command(about = "Resume pending etchings")]
  Resume(resume::Resume),
  #[command(about = "List unspent runic outputs in wallet")]
  Runics,
  #[command(about = "List wallet satoshis")]
  Sats(sats::Sats),
  #[command(about = "Send sat or inscription")]
  Send(send::Send),
  #[command(about = "Sign message")]
  Sign(sign::Sign),
  #[command(about = "Split outputs")]
  Split(split::Split),
  #[command(about = "See wallet transactions")]
  Transactions(transactions::Transactions),
}

impl WalletCommand {
  pub(crate) fn run(self, settings: Settings) -> SubcommandResult {
    match self.subcommand {
      Subcommand::Create(create) => return create.run(self.name, &settings),
      Subcommand::Restore(restore) => return restore.run(self.name, &settings),
      _ => {}
    };

    let wallet = WalletConstructor::construct(
      self.name.clone(),
      self.no_sync,
      settings.clone(),
      self
        .server_url
        .as_ref()
        .map(Url::as_str)
        .or(settings.server_url())
        .unwrap_or("http://127.0.0.1:80")
        .parse::<Url>()
        .context("invalid server URL")?,
    )?;

    match self.subcommand {
      Subcommand::Addresses => addresses::run(wallet),
      Subcommand::Balance => balance::run(wallet),
      Subcommand::Batch(batch) => batch.run(wallet),
      Subcommand::Burn(burn) => burn.run(wallet),
      Subcommand::Cardinals => cardinals::run(wallet),
      Subcommand::Create(_) | Subcommand::Restore(_) => unreachable!(),
      Subcommand::Dump => dump::run(wallet),
      Subcommand::Inscribe(inscribe) => inscribe.run(wallet),
      Subcommand::Inscriptions => inscriptions::run(wallet),
      Subcommand::Label => label::run(wallet),
      Subcommand::Mint(mint) => mint.run(wallet),
      Subcommand::Offer(offer) => offer.run(wallet),
      Subcommand::Outputs(outputs) => outputs.run(wallet),
      Subcommand::Pending(pending) => pending.run(wallet),
      Subcommand::Receive(receive) => receive.run(wallet),
      Subcommand::Resume(resume) => resume.run(wallet),
      Subcommand::Runics => runics::run(wallet),
      Subcommand::Sats(sats) => sats.run(wallet),
      Subcommand::Send(send) => send.run(wallet),
      Subcommand::Sign(sign) => sign.run(wallet),
      Subcommand::Split(split) => split.run(wallet),
      Subcommand::Transactions(transactions) => transactions.run(wallet),
    }
  }

  fn parse_metadata(cbor: Option<PathBuf>, json: Option<PathBuf>) -> Result<Option<Vec<u8>>> {
    match (cbor, json) {
      (None, None) => Ok(None),
      (Some(path), None) => {
        let cbor = fs::read(path)?;
        let _value: Value = ciborium::from_reader(Cursor::new(cbor.clone()))
          .context("failed to parse CBOR metadata")?;

        Ok(Some(cbor))
      }
      (None, Some(path)) => {
        let value: serde_json::Value =
          serde_json::from_reader(File::open(path)?).context("failed to parse JSON metadata")?;
        let mut cbor = Vec::new();
        ciborium::into_writer(&value, &mut cbor)?;

        Ok(Some(cbor))
      }
      (Some(_), Some(_)) => panic!(),
    }
  }
}

ord/src/subcommand/wallet/addresses.rs


use super::*;

#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct Output {
  pub output: OutPoint,
  pub amount: u64,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub inscriptions: Option<Vec<InscriptionId>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub runes: Option<BTreeMap<SpacedRune, Decimal>>,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let mut addresses: BTreeMap<Address<NetworkUnchecked>, Vec<Output>> = BTreeMap::new();

  for (output, txout) in wallet.utxos() {
    let address = wallet.chain().address_from_script(&txout.script_pubkey)?;

    let inscriptions = wallet.get_inscriptions_in_output(output)?;

    let runes = wallet
      .get_runes_balances_in_output(output)?
      .map(|balances| {
        balances
          .iter()
          .map(|(rune, pile)| {
            (
              *rune,
              Decimal {
                value: pile.amount,
                scale: pile.divisibility,
              },
            )
          })
          .collect()
      });

    let output = Output {
      output: *output,
      amount: txout.value.to_sat(),
      inscriptions,
      runes,
    };

    addresses
      .entry(address.as_unchecked().clone())
      .or_default()
      .push(output);
  }

  Ok(Some(Box::new(addresses)))
}

ord/src/subcommand/wallet/balance.rs


use super::*;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct Output {
  pub cardinal: u64,
  pub ordinal: u64,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub runes: Option<BTreeMap<SpacedRune, Decimal>>,
  #[serde(default, skip_serializing_if = "Option::is_none")]
  pub runic: Option<u64>,
  pub total: u64,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let unspent_outputs = wallet.utxos();

  let inscription_outputs = wallet
    .inscriptions()
    .keys()
    .map(|satpoint| satpoint.outpoint)
    .collect::<BTreeSet<OutPoint>>();

  let mut cardinal = 0;
  let mut ordinal = 0;
  let mut runes = BTreeMap::new();
  let mut runic = 0;

  for (output, txout) in unspent_outputs {
    let rune_balances = wallet
      .get_runes_balances_in_output(output)?
      .unwrap_or_default();

    let is_ordinal = inscription_outputs.contains(output);
    let is_runic = !rune_balances.is_empty();

    if is_ordinal {
      ordinal += txout.value.to_sat();
    }

    if is_runic {
      for (spaced_rune, pile) in rune_balances {
        runes
          .entry(spaced_rune)
          .and_modify(|decimal: &mut Decimal| {
            assert_eq!(decimal.scale, pile.divisibility);
            decimal.value += pile.amount;
          })
          .or_insert(Decimal {
            value: pile.amount,
            scale: pile.divisibility,
          });
      }
      runic += txout.value.to_sat();
    }

    if !is_ordinal && !is_runic {
      cardinal += txout.value.to_sat();
    }

    if is_ordinal && is_runic {
      eprintln!("warning: output {output} contains both inscriptions and runes");
    }
  }

  Ok(Some(Box::new(Output {
    cardinal,
    ordinal,
    runes: wallet.has_rune_index().then_some(runes),
    runic: wallet.has_rune_index().then_some(runic),
    total: cardinal + ordinal + runic,
  })))
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn runes_and_runic_fields_are_not_present_if_none() {
    assert_eq!(
      serde_json::to_string(&Output {
        cardinal: 0,
        ordinal: 0,
        runes: None,
        runic: None,
        total: 0
      })
      .unwrap(),
      r#"{"cardinal":0,"ordinal":0,"total":0}"#
    );
  }
}

ord/src/subcommand/wallet/batch_command.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Batch {
  #[command(flatten)]
  shared: SharedArgs,
  #[arg(
    long,
    help = "Inscribe multiple inscriptions and rune defined in YAML <BATCH_FILE>.",
    value_name = "BATCH_FILE"
  )]
  pub(crate) batch: PathBuf,
}

impl Batch {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let utxos = wallet.utxos();

    let batchfile = batch::File::load(&self.batch)?;

    for inscription in &batchfile.inscriptions {
      for inscription_id in &inscription.gallery {
        ensure! {
          wallet.inscription_exists(*inscription_id)?,
          "gallery item does not exist: {inscription_id}",
        }
      }
    }

    let parent_info = wallet.get_parent_info(&batchfile.parents)?;

    let (inscriptions, reveal_satpoints, postages, destinations) = batchfile.inscriptions(
      &wallet,
      utxos,
      parent_info
        .iter()
        .map(|info| info.tx_out.value.to_sat())
        .collect(),
      self.shared.compress,
    )?;

    let mut locked_utxos = wallet.locked_utxos().clone();

    locked_utxos.extend(
      reveal_satpoints
        .iter()
        .map(|(satpoint, txout)| (satpoint.outpoint, txout.clone())),
    );

    if let Some(etching) = batchfile.etching {
      Self::check_etching(&wallet, &etching)?;
    }

    batch::Plan {
      commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate),
      destinations,
      dry_run: self.shared.dry_run,
      etching: batchfile.etching,
      inscriptions,
      mode: batchfile.mode,
      no_backup: self.shared.no_backup,
      no_limit: self.shared.no_limit,
      parent_info,
      postages,
      reinscribe: batchfile.reinscribe,
      reveal_fee_rate: self.shared.fee_rate,
      reveal_satpoints,
      satpoint: if let Some(sat) = batchfile.sat {
        Some(wallet.find_sat_in_outputs(sat)?)
      } else {
        batchfile.satpoint
      },
    }
    .inscribe(
      &locked_utxos.into_keys().collect(),
      wallet.get_runic_outputs()?.unwrap_or_default(),
      utxos,
      &wallet,
    )
  }

  fn check_etching(wallet: &Wallet, etching: &batch::Etching) -> Result {
    let rune = etching.rune.rune;

    ensure!(
      wallet.load_etching(rune)?.is_none(),
      "rune `{rune}` has pending etching, resume with `ord wallet resume`"
    );

    ensure!(!rune.is_reserved(), "rune `{rune}` is reserved");

    ensure!(
      etching.divisibility <= Etching::MAX_DIVISIBILITY,
      "<DIVISIBILITY> must be less than or equal 38"
    );

    ensure!(
      wallet.has_rune_index(),
      "etching runes requires index created with `--index-runes`",
    );

    ensure!(
      wallet.get_rune(rune)?.is_none(),
      "rune `{rune}` has already been etched",
    );

    let premine = etching.premine.to_integer(etching.divisibility)?;

    let supply = etching.supply.to_integer(etching.divisibility)?;

    let mintable = etching
      .terms
      .map(|terms| -> Result<u128> {
        terms
          .cap
          .checked_mul(terms.amount.to_integer(etching.divisibility)?)
          .ok_or_else(|| anyhow!("`terms.cap` * `terms.amount` over maximum"))
      })
      .transpose()?
      .unwrap_or_default();

    ensure!(
      supply
        == premine
          .checked_add(mintable)
          .ok_or_else(|| anyhow!("`premine` + `terms.cap` * `terms.amount` over maximum"))?,
      "`supply` not equal to `premine` + `terms.cap` * `terms.amount`"
    );

    ensure!(supply > 0, "`supply` must be greater than zero");

    let bitcoin_client = wallet.bitcoin_client();

    let current_height = u32::try_from(bitcoin_client.get_block_count()?).unwrap();

    let reveal_height = current_height + u32::from(Runestone::COMMIT_CONFIRMATIONS);

    let first_rune_height = Rune::first_rune_height(wallet.chain().into());

    ensure!(
      reveal_height >= first_rune_height,
      "rune reveal height below rune activation height: {reveal_height} < {first_rune_height}",
    );

    if let Some(terms) = etching.terms {
      if let Some((start, end)) = terms.offset.and_then(|range| range.start.zip(range.end)) {
        ensure!(
          end > start,
          "`terms.offset.end` must be greater than `terms.offset.start`"
        );
      }

      if let Some((start, end)) = terms.height.and_then(|range| range.start.zip(range.end)) {
        ensure!(
          end > start,
          "`terms.height.end` must be greater than `terms.height.start`"
        );
      }

      if let Some(end) = terms.height.and_then(|range| range.end) {
        ensure!(
          end > u64::from(reveal_height),
          "`terms.height.end` must be greater than the reveal transaction block height of {reveal_height}"
        );
      }

      if let Some(start) = terms.height.and_then(|range| range.start) {
        ensure!(
            start > u64::from(reveal_height),
            "`terms.height.start` must be greater than the reveal transaction block height of {reveal_height}"
          );
      }

      ensure!(terms.cap > 0, "`terms.cap` must be greater than zero");

      ensure!(
        terms.amount.to_integer(etching.divisibility)? > 0,
        "`terms.amount` must be greater than zero",
      );
    }

    let minimum = Rune::minimum_at_height(wallet.chain().into(), Height(reveal_height));

    ensure!(
      rune >= minimum,
      "rune is less than minimum for next block: {rune} < {minimum}",
    );

    Ok(())
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    crate::wallet::batch,
    serde_yaml::{Mapping, Value},
    tempfile::TempDir,
  };

  #[test]
  fn batch_is_loaded_from_yaml_file() {
    let parent = "8d363b28528b0cb86b5fd48615493fb175bdf132d2a3d20b4251bba3f130a5abi0"
      .parse::<InscriptionId>()
      .unwrap();

    let tempdir = TempDir::new().unwrap();

    let inscription_path = tempdir.path().join("tulip.txt");
    fs::write(&inscription_path, "tulips are pretty").unwrap();

    let brc20_path = tempdir.path().join("token.json");

    let batch_path = tempdir.path().join("batch.yaml");
    fs::write(
      &batch_path,
      format!(
        "mode: separate-outputs
parents:
- {parent}
inscriptions:
- file: {}
  metadata:
    title: Lorem Ipsum
    description: Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.
- file: {}
  metaprotocol: brc-20
",
        inscription_path.display(),
        brc20_path.display()
      ),
    )
    .unwrap();

    let mut metadata = Mapping::new();
    metadata.insert(
      Value::String("title".to_string()),
      Value::String("Lorem Ipsum".to_string()),
    );
    metadata.insert(Value::String("description".to_string()), Value::String("Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tristique, massa nec condimentum venenatis, ante massa tempor velit, et accumsan ipsum ligula a massa. Nunc quis orci ante.".to_string()));

    assert_eq!(
      batch::File::load(&batch_path).unwrap(),
      batch::File {
        inscriptions: vec![
          batch::Entry {
            file: Some(inscription_path),
            metadata: Some(Value::Mapping(metadata)),
            ..default()
          },
          batch::Entry {
            file: Some(brc20_path),
            metaprotocol: Some("brc-20".to_string()),
            ..default()
          }
        ],
        parents: vec![parent],
        ..default()
      }
    );
  }

  #[test]
  fn batch_with_unknown_field_throws_error() {
    let tempdir = TempDir::new().unwrap();
    let batch_path = tempdir.path().join("batch.yaml");
    fs::write(
      &batch_path,
      "mode: shared-output\ninscriptions:\n- file: meow.wav\nunknown: 1.)what",
    )
    .unwrap();

    assert!(batch::File::load(&batch_path)
      .unwrap_err()
      .to_string()
      .contains("unknown field `unknown`"));
  }
}

ord/src/subcommand/wallet/burn.rs


use {super::*, bitcoin::opcodes};

#[derive(Debug, Parser)]
pub struct Burn {
  #[arg(
    long,
    conflicts_with = "json_metadata",
    help = "Include CBOR from <PATH> in OP_RETURN.",
    value_name = "PATH"
  )]
  cbor_metadata: Option<PathBuf>,
  #[arg(long, help = "Don't sign or broadcast transaction.")]
  dry_run: bool,
  #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB.")]
  fee_rate: FeeRate,
  #[arg(
    long,
    help = "Include JSON from <PATH> converted to CBOR in OP_RETURN.",
    conflicts_with = "cbor_metadata",
    value_name = "PATH"
  )]
  json_metadata: Option<PathBuf>,
  #[arg(
    long,
    alias = "nolimit",
    help = "Allow OP_RETURN greater than 83 bytes. Transactions over this limit are nonstandard \
    and will not be relayed by bitcoind in its default configuration. Do not use this flag unless \
    you understand the implications."
  )]
  no_limit: bool,
  asset: Outgoing,
}

impl Burn {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let (unsigned_transaction, burn_amount) = match self.asset {
      Outgoing::InscriptionId(id) => {
        let inscription_info = wallet
          .inscription_info()
          .get(&id)
          .ok_or_else(|| anyhow!("inscription {id} not found"))?
          .clone();

        let metadata = WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?;

        ensure!(
          inscription_info.value.is_some(),
          "Cannot burn unbound inscription"
        );

        let mut builder = script::Builder::new().push_opcode(opcodes::all::OP_RETURN);

        // add empty metadata if none is supplied so we can add padding
        let metadata = metadata.unwrap_or_default();

        let push: &script::PushBytes = metadata.as_slice().try_into().with_context(|| {
          format!(
            "metadata length {} over maximum {}",
            metadata.len(),
            u32::MAX
          )
        })?;
        builder = builder.push_slice(push);

        // pad OP_RETURN script to least five bytes to ensure transaction base size
        // is greater than 64 bytes
        let padding = 5usize.saturating_sub(builder.as_script().len());
        if padding > 0 {
          // subtract one byte push opcode from padding length
          let padding = vec![0; padding - 1];
          let push: &script::PushBytes = padding.as_slice().try_into().unwrap();
          builder = builder.push_slice(push);
        }

        let script_pubkey = builder.into_script();

        ensure!(
          self.no_limit || script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE,
          "OP_RETURN with metadata larger than maximum: {} > {}",
          script_pubkey.len(),
          MAX_STANDARD_OP_RETURN_SIZE,
        );

        let burn_amount = Amount::from_sat(1);

        (
          Self::create_unsigned_burn_satpoint_transaction(
            &wallet,
            inscription_info.satpoint,
            self.fee_rate,
            script_pubkey,
            burn_amount,
          )?,
          burn_amount,
        )
      }
      Outgoing::Rune { decimal, rune } => {
        ensure!(
          self.cbor_metadata.is_none() && self.json_metadata.is_none(),
          "metadata not supported when burning runes"
        );

        (
          wallet.create_unsigned_send_or_burn_runes_transaction(
            None,
            rune,
            decimal,
            None,
            self.fee_rate,
          )?,
          Amount::ZERO,
        )
      }
      Outgoing::Amount(_) => bail!("burning bitcoin not supported"),
      Outgoing::Sat(_) => bail!("burning sat not supported"),
      Outgoing::SatPoint(_) => bail!("burning satpoint not supported"),
    };

    let base_size = unsigned_transaction.base_size();

    assert!(
      base_size >= 65,
      "transaction base size less than minimum standard tx nonwitness size: {base_size} < 65",
    );

    let (txid, psbt, fee) = wallet.sign_and_broadcast_transaction(
      unsigned_transaction,
      self.dry_run,
      Some(burn_amount),
    )?;

    Ok(Some(Box::new(send::Output {
      txid,
      psbt,
      asset: self.asset,
      fee,
    })))
  }

  fn create_unsigned_burn_satpoint_transaction(
    wallet: &Wallet,
    satpoint: SatPoint,
    fee_rate: FeeRate,
    script_pubkey: ScriptBuf,
    burn_amount: Amount,
  ) -> Result<Transaction> {
    let runic_outputs = wallet.get_runic_outputs()?.unwrap_or_default();

    ensure!(
      !runic_outputs.contains(&satpoint.outpoint),
      "runic outpoints may not be burned"
    );

    let change = [wallet.get_change_address()?, wallet.get_change_address()?];

    Ok(
      TransactionBuilder::new(
        satpoint,
        wallet.inscriptions().clone(),
        wallet.utxos().clone(),
        wallet.locked_utxos().clone().into_keys().collect(),
        runic_outputs,
        script_pubkey,
        change,
        fee_rate,
        Target::ExactPostage(burn_amount),
        wallet.chain().network(),
      )
      .build_transaction()?,
    )
  }
}

ord/src/subcommand/wallet/cardinals.rs


use super::*;

#[derive(Serialize, Deserialize)]
pub struct CardinalUtxo {
  pub output: OutPoint,
  pub amount: u64,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let unspent_outputs = wallet.utxos();

  let inscribed_utxos = wallet
    .inscriptions()
    .keys()
    .map(|satpoint| satpoint.outpoint)
    .collect::<BTreeSet<OutPoint>>();

  let runic_utxos = wallet.get_runic_outputs()?.unwrap_or_default();

  let cardinal_utxos = unspent_outputs
    .iter()
    .filter_map(|(output, txout)| {
      if inscribed_utxos.contains(output) || runic_utxos.contains(output) {
        None
      } else {
        Some(CardinalUtxo {
          output: *output,
          amount: txout.value.to_sat(),
        })
      }
    })
    .collect::<Vec<CardinalUtxo>>();

  Ok(Some(Box::new(cardinal_utxos)))
}

ord/src/subcommand/wallet/create.rs


use {
  super::*,
  bitcoin::secp256k1::rand::{self, RngCore},
};

#[derive(Serialize, Deserialize)]
pub struct Output {
  pub mnemonic: Mnemonic,
  pub passphrase: Option<String>,
}

#[derive(Debug, Parser)]
pub(crate) struct Create {
  #[arg(
    long,
    default_value = "",
    help = "Use <PASSPHRASE> to derive wallet seed."
  )]
  pub(crate) passphrase: String,
}

impl Create {
  pub(crate) fn run(self, name: String, settings: &Settings) -> SubcommandResult {
    let mut entropy = [0; 16];
    rand::thread_rng().fill_bytes(&mut entropy);

    let mnemonic = Mnemonic::from_entropy(&entropy)?;

    Wallet::initialize(
      name,
      settings,
      mnemonic.to_seed(&self.passphrase),
      bitcoincore_rpc::json::Timestamp::Now,
    )?;

    Ok(Some(Box::new(Output {
      mnemonic,
      passphrase: Some(self.passphrase),
    })))
  }
}

ord/src/subcommand/wallet/dump.rs


use super::*;

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  eprintln!(
    "==========================================
= THIS STRING CONTAINS YOUR PRIVATE KEYS =
=        DO NOT SHARE WITH ANYONE        =
=========================================="
  );

  Ok(Some(Box::new(
    wallet
      .bitcoin_client()
      .call::<ListDescriptorsResult>("listdescriptors", &[serde_json::to_value(true)?])?,
  )))
}

ord/src/subcommand/wallet/inscribe.rs


use super::*;

#[derive(Debug, Parser)]
#[clap(group(
  ArgGroup::new("input")
    .required(true)
    .multiple(true)
    .args(&["delegate", "file"]))
)]
pub(crate) struct Inscribe {
  #[command(flatten)]
  shared: SharedArgs,
  #[arg(
    long,
    help = "Include CBOR in file at <METADATA> as inscription metadata",
    conflicts_with = "json_metadata"
  )]
  pub(crate) cbor_metadata: Option<PathBuf>,
  #[arg(long, help = "Delegate inscription content to <DELEGATE>.")]
  pub(crate) delegate: Option<InscriptionId>,
  #[arg(long, help = "Send inscription to <DESTINATION>.")]
  pub(crate) destination: Option<Address<NetworkUnchecked>>,
  #[arg(
    long,
    help = "Inscribe sat with contents of <FILE>. May be omitted if `--delegate` is supplied."
  )]
  pub(crate) file: Option<PathBuf>,
  #[arg(
    long,
    help = "Include JSON in file at <METADATA> converted to CBOR as inscription metadata",
    conflicts_with = "cbor_metadata"
  )]
  pub(crate) json_metadata: Option<PathBuf>,
  #[clap(long, help = "Set inscription metaprotocol to <METAPROTOCOL>.")]
  pub(crate) metaprotocol: Option<String>,
  #[clap(long, help = "Make inscription a child of <PARENT>.")]
  pub(crate) parent: Option<InscriptionId>,
  #[arg(
    long,
    help = "Include <AMOUNT> postage with inscription. [default: 10000sat]",
    value_name = "AMOUNT"
  )]
  pub(crate) postage: Option<Amount>,
  #[clap(long, help = "Allow reinscription.")]
  pub(crate) reinscribe: bool,
  #[arg(long, help = "Inscribe <SAT>.", conflicts_with = "satpoint")]
  pub(crate) sat: Option<Sat>,
  #[arg(long, help = "Inscribe <SATPOINT>.", conflicts_with = "sat")]
  pub(crate) satpoint: Option<SatPoint>,
  #[arg(
    long,
    help = "Include <INSCRIPTION_ID> in gallery.",
    value_name = "INSCRIPTION_ID"
  )]
  pub(crate) gallery: Vec<InscriptionId>,
}

impl Inscribe {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let chain = wallet.chain();

    if let Some(delegate) = self.delegate {
      ensure! {
        wallet.inscription_exists(delegate)?,
        "delegate {delegate} does not exist"
      }
    }

    for inscription_id in &self.gallery {
      ensure! {
        wallet.inscription_exists(*inscription_id)?,
        "gallery item does not exist: {inscription_id}",
      }
    }

    batch::Plan {
      commit_fee_rate: self.shared.commit_fee_rate.unwrap_or(self.shared.fee_rate),
      destinations: vec![match self.destination.clone() {
        Some(destination) => destination.require_network(chain.network())?,
        None => wallet.get_change_address()?,
      }],
      dry_run: self.shared.dry_run,
      etching: None,
      inscriptions: vec![Inscription::new(
        chain,
        self.shared.compress,
        self.delegate,
        WalletCommand::parse_metadata(self.cbor_metadata, self.json_metadata)?,
        self.metaprotocol,
        self.parent.into_iter().collect(),
        self.file,
        None,
        Properties {
          gallery: self.gallery,
        },
        None,
      )?],
      mode: batch::Mode::SeparateOutputs,
      no_backup: self.shared.no_backup,
      no_limit: self.shared.no_limit,
      parent_info: wallet.get_parent_info(self.parent.as_slice())?,
      postages: vec![self.postage.unwrap_or(TARGET_POSTAGE)],
      reinscribe: self.reinscribe,
      reveal_fee_rate: self.shared.fee_rate,
      reveal_satpoints: Vec::new(),
      satpoint: if let Some(sat) = self.sat {
        Some(wallet.find_sat_in_outputs(sat)?)
      } else {
        self.satpoint
      },
    }
    .inscribe(
      &wallet.locked_utxos().clone().into_keys().collect(),
      wallet.get_runic_outputs()?.unwrap_or_default(),
      wallet.utxos(),
      &wallet,
    )
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn cbor_and_json_metadata_flags_conflict() {
    assert_regex_match!(
      Arguments::try_parse_from([
        "ord",
        "wallet",
        "inscribe",
        "--cbor-metadata",
        "foo",
        "--json-metadata",
        "bar",
        "--file",
        "baz",
      ])
      .unwrap_err()
      .to_string(),
      ".*--cbor-metadata.*cannot be used with.*--json-metadata.*"
    );
  }

  #[test]
  fn satpoint_and_sat_flags_conflict() {
    assert_regex_match!(
      Arguments::try_parse_from([
        "ord",
        "--index-sats",
        "wallet",
        "inscribe",
        "--sat",
        "50000000000",
        "--satpoint",
        "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00:1:0",
        "--file",
        "baz",
      ])
      .unwrap_err()
      .to_string(),
      ".*--sat.*cannot be used with.*--satpoint.*"
    );
  }

  #[test]
  fn delegate_or_file_must_be_set() {
    assert_regex_match!(
      Arguments::try_parse_from(["ord", "wallet", "inscribe", "--fee-rate", "1"])
        .unwrap_err()
        .to_string(),
      r".*required arguments.*--delegate <DELEGATE>\|--file <FILE>.*"
    );

    assert!(Arguments::try_parse_from([
      "ord",
      "wallet",
      "inscribe",
      "--file",
      "hello.txt",
      "--fee-rate",
      "1"
    ])
    .is_ok());

    assert!(Arguments::try_parse_from([
      "ord",
      "wallet",
      "inscribe",
      "--delegate",
      "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00i0",
      "--fee-rate",
      "1"
    ])
    .is_ok());

    assert!(Arguments::try_parse_from([
      "ord",
      "wallet",
      "inscribe",
      "--file",
      "hello.txt",
      "--delegate",
      "038112028c55f3f77cc0b8b413df51f70675f66be443212da0642b7636f68a00i0",
      "--fee-rate",
      "1"
    ])
    .is_ok());
  }
}

ord/src/subcommand/wallet/inscriptions.rs


use super::*;

#[derive(Serialize, Deserialize)]
pub struct Output {
  pub inscription: InscriptionId,
  pub location: SatPoint,
  pub explorer: String,
  pub postage: u64,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let explorer = match wallet.chain() {
    Chain::Mainnet => "https://ordinals.com/inscription/",
    Chain::Regtest => "http://localhost/inscription/",
    Chain::Signet => "https://signet.ordinals.com/inscription/",
    Chain::Testnet => "https://testnet.ordinals.com/inscription/",
    Chain::Testnet4 => "https://testnet4.ordinals.com/inscription/",
  };

  let mut output = Vec::new();

  for (location, inscriptions) in wallet.inscriptions() {
    if let Some(txout) = wallet.utxos().get(&location.outpoint) {
      for inscription in inscriptions {
        output.push(Output {
          location: *location,
          inscription: *inscription,
          explorer: format!("{explorer}{inscription}"),
          postage: txout.value.to_sat(),
        })
      }
    }
  }

  Ok(Some(Box::new(output)))
}

ord/src/subcommand/wallet/label.rs


use super::*;

#[derive(Serialize)]
struct Label {
  first_sat: SatLabel,
  inscriptions: BTreeMap<u64, BTreeSet<InscriptionId>>,
}

#[derive(Serialize)]
struct SatLabel {
  name: String,
  number: u64,
  rarity: Rarity,
}

#[derive(Serialize)]
struct Line {
  label: String,
  r#ref: String,
  r#type: String,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let mut lines: Vec<Line> = Vec::new();

  let sat_ranges = wallet.get_wallet_sat_ranges()?;

  let mut inscriptions_by_output: BTreeMap<OutPoint, BTreeMap<u64, Vec<InscriptionId>>> =
    BTreeMap::new();

  for (satpoint, inscriptions) in wallet.inscriptions() {
    inscriptions_by_output
      .entry(satpoint.outpoint)
      .or_default()
      .insert(satpoint.offset, inscriptions.clone());
  }

  for (output, ranges) in sat_ranges {
    let sat = Sat(ranges[0].0);
    let mut inscriptions = BTreeMap::<u64, BTreeSet<InscriptionId>>::new();

    if let Some(output_inscriptions) = inscriptions_by_output.get(&output) {
      for (&offset, offset_inscriptions) in output_inscriptions {
        inscriptions
          .entry(offset)
          .or_default()
          .extend(offset_inscriptions);
      }
    }

    lines.push(Line {
      label: serde_json::to_string(&Label {
        first_sat: SatLabel {
          name: sat.name(),
          number: sat.n(),
          rarity: sat.rarity(),
        },
        inscriptions,
      })?,
      r#ref: output.to_string(),
      r#type: "output".into(),
    });
  }

  for line in lines {
    serde_json::to_writer(io::stdout(), &line)?;
    println!();
  }

  Ok(None)
}

ord/src/subcommand/wallet/mint.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Mint {
  #[clap(long, help = "Use <FEE_RATE> sats/vbyte for mint transaction.")]
  fee_rate: FeeRate,
  #[clap(long, help = "Mint <RUNE>. May contain `.` or `•`as spacers.")]
  rune: SpacedRune,
  #[clap(
    long,
    help = "Include <AMOUNT> postage with mint output. [default: 10000sat]"
  )]
  postage: Option<Amount>,
  #[clap(long, help = "Send minted runes to <DESTINATION>.")]
  destination: Option<Address<NetworkUnchecked>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Output {
  pub rune: SpacedRune,
  pub pile: Pile,
  pub mint: Txid,
}

impl Mint {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      wallet.has_rune_index(),
      "`ord wallet mint` requires index created with `--index-runes` flag",
    );

    let rune = self.rune.rune;

    let bitcoin_client = wallet.bitcoin_client();

    let block_height = bitcoin_client.get_block_count()?;

    let Some((id, rune_entry, _)) = wallet.get_rune(rune)? else {
      bail!("rune {rune} has not been etched");
    };

    let postage = self.postage.unwrap_or(TARGET_POSTAGE);

    let amount = rune_entry
      .mintable(block_height + 1)
      .map_err(|err| anyhow!("rune {rune} {err}"))?;

    let chain = wallet.chain();

    let destination = match self.destination {
      Some(destination) => destination.require_network(chain.network())?,
      None => wallet.get_change_address()?,
    };

    ensure!(
      destination.script_pubkey().minimal_non_dust() <= postage,
      "postage below dust limit of {}sat",
      destination.script_pubkey().minimal_non_dust().to_sat()
    );

    let runestone = Runestone {
      mint: Some(id),
      ..default()
    };

    let script_pubkey = runestone.encipher();

    ensure!(
      script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE,
      "runestone greater than maximum OP_RETURN size: {} > {}",
      script_pubkey.len(),
      MAX_STANDARD_OP_RETURN_SIZE,
    );

    let unfunded_transaction = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: Vec::new(),
      output: vec![
        TxOut {
          script_pubkey,
          value: Amount::from_sat(0),
        },
        TxOut {
          script_pubkey: destination.script_pubkey(),
          value: postage,
        },
      ],
    };

    wallet.lock_non_cardinal_outputs()?;

    let unsigned_transaction =
      fund_raw_transaction(bitcoin_client, self.fee_rate, &unfunded_transaction)?;

    let signed_transaction = bitcoin_client
      .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)?
      .hex;

    let signed_transaction = consensus::encode::deserialize(&signed_transaction)?;

    assert_eq!(
      Runestone::decipher(&signed_transaction),
      Some(Artifact::Runestone(runestone)),
    );

    let transaction = bitcoin_client.send_raw_transaction(&signed_transaction)?;

    Ok(Some(Box::new(Output {
      rune: self.rune,
      pile: Pile {
        amount,
        divisibility: rune_entry.divisibility,
        symbol: rune_entry.symbol,
      },
      mint: transaction,
    })))
  }
}

ord/src/subcommand/wallet/offer.rs


use super::*;

pub mod accept;
pub mod create;

#[derive(Debug, Parser)]
pub(crate) enum Offer {
  #[command(about = "Accept offer to buy inscription")]
  Accept(accept::Accept),
  #[command(about = "Create offer to buy inscription")]
  Create(create::Create),
}

impl Offer {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    match self {
      Self::Accept(accept) => accept.run(wallet),
      Self::Create(create) => create.run(wallet),
    }
  }
}

ord/src/subcommand/wallet/offer/accept.rs


use super::*;

#[derive(PartialEq)]
enum Signature<'a> {
  Script(&'a Script),
  Witness(&'a Witness),
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
  pub txid: Txid,
}

#[derive(Debug, Parser)]
pub(crate) struct Accept {
  #[arg(long, help = "Assert offer is for <AMOUNT>")]
  amount: Amount,
  #[arg(long, help = "Don't sign or broadcast transaction")]
  dry_run: bool,
  #[arg(long, help = "Assert offer is for <INSCRIPTION>")]
  inscription: InscriptionId,
  #[arg(long, help = "Accept <PSBT> offer")]
  psbt: String,
}

impl Accept {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    let psbt = base64_decode(&self.psbt).context("failed to base64 decode PSBT")?;

    let psbt = Psbt::deserialize(&psbt).context("failed to deserialize PSBT")?;

    let mut outgoing = BTreeMap::new();

    for (index, input) in psbt.unsigned_tx.input.iter().enumerate() {
      if wallet.utxos().contains_key(&input.previous_output) {
        outgoing.insert(index, input.previous_output);
      }
    }

    ensure! {
      outgoing.len() <= 1,
      "PSBT contains {} inputs owned by wallet", outgoing.len(),
    }

    let Some((index, outgoing)) = outgoing.into_iter().next() else {
      bail!("PSBT contains no inputs owned by wallet");
    };

    if let Some(runes) = wallet.get_runes_balances_in_output(&outgoing)? {
      ensure! {
        runes.is_empty(),
        "outgoing input {} contains runes", outgoing,
      }
    }

    let Some(inscriptions) = wallet.get_inscriptions_in_output(&outgoing)? else {
      bail! {
        "index must have inscription index to accept PSBT",
      }
    };

    ensure! {
      inscriptions.len() <= 1,
      "outgoing input {} contains {} inscriptions", outgoing, inscriptions.len(),
    }

    let Some(inscription) = inscriptions.into_iter().next() else {
      bail!("outgoing input contains no inscriptions");
    };

    ensure! {
      inscription == self.inscription,
      "unexpected outgoing inscription {inscription}",
    }

    let balance_change = wallet.simulate_transaction(&psbt.unsigned_tx)?;

    ensure! {
      balance_change == self.amount.to_signed()?,
      "unexpected balance change of {balance_change}",
    }

    let signatures = Self::psbt_signatures(&psbt)?;

    for (i, signature) in signatures.iter().enumerate() {
      let outpoint = psbt.unsigned_tx.input[i].previous_output;

      if i == index {
        ensure! {
          signature.is_none(),
          "seller input `{outpoint}` is signed: seller input must not be signed",
        }
      } else {
        ensure! {
          signature.is_some(),
          "buyer input `{outpoint}` is unsigned: buyer inputs must be signed",
        }
      }
    }

    let txid = if self.dry_run {
      psbt.unsigned_tx.compute_txid()
    } else {
      let signed_psbt = wallet
        .bitcoin_client()
        .wallet_process_psbt(&base64_encode(&psbt.serialize()), Some(true), None, None)?
        .psbt;

      let signed_tx = wallet
        .bitcoin_client()
        .finalize_psbt(&signed_psbt, None)?
        .hex
        .ok_or_else(|| anyhow!("unable to sign transaction"))?;

      {
        let signed_tx = Transaction::consensus_decode(&mut signed_tx.as_slice())
          .context("unable to decode finalized transaction")?;

        ensure! {
          signed_tx.input.len() == psbt.inputs.len() &&
          signed_tx.input.len() == psbt.unsigned_tx.input.len(),
          "signed transaction input length mismatch",
        }

        for (i, (old, new)) in signatures
          .into_iter()
          .zip(Self::tx_signatures(&signed_tx)?)
          .enumerate()
        {
          let outpoint = signed_tx.input[i].previous_output;

          if i == index {
            ensure! {
              new.is_some(),
              "seller input `{outpoint}` was not signed by wallet",
            }
          } else {
            ensure! {
              old == new,
              "buyer input `{outpoint}` signature changed after signing",
            }
          }
        }
      }

      wallet.send_raw_transaction(&signed_tx, None)?
    };

    Ok(Some(Box::new(Output { txid })))
  }

  fn psbt_signatures(psbt: &Psbt) -> Result<Vec<Option<Signature>>> {
    psbt
      .inputs
      .iter()
      .map(
        |input| match (&input.final_script_sig, &input.final_script_witness) {
          (None, None) => Ok(None),
          (Some(script), None) => Ok(Some(Signature::Script(script))),
          (None, Some(witness)) => Ok(Some(Signature::Witness(witness))),
          (Some(_), Some(_)) => bail!("input contains both scriptsig and witness"),
        },
      )
      .collect()
  }

  fn tx_signatures(tx: &Transaction) -> Result<Vec<Option<Signature>>> {
    tx.input
      .iter()
      .map(|input| {
        match (
          (!input.script_sig.is_empty()).then_some(&input.script_sig),
          (!input.witness.is_empty()).then_some(&input.witness),
        ) {
          (None, None) => Ok(None),
          (Some(script), None) => Ok(Some(Signature::Script(script))),
          (None, Some(witness)) => Ok(Some(Signature::Witness(witness))),
          (Some(_), Some(_)) => bail!("input contains both scriptsig and witness"),
        }
      })
      .collect()
  }
}

ord/src/subcommand/wallet/offer/create.rs


use super::*;

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
  pub psbt: String,
  pub seller_address: Address<NetworkUnchecked>,
  pub inscription: InscriptionId,
}

#[derive(Debug, Parser)]
pub(crate) struct Create {
  #[arg(long, help = "<INSCRIPTION> to make offer for.")]
  inscription: InscriptionId,
  #[arg(long, help = "<AMOUNT> to offer.")]
  amount: Amount,
  #[arg(long, help = "<FEE_RATE> for finalized transaction.")]
  fee_rate: FeeRate,
}

impl Create {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      !wallet.inscription_info().contains_key(&self.inscription),
      "inscription {} already in wallet",
      self.inscription
    );

    let Some(inscription) = wallet.get_inscription(self.inscription)? else {
      bail!("inscription {} does not exist", self.inscription);
    };

    let Some(postage) = inscription.value else {
      bail!("inscription {} unbound", self.inscription);
    };

    let Some(seller_address) = inscription.address else {
      bail!(
        "inscription {} script pubkey not valid address",
        self.inscription,
      );
    };

    let seller_address = seller_address
      .parse::<Address<NetworkUnchecked>>()
      .unwrap()
      .require_network(wallet.chain().network())?;

    let postage = Amount::from_sat(postage);

    let tx = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: vec![TxIn {
        previous_output: inscription.satpoint.outpoint,
        script_sig: ScriptBuf::new(),
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        witness: Witness::new(),
      }],
      output: vec![
        TxOut {
          value: postage,
          script_pubkey: wallet.get_change_address()?.into(),
        },
        TxOut {
          value: self.amount + postage,
          script_pubkey: seller_address.clone().into(),
        },
      ],
    };

    wallet.lock_non_cardinal_outputs()?;

    let tx = fund_raw_transaction(wallet.bitcoin_client(), self.fee_rate, &tx)?;

    let tx = Transaction::consensus_decode(&mut tx.as_slice())?;

    let psbt = Psbt::from_unsigned_tx(tx)?;

    let result = wallet
      .bitcoin_client()
      .call::<String>("utxoupdatepsbt", &[base64_encode(&psbt.serialize()).into()])?;

    let result = wallet
      .bitcoin_client()
      .wallet_process_psbt(&result, Some(true), None, None)?;

    ensure! {
      !result.complete,
      "PSBT unexpectedly complete after processing with wallet",
    }

    Ok(Some(Box::new(Output {
      psbt: result.psbt,
      inscription: self.inscription,
      seller_address: seller_address.into_unchecked(),
    })))
  }
}

ord/src/subcommand/wallet/outputs.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Outputs {
  #[arg(short, long, help = "Show list of sat <RANGES> in outputs.")]
  ranges: bool,
}

#[derive(Serialize, Deserialize, PartialEq)]
pub struct Output {
  pub output: OutPoint,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub address: Option<Address<NetworkUnchecked>>,
  pub amount: u64,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub inscriptions: Option<Vec<InscriptionId>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub runes: Option<BTreeMap<SpacedRune, Decimal>>,
  #[serde(skip_serializing_if = "Option::is_none")]
  pub sat_ranges: Option<Vec<String>>,
}

impl Outputs {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    let mut outputs = Vec::new();
    for (output, txout) in wallet.utxos() {
      let address = wallet
        .chain()
        .address_from_script(&txout.script_pubkey)
        .ok()
        .map(|address| address.as_unchecked().clone());

      let inscriptions = wallet.get_inscriptions_in_output(output)?;

      let runes = wallet
        .get_runes_balances_in_output(output)?
        .map(|balances| {
          balances
            .iter()
            .map(|(rune, pile)| {
              (
                *rune,
                Decimal {
                  value: pile.amount,
                  scale: pile.divisibility,
                },
              )
            })
            .collect()
        });

      let sat_ranges = if wallet.has_sat_index() && self.ranges {
        Some(
          wallet
            .get_output_sat_ranges(output)?
            .into_iter()
            .map(|(start, end)| format!("{start}-{end}"))
            .collect(),
        )
      } else {
        None
      };

      outputs.push(Output {
        address,
        amount: txout.value.to_sat(),
        inscriptions,
        output: *output,
        runes,
        sat_ranges,
      });
    }

    Ok(Some(Box::new(outputs)))
  }
}

ord/src/subcommand/wallet/pending.rs


use super::*;

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct PendingOutput {
  pub commit: Txid,
  pub rune: SpacedRune,
}
#[derive(Debug, Parser)]
pub(crate) struct Pending {}

impl Pending {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let etchings = wallet
      .pending_etchings()?
      .into_iter()
      .map(|(_, entry)| {
        let spaced_rune = entry.output.rune.unwrap().rune;

        PendingOutput {
          rune: spaced_rune,
          commit: entry.commit.compute_txid(),
        }
      })
      .collect::<Vec<PendingOutput>>();

    Ok(Some(Box::new(etchings) as Box<dyn Output>))
  }
}

ord/src/subcommand/wallet/receive.rs


use super::*;

#[derive(Deserialize, Serialize)]
pub struct Output {
  pub addresses: Vec<Address<NetworkUnchecked>>,
}

#[derive(Debug, Parser)]
pub(crate) struct Receive {
  #[arg(short, long, help = "Generate <NUMBER> addresses.")]
  number: Option<u64>,
}

impl Receive {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let mut addresses: Vec<Address<NetworkUnchecked>> = Vec::new();

    for _ in 0..self.number.unwrap_or(1) {
      addresses.push(
        wallet
          .bitcoin_client()
          .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32m))?,
      );
    }

    Ok(Some(Box::new(Output { addresses })))
  }
}

ord/src/subcommand/wallet/restore.rs


use super::*;

#[derive(Debug, Clone)]
pub(crate) struct Timestamp(bitcoincore_rpc::json::Timestamp);

impl FromStr for Timestamp {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self, Self::Err> {
    Ok(if s == "now" {
      Self(bitcoincore_rpc::json::Timestamp::Now)
    } else {
      Self(bitcoincore_rpc::json::Timestamp::Time(s.parse()?))
    })
  }
}

#[derive(Debug, Parser)]
pub(crate) struct Restore {
  #[clap(value_enum, long, help = "Restore wallet from <SOURCE> on stdin.")]
  from: Source,
  #[arg(long, help = "Use <PASSPHRASE> when deriving wallet.")]
  pub(crate) passphrase: Option<String>,
  #[arg(
    long,
    help = "Scan chain from <TIMESTAMP> onwards. Can be a unix timestamp in \
    seconds or the string `now`, to skip scanning"
  )]
  pub(crate) timestamp: Option<Timestamp>,
}

#[derive(clap::ValueEnum, Debug, Clone)]
enum Source {
  Descriptor,
  Mnemonic,
}

impl Restore {
  pub(crate) fn run(self, name: String, settings: &Settings) -> SubcommandResult {
    ensure!(
      !settings
        .bitcoin_rpc_client(None)?
        .list_wallet_dir()?
        .iter()
        .any(|wallet_name| wallet_name == &name),
      "wallet `{}` already exists",
      name
    );

    let mut buffer = String::new();

    match self.from {
      Source::Descriptor => {
        io::stdin().read_to_string(&mut buffer)?;

        ensure!(
          self.passphrase.is_none(),
          "descriptor does not take a passphrase"
        );

        ensure!(
          self.timestamp.is_none(),
          "descriptor does not take a timestamp"
        );

        let wallet_descriptors: ListDescriptorsResult = serde_json::from_str(&buffer)?;
        Wallet::initialize_from_descriptors(name, settings, wallet_descriptors.descriptors)?;
      }
      Source::Mnemonic => {
        io::stdin().read_line(&mut buffer)?;
        let mnemonic = Mnemonic::from_str(&buffer)?;
        Wallet::initialize(
          name,
          settings,
          mnemonic.to_seed(self.passphrase.unwrap_or_default()),
          self
            .timestamp
            .unwrap_or(Timestamp(bitcoincore_rpc::json::Timestamp::Time(0)))
            .0,
        )?;
      }
    }

    Ok(None)
  }
}

ord/src/subcommand/wallet/resume.rs


use {super::*, crate::wallet::Maturity};

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct ResumeOutput {
  pub etchings: Vec<batch::Output>,
}
#[derive(Debug, Parser)]
pub(crate) struct Resume {
  #[arg(long, help = "Don't broadcast transactions.")]
  pub(crate) dry_run: bool,
  #[arg(long, help = "Pending <RUNE> etching to resume.")]
  pub(crate) rune: Option<SpacedRune>,
}

impl Resume {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let mut etchings = Vec::new();
    loop {
      if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
        break;
      }

      let spaced_rune = self.rune;

      let pending_etchings = if let Some(spaced_rune) = spaced_rune {
        let pending_etching = wallet.load_etching(spaced_rune.rune)?;

        ensure!(
          pending_etching.is_some(),
          "rune {spaced_rune} does not correspond to any pending etching."
        );

        vec![(spaced_rune.rune, pending_etching.unwrap())]
      } else {
        wallet.pending_etchings()?
      };

      for (rune, entry) in pending_etchings {
        if self.dry_run {
          etchings.push(batch::Output {
            reveal_broadcast: false,
            ..entry.output.clone()
          });
          continue;
        };

        match wallet.check_maturity(rune, &entry.commit)? {
          Maturity::Mature => etchings.push(wallet.send_etching(rune, &entry)?),
          Maturity::CommitSpent(txid) => {
            eprintln!("Commitment for rune etching {rune} spent in {txid}");
            wallet.clear_etching(rune)?;
          }
          Maturity::CommitNotFound => {}
          Maturity::BelowMinimumHeight(_) => {}
          Maturity::ConfirmationsPending(_) => {}
        }
      }

      if wallet.pending_etchings()?.is_empty() {
        break;
      }

      if self.dry_run {
        break;
      }

      if !wallet.integration_test() {
        thread::sleep(Duration::from_secs(5));
      }
    }

    Ok(Some(Box::new(ResumeOutput { etchings }) as Box<dyn Output>))
  }
}

ord/src/subcommand/wallet/runics.rs


use super::*;

#[derive(Serialize, Deserialize)]
pub struct RunicUtxo {
  pub output: OutPoint,
  pub runes: BTreeMap<SpacedRune, Decimal>,
}

pub(crate) fn run(wallet: Wallet) -> SubcommandResult {
  let unspent_outputs = wallet.utxos();
  let Some(runic_utxos) = wallet.get_runic_outputs()? else {
    bail!("`ord wallet runics` requires index created with `--index-runes`")
  };

  let mut result = Vec::new();

  for output in unspent_outputs.keys() {
    if runic_utxos.contains(output) {
      let rune_balances = wallet
        .get_runes_balances_in_output(output)?
        .unwrap_or_default();

      let mut runes = BTreeMap::new();

      for (spaced_rune, pile) in rune_balances {
        runes
          .entry(spaced_rune)
          .and_modify(|decimal: &mut Decimal| {
            assert_eq!(decimal.scale, pile.divisibility);
            decimal.value += pile.amount;
          })
          .or_insert(Decimal {
            value: pile.amount,
            scale: pile.divisibility,
          });
      }

      result.push(RunicUtxo {
        output: *output,
        runes,
      });
    }
  }

  Ok(Some(Box::new(result)))
}

ord/src/subcommand/wallet/sats.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Sats {
  #[arg(
    long,
    conflicts_with = "all",
    help = "Find satoshis listed in first column of tab-separated value file <TSV>."
  )]
  tsv: Option<PathBuf>,
  #[arg(
    long,
    conflicts_with = "tsv",
    help = "Display list of all sat ranges in wallet."
  )]
  all: bool,
}

#[derive(Serialize, Deserialize)]
pub struct OutputTsv {
  pub found: BTreeMap<String, SatPoint>,
  pub lost: BTreeSet<String>,
}

#[derive(Serialize, Deserialize)]
pub struct OutputRare {
  pub sat: Sat,
  pub output: OutPoint,
  pub offset: u64,
  pub rarity: Rarity,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct OutputAll {
  pub output: OutPoint,
  pub ranges: Vec<String>,
}

impl Sats {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      wallet.has_sat_index(),
      "sats requires index created with `--index-sats` flag"
    );

    let haystacks = wallet.get_wallet_sat_ranges()?;

    if self.all {
      Ok(Some(Box::new(
        haystacks
          .into_iter()
          .map(|(outpoint, ranges)| OutputAll {
            output: outpoint,
            ranges: ranges
              .into_iter()
              .map(|range| format!("{}-{}", range.0, range.1))
              .collect::<Vec<String>>(),
          })
          .collect::<Vec<OutputAll>>(),
      )))
    } else if let Some(path) = &self.tsv {
      let tsv = fs::read_to_string(path)
        .with_context(|| format!("I/O error reading `{}`", path.display()))?;

      let needles = Self::needles(&tsv)?;

      let found = Self::find(&needles, &haystacks);

      let lost = needles
        .into_iter()
        .filter(|(_sat, value)| !found.contains_key(*value))
        .map(|(_sat, value)| value.into())
        .collect();

      Ok(Some(Box::new(OutputTsv { found, lost })))
    } else {
      let mut output = Vec::new();
      for (outpoint, sat, offset, rarity) in Self::rare_sats(haystacks) {
        output.push(OutputRare {
          sat,
          output: outpoint,
          offset,
          rarity,
        });
      }

      Ok(Some(Box::new(output)))
    }
  }

  fn find(
    needles: &[(Sat, &str)],
    ranges: &[(OutPoint, Vec<(u64, u64)>)],
  ) -> BTreeMap<String, SatPoint> {
    let mut haystacks = Vec::new();

    for (outpoint, ranges) in ranges {
      let mut offset = 0;
      for (start, end) in ranges {
        haystacks.push((start, end, offset, outpoint));
        offset += end - start;
      }
    }

    haystacks.sort_by_key(|(start, _, _, _)| *start);

    let mut i = 0;
    let mut j = 0;
    let mut results = BTreeMap::new();
    while i < needles.len() && j < haystacks.len() {
      let (needle, value) = needles[i];
      let (&start, &end, offset, outpoint) = haystacks[j];

      if needle >= start && needle < end {
        results.insert(
          value.into(),
          SatPoint {
            outpoint: *outpoint,
            offset: offset + needle.0 - start,
          },
        );
      }

      if needle >= end {
        j += 1;
      } else {
        i += 1;
      }
    }

    results
  }

  fn needles(tsv: &str) -> Result<Vec<(Sat, &str)>> {
    let mut needles = tsv
      .lines()
      .enumerate()
      .filter(|(_i, line)| !line.starts_with('#') && !line.is_empty())
      .filter_map(|(i, line)| {
        line.split('\t').next().map(|value| {
          Sat::from_str(value).map(|sat| (sat, value)).map_err(|err| {
            anyhow!(
              "failed to parse sat from string \"{value}\" on line {}: {err}",
              i + 1,
            )
          })
        })
      })
      .collect::<Result<Vec<(Sat, &str)>>>()?;

    needles.sort();

    Ok(needles)
  }

  fn rare_sats(haystacks: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(OutPoint, Sat, u64, Rarity)> {
    haystacks
      .into_iter()
      .flat_map(|(outpoint, sat_ranges)| {
        let mut offset = 0;
        sat_ranges.into_iter().filter_map(move |(start, end)| {
          let sat = Sat(start);
          let rarity = sat.rarity();
          let start_offset = offset;
          offset += end - start;
          if rarity > Rarity::Common {
            Some((outpoint, sat, start_offset, rarity))
          } else {
            None
          }
        })
      })
      .collect()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn identify_no_rare_sats() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(51 * COIN_VALUE, 100 * COIN_VALUE), (1234, 5678)],
      )]),
      Vec::new()
    )
  }

  #[test]
  fn identify_one_rare_sat() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(10, 80), (50 * COIN_VALUE, 100 * COIN_VALUE)],
      )]),
      vec![(outpoint(1), Sat(50 * COIN_VALUE), 70, Rarity::Uncommon)]
    )
  }

  #[test]
  fn identify_two_rare_sats() {
    assert_eq!(
      Sats::rare_sats(vec![(
        outpoint(1),
        vec![(0, 100), (1050000000000000, 1150000000000000)],
      )]),
      vec![
        (outpoint(1), Sat(0), 0, Rarity::Mythic),
        (outpoint(1), Sat(1050000000000000), 100, Rarity::Epic)
      ]
    )
  }

  #[test]
  fn identify_rare_sats_in_different_outpoints() {
    assert_eq!(
      Sats::rare_sats(vec![
        (outpoint(1), vec![(50 * COIN_VALUE, 55 * COIN_VALUE)]),
        (outpoint(2), vec![(100 * COIN_VALUE, 111 * COIN_VALUE)],),
      ]),
      vec![
        (outpoint(1), Sat(50 * COIN_VALUE), 0, Rarity::Uncommon),
        (outpoint(2), Sat(100 * COIN_VALUE), 0, Rarity::Uncommon)
      ]
    )
  }

  #[track_caller]
  fn case(tsv: &str, haystacks: &[(OutPoint, Vec<(u64, u64)>)], expected: &[(&str, SatPoint)]) {
    assert_eq!(
      Sats::find(&Sats::needles(tsv).unwrap(), haystacks),
      expected
        .iter()
        .map(|(sat, satpoint)| (sat.to_string(), *satpoint))
        .collect()
    );
  }

  #[test]
  fn tsv() {
    case("1\n", &[(outpoint(1), vec![(0, 1)])], &[]);
  }

  #[test]
  fn identify_from_tsv_single() {
    case(
      "0\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_two_in_one_range() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    );
  }

  #[test]
  fn identify_from_tsv_out_of_order_tsv() {
    case(
      "1\n0\n",
      &[(outpoint(1), vec![(0, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    );
  }

  #[test]
  fn identify_from_tsv_out_of_order_ranges() {
    case(
      "1\n0\n",
      &[(outpoint(1), vec![(1, 2), (0, 1)])],
      &[("0", satpoint(1, 1)), ("1", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_two_in_two_ranges() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 1), (1, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(1, 1))],
    )
  }

  #[test]
  fn identify_from_tsv_two_in_two_outputs() {
    case(
      "0\n1\n",
      &[(outpoint(1), vec![(0, 1)]), (outpoint(2), vec![(1, 2)])],
      &[("0", satpoint(1, 0)), ("1", satpoint(2, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_extra_columns() {
    case(
      "0\t===\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_empty_lines() {
    case(
      "0\n\n\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn identify_from_tsv_ignores_comments() {
    case(
      "0\n#===\n",
      &[(outpoint(1), vec![(0, 1)])],
      &[("0", satpoint(1, 0))],
    );
  }

  #[test]
  fn parse_error_reports_line_and_value() {
    assert_eq!(
      Sats::needles("0\n===\n")
        .unwrap_err()
        .to_string(),
      "failed to parse sat from string \"===\" on line 2: failed to parse sat `===`: invalid integer: invalid digit found in string",
    );
  }
}

ord/src/subcommand/wallet/send.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Send {
  #[arg(long, help = "Don't sign or broadcast transaction")]
  pub(crate) dry_run: bool,
  #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
  fee_rate: FeeRate,
  #[arg(
    long,
    help = "Target <AMOUNT> postage with sent inscriptions. [default: 10000 sat]",
    value_name = "AMOUNT"
  )]
  pub(crate) postage: Option<Amount>,
  #[arg(help = "Recipient address")]
  address: Address<NetworkUnchecked>,
  #[arg(
    help = "Outgoing asset formatted as a bitcoin amount, rune amount, sat name, satpoint, or \
    inscription ID. Bitcoin amounts are `DECIMAL UNIT` where `UNIT` is one of \
    `bit btc cbtc mbtc msat nbtc pbtc sat satoshi ubtc`. Rune amounts are `DECIMAL:RUNE` and \
    respect divisibility"
  )]
  asset: Outgoing,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
  pub txid: Txid,
  pub psbt: String,
  pub asset: Outgoing,
  pub fee: u64,
}

impl Send {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let address = self
      .address
      .clone()
      .require_network(wallet.chain().network())?;

    let unsigned_transaction = match self.asset {
      Outgoing::Amount(amount) => {
        wallet.create_unsigned_send_amount_transaction(address, amount, self.fee_rate)?
      }
      Outgoing::Rune { decimal, rune } => wallet.create_unsigned_send_or_burn_runes_transaction(
        Some(address),
        rune,
        decimal,
        self.postage,
        self.fee_rate,
      )?,
      Outgoing::InscriptionId(id) => wallet.create_unsigned_send_satpoint_transaction(
        address,
        wallet
          .inscription_info()
          .get(&id)
          .ok_or_else(|| anyhow!("inscription {id} not found"))?
          .satpoint,
        self.postage,
        self.fee_rate,
        true,
      )?,
      Outgoing::SatPoint(satpoint) => wallet.create_unsigned_send_satpoint_transaction(
        address,
        satpoint,
        self.postage,
        self.fee_rate,
        false,
      )?,
      Outgoing::Sat(sat) => wallet.create_unsigned_send_satpoint_transaction(
        address,
        wallet.find_sat_in_outputs(sat)?,
        self.postage,
        self.fee_rate,
        true,
      )?,
    };

    let (txid, psbt, fee) =
      wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, None)?;

    Ok(Some(Box::new(Output {
      txid,
      psbt,
      asset: self.asset,
      fee,
    })))
  }
}

ord/src/subcommand/wallet/shared_args.rs


use super::*;

#[derive(Debug, Parser)]
pub(super) struct SharedArgs {
  #[arg(
    long,
    help = "Use <COMMIT_FEE_RATE> sats/vbyte for commit transaction.\nDefaults to <FEE_RATE> if unset."
  )]
  pub(crate) commit_fee_rate: Option<FeeRate>,
  #[arg(long, help = "Compress inscription content with brotli.")]
  pub(crate) compress: bool,
  #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB.")]
  pub(crate) fee_rate: FeeRate,
  #[arg(long, help = "Don't sign or broadcast transactions.")]
  pub(crate) dry_run: bool,
  #[arg(long, alias = "nobackup", help = "Do not back up recovery key.")]
  pub(crate) no_backup: bool,
  #[arg(
    long,
    alias = "nolimit",
    help = "Allow transactions larger than MAX_STANDARD_TX_WEIGHT of 400,000 weight units and \
    OP_RETURNs greater than 83 bytes. Transactions over this limit are nonstandard and will not be \
    relayed by bitcoind in its default configuration. Do not use this flag unless you understand \
    the implications."
  )]
  pub(crate) no_limit: bool,
}

ord/src/subcommand/wallet/sign.rs


use {
  super::*,
  base64::{engine::general_purpose, Engine},
};

#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Output {
  pub address: Address<NetworkUnchecked>,
  pub witness: String,
}

#[derive(Debug, Parser)]
#[clap(
group(
  ArgGroup::new("input")
    .required(true)
    .args(&["text", "file"])))
]
pub(crate) struct Sign {
  #[arg(
    long,
    help = "Sign with public key associated with address, output, or inscription."
  )]
  signer: Signer,
  #[arg(long, help = "Sign <TEXT>.")]
  text: Option<String>,
  #[arg(long, help = "Sign contents of <FILE>.")]
  file: Option<PathBuf>,
}

impl Sign {
  pub(crate) fn run(&self, wallet: Wallet) -> SubcommandResult {
    let address = match &self.signer {
      Signer::Address(address) => address.clone().require_network(wallet.chain().network())?,
      Signer::Inscription(inscription) => Address::from_str(
        &wallet
          .inscription_info()
          .get(inscription)
          .ok_or_else(|| anyhow!("inscription {inscription} not in wallet"))?
          .address
          .clone()
          .ok_or_else(|| anyhow!("inscription {inscription} in output without address"))?,
      )?
      .require_network(wallet.chain().network())?,
      Signer::Output(output) => wallet.chain().address_from_script(
        &wallet
          .utxos()
          .get(output)
          .ok_or_else(|| anyhow!("output {output} has no address"))?
          .script_pubkey,
      )?,
    };

    let message = if let Some(text) = &self.text {
      text.as_bytes()
    } else if let Some(file) = &self.file {
      &fs::read(file)?
    } else {
      unreachable!()
    };

    let to_spend = bip322::create_to_spend(&address, message)?;

    let to_sign = bip322::create_to_sign(&to_spend, None)?;

    let result = wallet.bitcoin_client().sign_raw_transaction_with_wallet(
      &to_sign.extract_tx()?,
      Some(&[bitcoincore_rpc::json::SignRawTransactionInput {
        txid: to_spend.compute_txid(),
        vout: 0,
        script_pub_key: address.script_pubkey(),
        redeem_script: None,
        amount: Some(Amount::ZERO),
      }]),
      None,
    )?;

    let mut buffer = Vec::new();

    Transaction::consensus_decode(&mut result.hex.as_slice())?.input[0]
      .witness
      .consensus_encode(&mut buffer)?;

    Ok(Some(Box::new(Output {
      address: address.as_unchecked().clone(),
      witness: general_purpose::STANDARD.encode(buffer),
    })))
  }
}

ord/src/subcommand/wallet/split.rs


use {super::*, splitfile::Splitfile};

mod splitfile;

#[derive(Debug, PartialEq)]
enum Error {
  DustOutput {
    value: Amount,
    threshold: Amount,
    output: usize,
  },
  DustPostage {
    value: Amount,
    threshold: Amount,
  },
  NoOutputs,
  RunestoneSize {
    size: usize,
  },
  Shortfall {
    rune: SpacedRune,
    have: Pile,
    need: Pile,
  },
  ZeroValue {
    output: usize,
    rune: SpacedRune,
  },
}

impl Display for Error {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Self::DustOutput {
        value,
        threshold,
        output,
      } => write!(
        f,
        "output {output} value {value} below dust threshold {threshold}"
      ),
      Self::DustPostage { value, threshold } => {
        write!(f, "postage value {value} below dust threshold {threshold}")
      }
      Self::NoOutputs => write!(f, "split file must contain at least one output"),
      Self::RunestoneSize { size } => write!(
        f,
        "runestone size {size} over maximum standard OP_RETURN size {MAX_STANDARD_OP_RETURN_SIZE}"
      ),
      Self::Shortfall { rune, have, need } => {
        write!(f, "wallet contains {have} of {rune} but need {need}")
      }
      Self::ZeroValue { output, rune } => {
        write!(f, "output {output} has zero value for rune {rune}")
      }
    }
  }
}

impl std::error::Error for Error {}

#[derive(Debug, Parser)]
pub(crate) struct Split {
  #[arg(long, help = "Don't sign or broadcast transaction")]
  pub(crate) dry_run: bool,
  #[arg(long, help = "Use fee rate of <FEE_RATE> sats/vB")]
  fee_rate: FeeRate,
  #[arg(
    long,
    help = "Include <AMOUNT> postage with change output. [default: 10000 sat]",
    value_name = "AMOUNT"
  )]
  pub(crate) postage: Option<Amount>,
  #[arg(
    long,
    help = "Split outputs multiple inscriptions and rune defined in YAML <SPLIT_FILE>.",
    value_name = "SPLIT_FILE"
  )]
  pub(crate) splits: PathBuf,
  #[arg(
    long,
    alias = "nolimit",
    help = "Allow OP_RETURN greater than 83 bytes. Transactions over this limit are nonstandard \
    and will not be relayed by bitcoind in its default configuration. Do not use this flag unless \
    you understand the implications."
  )]
  pub(crate) no_limit: bool,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Output {
  pub txid: Txid,
  pub psbt: String,
  pub fee: u64,
}

impl Split {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    ensure!(
      wallet.has_rune_index(),
      "`ord wallet split` requires index created with `--index-runes`",
    );

    wallet.lock_non_cardinal_outputs()?;

    let splits = Splitfile::load(&self.splits, &wallet)?;

    let inscribed_outputs = wallet
      .inscriptions()
      .keys()
      .map(|satpoint| satpoint.outpoint)
      .collect::<HashSet<OutPoint>>();

    let balances = wallet
      .get_runic_outputs()?
      .unwrap_or_default()
      .into_iter()
      .filter(|output| !inscribed_outputs.contains(output))
      .map(|output| {
        wallet.get_runes_balances_in_output(&output).map(|balance| {
          (
            output,
            balance
              .unwrap_or_default()
              .into_iter()
              .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount))
              .collect(),
          )
        })
      })
      .collect::<Result<BTreeMap<OutPoint, BTreeMap<Rune, u128>>>>()?;

    let unfunded_transaction = Self::build_transaction(
      self.no_limit,
      balances,
      &wallet.get_change_address()?,
      self.postage,
      &splits,
    )?;

    let unsigned_transaction = fund_raw_transaction(
      wallet.bitcoin_client(),
      self.fee_rate,
      &unfunded_transaction,
    )?;

    let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?;

    let (txid, psbt, fee) =
      wallet.sign_and_broadcast_transaction(unsigned_transaction, self.dry_run, None)?;

    Ok(Some(Box::new(Output { txid, psbt, fee })))
  }

  fn build_transaction(
    no_runestone_limit: bool,
    balances: BTreeMap<OutPoint, BTreeMap<Rune, u128>>,
    change_address: &Address,
    postage: Option<Amount>,
    splits: &Splitfile,
  ) -> Result<Transaction, Error> {
    if splits.outputs.is_empty() {
      return Err(Error::NoOutputs);
    }

    let postage = postage.unwrap_or(TARGET_POSTAGE);

    let change_script_pubkey = change_address.script_pubkey();

    let change_dust_threshold = change_script_pubkey.minimal_non_dust();

    if postage < change_script_pubkey.minimal_non_dust() {
      return Err(Error::DustPostage {
        value: postage,
        threshold: change_dust_threshold,
      });
    }

    let mut input_runes_required = BTreeMap::<Rune, u128>::new();

    for (i, output) in splits.outputs.iter().enumerate() {
      for (&rune, &amount) in &output.runes {
        if amount == 0 {
          return Err(Error::ZeroValue {
            rune: splits.rune_info[&rune].spaced_rune,
            output: i,
          });
        }
        let required = input_runes_required.entry(rune).or_default();
        *required = (*required).checked_add(amount).unwrap();
      }
    }

    let mut input_rune_balances: BTreeMap<Rune, u128> = BTreeMap::new();

    let mut inputs = Vec::new();

    for (output, runes) in balances {
      for (rune, required) in &input_runes_required {
        if input_rune_balances.get(rune).copied().unwrap_or_default() >= *required {
          continue;
        }

        if runes.get(rune).copied().unwrap_or_default() == 0 {
          continue;
        }

        for (rune, balance) in &runes {
          *input_rune_balances.entry(*rune).or_default() += balance;
        }

        inputs.push(output);

        break;
      }
    }

    for (&rune, &need) in &input_runes_required {
      let have = input_rune_balances.get(&rune).copied().unwrap_or_default();
      if have < need {
        let info = splits.rune_info[&rune];
        return Err(Error::Shortfall {
          rune: info.spaced_rune,
          have: Pile {
            amount: have,
            divisibility: info.divisibility,
            symbol: info.symbol,
          },
          need: Pile {
            amount: need,
            divisibility: info.divisibility,
            symbol: info.symbol,
          },
        });
      }
    }

    let mut need_rune_change_output = false;
    for (rune, input) in input_rune_balances {
      if input > input_runes_required.get(&rune).copied().unwrap_or_default() {
        need_rune_change_output = true;
      }
    }

    let mut edicts = Vec::new();

    let base = if need_rune_change_output { 2 } else { 1 };

    for (i, output) in splits.outputs.iter().enumerate() {
      for (rune, amount) in &output.runes {
        edicts.push(Edict {
          id: splits.rune_info.get(rune).unwrap().id,
          amount: *amount,
          output: (i + base).try_into().unwrap(),
        });
      }
    }

    let runestone = Runestone {
      edicts,
      ..default()
    };

    let mut output = Vec::new();

    let runestone_script_pubkey = runestone.encipher();
    let size = runestone_script_pubkey.len();

    if !no_runestone_limit && size > MAX_STANDARD_OP_RETURN_SIZE {
      return Err(Error::RunestoneSize { size });
    }

    output.push(TxOut {
      script_pubkey: runestone_script_pubkey,
      value: Amount::from_sat(0),
    });

    if need_rune_change_output {
      output.push(TxOut {
        script_pubkey: change_script_pubkey,
        value: postage,
      });
    }

    for (i, split_output) in splits.outputs.iter().enumerate() {
      let script_pubkey = split_output.address.script_pubkey();
      let threshold = script_pubkey.minimal_non_dust();
      let value = split_output.value.unwrap_or(threshold);
      if value < threshold {
        return Err(Error::DustOutput {
          output: i,
          threshold,
          value,
        });
      }
      output.push(TxOut {
        script_pubkey,
        value,
      });
    }

    let tx = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: inputs
        .into_iter()
        .map(|previous_output| TxIn {
          previous_output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        })
        .collect(),
      output,
    };

    for output in &tx.output {
      assert!(output.value >= output.script_pubkey.minimal_non_dust());
    }

    assert_eq!(
      Runestone::decipher(&tx),
      Some(Artifact::Runestone(runestone)),
    );

    Ok(tx)
  }
}

#[cfg(test)]
mod tests {
  use {super::*, splitfile::RuneInfo};

  #[test]
  fn splits_must_have_at_least_one_output() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: Vec::new(),
          rune_info: BTreeMap::new(),
        },
      )
      .unwrap_err(),
      Error::NoOutputs,
    );
  }

  #[test]
  fn postage_may_not_be_dust() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        Some(Amount::from_sat(100)),
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: BTreeMap::new(),
        },
      )
      .unwrap_err(),
      Error::DustPostage {
        value: Amount::from_sat(100),
        threshold: Amount::from_sat(294),
      },
    );
  }

  #[test]
  fn output_rune_value_may_not_be_zero() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 0)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 1,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::ZeroValue {
        output: 0,
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 1,
        },
      },
    );

    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 100)].into(),
              value: Some(Amount::from_sat(1000)),
            },
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 0)].into(),
              value: Some(Amount::from_sat(1000)),
            },
          ],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 10,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::ZeroValue {
        output: 1,
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 10,
        },
      },
    );
  }

  #[test]
  fn wallet_must_have_enough_runes() {
    assert_eq!(
      Split::build_transaction(
        false,
        BTreeMap::new(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 10,
              symbol: Some('@'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 2,
              },
            },
          )]
          .into(),
        },
      )
      .unwrap_err(),
      Error::Shortfall {
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 2
        },
        have: Pile {
          amount: 0,
          divisibility: 10,
          symbol: Some('@'),
        },
        need: Pile {
          amount: 1000,
          divisibility: 10,
          symbol: Some('@'),
        },
      },
    );

    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 1000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 2000)].into(),
            value: Some(Amount::from_sat(1000)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 2,
              symbol: Some('x'),
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 1
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::Shortfall {
        rune: SpacedRune {
          rune: Rune(0),
          spacers: 1,
        },
        have: Pile {
          amount: 1000,
          divisibility: 2,
          symbol: Some('x'),
        },
        need: Pile {
          amount: 2000,
          divisibility: 2,
          symbol: Some('x'),
        },
      },
    );
  }

  #[test]
  fn split_output_values_may_not_be_dust() {
    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 1000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![splitfile::Output {
            address: address(0),
            runes: [(Rune(0), 1000)].into(),
            value: Some(Amount::from_sat(1)),
          }],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 0,
              symbol: None,
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 0,
              },
            },
          )]
          .into(),
        },
      )
      .unwrap_err(),
      Error::DustOutput {
        value: Amount::from_sat(1),
        threshold: Amount::from_sat(294),
        output: 0,
      }
    );

    assert_eq!(
      Split::build_transaction(
        false,
        [(outpoint(0), [(Rune(0), 2000)].into())].into(),
        &change(0),
        None,
        &Splitfile {
          outputs: vec![
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 1000)].into(),
              value: Some(Amount::from_sat(1000)),
            },
            splitfile::Output {
              address: address(0),
              runes: [(Rune(0), 1000)].into(),
              value: Some(Amount::from_sat(10)),
            },
          ],
          rune_info: [(
            Rune(0),
            RuneInfo {
              id: RuneId { block: 1, tx: 1 },
              divisibility: 0,
              symbol: None,
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 0,
              },
            },
          )]
          .into()
        },
      )
      .unwrap_err(),
      Error::DustOutput {
        value: Amount::from_sat(10),
        threshold: Amount::from_sat(294),
        output: 1,
      }
    );
  }

  #[test]
  fn one_output_no_change() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_outgoing_rune_with_default_postage() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };
    let change = change(0);

    let balances = [(output, [(rune, 2000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: TARGET_POSTAGE,
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_outgoing_rune_with_non_default_postage() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };
    let change = change(0);

    let balances = [(output, [(rune, 2000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(
      false,
      balances,
      &change,
      Some(Amount::from_sat(500)),
      &splits,
    )
    .unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: Amount::from_sat(500),
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn one_output_with_change_for_non_outgoing_rune() {
    let address = address(0);
    let output = outpoint(0);
    let change = change(0);

    let balances = [(output, [(Rune(0), 1000), (Rune(1), 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(Rune(0), 1000)].into(),
        value: None,
      }],
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change, None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id: rune_id(0),
                amount: 1000,
                output: 2
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: change.into(),
            value: TARGET_POSTAGE,
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn outputs_without_value_use_correct_dust_amount() {
    let address = "bc1p5d7rjq7g6rdk2yhzks9smlaqtedr4dekq08ge8ztwac72sfr9rusxg3297"
      .parse::<Address<NetworkUnchecked>>()
      .unwrap()
      .assume_checked();
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(330),
          }
        ],
      },
    );
  }

  #[test]
  fn excessive_inputs_are_not_selected() {
    let address = address(0);
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [
      (output, [(rune, 1000)].into()),
      (outpoint(1), [(rune, 1000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 1000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 1000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn multiple_inputs_may_be_selected() {
    let address = address(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [
      (outpoint(0), [(rune, 1000)].into()),
      (outpoint(1), [(rune, 1000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(rune, 2000)].into(),
        value: None,
      }],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![
          TxIn {
            previous_output: outpoint(0),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
          TxIn {
            previous_output: outpoint(1),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
        ],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![Edict {
                id,
                amount: 2000,
                output: 1
              }],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn two_outputs_no_change() {
    let output = outpoint(0);
    let rune = Rune(0);
    let id = RuneId { block: 1, tx: 1 };

    let balances = [(output, [(rune, 1000)].into())].into();

    let splits = Splitfile {
      outputs: vec![
        splitfile::Output {
          address: address(0),
          runes: [(rune, 500)].into(),
          value: None,
        },
        splitfile::Output {
          address: address(1),
          runes: [(rune, 500)].into(),
          value: None,
        },
      ],
      rune_info: [(
        rune,
        RuneInfo {
          id,
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: output,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![
                Edict {
                  id,
                  amount: 500,
                  output: 1
                },
                Edict {
                  id,
                  amount: 500,
                  output: 2
                }
              ],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address(0).into(),
            value: Amount::from_sat(294),
          },
          TxOut {
            script_pubkey: address(1).into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn outputs_may_receive_multiple_runes() {
    let address = address(0);

    let balances = [
      (outpoint(0), [(Rune(0), 1000)].into()),
      (outpoint(1), [(Rune(1), 2000)].into()),
    ]
    .into();

    let splits = Splitfile {
      outputs: vec![splitfile::Output {
        address: address.clone(),
        runes: [(Rune(0), 1000), (Rune(1), 2000)].into(),
        value: None,
      }],
      rune_info: [
        (
          Rune(0),
          RuneInfo {
            id: rune_id(0),
            divisibility: 0,
            symbol: None,
            spaced_rune: SpacedRune {
              rune: Rune(0),
              spacers: 0,
            },
          },
        ),
        (
          Rune(1),
          RuneInfo {
            id: rune_id(1),
            divisibility: 0,
            symbol: None,
            spaced_rune: SpacedRune {
              rune: Rune(1),
              spacers: 0,
            },
          },
        ),
      ]
      .into(),
    };

    let tx = Split::build_transaction(false, balances, &change(0), None, &splits).unwrap();

    pretty_assert_eq!(
      tx,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![
          TxIn {
            previous_output: outpoint(0),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
          TxIn {
            previous_output: outpoint(1),
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          },
        ],
        output: vec![
          TxOut {
            value: Amount::from_sat(0),
            script_pubkey: Runestone {
              edicts: vec![
                Edict {
                  id: rune_id(0),
                  amount: 1000,
                  output: 1
                },
                Edict {
                  id: rune_id(1),
                  amount: 2000,
                  output: 1
                },
              ],
              etching: None,
              mint: None,
              pointer: None,
            }
            .encipher()
          },
          TxOut {
            script_pubkey: address.into(),
            value: Amount::from_sat(294),
          }
        ],
      },
    );
  }

  #[test]
  fn oversize_op_return_is_an_error() {
    let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into();

    let splits = Splitfile {
      outputs: (0..10)
        .map(|i| splitfile::Output {
          address: address(i).clone(),
          runes: [(Rune(0), 1_000_000_000)].into(),
          value: None,
        })
        .collect(),
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    assert_eq!(
      Split::build_transaction(false, balances, &change(0), None, &splits).unwrap_err(),
      Error::RunestoneSize { size: 85 },
    );
  }

  #[test]
  fn oversize_op_return_is_allowed_with_flag() {
    let balances = [(outpoint(0), [(Rune(0), 10_000_000_000)].into())].into();

    let splits = Splitfile {
      outputs: (0..10)
        .map(|i| splitfile::Output {
          address: address(i).clone(),
          runes: [(Rune(0), 1_000_000_000)].into(),
          value: None,
        })
        .collect(),
      rune_info: [(
        Rune(0),
        RuneInfo {
          id: rune_id(0),
          divisibility: 0,
          symbol: None,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0,
          },
        },
      )]
      .into(),
    };

    pretty_assert_eq!(
      Split::build_transaction(true, balances, &change(0), None, &splits).unwrap(),
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![TxIn {
          previous_output: outpoint(0),
          script_sig: ScriptBuf::new(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }],
        output: (0..11)
          .map(|i| if i == 0 {
            TxOut {
              value: Amount::from_sat(0),
              script_pubkey: Runestone {
                edicts: (0..10)
                  .map(|i| Edict {
                    id: rune_id(0),
                    amount: 1_000_000_000,
                    output: i + 1,
                  })
                  .collect(),
                etching: None,
                mint: None,
                pointer: None,
              }
              .encipher(),
            }
          } else {
            TxOut {
              script_pubkey: address(i - 1).into(),
              value: Amount::from_sat(294),
            }
          })
          .collect()
      }
    );
  }
}

ord/src/subcommand/wallet/split/splitfile.rs


use super::*;

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SplitfileUnchecked {
  outputs: Vec<OutputUnchecked>,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct OutputUnchecked {
  address: Address<NetworkUnchecked>,
  value: Option<DeserializeFromStr<Amount>>,
  runes: BTreeMap<SpacedRune, Decimal>,
}

pub(crate) struct Splitfile {
  pub(crate) outputs: Vec<Output>,
  pub(crate) rune_info: BTreeMap<Rune, RuneInfo>,
}

pub(crate) struct Output {
  pub(crate) address: Address,
  pub(crate) value: Option<Amount>,
  pub(crate) runes: BTreeMap<Rune, u128>,
}

#[derive(Clone, Copy)]
pub(crate) struct RuneInfo {
  pub(crate) divisibility: u8,
  pub(crate) id: RuneId,
  pub(crate) spaced_rune: SpacedRune,
  pub(crate) symbol: Option<char>,
}

impl Splitfile {
  pub(crate) fn load(path: &Path, wallet: &Wallet) -> Result<Self> {
    let network = wallet.chain().network();

    let unchecked = Self::load_unchecked(path)?;

    let mut rune_info = BTreeMap::<Rune, RuneInfo>::new();

    let mut outputs = Vec::new();

    for output in unchecked.outputs {
      let mut runes = BTreeMap::new();

      for (spaced_rune, decimal) in output.runes {
        let info = if let Some(info) = rune_info.get(&spaced_rune.rune) {
          info
        } else {
          let (id, entry, _parent) = wallet
            .get_rune(spaced_rune.rune)?
            .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;
          rune_info.insert(
            spaced_rune.rune,
            RuneInfo {
              divisibility: entry.divisibility,
              id,
              spaced_rune: entry.spaced_rune,
              symbol: entry.symbol,
            },
          );
          rune_info.get(&spaced_rune.rune).unwrap()
        };

        let amount = decimal.to_integer(info.divisibility)?;

        runes.insert(spaced_rune.rune, amount);
      }

      outputs.push(Output {
        address: output.address.require_network(network)?,
        value: output.value.map(|DeserializeFromStr(value)| value),
        runes,
      });
    }

    Ok(Self { outputs, rune_info })
  }

  fn load_unchecked(path: &Path) -> Result<SplitfileUnchecked> {
    Ok(serde_yaml::from_reader(File::open(path)?)?)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn example_split_file_is_valid() {
    Splitfile::load_unchecked("splits.yaml".as_ref()).unwrap();
  }
}

ord/src/subcommand/wallet/transactions.rs


use super::*;

#[derive(Debug, Parser)]
pub(crate) struct Transactions {
  #[arg(long, help = "Fetch at most <LIMIT> transactions.")]
  limit: Option<u16>,
}

#[derive(Serialize, Deserialize)]
pub struct Output {
  pub transaction: Txid,
  pub confirmations: i32,
}

impl Transactions {
  pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult {
    let client = wallet.bitcoin_client();

    let mut output = Vec::new();
    for tx in client.list_transactions(
      None,
      Some(self.limit.unwrap_or(u16::MAX).into()),
      None,
      None,
    )? {
      output.push(Output {
        transaction: tx.info.txid,
        confirmations: tx.info.confirmations,
      });
    }

    Ok(Some(Box::new(output)))
  }
}

ord/src/subcommand/wallets.rs


use super::*;

pub(crate) fn run(settings: Settings) -> SubcommandResult {
  Ok(Some(Box::new(
    settings.bitcoin_rpc_client(None)?.list_wallet_dir()?,
  )))
}

ord/src/tally.rs


use super::*;

pub(crate) trait Tally {
  fn tally(self, count: usize) -> Tallied;
}

impl Tally for &'static str {
  fn tally(self, count: usize) -> Tallied {
    Tallied { noun: self, count }
  }
}

pub(crate) struct Tallied {
  count: usize,
  noun: &'static str,
}

impl Display for Tallied {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    if self.count == 1 {
      write!(f, "{} {}", self.count, self.noun)
    } else {
      write!(f, "{} {}s", self.count, self.noun)
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn zero() {
    assert_eq!("foo".tally(0).to_string(), "0 foos")
  }

  #[test]
  fn one() {
    assert_eq!("foo".tally(1).to_string(), "1 foo")
  }

  #[test]
  fn two() {
    assert_eq!("foo".tally(2).to_string(), "2 foos")
  }
}

ord/src/templates.rs


use {super::*, boilerplate::Boilerplate};

pub(crate) use {
  crate::subcommand::server::ServerConfig,
  address::AddressHtml,
  block::BlockHtml,
  children::ChildrenHtml,
  clock::ClockSvg,
  collections::CollectionsHtml,
  home::HomeHtml,
  iframe::Iframe,
  input::InputHtml,
  inscriptions::InscriptionsHtml,
  inscriptions_block::InscriptionsBlockHtml,
  metadata::MetadataHtml,
  output::OutputHtml,
  parents::ParentsHtml,
  preview::{
    PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml,
    PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml,
  },
  rare::RareTxt,
  rune_not_found::RuneNotFoundHtml,
  sat::SatHtml,
  satscard::SatscardHtml,
};

pub use {
  blocks::BlocksHtml, inscription::InscriptionHtml, rune::RuneHtml, runes::RunesHtml,
  status::StatusHtml, transaction::TransactionHtml,
};

pub mod address;
pub mod block;
pub mod blocks;
mod children;
mod clock;
pub mod collections;
mod home;
mod iframe;
mod input;
pub mod inscription;
pub mod inscriptions;
mod inscriptions_block;
mod metadata;
pub mod output;
mod parents;
mod preview;
mod rare;
pub mod rune;
pub mod rune_not_found;
pub mod runes;
pub mod sat;
mod satscard;
pub mod status;
pub mod transaction;

#[derive(Boilerplate)]
pub struct PageHtml<T: PageContent> {
  content: T,
  config: Arc<ServerConfig>,
}

impl<T> PageHtml<T>
where
  T: PageContent,
{
  pub fn new(content: T, config: Arc<ServerConfig>) -> Self {
    Self { content, config }
  }

  fn og_image(&self) -> String {
    if let Some(domain) = &self.config.domain {
      format!("https://{domain}/static/favicon.png")
    } else {
      "https://ordinals.com/static/favicon.png".into()
    }
  }

  fn superscript(&self) -> String {
    if self.config.chain == Chain::Mainnet {
      "beta".into()
    } else {
      self.config.chain.to_string()
    }
  }
}

pub trait PageContent: Display + 'static {
  fn title(&self) -> String;

  fn page(self, server_config: Arc<ServerConfig>) -> PageHtml<Self>
  where
    Self: Sized,
  {
    PageHtml::new(self, server_config)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  struct Foo;

  impl Display for Foo {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
      write!(f, "<h1>Foo</h1>")
    }
  }

  impl PageContent for Foo {
    fn title(&self) -> String {
      "Foo".to_string()
    }
  }

  #[test]
  fn page() {
    assert_regex_match!(
      Foo.page(Arc::new(ServerConfig {
        chain: Chain::Mainnet,
        csp_origin: Some("https://signet.ordinals.com".into()),
        domain: Some("signet.ordinals.com".into()),
        index_sats: true,
        ..default()
      }),),
      r"<!doctype html>
<html lang=en>
  <head>
    <meta charset=utf-8>
    <meta name=format-detection content='telephone=no'>
    <meta name=viewport content='width=device-width,initial-scale=1.0'>
    <meta property=og:title content='Foo'>
    <meta property=og:image content='https://signet.ordinals.com/static/favicon.png'>
    <meta property=twitter:card content=summary>
    <title>Foo</title>
    <link rel=alternate href=/feed.xml type=application/rss\+xml title='Inscription Feed'>
    <link rel=icon href=/static/favicon.png>
    <link rel=icon href=/static/favicon.svg>
    <link rel=stylesheet href=/static/index.css>
    <link rel=stylesheet href=/static/modern-normalize.css>
    <script src=/static/index.js></script>
  </head>
  <body>
  <header>
    <nav>
      <a href=/ title=home>Ordinals<sup>beta</sup></a>
      .*
      <a href=/clock title=clock>.*</a>
      <a href=/rare.txt title=rare>.*</a>
      .*
      <form action=/search method=get>
        <input type=text .*>
        <input class=icon type=image .*>
      </form>
    </nav>
  </header>
  <main>
<h1>Foo</h1>
  </main>
  </body>
</html>
"
    );
  }

  #[test]
  fn page_mainnet() {
    assert_regex_match!(
      Foo.page(Arc::new(ServerConfig {
        chain: Chain::Mainnet,
        csp_origin: None,
        domain: None,
        index_sats: true,
        ..default()
      })),
      r".*<nav>\s*<a href=/ title=home>Ordinals<sup>beta</sup></a>.*"
    );
  }

  #[test]
  fn page_no_sat_index() {
    assert_regex_match!(
      Foo.page(Arc::new(ServerConfig {
        chain: Chain::Mainnet,
        csp_origin: None,
        domain: None,
        index_sats: false,
        ..default()
      })),
      r".*<nav>\s*<a href=/ title=home>Ordinals<sup>beta</sup></a>.*<a href=/clock title=clock>.*</a>\s*<form action=/search.*",
    );
  }

  #[test]
  fn page_signet() {
    assert_regex_match!(
      Foo.page(Arc::new(ServerConfig {
        chain: Chain::Signet,
        csp_origin: None,
        domain: None,
        index_sats: true,
        ..default()
      })),
      r".*<nav>\s*<a href=/ title=home>Ordinals<sup>signet</sup></a>.*"
    );
  }
}

ord/src/templates/address.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct AddressHtml {
  pub(crate) address: Address,
  pub(crate) header: bool,
  pub(crate) inscriptions: Option<Vec<InscriptionId>>,
  pub(crate) outputs: Vec<OutPoint>,
  pub(crate) runes_balances: Option<Vec<(SpacedRune, Decimal, Option<char>)>>,
  pub(crate) sat_balance: u64,
}

impl PageContent for AddressHtml {
  fn title(&self) -> String {
    format!("Address {}", self.address)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  fn setup() -> AddressHtml {
    AddressHtml {
      address: Address::from_str("bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8")
        .unwrap()
        .require_network(Network::Bitcoin)
        .unwrap(),
      header: true,
      outputs: vec![outpoint(1), outpoint(2)],
      inscriptions: Some(vec![inscription_id(1)]),
      sat_balance: 99,
      runes_balances: Some(vec![
        (
          SpacedRune {
            rune: Rune::from_str("TEEEEEEEEESTRUNE").unwrap(),
            spacers: 0,
          },
          Decimal {
            scale: 0,
            value: 20000,
          },
          Some('R'),
        ),
        (
          SpacedRune {
            rune: Rune::from_str("ANOTHERTEESTRUNE").unwrap(),
            spacers: 0,
          },
          Decimal {
            scale: 0,
            value: 10000,
          },
          Some('F'),
        ),
      ]),
    }
  }

  #[test]
  fn test_address_rendering() {
    let address_html = setup();
    let expected_pattern =
      r#".*<h1>Address bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8</h1>.*"#;
    assert_regex_match!(address_html, expected_pattern);
  }

  #[test]
  fn test_sat_balance_rendering() {
    let address_html = setup();
    let expected_pattern = r#".*<dt>sat balance</dt>\n\s*<dd>99</dd>.*"#;
    assert_regex_match!(address_html, expected_pattern);
  }

  #[test]
  fn test_inscriptions_rendering() {
    let address_html = setup();
    let expected_pattern = r#".*<dt>inscriptions</dt>\n\s*<dd class=thumbnails>.*<a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>.*</dd>.*"#;
    assert_regex_match!(address_html, expected_pattern);
  }

  #[test]
  fn test_runes_balances_rendering() {
    let address_html = setup();
    let expected_pattern = r#".*<dt>rune balances</dt>\n\s*<dd><a class=monospace href=/rune/TEEEEEEEEESTRUNE>TEEEEEEEEESTRUNE</a>: 20000R</dd>\n\s*<dd><a class=monospace href=/rune/ANOTHERTEESTRUNE>ANOTHERTEESTRUNE</a>: 10000F</dd>.*"#;
    assert_regex_match!(address_html, expected_pattern);
  }

  #[test]
  fn test_outputs_rendering() {
    let address_html = setup();
    let expected_pattern = r#".*<dt>outputs</dt>\n\s*<dd>\n\s*<ul>\n\s*<li><a class=collapse href=/output/1{64}:1>1{64}:1</a></li>\n\s*<li><a class=collapse href=/output/2{64}:2>2{64}:2</a></li>\n\s*</ul>\n\s*</dd>.*"#;
    assert_regex_match!(address_html, expected_pattern);
  }
}

ord/src/templates/block.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct BlockHtml {
  best_height: Height,
  block: Block,
  featured_inscriptions: Vec<InscriptionId>,
  hash: BlockHash,
  height: Height,
  inscription_count: usize,
  runes: Vec<SpacedRune>,
  target: BlockHash,
}

impl BlockHtml {
  pub(crate) fn new(
    block: Block,
    height: Height,
    best_height: Height,
    inscription_count: usize,
    featured_inscriptions: Vec<InscriptionId>,
    runes: Vec<SpacedRune>,
  ) -> Self {
    Self {
      hash: block.header.block_hash(),
      target: target_as_block_hash(block.header.target()),
      block,
      height,
      best_height,
      inscription_count,
      featured_inscriptions,
      runes,
    }
  }
}

impl PageContent for BlockHtml {
  fn title(&self) -> String {
    format!("Block {}", self.height)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn html() {
    assert_regex_match!(
      BlockHtml::new(
        Chain::Mainnet.genesis_block(),
        Height(0),
        Height(0),
        0,
        Vec::new(),
        Vec::new()
      ),
      "
        <h1>Block 0</h1>
        <dl>
          <dt>hash</dt><dd class=collapse>[[:xdigit:]]{64}</dd>
          <dt>target</dt><dd class=collapse>[[:xdigit:]]{64}</dd>
          <dt>timestamp</dt><dd><time>2009-01-03 18:15:05 UTC</time></dd>
          <dt>size</dt><dd>285</dd>
          <dt>weight</dt><dd>1140</dd>
        </dl>
        .*
        prev
        next
        .*
        <h2>0 Runes</h2>
        <h2>0 Inscriptions</h2>
        <div class=thumbnails>
        </div>
        <h2>1 Transaction</h2>
        <ul>
          <li><a class=collapse href=/tx/[[:xdigit:]]{64}>[[:xdigit:]]{64}</a></li>
        </ul>
      "
      .unindent()
    );
  }

  #[test]
  fn next_active_when_not_last() {
    assert_regex_match!(
      BlockHtml::new(
        Chain::Mainnet.genesis_block(),
        Height(0),
        Height(1),
        0,
        Vec::new(),
        Vec::new()
      ),
      r"<h1>Block 0</h1>.*prev\s*<a class=next href=/block/1>next</a>.*"
    );
  }

  #[test]
  fn prev_active_when_not_first() {
    assert_regex_match!(
      BlockHtml::new(
        Chain::Mainnet.genesis_block(),
        Height(1),
        Height(1),
        0,
        Vec::new(),
        Vec::new()
      ),
      r"<h1>Block 1</h1>.*<a class=prev href=/block/0>prev</a>\s*next.*",
    );
  }

  #[test]
  fn block_hash_serializes_as_hex_string() {
    assert_eq!(
      serde_json::to_string(&BlockHash::all_zeros()).unwrap(),
      "\"0000000000000000000000000000000000000000000000000000000000000000\""
    );
  }
}

ord/src/templates/blocks.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlocksHtml {
  pub last: u32,
  pub blocks: Vec<BlockHash>,
  pub featured_blocks: BTreeMap<BlockHash, Vec<InscriptionId>>,
}

impl BlocksHtml {
  pub(crate) fn new(
    blocks: Vec<(u32, BlockHash)>,
    featured_blocks: BTreeMap<BlockHash, Vec<InscriptionId>>,
  ) -> Self {
    Self {
      last: blocks
        .first()
        .map(|(height, _)| height)
        .cloned()
        .unwrap_or(0),
      blocks: blocks.into_iter().map(|(_, hash)| hash).collect(),
      featured_blocks,
    }
  }
}

impl PageContent for BlocksHtml {
  fn title(&self) -> String {
    "Blocks".to_string()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn html() {
    let mut feature_blocks = BTreeMap::new();
    feature_blocks.insert(
      "2222222222222222222222222222222222222222222222222222222222222222"
        .parse()
        .unwrap(),
      vec![inscription_id(1), inscription_id(2)],
    );

    assert_regex_match!(
      &BlocksHtml::new(
        vec![
          (
            1260002,
            "2222222222222222222222222222222222222222222222222222222222222222"
              .parse()
              .unwrap()
          ),
          (
            1260001,
            "1111111111111111111111111111111111111111111111111111111111111111"
              .parse()
              .unwrap()
          ),
          (
            1260000,
            "0000000000000000000000000000000000000000000000000000000000000000"
              .parse()
              .unwrap()
          )
        ],
        feature_blocks,
      )
      .to_string()
      .unindent(),
      "<h1>Blocks</h1>
      <div class=block>
        <h2><a href=/block/1260002>Block 1260002</a></h2>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
      </div>
      <ol start=1260001 reversed class=block-list>
        <li><a class=collapse href=/block/1{64}>1{64}</a></li>
        <li><a class=collapse href=/block/0{64}>0{64}</a></li>
      </ol>
      "
      .unindent(),
    );
  }
}

ord/src/templates/children.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct ChildrenHtml {
  pub(crate) parent: InscriptionId,
  pub(crate) parent_number: i32,
  pub(crate) children: Vec<InscriptionId>,
  pub(crate) prev_page: Option<usize>,
  pub(crate) next_page: Option<usize>,
}

impl PageContent for ChildrenHtml {
  fn title(&self) -> String {
    format!("Inscription {} Children", self.parent_number)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_prev_and_next() {
    assert_regex_match!(
      ChildrenHtml {
        parent: inscription_id(1),
        parent_number: 0,
        children: vec![inscription_id(2), inscription_id(3)],
        prev_page: None,
        next_page: None,
      },
      "
        <h1><a href=/inscription/1{64}i1>Inscription 0</a> Children</h1>
        <div class=thumbnails>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
          <a href=/inscription/3{64}i3><iframe .* src=/preview/3{64}i3></iframe></a>
        </div>
        .*
        prev
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      ChildrenHtml {
        parent: inscription_id(1),
        parent_number: 0,
        children: vec![inscription_id(2), inscription_id(3)],
        next_page: Some(3),
        prev_page: Some(1),
      },
      "
        <h1><a href=/inscription/1{64}i1>Inscription 0</a> Children</h1>
        <div class=thumbnails>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
          <a href=/inscription/3{64}i3><iframe .* src=/preview/3{64}i3></iframe></a>
        </div>
        .*
          <a class=prev href=/children/1{64}i1/1>prev</a>
          <a class=next href=/children/1{64}i1/3>next</a>
        .*
      "
      .unindent()
    );
  }
}

ord/src/templates/clock.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct ClockSvg {
  height: Height,
  hour: f64,
  minute: f64,
  second: f64,
}

impl ClockSvg {
  pub(crate) fn new(height: Height) -> Self {
    let min = height.min(Epoch::FIRST_POST_SUBSIDY.starting_height());

    Self {
      height,
      hour: f64::from(min.n() % Epoch::FIRST_POST_SUBSIDY.starting_height().n())
        / f64::from(Epoch::FIRST_POST_SUBSIDY.starting_height().n())
        * 360.0,
      minute: f64::from(min.n() % SUBSIDY_HALVING_INTERVAL) / f64::from(SUBSIDY_HALVING_INTERVAL)
        * 360.0,
      second: f64::from(height.period_offset()) / f64::from(DIFFCHANGE_INTERVAL) * 360.0,
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn second() {
    pretty_assert_eq!(ClockSvg::new(Height(0)).second, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(504)).second, 90.0);
    pretty_assert_eq!(ClockSvg::new(Height(1008)).second, 180.0);
    pretty_assert_eq!(ClockSvg::new(Height(1512)).second, 270.0);
    pretty_assert_eq!(ClockSvg::new(Height(2016)).second, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).second, 180.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930504)).second, 270.0);
  }

  #[test]
  fn minute() {
    pretty_assert_eq!(ClockSvg::new(Height(0)).minute, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(52500)).minute, 90.0);
    pretty_assert_eq!(ClockSvg::new(Height(105000)).minute, 180.0);
    pretty_assert_eq!(ClockSvg::new(Height(157500)).minute, 270.0);
    pretty_assert_eq!(ClockSvg::new(Height(210000)).minute, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).minute, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930001)).minute, 0.0);
  }

  #[test]
  fn hour() {
    pretty_assert_eq!(ClockSvg::new(Height(0)).hour, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(1732500)).hour, 90.0);
    pretty_assert_eq!(ClockSvg::new(Height(3465000)).hour, 180.0);
    pretty_assert_eq!(ClockSvg::new(Height(5197500)).hour, 270.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).hour, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930001)).hour, 0.0);
  }

  #[test]
  fn final_subsidy_height() {
    pretty_assert_eq!(
      ClockSvg::new(Height(6929999)).second,
      1007.0 / 2016.0 * 360.0
    );
    pretty_assert_eq!(
      ClockSvg::new(Height(6929999)).minute,
      209_999.0 / 210_000.0 * 360.0
    );
    pretty_assert_eq!(
      ClockSvg::new(Height(6929999)).hour,
      6929999.0 / 6930000.0 * 360.0
    );
  }

  #[test]
  fn first_post_subsidy_height() {
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).second, 180.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).minute, 0.0);
    pretty_assert_eq!(ClockSvg::new(Height(6930000)).hour, 0.0);
  }

  #[test]
  fn clock_svg() {
    assert_regex_match!(
      ClockSvg::new(Height(6929999)).to_string(),
      r##"<\?xml version="1.0" encoding="UTF-8"\?>
<svg.*>.*
  <text.*>6929999</text>.*
  <line y2="-9" transform="rotate\(359.9999480519481\)"><title>Subsidy</title></line>.*
  <line y2="-13" stroke-width="0.6" transform="rotate\(359.9982857142857\)"><title>Epoch</title></line>.*
  <line y2="-16" stroke="#d00505" stroke-width="0.2" transform="rotate\(179.82142857142858\)"><title>Period</title></line>.*
  <circle r="0.7" stroke="#d00505" stroke-width="0.3"/>.*
</svg>
"##,
    );
  }
}

ord/src/templates/collections.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct CollectionsHtml {
  pub(crate) inscriptions: Vec<InscriptionId>,
  pub(crate) prev: Option<usize>,
  pub(crate) next: Option<usize>,
}

impl PageContent for CollectionsHtml {
  fn title(&self) -> String {
    "Collections".into()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_prev_and_next() {
    assert_regex_match!(
      CollectionsHtml {
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev: None,
        next: None,
      },
      "
        <h1>Collections</h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
        prev
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      CollectionsHtml {
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev: Some(1),
        next: Some(2),
      },
      "
        <h1>Collections</h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
        <a class=prev href=/collections/1>prev</a>
        <a class=next href=/collections/2>next</a>
        .*
      "
      .unindent()
    );
  }
}

ord/src/templates/home.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct HomeHtml {
  pub(crate) inscriptions: Vec<InscriptionId>,
}

impl PageContent for HomeHtml {
  fn title(&self) -> String {
    "Ordinals".to_string()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn html() {
    assert_regex_match!(
      HomeHtml {
        inscriptions: vec![inscription_id(1), inscription_id(2)],
      }
      .to_string()
      .unindent(),
      "<h1>Latest Inscriptions</h1>
      <div class=thumbnails>
        <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
        <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
      </div>
      "
      .unindent(),
    );
  }
}

ord/src/templates/iframe.rs


use super::*;

pub(crate) struct Iframe {
  inscription_id: InscriptionId,
  thumbnail: bool,
}

impl Iframe {
  pub(crate) fn thumbnail(inscription_id: InscriptionId) -> Trusted<Self> {
    Trusted(Self {
      inscription_id,
      thumbnail: true,
    })
  }

  pub(crate) fn main(inscription_id: InscriptionId) -> Trusted<Self> {
    Trusted(Self {
      inscription_id,
      thumbnail: false,
    })
  }
}

impl Display for Iframe {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    if self.thumbnail {
      write!(
        f,
        "<a href=/inscription/{}>\
          <iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/{}>\
          </iframe>\
        </a>",
        self.inscription_id, self.inscription_id,
      )
    } else {
      write!(
        f,
        "<iframe sandbox=allow-scripts loading=lazy src=/preview/{}></iframe>",
        self.inscription_id,
      )
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn thumbnail() {
    assert_regex_match!(
      Iframe::thumbnail(inscription_id(1))
      .0.to_string(),
      "<a href=/inscription/1{64}i1><iframe sandbox=allow-scripts scrolling=no loading=lazy src=/preview/1{64}i1></iframe></a>",
    );
  }

  #[test]
  fn main() {
    assert_regex_match!(
      Iframe::main(inscription_id(1)).0.to_string(),
      "<iframe sandbox=allow-scripts loading=lazy src=/preview/1{64}i1></iframe>",
    );
  }
}

ord/src/templates/input.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct InputHtml {
  pub(crate) path: (u32, usize, usize),
  pub(crate) input: TxIn,
}

impl PageContent for InputHtml {
  fn title(&self) -> String {
    format!("Input /{}/{}/{}", self.path.0, self.path.1, self.path.2)
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    bitcoin::{blockdata::script, Witness},
  };

  #[test]
  fn input_html() {
    let mut witness = Witness::new();
    witness.push([1]);
    pretty_assert_eq!(
      InputHtml {
        path: (1, 2, 3),
        input: TxIn {
          previous_output: "0000000000000000000000000000000000000000000000000000000000000000:0"
            .parse()
            .unwrap(),
          script_sig: ScriptBuf::builder().push_slice(b"foo").into_script(),
          sequence: Sequence::MAX,
          witness,
        }
      }
      .to_string(),
      "
      <h1>Input /1/2/3</h1>
      <dl>
        <dt>previous output</dt><dd class=collapse>0000000000000000000000000000000000000000000000000000000000000000:0</dd>
        <dt>witness</dt><dd class=monospace>010101</dd>
        <dt>script sig</dt><dd class=monospace>OP_PUSHBYTES_3 666f6f</dd>
        <dt>text</dt><dd>\x03foo</dd>
      </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn skip_empty_items() {
    pretty_assert_eq!(
      InputHtml {
        path: (1, 2, 3),
        input: TxIn {
          previous_output: OutPoint::null(),
          script_sig: script::Builder::new().into_script(),
          sequence: Sequence::MAX,
          witness: Witness::new(),
        }
      }
      .to_string(),
      "
      <h1>Input /1/2/3</h1>
      <dl>
      </dl>
      "
      .unindent()
    );
  }
}

ord/src/templates/inscription.rs


use super::*;

#[derive(Boilerplate, Default)]
pub struct InscriptionHtml {
  pub chain: Chain,
  pub charms: u16,
  pub child_count: u64,
  pub children: Vec<InscriptionId>,
  pub fee: u64,
  pub height: u32,
  pub inscription: Inscription,
  pub id: InscriptionId,
  pub number: i32,
  pub next: Option<InscriptionId>,
  pub output: Option<TxOut>,
  pub parents: Vec<InscriptionId>,
  pub previous: Option<InscriptionId>,
  pub rune: Option<SpacedRune>,
  pub sat: Option<Sat>,
  pub satpoint: SatPoint,
  pub timestamp: DateTime<Utc>,
}

impl PageContent for InscriptionHtml {
  fn title(&self) -> String {
    format!("Inscription {}", self.number)
  }
}

impl InscriptionHtml {
  pub fn burn_metadata(&self) -> Option<Value> {
    let script_pubkey = &self.output.as_ref()?.script_pubkey;

    if !script_pubkey.is_op_return() {
      return None;
    }

    let script::Instruction::PushBytes(metadata) = script_pubkey.instructions().nth(1)?.ok()?
    else {
      return None;
    };

    ciborium::from_reader(Cursor::new(metadata)).ok()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_sat_nav_links_or_output() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        <div class=inscription>
        <div>❮</div>
        <iframe .* src=/preview/1{64}i1></iframe>
        <div>❯</div>
        </div>
        <dl>
          <dt>id</dt>
          <dd class=collapse>1{64}i1</dd>
          <dt>preview</dt>
          <dd><a href=/preview/1{64}i1>link</a></dd>
          <dt>content</dt>
          <dd><a href=/content/1{64}i1>link</a></dd>
          <dt>content length</dt>
          <dd>10 bytes</dd>
          <dt>content type</dt>
          <dd>text/plain;charset=utf-8</dd>
          <dt>timestamp</dt>
          <dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>height</dt>
          <dd><a href=/block/0>0</a></dd>
          <dt>fee</dt>
          <dd>1</dd>
          <dt>reveal transaction</dt>
          <dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>location</dt>
          <dd><a class=collapse href=/satpoint/1{64}:1:0>1{64}:1:0</a></dd>
          <dt>output</dt>
          <dd><a class=collapse href=/output/1{64}:1>1{64}:1</a></dd>
          <dt>offset</dt>
          <dd>0</dd>
          <dt>ethereum teleburn address</dt>
          <dd class=collapse>0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059</dd>
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_output() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        output: Some(tx_out(1, address(0))),
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        .*<h1>Inscription 1</h1>
        <div class=inscription>
        <div>❮</div>
        <iframe .* src=/preview/1{64}i1></iframe>
        <div>❯</div>
        </div>
        <dl>
          .*
          <dt>address</dt>
          <dd><a class=collapse href=/address/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4>bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4</a></dd>
          <dt>value</dt>
          <dd>1</dd>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_sat() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        output: Some(tx_out(1, address(0))),
        sat: Some(Sat(1)),
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        .*
        <dl>
          .*
          <dt>sat</dt>
          <dd><a href=/sat/1>1</a></dd>
          <dt>sat name</dt>
          <dd><a href=/sat/nvtdijuwxlo>nvtdijuwxlo</a></dd>
          <dt>preview</dt>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      InscriptionHtml {
        children: Vec::new(),
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(2),
        next: Some(inscription_id(3)),
        number: 1,
        output: Some(tx_out(1, address(0))),
        previous: Some(inscription_id(1)),
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        <div class=inscription>
        <a class=prev href=/inscription/1{64}i1>❮</a>
        <iframe .* src=/preview/2{64}i2></iframe>
        <a class=next href=/inscription/3{64}i3>❯</a>
        </div>
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_cursed_and_unbound() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(2),
        number: -1,
        output: Some(tx_out(1, address(0))),
        satpoint: SatPoint {
          outpoint: unbound_outpoint(),
          offset: 0
        },
        timestamp: timestamp(0),
        ..default()
      },
      "
        <h1>Inscription -1</h1>
        .*
        <dl>
          .*
          <dt>location</dt>
          <dd><a class=collapse href=/satpoint/0{64}:0:0>0{64}:0:0</a></dd>
          <dt>output</dt>
          <dd><a class=collapse href=/output/0{64}:0>0{64}:0</a></dd>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_parent() {
    assert_regex_match!(
      InscriptionHtml {
        parents: vec![inscription_id(2)],
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        <div class=inscription>
        <div>❮</div>
        <iframe .* src=/preview/1{64}i1></iframe>
        <div>❯</div>
        </div>
        <dl>
          <dt>parents</dt>
          <dd>
            <div class=thumbnails>
              <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
            </div>
            <div class=center>
              <a href=/parents/1{64}i1>all</a>
            </div>
          </dd>
          <dt>id</dt>
          <dd class=collapse>1{64}i1</dd>
          <dt>preview</dt>
          <dd><a href=/preview/1{64}i1>link</a></dd>
          <dt>content</dt>
          <dd><a href=/content/1{64}i1>link</a></dd>
          <dt>content length</dt>
          <dd>10 bytes</dd>
          <dt>content type</dt>
          <dd>text/plain;charset=utf-8</dd>
          <dt>timestamp</dt>
          <dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>height</dt>
          <dd><a href=/block/0>0</a></dd>
          <dt>fee</dt>
          <dd>1</dd>
          <dt>reveal transaction</dt>
          <dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>location</dt>
          <dd><a class=collapse href=/satpoint/1{64}:1:0>1{64}:1:0</a></dd>
          <dt>output</dt>
          <dd><a class=collapse href=/output/1{64}:1>1{64}:1</a></dd>
          <dt>offset</dt>
          <dd>0</dd>
          <dt>ethereum teleburn address</dt>
          <dd class=collapse>0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059</dd>
        </dl>
"
      .unindent()
    );
  }

  #[test]
  fn with_children() {
    assert_regex_match!(
      InscriptionHtml {
        child_count: 2,
        children: vec![inscription_id(2), inscription_id(3)],
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        <div class=inscription>
        <div>❮</div>
        <iframe .* src=/preview/1{64}i1></iframe>
        <div>❯</div>
        </div>
        <dl>
          <dt>children</dt>
          <dd>
            <div class=thumbnails>
              <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
              <a href=/inscription/3{64}i3><iframe .* src=/preview/3{64}i3></iframe></a>
            </div>
            <div class=center>
              <a href=/children/1{64}i1>all \\(2\\)</a>
            </div>
          </dd>
          <dt>id</dt>
          <dd class=collapse>1{64}i1</dd>
          <dt>preview</dt>
          <dd><a href=/preview/1{64}i1>link</a></dd>
          <dt>content</dt>
          <dd><a href=/content/1{64}i1>link</a></dd>
          <dt>content length</dt>
          <dd>10 bytes</dd>
          <dt>content type</dt>
          <dd>text/plain;charset=utf-8</dd>
          <dt>timestamp</dt>
          <dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>height</dt>
          <dd><a href=/block/0>0</a></dd>
          <dt>fee</dt>
          <dd>1</dd>
          <dt>reveal transaction</dt>
          <dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>location</dt>
          <dd><a class=collapse href=/satpoint/1{64}:1:0>1{64}:1:0</a></dd>
          <dt>output</dt>
          <dd><a class=collapse href=/output/1{64}:1>1{64}:1</a></dd>
          <dt>offset</dt>
          <dd>0</dd>
          <dt>ethereum teleburn address</dt>
          <dd class=collapse>0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059</dd>
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_paginated_children() {
    assert_regex_match!(
      InscriptionHtml {
        child_count: 1,
        children: vec![inscription_id(2)],
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        <div class=inscription>
        <div>❮</div>
        <iframe .* src=/preview/1{64}i1></iframe>
        <div>❯</div>
        </div>
        <dl>
          <dt>children</dt>
          <dd>
            <div class=thumbnails>
              <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
            </div>
            <div class=center>
              <a href=/children/1{64}i1>all \\(1\\)</a>
            </div>
          </dd>
          <dt>id</dt>
          <dd class=collapse>1{64}i1</dd>
          <dt>preview</dt>
          <dd><a href=/preview/1{64}i1>link</a></dd>
          <dt>content</dt>
          <dd><a href=/content/1{64}i1>link</a></dd>
          <dt>content length</dt>
          <dd>10 bytes</dd>
          <dt>content type</dt>
          <dd>text/plain;charset=utf-8</dd>
          <dt>timestamp</dt>
          <dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>height</dt>
          <dd><a href=/block/0>0</a></dd>
          <dt>fee</dt>
          <dd>1</dd>
          <dt>reveal transaction</dt>
          <dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>location</dt>
          <dd><a class=collapse href=/satpoint/1{64}:1:0>1{64}:1:0</a></dd>
          <dt>output</dt>
          <dd><a class=collapse href=/output/1{64}:1>1{64}:1</a></dd>
          <dt>offset</dt>
          <dd>0</dd>
          <dt>ethereum teleburn address</dt>
          <dd class=collapse>0xa1DfBd1C519B9323FD7Fd8e498Ac16c2E502F059</dd>
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_rune() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: inscription("text/plain;charset=utf-8", "HELLOWORLD"),
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        rune: Some(SpacedRune {
          rune: Rune(26),
          spacers: 1
        }),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        .*
        <dl>
          <dt>rune</dt>
          <dd><a href=/rune/A•A>A•A</a></dd>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_content_encoding() {
    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: Inscription {
          content_encoding: Some("br".into()),
          ..inscription("text/plain;charset=utf-8", "HELLOWORLD")
        },
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        .*
        <dl>
          .*
          <dt>content encoding</dt>
          <dd>br</dd>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_burn_metadata() {
    let script_pubkey = script::Builder::new()
      .push_opcode(opcodes::all::OP_RETURN)
      .push_slice([
        0xA2, 0x63, b'f', b'o', b'o', 0x63, b'b', b'a', b'r', 0x63, b'b', b'a', b'z', 0x01,
      ])
      .into_script();

    assert_regex_match!(
      InscriptionHtml {
        fee: 1,
        inscription: Inscription {
          content_encoding: Some("br".into()),
          ..inscription("text/plain;charset=utf-8", "HELLOWORLD")
        },
        id: inscription_id(1),
        number: 1,
        satpoint: satpoint(1, 0),
        output: Some(TxOut {
          value: Amount::from_sat(1),
          script_pubkey,
        }),
        ..default()
      },
      "
        <h1>Inscription 1</h1>
        .*
        <dl>
          .*
          <dt>burn metadata</dt>
          <dd>
            <dl><dt>foo</dt><dd>bar</dd><dt>baz</dt><dd>1</dd></dl>
          </dd>
          .*
        </dl>
      "
      .unindent()
    );
  }
}

ord/src/templates/inscriptions.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct InscriptionsHtml {
  pub(crate) inscriptions: Vec<InscriptionId>,
  pub(crate) prev: Option<u32>,
  pub(crate) next: Option<u32>,
}

impl PageContent for InscriptionsHtml {
  fn title(&self) -> String {
    "Inscriptions".into()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_prev_and_next() {
    assert_regex_match!(
      InscriptionsHtml {
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev: None,
        next: None,
      },
      "
        <h1>All Inscriptions</h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
        prev
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      InscriptionsHtml {
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev: Some(1),
        next: Some(2),
      },
      "
        <h1>All Inscriptions</h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
        <a class=prev href=/inscriptions/1>prev</a>
        <a class=next href=/inscriptions/2>next</a>
        .*
      "
      .unindent()
    );
  }
}

ord/src/templates/inscriptions_block.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct InscriptionsBlockHtml {
  pub(crate) block: u32,
  pub(crate) inscriptions: Vec<InscriptionId>,
  pub(crate) prev_block: Option<u32>,
  pub(crate) next_block: Option<u32>,
  pub(crate) prev_page: Option<u32>,
  pub(crate) next_page: Option<u32>,
}

impl InscriptionsBlockHtml {
  pub(crate) fn new(
    block: u32,
    current_blockheight: u32,
    inscriptions: Vec<InscriptionId>,
    more_inscriptions: bool,
    page_index: u32,
  ) -> Result<Self> {
    if page_index != 0 && inscriptions.is_empty() {
      return Err(anyhow!("page index {page_index} exceeds inscription count"));
    }

    Ok(Self {
      block,
      inscriptions,
      prev_block: block.checked_sub(1),
      next_block: if current_blockheight > block {
        Some(block + 1)
      } else {
        None
      },
      prev_page: page_index.checked_sub(1),
      next_page: if more_inscriptions {
        Some(page_index + 1)
      } else {
        None
      },
    })
  }
}

impl PageContent for InscriptionsBlockHtml {
  fn title(&self) -> String {
    format!("Inscriptions in Block {0}", self.block)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_prev_and_next() {
    assert_regex_match!(
      InscriptionsBlockHtml {
        block: 21,
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev_block: None,
        next_block: None,
        prev_page: None,
        next_page: None,
      },
      "
        <h1>Inscriptions in <a href=/block/21>Block 21</a></h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
        prev
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      InscriptionsBlockHtml {
        block: 21,
        inscriptions: vec![inscription_id(1), inscription_id(2)],
        prev_block: Some(20),
        next_block: Some(22),
        next_page: Some(3),
        prev_page: Some(1),
      },
      "
        <h1>Inscriptions in <a href=/block/21>Block 21</a></h1>
        <div class=thumbnails>
          <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
        </div>
        .*
          <a class=prev href=/inscriptions/block/20>20</a>
        &bull;
          <a class=prev href=/inscriptions/block/21/1>prev</a>
          <a class=next href=/inscriptions/block/21/3>next</a>
        &bull;
          <a class=next href=/inscriptions/block/22>22</a>
        .*
      "
      .unindent()
    );
  }
}

ord/src/templates/metadata.rs


use super::*;

pub(crate) struct MetadataHtml<'a>(pub &'a Value);

impl Display for MetadataHtml<'_> {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self.0 {
      Value::Array(x) => {
        write!(f, "<ul>")?;
        for element in x {
          write!(f, "<li>{}</li>", MetadataHtml(element))?;
        }
        write!(f, "</ul>")
      }
      Value::Bool(x) => write!(f, "{x}"),
      Value::Bytes(x) => {
        for byte in x {
          write!(f, "{byte:02X}")?;
        }
        Ok(())
      }
      Value::Float(x) => write!(f, "{x}"),
      Value::Integer(x) => write!(f, "{}", i128::from(*x)),
      Value::Map(x) => {
        write!(f, "<dl>")?;
        for (key, value) in x {
          write!(f, "<dt>{}</dt>", MetadataHtml(key))?;
          write!(f, "<dd>{}</dd>", MetadataHtml(value))?;
        }
        write!(f, "</dl>")
      }
      Value::Null => write!(f, "null"),
      Value::Tag(tag, value) => write!(f, "<sup>{tag}</sup>{}", MetadataHtml(value)),
      Value::Text(x) => x.escape(f, false),
      _ => write!(f, "unknown"),
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn null() {
    assert_eq!(MetadataHtml(&Value::Null).to_string(), "null");
  }

  #[test]
  fn integer() {
    assert_eq!(MetadataHtml(&Value::Integer(100.into())).to_string(), "100");
  }

  #[test]
  fn bool() {
    assert_eq!(MetadataHtml(&Value::Bool(false)).to_string(), "false");
    assert_eq!(MetadataHtml(&Value::Bool(true)).to_string(), "true");
  }

  #[test]
  fn tag() {
    assert_eq!(
      MetadataHtml(&Value::Tag(0, Box::new(Value::Bool(false)))).to_string(),
      "<sup>0</sup>false"
    );
  }

  #[test]
  fn string() {
    assert_eq!(
      MetadataHtml(&Value::Text("hello".into())).to_string(),
      "hello"
    );
    assert_eq!(MetadataHtml(&Value::Text("<".into())).to_string(), "&lt;");
  }

  #[test]
  fn bytes() {
    assert_eq!(
      MetadataHtml(&Value::Bytes(vec![0, 1, 2, 0xFF])).to_string(),
      "000102FF"
    );
  }

  #[test]
  fn float() {
    assert_eq!(MetadataHtml(&Value::Float(0.5)).to_string(), "0.5");
  }

  #[test]
  fn array() {
    assert_eq!(
      MetadataHtml(&Value::Array(vec![
        Value::Null,
        Value::Null,
        Value::Text("hello".to_string())
      ]))
      .to_string(),
      "<ul><li>null</li><li>null</li><li>hello</li></ul>"
    )
  }

  #[test]
  fn map() {
    assert_eq!(
      MetadataHtml(&Value::Map(
        vec![
          (Value::Text("b".to_string()), Value::Null),
          (
            Value::Text("a".to_string()),
            Value::Text("hello".to_string())
          )
        ]
        .into_iter()
        .collect()
      ))
      .to_string(),
      "<dl><dt>b</dt><dd>null</dd><dt>a</dt><dd>hello</dd></dl>"
    );
    assert_eq!(
      MetadataHtml(&Value::Map(
        vec![(Value::Text("<".to_string()), Value::Null),]
          .into_iter()
          .collect()
      ))
      .to_string(),
      "<dl><dt>&lt;</dt><dd>null</dd></dl>"
    );
  }
}

ord/src/templates/output.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct OutputHtml {
  pub(crate) chain: Chain,
  pub(crate) confirmations: u32,
  pub(crate) inscriptions: Option<Vec<InscriptionId>>,
  pub(crate) outpoint: OutPoint,
  pub(crate) output: TxOut,
  pub(crate) runes: Option<BTreeMap<SpacedRune, Pile>>,
  pub(crate) sat_ranges: Option<Vec<(u64, u64)>>,
  pub(crate) spent: bool,
}

impl PageContent for OutputHtml {
  fn title(&self) -> String {
    format!("Output {}", self.outpoint)
  }
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    bitcoin::{blockdata::script, PubkeyHash},
  };

  #[test]
  fn unspent_output() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 6,
        inscriptions: Some(Vec::new()),
        outpoint: outpoint(1),
        output: TxOut { value: Amount::from_sat(3), script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), },
        runes: Some(BTreeMap::new()),
        sat_ranges: Some(vec![(0, 1), (1, 3)]),
        spent: false,
      },
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>value</dt><dd>3</dd>
          <dt>script pubkey</dt><dd class=monospace>OP_DUP OP_HASH160 OP_PUSHBYTES_20 0{40} OP_EQUALVERIFY OP_CHECKSIG</dd>
          <dt>address</dt><dd><a class=collapse href=/address/1111111111111111111114oLvT2>1111111111111111111114oLvT2</a></dd>
          <dt>transaction</dt><dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>confirmations</dt><dd>6</dd>
          <dt>spent</dt><dd>false</dd>
        </dl>
        <h2>2 Sat Ranges</h2>
        <ul class=monospace>
          <li><a href=/sat/0 class=mythic>0</a></li>
          <li><a href=/sat/1 class=common>1</a>-<a href=/sat/2 class=common>2</a> \\(2 sats\\)</li>
        </ul>
      "
      .unindent()
    );
  }

  #[test]
  fn spent_output() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 10,
        inscriptions: None,
        outpoint: outpoint(1),
        output: TxOut {
          value: Amount::from_sat(1),
          script_pubkey: script::Builder::new().push_int(0).into_script(),
        },
        runes: None,
        sat_ranges: None,
        spent: true,
      },
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>value</dt><dd>1</dd>
          <dt>script pubkey</dt><dd class=monospace>OP_0</dd>
          <dt>transaction</dt><dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>confirmations</dt><dd>10</dd>
          <dt>spent</dt><dd>true</dd>
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn spent_output_with_ranges() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 6,
        inscriptions: None,
        outpoint: outpoint(1),
        output: TxOut { value: Amount::from_sat(3), script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), },
        runes: None,
        sat_ranges: Some(vec![(0, 1), (1, 3)]),
        spent: true,
      },
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>value</dt><dd>3</dd>
          <dt>script pubkey</dt><dd class=monospace>OP_DUP OP_HASH160 OP_PUSHBYTES_20 0{40} OP_EQUALVERIFY OP_CHECKSIG</dd>
          <dt>address</dt><dd><a class=collapse href=/address/1111111111111111111114oLvT2>1111111111111111111114oLvT2</a></dd>
          <dt>transaction</dt><dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>confirmations</dt><dd>6</dd>
          <dt>spent</dt><dd>true</dd>
        </dl>
        <h2>2 Sat Ranges</h2>
        <ul class=monospace>
          <li><a href=/sat/0 class=mythic>0</a></li>
          <li><a href=/sat/1 class=common>1</a>-<a href=/sat/2 class=common>2</a> \\(2 sats\\)</li>
        </ul>
      "
      .unindent()
    );
  }

  #[test]
  fn no_list() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 6,
        inscriptions: None,
        outpoint: outpoint(1),
        output: TxOut { value: Amount::from_sat(3), script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()), },
        runes: None,
        sat_ranges: None,
        spent: false,
      }
      .to_string(),
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>value</dt><dd>3</dd>
          <dt>script pubkey</dt><dd class=monospace>OP_DUP OP_HASH160 OP_PUSHBYTES_20 0{40} OP_EQUALVERIFY OP_CHECKSIG</dd>
          <dt>address</dt><dd><a class=collapse href=/address/1111111111111111111114oLvT2>1111111111111111111114oLvT2</a></dd>
          <dt>transaction</dt><dd><a class=collapse href=/tx/1{64}>1{64}</a></dd>
          <dt>confirmations</dt><dd>6</dd>
          <dt>spent</dt><dd>false</dd>
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_inscriptions() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 6,
        inscriptions: Some(vec![inscription_id(1)]),
        outpoint: outpoint(1),
        output: TxOut {
          value: Amount::from_sat(3),
          script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()),
        },
        runes: None,
        sat_ranges: None,
        spent: false,
      },
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>inscriptions</dt>
          <dd class=thumbnails>
            <a href=/inscription/1{64}i1><iframe .* src=/preview/1{64}i1></iframe></a>
          </dd>
          .*
        </dl>
      "
      .unindent()
    );
  }

  #[test]
  fn with_runes() {
    assert_regex_match!(
      OutputHtml {
        chain: Chain::Mainnet,
        confirmations: 6,
        inscriptions: None,
        outpoint: outpoint(1),
        output: TxOut {
          value: Amount::from_sat(3),
          script_pubkey: ScriptBuf::new_p2pkh(&PubkeyHash::all_zeros()),
        },
        runes: Some(
          vec![(
            SpacedRune {
              rune: Rune(26),
              spacers: 1
            },
            Pile {
              amount: 11,
              divisibility: 1,
              symbol: None,
            }
          )]
          .into_iter()
          .collect()
        ),
        sat_ranges: None,
        spent: false,
      },
      "
        <h1>Output <span class=monospace>1{64}:1</span></h1>
        <dl>
          <dt>runes</dt>
          <dd>
            <table>
              <tr>
                <th>rune</th>
                <th>balance</th>
              </tr>
              <tr>
                <td><a href=/rune/A•A>A•A</a></td>
                <td>1.1\u{A0}¤</td>
              </tr>
            </table>
          </dd>
          .*
        </dl>
      "
      .unindent()
    );
  }
}

ord/src/templates/parents.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct ParentsHtml {
  pub(crate) id: InscriptionId,
  pub(crate) number: i32,
  pub(crate) parents: Vec<InscriptionId>,
  pub(crate) prev_page: Option<usize>,
  pub(crate) next_page: Option<usize>,
}

impl PageContent for ParentsHtml {
  fn title(&self) -> String {
    format!("Inscription {} Parents", self.number)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn without_prev_and_next() {
    assert_regex_match!(
      ParentsHtml {
        id: inscription_id(1),
        number: 0,
        parents: vec![inscription_id(2), inscription_id(3)],
        prev_page: None,
        next_page: None,
      },
      "
        <h1><a href=/inscription/1{64}i1>Inscription 0</a> Parents</h1>
        <div class=thumbnails>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
          <a href=/inscription/3{64}i3><iframe .* src=/preview/3{64}i3></iframe></a>
        </div>
        .*
        prev
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_regex_match!(
      ParentsHtml {
        id: inscription_id(1),
        number: 0,
        parents: vec![inscription_id(2), inscription_id(3)],
        next_page: Some(3),
        prev_page: Some(1),
      },
      "
        <h1><a href=/inscription/1{64}i1>Inscription 0</a> Parents</h1>
        <div class=thumbnails>
          <a href=/inscription/2{64}i2><iframe .* src=/preview/2{64}i2></iframe></a>
          <a href=/inscription/3{64}i3><iframe .* src=/preview/3{64}i3></iframe></a>
        </div>
        .*
          <a class=prev href=/parents/1{64}i1/1>prev</a>
          <a class=next href=/parents/1{64}i1/3>next</a>
        .*
      "
      .unindent()
    );
  }
}

ord/src/templates/preview.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct PreviewAudioHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewCodeHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) language: media::Language,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewFontHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewImageHtml {
  pub(crate) image_rendering: ImageRendering,
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewMarkdownHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewModelHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewPdfHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewTextHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

#[derive(Boilerplate)]
pub(crate) struct PreviewUnknownHtml;

#[derive(Boilerplate)]
pub(crate) struct PreviewVideoHtml {
  pub(crate) inscription_id: InscriptionId,
  pub(crate) inscription_number: i32,
}

ord/src/templates/rare.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct RareTxt(pub(crate) Vec<(Sat, SatPoint)>);

ord/src/templates/rune.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)]
pub struct RuneHtml {
  pub entry: RuneEntry,
  pub id: RuneId,
  pub mintable: bool,
  pub parent: Option<InscriptionId>,
}

impl RuneHtml {
  fn mint_progress(&self) -> Option<Decimal> {
    if !self.mintable {
      return None;
    }

    let cap = self.entry.terms?.cap?;

    if cap == 0 {
      return None;
    }

    let progress = self.entry.mints as f64 / cap as f64;

    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
    Some(Decimal {
      value: (progress * 10000.0) as u128,
      scale: 2,
    })
  }
}

impl PageContent for RuneHtml {
  fn title(&self) -> String {
    format!("Rune {}", self.entry.spaced_rune)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn display() {
    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 1,
          burned: 123456789123456789,
          divisibility: 9,
          etching: Txid::all_zeros(),
          mints: 100,
          terms: Some(Terms {
            cap: Some(101),
            offset: (None, None),
            height: (Some(10), Some(11)),
            amount: Some(1000000001),
          }),
          number: 25,
          premine: 123456789,
          spaced_rune: SpacedRune {
            rune: Rune(u128::MAX),
            spacers: 1
          },
          symbol: Some('@'),
          timestamp: 0,
          turbo: true,
        },
        id: RuneId { block: 10, tx: 9 },
        mintable: true,
        parent: Some(InscriptionId {
          txid: Txid::all_zeros(),
          index: 0,
        }),
      },
      "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
.*<a href=/inscription/.*<iframe .* src=/preview/0{64}i0></iframe></a>.*
<dl>
  <dt>number</dt>
  <dd>25</dd>
  <dt>timestamp</dt>
  <dd><time>1970-01-01 00:00:00 UTC</time></dd>
  <dt>id</dt>
  <dd>10:9</dd>
  <dt>etching block</dt>
  <dd><a href=/block/10>10</a></dd>
  <dt>etching transaction</dt>
  <dd>9</dd>
  <dt>mint</dt>
  <dd>
    <dl>
      <dt>start</dt>
      <dd><a href=/block/10>10</a></dd>
      <dt>end</dt>
      <dd><a href=/block/11>11</a></dd>
      <dt>amount</dt>
      <dd>1.000000001 @</dd>
      <dt>mints</dt>
      <dd>100</dd>
      <dt>cap</dt>
      <dd>101</dd>
      <dt>remaining</dt>
      <dd>1</dd>
      <dt>mintable</dt>
      <dd>true</dd>
      <dt>progress</dt>
      <dd>99%</dd>
    </dl>
  </dd>
  <dt>supply</dt>
  <dd>100.123456889\u{A0}@</dd>
  <dt>premine</dt>
  <dd>0.123456789\u{A0}@</dd>
  <dt>premine percentage</dt>
  <dd>0.12%</dd>
  <dt>burned</dt>
  <dd>123456789.123456789\u{A0}@</dd>
  <dt>divisibility</dt>
  <dd>9</dd>
  <dt>symbol</dt>
  <dd>@</dd>
  <dt>turbo</dt>
  <dd>true</dd>
  <dt>etching</dt>
  <dd><a class=collapse href=/tx/0{64}>0{64}</a></dd>
  <dt>parent</dt>
  <dd><a class=collapse href=/inscription/0{64}i0>0{64}i0</a></dd>
</dl>
"
    );
  }

  #[test]
  fn display_no_mint() {
    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 0,
          burned: 123456789123456789,
          terms: None,
          divisibility: 9,
          etching: Txid::all_zeros(),
          mints: 0,
          number: 25,
          premine: 0,
          spaced_rune: SpacedRune {
            rune: Rune(u128::MAX),
            spacers: 1
          },
          symbol: Some('%'),
          timestamp: 0,
          turbo: false,
        },
        id: RuneId { block: 10, tx: 9 },
        mintable: false,
        parent: None,
      },
      "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
<dl>.*
  <dt>mint</dt>
  <dd>no</dd>
.*</dl>
"
    );
  }

  #[test]
  fn display_no_turbo() {
    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 0,
          burned: 123456789123456789,
          terms: None,
          divisibility: 9,
          etching: Txid::all_zeros(),
          mints: 0,
          number: 25,
          premine: 0,
          spaced_rune: SpacedRune {
            rune: Rune(u128::MAX),
            spacers: 1
          },
          symbol: Some('%'),
          timestamp: 0,
          turbo: false,
        },
        id: RuneId { block: 10, tx: 9 },
        mintable: false,
        parent: None,
      },
      "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
<dl>.*
  <dt>turbo</dt>
  <dd>false</dd>
.*</dl>
"
    );
  }

  #[test]
  fn display_empty_mint() {
    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 0,
          burned: 123456789123456789,
          terms: Some(Terms {
            cap: None,
            offset: (None, None),
            height: (None, None),
            amount: None,
          }),
          divisibility: 9,
          etching: Txid::all_zeros(),
          mints: 0,
          premine: 0,
          number: 25,
          spaced_rune: SpacedRune {
            rune: Rune(u128::MAX),
            spacers: 1
          },
          symbol: Some('%'),
          timestamp: 0,
          turbo: false,
        },
        id: RuneId { block: 10, tx: 9 },
        mintable: false,
        parent: None,
      },
      "<h1>B•CGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
<dl>.*
  <dt>mint</dt>
  <dd>
    <dl>
      <dt>start</dt>
      <dd>none</dd>
      <dt>end</dt>
      <dd>none</dd>
      <dt>amount</dt>
      <dd>none</dd>
      <dt>mints</dt>
      <dd>0</dd>
      <dt>cap</dt>
      <dd>0</dd>
      <dt>remaining</dt>
      <dd>0</dd>
      <dt>mintable</dt>
      <dd>false</dd>
    </dl>
  </dd>
.*</dl>
"
    );
  }

  #[test]
  fn mint_progress() {
    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 0,
          burned: 0,
          divisibility: 0,
          etching: Txid::all_zeros(),
          mints: 5555,
          terms: Some(Terms {
            cap: Some(10000),
            offset: (None, None),
            height: (None, None),
            amount: None,
          }),
          number: 0,
          premine: 0,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0
          },
          symbol: None,
          timestamp: 0,
          turbo: false,
        },
        id: RuneId { block: 0, tx: 0 },
        mintable: false,
        parent: Some(InscriptionId {
          txid: Txid::all_zeros(),
          index: 0,
        }),
      },
      ".*
      <dt>mintable</dt>
      <dd>false</dd>
    </dl>.*"
    );

    assert_regex_match!(
      RuneHtml {
        entry: RuneEntry {
          block: 0,
          burned: 0,
          divisibility: 0,
          etching: Txid::all_zeros(),
          mints: 5555,
          terms: Some(Terms {
            cap: Some(10000),
            offset: (None, None),
            height: (None, None),
            amount: None,
          }),
          number: 0,
          premine: 0,
          spaced_rune: SpacedRune {
            rune: Rune(0),
            spacers: 0
          },
          symbol: None,
          timestamp: 0,
          turbo: false,
        },
        id: RuneId { block: 0, tx: 0 },
        mintable: true,
        parent: Some(InscriptionId {
          txid: Txid::all_zeros(),
          index: 0,
        }),
      },
      ".*
      <dt>mintable</dt>
      <dd>true</dd>
      <dt>progress</dt>
      <dd>55.55%</dd>.*"
    );
  }
}

ord/src/templates/rune_not_found.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize)]
pub struct RuneNotFoundHtml {
  pub rune: Rune,
  pub unlock: Option<(Height, Blocktime)>,
}

impl PageContent for RuneNotFoundHtml {
  fn title(&self) -> String {
    format!("Rune {}", self.rune)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn display_expected() {
    assert_regex_match!(
      RuneNotFoundHtml {
        rune: Rune(u128::MAX),
        unlock: Some((Height(111), Blocktime::Expected(DateTime::default()))),
      },
      r"<h1>BCGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
<dl>
  <dt>unlock height</dt>
  <dd>111</dd>
  <dt>unlock time</dt>
  <dd><time>1970-01-01 00:00:00 UTC</time> \(expected\)</dd>
  <dt>reserved</dt>
  <dd>false</dd>
</dl>
"
    );
  }

  #[test]
  fn display_confirmed() {
    assert_regex_match!(
      RuneNotFoundHtml {
        rune: Rune(u128::MAX),
        unlock: Some((Height(111), Blocktime::Confirmed(DateTime::default()))),
      },
      r"<h1>BCGDENLQRQWDSLRUGSNLBTMFIJAV</h1>
<dl>
  <dt>unlock height</dt>
  <dd>111</dd>
  <dt>unlock time</dt>
  <dd><time>1970-01-01 00:00:00 UTC</time></dd>
  <dt>reserved</dt>
  <dd>false</dd>
</dl>
"
    );
  }

  #[test]
  fn display_reserved() {
    assert_regex_match!(
      RuneNotFoundHtml {
        rune: Rune(Rune::RESERVED),
        unlock: None,
      },
      "<h1>AAAAAAAAAAAAAAAAAAAAAAAAAAA</h1>
<dl>
  <dt>unlock height</dt>
  <dd>none</dd>
  <dt>reserved</dt>
  <dd>true</dd>
</dl>
"
    );
  }
}

ord/src/templates/runes.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)]
pub struct RunesHtml {
  pub entries: Vec<(RuneId, RuneEntry)>,
  pub more: bool,
  pub prev: Option<usize>,
  pub next: Option<usize>,
}

impl PageContent for RunesHtml {
  fn title(&self) -> String {
    "Runes".to_string()
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn display() {
    assert_eq!(
      RunesHtml {
        entries: vec![(
          RuneId { block: 0, tx: 0 },
          RuneEntry {
            spaced_rune: SpacedRune {
              rune: Rune(26),
              spacers: 1
            },
            ..default()
          }
        )],
        more: false,
        prev: None,
        next: None,
      }
      .to_string(),
      "<h1>Runes</h1>
<ul>
  <li><a href=/rune/A•A>A•A</a></li>
</ul>
<div class=center>
    prev
      next
  </div>"
    );
  }

  #[test]
  fn with_prev_and_next() {
    assert_eq!(
      RunesHtml {
        entries: vec![
          (
            RuneId { block: 0, tx: 0 },
            RuneEntry {
              spaced_rune: SpacedRune {
                rune: Rune(0),
                spacers: 0
              },
              ..Default::default()
            }
          ),
          (
            RuneId { block: 0, tx: 1 },
            RuneEntry {
              spaced_rune: SpacedRune {
                rune: Rune(2),
                spacers: 0
              },
              ..Default::default()
            }
          )
        ],
        prev: Some(1),
        next: Some(2),
        more: true,
      }
      .to_string(),
      "<h1>Runes</h1>
<ul>
  <li><a href=/rune/A>A</a></li>
  <li><a href=/rune/C>C</a></li>
</ul>
<div class=center>
    <a class=prev href=/runes/1>prev</a>
      <a class=next href=/runes/2>next</a>
  </div>"
    );
  }
}

ord/src/templates/sat.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct SatHtml {
  pub(crate) address: Option<Address>,
  pub(crate) blocktime: Blocktime,
  pub(crate) inscriptions: Vec<InscriptionId>,
  pub(crate) sat: Sat,
  pub(crate) satpoint: Option<SatPoint>,
}

impl PageContent for SatHtml {
  fn title(&self) -> String {
    format!("Sat {}", self.sat)
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn first() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(0),
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      "
        <h1>Sat 0</h1>
        <dl>
          <dt>decimal</dt><dd>0.0</dd>
          <dt>degree</dt><dd>0°0′0″0‴</dd>
          <dt>percentile</dt><dd>0%</dd>
          <dt>name</dt><dd>nvtdijuwxlp</dd>
          <dt>cycle</dt><dd>0</dd>
          <dt>epoch</dt><dd>0</dd>
          <dt>period</dt><dd>0</dd>
          <dt>block</dt><dd><a href=/block/0>0</a></dd>
          <dt>offset</dt><dd>0</dd>
          <dt>timestamp</dt><dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>rarity</dt><dd><span class=mythic>mythic</span></dd>
          <dt>charms</dt>
          <dd>
            <span title=coin>🪙</span>
            <span title=mythic>🎃</span>
            <span title=palindrome>🦋</span>
          </dd>
        </dl>
        .*
        prev
        <a class=next href=/sat/1>next</a>
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn last() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(2099999997689999),
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      "
        <h1>Sat 2099999997689999</h1>
        <dl>
          <dt>decimal</dt><dd>6929999.0</dd>
          <dt>degree</dt><dd>5°209999′1007″0‴</dd>
          <dt>percentile</dt><dd>100%</dd>
          <dt>name</dt><dd>a</dd>
          <dt>cycle</dt><dd>5</dd>
          <dt>epoch</dt><dd>32</dd>
          <dt>period</dt><dd>3437</dd>
          <dt>block</dt><dd><a href=/block/6929999>6929999</a></dd>
          <dt>offset</dt><dd>0</dd>
          <dt>timestamp</dt><dd><time>1970-01-01 00:00:00 UTC</time></dd>
          <dt>rarity</dt><dd><span class=uncommon>uncommon</span></dd>
          <dt>charms</dt>
          <dd>
            <span title=uncommon>🌱</span>
          </dd>
        </dl>
        .*
        <a class=prev href=/sat/2099999997689998>prev</a>
        next
        .*
      "
      .unindent()
    );
  }

  #[test]
  fn sat_with_next_and_prev() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(1),
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      r"<h1>Sat 1</h1>.*<a class=prev href=/sat/0>prev</a>\n<a class=next href=/sat/2>next</a>.*",
    );
  }

  #[test]
  fn sat_with_inscription() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(0),
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: vec![inscription_id(1)],
      },
      "
        <h1>Sat 0</h1>
        .*
          <dt>inscriptions</dt>
          <dd class=thumbnails>
            <a href=/inscription/1{64}i1>.*</a>
          </dd>
        .*"
        .unindent(),
    );
  }

  #[test]
  fn sat_with_reinscription() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(0),
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: vec![inscription_id(1), inscription_id(2)],
      },
      "
        <h1>Sat 0</h1>
        .*
          <dt>inscriptions</dt>
          <dd class=thumbnails>
            <a href=/inscription/1{64}i1>.*</a>
            <a href=/inscription/2{64}i2>.*</a>
          </dd>
        .*"
        .unindent(),
    );
  }

  #[test]
  fn last_sat_next_link_is_disabled() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat::LAST,
        satpoint: None,
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      r"<h1>Sat 2099999997689999</h1>.*<a class=prev href=/sat/2099999997689998>prev</a>\nnext.*",
    );
  }

  #[test]
  fn sat_with_satpoint() {
    assert_regex_match!(
      SatHtml {
        address: None,
        sat: Sat(0),
        satpoint: Some(satpoint(1, 0)),
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      "<h1>Sat 0</h1>.*<dt>location</dt><dd><a class=collapse href=/satpoint/1{64}:1:0>1{64}:1:0</a></dd>.*",
    );
  }

  #[test]
  fn sat_with_address() {
    assert_regex_match!(
      SatHtml {
        address: Some(address(0)),
        sat: Sat(0),
        satpoint: Some(satpoint(1, 0)),
        blocktime: Blocktime::confirmed(0),
        inscriptions: Vec::new(),
      },
      "<h1>Sat 0</h1>.*<dt>address</dt><dd class=monospace><a href=/address/bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4>bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4</a></dd>.*",
    );
  }
}

ord/src/templates/satscard.rs


use super::*;

#[derive(Boilerplate)]
pub(crate) struct SatscardHtml {
  pub(crate) satscard: Option<(Satscard, Option<AddressHtml>)>,
}

impl SatscardHtml {
  fn form_value(&self) -> Option<String> {
    self.satscard.as_ref().map(|(satscard, _address_info)| {
      format!("https://satscard.com/start#{}", satscard.query_parameters)
    })
  }
}

impl PageContent for SatscardHtml {
  fn title(&self) -> String {
    if let Some((satscard, _address_info)) = &self.satscard {
      format!("Satscard {}", satscard.address)
    } else {
      "Satscard".into()
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn form_value() {
    assert_eq!(
      SatscardHtml {
        satscard: Some((crate::satscard::tests::satscard(), None)),
      }
      .form_value(),
      Some(crate::satscard::tests::URL.into()),
    );

    assert_eq!(SatscardHtml { satscard: None }.form_value(), None);
  }

  #[test]
  fn title() {
    assert_eq!(
      SatscardHtml {
        satscard: Some((crate::satscard::tests::satscard(), None)),
      }
      .title(),
      format!("Satscard {}", crate::satscard::tests::address())
    );

    assert_eq!(SatscardHtml { satscard: None }.title(), "Satscard");
  }

  #[test]
  fn no_address_info() {
    pretty_assert_eq!(
      SatscardHtml {
        satscard: Some((crate::satscard::tests::satscard(), None)),
      }
      .to_string(),
      r#"<h1>Satscard bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf</h1>
<form>
  <label for=url>Satscard URL</label>
  <input
    type=text
    id=url
    name=url
    pattern='^https://(get)?satscard.com/start#.*$'
    required
    title='The URL should begin with "https://(get)satscard.com/start#".'
    value='https://satscard.com/start#u=S&amp;o=0&amp;r=a5x2tplf&amp;n=7664168a4ef7b8e8&amp;s=42b209c86ab90be6418d36b0accc3a53c11901861b55be95b763799842d403dc17cd1b74695a7ffe2d78965535d6fe7f6aafc77f6143912a163cb65862e8fb53'
  >
  <input type="submit" value="Submit">
</form>
<dl>
  <dt>slot</dt>
  <dd>1</dd>
  <dt>state</dt>
  <dd class=satscard-sealed>sealed</dd>
  <dt>address</dt>
  <dd><a class=collapse href=/address/bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf>bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf</a></dd>
  <dt>nonce</dt>
  <dd>7664168a4ef7b8e8</dd>
</dl>
"#,
    );
  }

  #[test]
  fn with_address_info() {
    pretty_assert_eq!(
      SatscardHtml {
        satscard: Some((
          crate::satscard::tests::satscard(),
          Some(AddressHtml {
            address: crate::satscard::tests::address(),
            header: false,
            inscriptions: Some(Vec::new()),
            outputs: Vec::new(),
            runes_balances: None,
            sat_balance: 0,
          })
        )),
      }
      .to_string(),
      r#"<h1>Satscard bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf</h1>
<form>
  <label for=url>Satscard URL</label>
  <input
    type=text
    id=url
    name=url
    pattern='^https://(get)?satscard.com/start#.*$'
    required
    title='The URL should begin with "https://(get)satscard.com/start#".'
    value='https://satscard.com/start#u=S&amp;o=0&amp;r=a5x2tplf&amp;n=7664168a4ef7b8e8&amp;s=42b209c86ab90be6418d36b0accc3a53c11901861b55be95b763799842d403dc17cd1b74695a7ffe2d78965535d6fe7f6aafc77f6143912a163cb65862e8fb53'
  >
  <input type="submit" value="Submit">
</form>
<dl>
  <dt>slot</dt>
  <dd>1</dd>
  <dt>state</dt>
  <dd class=satscard-sealed>sealed</dd>
  <dt>address</dt>
  <dd><a class=collapse href=/address/bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf>bc1ql86vqdwylsgmgkkrae5nrafte8yp43a5x2tplf</a></dd>
  <dt>nonce</dt>
  <dd>7664168a4ef7b8e8</dd>
</dl>
<dl>
  <dt>sat balance</dt>
  <dd>0</dd>
  <dt>outputs</dt>
  <dd>
    <ul>
    </ul>
  </dd>
</dl>

"#,
    );
  }

  #[test]
  fn state_error() {
    assert_regex_match! {
      SatscardHtml {
        satscard: Some((
          Satscard {
            state: crate::satscard::State::Error,
            ..crate::satscard::tests::satscard()
          },
          Some(AddressHtml {
            address: crate::satscard::tests::address(),
            header: false,
            inscriptions: Some(Vec::new()),
            outputs: Vec::new(),
            runes_balances: None,
            sat_balance: 0,
          })
        )),
      }
      .to_string(),
      r#".*
  <dt>state</dt>
  <dd class=satscard-error>error</dd>
.*
"#,
    }
  }

  #[test]
  fn state_unsealed() {
    assert_regex_match! {
      SatscardHtml {
        satscard: Some((
          Satscard {
            state: crate::satscard::State::Unsealed,
            ..crate::satscard::tests::satscard()
          },
          Some(AddressHtml {
            address: crate::satscard::tests::address(),
            header: false,
            inscriptions: Some(Vec::new()),
            outputs: Vec::new(),
            runes_balances: None,
            sat_balance: 0,
          })
        )),
      }
      .to_string(),
      r#".*
  <dt>state</dt>
  <dd class=satscard-unsealed>unsealed</dd>
.*
"#,
    }
  }
}

ord/src/templates/status.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)]
pub struct StatusHtml {
  pub address_index: bool,
  pub blessed_inscriptions: u64,
  pub chain: Chain,
  pub cursed_inscriptions: u64,
  pub height: Option<u32>,
  pub initial_sync_time: Duration,
  pub inscription_index: bool,
  pub inscriptions: u64,
  pub json_api: bool,
  pub lost_sats: u64,
  pub minimum_rune_for_next_block: Rune,
  pub rune_index: bool,
  pub runes: u64,
  pub sat_index: bool,
  pub started: DateTime<Utc>,
  pub transaction_index: bool,
  pub unrecoverably_reorged: bool,
  pub uptime: Duration,
}

impl PageContent for StatusHtml {
  fn title(&self) -> String {
    "Status".into()
  }
}

ord/src/templates/transaction.rs


use super::*;

#[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)]
pub struct TransactionHtml {
  pub chain: Chain,
  pub etching: Option<SpacedRune>,
  pub inscription_count: u32,
  pub transaction: Transaction,
  pub txid: Txid,
}

impl PageContent for TransactionHtml {
  fn title(&self) -> String {
    format!("Transaction {}", self.txid)
  }
}

#[cfg(test)]
mod tests {
  use {super::*, bitcoin::blockdata::script};

  #[test]
  fn html() {
    let transaction = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: vec![TxIn {
        sequence: Default::default(),
        previous_output: Default::default(),
        script_sig: Default::default(),
        witness: Default::default(),
      }],
      output: vec![
        TxOut {
          value: Amount::from_sat(50 * COIN_VALUE),
          script_pubkey: script::Builder::new().push_int(0).into_script(),
        },
        TxOut {
          value: Amount::from_sat(50 * COIN_VALUE),
          script_pubkey: script::Builder::new().push_int(1).into_script(),
        },
      ],
    };

    let txid = transaction.compute_txid();

    pretty_assert_eq!(
      TransactionHtml {
        chain: Chain::Mainnet,
        etching: None,
        inscription_count: 0,
        txid: transaction.compute_txid(),
        transaction,
      }.to_string(),
      format!(
        "
        <h1>Transaction <span class=monospace>{txid}</span></h1>
        <dl>
        </dl>
        <h2>1 Input</h2>
        <ul>
          <li><a class=collapse href=/output/0000000000000000000000000000000000000000000000000000000000000000:4294967295>0000000000000000000000000000000000000000000000000000000000000000:4294967295</a></li>
        </ul>
        <h2>2 Outputs</h2>
        <ul class=monospace>
          <li>
            <a href=/output/{txid}:0 class=collapse>
              {txid}:0
            </a>
            <dl>
              <dt>value</dt><dd>5000000000</dd>
              <dt>script pubkey</dt><dd class=monospace>OP_0</dd>
            </dl>
          </li>
          <li>
            <a href=/output/{txid}:1 class=collapse>
              {txid}:1
            </a>
            <dl>
              <dt>value</dt><dd>5000000000</dd>
              <dt>script pubkey</dt><dd class=monospace>OP_PUSHNUM_1</dd>
            </dl>
          </li>
        </ul>
      "
      )
      .unindent()
    );
  }
}

ord/src/test.rs


pub(crate) use {
  super::*,
  bitcoin::{
    blockdata::script::{PushBytes, PushBytesBuf},
    opcodes, WPubkeyHash,
  },
  mockcore::TransactionTemplate,
  ordinals::COIN_VALUE,
  pretty_assertions::assert_eq as pretty_assert_eq,
  std::iter,
  tempfile::TempDir,
  unindent::Unindent,
};

pub(crate) fn rune_id(tx: u32) -> RuneId {
  RuneId { block: 1, tx }
}

pub(crate) fn txid(n: u32) -> Txid {
  let hex = format!("{n:x}");

  if hex.is_empty() || hex.len() > 1 {
    panic!();
  }

  hex.repeat(64).parse().unwrap()
}

pub(crate) fn outpoint(n: u32) -> OutPoint {
  OutPoint {
    txid: txid(n),
    vout: n,
  }
}

pub(crate) fn satpoint(n: u32, offset: u64) -> SatPoint {
  SatPoint {
    offset,
    outpoint: outpoint(n),
  }
}

pub(crate) fn address(n: u32) -> Address {
  match n {
    0 => "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
    1 => "bc1qhl452zcq3ng5kzajzkx9jnzncml9tnsk3w96s6",
    2 => "bc1qqqcjq9jydx79rywltc38g5qfrjq485a8xfmkf7",
    3 => "bc1qcq2uv5nk6hec6kvag3wyevp6574qmsm9scjxc2",
    4 => "bc1qukgekwq8e68ay0mewdrvg0d3cfuc094aj2rvx9",
    5 => "bc1qtdjs8tgkaja5ddxs0j7rn52uqfdtqa53mum8xc",
    6 => "bc1qd3ex6kwlc5ett55hgsnk94y8q2zhdyxyqyujkl",
    7 => "bc1q8dcv8r903evljd87mcg0hq8lphclch7pd776wt",
    8 => "bc1q9j6xvm3td447ygnhfra5tfkpkcupwe9937nhjq",
    9 => "bc1qlyrhjzvxdzmvxe2mnr37p68vkl5fysyhfph8z0",
    _ => panic!(),
  }
  .parse::<Address<NetworkUnchecked>>()
  .unwrap()
  .assume_checked()
}

pub(crate) fn recipient() -> ScriptBuf {
  recipient_address().script_pubkey()
}

pub(crate) fn recipient_address() -> Address {
  "tb1q6en7qjxgw4ev8xwx94pzdry6a6ky7wlfeqzunz"
    .parse::<Address<NetworkUnchecked>>()
    .unwrap()
    .assume_checked()
}

pub(crate) fn change(n: u64) -> Address {
  match n {
    0 => "tb1qjsv26lap3ffssj6hfy8mzn0lg5vte6a42j75ww",
    1 => "tb1qakxxzv9n7706kc3xdcycrtfv8cqv62hnwexc0l",
    2 => "tb1qxz9yk0td0yye009gt6ayn7jthz5p07a75luryg",
    3 => "tb1qe62s57n77pfhlw2vtqlhm87dwj75l6fguavjjq",
    _ => panic!(),
  }
  .parse::<Address<NetworkUnchecked>>()
  .unwrap()
  .assume_checked()
}

pub(crate) fn tx_in(previous_output: OutPoint) -> TxIn {
  TxIn {
    previous_output,
    script_sig: ScriptBuf::new(),
    sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
    witness: Witness::new(),
  }
}

pub(crate) fn tx_out(value: u64, address: Address) -> TxOut {
  TxOut {
    value: Amount::from_sat(value),
    script_pubkey: address.script_pubkey(),
  }
}

#[derive(Default, Debug)]
pub(crate) struct InscriptionTemplate {
  pub(crate) parents: Vec<InscriptionId>,
  pub(crate) pointer: Option<u64>,
}

impl From<InscriptionTemplate> for Inscription {
  fn from(template: InscriptionTemplate) -> Self {
    Self {
      parents: template.parents.into_iter().map(|id| id.value()).collect(),
      pointer: template.pointer.map(Inscription::pointer_value),
      ..default()
    }
  }
}

pub(crate) fn inscription(content_type: &str, body: impl AsRef<[u8]>) -> Inscription {
  Inscription {
    content_type: Some(content_type.into()),
    body: Some(body.as_ref().into()),
    ..default()
  }
}

pub(crate) fn inscription_id(n: u32) -> InscriptionId {
  let hex = format!("{n:x}");

  if hex.is_empty() || hex.len() > 1 {
    panic!();
  }

  format!("{}i{n}", hex.repeat(64)).parse().unwrap()
}

pub(crate) fn envelope(payload: &[&[u8]]) -> Witness {
  let mut builder = script::Builder::new()
    .push_opcode(opcodes::OP_FALSE)
    .push_opcode(opcodes::all::OP_IF);

  for data in payload {
    let mut buf = PushBytesBuf::new();
    buf.extend_from_slice(data).unwrap();
    builder = builder.push_slice(buf);
  }

  let script = builder.push_opcode(opcodes::all::OP_ENDIF).into_script();

  Witness::from_slice(&[script.into_bytes(), Vec::new()])
}

pub(crate) fn default_address(chain: Chain) -> Address {
  Address::from_script(
    &ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()),
    chain.network(),
  )
  .unwrap()
}

ord/src/wallet.rs


use {
  super::*,
  batch::ParentInfo,
  bitcoin::{
    bip32::{ChildNumber, DerivationPath, Xpriv},
    psbt::Psbt,
    secp256k1::Secp256k1,
  },
  bitcoincore_rpc::json::ImportDescriptors,
  entry::{EtchingEntry, EtchingEntryValue},
  fee_rate::FeeRate,
  index::entry::Entry,
  indicatif::{ProgressBar, ProgressStyle},
  log::log_enabled,
  miniscript::descriptor::{DescriptorSecretKey, DescriptorXKey, Wildcard},
  redb::{Database, DatabaseError, ReadableTable, RepairSession, StorageError, TableDefinition},
  std::sync::Once,
  transaction_builder::TransactionBuilder,
};

pub mod batch;
pub mod entry;
pub mod transaction_builder;
pub mod wallet_constructor;

const SCHEMA_VERSION: u64 = 1;

define_table! { RUNE_TO_ETCHING, u128, EtchingEntryValue }
define_table! { STATISTICS, u64, u64 }

#[derive(Copy, Clone)]
pub(crate) enum Statistic {
  Schema = 0,
}

impl Statistic {
  fn key(self) -> u64 {
    self.into()
  }
}

impl From<Statistic> for u64 {
  fn from(statistic: Statistic) -> Self {
    statistic as u64
  }
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct Descriptor {
  pub desc: String,
  pub timestamp: bitcoincore_rpc::bitcoincore_rpc_json::Timestamp,
  pub active: bool,
  pub internal: Option<bool>,
  pub range: Option<(u64, u64)>,
  pub next: Option<u64>,
}

#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct ListDescriptorsResult {
  pub wallet_name: String,
  pub descriptors: Vec<Descriptor>,
}

#[derive(Debug, PartialEq)]
pub(crate) enum Maturity {
  BelowMinimumHeight(u64),
  CommitNotFound,
  CommitSpent(Txid),
  ConfirmationsPending(u32),
  Mature,
}

pub(crate) struct Wallet {
  bitcoin_client: Client,
  database: Database,
  has_rune_index: bool,
  has_sat_index: bool,
  rpc_url: Url,
  utxos: BTreeMap<OutPoint, TxOut>,
  ord_client: reqwest::blocking::Client,
  inscription_info: BTreeMap<InscriptionId, api::Inscription>,
  output_info: BTreeMap<OutPoint, api::Output>,
  inscriptions: BTreeMap<SatPoint, Vec<InscriptionId>>,
  locked_utxos: BTreeMap<OutPoint, TxOut>,
  settings: Settings,
}

impl Wallet {
  pub(crate) fn get_wallet_sat_ranges(&self) -> Result<Vec<(OutPoint, Vec<(u64, u64)>)>> {
    ensure!(
      self.has_sat_index,
      "ord index must be built with `--index-sats` to use `--sat`"
    );

    let mut output_sat_ranges = Vec::new();
    for (output, info) in self.output_info.iter() {
      if let Some(sat_ranges) = &info.sat_ranges {
        output_sat_ranges.push((*output, sat_ranges.clone()));
      } else {
        bail!("output {output} in wallet but is spent according to ord server");
      }
    }

    Ok(output_sat_ranges)
  }

  pub(crate) fn get_output_sat_ranges(&self, output: &OutPoint) -> Result<Vec<(u64, u64)>> {
    ensure!(
      self.has_sat_index,
      "ord index must be built with `--index-sats` to see sat ranges"
    );

    if let Some(info) = self.output_info.get(output) {
      if let Some(sat_ranges) = &info.sat_ranges {
        Ok(sat_ranges.clone())
      } else {
        bail!("output {output} in wallet but is spent according to ord server");
      }
    } else {
      bail!("output {output} not found in wallet");
    }
  }

  pub(crate) fn find_sat_in_outputs(&self, sat: Sat) -> Result<SatPoint> {
    ensure!(
      self.has_sat_index,
      "ord index must be built with `--index-sats` to use `--sat`"
    );

    for (outpoint, info) in self.output_info.iter() {
      if let Some(sat_ranges) = &info.sat_ranges {
        let mut offset = 0;
        for (start, end) in sat_ranges {
          if start <= &sat.n() && &sat.n() < end {
            return Ok(SatPoint {
              outpoint: *outpoint,
              offset: offset + sat.n() - start,
            });
          }
          offset += end - start;
        }
      } else {
        continue;
      }
    }

    Err(anyhow!(format!(
      "could not find sat `{sat}` in wallet outputs"
    )))
  }

  pub(crate) fn bitcoin_client(&self) -> &Client {
    &self.bitcoin_client
  }

  pub(crate) fn utxos(&self) -> &BTreeMap<OutPoint, TxOut> {
    &self.utxos
  }

  pub(crate) fn locked_utxos(&self) -> &BTreeMap<OutPoint, TxOut> {
    &self.locked_utxos
  }

  pub(crate) fn lock_non_cardinal_outputs(&self) -> Result {
    let inscriptions = self
      .inscriptions()
      .keys()
      .map(|satpoint| satpoint.outpoint)
      .collect::<HashSet<OutPoint>>();

    let locked = self
      .locked_utxos()
      .keys()
      .cloned()
      .collect::<HashSet<OutPoint>>();

    let outputs = self
      .utxos()
      .keys()
      .filter(|utxo| inscriptions.contains(utxo))
      .chain(self.get_runic_outputs()?.unwrap_or_default().iter())
      .cloned()
      .filter(|utxo| !locked.contains(utxo))
      .collect::<Vec<OutPoint>>();

    if !self.bitcoin_client().lock_unspent(&outputs)? {
      bail!("failed to lock UTXOs");
    }

    Ok(())
  }

  pub(crate) fn inscriptions(&self) -> &BTreeMap<SatPoint, Vec<InscriptionId>> {
    &self.inscriptions
  }

  pub(crate) fn inscription_info(&self) -> BTreeMap<InscriptionId, api::Inscription> {
    self.inscription_info.clone()
  }

  pub(crate) fn get_inscription(
    &self,
    inscription_id: InscriptionId,
  ) -> Result<Option<api::Inscription>> {
    let inscription = self
      .ord_client
      .get(
        self
          .rpc_url
          .join(&format!("/inscription/{inscription_id}"))
          .unwrap(),
      )
      .send()?
      .json()?;

    Ok(inscription)
  }

  pub(crate) fn inscription_exists(&self, inscription_id: InscriptionId) -> Result<bool> {
    Ok(
      !self
        .ord_client
        .get(
          self
            .rpc_url
            .join(&format!("/inscription/{inscription_id}"))
            .unwrap(),
        )
        .send()?
        .status()
        .is_client_error(),
    )
  }

  pub(crate) fn get_inscriptions_in_output(
    &self,
    output: &OutPoint,
  ) -> Result<Option<Vec<InscriptionId>>> {
    Ok(
      self
        .output_info
        .get(output)
        .ok_or(anyhow!("output not found in wallet"))?
        .inscriptions
        .clone(),
    )
  }

  pub(crate) fn get_parent_info(&self, parents: &[InscriptionId]) -> Result<Vec<ParentInfo>> {
    let mut parent_info = Vec::new();
    for parent_id in parents {
      if !self.inscription_exists(*parent_id)? {
        return Err(anyhow!("parent {parent_id} does not exist"));
      }

      let satpoint = self
        .inscription_info
        .get(parent_id)
        .ok_or_else(|| anyhow!("parent {parent_id} not in wallet"))?
        .satpoint;

      let tx_out = self
        .utxos
        .get(&satpoint.outpoint)
        .ok_or_else(|| anyhow!("parent {parent_id} not in wallet"))?
        .clone();

      parent_info.push(ParentInfo {
        destination: self.get_change_address()?,
        id: *parent_id,
        location: satpoint,
        tx_out,
      });
    }

    Ok(parent_info)
  }

  pub(crate) fn get_runic_outputs(&self) -> Result<Option<BTreeSet<OutPoint>>> {
    let mut runic_outputs = BTreeSet::new();
    for (output, info) in &self.output_info {
      let Some(runes) = &info.runes else {
        return Ok(None);
      };

      if !runes.is_empty() {
        runic_outputs.insert(*output);
      }
    }

    Ok(Some(runic_outputs))
  }

  pub(crate) fn get_runes_balances_in_output(
    &self,
    output: &OutPoint,
  ) -> Result<Option<BTreeMap<SpacedRune, Pile>>> {
    Ok(
      self
        .output_info
        .get(output)
        .ok_or(anyhow!("output not found in wallet"))?
        .runes
        .clone(),
    )
  }

  pub(crate) fn get_rune(
    &self,
    rune: Rune,
  ) -> Result<Option<(RuneId, RuneEntry, Option<InscriptionId>)>> {
    let response = self
      .ord_client
      .get(
        self
          .rpc_url
          .join(&format!("/rune/{}", SpacedRune { rune, spacers: 0 }))
          .unwrap(),
      )
      .send()?;

    if response.status() == StatusCode::NOT_FOUND {
      return Ok(None);
    }

    let response = response.error_for_status()?;

    let rune_json: api::Rune = serde_json::from_str(&response.text()?)?;

    Ok(Some((rune_json.id, rune_json.entry, rune_json.parent)))
  }

  pub(crate) fn get_change_address(&self) -> Result<Address> {
    Ok(
      self
        .bitcoin_client
        .call::<Address<NetworkUnchecked>>("getrawchangeaddress", &["bech32m".into()])
        .context("could not get change addresses from wallet")?
        .require_network(self.chain().network())?,
    )
  }

  pub(crate) fn has_sat_index(&self) -> bool {
    self.has_sat_index
  }

  pub(crate) fn has_rune_index(&self) -> bool {
    self.has_rune_index
  }

  pub(crate) fn chain(&self) -> Chain {
    self.settings.chain()
  }

  pub(crate) fn integration_test(&self) -> bool {
    self.settings.integration_test()
  }

  fn is_above_minimum_at_height(&self, rune: Rune) -> Result<bool> {
    Ok(
      rune
        >= Rune::minimum_at_height(
          self.chain().network(),
          Height(u32::try_from(self.bitcoin_client().get_block_count()? + 1).unwrap()),
        ),
    )
  }

  pub(crate) fn check_maturity(&self, rune: Rune, commit: &Transaction) -> Result<Maturity> {
    Ok(
      if let Some(commit_tx) = self
        .bitcoin_client()
        .get_transaction(&commit.compute_txid(), Some(true))
        .into_option()?
      {
        let current_confirmations = u32::try_from(commit_tx.info.confirmations)?;
        if self
          .bitcoin_client()
          .get_tx_out(&commit.compute_txid(), 0, Some(true))?
          .is_none()
        {
          Maturity::CommitSpent(commit_tx.info.txid)
        } else if !self.is_above_minimum_at_height(rune)? {
          Maturity::BelowMinimumHeight(self.bitcoin_client().get_block_count()? + 1)
        } else if current_confirmations + 1 < u32::from(Runestone::COMMIT_CONFIRMATIONS) {
          Maturity::ConfirmationsPending(
            u32::from(Runestone::COMMIT_CONFIRMATIONS) - current_confirmations - 1,
          )
        } else {
          Maturity::Mature
        }
      } else {
        Maturity::CommitNotFound
      },
    )
  }

  pub(crate) fn wait_for_maturation(&self, rune: Rune) -> Result<batch::Output> {
    let Some(entry) = self.load_etching(rune)? else {
      bail!("no etching found");
    };

    eprintln!(
      "Waiting for rune {} commitment {} to mature…",
      rune,
      entry.commit.compute_txid()
    );

    let mut pending_confirmations: u32 = Runestone::COMMIT_CONFIRMATIONS.into();

    let progress = ProgressBar::new(pending_confirmations.into()).with_style(
      ProgressStyle::default_bar()
        .template("Maturing in...[{eta}] {spinner:.green} [{bar:40.cyan/blue}] {pos}/{len}")
        .unwrap()
        .progress_chars("█▓▒░ "),
    );

    loop {
      if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
        eprintln!("Suspending batch. Run `ord wallet resume` to continue.");
        return Ok(entry.output);
      }

      match self.check_maturity(rune, &entry.commit)? {
        Maturity::Mature => {
          progress.finish_with_message("Rune matured, submitting...");
          break;
        }
        Maturity::ConfirmationsPending(remaining) => {
          if remaining < pending_confirmations {
            pending_confirmations = remaining;
            progress.inc(1);
          }
        }
        Maturity::CommitSpent(txid) => {
          self.clear_etching(rune)?;
          bail!("rune commitment {} spent, can't send reveal tx", txid);
        }
        _ => {}
      }

      if !self.integration_test() {
        thread::sleep(Duration::from_secs(5));
      }
    }

    self.send_etching(rune, &entry)
  }

  pub(crate) fn send_etching(&self, rune: Rune, entry: &EtchingEntry) -> Result<batch::Output> {
    match self.bitcoin_client().send_raw_transaction(&entry.reveal) {
      Ok(txid) => txid,
      Err(err) => {
        return Err(anyhow!(
          "Failed to send reveal transaction: {err}\nCommit tx {} will be recovered once mined",
          entry.commit.compute_txid()
        ))
      }
    };

    self.clear_etching(rune)?;

    Ok(batch::Output {
      reveal_broadcast: true,
      ..entry.output.clone()
    })
  }

  fn check_descriptors(wallet_name: &str, descriptors: Vec<Descriptor>) -> Result<Vec<Descriptor>> {
    let tr = descriptors
      .iter()
      .filter(|descriptor| descriptor.desc.starts_with("tr("))
      .count();

    let rawtr = descriptors
      .iter()
      .filter(|descriptor| descriptor.desc.starts_with("rawtr("))
      .count();

    if tr != 2 || descriptors.len() != 2 + rawtr {
      bail!("wallet \"{}\" contains unexpected output descriptors, and does not appear to be an `ord` wallet, create a new wallet with `ord wallet create`", wallet_name);
    }

    Ok(descriptors)
  }

  pub(crate) fn initialize_from_descriptors(
    name: String,
    settings: &Settings,
    descriptors: Vec<Descriptor>,
  ) -> Result {
    let client = Self::check_version(settings.bitcoin_rpc_client(Some(name.clone()))?)?;

    let descriptors = Self::check_descriptors(&name, descriptors)?;

    client.create_wallet(&name, None, Some(true), None, None)?;

    let descriptors = descriptors
      .into_iter()
      .map(|descriptor| ImportDescriptors {
        descriptor: descriptor.desc.clone(),
        timestamp: descriptor.timestamp,
        active: Some(true),
        range: descriptor.range.map(|(start, end)| {
          (
            usize::try_from(start).unwrap_or(0),
            usize::try_from(end).unwrap_or(0),
          )
        }),
        next_index: descriptor
          .next
          .map(|next| usize::try_from(next).unwrap_or(0)),
        internal: descriptor.internal,
        label: None,
      })
      .collect::<Vec<ImportDescriptors>>();

    client.call::<serde_json::Value>("importdescriptors", &[serde_json::to_value(descriptors)?])?;

    Ok(())
  }

  pub(crate) fn initialize(
    name: String,
    settings: &Settings,
    seed: [u8; 64],
    timestamp: bitcoincore_rpc::json::Timestamp,
  ) -> Result {
    Self::check_version(settings.bitcoin_rpc_client(None)?)?.create_wallet(
      &name,
      None,
      Some(true),
      None,
      None,
    )?;

    let network = settings.chain().network();

    let secp = Secp256k1::new();

    let master_private_key = Xpriv::new_master(network, &seed)?;

    let fingerprint = master_private_key.fingerprint(&secp);

    let derivation_path = DerivationPath::master()
      .child(ChildNumber::Hardened { index: 86 })
      .child(ChildNumber::Hardened {
        index: u32::from(network != Network::Bitcoin),
      })
      .child(ChildNumber::Hardened { index: 0 });

    let derived_private_key = master_private_key.derive_priv(&secp, &derivation_path)?;

    let mut descriptors = Vec::new();
    for change in [false, true] {
      let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey {
        origin: Some((fingerprint, derivation_path.clone())),
        xkey: derived_private_key,
        derivation_path: DerivationPath::master().child(ChildNumber::Normal {
          index: change.into(),
        }),
        wildcard: Wildcard::Unhardened,
      });

      let public_key = secret_key.to_public(&secp)?;

      let mut key_map = BTreeMap::new();
      key_map.insert(public_key.clone(), secret_key);

      let descriptor = miniscript::descriptor::Descriptor::new_tr(public_key, None)?;

      descriptors.push(ImportDescriptors {
        descriptor: descriptor.to_string_with_secret(&key_map),
        timestamp,
        active: Some(true),
        range: None,
        next_index: None,
        internal: Some(change),
        label: None,
      });
    }

    match settings
      .bitcoin_rpc_client(Some(name.clone()))?
      .call::<serde_json::Value>(
        "importdescriptors",
        &[serde_json::to_value(descriptors.clone())?],
      ) {
      Ok(_) => Ok(()),
      Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
        if err.code == -4 && err.message == "Wallet already loading." =>
      {
        // wallet loading
        Ok(())
      }
      Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
        if err.code == -35 =>
      {
        // wallet already loaded
        Ok(())
      }
      Err(err) => {
        bail!("Failed to import descriptors for wallet {}: {err}", name)
      }
    }
  }

  pub(crate) fn check_version(client: Client) -> Result<Client> {
    const MIN_VERSION: usize = 280000;

    let bitcoin_version = client.version()?;
    if bitcoin_version < MIN_VERSION {
      bail!(
        "Bitcoin Core {} or newer required, current version is {}",
        Self::format_bitcoin_core_version(MIN_VERSION),
        Self::format_bitcoin_core_version(bitcoin_version),
      );
    } else {
      Ok(client)
    }
  }

  fn format_bitcoin_core_version(version: usize) -> String {
    format!(
      "{}.{}.{}",
      version / 10000,
      version % 10000 / 100,
      version % 100
    )
  }

  pub(crate) fn open_database(wallet_name: &String, settings: &Settings) -> Result<Database> {
    let path = settings
      .data_dir()
      .join("wallets")
      .join(format!("{wallet_name}.redb"));

    if let Err(err) = fs::create_dir_all(path.parent().unwrap()) {
      bail!(
        "failed to create data dir `{}`: {err}",
        path.parent().unwrap().display()
      );
    }

    let db_path = path.clone().to_owned();
    let once = Once::new();
    let progress_bar = Mutex::new(None);
    let integration_test = settings.integration_test();

    let repair_callback = move |progress: &mut RepairSession| {
      once.call_once(|| {
        println!(
          "Wallet database file `{}` needs recovery. This can take some time.",
          db_path.display()
        )
      });

      if !(cfg!(test) || log_enabled!(log::Level::Info) || integration_test) {
        let mut guard = progress_bar.lock().unwrap();

        let progress_bar = guard.get_or_insert_with(|| {
          let progress_bar = ProgressBar::new(100);
          progress_bar.set_style(
            ProgressStyle::with_template("[repairing database] {wide_bar} {pos}/{len}").unwrap(),
          );
          progress_bar
        });

        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
        progress_bar.set_position((progress.progress() * 100.0) as u64);
      }
    };

    let database = match Database::builder()
      .set_repair_callback(repair_callback)
      .open(&path)
    {
      Ok(database) => {
        {
          let schema_version = database
            .begin_read()?
            .open_table(STATISTICS)?
            .get(&Statistic::Schema.key())?
            .map(|x| x.value())
            .unwrap_or(0);

          match schema_version.cmp(&SCHEMA_VERSION) {
            cmp::Ordering::Less =>
              bail!(
                "wallet database at `{}` appears to have been built with an older, incompatible version of ord, consider deleting and rebuilding the index: index schema {schema_version}, ord schema {SCHEMA_VERSION}",
                path.display()
              ),
            cmp::Ordering::Greater =>
              bail!(
                "wallet database at `{}` appears to have been built with a newer, incompatible version of ord, consider updating ord: index schema {schema_version}, ord schema {SCHEMA_VERSION}",
                path.display()
              ),
            cmp::Ordering::Equal => {
            }
          }
        }

        database
      }
      Err(DatabaseError::Storage(StorageError::Io(error)))
        if error.kind() == io::ErrorKind::NotFound =>
      {
        let database = Database::builder().create(&path)?;

        let mut tx = database.begin_write()?;
        tx.set_quick_repair(true);

        tx.open_table(RUNE_TO_ETCHING)?;

        tx.open_table(STATISTICS)?
          .insert(&Statistic::Schema.key(), &SCHEMA_VERSION)?;

        tx.commit()?;

        database
      }
      Err(error) => bail!("failed to open wallet database: {error}"),
    };

    Ok(database)
  }

  pub(crate) fn save_etching(
    &self,
    rune: &Rune,
    commit: &Transaction,
    reveal: &Transaction,
    output: batch::Output,
  ) -> Result {
    let mut wtx = self.database.begin_write()?;
    wtx.set_quick_repair(true);

    wtx.open_table(RUNE_TO_ETCHING)?.insert(
      rune.0,
      EtchingEntry {
        commit: commit.clone(),
        reveal: reveal.clone(),
        output,
      }
      .store(),
    )?;

    wtx.commit()?;

    Ok(())
  }

  pub(crate) fn load_etching(&self, rune: Rune) -> Result<Option<EtchingEntry>> {
    let rtx = self.database.begin_read()?;

    Ok(
      rtx
        .open_table(RUNE_TO_ETCHING)?
        .get(rune.0)?
        .map(|result| EtchingEntry::load(result.value())),
    )
  }

  pub(crate) fn clear_etching(&self, rune: Rune) -> Result {
    let mut wtx = self.database.begin_write()?;
    wtx.set_quick_repair(true);

    wtx.open_table(RUNE_TO_ETCHING)?.remove(rune.0)?;
    wtx.commit()?;

    Ok(())
  }

  pub(crate) fn pending_etchings(&self) -> Result<Vec<(Rune, EtchingEntry)>> {
    let rtx = self.database.begin_read()?;

    Ok(
      rtx
        .open_table(RUNE_TO_ETCHING)?
        .iter()?
        .map(|result| {
          result.map(|(key, value)| (Rune(key.value()), EtchingEntry::load(value.value())))
        })
        .collect::<Result<Vec<(Rune, EtchingEntry)>, StorageError>>()?,
    )
  }

  pub(super) fn sign_and_broadcast_transaction(
    &self,
    unsigned_transaction: Transaction,
    dry_run: bool,
    burn_amount: Option<Amount>,
  ) -> Result<(Txid, String, u64)> {
    let unspent_outputs = self.utxos();

    let (txid, psbt) = if dry_run {
      let psbt = self
        .bitcoin_client()
        .wallet_process_psbt(
          &base64_encode(&Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
          Some(false),
          None,
          None,
        )?
        .psbt;

      (unsigned_transaction.compute_txid(), psbt)
    } else {
      let psbt = self
        .bitcoin_client()
        .wallet_process_psbt(
          &base64_encode(&Psbt::from_unsigned_tx(unsigned_transaction.clone())?.serialize()),
          Some(true),
          None,
          None,
        )?
        .psbt;

      let signed_tx = self
        .bitcoin_client()
        .finalize_psbt(&psbt, None)?
        .hex
        .ok_or_else(|| anyhow!("unable to sign transaction"))?;

      (self.send_raw_transaction(&signed_tx, burn_amount)?, psbt)
    };

    let mut fee = 0;
    for txin in unsigned_transaction.input.iter() {
      let Some(txout) = unspent_outputs.get(&txin.previous_output) else {
        panic!("input {} not found in utxos", txin.previous_output);
      };
      fee += txout.value.to_sat();
    }

    for txout in unsigned_transaction.output.iter() {
      fee = fee.checked_sub(txout.value.to_sat()).unwrap();
    }

    Ok((txid, psbt, fee))
  }

  pub(crate) fn send_raw_transaction<R: bitcoincore_rpc::RawTx>(
    &self,
    tx: R,
    burn_amount: Option<Amount>,
  ) -> Result<Txid> {
    let mut arguments = vec![tx.raw_hex().into()];

    if let Some(burn_amount) = burn_amount {
      arguments.push(serde_json::Value::Null);
      arguments.push(burn_amount.to_btc().into());
    }

    Ok(
      self
        .bitcoin_client()
        .call("sendrawtransaction", &arguments)?,
    )
  }

  pub fn create_unsigned_send_amount_transaction(
    &self,
    destination: Address,
    amount: Amount,
    fee_rate: FeeRate,
  ) -> Result<Transaction> {
    self.lock_non_cardinal_outputs()?;

    let unfunded_transaction = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: Vec::new(),
      output: vec![TxOut {
        script_pubkey: destination.script_pubkey(),
        value: amount,
      }],
    };

    let unsigned_transaction = consensus::encode::deserialize(&fund_raw_transaction(
      self.bitcoin_client(),
      fee_rate,
      &unfunded_transaction,
    )?)?;

    Ok(unsigned_transaction)
  }

  pub fn create_unsigned_send_satpoint_transaction(
    &self,
    destination: Address,
    satpoint: SatPoint,
    postage: Option<Amount>,
    fee_rate: FeeRate,
    sending_inscription: bool,
  ) -> Result<Transaction> {
    if !sending_inscription {
      for inscription_satpoint in self.inscriptions().keys() {
        if satpoint == *inscription_satpoint {
          bail!("inscriptions must be sent by inscription ID");
        }
      }
    }

    let runic_outputs = self.get_runic_outputs()?.unwrap_or_default();

    ensure!(
      !runic_outputs.contains(&satpoint.outpoint),
      "runic outpoints may not be sent by satpoint"
    );

    let change = [self.get_change_address()?, self.get_change_address()?];

    let postage = if let Some(postage) = postage {
      Target::ExactPostage(postage)
    } else {
      Target::Postage
    };

    Ok(
      TransactionBuilder::new(
        satpoint,
        self.inscriptions().clone(),
        self.utxos().clone(),
        self.locked_utxos().clone().into_keys().collect(),
        runic_outputs,
        destination.script_pubkey(),
        change,
        fee_rate,
        postage,
        self.chain().network(),
      )
      .build_transaction()?,
    )
  }

  pub fn create_unsigned_send_or_burn_runes_transaction(
    &self,
    destination: Option<Address>,
    spaced_rune: SpacedRune,
    decimal: Decimal,
    postage: Option<Amount>,
    fee_rate: FeeRate,
  ) -> Result<Transaction> {
    ensure!(
      self.has_rune_index(),
      "sending runes with `ord send` requires index created with `--index-runes` flag",
    );

    self.lock_non_cardinal_outputs()?;

    let (id, entry, _parent) = self
      .get_rune(spaced_rune.rune)?
      .with_context(|| format!("rune `{}` has not been etched", spaced_rune.rune))?;

    let amount = decimal.to_integer(entry.divisibility)?;

    let inscribed_outputs = self
      .inscriptions()
      .keys()
      .map(|satpoint| satpoint.outpoint)
      .collect::<HashSet<OutPoint>>();

    let balances = self
      .get_runic_outputs()?
      .unwrap_or_default()
      .into_iter()
      .filter(|output| !inscribed_outputs.contains(output))
      .map(|output| {
        self.get_runes_balances_in_output(&output).map(|balance| {
          (
            output,
            balance
              .unwrap_or_default()
              .into_iter()
              .map(|(spaced_rune, pile)| (spaced_rune.rune, pile.amount))
              .collect(),
          )
        })
      })
      .collect::<Result<BTreeMap<OutPoint, BTreeMap<Rune, u128>>>>()?;

    let mut inputs = Vec::new();
    let mut input_rune_balances: BTreeMap<Rune, u128> = BTreeMap::new();

    for (output, runes) in balances {
      if let Some(balance) = runes.get(&spaced_rune.rune) {
        if *balance > 0 {
          for (rune, balance) in runes {
            *input_rune_balances.entry(rune).or_default() += balance;
          }

          inputs.push(output);

          if input_rune_balances
            .get(&spaced_rune.rune)
            .cloned()
            .unwrap_or_default()
            >= amount
          {
            break;
          }
        }
      }
    }

    let input_rune_balance = input_rune_balances
      .get(&spaced_rune.rune)
      .cloned()
      .unwrap_or_default();

    let needs_runes_change_output = input_rune_balance > amount || input_rune_balances.len() > 1;

    ensure! {
      input_rune_balance >= amount,
      "insufficient `{}` balance, only {} in wallet",
      spaced_rune,
      Pile {
        amount: input_rune_balance,
        divisibility: entry.divisibility,
        symbol: entry.symbol
      },
    }

    let runestone;
    let postage = postage.unwrap_or(TARGET_POSTAGE);

    let unfunded_transaction = if let Some(destination) = destination {
      runestone = Runestone {
        edicts: vec![Edict {
          amount,
          id,
          output: 2,
        }],
        ..default()
      };

      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: inputs
          .into_iter()
          .map(|previous_output| TxIn {
            previous_output,
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          })
          .collect(),
        output: if needs_runes_change_output {
          vec![
            TxOut {
              script_pubkey: runestone.encipher(),
              value: Amount::from_sat(0),
            },
            TxOut {
              script_pubkey: self.get_change_address()?.script_pubkey(),
              value: postage,
            },
            TxOut {
              script_pubkey: destination.script_pubkey(),
              value: postage,
            },
          ]
        } else {
          vec![TxOut {
            script_pubkey: destination.script_pubkey(),
            value: postage,
          }]
        },
      }
    } else {
      runestone = Runestone {
        edicts: vec![Edict {
          amount,
          id,
          output: 0,
        }],
        ..default()
      };

      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: inputs
          .into_iter()
          .map(|previous_output| TxIn {
            previous_output,
            script_sig: ScriptBuf::new(),
            sequence: Sequence::MAX,
            witness: Witness::new(),
          })
          .collect(),
        output: if needs_runes_change_output {
          vec![
            TxOut {
              script_pubkey: runestone.encipher(),
              value: Amount::from_sat(0),
            },
            TxOut {
              script_pubkey: self.get_change_address()?.script_pubkey(),
              value: postage,
            },
          ]
        } else {
          vec![TxOut {
            script_pubkey: runestone.encipher(),
            value: Amount::from_sat(0),
          }]
        },
      }
    };

    let unsigned_transaction =
      fund_raw_transaction(self.bitcoin_client(), fee_rate, &unfunded_transaction)?;

    let unsigned_transaction = consensus::encode::deserialize(&unsigned_transaction)?;

    if needs_runes_change_output {
      assert_eq!(
        Runestone::decipher(&unsigned_transaction),
        Some(Artifact::Runestone(runestone)),
      );
    }

    Ok(unsigned_transaction)
  }

  pub(crate) fn simulate_transaction(&self, tx: &Transaction) -> Result<SignedAmount> {
    let tx = {
      let mut buffer = Vec::new();
      tx.consensus_encode(&mut buffer).unwrap();
      hex::encode(buffer)
    };

    Ok(
      self
        .bitcoin_client()
        .call::<SimulateRawTransactionResult>(
          "simulaterawtransaction",
          &[
            [tx].into(),
            serde_json::to_value(SimulateRawTransactionOptions {
              include_watchonly: false,
            })
            .unwrap(),
          ],
        )?
        .balance_change,
    )
  }
}

ord/src/wallet/batch.rs


use {
  super::*,
  bitcoin::{
    blockdata::{opcodes, script},
    key::PrivateKey,
    key::{TapTweak, TweakedKeypair, TweakedPublicKey, UntweakedKeypair},
    secp256k1::{self, constants::SCHNORR_SIGNATURE_SIZE, rand, Secp256k1, XOnlyPublicKey},
    sighash::{Prevouts, SighashCache, TapSighashType},
    taproot::Signature,
    taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder},
  },
  bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, SignRawTransactionInput, Timestamp},
  wallet::transaction_builder::Target,
};

pub(crate) use transactions::Transactions;

pub use {
  entry::Entry, etching::Etching, file::File, mode::Mode, plan::Plan, range::Range, terms::Terms,
};

pub mod entry;
mod etching;
pub mod file;
pub mod mode;
pub mod plan;
mod range;
mod terms;
mod transactions;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub struct Output {
  pub commit: Txid,
  pub commit_psbt: Option<String>,
  pub inscriptions: Vec<InscriptionInfo>,
  pub parents: Vec<InscriptionId>,
  pub reveal: Txid,
  pub reveal_broadcast: bool,
  pub reveal_psbt: Option<String>,
  pub rune: Option<RuneInfo>,
  pub total_fees: u64,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct InscriptionInfo {
  pub destination: Address<NetworkUnchecked>,
  pub id: InscriptionId,
  pub location: SatPoint,
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub struct RuneInfo {
  pub destination: Option<Address<NetworkUnchecked>>,
  pub location: Option<OutPoint>,
  pub rune: SpacedRune,
}

#[derive(Clone, Debug)]
pub struct ParentInfo {
  pub destination: Address,
  pub id: InscriptionId,
  pub location: SatPoint,
  pub tx_out: TxOut,
}

#[cfg(test)]
mod tests {
  use {
    super::*,
    crate::wallet::batch::{self, ParentInfo},
    bitcoin::policy::MAX_STANDARD_TX_WEIGHT,
  };

  #[test]
  fn reveal_transaction_pays_fee() {
    let utxos = vec![(outpoint(1), tx_out(20000, address(0)))];
    let inscription = inscription("text/plain", "ord");
    let commit_address = change(0);
    let reveal_address = recipient_address();
    let reveal_change = [commit_address, change(1)];

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint: Some(satpoint(1, 0)),
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      BTreeMap::new(),
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      reveal_change,
      change(2),
    )
    .unwrap();

    #[allow(clippy::cast_possible_truncation)]
    #[allow(clippy::cast_sign_loss)]
    let fee = Amount::from_sat((1.0 * (reveal_tx.vsize() as f64)).ceil() as u64);

    assert_eq!(
      reveal_tx.output[0].value.to_sat(),
      20000 - fee.to_sat() - (20000 - commit_tx.output[0].value.to_sat()),
    );
  }

  #[test]
  fn inscribe_transactions_opt_in_to_rbf() {
    let utxos = vec![(outpoint(1), tx_out(20000, address(0)))];
    let inscription = inscription("text/plain", "ord");
    let commit_address = change(0);
    let reveal_address = recipient_address();
    let reveal_change = [commit_address, change(1)];

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint: Some(satpoint(1, 0)),
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      BTreeMap::new(),
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      reveal_change,
      change(2),
    )
    .unwrap();

    assert!(commit_tx.is_explicitly_rbf());
    assert!(reveal_tx.is_explicitly_rbf());
  }

  #[test]
  fn inscribe_with_no_satpoint_and_no_cardinal_utxos() {
    let utxos = vec![(outpoint(1), tx_out(1000, address(0)))];
    let mut inscriptions = BTreeMap::new();
    inscriptions.insert(
      SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      vec![inscription_id(1)],
    );

    let inscription = inscription("text/plain", "ord");
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();

    let error = batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      inscriptions,
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .unwrap_err()
    .to_string();

    assert!(
      error.contains("wallet contains no cardinal utxos"),
      "{}",
      error
    );
  }

  #[test]
  fn inscribe_with_no_satpoint_and_enough_cardinal_utxos() {
    let utxos = vec![
      (outpoint(1), tx_out(20_000, address(0))),
      (outpoint(2), tx_out(20_000, address(0))),
    ];
    let mut inscriptions = BTreeMap::new();
    inscriptions.insert(
      SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      vec![inscription_id(1)],
    );

    let inscription = inscription("text/plain", "ord");
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();

    assert!(batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      inscriptions,
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .is_ok())
  }

  #[test]
  fn inscribe_with_custom_fee_rate() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(20_000, address(0))),
    ];
    let mut inscriptions = BTreeMap::new();
    inscriptions.insert(
      SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      vec![inscription_id(1)],
    );

    let inscription = inscription("text/plain", "ord");
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();
    let fee_rate = 3.3;

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(),
      reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .unwrap();

    let sig_vbytes = 17;
    let fee = FeeRate::try_from(fee_rate)
      .unwrap()
      .fee(commit_tx.vsize() + sig_vbytes)
      .to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 20_000 - fee);

    let fee = FeeRate::try_from(fee_rate)
      .unwrap()
      .fee(reveal_tx.vsize())
      .to_sat();

    assert_eq!(
      reveal_tx.output[0].value.to_sat(),
      20_000 - fee - (20_000 - commit_tx.output[0].value.to_sat()),
    );
  }

  #[test]
  fn inscribe_with_parent() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(20_000, address(0))),
    ];

    let mut inscriptions = BTreeMap::new();
    let parent_inscription = inscription_id(1);
    let parent_info = ParentInfo {
      destination: change(3),
      id: parent_inscription,
      location: SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10000),
      },
    };

    inscriptions.insert(parent_info.location, vec![parent_inscription]);

    let child_inscription = InscriptionTemplate {
      parents: vec![parent_inscription],
      ..default()
    }
    .into();

    let commit_address = change(1);
    let reveal_address = recipient_address();
    let fee_rate = 4.0;

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint: None,
      parent_info: vec![parent_info.clone()],
      inscriptions: vec![child_inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(fee_rate).unwrap(),
      reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(1),
    )
    .unwrap();

    let sig_vbytes = 17;
    let fee = FeeRate::try_from(fee_rate)
      .unwrap()
      .fee(commit_tx.vsize() + sig_vbytes)
      .to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 20_000 - fee);

    let sig_vbytes = 16;
    let fee = FeeRate::try_from(fee_rate)
      .unwrap()
      .fee(reveal_tx.vsize() + sig_vbytes);

    assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value);
    assert_eq!(
      reveal_tx.output[0].script_pubkey,
      parent_info.destination.script_pubkey()
    );
    assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value);
    pretty_assert_eq!(
      reveal_tx.input[0],
      TxIn {
        previous_output: parent_info.location.outpoint,
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        ..default()
      }
    );
  }

  #[test]
  fn inscribe_with_commit_fee_rate() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(20_000, address(0))),
    ];
    let mut inscriptions = BTreeMap::new();
    inscriptions.insert(
      SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      vec![inscription_id(1)],
    );

    let inscription = inscription("text/plain", "ord");
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();
    let commit_fee_rate = 3.3;
    let fee_rate = 1.0;

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(commit_fee_rate).unwrap(),
      reveal_fee_rate: FeeRate::try_from(fee_rate).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .unwrap();

    let sig_vbytes = 17;
    let fee = FeeRate::try_from(commit_fee_rate)
      .unwrap()
      .fee(commit_tx.vsize() + sig_vbytes)
      .to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 20_000 - fee);

    let fee = FeeRate::try_from(fee_rate)
      .unwrap()
      .fee(reveal_tx.vsize())
      .to_sat();

    assert_eq!(
      reveal_tx.output[0].value.to_sat(),
      20_000 - fee - (20_000 - commit_tx.output[0].value.to_sat()),
    );
  }

  #[test]
  fn inscribe_over_max_standard_tx_weight() {
    let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))];

    let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]);
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();

    let error = batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      BTreeMap::new(),
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .unwrap_err()
    .to_string();

    assert!(
      error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402799")),
      "{}",
      error
    );
  }

  #[test]
  fn inscribe_with_no_max_standard_tx_weight() {
    let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))];

    let inscription = inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize]);
    let satpoint = None;
    let commit_address = change(0);
    let reveal_address = recipient_address();

    let batch::Transactions { reveal_tx, .. } = batch::Plan {
      satpoint,
      parent_info: Vec::new(),
      inscriptions: vec![inscription],
      destinations: vec![reveal_address],
      commit_fee_rate: FeeRate::try_from(1.0).unwrap(),
      reveal_fee_rate: FeeRate::try_from(1.0).unwrap(),
      no_limit: true,
      reinscribe: false,
      postages: vec![TARGET_POSTAGE],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      BTreeMap::new(),
      Chain::Mainnet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(1)],
      change(2),
    )
    .unwrap();

    assert!(reveal_tx.total_size() >= MAX_STANDARD_TX_WEIGHT as usize);
  }

  #[test]
  fn batch_inscribe_with_parent() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(50_000, address(0))),
    ];

    let parent = inscription_id(1);

    let parent_info = ParentInfo {
      destination: change(3),
      id: parent,
      location: SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10000),
      },
    };

    let mut wallet_inscriptions = BTreeMap::new();
    wallet_inscriptions.insert(parent_info.location, vec![parent]);

    let commit_address = change(1);
    let reveal_addresses = vec![recipient_address()];

    let inscriptions = vec![
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
    ];

    let mode = batch::Mode::SharedOutput;

    let fee_rate = 4.0.try_into().unwrap();

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint: None,
      parent_info: vec![parent_info.clone()],
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: fee_rate,
      reveal_fee_rate: fee_rate,
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(10_000); 3],
      mode,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(2),
    )
    .unwrap();

    let sig_vbytes = 17;
    let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 50_000 - fee);

    let sig_vbytes = 16;
    let fee = fee_rate.fee(reveal_tx.vsize() + sig_vbytes);

    assert_eq!(fee, commit_tx.output[0].value - reveal_tx.output[1].value);
    assert_eq!(
      reveal_tx.output[0].script_pubkey,
      parent_info.destination.script_pubkey()
    );
    assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value);
    pretty_assert_eq!(
      reveal_tx.input[0],
      TxIn {
        previous_output: parent_info.location.outpoint,
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        ..default()
      }
    );
  }

  #[test]
  fn batch_inscribe_satpoints_with_parent() {
    let utxos = vec![
      (outpoint(1), tx_out(1_111, address(0))),
      (outpoint(2), tx_out(2_222, address(0))),
      (outpoint(3), tx_out(3_333, address(0))),
      (outpoint(4), tx_out(10_000, address(0))),
      (outpoint(5), tx_out(50_000, address(0))),
      (outpoint(6), tx_out(60_000, address(0))),
    ];

    let parent = inscription_id(1);

    let parent_info = ParentInfo {
      destination: change(3),
      id: parent,
      location: SatPoint {
        outpoint: outpoint(4),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10_000),
      },
    };

    let mut wallet_inscriptions = BTreeMap::new();
    wallet_inscriptions.insert(parent_info.location, vec![parent]);

    let commit_address = change(1);
    let reveal_addresses = vec![
      recipient_address(),
      recipient_address(),
      recipient_address(),
    ];

    let inscriptions = vec![
      InscriptionTemplate {
        parents: vec![parent],
        pointer: Some(10_000),
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        pointer: Some(11_111),
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        pointer: Some(13_3333),
      }
      .into(),
    ];

    let reveal_satpoints = utxos
      .iter()
      .take(3)
      .map(|(outpoint, txout)| {
        (
          SatPoint {
            outpoint: *outpoint,
            offset: 0,
          },
          txout.clone(),
        )
      })
      .collect::<Vec<(SatPoint, TxOut)>>();

    let mode = batch::Mode::SatPoints;

    let fee_rate = 1.0.try_into().unwrap();

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      reveal_satpoints: reveal_satpoints.clone(),
      parent_info: vec![parent_info.clone()],
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: fee_rate,
      reveal_fee_rate: fee_rate,
      postages: vec![
        Amount::from_sat(1_111),
        Amount::from_sat(2_222),
        Amount::from_sat(3_333),
      ],
      mode,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      reveal_satpoints
        .iter()
        .map(|(satpoint, _)| satpoint.outpoint)
        .collect(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    )
    .unwrap();

    let sig_vbytes = 17;
    let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 50_000 - fee);

    assert_eq!(
      reveal_tx.output[0].script_pubkey,
      parent_info.destination.script_pubkey()
    );
    assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value);
    pretty_assert_eq!(
      reveal_tx.input[0],
      TxIn {
        previous_output: parent_info.location.outpoint,
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        ..default()
      }
    );
  }

  #[test]
  fn batch_inscribe_with_parent_not_enough_cardinals_utxos_fails() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(20_000, address(0))),
    ];

    let parent = inscription_id(1);

    let parent_info = ParentInfo {
      destination: change(3),
      id: parent,
      location: SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10000),
      },
    };

    let mut wallet_inscriptions = BTreeMap::new();
    wallet_inscriptions.insert(parent_info.location, vec![parent]);

    let inscriptions = vec![
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
    ];

    let commit_address = change(1);
    let reveal_addresses = vec![recipient_address()];

    let error = batch::Plan {
      satpoint: None,
      parent_info: vec![parent_info.clone()],
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: 4.0.try_into().unwrap(),
      reveal_fee_rate: 4.0.try_into().unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(10_000); 3],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    )
    .unwrap_err()
    .to_string();

    assert!(error.contains(
      "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet."
    ));
  }

  #[test]
  #[should_panic(expected = "invariant: shared-output has only one destination")]
  fn batch_inscribe_with_inconsistent_reveal_addresses_panics() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(80_000, address(0))),
    ];

    let parent = inscription_id(1);

    let parent_info = ParentInfo {
      destination: change(3),
      id: parent,
      location: SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10000),
      },
    };

    let mut wallet_inscriptions = BTreeMap::new();
    wallet_inscriptions.insert(parent_info.location, vec![parent]);

    let inscriptions = vec![
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
    ];

    let commit_address = change(1);
    let reveal_addresses = vec![recipient_address(), recipient_address()];

    let _ = batch::Plan {
      satpoint: None,
      parent_info: vec![parent_info.clone()],
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: 4.0.try_into().unwrap(),
      reveal_fee_rate: 4.0.try_into().unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(10_000)],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    );
  }

  #[test]
  fn batch_inscribe_over_max_standard_tx_weight() {
    let utxos = vec![(outpoint(1), tx_out(50 * COIN_VALUE, address(0)))];

    let wallet_inscriptions = BTreeMap::new();

    let inscriptions = vec![
      inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]),
      inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]),
      inscription("text/plain", [0; MAX_STANDARD_TX_WEIGHT as usize / 3]),
    ];

    let commit_address = change(1);
    let reveal_addresses = vec![recipient_address()];

    let error = batch::Plan {
      satpoint: None,
      parent_info: Vec::new(),
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: 1.0.try_into().unwrap(),
      reveal_fee_rate: 1.0.try_into().unwrap(),
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(30_000); 3],
      mode: batch::Mode::SharedOutput,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    )
    .unwrap_err()
    .to_string();

    assert!(
      error.contains(&format!("reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): 402841")),
      "{}",
      error
    );
  }

  #[test]
  fn batch_inscribe_into_separate_outputs() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(80_000, address(0))),
    ];

    let wallet_inscriptions = BTreeMap::new();

    let commit_address = change(1);
    let reveal_addresses = vec![
      recipient_address(),
      recipient_address(),
      recipient_address(),
    ];

    let inscriptions = vec![
      inscription("text/plain", [b'O'; 100]),
      inscription("text/plain", [b'O'; 111]),
      inscription("text/plain", [b'O'; 222]),
    ];

    let mode = batch::Mode::SeparateOutputs;

    let fee_rate = 4.0.try_into().unwrap();

    let batch::Transactions { reveal_tx, .. } = batch::Plan {
      satpoint: None,
      parent_info: Vec::new(),
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: fee_rate,
      reveal_fee_rate: fee_rate,
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(10_000); 3],
      mode,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    )
    .unwrap();

    assert_eq!(reveal_tx.output.len(), 3);
    assert!(reveal_tx
      .output
      .iter()
      .all(|output| output.value == TARGET_POSTAGE));
  }

  #[test]
  fn batch_inscribe_into_separate_outputs_with_parent() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(50_000, address(0))),
    ];

    let parent = inscription_id(1);

    let parent_info = ParentInfo {
      destination: change(3),
      id: parent,
      location: SatPoint {
        outpoint: outpoint(1),
        offset: 0,
      },
      tx_out: TxOut {
        script_pubkey: change(0).script_pubkey(),
        value: Amount::from_sat(10000),
      },
    };

    let mut wallet_inscriptions = BTreeMap::new();
    wallet_inscriptions.insert(parent_info.location, vec![parent]);

    let commit_address = change(1);
    let reveal_addresses = vec![
      recipient_address(),
      recipient_address(),
      recipient_address(),
    ];

    let inscriptions = vec![
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
      InscriptionTemplate {
        parents: vec![parent],
        ..default()
      }
      .into(),
    ];

    let mode = batch::Mode::SeparateOutputs;

    let fee_rate = 4.0.try_into().unwrap();

    let batch::Transactions {
      commit_tx,
      reveal_tx,
      ..
    } = batch::Plan {
      satpoint: None,
      parent_info: vec![parent_info.clone()],
      inscriptions,
      destinations: reveal_addresses,
      commit_fee_rate: fee_rate,
      reveal_fee_rate: fee_rate,
      no_limit: false,
      reinscribe: false,
      postages: vec![Amount::from_sat(10_000); 3],
      mode,
      ..default()
    }
    .create_batch_transactions(
      wallet_inscriptions,
      Chain::Signet,
      BTreeSet::new(),
      BTreeSet::new(),
      utxos.into_iter().collect(),
      [commit_address, change(2)],
      change(3),
    )
    .unwrap();

    assert_eq!(
      vec![parent],
      ParsedEnvelope::from_transaction(&reveal_tx)[0]
        .payload
        .parents(),
    );
    assert_eq!(
      vec![parent],
      ParsedEnvelope::from_transaction(&reveal_tx)[1]
        .payload
        .parents(),
    );

    let sig_vbytes = 17;
    let fee = fee_rate.fee(commit_tx.vsize() + sig_vbytes).to_sat();

    let reveal_value = commit_tx
      .output
      .iter()
      .map(|o| o.value)
      .reduce(|acc, i| acc + i)
      .unwrap();

    assert_eq!(reveal_value.to_sat(), 50_000 - fee);

    assert_eq!(
      reveal_tx.output[0].script_pubkey,
      parent_info.destination.script_pubkey()
    );
    assert_eq!(reveal_tx.output[0].value, parent_info.tx_out.value);
    pretty_assert_eq!(
      reveal_tx.input[0],
      TxIn {
        previous_output: parent_info.location.outpoint,
        sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
        ..default()
      }
    );
  }
}

ord/src/wallet/batch/entry.rs


use super::*;

#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, Default, PartialEq, Debug, Clone)]
#[serde(deny_unknown_fields)]
pub struct Entry {
  pub delegate: Option<InscriptionId>,
  pub destination: Option<Address<NetworkUnchecked>>,
  pub file: Option<PathBuf>,
  #[serde(default)]
  pub gallery: Vec<InscriptionId>,
  pub metadata: Option<serde_yaml::Value>,
  pub metaprotocol: Option<String>,
  pub satpoint: Option<SatPoint>,
}

impl Entry {
  pub(crate) fn metadata(&self) -> Result<Option<Vec<u8>>> {
    match &self.metadata {
      None => Ok(None),
      Some(metadata) => {
        let mut cbor = Vec::new();
        ciborium::into_writer(&metadata, &mut cbor)?;
        Ok(Some(cbor))
      }
    }
  }

  pub(crate) fn properties(&self) -> Properties {
    Properties {
      gallery: self.gallery.clone(),
    }
  }
}

ord/src/wallet/batch/etching.rs


use super::*;

#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Etching {
  pub rune: SpacedRune,
  pub symbol: char,
  pub divisibility: u8,
  pub supply: Decimal,
  pub premine: Decimal,
  pub terms: Option<batch::Terms>,
  pub turbo: bool,
}

ord/src/wallet/batch/file.rs


use super::*;

#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct File {
  pub mode: Mode,
  #[serde(default)]
  pub parents: Vec<InscriptionId>,
  pub postage: Option<u64>,
  #[serde(default)]
  pub reinscribe: bool,
  pub sat: Option<Sat>,
  pub satpoint: Option<SatPoint>,
  pub inscriptions: Vec<batch::entry::Entry>,
  pub etching: Option<batch::Etching>,
}

impl File {
  pub(crate) fn load(path: &Path) -> Result<Self> {
    let batchfile: Self = serde_yaml::from_reader(fs::File::open(path)?)?;

    ensure!(
      !batchfile.inscriptions.is_empty(),
      "batchfile must contain at least one inscription",
    );

    let sat_and_satpoint = batchfile.sat.is_some() && batchfile.satpoint.is_some();

    ensure!(
      !sat_and_satpoint,
      "batchfile cannot set both `sat` and `satpoint`",
    );

    let sat_or_satpoint = batchfile.sat.is_some() || batchfile.satpoint.is_some();

    if sat_or_satpoint {
      ensure!(
        batchfile.mode == Mode::SameSat,
        "`sat` or `satpoint` can only be set in `same-sat` mode",
      );
    }

    if batchfile
      .inscriptions
      .iter()
      .any(|entry| entry.destination.is_some())
      && (batchfile.mode == Mode::SharedOutput || batchfile.mode == Mode::SameSat)
    {
      bail!(
        "individual inscription destinations cannot be set in `shared-output` or `same-sat` mode"
      );
    }

    for inscription in &batchfile.inscriptions {
      let mut items = BTreeSet::new();

      for item in &inscription.gallery {
        ensure! {
          items.insert(item),
          "duplicate gallery item: {item}",
        }
      }
    }

    let any_entry_has_satpoint = batchfile
      .inscriptions
      .iter()
      .any(|entry| entry.satpoint.is_some());

    if any_entry_has_satpoint {
      ensure!(
        batchfile.mode == Mode::SatPoints,
        "specifying `satpoint` in an inscription only works in `satpoints` mode"
      );

      ensure!(
        batchfile.inscriptions.iter().all(|entry| entry.satpoint.is_some()),
        "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint"
      );

      ensure!(
        batchfile
          .inscriptions
          .iter()
          .all(|entry| entry.satpoint.unwrap().offset == 0),
        "`satpoint` can only be specified for first sat of an output"
      );
    }

    if batchfile.mode == Mode::SatPoints {
      ensure!(
        batchfile.postage.is_none(),
        "`postage` cannot be set if in `satpoints` mode"
      );

      ensure!(
        batchfile.sat.is_none(),
        "`sat` cannot be set if in `satpoints` mode"
      );

      ensure!(
        batchfile.satpoint.is_none(),
        "`satpoint cannot be set if in `satpoints` mode"
      );

      let mut seen = HashSet::new();
      for entry in batchfile.inscriptions.iter() {
        let satpoint = entry.satpoint.unwrap_or_default();
        if !seen.insert(satpoint) {
          bail!("duplicate satpoint {}", satpoint);
        }
      }
    }

    Ok(batchfile)
  }

  pub(crate) fn inscriptions(
    &self,
    wallet: &Wallet,
    utxos: &BTreeMap<OutPoint, TxOut>,
    parent_values: Vec<u64>,
    compress: bool,
  ) -> Result<(
    Vec<Inscription>,
    Vec<(SatPoint, TxOut)>,
    Vec<Amount>,
    Vec<Address>,
  )> {
    let mut inscriptions = Vec::new();
    let mut reveal_satpoints = Vec::new();
    let mut postages = Vec::new();

    let mut pointer = parent_values.iter().sum();

    for (i, entry) in self.inscriptions.iter().enumerate() {
      if let Some(delegate) = entry.delegate {
        ensure! {
          wallet.inscription_exists(delegate)?,
          "delegate {delegate} does not exist"
        }
      }

      inscriptions.push(Inscription::new(
        wallet.chain(),
        compress,
        entry.delegate,
        entry.metadata()?,
        entry.metaprotocol.clone(),
        self.parents.clone(),
        entry.file.clone(),
        Some(pointer),
        entry.properties(),
        self
          .etching
          .and_then(|etch| (i == 0).then_some(etch.rune.rune)),
      )?);

      let postage = if self.mode == Mode::SatPoints {
        let satpoint = entry
          .satpoint
          .ok_or_else(|| anyhow!("no satpoint specified for entry {i}"))?;

        let txout = utxos
          .get(&satpoint.outpoint)
          .ok_or_else(|| anyhow!("{} not in wallet", satpoint))?;

        reveal_satpoints.push((satpoint, txout.clone()));

        txout.value
      } else {
        self.postage.map(Amount::from_sat).unwrap_or(TARGET_POSTAGE)
      };

      if self.mode != Mode::SameSat {
        pointer += postage.to_sat();
      }

      if self.mode == Mode::SameSat && i > 0 {
        continue;
      } else {
        postages.push(postage);
      }
    }

    let destinations = match self.mode {
      Mode::SharedOutput | Mode::SameSat => vec![wallet.get_change_address()?],
      Mode::SeparateOutputs | Mode::SatPoints => self
        .inscriptions
        .iter()
        .map(|entry| {
          entry.destination.as_ref().map_or_else(
            || wallet.get_change_address(),
            |address| {
              address
                .clone()
                .require_network(wallet.chain().network())
                .map_err(|e| e.into())
            },
          )
        })
        .collect::<Result<Vec<_>, _>>()?,
    };

    Ok((inscriptions, reveal_satpoints, postages, destinations))
  }
}

#[cfg(test)]
mod tests {
  use {super::*, pretty_assertions::assert_eq};

  #[test]
  fn batchfile_not_sat_and_satpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: same-sat
sat: 55555
satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
inscriptions:
- file: inscription.txt
- file: tulip.png
- file: meow.wav
"#,
    )
    .unwrap();

    assert_eq!(
      File::load(batch_file.as_path()).unwrap_err().to_string(),
      "batchfile cannot set both `sat` and `satpoint`"
    );
  }

  #[test]
  fn batchfile_wrong_mode_for_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: separate-outputs
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "specifying `satpoint` in an inscription only works in `satpoints` mode"
    );
  }

  #[test]
  fn batchfile_missing_satpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "if `satpoint` is set for any inscription, then all inscriptions need to specify a satpoint"
    );
  }

  #[test]
  fn batchfile_only_first_sat_of_outpoint() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:21
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "`satpoint` can only be specified for first sat of an output"
    );
  }

  #[test]
  fn batchfile_no_postage_if_mode_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
postage: 1111
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "`postage` cannot be set if in `satpoints` mode"
    );
  }

  #[test]
  fn batchfile_no_duplicate_satpoints() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: satpoints
inscriptions:
- file: inscription.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
- file: tulip.png
  satpoint: 5fddcbdc3eb21a93e8dd1dd3f9087c3677f422b82d5ba39a6b1ec37338154af6:0:0
- file: meow.wav
  satpoint: 4651dc5e964879b1eb9183d467d1187dcd252504698002b01853446c460db2c5:0:0
- file: inscription_1.txt
  satpoint: bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "duplicate satpoint bc4c30829a9564c0d58e6287195622b53ced54a25711d1b86be7cd3a70ef61ed:0:0"
    );
  }

  #[test]
  fn example_batchfile_deserializes_successfully() {
    assert_eq!(
      batch::File::load(Path::new("batch.yaml")).unwrap(),
      batch::File {
        mode: batch::Mode::SeparateOutputs,
        parents: vec![
          "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
            .parse()
            .unwrap()
        ],
        postage: Some(12345),
        reinscribe: true,
        sat: None,
        satpoint: None,
        etching: Some(Etching {
          rune: "THE•BEST•RUNE".parse().unwrap(),
          divisibility: 2,
          premine: "1000.00".parse().unwrap(),
          supply: "10000.00".parse().unwrap(),
          symbol: '$',
          terms: Some(batch::Terms {
            amount: "100.00".parse().unwrap(),
            cap: 90,
            height: Some(batch::Range {
              start: Some(840000),
              end: Some(850000),
            }),
            offset: Some(batch::Range {
              start: Some(1000),
              end: Some(9000),
            }),
          }),
          turbo: true,
        }),
        inscriptions: vec![
          batch::Entry {
            file: Some("mango.avif".into()),
            delegate: Some(
              "6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
                .parse()
                .unwrap()
            ),
            destination: Some(
              "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4"
                .parse()
                .unwrap()
            ),
            metadata: Some(serde_yaml::Value::Mapping({
              let mut mapping = serde_yaml::Mapping::new();
              mapping.insert("title".into(), "Delicious Mangos".into());
              mapping.insert(
                "description".into(),
                "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam semper, \
                ligula ornare laoreet tincidunt, odio nisi euismod tortor, vel blandit \
                metus est et odio. Nullam venenatis, urna et molestie vestibulum, orci \
                mi efficitur risus, eu malesuada diam lorem sed velit. Nam fermentum \
                dolor et luctus euismod.\n"
                  .into(),
              );
              mapping
            })),
            ..default()
          },
          batch::Entry {
            file: Some("token.json".into()),
            metaprotocol: Some("DOPEPROTOCOL-42069".into()),
            ..default()
          },
          batch::Entry {
            file: Some("tulip.png".into()),
            destination: Some(
              "bc1pdqrcrxa8vx6gy75mfdfj84puhxffh4fq46h3gkp6jxdd0vjcsdyspfxcv6"
                .parse()
                .unwrap()
            ),
            metadata: Some(serde_yaml::Value::Mapping({
              let mut mapping = serde_yaml::Mapping::new();
              mapping.insert("author".into(), "Satoshi Nakamoto".into());
              mapping
            })),
            ..default()
          },
          batch::Entry {
            file: Some("gallery.png".into()),
            gallery: vec![
              "a4676e57277b70171d69dc6ad2781485b491fe0ff5870f6f6b01999e7180b29ei0"
                .parse()
                .unwrap(),
              "a4676e57277b70171d69dc6ad2781485b491fe0ff5870f6f6b01999e7180b29ei3"
                .parse()
                .unwrap(),
            ],
            ..default()
          },
        ],
      }
    );
  }

  #[test]
  fn batchfile_no_delegate_no_file_allowed() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: shared-output
inscriptions:
  -
"#,
    )
    .unwrap();

    assert!(batch::File::load(batch_file.as_path()).is_ok());
  }

  #[test]
  fn batchfile_no_duplicate_gallery_items() {
    let tempdir = TempDir::new().unwrap();
    let batch_file = tempdir.path().join("batch.yaml");
    fs::write(
      batch_file.clone(),
      r#"
mode: separate-outputs
inscriptions:
- file: inscription.txt
  gallery:
  - 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0
  - 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0
"#,
    )
    .unwrap();

    assert_eq!(
      batch::File::load(batch_file.as_path())
        .unwrap_err()
        .to_string(),
      "duplicate gallery item: 6ac5cacb768794f4fd7a78bf00f2074891fce68bd65c4ff36e77177237aacacai0"
    );
  }
}

ord/src/wallet/batch/mode.rs


use super::*;

#[derive(PartialEq, Debug, Copy, Clone, Serialize, Deserialize, Default)]
pub enum Mode {
  #[serde(rename = "same-sat")]
  SameSat,
  #[serde(rename = "satpoints")]
  SatPoints,
  #[default]
  #[serde(rename = "separate-outputs")]
  SeparateOutputs,
  #[serde(rename = "shared-output")]
  SharedOutput,
}

ord/src/wallet/batch/plan.rs


use super::*;

pub struct Plan {
  pub(crate) commit_fee_rate: FeeRate,
  pub(crate) destinations: Vec<Address>,
  pub(crate) dry_run: bool,
  pub(crate) etching: Option<Etching>,
  pub(crate) inscriptions: Vec<Inscription>,
  pub(crate) mode: Mode,
  pub(crate) no_backup: bool,
  pub(crate) no_limit: bool,
  pub(crate) parent_info: Vec<ParentInfo>,
  pub(crate) postages: Vec<Amount>,
  pub(crate) reinscribe: bool,
  pub(crate) reveal_fee_rate: FeeRate,
  pub(crate) reveal_satpoints: Vec<(SatPoint, TxOut)>,
  pub(crate) satpoint: Option<SatPoint>,
}

impl Default for Plan {
  fn default() -> Self {
    Self {
      commit_fee_rate: 1.0.try_into().unwrap(),
      destinations: Vec::new(),
      dry_run: false,
      etching: None,
      inscriptions: Vec::new(),
      mode: Mode::SharedOutput,
      no_backup: false,
      no_limit: false,
      parent_info: Vec::new(),
      postages: vec![Amount::from_sat(10_000)],
      reinscribe: false,
      reveal_fee_rate: 1.0.try_into().unwrap(),
      reveal_satpoints: Vec::new(),
      satpoint: None,
    }
  }
}

impl Plan {
  pub(crate) fn inscribe(
    &self,
    locked_utxos: &BTreeSet<OutPoint>,
    runic_utxos: BTreeSet<OutPoint>,
    utxos: &BTreeMap<OutPoint, TxOut>,
    wallet: &Wallet,
  ) -> SubcommandResult {
    let Transactions {
      commit_tx,
      commit_vout,
      reveal_tx,
      recovery_key_pair,
      total_fees,
      rune,
    } = self.create_batch_transactions(
      wallet.inscriptions().clone(),
      wallet.chain(),
      locked_utxos.clone(),
      runic_utxos,
      utxos.clone(),
      [wallet.get_change_address()?, wallet.get_change_address()?],
      wallet.get_change_address()?,
    )?;

    if self.dry_run {
      let commit_psbt = wallet
        .bitcoin_client()
        .wallet_process_psbt(
          &base64_encode(
            &Psbt::from_unsigned_tx(Self::remove_witnesses(commit_tx.clone()))?.serialize(),
          ),
          Some(false),
          None,
          None,
        )?
        .psbt;

      let reveal_psbt = Psbt::from_unsigned_tx(Self::remove_witnesses(reveal_tx.clone()))?;

      return Ok(Some(Box::new(self.output(
        commit_tx.compute_txid(),
        Some(commit_psbt),
        reveal_tx.compute_txid(),
        false,
        Some(base64_encode(&reveal_psbt.serialize())),
        total_fees,
        self.inscriptions.clone(),
        rune,
      ))));
    }

    let signed_commit_tx = wallet
      .bitcoin_client()
      .sign_raw_transaction_with_wallet(&commit_tx, None, None)?
      .hex;

    let result = wallet.bitcoin_client().sign_raw_transaction_with_wallet(
      &reveal_tx,
      Some(
        &commit_tx
          .output
          .iter()
          .enumerate()
          .map(|(vout, output)| SignRawTransactionInput {
            txid: commit_tx.compute_txid(),
            vout: vout.try_into().unwrap(),
            script_pub_key: output.script_pubkey.clone(),
            redeem_script: None,
            amount: Some(output.value),
          })
          .collect::<Vec<SignRawTransactionInput>>(),
      ),
      None,
    )?;

    ensure!(
      result.complete,
      format!("Failed to sign reveal transaction: {:?}", result.errors)
    );

    let signed_reveal_tx = result.hex;

    if !self.no_backup {
      Self::backup_recovery_key(wallet, recovery_key_pair)?;
    }

    let commit_txid = wallet
      .bitcoin_client()
      .send_raw_transaction(&signed_commit_tx)?;

    if let Some(ref rune_info) = rune {
      wallet.bitcoin_client().lock_unspent(&[OutPoint {
        txid: commit_txid,
        vout: commit_vout.try_into().unwrap(),
      }])?;

      let commit = consensus::encode::deserialize::<Transaction>(&signed_commit_tx)?;
      let reveal = consensus::encode::deserialize::<Transaction>(&signed_reveal_tx)?;

      wallet.save_etching(
        &rune_info.rune.rune,
        &commit,
        &reveal,
        self.output(
          commit.compute_txid(),
          None,
          reveal.compute_txid(),
          false,
          None,
          total_fees,
          self.inscriptions.clone(),
          rune.clone(),
        ),
      )?;

      Ok(Some(Box::new(
        wallet.wait_for_maturation(rune_info.rune.rune)?,
      )))
    } else {
      let reveal = match wallet
        .bitcoin_client()
        .send_raw_transaction(&signed_reveal_tx)
      {
        Ok(txid) => txid,
        Err(err) => {
          return Err(anyhow!(
        "Failed to send reveal transaction: {err}\nCommit tx {commit_txid} will be recovered once mined"
      ))
        }
      };

      Ok(Some(Box::new(self.output(
        commit_txid,
        None,
        reveal,
        true,
        None,
        total_fees,
        self.inscriptions.clone(),
        rune,
      ))))
    }
  }

  fn remove_witnesses(mut transaction: Transaction) -> Transaction {
    for txin in transaction.input.iter_mut() {
      txin.witness = Witness::new();
    }

    transaction
  }

  fn output(
    &self,
    commit: Txid,
    commit_psbt: Option<String>,
    reveal: Txid,
    reveal_broadcast: bool,
    reveal_psbt: Option<String>,
    total_fees: u64,
    inscriptions: Vec<Inscription>,
    rune: Option<RuneInfo>,
  ) -> Output {
    let mut inscriptions_output = Vec::new();
    for i in 0..inscriptions.len() {
      let index = u32::try_from(i).unwrap();

      let vout = match self.mode {
        Mode::SharedOutput | Mode::SameSat => self.parent_info.len().try_into().unwrap(),
        Mode::SeparateOutputs | Mode::SatPoints => {
          index + u32::try_from(self.parent_info.len()).unwrap()
        }
      };

      let offset = match self.mode {
        Mode::SharedOutput => self.postages[0..i]
          .iter()
          .map(|amount| amount.to_sat())
          .sum(),
        Mode::SeparateOutputs | Mode::SameSat | Mode::SatPoints => 0,
      };

      let destination = match self.mode {
        Mode::SameSat | Mode::SharedOutput => &self.destinations[0],
        Mode::SatPoints | Mode::SeparateOutputs => &self.destinations[i],
      };

      inscriptions_output.push(InscriptionInfo {
        id: InscriptionId {
          txid: reveal,
          index,
        },
        destination: uncheck(destination),
        location: SatPoint {
          outpoint: OutPoint { txid: reveal, vout },
          offset,
        },
      });
    }

    Output {
      commit,
      commit_psbt,
      inscriptions: inscriptions_output,
      parents: self.parent_info.iter().map(|info| info.id).collect(),
      reveal,
      reveal_broadcast,
      reveal_psbt,
      rune,
      total_fees,
    }
  }

  pub(crate) fn create_batch_transactions(
    &self,
    wallet_inscriptions: BTreeMap<SatPoint, Vec<InscriptionId>>,
    chain: Chain,
    locked_utxos: BTreeSet<OutPoint>,
    runic_utxos: BTreeSet<OutPoint>,
    mut utxos: BTreeMap<OutPoint, TxOut>,
    commit_change: [Address; 2],
    reveal_change: Address,
  ) -> Result<Transactions> {
    for inscription in &self.inscriptions {
      assert_eq!(
        inscription.parents(),
        self
          .parent_info
          .iter()
          .map(|info| info.id)
          .collect::<Vec<InscriptionId>>()
      );
    }

    match self.mode {
      Mode::SameSat => {
        assert_eq!(
          self.postages.len(),
          1,
          "invariant: same-sat has only one postage"
        );
        assert_eq!(
          self.destinations.len(),
          1,
          "invariant: same-sat has only one destination"
        );
      }
      Mode::SeparateOutputs | Mode::SatPoints => {
        assert_eq!(
          self.destinations.len(),
          self.inscriptions.len(),
          "invariant: destination addresses and number of inscriptions doesn't match"
        );
        assert_eq!(
          self.destinations.len(),
          self.postages.len(),
          "invariant: destination addresses and number of postages doesn't match"
        );
      }
      Mode::SharedOutput => {
        assert_eq!(
          self.destinations.len(),
          1,
          "invariant: shared-output has only one destination"
        );
        assert_eq!(
          self.postages.len(),
          self.inscriptions.len(),
          "invariant: postages and number of inscriptions doesn't match"
        );
      }
    }

    let satpoint = if let Some(satpoint) = self.satpoint {
      satpoint
    } else {
      let inscribed_utxos = wallet_inscriptions
        .keys()
        .map(|satpoint| satpoint.outpoint)
        .collect::<BTreeSet<OutPoint>>();

      utxos
        .iter()
        .find(|(outpoint, txout)| {
          txout.value.to_sat() > 0
            && !inscribed_utxos.contains(outpoint)
            && !locked_utxos.contains(outpoint)
            && !runic_utxos.contains(outpoint)
        })
        .map(|(outpoint, _amount)| SatPoint {
          outpoint: *outpoint,
          offset: 0,
        })
        .ok_or_else(|| anyhow!("wallet contains no cardinal utxos"))?
    };

    let mut reinscription = false;

    for (inscribed_satpoint, inscription_ids) in &wallet_inscriptions {
      if *inscribed_satpoint == satpoint {
        reinscription = true;
        if self.reinscribe {
          continue;
        }

        bail!("sat at {} already inscribed", satpoint);
      }

      if inscribed_satpoint.outpoint == satpoint.outpoint {
        bail!(
          "utxo {} with sat {inscribed_satpoint} already inscribed with the following inscriptions:\n{}",
          satpoint.outpoint,
          inscription_ids
            .iter()
            .map(ToString::to_string)
            .collect::<Vec<String>>()
            .join("\n"),
        );
      }
    }

    if self.reinscribe && !reinscription {
      bail!("reinscribe flag set but this would not be a reinscription");
    }

    let secp256k1 = Secp256k1::new();
    let key_pair = UntweakedKeypair::new(&secp256k1, &mut rand::thread_rng());
    let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair);

    let reveal_script = Inscription::append_batch_reveal_script(
      &self.inscriptions,
      ScriptBuf::builder()
        .push_slice(public_key.serialize())
        .push_opcode(opcodes::all::OP_CHECKSIG),
    );

    let taproot_spend_info = TaprootBuilder::new()
      .add_leaf(0, reveal_script.clone())
      .expect("adding leaf should work")
      .finalize(&secp256k1, public_key)
      .expect("finalizing taproot builder should work");

    let control_block = taproot_spend_info
      .control_block(&(reveal_script.clone(), LeafVersion::TapScript))
      .expect("should compute control block");

    let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), chain.network());

    let total_postage = self.postages.clone().into_iter().sum();

    let mut reveal_inputs = Vec::new();
    let mut reveal_outputs = Vec::new();

    for ParentInfo {
      location,
      id: _,
      destination,
      tx_out,
    } in &self.parent_info
    {
      reveal_inputs.push(location.outpoint);
      reveal_outputs.push(TxOut {
        script_pubkey: destination.script_pubkey(),
        value: tx_out.value,
      });
    }

    if self.mode == Mode::SatPoints {
      for (satpoint, _txout) in self.reveal_satpoints.iter() {
        reveal_inputs.push(satpoint.outpoint);
      }
    }

    reveal_inputs.push(OutPoint::null());

    for (i, destination) in self.destinations.iter().enumerate() {
      reveal_outputs.push(TxOut {
        script_pubkey: destination.script_pubkey(),
        value: match self.mode {
          Mode::SeparateOutputs | Mode::SatPoints => self.postages[i],
          Mode::SharedOutput | Mode::SameSat => total_postage,
        },
      });
    }

    let rune;
    let premine;
    let runestone;

    if let Some(etching) = self.etching {
      let vout;
      let destination;
      premine = etching.premine.to_integer(etching.divisibility)?;

      if premine > 0 {
        let output = u32::try_from(reveal_outputs.len()).unwrap();
        destination = Some(reveal_change.clone());

        reveal_outputs.push(TxOut {
          script_pubkey: reveal_change.into(),
          value: TARGET_POSTAGE,
        });

        vout = Some(output);
      } else {
        vout = None;
        destination = None;
      }

      let inner = Runestone {
        edicts: Vec::new(),
        etching: Some(ordinals::Etching {
          divisibility: (etching.divisibility > 0).then_some(etching.divisibility),
          premine: (premine > 0).then_some(premine),
          rune: Some(etching.rune.rune),
          spacers: (etching.rune.spacers > 0).then_some(etching.rune.spacers),
          symbol: Some(etching.symbol),
          terms: etching
            .terms
            .map(|terms| -> Result<ordinals::Terms> {
              Ok(ordinals::Terms {
                cap: (terms.cap > 0).then_some(terms.cap),
                height: (
                  terms.height.and_then(|range| (range.start)),
                  terms.height.and_then(|range| (range.end)),
                ),
                amount: Some(terms.amount.to_integer(etching.divisibility)?),
                offset: (
                  terms.offset.and_then(|range| (range.start)),
                  terms.offset.and_then(|range| (range.end)),
                ),
              })
            })
            .transpose()?,
          turbo: etching.turbo,
        }),
        mint: None,
        pointer: (premine > 0).then_some((reveal_outputs.len() - 1).try_into().unwrap()),
      };

      let script_pubkey = inner.encipher();

      runestone = Some(inner);

      ensure!(
        self.no_limit || script_pubkey.len() <= MAX_STANDARD_OP_RETURN_SIZE,
        "runestone greater than maximum OP_RETURN size: {} > {}",
        script_pubkey.len(),
        MAX_STANDARD_OP_RETURN_SIZE,
      );

      reveal_outputs.push(TxOut {
        script_pubkey,
        value: Amount::from_sat(0),
      });

      rune = Some((destination, etching.rune, vout));
    } else {
      premine = 0;
      rune = None;
      runestone = None;
    }

    let commit_input = self.parent_info.len() + self.reveal_satpoints.len();

    let (_reveal_tx, reveal_fee) = Self::build_reveal_transaction(
      commit_input,
      &control_block,
      self.reveal_fee_rate,
      reveal_outputs.clone(),
      reveal_inputs.clone(),
      &reveal_script,
      rune.is_some(),
    );

    let mut target_value = reveal_fee;

    if self.mode != Mode::SatPoints {
      target_value += total_postage;
    }

    if premine > 0 {
      target_value += TARGET_POSTAGE;
    }

    let unsigned_commit_tx = TransactionBuilder::new(
      satpoint,
      wallet_inscriptions,
      utxos.clone(),
      locked_utxos.clone(),
      runic_utxos,
      commit_tx_address.script_pubkey(),
      commit_change,
      self.commit_fee_rate,
      Target::Value(target_value),
      chain.network(),
    )
    .build_transaction()?;

    let (vout, _commit_output) = unsigned_commit_tx
      .output
      .iter()
      .enumerate()
      .find(|(_vout, output)| output.script_pubkey == commit_tx_address.script_pubkey())
      .expect("should find sat commit/inscription output");

    reveal_inputs[commit_input] = OutPoint {
      txid: unsigned_commit_tx.compute_txid(),
      vout: vout.try_into().unwrap(),
    };

    let (mut reveal_tx, _fee) = Self::build_reveal_transaction(
      commit_input,
      &control_block,
      self.reveal_fee_rate,
      reveal_outputs.clone(),
      reveal_inputs,
      &reveal_script,
      rune.is_some(),
    );

    for output in reveal_tx.output.iter() {
      ensure!(
        output.value >= output.script_pubkey.minimal_non_dust(),
        "commit transaction output would be dust"
      );
    }

    let mut prevouts = Vec::new();

    for parent_info in &self.parent_info {
      prevouts.push(parent_info.tx_out.clone());
    }

    if self.mode == Mode::SatPoints {
      for (_satpoint, txout) in self.reveal_satpoints.iter() {
        prevouts.push(txout.clone());
      }
    }

    prevouts.push(unsigned_commit_tx.output[vout].clone());

    let mut sighash_cache = SighashCache::new(&mut reveal_tx);

    let sighash = sighash_cache
      .taproot_script_spend_signature_hash(
        commit_input,
        &Prevouts::All(&prevouts),
        TapLeafHash::from_script(&reveal_script, LeafVersion::TapScript),
        TapSighashType::Default,
      )
      .expect("signature hash should compute");

    let signature = secp256k1.sign_schnorr(
      &secp256k1::Message::from_digest_slice(sighash.as_ref())
        .expect("should be cryptographically secure hash"),
      &key_pair,
    );

    let witness = sighash_cache
      .witness_mut(commit_input)
      .expect("getting mutable witness reference should work");

    witness.push(
      Signature {
        signature,
        sighash_type: TapSighashType::Default,
      }
      .to_vec(),
    );

    witness.push(reveal_script);
    witness.push(control_block.serialize());

    let recovery_key_pair = key_pair.tap_tweak(&secp256k1, taproot_spend_info.merkle_root());

    let (x_only_pub_key, _parity) = recovery_key_pair.to_keypair().x_only_public_key();
    assert_eq!(
      Address::p2tr_tweaked(
        TweakedPublicKey::dangerous_assume_tweaked(x_only_pub_key),
        chain.network(),
      ),
      commit_tx_address
    );

    let reveal_weight = reveal_tx.weight();

    if !self.no_limit && reveal_weight > bitcoin::Weight::from_wu(MAX_STANDARD_TX_WEIGHT.into()) {
      bail!(
        "reveal transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): {reveal_weight}"
      );
    }

    utxos.insert(
      reveal_tx.input[commit_input].previous_output,
      unsigned_commit_tx.output[reveal_tx.input[commit_input].previous_output.vout as usize]
        .clone(),
    );

    let total_fees =
      Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos);

    match (Runestone::decipher(&reveal_tx), runestone) {
      (Some(actual), Some(expected)) => assert_eq!(
        actual,
        Artifact::Runestone(expected),
        "commit transaction runestone did not match expected runestone"
      ),
      (Some(_), None) => panic!("commit transaction contained runestone, but none was expected"),
      (None, Some(_)) => {
        panic!("commit transaction did not contain runestone, but one was expected")
      }
      (None, None) => {}
    }

    let rune = rune.map(|(destination, rune, vout)| RuneInfo {
      destination: destination.map(|destination| uncheck(&destination)),
      location: vout.map(|vout| OutPoint {
        txid: reveal_tx.compute_txid(),
        vout,
      }),
      rune,
    });

    Ok(Transactions {
      commit_tx: unsigned_commit_tx,
      commit_vout: vout,
      recovery_key_pair,
      reveal_tx,
      rune,
      total_fees,
    })
  }

  fn backup_recovery_key(wallet: &Wallet, recovery_key_pair: TweakedKeypair) -> Result {
    let recovery_private_key = PrivateKey::new(
      recovery_key_pair.to_keypair().secret_key(),
      wallet.chain().network(),
    );

    let info = wallet
      .bitcoin_client()
      .get_descriptor_info(&format!("rawtr({})", recovery_private_key.to_wif()))?;

    let response = wallet
      .bitcoin_client()
      .import_descriptors(ImportDescriptors {
        descriptor: format!(
          "rawtr({})#{}",
          recovery_private_key.to_wif(),
          info.checksum.unwrap_or_default()
        ),
        timestamp: Timestamp::Now,
        active: Some(false),
        range: None,
        next_index: None,
        internal: Some(false),
        label: Some("commit tx recovery key".to_string()),
      })?;

    for result in response {
      if !result.success {
        return Err(anyhow!("commit tx recovery key import failed"));
      }
    }

    Ok(())
  }

  fn build_reveal_transaction(
    commit_input_index: usize,
    control_block: &ControlBlock,
    fee_rate: FeeRate,
    output: Vec<TxOut>,
    input: Vec<OutPoint>,
    script: &Script,
    etching: bool,
  ) -> (Transaction, Amount) {
    let reveal_tx = Transaction {
      input: input
        .into_iter()
        .map(|previous_output| TxIn {
          previous_output,
          script_sig: script::Builder::new().into_script(),
          witness: Witness::new(),
          sequence: if etching {
            Sequence::from_height(Runestone::COMMIT_CONFIRMATIONS - 1)
          } else {
            Sequence::ENABLE_RBF_NO_LOCKTIME
          },
        })
        .collect(),
      output,
      lock_time: LockTime::ZERO,
      version: Version(2),
    };

    let fee = {
      let mut reveal_tx = reveal_tx.clone();

      for (current_index, txin) in reveal_tx.input.iter_mut().enumerate() {
        // add dummy inscription witness for reveal input/commit output
        if current_index == commit_input_index {
          txin.witness.push(
            Signature::from_slice(&[0; SCHNORR_SIGNATURE_SIZE])
              .unwrap()
              .to_vec(),
          );
          txin.witness.push(script);
          txin.witness.push(control_block.serialize());
        } else {
          txin.witness = Witness::from_slice(&[&[0; SCHNORR_SIGNATURE_SIZE]]);
        }
      }

      fee_rate.fee(reveal_tx.vsize())
    };

    (reveal_tx, fee)
  }

  fn calculate_fee(tx: &Transaction, utxos: &BTreeMap<OutPoint, TxOut>) -> u64 {
    tx.input
      .iter()
      .map(|txin| utxos.get(&txin.previous_output).unwrap().value)
      .sum::<Amount>()
      .checked_sub(tx.output.iter().map(|txout| txout.value).sum::<Amount>())
      .unwrap()
      .to_sat()
  }
}

ord/src/wallet/batch/range.rs


use super::*;

#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Range {
  pub start: Option<u64>,
  pub end: Option<u64>,
}

ord/src/wallet/batch/terms.rs


use super::*;

#[serde_with::skip_serializing_none]
#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)]
#[serde(deny_unknown_fields)]
pub struct Terms {
  pub amount: Decimal,
  pub cap: u128,
  pub height: Option<Range>,
  pub offset: Option<Range>,
}

ord/src/wallet/batch/transactions.rs


use super::*;

#[derive(Debug)]
pub(crate) struct Transactions {
  pub(crate) rune: Option<RuneInfo>,
  pub(crate) commit_tx: Transaction,
  pub(crate) commit_vout: usize,
  pub(crate) recovery_key_pair: TweakedKeypair,
  pub(crate) reveal_tx: Transaction,
  pub(crate) total_fees: u64,
}

ord/src/wallet/entry.rs


use super::*;

#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct EtchingEntry {
  pub commit: Transaction,
  pub reveal: Transaction,
  pub output: batch::Output,
}

pub(super) type EtchingEntryValue = (
  Vec<u8>, // commit
  Vec<u8>, // reveal
  Vec<u8>, // output
);

impl Entry for EtchingEntry {
  type Value = EtchingEntryValue;

  fn load((commit, reveal, output): EtchingEntryValue) -> Self {
    Self {
      commit: consensus::encode::deserialize::<Transaction>(&commit).unwrap(),
      reveal: consensus::encode::deserialize::<Transaction>(&reveal).unwrap(),
      output: serde_json::from_slice(&output).unwrap(),
    }
  }

  fn store(self) -> Self::Value {
    (
      consensus::encode::serialize(&self.commit),
      consensus::encode::serialize(&self.reveal),
      serde_json::to_string(&self.output)
        .unwrap()
        .as_bytes()
        .to_owned(),
    )
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn etching_entry() {
    let commit = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: vec![TxIn {
        previous_output: OutPoint::null(),
        script_sig: ScriptBuf::new(),
        sequence: Sequence::MAX,
        witness: Witness::new(),
      }],
      output: Vec::new(),
    };

    let reveal = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: vec![TxIn {
        previous_output: OutPoint::null(),
        script_sig: ScriptBuf::new(),
        sequence: Sequence::default(),
        witness: Witness::new(),
      }],
      output: Vec::new(),
    };

    let txid = Txid::from_byte_array([
      0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
      0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D,
      0x1E, 0x1F,
    ]);

    let output = batch::Output {
      commit: txid,
      commit_psbt: None,
      inscriptions: Vec::new(),
      parents: Vec::new(),
      reveal: txid,
      reveal_broadcast: true,
      reveal_psbt: None,
      rune: None,
      total_fees: 0,
    };

    let value = (
      consensus::encode::serialize(&commit),
      consensus::encode::serialize(&reveal),
      serde_json::to_string(&output)
        .unwrap()
        .as_bytes()
        .to_owned(),
    );

    let entry = EtchingEntry {
      commit,
      reveal,
      output,
    };

    assert_eq!(entry.clone().store(), value);
    assert_eq!(EtchingEntry::load(value), entry);
  }
}

ord/src/wallet/transaction_builder.rs


//! Ordinal transaction construction is fraught.
//!
//! Ordinal-aware transaction construction has additional invariants,
//! constraints, and concerns in addition to those of normal, non-ordinal-aware
//! Bitcoin transactions.
//!
//! This module contains a `TransactionBuilder` struct that facilitates
//! constructing ordinal-aware transactions that take these additional
//! conditions into account.
//!
//! The external interface is `TransactionBuilder::new`, which returns a
//! constructed transaction given the `Target`, which include the outgoing sat
//! to send, the wallets current UTXOs and their sat ranges, and the
//! recipient's address. To build the transaction call
//! `Transaction::build_transaction`.
//!
//! `Target::Postage` ensures that the outgoing value is at most 20,000 sats,
//! reducing it to 10,000 sats if coin selection requires adding excess value.
//!
//! `Target::Value(Amount)` ensures that the outgoing value is exactly the
//! requested amount,
//!
//! Internally, `TransactionBuilder` calls multiple methods that implement
//! transformations responsible for individual concerns, such as ensuring that
//! the transaction fee is paid, and that outgoing outputs aren't too large.
//!
//! This module is heavily tested. For all features of transaction
//! construction, there should be a positive test that checks that the feature
//! is implemented correctly, an assertion in the final
//! `Transaction::build_transaction` method that the built transaction is
//! correct with respect to the feature, and a test that the assertion fires as
//! expected.

use {
  super::*,
  std::cmp::{max, min},
};

#[derive(Debug, PartialEq)]
pub enum Error {
  DuplicateAddress(Address),
  Dust {
    output_value: Amount,
    dust_value: Amount,
  },
  InvalidAddress(bitcoin::address::FromScriptError),
  NotEnoughCardinalUtxos,
  NotInWallet(SatPoint),
  OutOfRange(SatPoint, u64),
  UtxoContainsAdditionalInscriptions {
    outgoing_satpoint: SatPoint,
    inscribed_satpoint: SatPoint,
    inscription_ids: Vec<InscriptionId>,
  },
  ValueOverflow,
}

#[derive(Debug, PartialEq)]
pub enum Target {
  Value(Amount),
  Postage,
  ExactPostage(Amount),
}

impl Display for Error {
  fn fmt(&self, f: &mut Formatter) -> fmt::Result {
    match self {
      Error::Dust {
        output_value,
        dust_value,
      } => write!(f, "output value is below dust value: {output_value} < {dust_value}"),
      Error::InvalidAddress(source) => write!(f, "invalid address: {source}", ),
      Error::NotInWallet(outgoing_satpoint) => write!(f, "outgoing satpoint {outgoing_satpoint} not in wallet"),
      Error::OutOfRange(outgoing_satpoint, maximum) => write!(f, "outgoing satpoint {outgoing_satpoint} offset higher than maximum {maximum}"),
      Error::NotEnoughCardinalUtxos => write!(
        f,
        "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet."
      ),
      Error::UtxoContainsAdditionalInscriptions {
        outgoing_satpoint,
        inscribed_satpoint,
        inscription_ids,
      } => write!(
        f,
        "cannot send {outgoing_satpoint} without also sending inscription {} at {inscribed_satpoint}",
        inscription_ids.iter().map(ToString::to_string).collect::<Vec<String>>().join(", "),
      ),
      Error::ValueOverflow => write!(f, "arithmetic overflow calculating value"),
      Error::DuplicateAddress(address) => write!(f, "duplicate input address: {address}"),
    }
  }
}

impl std::error::Error for Error {}

impl From<bitcoin::address::FromScriptError> for Error {
  fn from(source: bitcoin::address::FromScriptError) -> Self {
    Self::InvalidAddress(source)
  }
}

#[derive(Debug, PartialEq)]
pub struct TransactionBuilder {
  amounts: BTreeMap<OutPoint, TxOut>,
  change_addresses: BTreeSet<Address>,
  fee_rate: FeeRate,
  inputs: Vec<OutPoint>,
  inscriptions: BTreeMap<SatPoint, Vec<InscriptionId>>,
  locked_utxos: BTreeSet<OutPoint>,
  network: Network,
  outgoing: SatPoint,
  outputs: Vec<TxOut>,
  recipient: ScriptBuf,
  runic_utxos: BTreeSet<OutPoint>,
  target: Target,
  unused_change_addresses: Vec<Address>,
  utxos: BTreeSet<OutPoint>,
}

type Result<T> = std::result::Result<T, Error>;

impl TransactionBuilder {
  const ADDITIONAL_INPUT_VBYTES: usize = 57;
  const ADDITIONAL_OUTPUT_VBYTES: usize = 43;
  const SCHNORR_SIGNATURE_SIZE: usize = 64;
  pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000);

  pub fn new(
    outgoing: SatPoint,
    inscriptions: BTreeMap<SatPoint, Vec<InscriptionId>>,
    amounts: BTreeMap<OutPoint, TxOut>,
    locked_utxos: BTreeSet<OutPoint>,
    runic_utxos: BTreeSet<OutPoint>,
    recipient: ScriptBuf,
    change: [Address; 2],
    fee_rate: FeeRate,
    target: Target,
    network: Network,
  ) -> Self {
    Self {
      utxos: amounts.keys().cloned().collect(),
      amounts,
      change_addresses: change.iter().cloned().collect(),
      fee_rate,
      inputs: Vec::new(),
      inscriptions,
      locked_utxos,
      outgoing,
      outputs: Vec::new(),
      recipient,
      runic_utxos,
      target,
      unused_change_addresses: change.to_vec(),
      network,
    }
  }

  pub fn build_transaction(self) -> Result<Transaction> {
    if self.change_addresses.len() < 2 {
      return Err(Error::DuplicateAddress(
        self.change_addresses.first().unwrap().clone(),
      ));
    }

    if !self.recipient.is_op_return() {
      let recipient_as_address = Address::from_script(self.recipient.as_script(), self.network)?;

      if self.change_addresses.contains(&recipient_as_address) {
        return Err(Error::DuplicateAddress(recipient_as_address));
      }

      if let Target::Value(output_value) | Target::ExactPostage(output_value) = self.target {
        let dust_value = self.recipient.minimal_non_dust();

        if output_value < dust_value {
          return Err(Error::Dust {
            output_value,
            dust_value,
          });
        }
      }
    }

    self
      .select_outgoing()?
      .align_outgoing()
      .pad_alignment_output()?
      .add_value()?
      .strip_value()
      .deduct_fee()
      .build()
  }

  fn select_outgoing(mut self) -> Result<Self> {
    let dust_limit = self
      .unused_change_addresses
      .last()
      .unwrap()
      .script_pubkey()
      .minimal_non_dust()
      .to_sat();

    for (inscribed_satpoint, inscription_ids) in self.inscriptions.iter().rev() {
      if self.outgoing.outpoint == inscribed_satpoint.outpoint
        && self.outgoing.offset != inscribed_satpoint.offset
        && self.outgoing.offset < inscribed_satpoint.offset + dust_limit
      {
        return Err(Error::UtxoContainsAdditionalInscriptions {
          inscribed_satpoint: *inscribed_satpoint,
          inscription_ids: inscription_ids.clone(),
          outgoing_satpoint: self.outgoing,
        });
      }
    }

    let amount = self
      .amounts
      .get(&self.outgoing.outpoint)
      .ok_or(Error::NotInWallet(self.outgoing))?
      .value
      .to_sat();

    if self.outgoing.offset >= amount {
      return Err(Error::OutOfRange(self.outgoing, amount - 1));
    }

    self.utxos.remove(&self.outgoing.outpoint);
    self.inputs.push(self.outgoing.outpoint);
    self.outputs.push(TxOut {
      script_pubkey: self.recipient.clone(),
      value: Amount::from_sat(amount),
    });

    tprintln!(
      "selected outgoing outpoint {} with value {}",
      self.outgoing.outpoint,
      amount
    );

    Ok(self)
  }

  fn align_outgoing(mut self) -> Self {
    assert_eq!(self.outputs.len(), 1, "invariant: only one output");

    assert_eq!(
      self.outputs[0].script_pubkey, self.recipient,
      "invariant: first output is recipient"
    );

    let sat_offset = self.calculate_sat_offset();

    if sat_offset == 0 {
      tprintln!("outgoing is aligned");
    } else {
      tprintln!("aligned outgoing with {sat_offset} sat padding output");
      self.outputs.insert(
        0,
        TxOut {
          script_pubkey: self
            .unused_change_addresses
            .pop()
            .unwrap_or_else(|| panic!("not enough change addresses"))
            .script_pubkey(),
          value: Amount::from_sat(sat_offset),
        },
      );
      self.outputs.last_mut().expect("no output").value -= Amount::from_sat(sat_offset);
    }

    self
  }

  fn pad_alignment_output(mut self) -> Result<Self> {
    if self.outputs[0].script_pubkey == self.recipient {
      tprintln!("no alignment output");
    } else {
      let dust_limit = self
        .unused_change_addresses
        .last()
        .unwrap()
        .script_pubkey()
        .minimal_non_dust();

      if self.outputs[0].value >= dust_limit {
        tprintln!("no padding needed");
      } else {
        while self.outputs[0].value < dust_limit {
          let (utxo, size) =
            self.select_cardinal_utxo((dust_limit - self.outputs[0].value).to_sat(), true)?;

          self.inputs.insert(0, utxo);
          self.outputs[0].value += size;

          tprintln!(
            "padded alignment output to {} with additional {size} sat input",
            self.outputs[0].value
          );
        }
      }
    }

    Ok(self)
  }

  fn add_value(mut self) -> Result<Self> {
    let estimated_fee = self.estimate_fee();

    let min_value = match self.target {
      Target::Postage => self
        .outputs
        .last()
        .unwrap()
        .script_pubkey
        .minimal_non_dust(),
      Target::Value(value) | Target::ExactPostage(value) => value,
    };

    let total = min_value
      .checked_add(estimated_fee)
      .ok_or(Error::ValueOverflow)?;

    if let Some(mut deficit) = total.checked_sub(self.outputs.last().unwrap().value) {
      while deficit > Amount::ZERO {
        let additional_fee = self.fee_rate.fee(Self::ADDITIONAL_INPUT_VBYTES);

        let needed = deficit
          .checked_add(additional_fee)
          .ok_or(Error::ValueOverflow)?;

        let (utxo, value) = self.select_cardinal_utxo(needed.to_sat(), false)?;

        let benefit = value
          .checked_sub(additional_fee)
          .ok_or(Error::NotEnoughCardinalUtxos)?;

        self.inputs.push(utxo);

        self.outputs.last_mut().unwrap().value += value;

        if benefit > deficit {
          tprintln!("added {value} sat input to cover {deficit} sat deficit");
          deficit = Amount::ZERO;
        } else {
          tprintln!("added {value} sat input to reduce {deficit} sat deficit by {benefit} sat");
          deficit -= benefit;
        }
      }
    }

    Ok(self)
  }

  fn strip_value(mut self) -> Self {
    let sat_offset = self.calculate_sat_offset();

    let total_output_amount = self
      .outputs
      .iter()
      .map(|tx_out| tx_out.value)
      .sum::<Amount>();

    self
      .outputs
      .iter()
      .find(|tx_out| tx_out.script_pubkey == self.recipient)
      .expect("couldn't find output that contains the index");

    let value = total_output_amount - Amount::from_sat(sat_offset);

    if let Some(excess) = value.checked_sub(self.fee_rate.fee(self.estimate_vbytes())) {
      let (max, target) = match self.target {
        Target::ExactPostage(postage) => (postage, postage),
        Target::Postage => (Self::MAX_POSTAGE, TARGET_POSTAGE),
        Target::Value(value) => (value, value),
      };

      if excess > max
        && value.checked_sub(target).unwrap()
          > self
            .unused_change_addresses
            .last()
            .unwrap()
            .script_pubkey()
            .minimal_non_dust()
            + self
              .fee_rate
              .fee(self.estimate_vbytes() + Self::ADDITIONAL_OUTPUT_VBYTES)
      {
        tprintln!("stripped {} sats", (value - target).to_sat());
        self.outputs.last_mut().expect("no outputs found").value = target;
        self.outputs.push(TxOut {
          script_pubkey: self
            .unused_change_addresses
            .pop()
            .unwrap_or_else(|| panic!("not enough change addresses"))
            .script_pubkey(),
          value: value - target,
        });
      }
    }

    self
  }

  fn deduct_fee(mut self) -> Self {
    let sat_offset = self.calculate_sat_offset();

    let fee = self.estimate_fee();

    let total_output_amount = self
      .outputs
      .iter()
      .map(|tx_out| tx_out.value)
      .sum::<Amount>();

    let last_tx_out = self
      .outputs
      .last_mut()
      .expect("No output to deduct fee from");

    assert!(
      total_output_amount.checked_sub(fee).unwrap() > Amount::from_sat(sat_offset),
      "invariant: deducting fee does not consume sat",
    );

    assert!(
      last_tx_out.value >= fee,
      "invariant: last output can pay fee: {} {}",
      last_tx_out.value,
      fee,
    );

    last_tx_out.value -= fee;

    self
  }

  /// Estimate the size in virtual bytes of the transaction under construction.
  /// We initialize wallets with taproot descriptors only, so we know that all
  /// inputs are taproot key path spends, which allows us to know that witnesses
  /// will all consist of single Schnorr signatures.
  fn estimate_vbytes(&self) -> usize {
    Self::estimate_vbytes_with(self.inputs.len(), &self.outputs)
  }

  fn estimate_vbytes_with(inputs: usize, outputs: &[TxOut]) -> usize {
    Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: (0..inputs)
        .map(|_| TxIn {
          previous_output: OutPoint::null(),
          script_sig: ScriptBuf::new(),
          sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
          witness: Witness::from_slice(&[&[0; Self::SCHNORR_SIGNATURE_SIZE]]),
        })
        .collect(),
      output: outputs.to_vec(),
    }
    .vsize()
  }

  fn estimate_fee(&self) -> Amount {
    self.fee_rate.fee(self.estimate_vbytes())
  }

  fn build(self) -> Result<Transaction> {
    let transaction = Transaction {
      version: Version(2),
      lock_time: LockTime::ZERO,
      input: self
        .inputs
        .iter()
        .map(|outpoint| TxIn {
          previous_output: *outpoint,
          script_sig: ScriptBuf::new(),
          sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
          witness: Witness::new(),
        })
        .collect(),
      output: self.outputs.clone(),
    };

    assert_eq!(
      self
        .amounts
        .iter()
        .filter(|(outpoint, txout)| *outpoint == &self.outgoing.outpoint
          && self.outgoing.offset < txout.value.to_sat())
        .count(),
      1,
      "invariant: outgoing sat is contained in utxos"
    );

    assert_eq!(
      transaction
        .input
        .iter()
        .filter(|tx_in| tx_in.previous_output == self.outgoing.outpoint)
        .count(),
      1,
      "invariant: inputs spend outgoing sat"
    );

    let mut sat_offset = 0;
    let mut found = false;
    for tx_in in &transaction.input {
      if tx_in.previous_output == self.outgoing.outpoint {
        sat_offset += self.outgoing.offset;
        found = true;
        break;
      } else {
        sat_offset += self.amounts[&tx_in.previous_output].value.to_sat();
      }
    }
    assert!(found, "invariant: outgoing sat is found in inputs");

    let mut output_end = 0;
    let mut found = false;
    for tx_out in &transaction.output {
      output_end += tx_out.value.to_sat();
      if output_end > sat_offset {
        assert_eq!(
          tx_out.script_pubkey, self.recipient,
          "invariant: outgoing sat is sent to recipient"
        );
        found = true;
        break;
      }
    }
    assert!(found, "invariant: outgoing sat is found in outputs");

    assert_eq!(
      transaction
        .output
        .iter()
        .filter(|tx_out| tx_out.script_pubkey == self.recipient)
        .count(),
      1,
      "invariant: recipient address appears exactly once in outputs",
    );

    assert!(
      self
        .change_addresses
        .iter()
        .map(|change_address| transaction
          .output
          .iter()
          .filter(|tx_out| tx_out.script_pubkey == change_address.script_pubkey())
          .count())
        .all(|count| count <= 1),
      "invariant: change addresses appear at most once in outputs",
    );

    let mut offset = 0;
    for output in &transaction.output {
      if output.script_pubkey == self.recipient {
        let slop = self.fee_rate.fee(Self::ADDITIONAL_OUTPUT_VBYTES);

        match self.target {
          Target::Postage => {
            assert!(
              output.value <= Self::MAX_POSTAGE + slop,
              "invariant: excess postage is stripped"
            );
          }
          Target::ExactPostage(postage) => {
            assert!(
              output.value <= postage + slop,
              "invariant: excess postage is stripped"
            );
          }
          Target::Value(value) => {
            assert!(
              output.value.checked_sub(value).unwrap()
                <= self
                  .change_addresses
                  .iter()
                  .map(|address| address.script_pubkey().minimal_non_dust())
                  .max()
                  .unwrap_or_default()
                  + slop,
              "invariant: output equals target value",
            );
          }
        }
        assert_eq!(
          offset, sat_offset,
          "invariant: sat is at first position in recipient output"
        );
      } else {
        assert!(
          self
            .change_addresses
            .iter()
            .any(|change_address| change_address.script_pubkey() == output.script_pubkey),
          "invariant: all outputs are either change or recipient: unrecognized output {}",
          output.script_pubkey
        );
      }
      offset += output.value.to_sat();
    }

    let mut actual_fee = Amount::ZERO;
    for input in &transaction.input {
      actual_fee += self.amounts[&input.previous_output].value;
    }
    for output in &transaction.output {
      actual_fee -= output.value;
    }

    let mut modified_tx = transaction.clone();
    for input in &mut modified_tx.input {
      input.witness = Witness::from_slice(&[&[0; 64]]);
    }
    let expected_fee = self.fee_rate.fee(modified_tx.vsize());

    assert_eq!(
      actual_fee, expected_fee,
      "invariant: fee estimation is correct",
    );

    for tx_out in &transaction.output {
      assert!(
        tx_out.value >= tx_out.script_pubkey.minimal_non_dust(),
        "invariant: all outputs are above dust limit",
      );
    }

    Ok(transaction)
  }

  fn calculate_sat_offset(&self) -> u64 {
    let mut sat_offset = 0;
    for outpoint in &self.inputs {
      if *outpoint == self.outgoing.outpoint {
        return sat_offset + self.outgoing.offset;
      } else {
        sat_offset += self.amounts[outpoint].value.to_sat();
      }
    }

    panic!("Could not find outgoing sat in inputs");
  }

  /// Cardinal UTXOs are those that are unlocked, contain no inscriptions, and
  /// contain no runes, can therefore be used to pad transactions and pay fees.
  /// Sometimes multiple cardinal UTXOs are needed and depending on the context
  /// we want to select either ones above or under (when trying to consolidate
  /// dust outputs) the target value.
  fn select_cardinal_utxo(
    &mut self,
    target_value: u64,
    prefer_under: bool,
  ) -> Result<(OutPoint, Amount)> {
    tprintln!(
      "looking for {} cardinal worth {target_value}",
      if prefer_under { "smaller" } else { "bigger" }
    );

    let inscribed_utxos = self
      .inscriptions
      .keys()
      .map(|satpoint| satpoint.outpoint)
      .collect::<BTreeSet<OutPoint>>();

    let mut best_match = None;
    for utxo in &self.utxos {
      if self.runic_utxos.contains(utxo)
        || inscribed_utxos.contains(utxo)
        || self.locked_utxos.contains(utxo)
      {
        continue;
      }

      let current_value = self.amounts[utxo].value.to_sat();

      let (_, best_value) = match best_match {
        Some(prev) => prev,
        None => {
          best_match = Some((*utxo, current_value));
          (*utxo, current_value)
        }
      };

      let abs_diff = |a: u64, b: u64| -> u64 { max(a, b) - min(a, b) };
      let is_closer = abs_diff(current_value, target_value) < abs_diff(best_value, target_value);

      let not_preference_but_closer = if prefer_under {
        best_value > target_value && is_closer
      } else {
        best_value < target_value && is_closer
      };

      let is_preference_and_closer = if prefer_under {
        current_value <= target_value && is_closer
      } else {
        current_value >= target_value && is_closer
      };

      let newly_meets_preference = if prefer_under {
        best_value > target_value && current_value <= target_value
      } else {
        best_value < target_value && current_value >= target_value
      };

      if is_preference_and_closer || not_preference_but_closer || newly_meets_preference {
        best_match = Some((*utxo, current_value))
      }
    }

    let (utxo, value) = best_match.ok_or(Error::NotEnoughCardinalUtxos)?;

    self.utxos.remove(&utxo);
    tprintln!("found cardinal worth {}", value);

    Ok((utxo, Amount::from_sat(value)))
  }
}

#[cfg(test)]
mod tests {
  use {super::Error, super::*};

  #[test]
  fn select_sat() {
    let mut utxos = vec![
      (outpoint(1), tx_out(5_000, address(0))),
      (outpoint(2), tx_out(49 * COIN_VALUE, address(0))),
      (outpoint(3), tx_out(2_000, address(0))),
    ];

    let tx_builder = TransactionBuilder::new(
      satpoint(2, 0),
      BTreeMap::new(),
      utxos.clone().into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap();

    utxos.remove(1);
    assert_eq!(
      tx_builder.utxos,
      utxos.iter().map(|(outpoint, _ranges)| *outpoint).collect()
    );
    assert_eq!(tx_builder.inputs, [outpoint(2)]);
    assert_eq!(
      tx_builder.outputs,
      [TxOut {
        script_pubkey: recipient(),
        value: Amount::from_sat(100 * COIN_VALUE - 51 * COIN_VALUE)
      }]
    )
  }

  #[test]
  fn tx_builder_to_transaction() {
    let mut amounts = BTreeMap::new();
    amounts.insert(outpoint(1), tx_out(5_000, address(0)));
    amounts.insert(outpoint(2), tx_out(5_000, address(0)));
    amounts.insert(outpoint(3), tx_out(2_000, address(0)));

    let tx_builder = TransactionBuilder {
      amounts,
      fee_rate: FeeRate::try_from(1.0).unwrap(),
      utxos: BTreeSet::new(),
      outgoing: satpoint(1, 0),
      inscriptions: BTreeMap::new(),
      locked_utxos: BTreeSet::new(),
      runic_utxos: BTreeSet::new(),
      recipient: recipient(),
      unused_change_addresses: vec![change(0), change(1)],
      change_addresses: vec![change(0), change(1)].into_iter().collect(),
      inputs: vec![outpoint(1), outpoint(2), outpoint(3)],
      outputs: vec![
        TxOut {
          script_pubkey: recipient(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: change(0).script_pubkey(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: change(1).script_pubkey(),
          value: Amount::from_sat(1_724),
        },
      ],
      target: Target::Postage,
      network: Network::Testnet,
    };

    pretty_assert_eq!(
      tx_builder.build(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1)), tx_in(outpoint(2)), tx_in(outpoint(3))],
        output: vec![
          tx_out(5_000, recipient_address()),
          tx_out(5_000, change(0)),
          tx_out(1_724, change(1))
        ],
      })
    )
  }

  #[test]
  fn transactions_are_rbf() {
    let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))];

    assert!(TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .build_transaction()
    .unwrap()
    .is_explicitly_rbf())
  }

  #[test]
  fn deduct_fee() {
    let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(4901, recipient_address())],
      })
    )
  }

  #[test]
  #[should_panic(expected = "invariant: deducting fee does not consume sat")]
  fn invariant_deduct_fee_does_not_consume_sat() {
    let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))];

    TransactionBuilder::new(
      satpoint(1, 4_950),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .align_outgoing()
    .strip_value()
    .deduct_fee();
  }

  #[test]
  fn additional_postage_added_when_required() {
    let utxos = vec![
      (outpoint(1), tx_out(5_000, address(0))),
      (outpoint(2), tx_out(5_000, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 4_950),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))],
        output: vec![tx_out(4_950, change(1)), tx_out(4_862, recipient_address())],
      })
    )
  }

  #[test]
  fn insufficient_padding_to_add_postage_no_utxos() {
    let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 4_950),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos),
    )
  }

  #[test]
  fn insufficient_padding_to_add_postage_small_utxos() {
    let utxos = vec![
      (outpoint(1), tx_out(5_000, address(0))),
      (outpoint(2), tx_out(1, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 4_950),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos),
    )
  }

  #[test]
  fn excess_additional_postage_is_stripped() {
    let utxos = vec![
      (outpoint(1), tx_out(5_000, address(0))),
      (outpoint(2), tx_out(25_000, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 4_950),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))],
        output: vec![
          tx_out(4_950, change(1)),
          tx_out(TARGET_POSTAGE.to_sat(), recipient_address()),
          tx_out(14_831, change(0)),
        ],
      })
    )
  }

  #[test]
  #[should_panic(expected = "invariant: outgoing sat is contained in utxos")]
  fn invariant_satpoint_outpoint_is_contained_in_utxos() {
    TransactionBuilder::new(
      satpoint(2, 0),
      BTreeMap::new(),
      vec![(outpoint(1), tx_out(4, address(0)))]
        .into_iter()
        .collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: outgoing sat is contained in utxos")]
  fn invariant_satpoint_offset_is_contained_in_utxos() {
    TransactionBuilder::new(
      satpoint(1, 4),
      BTreeMap::new(),
      vec![(outpoint(1), tx_out(4, address(0)))]
        .into_iter()
        .collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: inputs spend outgoing sat")]
  fn invariant_inputs_spend_sat() {
    TransactionBuilder::new(
      satpoint(1, 2),
      BTreeMap::new(),
      vec![(outpoint(1), tx_out(5, address(0)))]
        .into_iter()
        .collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: outgoing sat is sent to recipient")]
  fn invariant_sat_is_sent_to_recipient() {
    let mut builder = TransactionBuilder::new(
      satpoint(1, 2),
      BTreeMap::new(),
      vec![(outpoint(1), tx_out(5, address(0)))]
        .into_iter()
        .collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap();

    builder.outputs[0].script_pubkey = "tb1qx4gf3ya0cxfcwydpq8vr2lhrysneuj5d7lqatw"
      .parse::<Address<NetworkUnchecked>>()
      .unwrap()
      .assume_checked()
      .script_pubkey();

    builder.build().unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: outgoing sat is found in outputs")]
  fn invariant_sat_is_found_in_outputs() {
    let mut builder = TransactionBuilder::new(
      satpoint(1, 2),
      BTreeMap::new(),
      vec![(outpoint(1), tx_out(5, address(0)))]
        .into_iter()
        .collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap();

    builder.outputs[0].value = Amount::from_sat(0);

    builder.build().unwrap();
  }

  #[test]
  fn excess_postage_is_stripped() {
    let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![
          tx_out(TARGET_POSTAGE.to_sat(), recipient_address()),
          tx_out(989_870, change(1))
        ],
      })
    )
  }

  #[test]
  #[should_panic(expected = "invariant: excess postage is stripped")]
  fn invariant_excess_postage_is_stripped() {
    let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))];

    TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .build()
    .unwrap();
  }

  #[test]
  fn sat_is_aligned() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 3_333),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(3_333, change(1)), tx_out(6_537, recipient_address())],
      })
    )
  }

  #[test]
  fn alignment_output_under_dust_limit_is_padded() {
    let utxos = vec![
      (outpoint(1), tx_out(10_000, address(0))),
      (outpoint(2), tx_out(10_000, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 1),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(2)), tx_in(outpoint(1))],
        output: vec![
          tx_out(10_001, change(1)),
          tx_out(9_811, recipient_address())
        ],
      })
    )
  }

  #[test]
  #[should_panic(expected = "invariant: all outputs are either change or recipient")]
  fn invariant_all_output_are_recognized() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    let mut builder = TransactionBuilder::new(
      satpoint(1, 3_333),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .align_outgoing()
    .add_value()
    .unwrap()
    .strip_value()
    .deduct_fee();

    builder.change_addresses = BTreeSet::new();

    builder.build().unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: all outputs are above dust limit")]
  fn invariant_all_output_are_above_dust_limit() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    TransactionBuilder::new(
      satpoint(1, 1),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .align_outgoing()
    .add_value()
    .unwrap()
    .strip_value()
    .deduct_fee()
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: sat is at first position in recipient output")]
  fn invariant_sat_is_aligned() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    TransactionBuilder::new(
      satpoint(1, 3_333),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .strip_value()
    .deduct_fee()
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: fee estimation is correct")]
  fn invariant_fee_is_at_least_target_fee_rate() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Postage,
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .strip_value()
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: recipient address appears exactly once in outputs")]
  fn invariant_recipient_appears_exactly_once() {
    let mut amounts = BTreeMap::new();
    amounts.insert(outpoint(1), tx_out(5_000, address(0)));
    amounts.insert(outpoint(2), tx_out(5_000, address(0)));
    amounts.insert(outpoint(3), tx_out(2_000, address(0)));

    TransactionBuilder {
      amounts,
      fee_rate: FeeRate::try_from(1.0).unwrap(),
      utxos: BTreeSet::new(),
      locked_utxos: BTreeSet::new(),
      runic_utxos: BTreeSet::new(),
      outgoing: satpoint(1, 0),
      inscriptions: BTreeMap::new(),
      recipient: recipient(),
      unused_change_addresses: vec![change(0), change(1)],
      change_addresses: vec![change(0), change(1)].into_iter().collect(),
      inputs: vec![outpoint(1), outpoint(2), outpoint(3)],
      outputs: vec![
        TxOut {
          script_pubkey: recipient(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: recipient(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: change(1).script_pubkey(),
          value: Amount::from_sat(1_774),
        },
      ],
      target: Target::Postage,
      network: Network::Testnet,
    }
    .build()
    .unwrap();
  }

  #[test]
  #[should_panic(expected = "invariant: change addresses appear at most once in outputs")]
  fn invariant_change_appears_at_most_once() {
    let mut amounts = BTreeMap::new();
    amounts.insert(outpoint(1), tx_out(5_000, address(0)));
    amounts.insert(outpoint(2), tx_out(5_000, address(0)));
    amounts.insert(outpoint(3), tx_out(2_000, address(0)));

    TransactionBuilder {
      amounts,
      fee_rate: FeeRate::try_from(1.0).unwrap(),
      utxos: BTreeSet::new(),
      locked_utxos: BTreeSet::new(),
      runic_utxos: BTreeSet::new(),
      outgoing: satpoint(1, 0),
      inscriptions: BTreeMap::new(),
      recipient: recipient(),
      unused_change_addresses: vec![change(0), change(1)],
      change_addresses: vec![change(0), change(1)].into_iter().collect(),
      inputs: vec![outpoint(1), outpoint(2), outpoint(3)],
      outputs: vec![
        TxOut {
          script_pubkey: recipient(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: change(0).script_pubkey(),
          value: Amount::from_sat(5_000),
        },
        TxOut {
          script_pubkey: change(0).script_pubkey(),
          value: Amount::from_sat(1_774),
        },
      ],
      target: Target::Postage,
      network: Network::Testnet,
    }
    .build()
    .unwrap();
  }

  #[test]
  fn do_not_select_already_inscribed_sats_for_cardinal_utxos() {
    let utxos = vec![
      (outpoint(1), tx_out(100, address(0))),
      (outpoint(2), tx_out(49 * COIN_VALUE, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::from([(satpoint(2, 10 * COIN_VALUE), vec![inscription_id(1)])]),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos)
    )
  }

  #[test]
  fn do_not_select_runic_utxos_for_cardinal_utxos() {
    let utxos = vec![
      (outpoint(1), tx_out(100, address(0))),
      (outpoint(2), tx_out(49 * COIN_VALUE, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        vec![outpoint(2)].into_iter().collect(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos)
    )
  }

  #[test]
  fn do_not_send_two_inscriptions_at_once() {
    let utxos = vec![(outpoint(1), tx_out(1_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::from([(satpoint(1, 500), vec![inscription_id(1)])]),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::UtxoContainsAdditionalInscriptions {
        inscribed_satpoint: satpoint(1, 500),
        inscription_ids: vec![inscription_id(1)],
        outgoing_satpoint: satpoint(1, 0),
      })
    )
  }

  #[test]
  fn build_transaction_with_custom_fee_rate() {
    let utxos = vec![(outpoint(1), tx_out(10_000, address(0)))];

    let fee_rate = FeeRate::try_from(17.3).unwrap();

    let transaction = TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::from([(satpoint(1, 0), vec![inscription_id(1)])]),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      fee_rate,
      Target::Postage,
      Network::Testnet,
    )
    .build_transaction()
    .unwrap();

    let fee =
      fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1);

    pretty_assert_eq!(
      transaction,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(10_000 - fee.to_sat(), recipient_address())],
      }
    )
  }

  #[test]
  fn exact_transaction_has_correct_value() {
    let utxos = vec![(outpoint(1), tx_out(5_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(1000, recipient_address()), tx_out(3870, change(1))],
      })
    )
  }

  #[test]
  fn exact_transaction_adds_output_to_cover_value() {
    let utxos = vec![
      (outpoint(1), tx_out(1_000, address(0))),
      (outpoint(2), tx_out(1_000, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Value(Amount::from_sat(1500)),
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1)), tx_in(outpoint(2))],
        output: vec![tx_out(1500, recipient_address()), tx_out(312, change(1))],
      })
    )
  }

  #[test]
  fn refuse_to_send_dust() {
    let utxos = vec![(outpoint(1), tx_out(1_000, address(0)))];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::from([(satpoint(1, 500), vec![inscription_id(1)])]),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Value(Amount::from_sat(1)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::Dust {
        output_value: Amount::from_sat(1),
        dust_value: Amount::from_sat(294)
      })
    )
  }

  #[test]
  fn do_not_select_outputs_which_do_not_pay_for_their_own_fee_at_default_fee_rate() {
    let utxos = vec![
      (outpoint(1), tx_out(1_000, address(0))),
      (outpoint(2), tx_out(100, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos),
    )
  }

  #[test]
  fn do_not_select_outputs_which_do_not_pay_for_their_own_fee_at_higher_fee_rate() {
    let utxos = vec![
      (outpoint(1), tx_out(1_000, address(0))),
      (outpoint(2), tx_out(500, address(0))),
    ];

    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        utxos.into_iter().collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(4.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos),
    )
  }

  #[test]
  fn additional_input_size_is_correct() {
    let before = TransactionBuilder::estimate_vbytes_with(0, &[]);
    let after = TransactionBuilder::estimate_vbytes_with(1, &[]);
    assert_eq!(after - before, TransactionBuilder::ADDITIONAL_INPUT_VBYTES);
  }

  #[test]
  fn additional_output_size_is_correct() {
    let before = TransactionBuilder::estimate_vbytes_with(0, &[]);
    let after = TransactionBuilder::estimate_vbytes_with(
      0,
      &[TxOut {
        script_pubkey: "bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k"
          .parse::<Address<NetworkUnchecked>>()
          .unwrap()
          .assume_checked()
          .script_pubkey(),
        value: Amount::from_sat(0),
      }],
    );
    assert_eq!(after - before, TransactionBuilder::ADDITIONAL_OUTPUT_VBYTES);
  }

  #[test]
  fn do_not_strip_excess_value_if_it_would_create_dust() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(1_000, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Value(Amount::from_sat(707)),
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(901, recipient_address())],
      }),
    );
  }

  #[test]
  fn possible_to_create_output_of_exactly_max_postage() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(20_099, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(1.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(20_000, recipient_address())],
      }),
    );
  }

  #[test]
  fn do_not_strip_excess_value_if_additional_output_cannot_pay_fee() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(1_500, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(5.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(1005, recipient_address())],
      }),
    );
  }

  #[test]
  fn correct_error_is_returned_when_fee_cannot_be_paid() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(1_500, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(6.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::NotEnoughCardinalUtxos)
    );
  }

  #[test]
  fn recipient_address_must_be_unique() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(1000, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [recipient_address(), change(1)],
        FeeRate::try_from(0.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::DuplicateAddress(recipient_address()))
    );
  }

  #[test]
  fn change_addresses_must_be_unique() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(1000, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(0)],
        FeeRate::try_from(0.0).unwrap(),
        Target::Value(Amount::from_sat(1000)),
        Network::Testnet,
      )
      .build_transaction(),
      Err(Error::DuplicateAddress(change(0)))
    );
  }

  #[test]
  fn output_over_value_because_fees_prevent_excess_value_stripping() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(2000, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(2.0).unwrap(),
        Target::Value(Amount::from_sat(1500)),
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(1802, recipient_address())],
      }),
    );
  }

  #[test]
  fn output_over_max_postage_because_fees_prevent_excess_value_stripping() {
    pretty_assert_eq!(
      TransactionBuilder::new(
        satpoint(1, 0),
        BTreeMap::new(),
        vec![(outpoint(1), tx_out(45000, address(0)))]
          .into_iter()
          .collect(),
        BTreeSet::new(),
        BTreeSet::new(),
        recipient(),
        [change(0), change(1)],
        FeeRate::try_from(250.0).unwrap(),
        Target::Postage,
        Network::Testnet,
      )
      .build_transaction(),
      Ok(Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![tx_out(20250, recipient_address())],
      }),
    );
  }

  #[test]
  fn select_outgoing_can_select_multiple_utxos() {
    let mut utxos = vec![
      (outpoint(2), tx_out(3_006, address(0))), // 2. biggest utxo is selected 2nd leaving us needing 4206 more
      (outpoint(1), tx_out(3_003, address(0))), // 1. satpoint is selected 1st leaving us needing 7154 more
      (outpoint(5), tx_out(3_004, address(0))),
      (outpoint(4), tx_out(3_001, address(0))), // 4. smallest utxo >= 1259 is selected 4th, filling deficit
      (outpoint(3), tx_out(3_005, address(0))), // 3. next biggest utxo is selected 3rd leaving us needing 1259 more
      (outpoint(6), tx_out(3_002, address(0))),
    ];

    let tx_builder = TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::new(),
      utxos.clone().into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .add_value()
    .unwrap();

    utxos.remove(4);
    utxos.remove(3);
    utxos.remove(1);
    utxos.remove(0);
    assert_eq!(
      tx_builder.utxos,
      utxos.iter().map(|(outpoint, _ranges)| *outpoint).collect()
    );
    assert_eq!(
      tx_builder.inputs,
      [outpoint(1), outpoint(2), outpoint(3), outpoint(4)]
    ); // value inputs are pushed at the end
    assert_eq!(
      tx_builder.outputs,
      [TxOut {
        script_pubkey: recipient(),
        value: Amount::from_sat(3_003 + 3_006 + 3_005 + 3_001)
      }]
    )
  }

  #[test]
  fn pad_alignment_output_can_select_multiple_utxos() {
    let mut utxos = vec![
      (outpoint(4), tx_out(101, address(0))), // 4. smallest utxo >= 84 is selected 4th, filling deficit
      (outpoint(1), tx_out(20_000, address(0))), // 1. satpoint is selected 1st leaving deficit 293
      (outpoint(2), tx_out(105, address(0))), // 2. biggest utxo <= 293 is selected 2nd leaving deficit 188
      (outpoint(5), tx_out(103, address(0))),
      (outpoint(6), tx_out(10_000, address(0))),
      (outpoint(3), tx_out(104, address(0))), // 3. biggest utxo <= 188 is selected 3rd leaving deficit 84
      (outpoint(7), tx_out(102, address(0))),
    ];

    let tx_builder = TransactionBuilder::new(
      satpoint(1, 1),
      BTreeMap::new(),
      utxos.clone().into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    )
    .select_outgoing()
    .unwrap()
    .align_outgoing()
    .pad_alignment_output()
    .unwrap();

    utxos.remove(5);
    utxos.remove(2);
    utxos.remove(1);
    utxos.remove(0);
    assert_eq!(
      tx_builder.utxos,
      utxos.iter().map(|(outpoint, _ranges)| *outpoint).collect()
    );
    assert_eq!(
      tx_builder.inputs,
      [outpoint(4), outpoint(3), outpoint(2), outpoint(1)]
    ); // padding inputs are inserted at the start
    assert_eq!(
      tx_builder.outputs,
      [
        TxOut {
          script_pubkey: change(1).script_pubkey(),
          value: Amount::from_sat(101 + 104 + 105 + 1)
        },
        TxOut {
          script_pubkey: recipient(),
          value: Amount::from_sat(19_999)
        }
      ]
    )
  }

  fn select_cardinal_utxo_prefer_under_helper(
    target_value: Amount,
    prefer_under: bool,
    expected_value: Amount,
  ) {
    let utxos = vec![
      (outpoint(4), tx_out(101, address(0))),
      (outpoint(1), tx_out(20_000, address(0))),
      (outpoint(2), tx_out(105, address(0))),
      (outpoint(5), tx_out(103, address(0))),
      (outpoint(6), tx_out(10_000, address(0))),
      (outpoint(3), tx_out(104, address(0))),
      (outpoint(7), tx_out(102, address(0))),
    ];

    let mut tx_builder = TransactionBuilder::new(
      satpoint(0, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    );

    assert_eq!(
      tx_builder
        .select_cardinal_utxo(target_value.to_sat(), prefer_under)
        .unwrap()
        .1,
      expected_value
    );
  }

  #[test]
  fn select_cardinal_utxo_prefer_under() {
    // select biggest utxo <= 104
    select_cardinal_utxo_prefer_under_helper(Amount::from_sat(104), true, Amount::from_sat(104));

    // select biggest utxo <= 1_000
    select_cardinal_utxo_prefer_under_helper(Amount::from_sat(1_000), true, Amount::from_sat(105));

    // select biggest utxo <= 10, else smallest > 10
    select_cardinal_utxo_prefer_under_helper(Amount::from_sat(10), true, Amount::from_sat(101));

    // select smallest utxo >= 104
    select_cardinal_utxo_prefer_under_helper(Amount::from_sat(104), false, Amount::from_sat(104));

    // select smallest utxo >= 1_000
    select_cardinal_utxo_prefer_under_helper(
      Amount::from_sat(1000),
      false,
      Amount::from_sat(10_000),
    );

    // select smallest utxo >= 100_000, else biggest < 100_000
    select_cardinal_utxo_prefer_under_helper(
      Amount::from_sat(100_000),
      false,
      Amount::from_sat(20_000),
    );
  }

  #[test]
  fn build_transaction_with_custom_postage() {
    let utxos = vec![(outpoint(1), tx_out(1_000_000, address(0)))];

    let fee_rate = FeeRate::try_from(17.3).unwrap();

    let transaction = TransactionBuilder::new(
      satpoint(1, 0),
      BTreeMap::from([(satpoint(1, 0), vec![inscription_id(1)])]),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      fee_rate,
      Target::ExactPostage(Amount::from_sat(66_000)),
      Network::Testnet,
    )
    .build_transaction()
    .unwrap();

    let fee =
      fee_rate.fee(transaction.vsize() + TransactionBuilder::SCHNORR_SIGNATURE_SIZE / 4 + 1);

    pretty_assert_eq!(
      transaction,
      Transaction {
        version: Version(2),
        lock_time: LockTime::ZERO,
        input: vec![tx_in(outpoint(1))],
        output: vec![
          tx_out(66_000, recipient_address()),
          tx_out(1_000_000 - 66_000 - fee.to_sat(), change(1))
        ],
      }
    )
  }

  #[test]
  fn select_cardinal_utxo_ignores_locked_utxos_and_errors_if_none_available() {
    let utxos = vec![(outpoint(1), tx_out(500, address(0)))];
    let locked_utxos = vec![outpoint(1)];

    let mut tx_builder = TransactionBuilder::new(
      satpoint(0, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      locked_utxos.into_iter().collect(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    );

    assert_eq!(
      tx_builder.select_cardinal_utxo(500, false),
      Err(Error::NotEnoughCardinalUtxos),
    );
  }

  #[test]
  fn select_cardinal_utxo_ignores_locked_utxos() {
    let utxos = vec![
      (outpoint(1), tx_out(500, address(0))),
      (outpoint(2), tx_out(500, address(0))),
    ];
    let locked_utxos = vec![outpoint(1)];

    let mut tx_builder = TransactionBuilder::new(
      satpoint(0, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      locked_utxos.into_iter().collect(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    );

    assert_eq!(
      tx_builder.select_cardinal_utxo(500, false).unwrap().0,
      outpoint(2),
    );
  }

  #[test]
  fn prefer_further_away_utxos_if_they_are_newly_under_target() {
    let utxos = vec![
      (outpoint(1), tx_out(510, address(0))),
      (outpoint(2), tx_out(400, address(0))),
    ];

    let mut tx_builder = TransactionBuilder::new(
      satpoint(0, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    );

    assert_eq!(
      tx_builder.select_cardinal_utxo(500, true).unwrap().0,
      outpoint(2),
    );
  }

  #[test]
  fn prefer_further_away_utxos_if_they_are_newly_over_target() {
    let utxos = vec![
      (outpoint(1), tx_out(490, address(0))),
      (outpoint(2), tx_out(600, address(0))),
    ];

    let mut tx_builder = TransactionBuilder::new(
      satpoint(0, 0),
      BTreeMap::new(),
      utxos.into_iter().collect(),
      BTreeSet::new(),
      BTreeSet::new(),
      recipient(),
      [change(0), change(1)],
      FeeRate::try_from(1.0).unwrap(),
      Target::Value(Amount::from_sat(10_000)),
      Network::Testnet,
    );

    assert_eq!(
      tx_builder.select_cardinal_utxo(500, false).unwrap().0,
      outpoint(2),
    );
  }
}

ord/src/wallet/wallet_constructor.rs


use super::*;

#[derive(Clone)]
pub(crate) struct WalletConstructor {
  ord_client: reqwest::blocking::Client,
  name: String,
  no_sync: bool,
  rpc_url: Url,
  settings: Settings,
}

impl WalletConstructor {
  pub(crate) fn construct(
    name: String,
    no_sync: bool,
    settings: Settings,
    rpc_url: Url,
  ) -> Result<Wallet> {
    let mut headers = HeaderMap::new();
    headers.insert(
      reqwest::header::ACCEPT,
      reqwest::header::HeaderValue::from_static("application/json"),
    );

    if let Some((username, password)) = settings.credentials() {
      let credentials = base64_encode(format!("{username}:{password}").as_bytes());
      headers.insert(
        reqwest::header::AUTHORIZATION,
        reqwest::header::HeaderValue::from_str(&format!("Basic {credentials}")).unwrap(),
      );
    }

    Self {
      ord_client: reqwest::blocking::ClientBuilder::new()
        .timeout(None)
        .default_headers(headers.clone())
        .build()?,
      name,
      no_sync,
      rpc_url,
      settings,
    }
    .build()
  }

  pub(crate) fn build(self) -> Result<Wallet> {
    let database = Wallet::open_database(&self.name, &self.settings)?;

    let bitcoin_client = {
      let client =
        Wallet::check_version(self.settings.bitcoin_rpc_client(Some(self.name.clone()))?)?;

      if !client.list_wallets()?.contains(&self.name) {
        loop {
          match client.load_wallet(&self.name) {
            Ok(_) => {
              break;
            }
            Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
              if err.code == -4 && err.message == "Wallet already loading." =>
            {
              // wallet loading
              eprint!(".");
              thread::sleep(Duration::from_secs(3));
              continue;
            }
            Err(bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(err)))
              if err.code == -35 =>
            {
              // wallet already loaded
              break;
            }
            Err(err) => {
              bail!("Failed to load wallet {}: {err}", self.name);
            }
          }
        }
      }

      if client.get_wallet_info()?.private_keys_enabled {
        Wallet::check_descriptors(
          &self.name,
          client
            .call::<ListDescriptorsResult>("listdescriptors", &[serde_json::Value::Null])?
            .descriptors,
        )?;
      }

      client
    };

    let bitcoin_block_count = bitcoin_client.get_block_count().unwrap() + 1;

    if !self.no_sync {
      for i in 0.. {
        let ord_block_count = self.get("/blockcount")?.text()?.parse::<u64>().expect(
          "wallet failed to retrieve block count from server. Make sure `ord server` is running.",
        );

        if ord_block_count >= bitcoin_block_count {
          break;
        } else if i == 20 {
          bail!(
            "`ord server` {} blocks behind `bitcoind`, consider using `--no-sync` to ignore this error",
            bitcoin_block_count - ord_block_count
          );
        }
        std::thread::sleep(Duration::from_millis(50));
      }
    }

    let mut utxos = Self::get_utxos(&bitcoin_client)?;
    let locked_utxos = Self::get_locked_utxos(&bitcoin_client)?;
    utxos.extend(locked_utxos.clone());

    let output_info = self.get_output_info(utxos.clone().into_keys().collect())?;

    let inscriptions = output_info
      .iter()
      .flat_map(|(_output, info)| info.inscriptions.clone().unwrap_or_default())
      .collect::<Vec<InscriptionId>>();

    let (inscriptions, inscription_info) = self.get_inscriptions(&inscriptions)?;

    let status = self.get_server_status()?;

    Ok(Wallet {
      bitcoin_client,
      database,
      has_rune_index: status.rune_index,
      has_sat_index: status.sat_index,
      inscription_info,
      inscriptions,
      locked_utxos,
      ord_client: self.ord_client,
      output_info,
      rpc_url: self.rpc_url,
      settings: self.settings,
      utxos,
    })
  }

  fn get_output_info(&self, outputs: Vec<OutPoint>) -> Result<BTreeMap<OutPoint, api::Output>> {
    let response = self.post("/outputs", &outputs)?;

    if !response.status().is_success() {
      bail!("wallet failed get outputs: {}", response.text()?);
    }

    let response_outputs = serde_json::from_str::<Vec<api::Output>>(&response.text()?)?;

    ensure! {
      response_outputs.len() == outputs.len(),
      "unexpected server `/outputs` response length",
    }

    let output_info: BTreeMap<OutPoint, api::Output> =
      outputs.into_iter().zip(response_outputs).collect();

    for (output, info) in &output_info {
      if !info.indexed {
        bail!("output in wallet but not in ord server: {output}");
      }
    }

    Ok(output_info)
  }

  fn get_inscriptions(
    &self,
    inscriptions: &Vec<InscriptionId>,
  ) -> Result<(
    BTreeMap<SatPoint, Vec<InscriptionId>>,
    BTreeMap<InscriptionId, api::Inscription>,
  )> {
    let response = self.post("/inscriptions", inscriptions)?;

    if !response.status().is_success() {
      bail!("wallet failed get inscriptions: {}", response.text()?);
    }

    let mut inscriptions = BTreeMap::new();
    let mut inscription_infos = BTreeMap::new();
    for info in serde_json::from_str::<Vec<api::Inscription>>(&response.text()?)? {
      inscriptions
        .entry(info.satpoint)
        .or_insert_with(Vec::new)
        .push(info.id);

      inscription_infos.insert(info.id, info);
    }

    Ok((inscriptions, inscription_infos))
  }

  fn get_utxos(bitcoin_client: &Client) -> Result<BTreeMap<OutPoint, TxOut>> {
    Ok(
      bitcoin_client
        .list_unspent(None, None, None, None, None)?
        .into_iter()
        .map(|utxo| {
          let outpoint = OutPoint::new(utxo.txid, utxo.vout);
          let txout = TxOut {
            script_pubkey: utxo.script_pub_key,
            value: utxo.amount,
          };

          (outpoint, txout)
        })
        .collect(),
    )
  }

  fn get_locked_utxos(bitcoin_client: &Client) -> Result<BTreeMap<OutPoint, TxOut>> {
    #[derive(Deserialize)]
    pub(crate) struct JsonOutPoint {
      txid: Txid,
      vout: u32,
    }

    let outpoints = bitcoin_client.call::<Vec<JsonOutPoint>>("listlockunspent", &[])?;

    let mut utxos = BTreeMap::new();

    for outpoint in outpoints {
      let Some(tx_out) = bitcoin_client.get_tx_out(&outpoint.txid, outpoint.vout, Some(false))?
      else {
        continue;
      };

      utxos.insert(
        OutPoint::new(outpoint.txid, outpoint.vout),
        TxOut {
          value: tx_out.value,
          script_pubkey: ScriptBuf::from_bytes(tx_out.script_pub_key.hex),
        },
      );
    }

    Ok(utxos)
  }

  fn get_server_status(&self) -> Result<api::Status> {
    let response = self.get("/status")?;

    if !response.status().is_success() {
      bail!("could not get status: {}", response.text()?)
    }

    Ok(serde_json::from_str(&response.text()?)?)
  }

  pub fn get(&self, path: &str) -> Result<reqwest::blocking::Response> {
    self
      .ord_client
      .get(self.rpc_url.join(path)?)
      .send()
      .map_err(|err| anyhow!(err))
  }

  pub fn post(&self, path: &str, body: &impl Serialize) -> Result<reqwest::blocking::Response> {
    self
      .ord_client
      .post(self.rpc_url.join(path)?)
      .json(body)
      .header(reqwest::header::ACCEPT, "application/json")
      .send()
      .map_err(|err| anyhow!(err))
  }
}