diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e2b86b17..2379827477 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,37 @@ Changelog ========= +[0.8.3](https://github.com/ordinals/ord/releases/tag/0.8.3) - 2023-08-28 +------------------------------------------------------------------------ + +### Added + +- Tweaks to front-end (#2381) +- Add some links to docs (#2364) +- Add testing guide for recursion (#2357) +- Make homepage more interesting (#2374) +- Add proper block inscriptions HTML (#2337) +- Render GLB/GLTF models in preview (#2369) +- Add tags and inscription id documentation (#2351) +- Add hint about maximum number of open files for testing (#2348) +- Reduce index durability when testing (#2347) +- Homogenize design (#2346) + +### Fixed + +- Fix slice error for inscriptions block view (#2378) +- Use correct height and depth in reorg log (#2352) + +### Changed + +- Remove transaction ID to inscription ID conversion (#2370) +- Return JSON from all commands (#2355) +- Allow splitting merged inscriptions (#1927) +- Update explorer.md (#2215) +- Recognize media types without explicit charset (#2349) + [0.8.2](https://github.com/ordinals/ord/releases/tag/0.8.2) - 2023-08-17 ---------------------------------------------------------------------- +------------------------------------------------------------------------ ### Added @@ -14,14 +43,12 @@ Changelog - Add JSON API endpoint `/sat/` (#2250) - Add `amount` field to `wallet inscriptions` output. (#1928) - ### Changed - Only fetch inscriptions that are owned by the ord wallet (#2310) - Inform user when redb starts in recovery mode (#2304) - Select multiple utxos (#2303) - ### Fixed - Use `--fee-rate` when sending an amount (#1922) @@ -29,7 +56,6 @@ Changelog - Fix dust limit for padding in `TransactionBuilder` (#1929) - Fix remote RPC wallet commands (#1766) - [0.8.1](https://github.com/ordinals/ord/releases/tag/0.8.1) - 2023-07-23 --------------------------------------------------------------------- @@ -55,7 +81,6 @@ Changelog - Fix docs inconsistency (#2276) - Add contributing section (#2261) - [0.8.0](https://github.com/ordinals/ord/releases/tag/0.8.0) - 2023-07-01 --------------------------------------------------------------------- @@ -66,7 +91,6 @@ Changelog - Update redb from 0.13.0 to 1.0.2 (#2141) - Fix typo in BIP (#2220) - [0.7.0](https://github.com/ordinals/ord/releases/tag/0.7.0) - 2023-06-23 --------------------------------------------------------------------- @@ -78,7 +102,6 @@ Changelog - Add blob urls to Content Security Policy headers (#2203) - Check inscribe destination address network (#2189) - [0.6.2](https://github.com/ordinals/ord/releases/tag/0.6.2) - 2023-06-15 --------------------------------------------------------------------- @@ -91,7 +114,6 @@ Changelog ### Misc - Update ord dependency in lockfile (#2168) - [0.6.1](https://github.com/ordinals/ord/releases/tag/0.6.1) - 2023-06-06 --------------------------------------------------------------------- @@ -99,7 +121,6 @@ Changelog - Fix sat index test and unbound assignment (#2154) - Updated install.sh for new repo name (#2155) - [0.6.0](https://github.com/ordinals/ord/releases/tag/0.6.0) - 2023-06-04 --------------------------------------------------------------------- @@ -127,7 +148,6 @@ Changelog - Fix test name typos(#2043) - Switch to nightly clippy (#2037) - [0.5.2](https://github.com/ordinals/ord/releases/tag/0.5.2) - 2023-04-17 --------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 17f34a3bfc..f235c1474e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2037,7 +2037,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord-litecoin" -version = "0.8.2" +version = "0.8.3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 8442cba8ad..423664a487 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord-litecoin" description = "◉ Ordinal wallet and block explorer for litecoin" -version = "0.8.2" +version = "0.8.3" license = "CC0-1.0" edition = "2021" autotests = false diff --git a/README.md b/README.md index 7d73143e53..307bea4647 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,10 @@ just doc just watch ltest --all ``` +If the tests are failing or hanging, you might need to increase the maximum +number of open files by running `ulimit -n 1024` in your shell before you run +the tests, or in your shell configuration. + We also try to follow a TDD (Test-Driven-Development) approach, which means we use tests as a way to get visibility into the code. Tests have to run fast for that reason so that the feedback loop between making a change, running the test and diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 46b956fd39..9f9e404788 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -3,6 +3,7 @@ - [Overview](overview.md) - [Digital Artifacts](digital-artifacts.md) - [Inscriptions](inscriptions.md) + - [Recursion](inscriptions/recursion.md) - [FAQ](faq.md) - [Contributing](contributing.md) - [Donate](donate.md) @@ -12,6 +13,7 @@ - [Sat Hunting](guides/sat-hunting.md) - [Collecting](guides/collecting.md) - [Sparrow Wallet](guides/collecting/sparrow-wallet.md) + - [Testing](guides/testing.md) - [Moderation](guides/moderation.md) - [Bounties](bounties.md) - [Bounty 0: 100,000 sats Claimed!](bounty/0.md) diff --git a/docs/src/guides/explorer.md b/docs/src/guides/explorer.md index 311390aac0..94799a71e7 100644 --- a/docs/src/guides/explorer.md +++ b/docs/src/guides/explorer.md @@ -5,6 +5,19 @@ The `ord` binary includes a block explorer. We host a instance of the block explorer on mainnet at [ordinals.com](https://ordinals.com), and on signet at [signet.ordinals.com](https://signet.ordinals.com). +### 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` + +To test how your inscriptions will look you can run: + +`ord preview ...` + Search ------ diff --git a/docs/src/guides/testing.md b/docs/src/guides/testing.md new file mode 100644 index 0000000000..e82ebd37a4 --- /dev/null +++ b/docs/src/guides/testing.md @@ -0,0 +1,73 @@ +Testing +======= + +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 [inscriptions](inscriptions.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 or indexing ord. + +Example +------- + +Run bitcoind in regtest with: +``` +bitcoind -regtest -txindex +``` +Create a wallet in regtest with: +``` +ord -r wallet create +``` +Get a regtest receive address with: +``` +ord -r wallet receive +``` +Mine 101 blocks (to unlock the coinbase) with: +``` +bitcoin-cli generatetoaddress 101 +``` +Inscribe in regtest with: +``` +ord -r wallet inscribe --fee-rate 1 +``` +Mine the inscription with: +``` +bitcoin-cli generatetoaddress 1 +``` +View the inscription in the regtest explorer: +``` +ord -r server +``` + +Testing Recursion +----------------- + +When testing out [recursion](../inscriptions/recursion.md), inscribe the +dependencies first (example with [p5.js](https://p5js.org): +``` +ord -r wallet inscribe --fee-rate 1 p5.js +``` +This should return a `inscription_id` which you can then reference in your +recursive inscription. + +ATTENTION: These ids will be different when inscribing on +mainnet or signet, so be sure to change those in your recursive inscription for +each chain. + +Then you can inscribe your recursive inscription with: +``` +ord -r wallet inscribe --fee-rate 1 recursive-inscription.html +``` +Finally you will have to mine some blocks and start the server: +``` +bitcoin-cli generatetoaddress 6 +ord -r server +``` diff --git a/docs/src/inscriptions.md b/docs/src/inscriptions.md index 27ba6971f0..056918dd2c 100644 --- a/docs/src/inscriptions.md +++ b/docs/src/inscriptions.md @@ -69,40 +69,59 @@ 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. -Sandboxing ----------- +Fields +------ -HTML and SVG inscriptions are sandboxed in order to prevent references to -off-chain content, thus keeping inscriptions immutable and self-contained. +Inscriptions may include fields before an optional body. Each field consists of +two data pushes, a tag and a value. -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. +Currently, the only defined field is `content-type`, with a tag of `1`, whose +value is the MIME type of the body. + +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. -Recursion ---------- +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. -An important exception to sandboxing is recursion: access to `ord`'s `/content` -endpoint is permitted, allowing inscriptions to access the content of other -inscriptions by requesting `/content/`. +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. -This has a number of interesting use-cases: +Inscription IDs +--------------- -- Remixing the content of existing inscriptions. +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: -- Publishing snippets of code, images, audio, or stylesheets as shared public - resources. +`521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0` -- Generative art collections where an algorithm is inscribed as JavaScript, - and instantiated from multiple inscriptions with unique seeds. +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. -- 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. +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`. -A couple other endpoints that inscriptions may access are the following: +| Input | Inscription Count | Indices | +|:-----:|:-----------------:|:----------:| +| 0 | 2 | i0, i1 | +| 1 | 1 | i2 | +| 2 | 3 | i3, i4, i5 | +| 3 | 0 | | +| 4 | 1 | i6 | -- `/blockheight`: latest block height. -- `/blockhash`: latest block hash. -- `/blockhash/`: block hash at given block height. -- `/blocktime`: UNIX time stamp of latest block. +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. diff --git a/docs/src/inscriptions/recursion.md b/docs/src/inscriptions/recursion.md new file mode 100644 index 0000000000..43a2eabdb7 --- /dev/null +++ b/docs/src/inscriptions/recursion.md @@ -0,0 +1,27 @@ +Recursion +========= + +An important exception to [sandboxing](../inscriptions.md#sandboxing) is recursion: access to `ord`'s `/content` +endpoint is permitted, allowing inscriptions to access the content of other +inscriptions by requesting `/content/`. + +This 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. + +A few other endpoints that inscriptions may access are the following: + +- `/blockheight`: latest block height. +- `/blockhash`: latest block hash. +- `/blockhash/`: block hash at given block height. +- `/blocktime`: UNIX time stamp of latest block. diff --git a/docs/src/introduction.md b/docs/src/introduction.md index d6ff031a49..28686f5270 100644 --- a/docs/src/introduction.md +++ b/docs/src/introduction.md @@ -38,6 +38,8 @@ 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) diff --git a/src/arguments.rs b/src/arguments.rs index c3af6814d6..17c6050ebb 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -10,7 +10,7 @@ pub(crate) struct Arguments { } impl Arguments { - pub(crate) fn run(self) -> Result { + pub(crate) fn run(self) -> SubcommandResult { self.subcommand.run(self.options) } } diff --git a/src/index.rs b/src/index.rs index 8798aa139a..ee956ddc00 100644 --- a/src/index.rs +++ b/src/index.rs @@ -137,12 +137,13 @@ impl BitcoinCoreRpcResultExt for Result { pub(crate) struct Index { client: Client, database: Database, - path: PathBuf, + durability: redb::Durability, first_inscription_height: u64, genesis_block_coinbase_transaction: Transaction, genesis_block_coinbase_txid: Txid, height_limit: Option, options: Options, + path: PathBuf, unrecoverably_reorged: AtomicBool, } @@ -188,6 +189,12 @@ impl Index { log::info!("Setting DB cache size to {} bytes", db_cache_size); + let durability = if cfg!(test) { + redb::Durability::None + } else { + redb::Durability::Immediate + }; + let database = match Database::builder() .set_cache_size(db_cache_size) .open(&path) @@ -224,7 +231,7 @@ impl Index { let mut tx = database.begin_write()?; - tx.set_durability(redb::Durability::Immediate); + tx.set_durability(durability); tx.open_table(HEIGHT_TO_BLOCK_HASH)?; tx.open_table(INSCRIPTION_ID_TO_INSCRIPTION_ENTRY)?; @@ -258,15 +265,21 @@ impl Index { genesis_block_coinbase_txid: genesis_block_coinbase_transaction.txid(), client, database, - path, + durability, first_inscription_height: options.first_inscription_height(), genesis_block_coinbase_transaction, height_limit: options.height_limit, options: options.clone(), + path, unrecoverably_reorged: AtomicBool::new(false), }) } + #[cfg(test)] + fn set_durability(&mut self, durability: redb::Durability) { + self.durability = durability; + } + pub(crate) fn get_unspent_outputs(&self, _wallet: Wallet) -> Result> { let mut utxos = BTreeMap::new(); utxos.extend( @@ -411,7 +424,7 @@ impl Index { log::info!("{}", err.to_string()); match err.downcast_ref() { - Some(&ReorgError::Recoverable((height, depth))) => { + Some(&ReorgError::Recoverable { height, depth }) => { Reorg::handle_reorg(self, height, depth)?; updater = Updater::new(self)?; @@ -507,7 +520,9 @@ impl Index { } fn begin_write(&self) -> Result { - Ok(self.database.begin_write()?) + let mut tx = self.database.begin_write()?; + tx.set_durability(self.durability); + Ok(tx) } fn increment_statistic(wtx: &WriteTransaction, statistic: Statistic, n: u64) -> Result { @@ -868,20 +883,6 @@ impl Index { Ok(result) } - pub(crate) fn get_homepage_inscriptions(&self) -> Result> { - Ok( - self - .database - .begin_read()? - .open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)? - .iter()? - .rev() - .take(8) - .flat_map(|result| result.map(|(_number, id)| Entry::load(*id.value()))) - .collect(), - ) - } - pub(crate) fn get_latest_inscriptions_with_prev_and_next( &self, n: usize, @@ -1290,7 +1291,7 @@ mod tests { let context = Context::builder().build(); context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(template.clone()); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); assert_eq!( @@ -1316,7 +1317,7 @@ mod tests { .build(); context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(template); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); assert_eq!( @@ -1631,7 +1632,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1665,7 +1666,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1694,7 +1695,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1719,7 +1720,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1764,14 +1765,20 @@ mod tests { ..Default::default() }); - let first_inscription_id = InscriptionId::from(first_txid); + let first_inscription_id = InscriptionId { + txid: first_txid, + index: 0, + }; let second_txid = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0)], witness: inscription("text/png", [1; 100]).to_witness(), ..Default::default() }); - let second_inscription_id = InscriptionId::from(second_txid); + let second_inscription_id = InscriptionId { + txid: second_txid, + index: 0, + }; context.mine_blocks(1); @@ -1818,7 +1825,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1867,7 +1874,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1911,7 +1918,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1947,7 +1954,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -1984,7 +1991,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; let coinbase_tx = context.mine_blocks(1)[0].txdata[0].txid(); @@ -2013,7 +2020,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks_with_subsidy(1, 0); @@ -2039,7 +2046,10 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let first_inscription_id = InscriptionId::from(first_txid); + let first_inscription_id = InscriptionId { + txid: first_txid, + index: 0, + }; context.mine_blocks_with_subsidy(1, 0); context.mine_blocks(1); @@ -2050,7 +2060,10 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let second_inscription_id = InscriptionId::from(second_txid); + let second_inscription_id = InscriptionId { + txid: second_txid, + index: 0, + }; context.mine_blocks_with_subsidy(1, 0); @@ -2164,7 +2177,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); context.rpc_server.broadcast_tx(TransactionTemplate { @@ -2197,7 +2210,7 @@ mod tests { output_values: &[0, 50 * COIN_VALUE], ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); context.index.assert_inscription_location( @@ -2222,7 +2235,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks_with_subsidy(1, 0); context.index.assert_inscription_location( @@ -2332,7 +2345,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; assert_eq!( context @@ -2393,7 +2406,10 @@ mod tests { context.mine_blocks(1); - let inscription_id = InscriptionId::from(first); + let inscription_id = InscriptionId { + txid: first, + index: 0, + }; assert_eq!( context @@ -2424,7 +2440,10 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(second); + let inscription_id = InscriptionId { + txid: second, + index: 0, + }; context.mine_blocks(1); @@ -2442,13 +2461,19 @@ mod tests { assert!(context .index - .get_inscription_entry(second.into()) + .get_inscription_by_id(InscriptionId { + txid: second, + index: 0 + }) .unwrap() .is_some()); assert!(context .index - .get_inscription_by_id(second.into()) + .get_inscription_by_id(InscriptionId { + txid: second, + index: 0 + }) .unwrap() .is_some()); } @@ -2464,7 +2489,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -2491,7 +2516,7 @@ mod tests { witness: inscription("text/plain", "hello").to_witness(), ..Default::default() }); - ids.push(InscriptionId::from(txid)); + ids.push(InscriptionId { txid, index: 0 }); context.mine_blocks(1); } @@ -2564,7 +2589,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; context.mine_blocks(1); @@ -3305,7 +3330,9 @@ mod tests { #[test] fn recover_from_reorg() { - for context in Context::configurations() { + for mut context in Context::configurations() { + context.index.set_durability(redb::Durability::Immediate); + context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -3355,7 +3382,9 @@ mod tests { #[test] fn recover_from_3_block_deep_and_consecutive_reorg() { - for context in Context::configurations() { + for mut context in Context::configurations() { + context.index.set_durability(redb::Durability::Immediate); + context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { @@ -3408,7 +3437,9 @@ mod tests { #[test] fn recover_from_very_unlikely_7_block_deep_reorg() { - for context in Context::configurations() { + for mut context in Context::configurations() { + context.index.set_durability(redb::Durability::Immediate); + context.mine_blocks(1); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { diff --git a/src/index/block_index.rs b/src/index/block_index.rs index c036080f22..5a16928ca2 100644 --- a/src/index/block_index.rs +++ b/src/index/block_index.rs @@ -171,9 +171,23 @@ impl BlockIndex { index: &Index, block_height: u64, ) -> Result> { + if index.block_count()? + > self.lowest_blessed_by_block.len() as u64 + self.first_inscription_height + { + return Err(anyhow!( + "Block index not fully indexed ({} indexed of {})", + self.lowest_blessed_by_block.len() as u64 + self.first_inscription_height, + index.block_count()? + )); + } if block_height >= index.block_count()? || block_height < self.first_inscription_height { return Ok(Vec::new()); } + + if self.lowest_blessed_by_block.is_empty() { + return Err(anyhow!("Block index not yet initialized")); + } + let lowest_cursed = self.lowest_cursed_by_block [usize::try_from(block_height.saturating_sub(self.first_inscription_height))?]; let lowest_blessed = self.lowest_blessed_by_block @@ -198,4 +212,30 @@ impl BlockIndex { Ok(inscriptions) } + + pub(crate) fn get_highest_paying_inscriptions_in_block( + &self, + index: &Index, + block_height: u64, + n: usize, + ) -> Result<(Vec, usize)> { + let inscription_ids = self.get_inscriptions_in_block(index, block_height)?; + + let mut inscription_to_fee: Vec<(InscriptionId, u64)> = Vec::new(); + for id in &inscription_ids { + inscription_to_fee.push((*id, index.get_inscription_entry(*id)?.unwrap().fee)); + } + + inscription_to_fee.sort_by_key(|(_, fee)| *fee); + + Ok(( + inscription_to_fee + .iter() + .map(|(id, _)| *id) + .rev() + .take(n) + .collect(), + inscription_ids.len(), + )) + } } diff --git a/src/index/reorg.rs b/src/index/reorg.rs index 8de5da973c..fc6164282c 100644 --- a/src/index/reorg.rs +++ b/src/index/reorg.rs @@ -2,14 +2,14 @@ use {super::*, updater::BlockData}; #[derive(Debug, PartialEq)] pub(crate) enum ReorgError { - Recoverable((u64, u64)), + Recoverable { height: u64, depth: u64 }, Unrecoverable, } impl fmt::Display for ReorgError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ReorgError::Recoverable((height, depth)) => { + ReorgError::Recoverable { height, depth } => { write!(f, "{depth} block deep reorg detected at height {height}") } ReorgError::Unrecoverable => write!(f, "unrecoverable reorg detected"), @@ -43,7 +43,7 @@ impl Reorg { .into_option()?; if index_block_hash == bitcoind_block_hash { - return Err(anyhow!(ReorgError::Recoverable((depth, height)))); + return Err(anyhow!(ReorgError::Recoverable { height, depth })); } } @@ -56,6 +56,10 @@ impl Reorg { pub(crate) fn handle_reorg(index: &Index, height: u64, depth: u64) -> 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 = @@ -75,6 +79,10 @@ impl Reorg { } pub(crate) fn update_savepoints(index: &Index, height: u64) -> Result { + if let redb::Durability::None = index.durability { + return Ok(()); + } + if (height < SAVEPOINT_INTERVAL || height % SAVEPOINT_INTERVAL == 0) && index .client diff --git a/src/index/updater.rs b/src/index/updater.rs index 0828f156d3..d053e1bd8e 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -273,7 +273,7 @@ impl<'index> Updater<'_> { // 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 { + for _ in 0..BATCH_SIZE - 1 { let Ok(outpoint) = outpoint_receiver.try_recv() else { break; }; @@ -296,7 +296,10 @@ impl<'index> Updater<'_> { }; // Send all tx output values back in order for (i, tx) in txs.iter().flatten().enumerate() { - let Ok(_) = value_sender.send(tx.output[usize::try_from(outpoints[i].vout).unwrap()].value).await else { + let Ok(_) = value_sender + .send(tx.output[usize::try_from(outpoints[i].vout).unwrap()].value) + .await + else { log::error!("Value channel closed unexpectedly"); return; }; diff --git a/src/inscription.rs b/src/inscription.rs index 5506c5f361..a63d4f0d4f 100644 --- a/src/inscription.rs +++ b/src/inscription.rs @@ -44,7 +44,9 @@ impl Inscription { pub(crate) fn from_transaction(tx: &Transaction) -> Vec { let mut result = Vec::new(); for (index, tx_in) in tx.input.iter().enumerate() { - let Ok(inscriptions) = InscriptionParser::parse(&tx_in.witness) else { continue }; + let Ok(inscriptions) = InscriptionParser::parse(&tx_in.witness) else { + continue; + }; result.extend( inscriptions diff --git a/src/inscription_id.rs b/src/inscription_id.rs index 998faeb3c3..cb70b4baa1 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -85,12 +85,6 @@ impl FromStr for InscriptionId { } } -impl From for InscriptionId { - fn from(txid: Txid) -> Self { - Self { txid, index: 0 } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/lib.rs b/src/lib.rs index 75639f9ced..a05274a6d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ use { options::Options, outgoing::Outgoing, representation::Representation, - subcommand::Subcommand, + subcommand::{Subcommand, SubcommandResult}, tally::Tally, }, anyhow::{anyhow, bail, Context, Error}, @@ -180,22 +180,25 @@ pub fn main() { }) .expect("Error setting handler"); - if let Err(err) = Arguments::parse().run() { - eprintln!("error: {err}"); - err - .chain() - .skip(1) - .for_each(|cause| eprintln!("because: {cause}")); - if env::var_os("RUST_BACKTRACE") - .map(|val| val == "1") - .unwrap_or_default() - { - eprintln!("{}", err.backtrace()); - } + match Arguments::parse().run() { + Err(err) => { + eprintln!("error: {err}"); + err + .chain() + .skip(1) + .for_each(|cause| eprintln!("because: {cause}")); + if env::var_os("RUST_BACKTRACE") + .map(|val| val == "1") + .unwrap_or_default() + { + eprintln!("{}", err.backtrace()); + } - gracefully_shutdown_indexer(); + gracefully_shutdown_indexer(); - process::exit(1); + process::exit(1); + } + Ok(output) => output.print_json(), } gracefully_shutdown_indexer(); diff --git a/src/media.rs b/src/media.rs index 2ec4e63dbe..ff43ba40a2 100644 --- a/src/media.rs +++ b/src/media.rs @@ -9,6 +9,7 @@ pub(crate) enum Media { Audio, Iframe, Image, + Model, Pdf, Text, Unknown, @@ -31,13 +32,17 @@ impl Media { ("image/png", Media::Image, &["png"]), ("image/svg+xml", Media::Iframe, &["svg"]), ("image/webp", Media::Image, &["webp"]), - ("model/gltf-binary", Media::Unknown, &["glb"]), + ("model/gltf+json", Media::Model, &["gltf"]), + ("model/gltf-binary", Media::Model, &["glb"]), ("model/stl", Media::Unknown, &["stl"]), ("text/css", Media::Text, &["css"]), + ("text/html", Media::Iframe, &[]), ("text/html;charset=utf-8", Media::Iframe, &["html"]), ("text/javascript", Media::Text, &["js"]), - ("text/plain;charset=utf-8", Media::Text, &["txt"]), + ("text/markdown", Media::Text, &[]), ("text/markdown;charset=utf-8", Media::Text, &["md"]), + ("text/plain", Media::Text, &[]), + ("text/plain;charset=utf-8", Media::Text, &["txt"]), ("video/mp4", Media::Video, &["mp4"]), ("video/webm", Media::Video, &["webm"]), ]; diff --git a/src/options.rs b/src/options.rs index a8bc522411..38d4190d38 100644 --- a/src/options.rs +++ b/src/options.rs @@ -386,9 +386,14 @@ mod tests { #[test] fn cookie_file_defaults_to_bitcoin_data_dir() { - let arguments = - Arguments::try_parse_from(["ord", "--litecoin-data-dir=foo", "--chain=signet", "index", "run"]) - .unwrap(); + let arguments = Arguments::try_parse_from([ + "ord", + "--litecoin-data-dir=foo", + "--chain=signet", + "index", + "run", + ]) + .unwrap(); let cookie_file = arguments .options diff --git a/src/subcommand.rs b/src/subcommand.rs index 7a880923ca..1f8036c4ed 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -13,12 +13,6 @@ pub mod supply; pub mod traits; pub mod wallet; -fn print_json(output: impl Serialize) -> Result { - serde_json::to_writer_pretty(io::stdout(), &output)?; - println!(); - Ok(()) -} - #[derive(Debug, Parser)] pub(crate) enum Subcommand { #[clap(about = "List the first litoshis of each reward epoch")] @@ -48,7 +42,7 @@ pub(crate) enum Subcommand { } impl Subcommand { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { Self::Epochs => epochs::run(), Self::Preview(preview) => preview.run(), @@ -70,3 +64,22 @@ impl Subcommand { } } } + +#[derive(Serialize, Deserialize)] +pub struct Empty {} + +pub(crate) trait Output: Send { + fn print_json(&self); +} + +impl Output for T +where + T: Serialize + Send, +{ + fn print_json(&self) { + serde_json::to_writer_pretty(io::stdout(), self).ok(); + println!(); + } +} + +pub(crate) type SubcommandResult = Result>; diff --git a/src/subcommand/epochs.rs b/src/subcommand/epochs.rs index 53293cb025..39534a65ca 100644 --- a/src/subcommand/epochs.rs +++ b/src/subcommand/epochs.rs @@ -5,13 +5,11 @@ pub struct Output { pub starting_sats: Vec, } -pub(crate) fn run() -> Result { +pub(crate) fn run() -> SubcommandResult { let mut starting_sats = Vec::new(); for sat in Epoch::STARTING_SATS { starting_sats.push(sat); } - print_json(Output { starting_sats })?; - - Ok(()) + Ok(Box::new(Output { starting_sats })) } diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index fdf314cc48..fb81e19b33 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -12,16 +12,13 @@ pub struct Output { } impl Find { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; match index.find(self.sat.0)? { - Some(satpoint) => { - print_json(Output { satpoint })?; - Ok(()) - } + Some(satpoint) => Ok(Box::new(Output { satpoint })), None => Err(anyhow!("sat has not been mined as of index height")), } } diff --git a/src/subcommand/index.rs b/src/subcommand/index.rs index 0193011636..aea938a8a3 100644 --- a/src/subcommand/index.rs +++ b/src/subcommand/index.rs @@ -9,7 +9,7 @@ pub(crate) enum IndexSubcommand { } impl IndexSubcommand { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { Self::Export(export) => export.run(options), Self::Run => index::run(options), @@ -30,20 +30,20 @@ pub(crate) struct Export { } impl Export { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; index.export(&self.tsv, self.include_addresses)?; - Ok(()) + Ok(Box::new(Empty {})) } } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; - Ok(()) + Ok(Box::new(Empty {})) } diff --git a/src/subcommand/info.rs b/src/subcommand/info.rs index aa96de00b4..737cdd2228 100644 --- a/src/subcommand/info.rs +++ b/src/subcommand/info.rs @@ -15,7 +15,7 @@ pub struct TransactionsOutput { } impl Info { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; let info = index.info()?; @@ -32,11 +32,9 @@ impl Info { elapsed: (end.starting_timestamp - start.starting_timestamp) as f64 / 1000.0 / 60.0, }); } - print_json(output)?; + Ok(Box::new(output)) } else { - print_json(info)?; + Ok(Box::new(info)) } - - Ok(()) } } diff --git a/src/subcommand/list.rs b/src/subcommand/list.rs index 74a9d46731..d406e39357 100644 --- a/src/subcommand/list.rs +++ b/src/subcommand/list.rs @@ -18,7 +18,7 @@ pub struct Output { } impl List { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -47,9 +47,7 @@ impl List { }); } - print_json(outputs)?; - - Ok(()) + Ok(Box::new(outputs)) } Some(crate::index::List::Spent) => Err(anyhow!("output spent.")), None => Err(anyhow!("output not found")), @@ -92,8 +90,8 @@ mod tests { offset: u64, rarity: Rarity, name: String, - ) -> Output { - Output { + ) -> super::Output { + super::Output { output, start, end, diff --git a/src/subcommand/parse.rs b/src/subcommand/parse.rs index 73c815d3de..40854b9b73 100644 --- a/src/subcommand/parse.rs +++ b/src/subcommand/parse.rs @@ -12,10 +12,9 @@ pub struct Output { } impl Parse { - pub(crate) fn run(self) -> Result { - print_json(Output { + pub(crate) fn run(self) -> SubcommandResult { + Ok(Box::new(Output { object: self.object, - })?; - Ok(()) + })) } } diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 543598bd68..66b058a5ac 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -16,7 +16,7 @@ impl Drop for KillOnDrop { } impl Preview { - pub(crate) fn run(self) -> Result { + pub(crate) fn run(self) -> SubcommandResult { let tmpdir = TempDir::new()?; let rpc_port = TcpListener::bind("127.0.0.1:0")?.local_addr()?.port(); @@ -102,8 +102,6 @@ impl Preview { options, subcommand: Subcommand::Server(self.server), } - .run()?; - - Ok(()) + .run() } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 2363f2e747..ee8a2b623a 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -8,10 +8,11 @@ use { crate::index::block_index::BlockIndex, crate::page_config::PageConfig, crate::templates::{ - BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, InscriptionsHtml, - InscriptionsJson, OutputHtml, OutputJson, PageContent, PageHtml, PreviewAudioHtml, - PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, - RangeHtml, RareTxt, SatHtml, SatJson, TransactionHtml, + BlockHtml, ClockSvg, HomeHtml, InputHtml, InscriptionHtml, InscriptionJson, + InscriptionsBlockHtml, InscriptionsHtml, InscriptionsJson, OutputHtml, OutputJson, PageContent, + PageHtml, PreviewAudioHtml, PreviewImageHtml, PreviewModelHtml, PreviewPdfHtml, + PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, SatHtml, SatJson, + TransactionHtml, }, axum::{ body, @@ -137,7 +138,7 @@ pub(crate) struct Server { } impl Server { - pub(crate) fn run(self, options: Options, index: Arc, handle: Handle) -> Result { + pub(crate) fn run(self, options: Options, index: Arc, handle: Handle) -> SubcommandResult { Runtime::new()?.block_on(async { let block_index_state = BlockIndexState { block_index: RwLock::new(BlockIndex::new(&index)?), @@ -196,7 +197,14 @@ impl Server { .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_id", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) - .route("/inscriptions/block/:n", get(Self::inscriptions_in_block)) + .route( + "/inscriptions/block/:height", + get(Self::inscriptions_in_block), + ) + .route( + "/inscriptions/block/:height/:page_index", + get(Self::inscriptions_in_block_from_page), + ) .route("/inscriptions/:from", get(Self::inscriptions_from)) .route("/inscriptions/:from/:n", get(Self::inscriptions_from_n)) .route("/install.sh", get(Self::install_script)) @@ -272,7 +280,7 @@ impl Server { (None, None) => unreachable!(), } - Ok(()) + Ok(Box::new(Empty {}) as Box) }) } @@ -552,20 +560,30 @@ impl Server { async fn home( Extension(page_config): Extension>, Extension(index): Extension>, + Extension(block_index_state): Extension>, ) -> ServerResult> { - Ok( - HomeHtml::new(index.blocks(100)?, index.get_homepage_inscriptions()?) - .page(page_config, index.has_sat_index()?), - ) + let blocks = index.blocks(100)?; + let mut featured_blocks = BTreeMap::new(); + for (height, hash) in blocks.iter().take(5) { + let (inscriptions, _total_num) = block_index_state + .block_index + .read() + .map_err(|err| anyhow!("block index RwLock poisoned: {}", err))? + .get_highest_paying_inscriptions_in_block(&index, *height, 8)?; + featured_blocks.insert(*hash, inscriptions); + } + + Ok(HomeHtml::new(blocks, featured_blocks).page(page_config, index.has_sat_index()?)) } async fn install_script() -> Redirect { - Redirect::to("https://raw.githubusercontent.com/casey/ord/master/install.sh") + Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh") } async fn block( Extension(page_config): Extension>, Extension(index): Extension>, + Extension(block_index_state): Extension>, Path(DeserializeFromStr(query)): Path>, ) -> ServerResult> { let (block, height) = match query { @@ -589,9 +607,21 @@ impl Server { } }; + let (featured_inscriptions, total_num) = block_index_state + .block_index + .read() + .map_err(|err| anyhow!("block index RwLock poisoned: {}", err))? + .get_highest_paying_inscriptions_in_block(&index, height, 8)?; + Ok( - BlockHtml::new(block, Height(height), Self::index_height(&index)?) - .page(page_config, index.has_sat_index()?), + BlockHtml::new( + block, + Height(height), + Self::index_height(&index)?, + total_num, + featured_inscriptions, + ) + .page(page_config, index.has_sat_index()?), ) } @@ -600,7 +630,7 @@ impl Server { Extension(index): Extension>, Path(txid): Path, ) -> ServerResult> { - let inscription = index.get_inscription_by_id(txid.into())?; + let inscription = index.get_inscription_by_id(InscriptionId { txid, index: 0 })?; let blockhash = index.get_transaction_blockhash(txid)?; @@ -610,7 +640,7 @@ impl Server { .get_transaction(txid)? .ok_or_not_found(|| format!("transaction {txid}"))?, blockhash, - inscription.map(|_| txid.into()), + inscription.map(|_| InscriptionId { txid, index: 0 }), page_config.chain, ) .page(page_config, index.has_sat_index()?), @@ -919,6 +949,16 @@ impl Server { ) .into_response(), ), + Media::Model => Ok( + ( + [( + header::CONTENT_SECURITY_POLICY, + "script-src-elem 'self' https://ajax.googleapis.com", + )], + PreviewModelHtml { inscription_id }, + ) + .into_response(), + ), Media::Pdf => Ok( ( [( @@ -1033,16 +1073,47 @@ impl Server { Path(block_height): Path, accept_json: AcceptJson, ) -> ServerResult { + Self::inscriptions_in_block_from_page( + Extension(page_config), + Extension(index), + Extension(block_index_state), + Path((block_height, 0)), + accept_json, + ) + .await + } + + async fn inscriptions_in_block_from_page( + Extension(page_config): Extension>, + Extension(index): Extension>, + Extension(block_index_state): Extension>, + Path((block_height, page_index)): Path<(u64, usize)>, + accept_json: AcceptJson, + ) -> ServerResult { + let block_index = block_index_state + .block_index + .read() + .map_err(|err| anyhow!("block index RwLock poisoned: {}", err))?; + let inscriptions = index - .get_inscriptions_in_block(&block_index_state.block_index.read().unwrap(), block_height)?; + .get_inscriptions_in_block(&block_index, block_height) + .map_err(|e| ServerError::NotFound(format!("Failed to get inscriptions in block: {}", e)))?; + Ok(if accept_json.0 { Json(InscriptionsJson::new(inscriptions, None, None, None, None)).into_response() } else { - InscriptionsHtml { + InscriptionsBlockHtml::new( + block_height, + index.block_height()?.unwrap_or(Height(0)).n(), inscriptions, - prev: None, - next: None, - } + page_index, + ) + .map_err(|e| { + ServerError::NotFound(format!( + "Failed to get inscriptions in inscriptions block page: {}", + e + )) + })? .page(page_config, index.has_sat_index()?) .into_response() }) @@ -1497,7 +1568,7 @@ mod tests { fn install_sh_redirects_to_github() { TestServer::new().assert_redirect( "/install.sh", - "https://raw.githubusercontent.com/casey/ord/master/install.sh", + "https://raw.githubusercontent.com/ordinals/ord/master/install.sh", ); } @@ -1872,7 +1943,7 @@ mod tests { server.mine_blocks(1); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.assert_response_regex( format!("/inscription/{}", inscription_id), @@ -1912,15 +1983,21 @@ mod tests { test_server.mine_blocks(1); test_server.assert_response_regex( - "/", - StatusCode::OK, - ".*Ordinals.* -

Latest Blocks

-
    -
  1. [[:xdigit:]]{64}
  2. -
  3. 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2
  4. + "/", + StatusCode::OK, + ".*Ordinals.* +
    +

    Block 1

    +
    +
    +
    +
    +

    Block 0

    +
    +
    +
.*", - ); + ); } #[test] @@ -1941,7 +2018,7 @@ mod tests { test_server.assert_response_regex( "/", StatusCode::OK, - ".*
    \n(
  1. [[:xdigit:]]{64}
  2. \n){100}
.*" + ".*
    \n(
  1. [[:xdigit:]]{64}
  2. \n){95}
.*" ); } @@ -2147,7 +2224,7 @@ mod tests { let server = TestServer::new(); thread::sleep(Duration::from_millis(100)); - assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 3); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 1); @@ -2155,7 +2232,7 @@ mod tests { server.index.update().unwrap(); - assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 3); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 1); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 1); @@ -2166,7 +2243,7 @@ mod tests { thread::sleep(Duration::from_millis(10)); server.index.update().unwrap(); - assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 6); + assert_eq!(server.index.statistic(crate::index::Statistic::Commits), 2); let info = server.index.info().unwrap(); assert_eq!(info.transactions.len(), 2); @@ -2339,7 +2416,7 @@ mod tests { server.mine_blocks(1); server.assert_response_csp( - format!("/preview/{}", InscriptionId::from(txid)), + format!("/preview/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, "default-src 'self'", ".*
hello
.*", @@ -2360,7 +2437,7 @@ mod tests { server.mine_blocks(1); server.assert_response( - format!("/preview/{}", InscriptionId::from(txid)), + format!("/preview/{}", InscriptionId { txid, index: 0 }), StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error", ); @@ -2384,7 +2461,7 @@ mod tests { server.mine_blocks(1); server.assert_response_csp( - format!("/preview/{}", InscriptionId::from(txid)), + format!("/preview/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, "default-src 'self'", r".*
<script>alert\('hello'\);</script>
.*", @@ -2401,7 +2478,7 @@ mod tests { witness: inscription("audio/flac", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2422,7 +2499,7 @@ mod tests { witness: inscription("application/pdf", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2443,7 +2520,7 @@ mod tests { witness: inscription("image/png", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2469,7 +2546,7 @@ mod tests { server.mine_blocks(1); server.assert_response_csp( - format!("/preview/{}", InscriptionId::from(txid)), + format!("/preview/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, "default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:", "hello", @@ -2490,7 +2567,7 @@ mod tests { server.mine_blocks(1); server.assert_response_csp( - format!("/preview/{}", InscriptionId::from(txid)), + format!("/preview/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, "default-src 'self'", fs::read_to_string("templates/preview-unknown.html").unwrap(), @@ -2507,7 +2584,7 @@ mod tests { witness: inscription("video/webm", "hello").to_witness(), ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2532,7 +2609,7 @@ mod tests { server.mine_blocks(1); server.assert_response_regex( - format!("/inscription/{}", InscriptionId::from(txid)), + format!("/inscription/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, ".*Inscription 0.*", ); @@ -2552,7 +2629,7 @@ mod tests { server.mine_blocks(1); server.assert_response_regex( - format!("/inscription/{}", InscriptionId::from(txid)), + format!("/inscription/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, r".*
sat
\s*
5000000000
\s*
preview
.*", ); @@ -2572,7 +2649,7 @@ mod tests { server.mine_blocks(1); server.assert_response_regex( - format!("/inscription/{}", InscriptionId::from(txid)), + format!("/inscription/{}", InscriptionId { txid, index: 0 }), StatusCode::OK, r".*
output value
\s*
5000000000
\s*
preview
.*", ); @@ -2621,7 +2698,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2643,7 +2720,7 @@ mod tests { ..Default::default() }); - let inscription_id = InscriptionId::from(txid); + let inscription_id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); @@ -2667,7 +2744,7 @@ mod tests { server.mine_blocks(1); - let response = server.get(format!("/content/{}", InscriptionId::from(txid))); + let response = server.get(format!("/content/{}", InscriptionId { txid, index: 0 })); assert_eq!(response.status(), StatusCode::OK); assert_eq!( @@ -2782,7 +2859,7 @@ mod tests { witness: inscription("text/plain;charset=utf-8", "hello").to_witness(), ..Default::default() }); - let inscription = InscriptionId::from(txid); + let inscription = InscriptionId { txid, index: 0 }; bitcoin_rpc_server.mine_blocks(1); let server = TestServer::new_with_bitcoin_rpc_server_and_config( diff --git a/src/subcommand/subsidy.rs b/src/subcommand/subsidy.rs index e293d6d7c6..0a7fbb54ce 100644 --- a/src/subcommand/subsidy.rs +++ b/src/subcommand/subsidy.rs @@ -14,7 +14,7 @@ pub struct Output { } impl Subsidy { - pub(crate) fn run(self) -> Result { + pub(crate) fn run(self) -> SubcommandResult { let first = self.height.starting_sat(); let subsidy = self.height.subsidy(); @@ -23,12 +23,10 @@ impl Subsidy { bail!("block {} has no subsidy", self.height); } - print_json(Output { + Ok(Box::new(Output { first: first.0, subsidy, name: first.name(), - })?; - - Ok(()) + })) } } diff --git a/src/subcommand/supply.rs b/src/subcommand/supply.rs index 4a4b468cca..dcf86d3ed5 100644 --- a/src/subcommand/supply.rs +++ b/src/subcommand/supply.rs @@ -8,7 +8,7 @@ pub struct Output { pub last_mined_in_block: u64, } -pub(crate) fn run() -> Result { +pub(crate) fn run() -> SubcommandResult { let mut last = 0; loop { @@ -18,12 +18,10 @@ pub(crate) fn run() -> Result { last += 1; } - print_json(Output { + Ok(Box::new(Output { supply: Sat::SUPPLY, first: 0, last: Sat::SUPPLY - 1, last_mined_in_block: last, - })?; - - Ok(()) + })) } diff --git a/src/subcommand/traits.rs b/src/subcommand/traits.rs index bfb4999be6..9ef2726655 100644 --- a/src/subcommand/traits.rs +++ b/src/subcommand/traits.rs @@ -21,8 +21,8 @@ pub struct Output { } impl Traits { - pub(crate) fn run(self) -> Result { - print_json(Output { + pub(crate) fn run(self) -> SubcommandResult { + Ok(Box::new(Output { number: self.sat.n(), decimal: self.sat.decimal().to_string(), degree: self.sat.degree().to_string(), @@ -33,8 +33,6 @@ impl Traits { period: self.sat.period(), offset: self.sat.third(), rarity: self.sat.rarity(), - })?; - - Ok(()) + })) } } diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 40b8573c48..376e55631e 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -17,7 +17,7 @@ use { pub mod balance; pub mod cardinals; pub mod create; -pub(crate) mod inscribe; +pub mod inscribe; pub mod inscriptions; pub mod outputs; pub mod receive; @@ -54,7 +54,7 @@ pub(crate) enum Wallet { } impl Wallet { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { Self::Balance => balance::run(options), Self::Create(create) => create.run(options), diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 9813634118..be08751479 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -5,7 +5,7 @@ pub struct Output { pub cardinal: u64, } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -24,7 +24,5 @@ pub(crate) fn run(options: Options) -> Result { } } - print_json(Output { cardinal: balance })?; - - Ok(()) + Ok(Box::new(Output { cardinal: balance })) } diff --git a/src/subcommand/wallet/cardinals.rs b/src/subcommand/wallet/cardinals.rs index 4df85204fd..ccbb187fcb 100644 --- a/src/subcommand/wallet/cardinals.rs +++ b/src/subcommand/wallet/cardinals.rs @@ -1,12 +1,12 @@ use {super::*, crate::wallet::Wallet, std::collections::BTreeSet}; #[derive(Serialize, Deserialize)] -pub struct Cardinal { +pub struct CardinalUtxo { pub output: OutPoint, pub amount: u64, } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -24,15 +24,13 @@ pub(crate) fn run(options: Options) -> Result { if inscribed_utxos.contains(output) { None } else { - Some(Cardinal { + Some(CardinalUtxo { output: *output, amount: amount.to_sat(), }) } }) - .collect::>(); + .collect::>(); - print_json(cardinal_utxos)?; - - Ok(()) + Ok(Box::new(cardinal_utxos)) } diff --git a/src/subcommand/wallet/create.rs b/src/subcommand/wallet/create.rs index dd83894611..d3a98b6e03 100644 --- a/src/subcommand/wallet/create.rs +++ b/src/subcommand/wallet/create.rs @@ -1,10 +1,10 @@ use super::*; -#[derive(Serialize)] -struct Output { - mnemonic: Mnemonic, - passphrase: Option, - message: String, +#[derive(Serialize, Deserialize)] +pub struct Output { + pub mnemonic: Mnemonic, + pub passphrase: Option, + pub message: String, } #[derive(Debug, Parser)] @@ -18,7 +18,7 @@ pub(crate) struct Create { } impl Create { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let mut entropy = [0; 16]; rand::thread_rng().fill_bytes(&mut entropy); @@ -34,12 +34,10 @@ impl Create { supported in Litecoincore!!!! Please make a backup of the \ wallet.dat file and store it in a safe place."; - print_json(Output { + Ok(Box::new(Output { mnemonic, - passphrase: None, + passphrase: Some(self.passphrase), message: warn, - })?; - - Ok(()) + })) } } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 74f802fa51..a75480bed6 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -19,12 +19,12 @@ use { std::collections::BTreeSet, }; -#[derive(Serialize)] -struct Output { - commit: Txid, - inscription: InscriptionId, - reveal: Txid, - fees: u64, +#[derive(Serialize, Deserialize)] +pub struct Output { + pub commit: Txid, + pub inscription: InscriptionId, + pub reveal: Txid, + pub fees: u64, } #[derive(Debug, Parser)] @@ -59,7 +59,7 @@ pub(crate) struct Inscribe { } impl Inscribe { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let inscription = Inscription::from_file(options.chain(), &self.file)?; let index = Index::open(&options)?; @@ -110,12 +110,15 @@ impl Inscribe { Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); if self.dry_run { - print_json(Output { + Ok(Box::new(Output { commit: unsigned_commit_tx.txid(), reveal: reveal_tx.txid(), - inscription: reveal_tx.txid().into(), + inscription: InscriptionId { + txid: reveal_tx.txid(), + index: 0, + }, fees, - })?; + })) } else { // Litecoin does not support this functionality // if !self.no_backup { @@ -134,15 +137,16 @@ impl Inscribe { .send_raw_transaction(&reveal_tx) .context("Failed to send reveal transaction")?; - print_json(Output { + Ok(Box::new(Output { commit, reveal, - inscription: reveal.into(), + inscription: InscriptionId { + txid: reveal, + index: 0, + }, fees, - })?; - }; - - Ok(()) + })) + } } fn calculate_fee(tx: &Transaction, utxos: &BTreeMap) -> u64 { diff --git a/src/subcommand/wallet/inscriptions.rs b/src/subcommand/wallet/inscriptions.rs index 1d64a48c37..bc97a63734 100644 --- a/src/subcommand/wallet/inscriptions.rs +++ b/src/subcommand/wallet/inscriptions.rs @@ -8,7 +8,7 @@ pub struct Output { pub postage: u64, } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -35,7 +35,5 @@ pub(crate) fn run(options: Options) -> Result { } } - print_json(&output)?; - - Ok(()) + Ok(Box::new(output)) } diff --git a/src/subcommand/wallet/outputs.rs b/src/subcommand/wallet/outputs.rs index d064cd312f..dee75fe70e 100644 --- a/src/subcommand/wallet/outputs.rs +++ b/src/subcommand/wallet/outputs.rs @@ -6,7 +6,7 @@ pub struct Output { pub amount: u64, } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -18,7 +18,5 @@ pub(crate) fn run(options: Options) -> Result { }); } - print_json(outputs)?; - - Ok(()) + Ok(Box::new(outputs)) } diff --git a/src/subcommand/wallet/receive.rs b/src/subcommand/wallet/receive.rs index 8a04f6b539..69d6cf5816 100644 --- a/src/subcommand/wallet/receive.rs +++ b/src/subcommand/wallet/receive.rs @@ -5,12 +5,10 @@ pub struct Output { pub address: Address, } -pub(crate) fn run(options: Options) -> Result { +pub(crate) fn run(options: Options) -> SubcommandResult { let address = options .bitcoin_rpc_client_for_wallet_command(false)? .get_new_address(None, Some(bitcoincore_rpc::json::AddressType::Bech32))?; - print_json(Output { address })?; - - Ok(()) + Ok(Box::new(Output { address })) } diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index c0839debee..578e22c7a4 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -13,14 +13,13 @@ pub(crate) struct Restore { } impl Restore { - pub(crate) fn run(self, _options: Options) -> Result { + pub(crate) fn run(self, _options: Options) -> SubcommandResult { bail!( "Descriptor wallets are not supported in Litecoincore 21.2.1, copy your wallet.dat into \ your Litecoincore data directory." ); // initialize_wallet(&options, self.mnemonic.to_seed(self.passphrase))?; - // - // Ok(()) + // Ok(Box::new(Empty {})) } } diff --git a/src/subcommand/wallet/sats.rs b/src/subcommand/wallet/sats.rs index a14b54a42e..7ddf829519 100644 --- a/src/subcommand/wallet/sats.rs +++ b/src/subcommand/wallet/sats.rs @@ -24,7 +24,7 @@ pub struct OutputRare { } impl Sats { - pub(crate) fn run(&self, options: Options) -> Result { + pub(crate) fn run(&self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; index.update()?; @@ -42,7 +42,7 @@ impl Sats { output: outpoint, }); } - print_json(output)?; + Ok(Box::new(output)) } else { let mut output = Vec::new(); for (outpoint, sat, offset, rarity) in rare_sats(utxos) { @@ -53,10 +53,8 @@ impl Sats { rarity, }); } - print_json(output)?; + Ok(Box::new(output)) } - - Ok(()) } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index ac8faf52c4..8aa015a297 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -19,7 +19,7 @@ pub struct Output { } impl Send { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let address = self .address .clone() @@ -49,8 +49,7 @@ impl Send { Outgoing::Amount(amount) => { Self::lock_inscriptions(&client, inscriptions, unspent_outputs)?; let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?; - print_json(Output { transaction: txid })?; - return Ok(()); + return Ok(Box::new(Output { transaction: txid })); } }; @@ -82,9 +81,7 @@ impl Send { let txid = client.send_raw_transaction(&signed_tx)?; - println!("{txid}"); - - Ok(()) + Ok(Box::new(Output { transaction: txid })) } fn lock_inscriptions( diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 77003bc53a..87a12e624a 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -182,9 +182,18 @@ impl TransactionBuilder { } fn select_outgoing(mut self) -> Result { - for (inscribed_satpoint, inscription_id) in &self.inscriptions { + let dust_limit = self + .unused_change_addresses + .last() + .unwrap() + .script_pubkey() + .dust_value() + .to_sat(); + + for (inscribed_satpoint, inscription_id) 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::UtxoContainsAdditionalInscription { outgoing_satpoint: self.outgoing, diff --git a/src/subcommand/wallet/transactions.rs b/src/subcommand/wallet/transactions.rs index b0bacef8a7..942315be53 100644 --- a/src/subcommand/wallet/transactions.rs +++ b/src/subcommand/wallet/transactions.rs @@ -13,7 +13,7 @@ pub struct Output { } impl Transactions { - pub(crate) fn run(self, options: Options) -> Result { + pub(crate) fn run(self, options: Options) -> SubcommandResult { let mut output = Vec::new(); for tx in options .bitcoin_rpc_client_for_wallet_command(false)? @@ -34,8 +34,6 @@ impl Transactions { }); } - print_json(output)?; - - Ok(()) + Ok(Box::new(output)) } } diff --git a/src/templates.rs b/src/templates.rs index 259ad50370..7df7f4442a 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -8,11 +8,12 @@ pub(crate) use { input::InputHtml, inscription::{InscriptionHtml, InscriptionJson}, inscriptions::{InscriptionsHtml, InscriptionsJson}, + inscriptions_block::InscriptionsBlockHtml, output::{OutputHtml, OutputJson}, page_config::PageConfig, preview::{ - PreviewAudioHtml, PreviewImageHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, - PreviewVideoHtml, + PreviewAudioHtml, PreviewImageHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, + PreviewUnknownHtml, PreviewVideoHtml, }, range::RangeHtml, rare::RareTxt, @@ -27,6 +28,7 @@ mod iframe; mod input; pub mod inscription; pub mod inscriptions; +mod inscriptions_block; pub mod output; mod preview; mod range; @@ -137,7 +139,7 @@ mod tests { rare.txt
- +
diff --git a/src/templates/block.rs b/src/templates/block.rs index c499eb70b1..e4dd29d3a2 100644 --- a/src/templates/block.rs +++ b/src/templates/block.rs @@ -7,16 +7,26 @@ pub(crate) struct BlockHtml { best_height: Height, block: Block, height: Height, + total_num_inscriptions: usize, + featured_inscriptions: Vec, } impl BlockHtml { - pub(crate) fn new(block: Block, height: Height, best_height: Height) -> Self { + pub(crate) fn new( + block: Block, + height: Height, + best_height: Height, + total_num_inscriptions: usize, + featured_inscriptions: Vec, + ) -> Self { Self { hash: block.header.block_hash(), target: BlockHash::from_raw_hash(Hash::from_byte_array(block.header.target().to_be_bytes())), block, height, best_height, + total_num_inscriptions, + featured_inscriptions, } } } @@ -34,7 +44,13 @@ mod tests { #[test] fn html() { assert_regex_match!( - BlockHtml::new(Chain::Mainnet.genesis_block(), Height(0), Height(0)), + BlockHtml::new( + Chain::Mainnet.genesis_block(), + Height(0), + Height(0), + 0, + Vec::new() + ), "

Block 0

@@ -48,6 +64,9 @@ mod tests { prev next .* +

0 Inscriptions

+
+

1 Transaction