From 1640a9c4b4d8ea1bc03828769713cd95e19f7077 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 9 Nov 2023 22:19:42 +0000 Subject: [PATCH 001/109] Merging my changes from 0.10.x to 0.11.x --- .github/workflows/release.yaml | 5 +- CHANGELOG.md | 84 ++++ src/index.rs | 131 +++++- src/index/updater.rs | 7 + src/index/updater/inscription_updater.rs | 6 + src/options.rs | 6 + src/subcommand.rs | 8 + src/subcommand/children.rs | 16 + src/subcommand/preview.rs | 3 + src/subcommand/server.rs | 495 ++++++++++++++++++++++- src/subcommand/transfer.rs | 45 +++ src/subcommand/wallet.rs | 26 +- src/subcommand/wallet/create.rs | 4 +- src/subcommand/wallet/inscribe.rs | 22 +- src/subcommand/wallet/restore.rs | 4 +- src/subcommand/wallet/send.rs | 28 +- 16 files changed, 875 insertions(+), 15 deletions(-) create mode 100644 src/subcommand/children.rs create mode 100644 src/subcommand/transfer.rs diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 374d8f0b1b..59936fa6f0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,9 @@ defaults: run: shell: bash +permissions: + contents: write + jobs: release: strategy: @@ -54,7 +57,7 @@ jobs: - name: Release Type id: release-type run: | - if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+[.][0-9]+[.][0-9]+$ ]]; then + if [[ ${{ github.ref }} =~ ^refs/tags/[0-9]+[.][0-9]+[.][0-9]+(-gms?[0-9]+)?$ ]]; then echo ::set-output name=value::release else echo ::set-output name=value::prerelease diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4c692aeb..3156d7a5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,18 @@ Changelog - Ignore non push opcodes in runestones (#2553) - Improve rune minimum at height (#2546) +[0.10.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.10.0-gm2) - 2023-11-03 +---------------------------------------------------------------------------------- + +### Added +- Add `--address-type` flag to `wallet create` and `wallet restore`. + +[0.10.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.10.0-gm1) - 2023-10-25 +---------------------------------------------------------------------------------- + +### Added +- Merged my changes from 0.9.x to 0.10.x. + [0.10.0](https://github.com/ordinals/ord/releases/tag/0.10.0) - 2023-10-23 -------------------------------------------------------------------------- @@ -115,6 +127,78 @@ Changelog - Format rune supply using divisibility (#2509) - Add pre-alpha unstable incomplete half-baked rune index (#2491) +[0.9.0-gm5](https://github.com/ordinals/ord/releases/tag/0.9.0-gm5) - 2023-10-21 +-------------------------------------------------------------------------------- + +### Added + +- Add `/outputs` endpoint to fetch details for multiple outputs per request. + +[0.9.0-gm4](https://github.com/ordinals/ord/releases/tag/0.9.0-gm4) - 2023-10-18 +-------------------------------------------------------------------------------- + +### Added + +- Add `/transfers//` and `/transfers///` endpoints to allow pagination. + +[0.9.0-gm3](https://github.com/ordinals/ord/releases/tag/0.9.0-gm3) - 2023-10-10 +-------------------------------------------------------------------------------- + +### Changed + +- Modify the /ranges endpoint to group the ranges by output. + +[0.9.0-gm2](https://github.com/ordinals/ord/releases/tag/0.9.0-gm2) - 2023-10-10 +-------------------------------------------------------------------------------- + +### Changed + +- Fix github releases. + +[0.9.0-gm1](https://github.com/ordinals/ord/releases/tag/0.9.0-gm1) - 2023-10-10 +-------------------------------------------------------------------------------- + +### Added + +- Add `--ignore-descriptors` flag to allow ord to work with non-ord wallets. + +[0.9.0-gms4](https://github.com/ordinals/ord/releases/tag/0.9.0-gms4) - 2023-09-18 +---------------------------------------------------------------------------------- + +### Added + +- Speed up `/transfers/` endpoint and don't block while running it. +- Add `application/cbor` media type with extension `.cbor` (#2446) +- Add --utxo flag to allow the use of unconfirmed outputs. +- Add --coin-control flag to limit which outputs can be spent. +- Add `/ranges` endpoint for looking up the sat ranges for a batch of outputs. + +[0.9.0-gms3](https://github.com/ordinals/ord/releases/tag/0.9.0-gms3) - 2023-09-12 +---------------------------------------------------------------------------------- + +### Added + +- Add subcommand `children` to list all the child/parent pairs + +[0.9.0-gms2](https://github.com/ordinals/ord/releases/tag/0.9.0-gms2) - 2023-09-11 +---------------------------------------------------------------------------------- + +### Added + +- Add `parent` and `children` to `/inscriptions_json/` endpoint + +[0.9.0-gms1](https://github.com/ordinals/ord/releases/tag/0.9.0-gms1) - 2023-09-11 +---------------------------------------------------------------------------------- + +### Added + +- Add `/inscriptions_json/` endpoint +- Add `/transfers/` endpoint +- Add `/stats/` endpoint +- Only index blocks when new blocks exist and the height limit isn't reached +- Add `--no-progress-bar` flag to inhibit the display of the progress bar +- Add server request logging + [0.9.0](https://github.com/ordinals/ord/releases/tag/0.9.0) - 2023-09-11 ------------------------------------------------------------------------ diff --git a/src/index.rs b/src/index.rs index 9ce53cfaca..86b235bc4c 100644 --- a/src/index.rs +++ b/src/index.rs @@ -53,6 +53,7 @@ macro_rules! define_multimap_table { define_multimap_table! { INSCRIPTION_ID_TO_CHILDREN, &InscriptionIdValue, &InscriptionIdValue } define_multimap_table! { SATPOINT_TO_INSCRIPTION_ID, &SatPointValue, &InscriptionIdValue } define_multimap_table! { SAT_TO_INSCRIPTION_ID, u64, &InscriptionIdValue } +define_multimap_table! { HEIGHT_TO_INSCRIPTION_ID, u64, &InscriptionIdValue } define_table! { HEIGHT_TO_BLOCK_HASH, u64, &BlockHashValue } define_table! { HEIGHT_TO_LAST_SEQUENCE_NUMBER, u64, u64 } define_table! { INSCRIPTION_ID_TO_INSCRIPTION_ENTRY, &InscriptionIdValue, InscriptionEntryValue } @@ -160,6 +161,7 @@ pub(crate) struct Index { height_limit: Option, index_runes: bool, index_sats: bool, + no_progress_bar: bool, options: Options, path: PathBuf, unrecoverably_reorged: AtomicBool, @@ -272,6 +274,7 @@ impl Index { tx.open_multimap_table(INSCRIPTION_ID_TO_CHILDREN)?; tx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?; tx.open_multimap_table(SAT_TO_INSCRIPTION_ID)?; + tx.open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)?; tx.open_table(HEIGHT_TO_BLOCK_HASH)?; tx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?; tx.open_table(INSCRIPTION_ID_TO_INSCRIPTION_ENTRY)?; @@ -323,6 +326,7 @@ impl Index { first_inscription_height: options.first_inscription_height(), genesis_block_coinbase_transaction, height_limit: options.height_limit, + no_progress_bar: options.no_progress_bar, options: options.clone(), index_runes, index_sats, @@ -793,7 +797,6 @@ impl Index { self.client.get_block(&hash).into_option() } - #[cfg(test)] pub(crate) fn get_children_by_inscription_id( &self, inscription_id: InscriptionId, @@ -865,6 +868,23 @@ impl Index { ) } + pub(crate) fn get_inscription_ids_by_height(&self, height: u64) -> Result> { + let mut ret = Vec::new(); + for range in self + .database + .begin_read()? + .open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)? + .range::<&u64>(&height..&(height + 1))? + { + let (_, ids) = range?; + for id in ids { + ret.push(Entry::load(*id?.value())); + } + } + + Ok(ret) + } + pub(crate) fn get_inscription_ids_by_sat(&self, sat: Sat) -> Result> { let rtx = &self.database.begin_read()?; @@ -1151,6 +1171,18 @@ impl Index { } } + pub(crate) fn ranges(&self, outpoint: OutPoint) -> Result> { + match self.list_inner(outpoint.store())? { + Some(sat_ranges) => + Ok(sat_ranges + .chunks_exact(11) + .map(|chunk| SatRange::load(chunk.try_into().unwrap())) + .collect(), + ), + None => Err(anyhow!("no ranges")), + } + } + pub(crate) fn block_time(&self, height: Height) -> Result { let height = height.n(); @@ -1336,6 +1368,103 @@ impl Index { ) } + pub(crate) fn delete_transfer_log(&self) -> Result { + let wtx = self.database.begin_write().unwrap(); + wtx.delete_multimap_table(HEIGHT_TO_INSCRIPTION_ID)?; + Ok(wtx.commit()?) + } + + pub(crate) fn trim_transfer_log(&self, height: u64) -> Result { + let wtx = self.begin_write()?; + for pair in self + .database + .begin_read()? + .open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)? + .range(..height)? + { + wtx + .open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)? + .remove_all(pair?.0.value())?; + } + Ok(wtx.commit()?) + } + + pub(crate) fn show_transfer_log_stats(&self) -> Result<(u64, Option, Option)> { + let rtx = self.database.begin_read().unwrap(); + let table = rtx.open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)?; + let mut iter = table.iter()?; + + let rows = table.len()?; + + let first = iter + .next() + .and_then(|result| result.ok()) + .map(|(height, _id)| height.value()); + + let last = iter + .next_back() + .and_then(|result| result.ok()) + .map(|(height, _id)| height.value()); + + if first.is_none() { + Ok((rows, None, None)) + } else if last.is_none() { + Ok((rows, first, first)) + } else { + Ok((rows, first, last)) + } + } + + pub(crate) fn get_children(&self) -> Result<()> { + for range in self + .database + .begin_read()? + .open_multimap_table(INSCRIPTION_ID_TO_CHILDREN)? + .iter()? + { + let (parent, children) = range?; + for child in children { + println!("{} {}", ::load(*parent.value()), ::load(*child?.value())); + } + } + + Ok(()) + } + + pub(crate) fn get_stats(&self) -> Result<(Option, Option, Option)> { + let rtx = self.database.begin_read().unwrap(); + + let height = rtx + .open_table(HEIGHT_TO_BLOCK_HASH)? + .iter()? + .next_back() + .and_then(|result| result.ok()) + .map(|(height, _hash)| height.value()); + + let table = rtx.open_table(INSCRIPTION_NUMBER_TO_INSCRIPTION_ID)?; + let mut iter = table.iter()?; + + let lowest_number = iter + .next() + .and_then(|result| result.ok()) + .map(|(number, _id)| number.value()); + + let highest_number = iter + .next_back() + .and_then(|result| result.ok()) + .map(|(number, _id)| number.value()); + + Ok(( + height, + lowest_number, + if highest_number.is_none() { + lowest_number + } else { + highest_number + }, + )) + } + #[cfg(test)] fn assert_inscription_location( &self, diff --git a/src/index/updater.rs b/src/index/updater.rs index 4affe64fdf..8e9fb85685 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -68,6 +68,7 @@ impl<'index> Updater<'_> { )?; let mut progress_bar = if cfg!(test) + || self.index.no_progress_bar || log_enabled!(log::Level::Info) || starting_height <= self.height || integration_test() @@ -82,6 +83,9 @@ impl<'index> Updater<'_> { Some(progress_bar) }; + if starting_height > self.height + && (self.index.height_limit.is_none() || self.index.height_limit.unwrap() > self.height) + { let rx = Self::fetch_blocks_from(self.index, self.height, self.index.index_sats)?; let (mut outpoint_sender, mut value_receiver) = Self::spawn_fetcher(self.index)?; @@ -152,6 +156,7 @@ impl<'index> Updater<'_> { if let Some(progress_bar) = &mut progress_bar { progress_bar.finish_and_clear(); } + } Ok(()) } @@ -377,6 +382,7 @@ impl<'index> Updater<'_> { } let mut height_to_block_hash = wtx.open_table(HEIGHT_TO_BLOCK_HASH)?; + let mut height_to_inscription_id = wtx.open_multimap_table(HEIGHT_TO_INSCRIPTION_ID)?; let mut height_to_last_sequence_number = wtx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?; let mut inscription_id_to_inscription_entry = wtx.open_table(INSCRIPTION_ID_TO_INSCRIPTION_ENTRY)?; @@ -413,6 +419,7 @@ impl<'index> Updater<'_> { { let mut inscription_updater = InscriptionUpdater::new( self.height, + &mut height_to_inscription_id, &mut inscription_id_to_children, &mut inscription_id_to_satpoint, value_receiver, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index edd0ef74ba..89f8f62732 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -24,6 +24,7 @@ enum Origin { pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { flotsam: Vec, height: u64, + height_to_inscription_id: &'a mut MultimapTable<'db, 'tx, u64, &'static InscriptionIdValue>, id_to_children: &'a mut MultimapTable<'db, 'tx, &'static InscriptionIdValue, &'static InscriptionIdValue>, id_to_satpoint: &'a mut Table<'db, 'tx, &'static InscriptionIdValue, &'static SatPointValue>, @@ -48,6 +49,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { pub(super) fn new( height: u64, + height_to_inscription_id: &'a mut MultimapTable<'db, 'tx, u64, &'static InscriptionIdValue>, id_to_children: &'a mut MultimapTable< 'db, 'tx, @@ -84,6 +86,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { Ok(Self { flotsam: Vec::new(), height, + height_to_inscription_id, id_to_children, id_to_satpoint, value_receiver, @@ -435,6 +438,9 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let inscription_id = flotsam.inscription_id.store(); let unbound = match flotsam.origin { Origin::Old { old_satpoint } => { + self + .height_to_inscription_id + .insert(&self.height, &inscription_id)?; self.satpoint_to_id.remove_all(&old_satpoint.store())?; false diff --git a/src/options.rs b/src/options.rs index 0b5d486457..4f2b0f2a67 100644 --- a/src/options.rs +++ b/src/options.rs @@ -49,6 +49,8 @@ pub(crate) struct Options { pub(crate) index_runes_pre_alpha_i_agree_to_get_rekt: bool, #[arg(long, help = "Track location of all satoshis.")] pub(crate) index_sats: bool, + #[arg(long, help = "Inhibit the display of the progress bar while updating the index.")] + pub(crate) no_progress_bar: bool, #[arg(long, short, help = "Use regtest. Equivalent to `--chain regtest`.")] pub(crate) regtest: bool, #[arg(long, help = "Connect to Bitcoin Core RPC at .")] @@ -59,6 +61,8 @@ pub(crate) struct Options { pub(crate) testnet: bool, #[arg(long, default_value = "ord", help = "Use wallet named .")] pub(crate) wallet: String, + #[arg(long, short, help = "Don't check for standard wallet descriptors.")] + pub(crate) ignore_descriptors: bool, } impl Options { @@ -258,6 +262,7 @@ impl Options { client.load_wallet(&self.wallet)?; } + if !self.ignore_descriptors { let descriptors = client.list_descriptors(None)?.descriptors; let tr = descriptors @@ -273,6 +278,7 @@ impl Options { 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`", self.wallet); } + } } Ok(client) diff --git a/src/subcommand.rs b/src/subcommand.rs index 93e393dcea..5cc88dbacb 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,5 +1,6 @@ use super::*; +pub mod children; pub mod decode; pub mod epochs; pub mod find; @@ -13,10 +14,13 @@ pub mod subsidy; pub mod supply; pub mod teleburn; pub mod traits; +pub mod transfer; pub mod wallet; #[derive(Debug, Parser)] pub(crate) enum Subcommand { + #[command(about = "List all the child inscriptions")] + Children(children::Children), #[command(about = "Decode a transaction")] Decode(decode::Decode), #[command(about = "List the first satoshis of each reward epoch")] @@ -43,6 +47,8 @@ pub(crate) enum Subcommand { Teleburn(teleburn::Teleburn), #[command(about = "Display satoshi traits")] Traits(traits::Traits), + #[command(about = "Modify transfer log table")] + Transfer(transfer::Transfer), #[command(subcommand, about = "Wallet commands")] Wallet(wallet::Wallet), } @@ -50,6 +56,7 @@ pub(crate) enum Subcommand { impl Subcommand { pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { + Self::Children(children) => children.run(options), Self::Decode(decode) => decode.run(), Self::Epochs => epochs::run(), Self::Find(find) => find.run(options), @@ -68,6 +75,7 @@ impl Subcommand { Self::Supply => supply::run(), Self::Teleburn(teleburn) => teleburn.run(), Self::Traits(traits) => traits.run(), + Self::Transfer(transfer) => transfer.run(options), Self::Wallet(wallet) => wallet.run(options), } } diff --git a/src/subcommand/children.rs b/src/subcommand/children.rs new file mode 100644 index 0000000000..dc9f355ddf --- /dev/null +++ b/src/subcommand/children.rs @@ -0,0 +1,16 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(crate) struct Children { +} + +impl Children { + pub(crate) fn run(self, options: Options) -> SubcommandResult { + let index = Index::open(&options)?; + index.update()?; + + index.get_children()?; + + Ok(Box::new(Empty {})) + } +} diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 657aaee7df..c4fb887fbb 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -63,6 +63,7 @@ impl Preview { super::wallet::Wallet::Create(super::wallet::create::Create { passphrase: "".into(), + address_type: super::wallet::AddressType::Bech32m, }) .run(options.clone())?; @@ -81,6 +82,8 @@ impl Preview { super::wallet::inscribe::Inscribe { batch: None, cbor_metadata: None, + utxo: Vec::new(), + coin_control: false, commit_fee_rate: None, destination: None, dry_run: false, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 83f80b6d7a..c555102ed2 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -22,7 +22,7 @@ use { headers::UserAgent, http::{header, HeaderMap, HeaderValue, StatusCode, Uri}, response::{IntoResponse, Redirect, Response}, - routing::get, + routing::{get, post}, Router, TypedHeader, }, axum_server::Handle, @@ -33,7 +33,8 @@ use { caches::DirCache, AcmeConfig, }, - std::{cmp::Ordering, str, sync::Arc}, + std::{cmp::Ordering, collections::HashMap, str, sync::Arc}, + tokio::time::sleep, tokio_stream::StreamExt, tower_http::{ compression::CompressionLayer, @@ -50,6 +51,18 @@ pub struct ServerConfig { pub is_json_api_enabled: bool, } +#[derive(Serialize)] +pub struct Outputs { + pub output: OutPoint, + pub details: OutputJson, +} + +#[derive(Serialize)] +pub struct Ranges { + pub output: OutPoint, + pub ranges: Vec<(u64, u64)>, +} + enum InscriptionQuery { Id(InscriptionId), Number(i64), @@ -95,6 +108,49 @@ struct Search { query: String, } +#[derive(Serialize)] +struct MyInscriptionJson { + number: i64, + id: InscriptionId, + parent: Option, + address: Option, + output_value: Option, + sat: Option, + content_length: Option, + content_type: String, + timestamp: u32, + genesis_height: u64, + genesis_fee: u64, + genesis_transaction: Txid, + location: String, + output: String, + offset: u64, + children: Vec, +} + +#[derive(Serialize)] +struct SatoshiJson { + number: u64, + decimal: String, + degree: String, + percentile: String, + name: String, + cycle: u64, + epoch: u64, + period: u64, + block: u64, + offset: u64, + rarity: Rarity, + // timestamp: i64, +} + +#[derive(Serialize)] +struct StatsJson { + highest_block_indexed: Option, + lowest_inscription_number: Option, + highest_inscription_number: Option, +} + #[derive(RustEmbed)] #[folder = "static"] struct StaticAssets; @@ -215,9 +271,18 @@ impl Server { "/inscriptions/block/:height/:page", get(Self::inscriptions_in_block_from_page), ) + .route( + "/inscriptions_json/:start", + get(Self::inscriptions_json_start), + ) + .route( + "/inscriptions_json/:start/:end", + get(Self::inscriptions_json_start_end), + ) .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("/preview/:inscription_id", get(Self::preview)) .route("/r/blockhash", get(Self::block_hash_json)) .route( @@ -228,6 +293,7 @@ impl Server { .route("/r/blocktime", get(Self::block_time)) .route("/r/metadata/:inscription_id", get(Self::metadata)) .route("/range/:start/:end", get(Self::range)) + .route("/ranges", post(Self::ranges)) .route("/rare.txt", get(Self::rare_txt)) .route("/rune/:rune", get(Self::rune)) .route("/runes", get(Self::runes)) @@ -235,7 +301,11 @@ impl Server { .route("/search", get(Self::search_by_query)) .route("/search/*query", get(Self::search_by_path)) .route("/static/*path", get(Self::static_asset)) + .route("/stats", get(Self::stats)) .route("/status", get(Self::status)) + .route("/transfers/:height", get(Self::inscriptionids_from_height)) + .route("/transfers/:height/:start", get(Self::inscriptionids_from_height_start)) + .route("/transfers/:height/:start/:end", get(Self::inscriptionids_from_height_start_end)) .route("/tx/:txid", get(Self::transaction)) .layer(Extension(index)) .layer(Extension(page_config)) @@ -428,6 +498,7 @@ impl Server { } async fn clock(Extension(index): Extension>) -> ServerResult { + log::info!("GET /clock"); Ok( ( [( @@ -446,6 +517,7 @@ impl Server { Path(DeserializeFromStr(sat)): Path>, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /sat/{sat}"); 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| { @@ -487,6 +559,7 @@ impl Server { } async fn ordinal(Path(sat): Path) -> Redirect { + log::info!("GET /ordinal/{sat}"); Redirect::to(&format!("/sat/{sat}")) } @@ -496,6 +569,7 @@ impl Server { Path(outpoint): Path, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /output/{outpoint}"); let list = index.list(outpoint)?; let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { @@ -552,6 +626,80 @@ impl Server { }) } + async fn outputs( + Extension(page_config): Extension>, + Extension(index): Extension>, + Json(data): Json + ) -> ServerResult { + log::info!("POST /outputs"); + + if !data.is_array() { + return Err(ServerError::BadRequest("expected array".to_string())); + } + + let mut result = Vec::new(); + + for outpoint in data.as_array().unwrap() { + if !outpoint.is_string() { + return Err(ServerError::BadRequest("expected array of strings".to_string())); + } + + match OutPoint::from_str(outpoint.as_str().unwrap()) { + Ok(outpoint) => { + sleep(Duration::from_millis(0)).await; + + let list = index.list(outpoint)?; + + let output = if outpoint == OutPoint::null() || outpoint == unbound_outpoint() { + let mut value = 0; + + if let Some(List::Unspent(ranges)) = &list { + for (start, end) in ranges { + value += end - start; + } + } + + TxOut { + value, + script_pubkey: ScriptBuf::new(), + } + } else { + index + .get_transaction(outpoint.txid)? + .ok_or_not_found(|| format!("output {outpoint}"))? + .output + .into_iter() + .nth(outpoint.vout as usize) + .ok_or_not_found(|| format!("output {outpoint}"))? + }; + + let inscriptions = index.get_inscriptions_on_output(outpoint)?; + + let runes = index.get_rune_balances_for_outpoint(outpoint)?; + + result.push( + Outputs {output: outpoint, details: + OutputJson::new( + outpoint, + list, + page_config.chain, + output, + inscriptions, + runes + .into_iter() + .map(|(rune, pile)| (rune, pile.amount)) + .collect(), + ) + } + ) + } + _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), + } + } + + Ok(Json(result).into_response()) + } + async fn range( Extension(page_config): Extension>, Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<( @@ -559,6 +707,7 @@ impl Server { DeserializeFromStr, )>, ) -> ServerResult> { + log::info!("GET /range/{start}/{end}"); match start.cmp(&end) { Ordering::Equal => Err(ServerError::BadRequest("empty range".to_string())), Ordering::Greater => Err(ServerError::BadRequest( @@ -568,7 +717,57 @@ impl Server { } } + async fn ranges( + Extension(index): Extension>, + Json(data): Json + ) -> ServerResult { + log::info!("POST /ranges"); + + if !index.has_sat_index() { + return Err(ServerError::BadRequest("the /ranges endpoint needs the server to have a sat index".to_string())); + } + + if !data.is_array() { + return Err(ServerError::BadRequest("expected array".to_string())); + } + + let mut result = Vec::new(); + let mut range_count = 0; + let mut outpoint_count = 0; + let start_time = Instant::now(); + + for outpoint in data.as_array().unwrap() { + if start_time.elapsed() > Duration::from_secs(5) { + return Err(ServerError::BadRequest("request timed out".to_string())); + } + + if !outpoint.is_string() { + return Err(ServerError::BadRequest("expected array of strings".to_string())); + } + + match OutPoint::from_str(outpoint.as_str().unwrap()) { + Ok(outpoint) => { + sleep(Duration::from_millis(0)).await; + match index.ranges(outpoint) { + Ok(ranges) => { + range_count += ranges.len(); + outpoint_count += 1; + result.push(Ranges {output: outpoint, ranges}); + } + _ => println!("no ranges for {}", outpoint), + } + } + _ => return Err(ServerError::BadRequest(format!("expected array of OutPoint strings ({} is bad)", outpoint))), + } + } + + println!(" {} ranges from {} outputs in {:?}", range_count, outpoint_count, start_time.elapsed()); + + Ok(Json(result).into_response()) + } + async fn rare_txt(Extension(index): Extension>) -> ServerResult { + log::info!("GET /rare.txt"); Ok(RareTxt(index.rare_sat_satpoints()?)) } @@ -609,6 +808,7 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, ) -> ServerResult> { + log::info!("GET /"); let blocks = index.blocks(100)?; let mut featured_blocks = BTreeMap::new(); for (height, hash) in blocks.iter().take(5) { @@ -622,6 +822,7 @@ impl Server { } async fn install_script() -> Redirect { + log::info!("GET /install.sh"); Redirect::to("https://raw.githubusercontent.com/ordinals/ord/master/install.sh") } @@ -633,6 +834,7 @@ impl Server { ) -> ServerResult { let (block, height) = match query { BlockQuery::Height(height) => { + log::info!("GET /block/{height}/"); let block = index .get_block_by_height(height)? .ok_or_not_found(|| format!("block {height}"))?; @@ -640,6 +842,7 @@ impl Server { (block, height) } BlockQuery::Hash(hash) => { + log::info!("GET /block/{hash}/"); let info = index .block_header_info(hash)? .ok_or_not_found(|| format!("block {hash}"))?; @@ -676,11 +879,104 @@ impl Server { }) } + async fn inscriptionids_from_height( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(height): Path, + ) -> ServerResult { + log::info!("GET /transfers/{height}"); + Self::inscriptionids_from_height_inner(page_config, index.clone(), index.get_inscription_ids_by_height(height)?).await + } + + async fn inscriptionids_from_height_start( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(path): Path<(u64, usize)>, + ) -> ServerResult { + let height = path.0; + let start = path.1; + log::info!("GET /transfers/{height}/{start}"); + + let inscription_ids = index.get_inscription_ids_by_height(height)?; + let end = inscription_ids.len(); + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + Self::inscriptionids_from_height_inner(page_config, index.clone(), inscription_ids[start..end].to_vec()).await + } + } + } + + async fn inscriptionids_from_height_start_end( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(path): Path<(u64, usize, usize)>, + ) -> ServerResult { + let height = path.0; + let start = path.1; + let mut end = path.2; + log::info!("GET /transfers/{height}/{start}/{end}"); + + let inscription_ids = index.get_inscription_ids_by_height(height)?; + end = usize::min(end, inscription_ids.len()); + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + Self::inscriptionids_from_height_inner(page_config, index.clone(), inscription_ids[start..end].to_vec()).await + } + } + } + + async fn inscriptionids_from_height_inner( + page_config: Arc, + index: Arc, + inscription_ids: Vec, + ) -> ServerResult { + let mut ret = String::from(""); + let mut tx_cache = HashMap::new(); + for inscription_id in inscription_ids { + sleep(Duration::from_millis(0)).await; + let satpoint = index + .get_inscription_satpoint_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + let address = if satpoint.outpoint == unbound_outpoint() { + String::from("unbound") + } else { + if !tx_cache.contains_key(&satpoint.outpoint.txid) { + tx_cache.insert(satpoint.outpoint.txid, + index + .get_transaction(satpoint.outpoint.txid)? + .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))?); + } + + let output = tx_cache.get(&satpoint.outpoint.txid).unwrap().clone() + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .ok_or_not_found(|| format!("inscription {inscription_id} current transaction output"))?; + if let Ok(address) = page_config.chain.address_from_script(&output.script_pubkey) { + address.to_string() + } else { + String::from("error") + } + }; + + ret += &format!("{} {}\n", inscription_id, address); + } + + Ok(ret) + } + async fn transaction( Extension(page_config): Extension>, Extension(index): Extension>, Path(txid): Path, ) -> ServerResult> { + log::info!("GET /tx/{txid}"); let inscription = index.get_inscription_by_id(InscriptionId { txid, index: 0 })?; let blockhash = index.get_transaction_blockhash(txid)?; @@ -712,7 +1008,22 @@ impl Server { Ok(Json(hex::encode(metadata))) } + async fn stats(Extension(index): Extension>) -> ServerResult { + log::info!("GET /stats"); + let stats = index.get_stats()?; + Ok( + serde_json::to_string_pretty(&StatsJson { + highest_block_indexed: stats.0, + lowest_inscription_number: stats.1, + highest_inscription_number: stats.2, + }) + .ok() + .unwrap(), + ) + } + async fn status(Extension(index): Extension>) -> (StatusCode, &'static str) { + log::info!("GET /status"); if index.is_unrecoverably_reorged() { ( StatusCode::OK, @@ -730,6 +1041,7 @@ impl Server { Extension(index): Extension>, Query(search): Query, ) -> ServerResult { + log::info!("GET /search"); Self::search(&index, &search.query).await } @@ -737,6 +1049,7 @@ impl Server { Extension(index): Extension>, Path(search): Path, ) -> ServerResult { + log::info!("GET /search/{}", search.query); Self::search(&index, &search.query).await } @@ -781,6 +1094,7 @@ impl Server { } async fn favicon(user_agent: Option>) -> ServerResult { + log::info!("GET /favicon.ico"); if user_agent .map(|user_agent| { user_agent.as_str().contains("Safari/") @@ -812,6 +1126,7 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, ) -> ServerResult { + log::info!("GET /feed.xml"); let mut builder = rss::ChannelBuilder::default(); let chain = page_config.chain; @@ -852,8 +1167,10 @@ impl Server { async fn static_asset(Path(path): Path) -> ServerResult { let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') { + log::info!("GET /static/{stripped}"); stripped } else { + log::info!("GET /static/{path}"); &path }) .ok_or_not_found(|| format!("asset {path}"))?; @@ -868,10 +1185,12 @@ impl Server { } async fn block_count(Extension(index): Extension>) -> ServerResult { + log::info!("GET /blockcount"); Ok(index.block_count()?.to_string()) } async fn block_height(Extension(index): Extension>) -> ServerResult { + log::info!("GET /blockheight"); Ok( index .block_height()? @@ -881,6 +1200,7 @@ impl Server { } async fn block_hash(Extension(index): Extension>) -> ServerResult { + log::info!("GET /blockhash"); Ok( index .block_hash(None)? @@ -902,6 +1222,7 @@ impl Server { Extension(index): Extension>, Path(height): Path, ) -> ServerResult { + log::info!("GET /blockhash/{height}"); Ok( index .block_hash(Some(height))? @@ -923,6 +1244,7 @@ impl Server { } async fn block_time(Extension(index): Extension>) -> ServerResult { + log::info!("GET /blocktime"); Ok( index .block_time(index.block_height()?.ok_or_not_found(|| "blocktime")?)? @@ -936,6 +1258,7 @@ impl Server { Extension(index): Extension>, Path(path): Path<(u64, usize, usize)>, ) -> Result, ServerError> { + log::info!("GET /input/{}/{}/{}", path.0, path.1, path.2); let not_found = || format!("input /{}/{}/{}", path.0, path.1, path.2); let block = index @@ -958,10 +1281,12 @@ impl Server { } async fn faq() -> Redirect { + log::info!("GET /faq"); Redirect::to("https://docs.ordinals.com/faq/") } async fn bounties() -> Redirect { + log::info!("GET /bounties"); Redirect::to("https://docs.ordinals.com/bounty/") } @@ -970,6 +1295,7 @@ impl Server { Extension(config): Extension>, Path(inscription_id): Path, ) -> ServerResult { + log::info!("GET /content/{inscription_id}"); if config.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); } @@ -1016,6 +1342,7 @@ impl Server { Extension(config): Extension>, Path(inscription_id): Path, ) -> ServerResult { + log::info!("GET /preview/{inscription_id}"); if config.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); } @@ -1105,10 +1432,16 @@ impl Server { accept_json: AcceptJson, ) -> ServerResult { let inscription_id = match query { - InscriptionQuery::Id(id) => id, - InscriptionQuery::Number(inscription_number) => index - .get_inscription_id_by_inscription_number(inscription_number)? - .ok_or_not_found(|| format!("{inscription_number}"))?, + InscriptionQuery::Id(id) => { + log::info!("GET /inscription/{id}"); + id + }, + InscriptionQuery::Number(inscription_number) => { + log::info!("GET /inscription/{inscription_number}"); + index + .get_inscription_id_by_inscription_number(inscription_number)? + .ok_or_not_found(|| format!("{inscription_number}"))? + }, }; let entry = index @@ -1240,6 +1573,7 @@ impl Server { Extension(index): Extension>, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /inscriptions"); Self::inscriptions_inner(page_config, index, None, 100, accept_json).await } @@ -1249,6 +1583,7 @@ impl Server { Path(block_height): Path, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /inscriptions/block/{block_height}"); Self::inscriptions_in_block_from_page( Extension(page_config), Extension(index), @@ -1264,6 +1599,7 @@ impl Server { Path((block_height, page)): Path<(u64, usize)>, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /inscriptions/block/{block_height}/{page}"); let inscriptions = index.get_inscriptions_in_block(block_height)?; Ok(if accept_json.0 { @@ -1286,6 +1622,7 @@ impl Server { Path(from): Path, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /inscriptions/{from}"); Self::inscriptions_inner(page_config, index, Some(from), 100, accept_json).await } @@ -1295,6 +1632,7 @@ impl Server { Path((from, n)): Path<(u64, usize)>, accept_json: AcceptJson, ) -> ServerResult { + log::info!("GET /inscriptions/{from}/{n}"); Self::inscriptions_inner(page_config, index, Some(from), n, accept_json).await } @@ -1327,6 +1665,151 @@ impl Server { }) } + async fn inscriptions_json_start( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(start): Path, + ) -> ServerResult { + log::info!("GET /inscriptions_json/{start}"); + Self::inscriptions_json(page_config, index, start, start + 1).await + } + + async fn inscriptions_json_start_end( + Extension(page_config): Extension>, + Extension(index): Extension>, + Path(path): Path<(i64, i64)>, + ) -> ServerResult { + log::info!("GET /inscriptions_json/{}/{}", path.0, path.1); + Self::inscriptions_json(page_config, index, path.0, path.1).await + } + + async fn inscriptions_json( + page_config: Arc, + index: Arc, + start: i64, + end: i64, + ) -> ServerResult { + const MAX_JSON_INSCRIPTIONS: i64 = 1000; + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + if end - start > MAX_JSON_INSCRIPTIONS { + return Err(ServerError::BadRequest(format!( + "range length > {MAX_JSON_INSCRIPTIONS}" + ))); + } + + let mut ret = Vec::new(); + + for i in start..end { + sleep(Duration::from_millis(0)).await; + match index.get_inscription_id_by_inscription_number(i) { + Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + Ok(inscription_id) => match inscription_id { + Some(inscription_id) => { + let entry = index + .get_inscription_entry(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let tx = index.get_transaction(inscription_id.txid)?.unwrap(); + let inscription = index + .get_inscription_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let satpoint = index + .get_inscription_satpoint_by_id(inscription_id)? + .ok_or_not_found(|| format!("inscription {inscription_id}"))?; + + let output = if satpoint.outpoint == unbound_outpoint() { + None + } else { + Some( + if satpoint.outpoint.txid == inscription_id.txid { + tx + } else { + 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 mut address = None; + if let Some(output) = &output { + if let Ok(a) = page_config.chain.address_from_script(&output.script_pubkey) { + address = Some(a.to_string()); + } + } + + let sat = entry.sat.map(|s| SatoshiJson { + number: s.n(), + decimal: s.decimal().to_string(), + degree: s.degree().to_string(), + percentile: s.percentile().to_string(), + name: s.name(), + cycle: s.cycle(), + epoch: s.epoch().0, + period: s.period(), + block: s.height().0, + offset: s.third(), + rarity: s.rarity(), + // timestamp: index.block_time(s.height())?.unix_timestamp(), + }); + + let content_type = inscription.content_type(); + let unbound_suffix = if satpoint.outpoint == unbound_outpoint() { + " (unbound)" + } else { + "" + }; + + ret.push(MyInscriptionJson { + number: i, + id: inscription_id, + parent: entry.parent, + address, + output_value: if output.is_some() { + Some(output.unwrap().value) + } else { + None + }, + sat, + content_length: inscription.content_length(), + content_type: if content_type.is_some() { + content_type.unwrap().to_string() + } else { + "".to_string() + }, + timestamp: entry.timestamp, + genesis_height: entry.height, + genesis_fee: entry.fee, + genesis_transaction: inscription_id.txid, + location: satpoint.to_string() + unbound_suffix, + output: satpoint.outpoint.to_string() + unbound_suffix, + offset: satpoint.offset, + children: index.get_children_by_inscription_id(inscription_id)?, + }); + } + None => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + }, + } + } + + Ok(serde_json::to_string_pretty(&ret).ok().unwrap()) + } + } + } + async fn redirect_http_to_https( Extension(mut destination): Extension, uri: Uri, diff --git a/src/subcommand/transfer.rs b/src/subcommand/transfer.rs new file mode 100644 index 0000000000..7d7adf10e7 --- /dev/null +++ b/src/subcommand/transfer.rs @@ -0,0 +1,45 @@ +use super::*; + +#[derive(Debug, Parser)] +pub(crate) struct Transfer { + #[clap(long, help = "Delete the whole transfer log table.")] + delete: bool, + #[clap(long, help = "Delete transfer logs for blocks before height .")] + trim: Option, +} + +impl Transfer { + pub(crate) fn run(self, options: Options) -> SubcommandResult { + let index = Index::open(&options)?; + index.update()?; + + if self.delete && self.trim.is_some() { + return Err(anyhow!("Cannot use both --delete and --trim")); + } + + if self.delete { + println!("deleting transfer log table"); + index.delete_transfer_log()?; + return Ok(Box::new(Empty {})); + } + + if self.trim.is_some() { + let trim = self.trim.unwrap(); + println!("deleting transfer logs for blocks before {trim}"); + index.trim_transfer_log(trim)?; + } + + let (rows, first_key, last_key) = index.show_transfer_log_stats()?; + if rows == 0 { + println!("the transfer table has {rows} rows"); + } else { + println!( + "the transfer table has {rows} rows from height {} to height {}", + first_key.unwrap(), + last_key.unwrap() + ); + } + + Ok(Box::new(Empty {})) + } +} diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index a526ac7b05..7d4d1c3e1a 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -53,6 +53,14 @@ pub(crate) enum Wallet { Cardinals, } +#[derive(clap::ValueEnum, Clone, Debug)] +pub(crate) enum AddressType { + Legacy, + P2SHSegwit, + Bech32, + Bech32m, +} + impl Wallet { pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { @@ -80,7 +88,7 @@ fn get_change_address(client: &Client, chain: Chain) -> Result
{ ) } -pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64]) -> Result { +pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64], address_type: AddressType) -> Result { let client = options.bitcoin_rpc_client_for_wallet_command(true)?; let network = options.chain().network(); @@ -93,7 +101,12 @@ pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64]) -> Result { let fingerprint = master_private_key.fingerprint(&secp); let derivation_path = DerivationPath::master() - .child(ChildNumber::Hardened { index: 86 }) + .child(ChildNumber::Hardened { index: match address_type { + AddressType::Legacy => 44, + AddressType::P2SHSegwit => 49, + AddressType::Bech32 => 84, + AddressType::Bech32m => 86, + } }) .child(ChildNumber::Hardened { index: u32::from(network != Network::Bitcoin), }) @@ -108,6 +121,7 @@ pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64]) -> Result { (fingerprint, derivation_path.clone()), derived_private_key, change, + &address_type, )?; } @@ -120,6 +134,7 @@ fn derive_and_import_descriptor( origin: (Fingerprint, DerivationPath), derived_private_key: ExtendedPrivKey, change: bool, + address_type: &AddressType, ) -> Result { let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey { origin: Some(origin), @@ -135,7 +150,12 @@ fn derive_and_import_descriptor( let mut key_map = std::collections::HashMap::new(); key_map.insert(public_key.clone(), secret_key); - let desc = Descriptor::new_tr(public_key, None)?; + let desc = match address_type { + AddressType::Legacy => Descriptor::new_pkh(public_key), + AddressType::P2SHSegwit => Descriptor::new_sh_wpkh(public_key), + AddressType::Bech32 => Descriptor::new_wpkh(public_key), + AddressType::Bech32m => Descriptor::new_tr(public_key, None), + }?; client.import_descriptors(ImportDescriptors { descriptor: desc.to_string_with_secret(&key_map), diff --git a/src/subcommand/wallet/create.rs b/src/subcommand/wallet/create.rs index 34cd762450..a95b79015f 100644 --- a/src/subcommand/wallet/create.rs +++ b/src/subcommand/wallet/create.rs @@ -14,6 +14,8 @@ pub(crate) struct Create { help = "Use to derive wallet seed." )] pub(crate) passphrase: String, + #[arg(long, value_enum, default_value="bech32m")] + pub(crate) address_type: AddressType, } impl Create { @@ -23,7 +25,7 @@ impl Create { let mnemonic = Mnemonic::from_entropy(&entropy)?; - initialize_wallet(&options, mnemonic.to_seed(self.passphrase.clone()))?; + initialize_wallet(&options, mnemonic.to_seed(self.passphrase.clone()), self.address_type)?; Ok(Box::new(Output { mnemonic, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 9042cd443e..2442f9841f 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -65,6 +65,13 @@ pub(crate) struct Inscribe { conflicts_with = "json_metadata" )] pub(crate) cbor_metadata: Option, + #[arg( + long, + help = "Consider spending outpoint , even if it is unconfirmed or contains inscriptions" + )] + pub(crate) utxo: Vec, + #[arg(long, help = "Only spend outpoints given with --utxo")] + pub(crate) coin_control: bool, #[arg( long, help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." @@ -113,12 +120,25 @@ impl Inscribe { let index = Index::open(&options)?; index.update()?; - let utxos = index.get_unspent_outputs(Wallet::load(&options)?)?; + let mut utxos = if self.coin_control { + BTreeMap::new() + } else { + index.get_unspent_outputs(Wallet::load(&options)?)? + }; let locked_utxos = index.get_locked_outputs(Wallet::load(&options)?)?; let client = options.bitcoin_rpc_client_for_wallet_command(false)?; + for outpoint in &self.utxo { + utxos.insert( + *outpoint, + Amount::from_sat( + client.get_raw_transaction(&outpoint.txid, None)?.output[outpoint.vout as usize].value, + ), + ); + } + let chain = options.chain(); let postage = self.postage.unwrap_or(TransactionBuilder::TARGET_POSTAGE); diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index 0d2f596ad0..7478f27a30 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -10,11 +10,13 @@ pub(crate) struct Restore { help = "Use when deriving wallet" )] pub(crate) passphrase: String, + #[arg(long, value_enum, default_value="bech32m")] + pub(crate) address_type: AddressType, } impl Restore { pub(crate) fn run(self, options: Options) -> SubcommandResult { - initialize_wallet(&options, self.mnemonic.to_seed(self.passphrase))?; + initialize_wallet(&options, self.mnemonic.to_seed(self.passphrase), self.address_type)?; Ok(Box::new(Empty {})) } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 32646dfeda..1fdd0d43f3 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -4,6 +4,16 @@ use {super::*, crate::subcommand::wallet::transaction_builder::Target, crate::wa pub(crate) struct Send { address: Address, outgoing: Outgoing, + #[arg( + long, + help = "Consider spending outpoint , even if it is unconfirmed or contains inscriptions" + )] + utxo: Vec, + #[clap( + long, + help = "Only spend outpoints given with --utxo when sending inscriptions or satpoints" + )] + pub(crate) coin_control: bool, #[arg(long, help = "Use fee rate of sats/vB")] fee_rate: FeeRate, #[arg( @@ -32,7 +42,20 @@ impl Send { let client = options.bitcoin_rpc_client_for_wallet_command(false)?; - let unspent_outputs = index.get_unspent_outputs(Wallet::load(&options)?)?; + let mut unspent_outputs = if self.coin_control { + BTreeMap::new() + } else { + index.get_unspent_outputs(Wallet::load(&options)?)? + }; + + for outpoint in &self.utxo { + unspent_outputs.insert( + *outpoint, + Amount::from_sat( + client.get_raw_transaction(&outpoint.txid, None)?.output[outpoint.vout as usize].value, + ), + ); + } let locked_outputs = index.get_locked_outputs(Wallet::load(&options)?)?; @@ -51,6 +74,9 @@ impl Send { .get_inscription_satpoint_by_id(id)? .ok_or_else(|| anyhow!("Inscription {id} not found"))?, Outgoing::Amount(amount) => { + if self.coin_control || !self.utxo.is_empty() { + bail!("--coin_control and --utxo don't work when sending cardinals"); + } Self::lock_inscriptions(&client, inscriptions, unspent_outputs)?; let txid = Self::send_amount(&client, amount, address, self.fee_rate.n())?; return Ok(Box::new(Output { transaction: txid })); From 294d86766a44962d48a643bf36154e8a20bd40a1 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 9 Nov 2023 22:20:18 +0000 Subject: [PATCH 002/109] Release 0.11.0-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3156d7a5f7..eb21c4fb22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.11.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.11.0-gm1) - 2023-11-09 +---------------------------------------------------------------------------------- + +### Added +- Merged my changes from 0.10.x to 0.11.x. + [0.11.0](https://github.com/ordinals/ord/releases/tag/0.11.0) - 2023-11-07 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 9ccfad0a88..7a4586df90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.0" +version = "0.11.0-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 9c9e761fbb..b8f0fbc7da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.0" +version = "0.11.0-gm1" license = "CC0-1.0" edition = "2021" autotests = false From c70cc42b1c5b73c0f97d25f63369f6340c5376aa Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 9 Nov 2023 22:44:48 +0000 Subject: [PATCH 003/109] Move debug logs to log::debug level. --- src/index/updater/inscription_updater.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 89f8f62732..e85964f9a7 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -197,7 +197,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let sat = Self::calculate_sat(input_sat_ranges, offset); - log::info!("processing reinscription {inscription_id} on sat {:?}: sequence number {seq_num}, inscribed offsets {:?}", sat, inscribed_offsets); + log::debug!("processing reinscription {inscription_id} on sat {:?}: sequence number {seq_num}, inscribed offsets {:?}", sat, inscribed_offsets); Some(Curse::Reinscription) } else { @@ -205,7 +205,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { }; if curse.is_some() { - log::info!("found cursed inscription {inscription_id}: {:?}", curse); + log::debug!("found cursed inscription {inscription_id}: {:?}", curse); } let cursed = if let Some(Curse::Reinscription) = curse { @@ -227,7 +227,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { }) .unwrap_or(false); - log::info!("{inscription_id}: is first reinscription: {first_reinscription}, initial inscription is cursed: {initial_inscription_is_cursed}"); + log::debug!("{inscription_id}: is first reinscription: {first_reinscription}, initial inscription is cursed: {initial_inscription_is_cursed}"); !(initial_inscription_is_cursed && first_reinscription) } else { @@ -237,7 +237,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let unbound = current_input_value == 0 || curse == Some(Curse::UnrecognizedEvenField); if curse.is_some() || unbound { - log::info!( + log::debug!( "indexing inscription {inscription_id} with curse {:?} as cursed {} and unbound {}", curse, cursed, From 678bb9ffd789a41b9e98c9281df26f783dc24d91 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 9 Nov 2023 23:47:11 +0000 Subject: [PATCH 004/109] Add logging for new upstream endpoints. --- src/subcommand/server.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index c555102ed2..0ed6a2a671 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -776,6 +776,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(rune)): Path>, ) -> ServerResult> { + log::info!("GET /rune/{rune}"); let (id, entry) = index.rune(rune)?.ok_or_else(|| { ServerError::NotFound( "tracking runes requires index created with `--index-runes-pre-alpha-i-agree-to-get-rekt` flag".into(), @@ -796,6 +797,7 @@ impl Server { Extension(page_config): Extension>, Extension(index): Extension>, ) -> ServerResult> { + log::info!("GET /runes"); Ok( RunesHtml { entries: index.runes()?, @@ -999,6 +1001,7 @@ impl Server { Extension(index): Extension>, Path(inscription_id): Path, ) -> ServerResult> { + log::info!("GET /r/metadata/{inscription_id}"); let metadata = index .get_inscription_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))? @@ -1210,6 +1213,7 @@ impl Server { } async fn block_hash_json(Extension(index): Extension>) -> ServerResult> { + log::info!("GET /r/blockhash"); Ok(Json( index .block_hash(None)? @@ -1235,6 +1239,7 @@ impl Server { Extension(index): Extension>, Path(height): Path, ) -> ServerResult> { + log::info!("GET /r/blockhash/{height}"); Ok(Json( index .block_hash(Some(height))? @@ -1543,6 +1548,7 @@ impl Server { Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, ) -> ServerResult { + log::info!("GET /children/{parent}/{page}"); let parent_number = index .get_inscription_entry(parent)? .ok_or_not_found(|| format!("inscription {parent}"))? From ded75ece4705343c91f6229adbb1f19c45d21ba7 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 16:27:11 +0000 Subject: [PATCH 005/109] Remove `children` subcommand and replace it with `/children` server endpoint. --- src/index.rs | 8 +++++--- src/subcommand.rs | 4 ---- src/subcommand/children.rs | 16 ---------------- src/subcommand/server.rs | 10 ++++++++++ 4 files changed, 15 insertions(+), 23 deletions(-) delete mode 100644 src/subcommand/children.rs diff --git a/src/index.rs b/src/index.rs index 86b235bc4c..e865295d9d 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1415,7 +1415,8 @@ impl Index { } } - pub(crate) fn get_children(&self) -> Result<()> { + pub(crate) fn get_children(&self) -> Result> { + let mut result = Vec::new(); for range in self .database .begin_read()? @@ -1423,12 +1424,13 @@ impl Index { .iter()? { let (parent, children) = range?; + let parent = ::load(*parent.value()); for child in children { - println!("{} {}", ::load(*parent.value()), ::load(*child?.value())); + result.push((parent, ::load(*child?.value()))); } } - Ok(()) + Ok(result) } pub(crate) fn get_stats(&self) -> Result<(Option, Option, Option)> { diff --git a/src/subcommand.rs b/src/subcommand.rs index 5cc88dbacb..d2d9501c29 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,6 +1,5 @@ use super::*; -pub mod children; pub mod decode; pub mod epochs; pub mod find; @@ -19,8 +18,6 @@ pub mod wallet; #[derive(Debug, Parser)] pub(crate) enum Subcommand { - #[command(about = "List all the child inscriptions")] - Children(children::Children), #[command(about = "Decode a transaction")] Decode(decode::Decode), #[command(about = "List the first satoshis of each reward epoch")] @@ -56,7 +53,6 @@ pub(crate) enum Subcommand { impl Subcommand { pub(crate) fn run(self, options: Options) -> SubcommandResult { match self { - Self::Children(children) => children.run(options), Self::Decode(decode) => decode.run(), Self::Epochs => epochs::run(), Self::Find(find) => find.run(options), diff --git a/src/subcommand/children.rs b/src/subcommand/children.rs deleted file mode 100644 index dc9f355ddf..0000000000 --- a/src/subcommand/children.rs +++ /dev/null @@ -1,16 +0,0 @@ -use super::*; - -#[derive(Debug, Parser)] -pub(crate) struct Children { -} - -impl Children { - pub(crate) fn run(self, options: Options) -> SubcommandResult { - let index = Index::open(&options)?; - index.update()?; - - index.get_children()?; - - Ok(Box::new(Empty {})) - } -} diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 0ed6a2a671..774599b360 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -255,6 +255,7 @@ impl Server { .route("/feed.xml", get(Self::feed)) .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) + .route("/children", get(Self::children_all)) .route("/children/:inscription_id", get(Self::children)) .route( "/children/:inscription_id/:page", @@ -497,6 +498,15 @@ impl Server { index.block_height()?.ok_or_not_found(|| "genesis block") } + async fn children_all(Extension(index): Extension>) -> ServerResult { + log::info!("GET /children"); + let mut result = String::new(); + for (parent, child) in index.get_children()? { + result += format!("{} {}", parent, child).as_str(); + } + Ok(result) + } + async fn clock(Extension(index): Extension>) -> ServerResult { log::info!("GET /clock"); Ok( From 27c740f6e3781c1406a286853d26adce19e9f608 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 16:40:17 +0000 Subject: [PATCH 006/109] Add ord version to `/stats` endpoint output. --- src/subcommand/server.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 774599b360..152ac80bba 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -146,6 +146,7 @@ struct SatoshiJson { #[derive(Serialize)] struct StatsJson { + version: String, highest_block_indexed: Option, lowest_inscription_number: Option, highest_inscription_number: Option, @@ -1026,6 +1027,7 @@ impl Server { let stats = index.get_stats()?; Ok( serde_json::to_string_pretty(&StatsJson { + version: env!("CARGO_PKG_VERSION").to_string(), highest_block_indexed: stats.0, lowest_inscription_number: stats.1, highest_inscription_number: stats.2, From b9d35b387ae14cbc72ca2ae97f82efc9f7eba764 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 16:44:21 +0000 Subject: [PATCH 007/109] Release 0.11.0-gm2 --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb21c4fb22..8e86467211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ Changelog ========= +[0.11.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.11.0-gm2) - 2023-11-14 +---------------------------------------------------------------------------------- + +### Added +- Add logging for new server endpoints. +- Add ord version to `/stats` endpoint output. + +### Changed +- Move server debug logging to debug level. +- Remove `children` subcommand and replace it with `/children` server endpoint. + [0.11.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.11.0-gm1) - 2023-11-09 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 7a4586df90..073553b490 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.0-gm1" +version = "0.11.0-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b8f0fbc7da..e5aa300c19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.0-gm1" +version = "0.11.0-gm2" license = "CC0-1.0" edition = "2021" autotests = false From 513ded0b9e4c67b5fb5c4d2fb740e0257be94f7e Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 16:59:41 +0000 Subject: [PATCH 008/109] Add missing newline in children list. --- src/subcommand/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 152ac80bba..8cc4c60f2b 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -503,7 +503,7 @@ impl Server { log::info!("GET /children"); let mut result = String::new(); for (parent, child) in index.get_children()? { - result += format!("{} {}", parent, child).as_str(); + result += format!("{} {}\n", parent, child).as_str(); } Ok(result) } From 484243a502a8c06882814c36c0b2dc47a6f50350 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 17:02:18 +0000 Subject: [PATCH 009/109] Add a header line to child list. --- src/subcommand/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 8cc4c60f2b..e2b765e154 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -501,7 +501,7 @@ impl Server { async fn children_all(Extension(index): Extension>) -> ServerResult { log::info!("GET /children"); - let mut result = String::new(); + let mut result = "parent child\n".to_string(); for (parent, child) in index.get_children()? { result += format!("{} {}\n", parent, child).as_str(); } From aacbe95e1d8a72528d2aab243e5d40c63108ec37 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 14 Nov 2023 17:10:56 +0000 Subject: [PATCH 010/109] Release 0.11.1-gm2 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edbd2c1ba4..23b5922013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.11.1-gm2](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm2) - 2023-11-14 +---------------------------------------------------------------------------------- + +### Changed +- Fixed the `/children` endpoint. + [0.11.1-gm1](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm1) - 2023-11-14 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index b9dd7cc25b..8af21b63bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.1-gm1" +version = "0.11.1-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 097a0f1a41..eeb6919c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.1-gm1" +version = "0.11.1-gm2" license = "CC0-1.0" edition = "2021" autotests = false From c1bde8125a41ea113b2d8c22a39cf9c6df92aeab Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 15 Nov 2023 14:45:30 +0000 Subject: [PATCH 011/109] Add `--key` flag to `wallet inscribe` to allow using a specific recovery key. --- src/subcommand/preview.rs | 1 + src/subcommand/wallet/inscribe.rs | 3 +++ src/subcommand/wallet/inscribe/batch.rs | 10 +++++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index c4fb887fbb..7846eb6f28 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -97,6 +97,7 @@ impl Preview { postage: Some(TransactionBuilder::TARGET_POSTAGE), reinscribe: false, satpoint: None, + key: None, }, )), } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 2442f9841f..869395c40e 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -111,6 +111,8 @@ pub(crate) struct Inscribe { pub(crate) reinscribe: bool, #[arg(long, help = "Inscribe .")] pub(crate) satpoint: Option, + #[clap(long, help = "Use provided recovery key instead of a random one.")] + pub(crate) key: Option, } impl Inscribe { @@ -196,6 +198,7 @@ impl Inscribe { destinations, dry_run: self.dry_run, inscriptions, + key: self.key, mode, no_backup: self.no_backup, no_limit: self.no_limit, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index df3164c8d6..98ccc8904f 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -5,6 +5,7 @@ pub(super) struct Batch { pub(super) destinations: Vec
, pub(super) dry_run: bool, pub(super) inscriptions: Vec, + pub(super) key: Option, pub(super) mode: Mode, pub(super) no_backup: bool, pub(super) no_limit: bool, @@ -22,6 +23,7 @@ impl Default for Batch { destinations: Vec::new(), dry_run: false, inscriptions: Vec::new(), + key: None, mode: Mode::SharedOutput, no_backup: false, no_limit: false, @@ -255,7 +257,13 @@ impl Batch { } let secp256k1 = Secp256k1::new(); - let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng()); + let key_pair = if self.key.is_some() { + secp256k1::KeyPair::from_secret_key(&secp256k1, &PrivateKey::from_wif(&self.key.clone().unwrap())?.inner) + } else { + let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng()); + log::info!("random backup key: {}", PrivateKey::new(key_pair.secret_key(), chain.network()).to_wif()); + key_pair + }; let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); let reveal_script = Inscription::append_batch_reveal_script( From 9fd8dfa0e5473d932e15feaf148f31d327eb7cc3 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 15 Nov 2023 16:01:15 +0000 Subject: [PATCH 012/109] Add `--ignore-outdated-index` flag to allow ord to run without having to fully index the blockchain. Be careful. Inscriptions that haven't been indexed will be treated as if they are cardinals, and so can be accidentally sent to spent as fees. --- src/index.rs | 2 ++ src/options.rs | 4 +++- src/subcommand/wallet/inscribe.rs | 4 ++++ src/subcommand/wallet/send.rs | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/index.rs b/src/index.rs index e865295d9d..46f5c604d2 100644 --- a/src/index.rs +++ b/src/index.rs @@ -385,6 +385,7 @@ impl Index { ), ); } + if !self.options.ignore_outdated_index { let rtx = self.database.begin_read()?; let outpoint_to_value = rtx.open_table(OUTPOINT_TO_VALUE)?; for outpoint in utxos.keys() { @@ -394,6 +395,7 @@ impl Index { )); } } + } Ok(utxos) } diff --git a/src/options.rs b/src/options.rs index 4f2b0f2a67..d2864b1f40 100644 --- a/src/options.rs +++ b/src/options.rs @@ -61,8 +61,10 @@ pub(crate) struct Options { pub(crate) testnet: bool, #[arg(long, default_value = "ord", help = "Use wallet named .")] pub(crate) wallet: String, - #[arg(long, short, help = "Don't check for standard wallet descriptors.")] + #[arg(long, help = "Don't check for standard wallet descriptors.")] pub(crate) ignore_descriptors: bool, + #[arg(long, help = "Don't fail when the index is out of date. This is dangerous, and results in ord treating inscriptions as cardinals if their corresponding utxos haven't been indexed. Use at your own risk.")] + pub(crate) ignore_outdated_index: bool, } impl Options { diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 869395c40e..f689b6303a 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -124,6 +124,10 @@ impl Inscribe { let mut utxos = if self.coin_control { BTreeMap::new() + } else if options.ignore_outdated_index { + return Err(anyhow!( + "--ignore-outdated-index only works in conjunction with --coin-control when inscribing" + )); } else { index.get_unspent_outputs(Wallet::load(&options)?)? }; diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 1fdd0d43f3..073c4832ac 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -44,6 +44,10 @@ impl Send { let mut unspent_outputs = if self.coin_control { BTreeMap::new() + } else if options.ignore_outdated_index { + return Err(anyhow!( + "--ignore-outdated-index only works in conjunction with --coin-control when sending" + )); } else { index.get_unspent_outputs(Wallet::load(&options)?)? }; From 9f72496beab61cf2e16a1c5bb732fda464733d11 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 15 Nov 2023 16:02:38 +0000 Subject: [PATCH 013/109] Release 0.11.1-gm3 --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23b5922013..69054eac06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +[0.11.1-gm3](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm3) - 2023-11-15 +---------------------------------------------------------------------------------- + +### Added +Add `--key` flag to `wallet inscribe` to allow using a specific recovery key. +Add `--ignore-outdated-index` flag to allow ord to run without having to fully index the blockchain. Be careful. Inscriptions that haven't been indexed will be treated as if they are cardinals, and so can be accidentally sent to spent as fees. + [0.11.1-gm2](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm2) - 2023-11-14 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 8af21b63bf..83640d7a48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.1-gm2" +version = "0.11.1-gm3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index eeb6919c74..28e03235ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.1-gm2" +version = "0.11.1-gm3" license = "CC0-1.0" edition = "2021" autotests = false From 186fbf0bd7d033faa811827d21ece28369e90b11 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 17 Nov 2023 01:09:08 +0000 Subject: [PATCH 014/109] Add `--dump` and `--no-broadcast` flags to `wallet inscribe`. --- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 19 ++++++++++++ src/subcommand/wallet/inscribe/batch.rs | 39 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 7846eb6f28..d9256d6729 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -98,6 +98,8 @@ impl Preview { reinscribe: false, satpoint: None, key: None, + dump: false, + no_broadcast: false, }, )), } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index f689b6303a..b0c408cd6c 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -16,6 +16,7 @@ use { }, bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, SignRawTransactionInput, Timestamp}, bitcoincore_rpc::Client, + bitcoincore_rpc::RawTx, std::collections::BTreeSet, }; @@ -30,9 +31,16 @@ pub struct InscriptionInfo { #[derive(Serialize, Deserialize)] pub struct Output { pub commit: Txid, + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_hex: Option, pub inscriptions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] pub parent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub recovery_descriptor: Option, pub reveal: Txid, + #[serde(skip_serializing_if = "Option::is_none")] + pub reveal_hex: Option, pub total_fees: u64, } @@ -113,12 +121,21 @@ pub(crate) struct Inscribe { pub(crate) satpoint: Option, #[clap(long, help = "Use provided recovery key instead of a random one.")] pub(crate) key: Option, + #[clap(long, help = "Dump raw hex transactions and recovery keys to standard output.")] + pub(crate) dump: bool, + #[clap(long, help = "Do not broadcast any transactions. Implies --dump.")] + pub(crate) no_broadcast: bool, } impl Inscribe { pub(crate) fn run(self, options: Options) -> SubcommandResult { + let mut dump = self.dump; let metadata = Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?; + if self.no_broadcast { + dump = true; + } + let index = Index::open(&options)?; index.update()?; @@ -200,11 +217,13 @@ impl Inscribe { Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), destinations, + dump, dry_run: self.dry_run, inscriptions, key: self.key, mode, no_backup: self.no_backup, + no_broadcast: self.no_broadcast, no_limit: self.no_limit, parent_info, postage, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 98ccc8904f..f9bcd0b37f 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -3,11 +3,13 @@ use super::*; pub(super) struct Batch { pub(super) commit_fee_rate: FeeRate, pub(super) destinations: Vec
, + pub(super) dump: bool, pub(super) dry_run: bool, pub(super) inscriptions: Vec, pub(super) key: Option, pub(super) mode: Mode, pub(super) no_backup: bool, + pub(super) no_broadcast: bool, pub(super) no_limit: bool, pub(super) parent_info: Option, pub(super) postage: Amount, @@ -21,11 +23,13 @@ impl Default for Batch { Batch { commit_fee_rate: 1.0.try_into().unwrap(), destinations: Vec::new(), + dump: false, dry_run: false, inscriptions: Vec::new(), key: None, mode: Mode::SharedOutput, no_backup: false, + no_broadcast: false, no_limit: false, parent_info: None, postage: Amount::from_sat(10_000), @@ -65,6 +69,9 @@ impl Batch { return Ok(Box::new(self.output( commit_tx.txid(), reveal_tx.txid(), + None, + None, + None, total_fees, self.inscriptions.clone(), ))); @@ -103,6 +110,10 @@ impl Batch { Self::backup_recovery_key(client, recovery_key_pair, chain.network())?; } + let (commit, reveal) = if self.no_broadcast { + (client.decode_raw_transaction(&signed_commit_tx, None)?.txid, + client.decode_raw_transaction(&signed_reveal_tx, None)?.txid) + } else { let commit = client.send_raw_transaction(&signed_commit_tx)?; let reveal = match client.send_raw_transaction(&signed_reveal_tx) { @@ -114,9 +125,15 @@ impl Batch { } }; + (commit, reveal) + }; + Ok(Box::new(self.output( commit, reveal, + if self.dump { Some(signed_commit_tx.raw_hex()) } else { None }, + if self.dump { Some(signed_reveal_tx.raw_hex()) } else { None }, + if self.dump { Some(Self::get_recovery_key(&client, recovery_key_pair, chain.network())?.to_string()) } else { None }, total_fees, self.inscriptions.clone(), ))) @@ -126,6 +143,9 @@ impl Batch { &self, commit: Txid, reveal: Txid, + commit_hex: Option, + reveal_hex: Option, + recovery_descriptor: Option, total_fees: u64, inscriptions: Vec, ) -> super::Output { @@ -169,7 +189,10 @@ impl Batch { super::Output { commit, + commit_hex, reveal, + reveal_hex, + recovery_descriptor, total_fees, parent: self.parent_info.clone().map(|info| info.id), inscriptions: inscriptions_output, @@ -441,6 +464,22 @@ impl Batch { Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) } + fn get_recovery_key( + client: &Client, + recovery_key_pair: TweakedKeyPair, + network: Network, + ) -> Result { + let recovery_private_key = + PrivateKey::new(recovery_key_pair.to_inner().secret_key(), network).to_wif(); + Ok(format!( + "rawtr({})#{}", + recovery_private_key, + client + .get_descriptor_info(&format!("rawtr({})", recovery_private_key))? + .checksum + )) + } + fn backup_recovery_key( client: &Client, recovery_key_pair: TweakedKeyPair, From 58bb77201736d53e3732d2be3750d3d71f4dc7a3 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 20 Nov 2023 03:10:14 +0000 Subject: [PATCH 015/109] Add `wallet send-many`. --- src/index.rs | 12 +- src/inscription_id.rs | 2 +- src/subcommand/wallet.rs | 4 + src/subcommand/wallet/inscriptions.rs | 2 +- src/subcommand/wallet/sendmany.rs | 276 ++++++++++++++++++++++++++ 5 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 src/subcommand/wallet/sendmany.rs diff --git a/src/index.rs b/src/index.rs index 46f5c604d2..979d1f07b3 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1222,9 +1222,19 @@ impl Index { &self, utxos: &BTreeMap, ) -> Result> { + let mut result = BTreeMap::new(); + + result.extend(self.get_inscriptions_vector(utxos)?.into_iter()); + Ok(result) + } + + pub(crate) fn get_inscriptions_vector( + &self, + utxos: &BTreeMap, + ) -> Result> { let rtx = self.database.begin_read()?; - let mut result = BTreeMap::new(); + let mut result = Vec::new(); let table = rtx.open_multimap_table(SATPOINT_TO_INSCRIPTION_ID)?; for utxo in utxos.keys() { diff --git a/src/inscription_id.rs b/src/inscription_id.rs index 36b05495a2..17cc45e27a 100644 --- a/src/inscription_id.rs +++ b/src/inscription_id.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq)] +#[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, Ord, PartialOrd)] pub struct InscriptionId { pub txid: Txid, pub index: u32, diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 7d4d1c3e1a..09cce2effd 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -24,6 +24,7 @@ pub mod receive; mod restore; pub mod sats; pub mod send; +pub mod sendmany; pub mod transaction_builder; pub mod transactions; @@ -45,6 +46,8 @@ pub(crate) enum Wallet { Sats(sats::Sats), #[command(about = "Send sat or inscription")] Send(send::Send), + #[command(about = "Send multiple inscriptions in a single transaction")] + SendMany(sendmany::SendMany), #[command(about = "See wallet transactions")] Transactions(transactions::Transactions), #[command(about = "List all unspent outputs in wallet")] @@ -72,6 +75,7 @@ impl Wallet { Self::Restore(restore) => restore.run(options), Self::Sats(sats) => sats.run(options), Self::Send(send) => send.run(options), + Self::SendMany(sendmany) => sendmany.run(options), Self::Transactions(transactions) => transactions.run(options), Self::Outputs => outputs::run(options), Self::Cardinals => cardinals::run(options), diff --git a/src/subcommand/wallet/inscriptions.rs b/src/subcommand/wallet/inscriptions.rs index 61335d09d9..fe2ba458b0 100644 --- a/src/subcommand/wallet/inscriptions.rs +++ b/src/subcommand/wallet/inscriptions.rs @@ -13,7 +13,7 @@ pub(crate) fn run(options: Options) -> SubcommandResult { index.update()?; let unspent_outputs = index.get_unspent_outputs(Wallet::load(&options)?)?; - let inscriptions = index.get_inscriptions(&unspent_outputs)?; + let inscriptions = index.get_inscriptions_vector(&unspent_outputs)?; let explorer = match options.chain() { Chain::Mainnet => "https://ordinals.com/inscription/", diff --git a/src/subcommand/wallet/sendmany.rs b/src/subcommand/wallet/sendmany.rs new file mode 100644 index 0000000000..37bce73f85 --- /dev/null +++ b/src/subcommand/wallet/sendmany.rs @@ -0,0 +1,276 @@ +use { + super::*, + crate::wallet::Wallet, + bitcoin::{ + locktime::absolute::LockTime, + Witness, + }, + bitcoincore_rpc::RawTx, + std::{ + collections::BTreeSet, + fs::File, + io::{BufRead, BufReader}, + }, +}; + +#[derive(Debug, Parser, Clone)] +pub(crate) struct SendMany { + #[arg(long, help = "Use fee rate of sats/vB")] + fee_rate: FeeRate, + #[clap(long, help = "Location of a CSV file containing `inscriptionid`,`destination` pairs.")] + pub(crate) csv: PathBuf, + #[clap(long, help = "Broadcast the transaction; the default is to output the raw tranasction hex so you can check it before broadcasting.")] + pub(crate) broadcast: bool, +} + +#[derive(Serialize, Deserialize)] +pub struct Output { + pub tx: String, +} + +impl SendMany { + const SCHNORR_SIGNATURE_SIZE: usize = 64; + + pub(crate) fn run(self, options: Options) -> SubcommandResult { + let file = File::open(&self.csv)?; + let reader = BufReader::new(file); + let mut line_number = 1; + let mut requested = BTreeMap::new(); + + let chain = options.chain(); + + for line in reader.lines() { + let line = line?; + let mut line = line.trim_start_matches('\u{feff}').split(','); + + let inscriptionid = line.next().ok_or_else(|| { + anyhow!("CSV file '{}' is not formatted correctly - no inscriptionid on line {line_number}", self.csv.display()) + })?; + + let inscriptionid = match InscriptionId::from_str(inscriptionid) { + Err(e) => bail!("bad inscriptionid on line {line_number}: {}", e), + Ok(ok) => ok, + }; + + let destination = line.next().ok_or_else(|| { + anyhow!("CSV file '{}' is not formatted correctly - no comma on line {line_number}", self.csv.display()) + })?; + + let destination = match match Address::from_str(destination) { + Err(e) => bail!("bad address on line {line_number}: {}", e), + Ok(ok) => ok, + }.require_network(chain.network()) { + Err(e) => bail!("bad network for address on line {line_number}: {}", e), + Ok(ok) => ok, + }; + + if requested.contains_key(&inscriptionid) { + bail!("duplicate entry for {} on line {}", inscriptionid.to_string(), line_number); + } + + requested.insert(inscriptionid, destination); + line_number += 1; + } + + let index = Index::open(&options)?; + index.update()?; + + let client = options.bitcoin_rpc_client_for_wallet_command(false)?; + let unspent_outputs = index.get_unspent_outputs(Wallet::load(&options)?)?; + let locked_outputs = index.get_locked_outputs(Wallet::load(&options)?)?; + + // we get a vector of (SatPoint, InscriptionId), and turn it into a map -> + let mut inscriptions = BTreeMap::new(); + for (satpoint, inscriptionid) in index.get_inscriptions_vector(&unspent_outputs)? { + inscriptions.insert(inscriptionid, satpoint); + } + + let mut inputs = Vec::new(); + let mut outputs = Vec::new(); + + let mut requested_satpoints: BTreeMap = BTreeMap::new(); + + // this loop checks that we own all the listed inscriptions, and that we aren't listing the same sat more than once + for (inscriptionid, address) in &requested { + if !inscriptions.contains_key(&inscriptionid) { + bail!("inscriptionid {} isn't in the wallet", inscriptionid.to_string()); + } + + let satpoint = inscriptions[&inscriptionid]; + if requested_satpoints.contains_key(&satpoint) { + bail!("inscriptionid {} is on the same sat as {}, and both appear in the CSV file", inscriptionid.to_string(), requested_satpoints[&satpoint].0); + } + requested_satpoints.insert(satpoint, (inscriptionid.clone(), address.clone())); + } + + // this loop handles the inscriptions in order of offset in each utxo + while !requested.is_empty() { + let mut inscriptions_on_outpoint = Vec::new(); + // pick the first remaining inscriptionid from the list + for (inscriptionid, _address) in &requested { + // look up which utxo it's in + let outpoint = inscriptions[inscriptionid].outpoint; + // get a list of the inscriptions in that utxo + inscriptions_on_outpoint = index.get_inscriptions_on_output_with_satpoints(outpoint)?; + // sort it by offset + inscriptions_on_outpoint.sort_by_key(|(s, _)| s.offset); + // make sure that they are all in the csv file + for (satpoint, outpoint_inscriptionid) in &inscriptions_on_outpoint { + if !requested_satpoints.contains_key(&satpoint) { + bail!("inscriptionid {} is in the same output as {} but wasn't in the CSV file", outpoint_inscriptionid.to_string(), inscriptionid.to_string()); + } + } + break; + } + + // create an input for the first inscription of each utxo + let (first_satpoint, _first_inscription) = inscriptions_on_outpoint[0]; + let first_offset = first_satpoint.offset; + let first_outpoint = first_satpoint.outpoint; + let utxo_value = unspent_outputs[&first_outpoint].to_sat(); + if first_offset != 0 { + bail!("the first inscription in {} is at non-zero offset {}", first_outpoint, first_offset); + } + inputs.push(first_outpoint); + + // filter out the inscriptions that aren't in our list, but are still to be sent - these are inscriptions that are on the same sat as the ones we listed + // we want to remove just the ones where the satpoint is requested but that particular inscriptionid isn't + // ie. keep the ones where the satpoint isn't requested or the inscriptionid is + inscriptions_on_outpoint = inscriptions_on_outpoint.into_iter().filter( + |(satpoint, inscriptionid)| !requested_satpoints.contains_key(&satpoint) || requested.contains_key(&inscriptionid) + ).collect(); + + // create an output for each inscription in this utxo + for (i, (satpoint, inscriptionid)) in inscriptions_on_outpoint.iter().enumerate() { + let destination = &requested_satpoints[&satpoint].1; + let offset = satpoint.offset; + let value = if i == inscriptions_on_outpoint.len() - 1 { + utxo_value - offset + } else { + inscriptions_on_outpoint[i + 1].0.offset - offset + }; + let script_pubkey = destination.script_pubkey(); + let dust_limit = script_pubkey.dust_value().to_sat(); + if value < dust_limit { + bail!("inscription {} at {} is only followed by {} sats, less than dust limit {} for address {}", + inscriptionid, satpoint.to_string(), value, dust_limit, destination); + } + outputs.push(TxOut{script_pubkey, value}); + + // remove each inscription in this utxo from the list + requested.remove(&inscriptionid); + } + } + + // get a list of available unlocked cardinals + let cardinals = Self::get_cardinals(unspent_outputs, locked_outputs, inscriptions); + + if cardinals.is_empty() { + bail!("wallet has no cardinals"); + } + + // select the biggest cardinal - this could be improved by figuring out what size we need, and picking the next biggest for example + let (cardinal_outpoint, cardinal_value) = cardinals[0]; + + // use the biggest cardinal as the last input + inputs.push(cardinal_outpoint); + + let change_address = get_change_address(&client, chain)?; + let script_pubkey = change_address.script_pubkey(); + let dust_limit = script_pubkey.dust_value().to_sat(); + let value = 0; // we don't know how much change to take until we know the fee, which means knowing the tx vsize + outputs.push(TxOut{script_pubkey: script_pubkey.clone(), value}); + + // calculate the vsize of the tx once it is signed + let vsize = Self::estimate_transaction_vsize(inputs.len(), outputs.clone()); + let fee = self.fee_rate.fee(vsize).to_sat(); + let needed = fee + dust_limit; + if cardinal_value < needed { + bail!("cardinal ({}) is too small: we need enough for fee {} plus dust limit {} = {}", cardinal_value, fee, dust_limit, needed); + } + let value = cardinal_value - fee; + let last = outputs.len() - 1; + outputs[last] = TxOut{script_pubkey, value}; + + let tx = Self::build_transaction(inputs, outputs); + + let signed_tx = client.sign_raw_transaction_with_wallet(&tx, None, None)?; + let signed_tx = signed_tx.hex; + + if self.broadcast { + let txid = client.send_raw_transaction(&signed_tx)?.to_string(); + Ok(Box::new(Output { tx: txid })) + } else { + Ok(Box::new(Output { tx: signed_tx.raw_hex() })) + } + } + + fn get_cardinals( + unspent_outputs: BTreeMap, + locked_outputs: BTreeSet, + inscriptions: BTreeMap, + ) -> Vec<(OutPoint, u64)> { + let inscribed_utxos = + inscriptions // get a tree of the inscriptions we own + .values() // just the SatPoints + .map(|satpoint| satpoint.outpoint) // just the OutPoints of those SatPoints + .collect::>(); // as a set of OutPoints + + let mut cardinal_utxos = unspent_outputs + .iter() + .filter_map(|(output, amount)| { + if inscribed_utxos.contains(output) || locked_outputs.contains(output) { + None + } else { + Some(( + *output, + amount.to_sat(), + )) + } + }) + .collect::>(); + + cardinal_utxos.sort_by_key(|x| x.1); + cardinal_utxos.reverse(); + cardinal_utxos + } + + fn build_transaction( + inputs: Vec, + outputs: Vec, + ) -> Transaction { + Transaction { + input: inputs + .iter() + .map(|outpoint| TxIn { + previous_output: *outpoint, + script_sig: script::Builder::new().into_script(), + witness: Witness::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + }) + .collect(), + output: outputs, + lock_time: LockTime::ZERO, + version: 1, + } + } + + fn estimate_transaction_vsize( + inputs: usize, + outputs: Vec, + ) -> usize { + Transaction { + 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, + lock_time: LockTime::ZERO, + version: 1, + }.vsize() + } +} From 468b7a76d9738fec301c0c3a2dc64f1f8865fd03 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 20 Nov 2023 03:15:03 +0000 Subject: [PATCH 016/109] Release 0.11.1-gm4 --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 69054eac06..d00d498738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +[0.11.1-gm4](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm4) - 2023-11-19 +---------------------------------------------------------------------------------- + +### Added +Add `--dump` and `--no-broadcast` flags to `wallet inscribe`. +Add `wallet send-many` to allow sending multiple inscriptions in a single command. + [0.11.1-gm3](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm3) - 2023-11-15 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 83640d7a48..7684ea2aa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.1-gm3" +version = "0.11.1-gm4" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 28e03235ef..79452b167e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.1-gm3" +version = "0.11.1-gm4" license = "CC0-1.0" edition = "2021" autotests = false From 8519c342f4763d4ea1752943010fbde1707b01bd Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 21 Nov 2023 13:17:34 +0000 Subject: [PATCH 017/109] Check that the tx doesn't exceed the maximum weight. Add `--no-limit` flag to disable the check. --- src/subcommand/wallet/sendmany.rs | 45 +++++++++++++++++++------------ 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/subcommand/wallet/sendmany.rs b/src/subcommand/wallet/sendmany.rs index 37bce73f85..556053c3c2 100644 --- a/src/subcommand/wallet/sendmany.rs +++ b/src/subcommand/wallet/sendmany.rs @@ -3,6 +3,7 @@ use { crate::wallet::Wallet, bitcoin::{ locktime::absolute::LockTime, + policy::MAX_STANDARD_TX_WEIGHT, Witness, }, bitcoincore_rpc::RawTx, @@ -17,10 +18,13 @@ use { pub(crate) struct SendMany { #[arg(long, help = "Use fee rate of sats/vB")] fee_rate: FeeRate, - #[clap(long, help = "Location of a CSV file containing `inscriptionid`,`destination` pairs.")] + #[arg(long, help = "Location of a CSV file containing `inscriptionid`,`destination` pairs.")] pub(crate) csv: PathBuf, - #[clap(long, help = "Broadcast the transaction; the default is to output the raw tranasction hex so you can check it before broadcasting.")] + #[arg(long, help = "Broadcast the transaction; the default is to output the raw tranasction hex so you can check it before broadcasting.")] pub(crate) broadcast: bool, + #[arg(long, help = "Do not check that the transaction is equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently 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(Serialize, Deserialize)] @@ -181,18 +185,25 @@ impl SendMany { let value = 0; // we don't know how much change to take until we know the fee, which means knowing the tx vsize outputs.push(TxOut{script_pubkey: script_pubkey.clone(), value}); - // calculate the vsize of the tx once it is signed - let vsize = Self::estimate_transaction_vsize(inputs.len(), outputs.clone()); - let fee = self.fee_rate.fee(vsize).to_sat(); + // calculate the size of the tx once it is signed + let fake_tx = Self::build_fake_transaction(&inputs, &outputs); + let weight = fake_tx.weight(); + if !self.no_limit && weight > bitcoin::Weight::from_wu(MAX_STANDARD_TX_WEIGHT.into()) { + bail!( + "transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): {weight}" + ); + } + let fee = self.fee_rate.fee(fake_tx.vsize()).to_sat(); let needed = fee + dust_limit; if cardinal_value < needed { - bail!("cardinal ({}) is too small: we need enough for fee {} plus dust limit {} = {}", cardinal_value, fee, dust_limit, needed); + bail!("cardinal {} ({} sats) is too small\n we need enough for fee {} plus dust limit {} = {} sats", + cardinal_outpoint.to_string(), cardinal_value, fee, dust_limit, needed); } let value = cardinal_value - fee; let last = outputs.len() - 1; outputs[last] = TxOut{script_pubkey, value}; - let tx = Self::build_transaction(inputs, outputs); + let tx = Self::build_transaction(&inputs, &outputs); let signed_tx = client.sign_raw_transaction_with_wallet(&tx, None, None)?; let signed_tx = signed_tx.hex; @@ -236,8 +247,8 @@ impl SendMany { } fn build_transaction( - inputs: Vec, - outputs: Vec, + inputs: &Vec, + outputs: &Vec, ) -> Transaction { Transaction { input: inputs @@ -249,18 +260,18 @@ impl SendMany { sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }) .collect(), - output: outputs, + output: outputs.clone(), lock_time: LockTime::ZERO, version: 1, } } - fn estimate_transaction_vsize( - inputs: usize, - outputs: Vec, - ) -> usize { + fn build_fake_transaction( + inputs: &Vec, + outputs: &Vec, + ) -> Transaction { Transaction { - input: (0..inputs) + input: (0..inputs.len()) .map(|_| TxIn { previous_output: OutPoint::null(), script_sig: ScriptBuf::new(), @@ -268,9 +279,9 @@ impl SendMany { witness: Witness::from_slice(&[&[0; Self::SCHNORR_SIGNATURE_SIZE]]), }) .collect(), - output: outputs, + output: outputs.clone(), lock_time: LockTime::ZERO, version: 1, - }.vsize() + } } } From 2efe8724e07bbe68c8a954ebc3118c75617b56e4 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 22 Nov 2023 19:13:13 +0000 Subject: [PATCH 018/109] Add `--ignore-cursed` flag to treat all cursed inscriptions as regular inscriptions when indexing. --- src/index/updater.rs | 1 + src/index/updater/inscription_updater.rs | 7 ++++++- src/options.rs | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index 8e9fb85685..31cfc88d89 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -435,6 +435,7 @@ impl<'index> Updater<'_> { block.header.time, unbound_inscriptions, value_cache, + index.options.ignore_cursed, )?; if self.index.index_sats { diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index e85964f9a7..f58d029596 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -44,6 +44,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { timestamp: u32, pub(super) unbound_inscriptions: u64, value_cache: &'a mut HashMap, + pub(super) ignore_cursed: bool, } impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { @@ -75,6 +76,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { timestamp: u32, unbound_inscriptions: u64, value_cache: &'a mut HashMap, + ignore_cursed: bool, ) -> Result { let next_sequence_number = sequence_number_to_id .iter()? @@ -104,6 +106,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { timestamp, unbound_inscriptions, value_cache, + ignore_cursed, }) } @@ -178,7 +181,9 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { index: id_counter, }; - let curse = if inscription.payload.unrecognized_even_field { + let curse = if self.ignore_cursed { + None + } else if inscription.payload.unrecognized_even_field { Some(Curse::UnrecognizedEvenField) } else if inscription.payload.duplicate_field { Some(Curse::DuplicateField) diff --git a/src/options.rs b/src/options.rs index d2864b1f40..2c079b55ff 100644 --- a/src/options.rs +++ b/src/options.rs @@ -65,6 +65,8 @@ pub(crate) struct Options { pub(crate) ignore_descriptors: bool, #[arg(long, help = "Don't fail when the index is out of date. This is dangerous, and results in ord treating inscriptions as cardinals if their corresponding utxos haven't been indexed. Use at your own risk.")] pub(crate) ignore_outdated_index: bool, + #[arg(long, help = "Treat cursed inscriptions as regular inscriptions when indexing. Be consistent; either specify this flag every time you use a given index file or never.")] + pub(crate) ignore_cursed: bool, } impl Options { From 6bbba4c7d943e5c7827dd3a591ed8e4c1b6d598c Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 22 Nov 2023 22:26:50 +0000 Subject: [PATCH 019/109] Fix coin selection. --- src/subcommand/wallet/transaction_builder.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index ebdecf6a79..e9617ffadb 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -697,7 +697,13 @@ impl TransactionBuilder { current_value >= target_value && is_closer }; - if is_preference_and_closer || not_preference_but_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)) } } From bb7d3b0aad30ffcbb7aa6747e46c0ffbe8f2a83e Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 16:00:25 +0000 Subject: [PATCH 020/109] Add `--ordinals-wallet` flag to `wallet restore` to help with recovering coins from an ordinalswallet seed phrase. --- src/subcommand/wallet.rs | 14 ++++++++++---- src/subcommand/wallet/create.rs | 2 +- src/subcommand/wallet/restore.rs | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 09cce2effd..afd766cdc5 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -92,7 +92,7 @@ fn get_change_address(client: &Client, chain: Chain) -> Result
{ ) } -pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64], address_type: AddressType) -> Result { +pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64], address_type: AddressType, ordinalswallet: bool) -> Result { let client = options.bitcoin_rpc_client_for_wallet_command(true)?; let network = options.chain().network(); @@ -126,6 +126,7 @@ pub(crate) fn initialize_wallet(options: &Options, seed: [u8; 64], address_type: derived_private_key, change, &address_type, + ordinalswallet, )?; } @@ -139,13 +140,18 @@ fn derive_and_import_descriptor( derived_private_key: ExtendedPrivKey, change: bool, address_type: &AddressType, + ordinalswallet: bool, ) -> Result { let secret_key = DescriptorSecretKey::XPrv(DescriptorXKey { origin: Some(origin), xkey: derived_private_key, - derivation_path: DerivationPath::master().child(ChildNumber::Normal { - index: change.into(), - }), + derivation_path: if ordinalswallet { + DerivationPath::master() + } else { + DerivationPath::master().child(ChildNumber::Normal { + index: change.into(), + }) + }, wildcard: Wildcard::Unhardened, }); diff --git a/src/subcommand/wallet/create.rs b/src/subcommand/wallet/create.rs index a95b79015f..5fcf233455 100644 --- a/src/subcommand/wallet/create.rs +++ b/src/subcommand/wallet/create.rs @@ -25,7 +25,7 @@ impl Create { let mnemonic = Mnemonic::from_entropy(&entropy)?; - initialize_wallet(&options, mnemonic.to_seed(self.passphrase.clone()), self.address_type)?; + initialize_wallet(&options, mnemonic.to_seed(self.passphrase.clone()), self.address_type, false)?; Ok(Box::new(Output { mnemonic, diff --git a/src/subcommand/wallet/restore.rs b/src/subcommand/wallet/restore.rs index 7478f27a30..f89dbb88e0 100644 --- a/src/subcommand/wallet/restore.rs +++ b/src/subcommand/wallet/restore.rs @@ -12,11 +12,13 @@ pub(crate) struct Restore { pub(crate) passphrase: String, #[arg(long, value_enum, default_value="bech32m")] pub(crate) address_type: AddressType, + #[arg(long, help = "Restore from an ordinalswallet seed phrase. This will break most things, but might be useful rarely.")] + pub(crate) ordinalswallet: bool, } impl Restore { pub(crate) fn run(self, options: Options) -> SubcommandResult { - initialize_wallet(&options, self.mnemonic.to_seed(self.passphrase), self.address_type)?; + initialize_wallet(&options, self.mnemonic.to_seed(self.passphrase), self.address_type, self.ordinalswallet)?; Ok(Box::new(Empty {})) } } From c5dd89989684e32003e511f5aad9735d5978f39a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 16:04:29 +0000 Subject: [PATCH 021/109] Add some experimental options to allow creating just a commit tx, or just a reveal tx. --- src/subcommand/preview.rs | 4 + src/subcommand/wallet/inscribe.rs | 59 ++++- src/subcommand/wallet/inscribe/batch.rs | 253 +++++++++++++++---- src/subcommand/wallet/transaction_builder.rs | 10 +- 4 files changed, 264 insertions(+), 62 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index d9256d6729..047710d54f 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -98,6 +98,10 @@ impl Preview { reinscribe: false, satpoint: None, key: None, + commit_only: false, + commitment: None, + next_file: None, + reveal_input: Vec::new(), dump: false, no_broadcast: false, }, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b0c408cd6c..f52392e1da 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -14,7 +14,7 @@ use { taproot::{ControlBlock, LeafVersion, TapLeafHash, TaprootBuilder}, ScriptBuf, Witness, }, - bitcoincore_rpc::bitcoincore_rpc_json::{ImportDescriptors, SignRawTransactionInput, Timestamp}, + bitcoincore_rpc::bitcoincore_rpc_json::{GetRawTransactionResultVout, ImportDescriptors, SignRawTransactionInput, Timestamp}, bitcoincore_rpc::Client, bitcoincore_rpc::RawTx, std::collections::BTreeSet, @@ -30,7 +30,8 @@ pub struct InscriptionInfo { #[derive(Serialize, Deserialize)] pub struct Output { - pub commit: Txid, + #[serde(skip_serializing_if = "Option::is_none")] + pub commit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub commit_hex: Option, pub inscriptions: Vec, @@ -38,7 +39,8 @@ pub struct Output { pub parent: Option, #[serde(skip_serializing_if = "Option::is_none")] pub recovery_descriptor: Option, - pub reveal: Txid, + #[serde(skip_serializing_if = "Option::is_none")] + pub reveal: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reveal_hex: Option, pub total_fees: u64, @@ -121,6 +123,14 @@ pub(crate) struct Inscribe { pub(crate) satpoint: Option, #[clap(long, help = "Use provided recovery key instead of a random one.")] pub(crate) key: Option, + #[clap(long, help = "Don't make a reveal tx; just create a commit tx that sends all the sats to a new commitment. Requires --key to be specified.")] + pub(crate) commit_only: bool, + #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires --key to be specified.")] + pub(crate) commitment: Option, + #[clap(long, help = "Make the change of the reveal tx commit to the contents of .")] + pub(crate) next_file: Option, + #[clap(long, help = "Use as an extra input to the reveal tx. For use with `--commitment`.")] + pub(crate) reveal_input: Vec, #[clap(long, help = "Dump raw hex transactions and recovery keys to standard output.")] pub(crate) dump: bool, #[clap(long, help = "Do not broadcast any transactions. Implies --dump.")] @@ -129,6 +139,22 @@ pub(crate) struct Inscribe { impl Inscribe { pub(crate) fn run(self, options: Options) -> SubcommandResult { + if self.commitment.is_some() && self.key.is_none() { + return Err(anyhow!("--commitment only works with --key")); + } + + if self.commit_only && self.commitment.is_some() { + return Err(anyhow!("--commit-only and --commitment don't work together")); + } + + if self.commit_only && self.next_file.is_some() { + return Err(anyhow!("--commit-only and --next_file don't work together")); + } + + if self.commitment.is_none() && !self.reveal_input.is_empty() { + return Err(anyhow!("--reveal-input only works with --commitment")); + } + let mut dump = self.dump; let metadata = Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?; @@ -170,6 +196,7 @@ impl Inscribe { let inscriptions; let mode; let parent_info; + let next_inscription; match (self.file, self.batch) { (Some(file), None) => { @@ -179,9 +206,21 @@ impl Inscribe { file, self.parent, None, - self.metaprotocol, - metadata, + self.metaprotocol.clone(), + metadata.clone(), )?]; + next_inscription = if self.next_file.is_some() { + Some(Inscription::from_file( + chain, + self.next_file.unwrap(), + self.parent, + None, + self.metaprotocol, + metadata, + )?) + } else { + None + }; mode = Mode::SeparateOutputs; destinations = vec![match self.destination.clone() { Some(destination) => destination.require_network(chain.network())?, @@ -199,6 +238,7 @@ impl Inscribe { metadata, postage, )?; + next_inscription = None; mode = batchfile.mode; @@ -216,12 +256,20 @@ impl Inscribe { Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), + commit_only: self.commit_only, + commitment: self.commitment, + commitment_output: if self.commitment.is_some() { + Some(client.get_raw_transaction_info(&self.commitment.unwrap().txid, None)?.vout[self.commitment.unwrap().vout as usize].clone()) + } else { + None + }, destinations, dump, dry_run: self.dry_run, inscriptions, key: self.key, mode, + next_inscription, no_backup: self.no_backup, no_broadcast: self.no_broadcast, no_limit: self.no_limit, @@ -229,6 +277,7 @@ impl Inscribe { postage, reinscribe: self.reinscribe, reveal_fee_rate: self.fee_rate, + reveal_input: self.reveal_input, satpoint: self.satpoint, } .inscribe(chain, &index, &client, &locked_utxos, &utxos) diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index f9bcd0b37f..34d6af897b 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -2,12 +2,16 @@ use super::*; pub(super) struct Batch { pub(super) commit_fee_rate: FeeRate, + pub(super) commit_only: bool, + pub(super) commitment: Option, + pub(super) commitment_output: Option, pub(super) destinations: Vec
, pub(super) dump: bool, pub(super) dry_run: bool, pub(super) inscriptions: Vec, pub(super) key: Option, pub(super) mode: Mode, + pub(super) next_inscription: Option, pub(super) no_backup: bool, pub(super) no_broadcast: bool, pub(super) no_limit: bool, @@ -15,6 +19,7 @@ pub(super) struct Batch { pub(super) postage: Amount, pub(super) reinscribe: bool, pub(super) reveal_fee_rate: FeeRate, + pub(super) reveal_input: Vec, pub(super) satpoint: Option, } @@ -22,12 +27,16 @@ impl Default for Batch { fn default() -> Batch { Batch { commit_fee_rate: 1.0.try_into().unwrap(), + commit_only: false, + commitment: None, + commitment_output: None, destinations: Vec::new(), dump: false, dry_run: false, inscriptions: Vec::new(), key: None, mode: Mode::SharedOutput, + next_inscription: None, no_backup: false, no_broadcast: false, no_limit: false, @@ -35,6 +44,7 @@ impl Default for Batch { postage: Amount::from_sat(10_000), reinscribe: false, reveal_fee_rate: 1.0.try_into().unwrap(), + reveal_input: Vec::new(), satpoint: None, } } @@ -59,6 +69,7 @@ impl Batch { let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self .create_batch_inscription_transactions( wallet_inscriptions, + index, chain, locked_utxos.clone(), utxos.clone(), @@ -67,8 +78,16 @@ impl Batch { if self.dry_run { return Ok(Box::new(self.output( - commit_tx.txid(), - reveal_tx.txid(), + if self.commitment.is_some() { + None + } else { + Some(commit_tx.txid()) + }, + if self.commit_only { + None + } else { + Some(reveal_tx.txid()) + }, None, None, None, @@ -77,52 +96,78 @@ impl Batch { ))); } - let signed_commit_tx = client + let signed_commit_tx = if self.commitment.is_some() { + Vec::new() + } else { + client .sign_raw_transaction_with_wallet(&commit_tx, None, None)? - .hex; + .hex + }; + + let mut reveal_input_info = Vec::new(); + + if self.parent_info.is_some() { + for (vout, output) in commit_tx.output.iter().enumerate() { + reveal_input_info.push(SignRawTransactionInput { + txid: commit_tx.txid(), + vout: vout.try_into().unwrap(), + script_pub_key: output.script_pubkey.clone(), + redeem_script: None, + amount: Some(Amount::from_sat(output.value)), + }); + } + } + + for input in &self.reveal_input { + let output = index.get_transaction(input.txid)?.unwrap().output[input.vout as usize].clone(); + reveal_input_info.push(SignRawTransactionInput { + txid: input.txid, + vout: input.vout, + script_pub_key: output.script_pubkey.clone(), + redeem_script: None, + amount: Some(Amount::from_sat(output.value)), + }); + } - let signed_reveal_tx = if self.parent_info.is_some() { + let signed_reveal_tx = if reveal_input_info.is_empty() { + bitcoin::consensus::encode::serialize(&reveal_tx) + } else { client .sign_raw_transaction_with_wallet( &reveal_tx, - Some( - &commit_tx - .output - .iter() - .enumerate() - .map(|(vout, output)| SignRawTransactionInput { - txid: commit_tx.txid(), - vout: vout.try_into().unwrap(), - script_pub_key: output.script_pubkey.clone(), - redeem_script: None, - amount: Some(Amount::from_sat(output.value)), - }) - .collect::>(), - ), + Some(&reveal_input_info), None, )? .hex - } else { - bitcoin::consensus::encode::serialize(&reveal_tx) }; - if !self.no_backup { + if !self.no_backup && self.key.is_none() { Self::backup_recovery_key(client, recovery_key_pair, chain.network())?; } let (commit, reveal) = if self.no_broadcast { - (client.decode_raw_transaction(&signed_commit_tx, None)?.txid, - client.decode_raw_transaction(&signed_reveal_tx, None)?.txid) + (if self.commitment.is_some() { None } + else { Some(client.decode_raw_transaction(&signed_commit_tx, None)?.txid) }, + if self.commit_only { None } + else { Some(client.decode_raw_transaction(&signed_reveal_tx, None)?.txid) }) + } else { + let commit = if self.commitment.is_some() { + None } else { - let commit = client.send_raw_transaction(&signed_commit_tx)?; + Some(client.send_raw_transaction(&signed_commit_tx)?) + }; - let reveal = match client.send_raw_transaction(&signed_reveal_tx) { - Ok(txid) => txid, - Err(err) => { - return Err(anyhow!( - "Failed to send reveal transaction: {err}\nCommit tx {commit} will be recovered once mined" + let reveal = if self.commit_only { + None + } else { + match client.send_raw_transaction(&signed_reveal_tx) { + Ok(txid) => Some(txid), + Err(err) => { + return Err(anyhow!( + format!("Failed to send reveal transaction: {err}{}", if commit.is_some() { format!("\nCommit tx {:?} will be recovered once mined", commit) } else { "".to_string() }) )) - } + } + } }; (commit, reveal) @@ -131,8 +176,8 @@ impl Batch { Ok(Box::new(self.output( commit, reveal, - if self.dump { Some(signed_commit_tx.raw_hex()) } else { None }, - if self.dump { Some(signed_reveal_tx.raw_hex()) } else { None }, + if self.dump && self.commitment.is_none() { Some(signed_commit_tx.raw_hex()) } else { None }, + if self.dump && !self.commit_only { Some(signed_reveal_tx.raw_hex()) } else { None }, if self.dump { Some(Self::get_recovery_key(&client, recovery_key_pair, chain.network())?.to_string()) } else { None }, total_fees, self.inscriptions.clone(), @@ -141,8 +186,8 @@ impl Batch { fn output( &self, - commit: Txid, - reveal: Txid, + commit: Option, + reveal: Option, commit_hex: Option, reveal_hex: Option, recovery_descriptor: Option, @@ -175,17 +220,19 @@ impl Batch { Mode::SeparateOutputs => 0, }; + if !self.commit_only { inscriptions_output.push(InscriptionInfo { id: InscriptionId { - txid: reveal, + txid: reveal.unwrap(), index, }, location: SatPoint { - outpoint: OutPoint { txid: reveal, vout }, + outpoint: OutPoint { txid: reveal.unwrap(), vout }, offset, }, }); } + } super::Output { commit, @@ -202,6 +249,7 @@ impl Batch { pub(crate) fn create_batch_inscription_transactions( &self, wallet_inscriptions: BTreeMap, + index: &Index, chain: Chain, locked_utxos: BTreeSet, mut utxos: BTreeMap, @@ -214,6 +262,10 @@ impl Batch { .all(|inscription| inscription.parent().unwrap() == parent_info.id)) } + if self.next_inscription.is_some() && self.commitment.is_none() { + return Err(anyhow!("--next-file doesn't work without --commitment")); + } + if self.satpoint.is_some() { assert_eq!( self.inscriptions.len(), @@ -235,7 +287,9 @@ impl Batch { ), } - let satpoint = if let Some(satpoint) = self.satpoint { + let satpoint = if self.commitment.is_some() { + SatPoint::from_str("0000000000000000000000000000000000000000000000000000000000000000:0:0")? + } else if let Some(satpoint) = self.satpoint { satpoint } else { let inscribed_utxos = wallet_inscriptions @@ -284,7 +338,9 @@ impl Batch { secp256k1::KeyPair::from_secret_key(&secp256k1, &PrivateKey::from_wif(&self.key.clone().unwrap())?.inner) } else { let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng()); - log::info!("random backup key: {}", PrivateKey::new(key_pair.secret_key(), chain.network()).to_wif()); + if self.commit_only { + eprintln!("use --key {} to reveal this commitment", PrivateKey::new(key_pair.secret_key(), chain.network()).to_wif()); + } key_pair }; let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); @@ -308,9 +364,30 @@ impl Batch { let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), chain.network()); + let reveal_change_address = if self.next_inscription.is_some() { + let next_inscriptions = vec![self.next_inscription.clone().unwrap()]; + let next_reveal_script = Inscription::append_batch_reveal_script( + &next_inscriptions, + ScriptBuf::builder() + .push_slice(public_key.serialize()) + .push_opcode(opcodes::all::OP_CHECKSIG), + ); + + let next_taproot_spend_info = TaprootBuilder::new() + .add_leaf(0, next_reveal_script.clone()) + .expect("adding leaf should work") + .finalize(&secp256k1, public_key) + .expect("finalizing taproot builder should work"); + + Address::p2tr_tweaked(next_taproot_spend_info.output_key(), chain.network()) + } else { + change[0].clone() + }; + let total_postage = self.postage * u64::try_from(self.inscriptions.len()).unwrap(); - let mut reveal_inputs = vec![OutPoint::null()]; + let mut reveal_inputs = self.reveal_input.clone(); + reveal_inputs.insert(0, OutPoint::null()); let mut reveal_outputs = self .destinations .iter() @@ -342,6 +419,13 @@ impl Batch { let commit_input = if self.parent_info.is_some() { 1 } else { 0 }; + if self.commitment.is_some() { + reveal_outputs.push(TxOut { + script_pubkey: reveal_change_address.script_pubkey(), + value: 0, + }); + } + let (_, reveal_fee) = Self::build_reveal_transaction( &control_block, self.reveal_fee_rate, @@ -351,7 +435,15 @@ impl Batch { &reveal_script, ); - let unsigned_commit_tx = TransactionBuilder::new( + let unsigned_commit_tx = if self.commitment.is_some() { + Transaction { + version: 0, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + } + } else { + TransactionBuilder::new( satpoint, wallet_inscriptions, utxos.clone(), @@ -359,20 +451,46 @@ impl Batch { commit_tx_address.clone(), change, self.commit_fee_rate, - Target::Value(reveal_fee + total_postage), - ) - .build_transaction()?; + if self.commit_only { + Target::NoChange(reveal_fee + total_postage) + } else { + Target::Value(reveal_fee + total_postage) + }, + ) + .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"); + let mut reveal_input_value = Amount::from_sat(0); + let mut reveal_input_prevouts = Vec::new(); + for i in &self.reveal_input { + let output = index.get_transaction(i.txid)?.unwrap().output[i.vout as usize].clone(); + reveal_input_value += Amount::from_sat(output.value); + reveal_input_prevouts.push(output.clone()); + utxos.insert(*i, Amount::from_sat(output.value)); + } + + let vout = if self.commitment.is_some() { + reveal_inputs[commit_input] = self.commitment.unwrap(); - reveal_inputs[commit_input] = OutPoint { - txid: unsigned_commit_tx.txid(), - vout: vout.try_into().unwrap(), + if let Some(last) = reveal_outputs.last_mut() { + (*last).value = (reveal_input_value + self.commitment_output.clone().unwrap().value - total_postage - reveal_fee).to_sat(); + } + + 0 + } else { + 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.txid(), + vout: vout.try_into().unwrap(), + }; + + vout }; let (mut reveal_tx, _fee) = Self::build_reveal_transaction( @@ -393,12 +511,23 @@ impl Batch { bail!("commit transaction output would be dust"); } - let mut prevouts = vec![unsigned_commit_tx.output[vout].clone()]; + let mut prevouts = vec![ + if self.commitment.is_some() { + TxOut { + value: self.commitment_output.clone().unwrap().value.to_sat(), + script_pubkey: self.commitment_output.clone().unwrap().script_pub_key.script()? + } + } else { + unsigned_commit_tx.output[vout].clone() + } + ]; if let Some(parent_info) = self.parent_info.clone() { prevouts.insert(0, parent_info.tx_out); } + prevouts.extend(reveal_input_prevouts); + let mut sighash_cache = SighashCache::new(&mut reveal_tx); let sighash = sighash_cache @@ -452,14 +581,26 @@ impl Batch { utxos.insert( reveal_tx.input[commit_input].previous_output, + if self.commitment.is_some() { + self.commitment_output.clone().unwrap().value + } else { Amount::from_sat( unsigned_commit_tx.output[reveal_tx.input[commit_input].previous_output.vout as usize] .value, - ), + ) + }, ); let total_fees = - Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); + if self.commitment.is_some() { + 0 + } else { + Self::calculate_fee(&unsigned_commit_tx, &utxos) + } + if self.commit_only { + 0 + } else { + Self::calculate_fee(&reveal_tx, &utxos) + }; Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) } diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index e9617ffadb..819349e0e4 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -66,6 +66,7 @@ pub enum Target { Value(Amount), Postage, ExactPostage(Amount), + NoChange(Amount), } impl fmt::Display for Error { @@ -293,7 +294,7 @@ impl TransactionBuilder { let min_value = match self.target { Target::Postage => self.outputs.last().unwrap().0.script_pubkey().dust_value(), - Target::Value(value) | Target::ExactPostage(value) => value, + Target::Value(value) | Target::ExactPostage(value) | Target::NoChange(value) => value, }; let total = min_value @@ -353,6 +354,7 @@ impl TransactionBuilder { Target::ExactPostage(postage) => (postage, postage), Target::Postage => (Self::MAX_POSTAGE, Self::TARGET_POSTAGE), Target::Value(value) => (value, value), + Target::NoChange(_) => (excess, excess), }; if excess > max @@ -586,6 +588,12 @@ impl TransactionBuilder { "invariant: output equals target value", ); } + Target::NoChange(value) => { + assert!( + Amount::from_sat(output.value) >= value, + "invariant: output is at least the target amount" + ); + } } assert_eq!( offset, sat_offset, From 544133f7c9836bb360d0d979fe157278451a3746 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 17:03:09 +0000 Subject: [PATCH 022/109] Release 0.11.1-gm5 --- CHANGELOG.md | 12 ++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d00d498738..2138d7b713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ Changelog ========= +[0.11.1-gm5](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm5) - 2023-11-23 +---------------------------------------------------------------------------------- + +### Added +Add `--ignore-cursed` flag to treat all cursed inscriptions as regular inscriptions when indexing. +Add `--ordinals-wallet` flag to `wallet restore` to help with recovering coins from an ordinalswallet seed phrase. +Add some experimental options to allow creating just a commit tx, or just a reveal tx. + +### Changed +Add a max-weight check to the `wallet send-many` command. +Fixed coin selection algorithm (#2723). + [0.11.1-gm4](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm4) - 2023-11-19 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 7684ea2aa5..d367faff88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2036,7 +2036,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.1-gm4" +version = "0.11.1-gm5" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 79452b167e..bb3783cae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.1-gm4" +version = "0.11.1-gm5" license = "CC0-1.0" edition = "2021" autotests = false From e6ea6afb262bfa68b1b59f1ec595068619105022 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 22:57:25 +0000 Subject: [PATCH 023/109] Fixing the merge. --- src/index.rs | 2 -- src/subcommand/server.rs | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/index.rs b/src/index.rs index c9e98ad486..61c83bd7da 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1053,7 +1053,6 @@ impl Index { let mut ret = Vec::new(); let rtx = self.database.begin_read().unwrap(); - let sequence_number_to_inscription_entry = rtx .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY) .unwrap(); @@ -1678,7 +1677,6 @@ impl Index { let mut result = Vec::new(); let rtx = self.database.begin_read().unwrap(); - let sequence_number_to_inscription_entry = rtx .open_table(SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY) .unwrap(); diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 490d3b128d..d032f16373 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -116,8 +116,7 @@ struct Search { struct MyInscriptionJson { number: i32, id: InscriptionId, - // parent: Option, - parent_seq: Option, + parent: Option, address: Option, output_value: Option, sat: Option, @@ -1917,10 +1916,15 @@ impl Server { "" }; + let parent = match entry.parent { + Some(parent) => index.get_inscription_id_by_sequence_number(parent)?, + None => None, + }; + ret.push(MyInscriptionJson { number: i, id: inscription_id, - parent_seq: entry.parent, + parent, address, output_value: if output.is_some() { Some(output.unwrap().value) From d882d6f4f0bb1e8fb944dd57a2debe9ce0fdf1a7 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 23:04:39 +0000 Subject: [PATCH 024/109] Release 0.12.99-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2138d7b713..8f5302a504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.99-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.99-gm1) - 2023-11-23 +------------------------------------------------------------------------------------ + +### Added +Merged upstream master branch. + [0.11.1-gm5](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm5) - 2023-11-23 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index ae5bede8c9..fccaff5967 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2064,7 +2064,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.11.1-gm5" +version = "0.12.99-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index a5e5d63747..3f8116a244 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.11.1-gm5" +version = "0.12.99-gm1" license = "CC0-1.0" edition = "2021" autotests = false From ff8b0119c1591955a20e114b903eb3add5713718 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 23 Nov 2023 23:59:48 +0000 Subject: [PATCH 025/109] Add `--commit` flag to set how often blocks are committed to disk. Default 5000. --- src/index/updater.rs | 3 ++- src/options.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index 900950fd4f..7a43d16221 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -116,7 +116,8 @@ impl<'index> Updater<'_> { uncommitted += 1; - if uncommitted == 5000 { + if uncommitted == self.index.options.commit { + // eprintln!("\ncommitting after {} blocks at {}", uncommitted, self.height); self.commit(wtx, value_cache)?; value_cache = HashMap::new(); uncommitted = 0; diff --git a/src/options.rs b/src/options.rs index fbf2d2ab50..1dc5127506 100644 --- a/src/options.rs +++ b/src/options.rs @@ -67,6 +67,8 @@ pub(crate) struct Options { pub(crate) ignore_outdated_index: bool, #[arg(long, help = "Treat cursed inscriptions as regular inscriptions when indexing. Be consistent; either specify this flag every time you use a given index file or never.")] pub(crate) ignore_cursed: bool, + #[arg(long, default_value = "5000", help = "Commit changes to the index file on disk every blocks.")] + pub(crate) commit: usize, } impl Options { From 1a91575a8ae265f410e483533ba0a0111af76d25 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 26 Nov 2023 15:41:49 +0000 Subject: [PATCH 026/109] Add `--force-input` to `wallet inscribe` and `wallet send`. --- src/subcommand/preview.rs | 1 + src/subcommand/wallet/inscribe.rs | 4 +++- src/subcommand/wallet/inscribe/batch.rs | 4 ++++ src/subcommand/wallet/send.rs | 3 +++ src/subcommand/wallet/transaction_builder.rs | 10 +++++++++- 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index f972391752..e9e3118979 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -89,6 +89,7 @@ impl Preview { destination: None, dry_run: false, fee_rate: FeeRate::try_from(1.0).unwrap(), + force_input: Vec::new(), file: Some(file), json_metadata: None, metaprotocol: None, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 864347f020..35fdea6d45 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -137,6 +137,8 @@ pub(crate) struct Inscribe { pub(crate) dump: bool, #[clap(long, help = "Do not broadcast any transactions. Implies --dump.")] pub(crate) no_broadcast: bool, + #[clap(long, help = "Require this utxo to be spent. Useful for forcing CPFP.")] + pub(crate) force_input: Vec, } impl Inscribe { @@ -286,7 +288,7 @@ impl Inscribe { reveal_input: self.reveal_input, satpoint: self.satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, &utxos) + .inscribe(chain, &index, &client, &locked_utxos, &utxos, self.force_input) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index b04798f663..f7bca2fc25 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -58,6 +58,7 @@ impl Batch { client: &Client, locked_utxos: &BTreeSet, utxos: &BTreeMap, + force_input: Vec, ) -> SubcommandResult { let wallet_inscriptions = index.get_inscriptions(utxos)?; @@ -74,6 +75,7 @@ impl Batch { locked_utxos.clone(), utxos.clone(), commit_tx_change, + force_input, )?; if self.dry_run { @@ -254,6 +256,7 @@ impl Batch { locked_utxos: BTreeSet, mut utxos: BTreeMap, change: [Address; 2], + force_input: Vec, ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { if let Some(parent_info) = &self.parent_info { assert!(self @@ -456,6 +459,7 @@ impl Batch { } else { Target::Value(reveal_fee + total_postage) }, + force_input, ) .build_transaction()? }; diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 073c4832ac..4220adca0c 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -21,6 +21,8 @@ pub(crate) struct Send { help = "Target amount of postage to include with sent inscriptions. Default `10000sat`" )] pub(crate) postage: Option, + #[clap(long, help = "Require this utxo to be spent. Useful for forcing CPFP.")] + pub(crate) force_input: Vec, } #[derive(Serialize, Deserialize)] @@ -107,6 +109,7 @@ impl Send { change, self.fee_rate, postage, + self.force_input, ) .build_transaction()?; diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 819349e0e4..9be5048b28 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -103,6 +103,7 @@ pub struct TransactionBuilder { amounts: BTreeMap, change_addresses: BTreeSet
, fee_rate: FeeRate, + force_input: Vec, inputs: Vec, inscriptions: BTreeMap, outgoing: SatPoint, @@ -132,6 +133,7 @@ impl TransactionBuilder { change: [Address; 2], fee_rate: FeeRate, target: Target, + force_input: Vec, ) -> Self { Self { utxos: amounts.keys().cloned().collect(), @@ -140,6 +142,7 @@ impl TransactionBuilder { change_addresses: change.iter().cloned().collect(), fee_rate, inputs: Vec::new(), + force_input: force_input, inscriptions, outgoing, outputs: Vec::new(), @@ -206,7 +209,7 @@ impl TransactionBuilder { } } - let amount = *self + let mut amount = *self .amounts .get(&self.outgoing.outpoint) .ok_or(Error::NotInWallet(self.outgoing))?; @@ -217,6 +220,11 @@ impl TransactionBuilder { self.utxos.remove(&self.outgoing.outpoint); self.inputs.push(self.outgoing.outpoint); + for input in &self.force_input { + self.inputs.push(*input); + amount += *self.amounts.get(&input).unwrap(); + self.utxos.remove(&input); + } self.outputs.push((self.recipient.clone(), amount)); tprintln!( From a174247bed161fd8c3b70ea3ef07ad08723233a5 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 26 Nov 2023 19:06:37 +0000 Subject: [PATCH 027/109] Add the sequence_number to the /inscriptions/json/ endpoint. --- src/subcommand/server.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index bf6d5daaed..72c588610d 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -115,6 +115,7 @@ struct Search { #[derive(Serialize)] struct MyInscriptionJson { number: i32, + sequence_number: u32, id: InscriptionId, parent: Option, address: Option, @@ -1950,6 +1951,7 @@ impl Server { ret.push(MyInscriptionJson { number: i, + sequence_number, id: inscription_id, parent, address, From 0da07b00e0ba6168d8d5065799135c84f6c2e070 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 26 Nov 2023 19:18:56 +0000 Subject: [PATCH 028/109] Rename `--force-input` to `--commit-input` for the `wallet inscribe` subcommand so it matches `--reveal-input`. --- src/subcommand/preview.rs | 2 +- src/subcommand/wallet/inscribe.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index e9e3118979..25864d9ac9 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -85,11 +85,11 @@ impl Preview { utxo: Vec::new(), coin_control: false, commit_fee_rate: None, + commit_input: Vec::new(), compress: false, destination: None, dry_run: false, fee_rate: FeeRate::try_from(1.0).unwrap(), - force_input: Vec::new(), file: Some(file), json_metadata: None, metaprotocol: None, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 35fdea6d45..e224a690cc 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -137,8 +137,8 @@ pub(crate) struct Inscribe { pub(crate) dump: bool, #[clap(long, help = "Do not broadcast any transactions. Implies --dump.")] pub(crate) no_broadcast: bool, - #[clap(long, help = "Require this utxo to be spent. Useful for forcing CPFP.")] - pub(crate) force_input: Vec, + #[clap(long, help = "Use as an extra input to the commit tx. Useful for forcing CPFP.")] + pub(crate) commit_input: Vec, } impl Inscribe { @@ -288,7 +288,7 @@ impl Inscribe { reveal_input: self.reveal_input, satpoint: self.satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, &utxos, self.force_input) + .inscribe(chain, &index, &client, &locked_utxos, &utxos, self.commit_input) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { From 7db49baecad591a24e5dd693250bb4bb49c3daee Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 26 Nov 2023 19:20:52 +0000 Subject: [PATCH 029/109] Release 0.12.0-gm2 --- CHANGELOG.md | 7 +++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9b879a14f..b484882379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +[0.12.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm2) - 2023-11-26 +---------------------------------------------------------------------------------- + +### Added +Add the sequence_number to the /inscriptions/json/ endpoint. +Add `--commit-input` to `wallet inscribe` and `--force-input` to `wallet send`. + [0.12.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm1) - 2023-11-25 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 2b562cdd64..84704d7c11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2064,7 +2064,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.0-gm1" +version = "0.12.0-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0702f70aeb..8eb15683bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.0-gm1" +version = "0.12.0-gm2" license = "CC0-1.0" edition = "2021" autotests = false From 0738434ded67c685fe6fa519ee3ec6f7ca822abe Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 27 Nov 2023 22:46:47 +0000 Subject: [PATCH 030/109] Add endpoint `/inscriptions_sequence_numbers/:start/:end` to get the mapping from inscription number to sequence number. --- src/index.rs | 17 +++++++++++++++++ src/subcommand/server.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/index.rs b/src/index.rs index 61c83bd7da..f48a09ac0e 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1182,6 +1182,23 @@ impl Index { ) } + pub(crate) fn get_sequence_number_by_inscription_number( + &self, + inscription_number: i32, + ) -> Result { + 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 Err(anyhow!("no inscription number {inscription_number}")); + }; + + Ok(sequence_number) + } + pub(crate) fn get_inscription_id_by_inscription_number( &self, inscription_number: i32, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 72c588610d..3c56e34343 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -294,6 +294,10 @@ impl Server { "/inscriptions_json/:start/:end", get(Self::inscriptions_json_start_end), ) + .route( + "/inscriptions_sequence_numbers/:start/:end", + get(Self::inscriptions_sequence_numbers), + ) .route("/install.sh", get(Self::install_script)) .route("/ordinal/:sat", get(Self::ordinal)) .route("/output/:output", get(Self::output)) @@ -1987,6 +1991,34 @@ impl Server { } } + async fn inscriptions_sequence_numbers( + Extension(index): Extension>, + Path(path): Path<(i32, i32)>, + ) -> ServerResult { + log::info!("GET /inscriptions_sequence_numbers/{}/{}", path.0, path.1); + + let start = path.0; + let end = path.1; + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + let mut ret = String::new(); + + for i in start..end { + sleep(Duration::from_millis(0)).await; + match index.get_sequence_number_by_inscription_number(i) { + Err(_) => return Err(ServerError::BadRequest(format!("no inscription {i}"))), + Ok(sequence_number) => ret += format!("{i},{sequence_number}\n").as_str(), + } + } + + Ok(ret) + } + } + } + async fn sat_inscriptions( Extension(index): Extension>, Path(sat): Path, From 91df91714f3d6f9a40cc5136dae2c7695d48284a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 27 Nov 2023 22:49:08 +0000 Subject: [PATCH 031/109] Release 0.12.0-gm3 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b484882379..ce6c834eb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.0-gm3](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm3) - 2023-11-27 +---------------------------------------------------------------------------------- + +### Added +Add endpoint `/inscriptions_sequence_numbers/:start/:end` to get the mapping from inscription number to sequence number. + [0.12.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm2) - 2023-11-26 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 84704d7c11..475276545e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2064,7 +2064,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.0-gm2" +version = "0.12.0-gm3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 8eb15683bc..dc22acd49c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.0-gm2" +version = "0.12.0-gm3" license = "CC0-1.0" edition = "2021" autotests = false From bfb26e9f8d752599399abd0bd55da059408a0059 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 1 Dec 2023 00:15:57 +0000 Subject: [PATCH 032/109] Release 0.12.1-gm1 --- CHANGELOG.md | 32 +++++++++++++++++++------------- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd2eca8cc4..731b09dc3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.1-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.1-gm1) - 2023-11-30 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.12.1 from upstream. + [0.12.1](https://github.com/ordinals/ord/releases/tag/0.12.1) - 2023-11-29 -------------------------------------------------------------------------- @@ -23,46 +29,46 @@ Changelog ---------------------------------------------------------------------------------- ### Added -Add endpoint `/inscriptions_sequence_numbers/:start/:end` to get the mapping from inscription number to sequence number. +- Add endpoint `/inscriptions_sequence_numbers/:start/:end` to get the mapping from inscription number to sequence number. [0.12.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm2) - 2023-11-26 ---------------------------------------------------------------------------------- ### Added -Add the sequence_number to the /inscriptions/json/ endpoint. -Add `--commit-input` to `wallet inscribe` and `--force-input` to `wallet send`. +- Add the sequence_number to the /inscriptions/json/ endpoint. +- Add `--commit-input` to `wallet inscribe` and `--force-input` to `wallet send`. [0.12.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.0-gm1) - 2023-11-25 ---------------------------------------------------------------------------------- ### Added -Merged upstream 0.12.0 release. +- Merged upstream 0.12.0 release. [0.11.1-gm5](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm5) - 2023-11-23 ---------------------------------------------------------------------------------- ### Added -Add `--ignore-cursed` flag to treat all cursed inscriptions as regular inscriptions when indexing. -Add `--ordinals-wallet` flag to `wallet restore` to help with recovering coins from an ordinalswallet seed phrase. -Add some experimental options to allow creating just a commit tx, or just a reveal tx. +- Add `--ignore-cursed` flag to treat all cursed inscriptions as regular inscriptions when indexing. +- Add `--ordinals-wallet` flag to `wallet restore` to help with recovering coins from an ordinalswallet seed phrase. +- Add some experimental options to allow creating just a commit tx, or just a reveal tx. ### Changed -Add a max-weight check to the `wallet send-many` command. -Fixed coin selection algorithm (#2723). +- Add a max-weight check to the `wallet send-many` command. +- Fixed coin selection algorithm (#2723). [0.11.1-gm4](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm4) - 2023-11-19 ---------------------------------------------------------------------------------- ### Added -Add `--dump` and `--no-broadcast` flags to `wallet inscribe`. -Add `wallet send-many` to allow sending multiple inscriptions in a single command. +- Add `--dump` and `--no-broadcast` flags to `wallet inscribe`. +- Add `wallet send-many` to allow sending multiple inscriptions in a single command. [0.11.1-gm3](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm3) - 2023-11-15 ---------------------------------------------------------------------------------- ### Added -Add `--key` flag to `wallet inscribe` to allow using a specific recovery key. -Add `--ignore-outdated-index` flag to allow ord to run without having to fully index the blockchain. Be careful. Inscriptions that haven't been indexed will be treated as if they are cardinals, and so can be accidentally sent to spent as fees. +- Add `--key` flag to `wallet inscribe` to allow using a specific recovery key. +- Add `--ignore-outdated-index` flag to allow ord to run without having to fully index the blockchain. Be careful. Inscriptions that haven't been indexed will be treated as if they are cardinals, and so can be accidentally sent to spent as fees. [0.11.1-gm2](https://github.com/gmart7t2/ord/releases/tag/0.11.1-gm2) - 2023-11-14 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 408af4bc1c..0ad93a7616 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,7 +2065,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.1" +version = "0.12.1-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 137c96bc4c..549be6b169 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.1" +version = "0.12.1-gm1" license = "CC0-1.0" edition = "2021" autotests = false From 44c664f7f91dfe74aa1dc160bb7a4028913dcafc Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 1 Dec 2023 00:21:51 +0000 Subject: [PATCH 033/109] Release 0.12.2-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db0513b865..de1f75a763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.2-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.2-gm1) - 2023-11-30 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.12.2 from upstream. + [0.12.2](https://github.com/ordinals/ord/releases/tag/0.12.2) - 2023-11-29 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 7707d87531..91a54dc327 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2065,7 +2065,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.2" +version = "0.12.2-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 1521e1d377..06b43a5eb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.2" +version = "0.12.2-gm1" license = "CC0-1.0" edition = "2021" autotests = false From 379a3620b7c21f855111abad5332e51049e092df Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 1 Dec 2023 01:08:03 +0000 Subject: [PATCH 034/109] Fix issue with the --key getting backed up to the wallet and breaking the reveal tx witnesses. --- src/subcommand/wallet/inscribe.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index fd8b51c017..0f24482913 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -125,9 +125,9 @@ pub(crate) struct Inscribe { pub(crate) satpoint: Option, #[clap(long, help = "Use provided recovery key instead of a random one.")] pub(crate) key: Option, - #[clap(long, help = "Don't make a reveal tx; just create a commit tx that sends all the sats to a new commitment. Requires --key to be specified.")] + #[clap(long, help = "Don't make a reveal tx; just create a commit tx that sends all the sats to a new commitment. Either specify --key if you have one, or note the --key it generates for you. Implies --no-backup.")] pub(crate) commit_only: bool, - #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires --key to be specified.")] + #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires the same --key as was used to make the commitment. Implies --no-backup. This doesn't work if the --key has ever been backed up to the wallet.")] pub(crate) commitment: Option, #[clap(long, help = "Make the change of the reveal tx commit to the contents of .")] pub(crate) next_file: Option, @@ -161,6 +161,11 @@ impl Inscribe { return Err(anyhow!("--reveal-input only works with --commitment")); } + let mut no_backup = self.no_backup; + if self.commit_only || self.commitment.is_some() { + no_backup = true; + } + let mut dump = self.dump; let metadata = Inscribe::parse_metadata(self.cbor_metadata, self.json_metadata)?; @@ -307,7 +312,7 @@ impl Inscribe { key: self.key, mode, next_inscription, - no_backup: self.no_backup, + no_backup, no_broadcast: self.no_broadcast, no_limit: self.no_limit, parent_info, From fd595be11cb4cfee668ca517543db8ffb1573810 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 2 Dec 2023 02:53:25 +0000 Subject: [PATCH 035/109] Add a bunch of new flags to `ord wallet send-many`. --- src/subcommand/wallet/sendmany.rs | 160 ++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 33 deletions(-) diff --git a/src/subcommand/wallet/sendmany.rs b/src/subcommand/wallet/sendmany.rs index 556053c3c2..cd93f0a019 100644 --- a/src/subcommand/wallet/sendmany.rs +++ b/src/subcommand/wallet/sendmany.rs @@ -25,6 +25,16 @@ pub(crate) struct SendMany { #[arg(long, help = "Do not check that the transaction is equal to or below the MAX_STANDARD_TX_WEIGHT of 400,000 weight units. Transactions over this limit are currently 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, + #[arg(long, help = "By default it is an error to list only some of the inscriptions in an output. This flag allows you to not care about the inscriptions you don't list in the CVS file.")] + pub(crate) ignore_unlisted: bool, + #[arg(long, help = "The smallest amount to use for each inscription output.")] + pub(crate) min_postage: Option, + #[arg(long, help = "The largest amount to use for each inscription output.")] + pub(crate) max_postage: Option, + #[arg(long, help = "The address to send cardinal outputs to.")] + pub(crate) change: Option>, + #[arg(long, help = "Which cardinal to use to pay the fees.")] + pub(crate) cardinal: Option, } #[derive(Serialize, Deserialize)] @@ -43,6 +53,10 @@ impl SendMany { let chain = options.chain(); + if self.min_postage.is_some() && self.max_postage.is_some() && self.min_postage.unwrap() > self.max_postage.unwrap() { + bail!("--min-postage {} sats is bigger than --max-postage {} sats", self.min_postage.unwrap().to_sat(), self.max_postage.unwrap().to_sat()); + } + for line in reader.lines() { let line = line?; let mut line = line.trim_start_matches('\u{feff}').split(','); @@ -107,9 +121,13 @@ impl SendMany { requested_satpoints.insert(satpoint, (inscriptionid.clone(), address.clone())); } + let change_dust_limit = Self::get_change_pubkey(&client, chain, self.change.clone())?.dust_value().to_sat(); + + let mut cardinal_value = 0; // this loop handles the inscriptions in order of offset in each utxo while !requested.is_empty() { - let mut inscriptions_on_outpoint = Vec::new(); + let mut inscriptions_on_outpoint; + let mut inscriptions_to_send = Vec::new(); // pick the first remaining inscriptionid from the list for (inscriptionid, _address) in &requested { // look up which utxo it's in @@ -118,45 +136,87 @@ impl SendMany { inscriptions_on_outpoint = index.get_inscriptions_on_output_with_satpoints(outpoint)?; // sort it by offset inscriptions_on_outpoint.sort_by_key(|(s, _)| s.offset); - // make sure that they are all in the csv file + // make sure that they are all in the csv file, unless --ignore-unlisted is in effect for (satpoint, outpoint_inscriptionid) in &inscriptions_on_outpoint { - if !requested_satpoints.contains_key(&satpoint) { - bail!("inscriptionid {} is in the same output as {} but wasn't in the CSV file", outpoint_inscriptionid.to_string(), inscriptionid.to_string()); + if self.ignore_unlisted { + if requested_satpoints.contains_key(&satpoint) { + inscriptions_to_send.push((satpoint, outpoint_inscriptionid)); + } + } else { + if !requested_satpoints.contains_key(&satpoint) { + bail!("inscriptionid {} is in the same output as {} but wasn't in the CSV file", outpoint_inscriptionid.to_string(), inscriptionid.to_string()); + } + inscriptions_to_send.push((satpoint, outpoint_inscriptionid)); } } break; } // create an input for the first inscription of each utxo - let (first_satpoint, _first_inscription) = inscriptions_on_outpoint[0]; + let (first_satpoint, _first_inscription) = inscriptions_to_send[0]; let first_offset = first_satpoint.offset; let first_outpoint = first_satpoint.outpoint; let utxo_value = unspent_outputs[&first_outpoint].to_sat(); if first_offset != 0 { - bail!("the first inscription in {} is at non-zero offset {}", first_outpoint, first_offset); + cardinal_value += first_offset } inputs.push(first_outpoint); // filter out the inscriptions that aren't in our list, but are still to be sent - these are inscriptions that are on the same sat as the ones we listed // we want to remove just the ones where the satpoint is requested but that particular inscriptionid isn't // ie. keep the ones where the satpoint isn't requested or the inscriptionid is - inscriptions_on_outpoint = inscriptions_on_outpoint.into_iter().filter( + inscriptions_to_send = inscriptions_to_send.into_iter().filter( |(satpoint, inscriptionid)| !requested_satpoints.contains_key(&satpoint) || requested.contains_key(&inscriptionid) ).collect(); // create an output for each inscription in this utxo - for (i, (satpoint, inscriptionid)) in inscriptions_on_outpoint.iter().enumerate() { + for (i, (satpoint, inscriptionid)) in inscriptions_to_send.iter().enumerate() { + if cardinal_value != 0 { + outputs.push(TxOut{ + script_pubkey: Self::get_change_pubkey(&client, chain, self.change.clone())?, + value: cardinal_value + }); + cardinal_value = 0; + } + let destination = &requested_satpoints[&satpoint].1; let offset = satpoint.offset; - let value = if i == inscriptions_on_outpoint.len() - 1 { + let mut value = if i == inscriptions_to_send.len() - 1 { // if this is the last inscription in the output, use all the remaining sats utxo_value - offset - } else { - inscriptions_on_outpoint[i + 1].0.offset - offset + } else { // else use the sats up to the next inscription + inscriptions_to_send[i + 1].0.offset - offset }; + let script_pubkey = destination.script_pubkey(); let dust_limit = script_pubkey.dust_value().to_sat(); + + if let Some(min_postage) = self.min_postage { + if value < min_postage.to_sat() { + bail!("inscription {} at {} is only followed by {} sats, less than the specified --min-postage of {} sats", + inscriptionid, satpoint.to_string(), value, min_postage.to_sat()); + } + } + + if let Some(max_postage) = self.max_postage { + if value > max_postage.to_sat() { + if value - max_postage.to_sat() >= change_dust_limit { // if using the max-postage size would leave a big enough change, do that + cardinal_value = value - max_postage.to_sat(); + value -= cardinal_value; + } else { // otherwise leave a big enough change + cardinal_value = change_dust_limit; + value -= cardinal_value; + + if let Some(min_postage) = self.min_postage { + if value < min_postage.to_sat() { + bail!("trimming inscription {} at {} output of size {} sats so it doesn't exceed --max-postage {} sats leaves it smaller than --min-postage of {} sats", + inscriptionid, satpoint.to_string(), value, min_postage.to_sat(), max_postage.to_sat()); + } + } + } + } + } if value < dust_limit { - bail!("inscription {} at {} is only followed by {} sats, less than dust limit {} for address {}", + bail!("inscription {} at {} would only have size {} sats, less than dust limit {} for address {}", inscriptionid, satpoint.to_string(), value, dust_limit, destination); } outputs.push(TxOut{script_pubkey, value}); @@ -166,26 +226,11 @@ impl SendMany { } } - // get a list of available unlocked cardinals - let cardinals = Self::get_cardinals(unspent_outputs, locked_outputs, inscriptions); - - if cardinals.is_empty() { - bail!("wallet has no cardinals"); - } - - // select the biggest cardinal - this could be improved by figuring out what size we need, and picking the next biggest for example - let (cardinal_outpoint, cardinal_value) = cardinals[0]; - - // use the biggest cardinal as the last input - inputs.push(cardinal_outpoint); - - let change_address = get_change_address(&client, chain)?; - let script_pubkey = change_address.script_pubkey(); - let dust_limit = script_pubkey.dust_value().to_sat(); + let script_pubkey = Self::get_change_pubkey(&client, chain, self.change.clone())?; let value = 0; // we don't know how much change to take until we know the fee, which means knowing the tx vsize outputs.push(TxOut{script_pubkey: script_pubkey.clone(), value}); - // calculate the size of the tx once it is signed + // calculate the size of the tx without an extra cardinal input once it is signed let fake_tx = Self::build_fake_transaction(&inputs, &outputs); let weight = fake_tx.weight(); if !self.no_limit && weight > bitcoin::Weight::from_wu(MAX_STANDARD_TX_WEIGHT.into()) { @@ -194,12 +239,50 @@ impl SendMany { ); } let fee = self.fee_rate.fee(fake_tx.vsize()).to_sat(); - let needed = fee + dust_limit; + let needed = fee + change_dust_limit; + let value; if cardinal_value < needed { - bail!("cardinal {} ({} sats) is too small\n we need enough for fee {} plus dust limit {} = {} sats", - cardinal_outpoint.to_string(), cardinal_value, fee, dust_limit, needed); + // eprintln!("left over amount ({} sats) is too small\n we need enough for fee {} plus dust limit {} = {} sats", cardinal_value, fee, change_dust_limit, needed); + + let (cardinal_outpoint, new_cardinal_value) = match self.cardinal { + Some(cardinal) => (cardinal, unspent_outputs[&cardinal].to_sat()), + None => { + // select the biggest cardinal - this could be improved by figuring out what size we need, and picking the next biggest for example + // get a list of available unlocked cardinals + let cardinals = Self::get_cardinals(unspent_outputs.clone(), locked_outputs, inscriptions); + + if cardinals.is_empty() { + bail!("wallet has no cardinals"); + } + + cardinals[0] + } + }; + + // eprintln!("we have {} left over, and {} in the biggest cardinal", cardinal_value, new_cardinal_value); + + // use the biggest cardinal as the last input + inputs.push(cardinal_outpoint); + + // calculate the size of the tx once it is signed + let fake_tx = Self::build_fake_transaction(&inputs, &outputs); + let weight = fake_tx.weight(); + if !self.no_limit && weight > bitcoin::Weight::from_wu(MAX_STANDARD_TX_WEIGHT.into()) { + bail!( + "transaction weight greater than {MAX_STANDARD_TX_WEIGHT} (MAX_STANDARD_TX_WEIGHT): {weight}" + ); + } + let fee = self.fee_rate.fee(fake_tx.vsize()).to_sat(); + let needed = fee + change_dust_limit; + if cardinal_value + new_cardinal_value < needed { + bail!("cardinal {} ({} sats) is too small\n we need enough for fee {} plus dust limit {} = {} sats", + cardinal_outpoint.to_string(), new_cardinal_value, fee, change_dust_limit, needed - cardinal_value); + } + value = cardinal_value + new_cardinal_value - fee; + } else { + value = cardinal_value - fee; } - let value = cardinal_value - fee; + let last = outputs.len() - 1; outputs[last] = TxOut{script_pubkey, value}; @@ -216,6 +299,17 @@ impl SendMany { } } + fn get_change_pubkey( + client: &Client, + chain: Chain, + change: Option>, + ) -> Result { + Ok(match change { + Some(change) => change.require_network(chain.network()).unwrap(), + None => get_change_address(&client, chain)?, + }.script_pubkey()) + } + fn get_cardinals( unspent_outputs: BTreeMap, locked_outputs: BTreeSet, From c8121b3ea84e751a58fdcde618cec002da29226d Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 2 Dec 2023 04:10:25 +0000 Subject: [PATCH 036/109] Release 0.12.3-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c9b54f11..fc76f3dc66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.3-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.3-gm1) - 2023-12-02 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.12.3 from upstream. + [0.12.3](https://github.com/ordinals/ord/releases/tag/0.12.3) - 2023-12-01 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 1f0a8f86fc..b1f26493f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,7 +2084,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.3" +version = "0.12.3-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 21574b66fc..8be49e3db1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.3" +version = "0.12.3-gm1" license = "CC0-1.0" edition = "2021" autotests = false From 148e3b2ee5ff2653405fbf7d2c7b675e2a312b7f Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 2 Dec 2023 16:51:27 +0000 Subject: [PATCH 037/109] HTML endpoint /inscriptions/block// returns no inscriptions past page 0. --- src/subcommand/server.rs | 10 +++++----- src/templates/inscriptions_block.rs | 9 ++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 0452c231f7..e8532bae45 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1453,13 +1453,13 @@ impl Server { .take(page_size.saturating_add(1)) .collect::>(); - let more = inscriptions.len() > page_size; + Ok(if accept_json.0 { + let more = inscriptions.len() > page_size; - if more { - inscriptions.pop(); - } + if more { + inscriptions.pop(); + } - Ok(if accept_json.0 { Json(InscriptionsJson { inscriptions, page_index, diff --git a/src/templates/inscriptions_block.rs b/src/templates/inscriptions_block.rs index 3b95f82078..ed899640e0 100644 --- a/src/templates/inscriptions_block.rs +++ b/src/templates/inscriptions_block.rs @@ -19,13 +19,12 @@ impl InscriptionsBlockHtml { ) -> Result { let num_inscriptions = inscriptions.len(); - let start = page_index * 100; - let end = usize::min(start + 100, num_inscriptions); + let end = usize::min(100, num_inscriptions); - if start > num_inscriptions || start > end { + if inscriptions.is_empty() { return Err(anyhow!("page index {page_index} exceeds inscription count")); } - let inscriptions = inscriptions[start..end].to_vec(); + let inscriptions = inscriptions[..end].to_vec(); Ok(Self { block, @@ -41,7 +40,7 @@ impl InscriptionsBlockHtml { } else { None }, - next_page: if (page_index + 1) * 100 <= num_inscriptions { + next_page: if num_inscriptions > 100 { Some(page_index + 1) } else { None From a79757aab75723cae143748f80993376983a2ecf Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 2 Dec 2023 17:08:05 +0000 Subject: [PATCH 038/109] Release 0.12.3-gm2 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc76f3dc66..b0202f1e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.3-gm2](https://github.com/gmart7t2/ord/releases/tag/0.12.3-gm2) - 2023-12-02 +---------------------------------------------------------------------------------- + +### Fixed +- HTML endpoint /inscriptions/block// was returning no inscriptions past page 0. + [0.12.3-gm1](https://github.com/gmart7t2/ord/releases/tag/0.12.3-gm1) - 2023-12-02 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index b1f26493f3..ede510e2cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,7 +2084,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.3-gm1" +version = "0.12.3-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 8be49e3db1..1e212d5ce1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.3-gm1" +version = "0.12.3-gm2" license = "CC0-1.0" edition = "2021" autotests = false From dc7291640d44c9ae38dcf5075ec673367d64bd32 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 9 Dec 2023 14:04:39 +0000 Subject: [PATCH 039/109] Fix merge. --- src/subcommand/server.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 072b8cca39..4d88bcf44c 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1806,13 +1806,13 @@ impl Server { .take(page_size.saturating_add(1)) .collect::>(); - Ok(if accept_json.0 { - let more = inscriptions.len() > page_size; + let more = inscriptions.len() > page_size; - if more { - inscriptions.pop(); - } + if more { + inscriptions.pop(); + } + Ok(if accept_json.0 { Json(InscriptionsJson { inscriptions, page_index, From d54efabb3331c46cef26a93c8825036bd5ab5a89 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 11 Dec 2023 15:32:03 +0000 Subject: [PATCH 040/109] Add option `--index-transfers` to have the index track which inscriptions are transferred in each block. Previously this was already enabled, using up space in the index whether it was needed or not. --- src/index.rs | 2 ++ src/index/updater.rs | 6 +++++- src/index/updater/inscription_updater.rs | 8 ++++---- src/options.rs | 2 ++ 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/index.rs b/src/index.rs index 2346d957dd..de574e87c9 100644 --- a/src/index.rs +++ b/src/index.rs @@ -180,6 +180,7 @@ pub(crate) struct Index { height_limit: Option, index_runes: bool, index_sats: bool, + index_transfers: bool, no_progress_bar: bool, options: Options, path: PathBuf, @@ -345,6 +346,7 @@ impl Index { first_inscription_height: options.first_inscription_height(), genesis_block_coinbase_transaction, height_limit: options.height_limit, + index_transfers: options.index_transfers, no_progress_bar: options.no_progress_bar, options: options.clone(), index_runes, diff --git a/src/index/updater.rs b/src/index/updater.rs index 3afb5232f5..c2a1cc34cb 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -383,7 +383,11 @@ impl<'index> Updater<'_> { } let mut height_to_block_hash = wtx.open_table(HEIGHT_TO_BLOCK_HASH)?; - let mut height_to_sequence_number = wtx.open_multimap_table(HEIGHT_TO_SEQUENCE_NUMBER)?; + let mut height_to_sequence_number = if index.index_transfers { + Some(wtx.open_multimap_table(HEIGHT_TO_SEQUENCE_NUMBER)?) + } else { + None + }; 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_id_to_sequence_number = diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 3a3a577681..d9313895ab 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -42,7 +42,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) cursed_inscription_count: u64, pub(super) flotsam: Vec, pub(super) height: u32, - pub(super) height_to_sequence_number: &'a mut MultimapTable<'db, 'tx, u32, u32>, + pub(super) height_to_sequence_number: &'a mut Option>, pub(super) home_inscription_count: u64, pub(super) home_inscriptions: &'a mut Table<'db, 'tx, u32, InscriptionIdValue>, pub(super) id_to_sequence_number: &'a mut Table<'db, 'tx, InscriptionIdValue, u32>, @@ -371,9 +371,9 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .get(&inscription_id.store())? .unwrap() .value(); - self - .height_to_sequence_number - .insert(&self.height, &sequence_number)?; + if let Some(height_to_sequence_number) = &mut self.height_to_sequence_number { + height_to_sequence_number.insert(&self.height, &sequence_number)?; + } self .satpoint_to_sequence_number .remove_all(&old_satpoint.store())?; diff --git a/src/options.rs b/src/options.rs index b1ba988ba2..e1ea4a0579 100644 --- a/src/options.rs +++ b/src/options.rs @@ -49,6 +49,8 @@ pub(crate) struct Options { pub(crate) index_runes_pre_alpha_i_agree_to_get_rekt: bool, #[arg(long, help = "Track location of all satoshis.")] pub(crate) index_sats: bool, + #[clap(long, help = "Track transfers of inscriptions.")] + pub(crate) index_transfers: bool, #[arg(long, help = "Inhibit the display of the progress bar while updating the index.")] pub(crate) no_progress_bar: bool, #[arg(long, short, help = "Use regtest. Equivalent to `--chain regtest`.")] From 469adf39a7aa6e5652b015a2670447320774f43b Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 11 Dec 2023 15:33:24 +0000 Subject: [PATCH 041/109] Release 0.12.3-gm3 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0202f1e78..2e7d4183e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.12.3-gm3](https://github.com/gmart7t2/ord/releases/tag/0.12.3-gm3) - 2023-12-11 +---------------------------------------------------------------------------------- + +### Added +- Add option `--index-transfers` to have the index track which inscriptions are transferred in each block. Previously this was already enabled, using up space in the index whether it was needed or not. + [0.12.3-gm2](https://github.com/gmart7t2/ord/releases/tag/0.12.3-gm2) - 2023-12-02 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index ede510e2cd..cd3e06b5c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2084,7 +2084,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.3-gm2" +version = "0.12.3-gm3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 1e212d5ce1..7532012745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.12.3-gm2" +version = "0.12.3-gm3" license = "CC0-1.0" edition = "2021" autotests = false From 6442ad783c8df3591ba8f701e582882d22abb6c9 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 12 Dec 2023 16:38:09 +0000 Subject: [PATCH 042/109] Use `--change` flag to specify the change address for `wallet inscribe` and `wallet send`. --- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 9 ++++++++- src/subcommand/wallet/inscribe/batch.rs | 6 +++++- src/subcommand/wallet/send.rs | 12 +++++++++++- src/subcommand/wallet/transaction_builder.rs | 10 ++-------- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 0d9aba45dc..d12e79c359 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -105,6 +105,7 @@ impl Preview { super::wallet::inscribe::Inscribe { batch: None, cbor_metadata: None, + change: None, coin_control: false, commit_fee_rate: None, commit_input: Vec::new(), @@ -147,6 +148,7 @@ impl Preview { super::wallet::inscribe::Inscribe { batch: Some(batch), cbor_metadata: None, + change: None, coin_control: false, commit_fee_rate: None, commit_input: Vec::new(), diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b2a9cbf27d..dfd9f183f1 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -82,6 +82,8 @@ pub(crate) struct Inscribe { pub(crate) utxo: Vec, #[arg(long, help = "Only spend outpoints given with --utxo")] pub(crate) coin_control: bool, + #[arg(long, help = "Send any change output to .")] + pub(crate) change: Option>, #[arg( long, help = "Use sats/vbyte for commit transaction.\nDefaults to if unset." @@ -205,6 +207,11 @@ impl Inscribe { let chain = options.chain(); + let change = match self.change { + Some(change) => Some(change.require_network(chain.network())?), + None => None, + }; + let postage; let destinations; let inscriptions; @@ -322,7 +329,7 @@ impl Inscribe { reveal_input: self.reveal_input, satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &utxos, self.commit_input) + .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &utxos, self.commit_input, change) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index ba17a5816f..b0c4c5f43e 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -60,12 +60,16 @@ impl Batch { runic_utxos: BTreeSet, utxos: &BTreeMap, force_input: Vec, + change: Option
, ) -> SubcommandResult { let wallet_inscriptions = index.get_inscriptions(utxos)?; let commit_tx_change = [ get_change_address(client, chain)?, - get_change_address(client, chain)?, + match change { + Some(change) => change, + None => get_change_address(client, chain)?, + }, ]; let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index e15388ec64..b21c9ad563 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -14,6 +14,8 @@ pub(crate) struct Send { help = "Only spend outpoints given with --utxo when sending inscriptions or satpoints" )] pub(crate) coin_control: bool, + #[arg(long, help = "Send any change output to .")] + pub(crate) change: Option>, #[arg(long, help = "Use fee rate of sats/vB")] fee_rate: FeeRate, #[arg( @@ -102,9 +104,17 @@ impl Send { } }; + let change = match self.change { + Some(change) => Some(change.require_network(chain.network())?), + None => None, + }; + let change = [ get_change_address(&client, chain)?, - get_change_address(&client, chain)?, + match change { + Some(change) => change, + None => get_change_address(&client, chain)?, + }, ]; let postage = if let Some(postage) = self.postage { diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 61411f7962..f1cf5b48a6 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -252,10 +252,7 @@ impl TransactionBuilder { self.outputs.insert( 0, ( - self - .unused_change_addresses - .pop() - .expect("not enough change addresses"), + self.unused_change_addresses[0].clone(), Amount::from_sat(sat_offset), ), ); @@ -379,10 +376,7 @@ impl TransactionBuilder { tprintln!("stripped {} sats", (value - target).to_sat()); self.outputs.last_mut().expect("no outputs found").1 = target; self.outputs.push(( - self - .unused_change_addresses - .pop() - .expect("not enough change addresses"), + self.unused_change_addresses[1].clone(), value - target, )); } From 7db93beeb4e478ef686e0ec94e1b663e21203eda Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 17 Dec 2023 00:32:29 +0000 Subject: [PATCH 043/109] Release 0.13.1-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bc6f97071..f34ec5c818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.13.1-gm1](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm1) - 2023-12-16 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.13.1 from upstream. + [0.13.1](https://github.com/ordinals/ord/releases/tag/0.13.1) - 2023-12-16 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 4ef42cfb21..1c635d6906 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.13.1-gm1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" diff --git a/Cargo.toml b/Cargo.toml index 4fa8a097ef..0c989d9774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1" +version = "0.13.1-gm1" license = "CC0-1.0" edition = "2021" autotests = false From 4f1c1bfb90ecc4544c2e190241664cf78c84f77f Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 18 Dec 2023 15:17:09 +0000 Subject: [PATCH 044/109] Include HEIGHT_TO_SEQUENCE_NUMBER in `index info` report. --- src/index.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.rs b/src/index.rs index db1dbe5a4b..9ef912a0cb 100644 --- a/src/index.rs +++ b/src/index.rs @@ -567,6 +567,7 @@ impl Index { insert_multimap_table_info(&mut tables, &wtx, total_bytes, SATPOINT_TO_SEQUENCE_NUMBER); insert_multimap_table_info(&mut tables, &wtx, total_bytes, SAT_TO_SEQUENCE_NUMBER); insert_multimap_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_CHILDREN); + insert_multimap_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_SEQUENCE_NUMBER); insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); insert_table_info( From 8edb812aa128a2d879de00e648ae5a40cc44b66a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 18 Dec 2023 15:17:18 +0000 Subject: [PATCH 045/109] Release 0.13.1-gm2 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 4 ++-- Cargo.toml | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f34ec5c818..0896105f1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.13.1-gm2](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm2) - 2023-12-18 +---------------------------------------------------------------------------------- + +### Fixed +- Include HEIGHT_TO_SEQUENCE_NUMBER in `index info` report. + [0.13.1-gm1](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm1) - 2023-12-16 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 1c635d6906..52f20aae71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1-gm1" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" @@ -2095,7 +2095,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.13.1" +version = "0.13.1-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0c989d9774..085c85a567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1-gm1" +version = "0.13.1-gm2" license = "CC0-1.0" edition = "2021" autotests = false From 0ef2f74ecdb5396a3b475fbb811dd0502e8ff00c Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 19 Dec 2023 19:05:52 +0000 Subject: [PATCH 046/109] Don't let the user corrupt their index by hitting control-C repeatedly. --- src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c18bf58866..c5782223fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,11 +221,7 @@ pub fn main() { env_logger::init(); ctrlc::set_handler(move || { - if SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Relaxed) { - process::exit(1); - } - - println!("Shutting down gracefully. Press again to shutdown immediately."); + println!("Shutting down gracefully. Please wait. Pressing again won't do anything."); LISTENERS .lock() From fe4eb58643dc38cd6745f0ac16c82bc87b38929e Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 19 Dec 2023 19:15:48 +0000 Subject: [PATCH 047/109] Release 0.13.1-gm3 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0896105f1d..da7a4823b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.13.1-gm3](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm3) - 2023-12-19 +---------------------------------------------------------------------------------- + +### Changed +- Don't let the user corrupt their index by hitting control-C repeatedly. + [0.13.1-gm2](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm2) - 2023-12-18 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 52f20aae71..faf35293cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,7 +2095,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.13.1-gm2" +version = "0.13.1-gm3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 085c85a567..3a9351cd00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1-gm2" +version = "0.13.1-gm3" license = "CC0-1.0" edition = "2021" autotests = false From 1de55c305dc5b452d48cf1cbf49c7f5d7fd596dd Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 21 Dec 2023 17:55:30 +0000 Subject: [PATCH 048/109] Release 0.13.1-gm4 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da7a4823b9..23fa2e71a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.13.1-gm4](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm4) - 2023-12-21 +---------------------------------------------------------------------------------- + +### Added +- Use `--change` flag to specify the change address for `wallet inscribe` and `wallet send`. + [0.13.1-gm3](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm3) - 2023-12-19 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index faf35293cc..2696a43d2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,7 +2095,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.13.1-gm3" +version = "0.13.1-gm4" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 3a9351cd00..0826e18377 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1-gm3" +version = "0.13.1-gm4" license = "CC0-1.0" edition = "2021" autotests = false From ef5dc7662f51515d07d432317410235ad8c80313 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 14:28:34 +0000 Subject: [PATCH 049/109] Track inscription transfer histories. --- src/index.rs | 48 ++++++-- src/index/entry.rs | 66 +++++++++- src/index/updater.rs | 10 +- src/index/updater/inscription_updater.rs | 19 ++- src/options.rs | 6 +- src/subcommand/server.rs | 146 ++++++++++++++++++++++- src/templates.rs | 2 + src/templates/transfers.rs | 55 +++++++++ templates/transfers.html | 13 ++ 9 files changed, 345 insertions(+), 20 deletions(-) create mode 100644 src/templates/transfers.rs create mode 100644 templates/transfers.html diff --git a/src/index.rs b/src/index.rs index 9ef912a0cb..2a3779c7ae 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2,7 +2,7 @@ use { self::{ entry::{ BlockHashValue, Entry, InscriptionEntry, InscriptionEntryValue, InscriptionIdValue, - OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxidValue, + OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TransferEntry, TransferEntryValue, TxidValue, }, reorg::*, runes::{Rune, RuneId}, @@ -54,10 +54,11 @@ macro_rules! define_multimap_table { }; } -define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 } +define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, SatPointValue, u32 } define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 } define_multimap_table! { HEIGHT_TO_SEQUENCE_NUMBER, u32, u32 } +define_multimap_table! { SEQUENCE_NUMBER_TO_TRANSFERS, u32, TransferEntryValue } define_table! { HEIGHT_TO_BLOCK_HASH, u32, &BlockHashValue } define_table! { HEIGHT_TO_LAST_SEQUENCE_NUMBER, u32, u32 } define_table! { HOME_INSCRIPTIONS, u32, InscriptionIdValue } @@ -68,10 +69,10 @@ define_table! { OUTPOINT_TO_SAT_RANGES, &OutPointValue, &[u8] } define_table! { OUTPOINT_TO_VALUE, &OutPointValue, u64} 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! { SAT_TO_SATPOINT, u64, SatPointValue } define_table! { SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, u32, InscriptionEntryValue } define_table! { SEQUENCE_NUMBER_TO_RUNE, u32, u128 } -define_table! { SEQUENCE_NUMBER_TO_SATPOINT, u32, &SatPointValue } +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! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u32, u128 } @@ -182,6 +183,7 @@ pub(crate) struct Index { index_runes: bool, index_sats: bool, index_transfers: bool, + index_transfer_history: bool, no_progress_bar: bool, options: Options, path: PathBuf, @@ -293,6 +295,7 @@ impl Index { tx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?; tx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?; tx.open_multimap_table(HEIGHT_TO_SEQUENCE_NUMBER)?; + tx.open_multimap_table(SEQUENCE_NUMBER_TO_TRANSFERS)?; tx.open_table(HEIGHT_TO_BLOCK_HASH)?; tx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?; tx.open_table(HOME_INSCRIPTIONS)?; @@ -350,7 +353,8 @@ impl Index { height_limit: options.height_limit, index_runes, index_sats, - index_transfers: options.index_transfers, + index_transfers: options.index_transfers || options.index_transfer_history, + index_transfer_history: options.index_transfer_history, no_progress_bar: options.no_progress_bar, options: options.clone(), path, @@ -568,6 +572,7 @@ impl Index { insert_multimap_table_info(&mut tables, &wtx, total_bytes, SAT_TO_SEQUENCE_NUMBER); insert_multimap_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_CHILDREN); insert_multimap_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_SEQUENCE_NUMBER); + insert_multimap_table_info(&mut tables, &wtx, total_bytes, SEQUENCE_NUMBER_TO_TRANSFERS); insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); insert_table_info(&mut tables, &wtx, total_bytes, HEIGHT_TO_BLOCK_HASH); insert_table_info( @@ -724,7 +729,7 @@ impl Index { let sequence_number = entry.0.value(); let entry = InscriptionEntry::load(entry.1.value()); let satpoint = SatPoint::load( - *sequence_number_to_satpoint + sequence_number_to_satpoint .get(sequence_number)? .unwrap() .value(), @@ -839,7 +844,7 @@ impl Index { for range in sat_to_satpoint.range(0..)? { let (sat, satpoint) = range?; - result.push((Sat(sat.value()), Entry::load(*satpoint.value()))); + result.push((Sat(sat.value()), Entry::load(satpoint.value()))); } Ok(result) @@ -852,7 +857,7 @@ impl Index { .begin_read()? .open_table(SAT_TO_SATPOINT)? .get(&sat.n())? - .map(|satpoint| Entry::load(*satpoint.value())), + .map(|satpoint| Entry::load(satpoint.value())), ) } @@ -1234,6 +1239,25 @@ impl Index { Ok(Some(RuneEntry::load(entry.value()).spaced_rune())) } + pub(crate) fn get_transfers_by_sequence_number( + &self, + sequence_number: u32, + ) -> Result> { + Ok(self.database.begin_read()? + .open_multimap_table(SEQUENCE_NUMBER_TO_TRANSFERS)? + .get(sequence_number)? + .map(|transfer| Entry::load(transfer.unwrap().value())) + .collect()) + } + + pub(crate) fn get_sequence_numbers_by_height(&self, height: u32) -> Result> { + Ok(self.database.begin_read()? + .open_multimap_table(HEIGHT_TO_SEQUENCE_NUMBER)? + .get(height)? + .map(|seq| seq.unwrap().value()) + .collect()) + } + pub(crate) fn get_inscription_ids_by_height(&self, height: u32) -> Result> { let mut ret = Vec::new(); @@ -1423,7 +1447,7 @@ impl Index { let satpoint = rtx .open_table(SEQUENCE_NUMBER_TO_SATPOINT)? .get(sequence_number)? - .map(|satpoint| Entry::load(*satpoint.value())); + .map(|satpoint| Entry::load(satpoint.value())); Ok(satpoint) } @@ -2042,7 +2066,7 @@ impl Index { } fn inscriptions_on_output<'a: 'tx, 'tx>( - satpoint_to_sequence_number: &'a impl ReadableMultimapTable<&'static SatPointValue, u32>, + satpoint_to_sequence_number: &'a impl ReadableMultimapTable, sequence_number_to_inscription_entry: &'a impl ReadableTable, outpoint: OutPoint, ) -> Result> { @@ -2060,7 +2084,7 @@ impl Index { let mut inscriptions = Vec::new(); - for range in satpoint_to_sequence_number.range::<&[u8; 44]>(&start..=&end)? { + for range in satpoint_to_sequence_number.range::<(u128, u128, u64, u32)>(&start..=&end)? { let (satpoint, sequence_numbers) = range?; for sequence_number_result in sequence_numbers { let sequence_number = sequence_number_result?.value(); @@ -2069,7 +2093,7 @@ impl Index { .unwrap(); inscriptions.push(( sequence_number, - SatPoint::load(*satpoint.value()), + SatPoint::load(satpoint.value()), InscriptionEntry::load(entry.value()).id, )); } diff --git a/src/index/entry.rs b/src/index/entry.rs index f2347f98a3..056432f015 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -151,6 +151,51 @@ impl Entry for RuneId { } } +#[derive(Debug)] +pub(crate) struct TransferEntry { + pub(crate) height: u32, + pub(crate) tx_count: u32, + pub(crate) new_satpoint: SatPoint, + pub(crate) old_satpoint: SatPoint, +} + +pub(crate) type TransferEntryValue = ( + u32, // height + u32, // tx_count + SatPointValue, // new_satpoint + SatPointValue, // old_satpoint +); + +impl Entry for TransferEntry { + type Value = TransferEntryValue; + + #[rustfmt::skip] + fn load( + ( + height, + tx_count, + new_satpoint, + old_satpoint, + ): TransferEntryValue, + ) -> Self { + Self { + height, + tx_count, + new_satpoint: SatPoint::load(new_satpoint), + old_satpoint: SatPoint::load(old_satpoint), + } + } + + fn store(self) -> Self::Value { + ( + self.height, + self.tx_count, + self.new_satpoint.store(), + self.old_satpoint.store(), + ) + } +} + #[derive(Debug)] pub(crate) struct InscriptionEntry { pub(crate) charms: u16, @@ -295,19 +340,34 @@ impl Entry for OutPoint { } } -pub(super) type SatPointValue = [u8; 44]; +pub(super) type SatPointValue = (u128, u128, u64, u32); impl Entry for SatPoint { type Value = SatPointValue; fn load(value: Self::Value) -> Self { - Decodable::consensus_decode(&mut io::Cursor::new(value)).unwrap() + let (a, b, c, d) = value; + let a_array = a.to_le_bytes(); + let b_array = b.to_le_bytes(); + let c_array = c.to_le_bytes(); + let d_array = d.to_le_bytes(); + let mut array = Vec::new(); + array.extend(a_array); + array.extend(b_array); + array.extend(c_array); + array.extend(d_array); + let result: SatPoint = Decodable::consensus_decode(&mut io::Cursor::new(array)).unwrap(); + result } fn store(self) -> Self::Value { let mut value = [0; 44]; self.consensus_encode(&mut value.as_mut_slice()).unwrap(); - value + (u128::from_le_bytes(value[ 0..16].try_into().unwrap()), + u128::from_le_bytes(value[16..32].try_into().unwrap()), + u64::from_le_bytes(value[32..40].try_into().unwrap()), + u32::from_le_bytes(value[40..44].try_into().unwrap()), + ) } } diff --git a/src/index/updater.rs b/src/index/updater.rs index 22a42b6a71..7627467d0a 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -388,6 +388,11 @@ impl<'index> Updater<'_> { } else { None }; + let mut sequence_number_to_transfers = if index.index_transfer_history { + Some(wtx.open_multimap_table(SEQUENCE_NUMBER_TO_TRANSFERS)?) + } else { + None + }; 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_id_to_sequence_number = @@ -438,10 +443,12 @@ impl<'index> Updater<'_> { flotsam: Vec::new(), height: self.height, height_to_sequence_number: &mut height_to_sequence_number, + sequence_number_to_transfers: &mut sequence_number_to_transfers, home_inscription_count, home_inscriptions: &mut home_inscriptions, id_to_sequence_number: &mut inscription_id_to_sequence_number, ignore_cursed: index.options.ignore_cursed, + index_only_first_transfer: index.options.index_only_first_transfer, inscription_number_to_sequence_number: &mut inscription_number_to_sequence_number, lost_sats, next_sequence_number, @@ -453,6 +460,7 @@ impl<'index> Updater<'_> { sequence_number_to_entry: &mut sequence_number_to_inscription_entry, sequence_number_to_satpoint: &mut sequence_number_to_satpoint, timestamp: block.header.time, + tx_count: 0, unbound_inscriptions, value_cache, value_receiver, @@ -631,7 +639,7 @@ impl<'index> Updater<'_> { &mut self, tx: &Transaction, txid: Txid, - sat_to_satpoint: &mut Table, + sat_to_satpoint: &mut Table, input_sat_ranges: &mut VecDeque<(u64, u64)>, sat_ranges_written: &mut u64, outputs_traversed: &mut u64, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 0f136ef231..88c7e2c94f 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -47,6 +47,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) home_inscriptions: &'a mut Table<'db, 'tx, u32, InscriptionIdValue>, pub(super) id_to_sequence_number: &'a mut Table<'db, 'tx, InscriptionIdValue, u32>, pub(super) ignore_cursed: bool, + pub(super) index_only_first_transfer: bool, pub(super) inscription_number_to_sequence_number: &'a mut Table<'db, 'tx, i32, u32>, pub(super) lost_sats: u64, pub(super) next_sequence_number: u32, @@ -54,11 +55,13 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) reward: u64, pub(super) sat_to_sequence_number: &'a mut MultimapTable<'db, 'tx, u64, u32>, pub(super) satpoint_to_sequence_number: - &'a mut MultimapTable<'db, 'tx, &'static SatPointValue, u32>, + &'a mut MultimapTable<'db, 'tx, SatPointValue, u32>, pub(super) sequence_number_to_children: &'a mut MultimapTable<'db, 'tx, u32, u32>, pub(super) sequence_number_to_entry: &'a mut Table<'db, 'tx, u32, InscriptionEntryValue>, - pub(super) sequence_number_to_satpoint: &'a mut Table<'db, 'tx, u32, &'static SatPointValue>, + pub(super) sequence_number_to_satpoint: &'a mut Table<'db, 'tx, u32, SatPointValue>, + pub(super) sequence_number_to_transfers: &'a mut Option>, pub(super) timestamp: u32, + pub(super) tx_count: u32, pub(super) unbound_inscriptions: u64, pub(super) value_cache: &'a mut HashMap, pub(super) value_receiver: &'a mut Receiver, @@ -78,6 +81,8 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let mut total_input_value = 0; let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); + self.tx_count += 1; + for (input_index, tx_in) in tx.input.iter().enumerate() { // skip subsidy since no inscriptions possible if tx_in.previous_output.is_null() { @@ -374,6 +379,16 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { if let Some(height_to_sequence_number) = &mut self.height_to_sequence_number { height_to_sequence_number.insert(&self.height, &sequence_number)?; } + if let Some(sequence_number_to_transfers) = &mut self.sequence_number_to_transfers { + if !self.index_only_first_transfer || sequence_number_to_transfers.get(sequence_number)?.next().is_none() { + sequence_number_to_transfers.insert(&sequence_number, TransferEntry { + height: self.height, + tx_count: self.tx_count, + new_satpoint, + old_satpoint, + }.store())?; + } + } self .satpoint_to_sequence_number .remove_all(&old_satpoint.store())?; diff --git a/src/options.rs b/src/options.rs index 6a1d919b16..ca7bdf45c1 100644 --- a/src/options.rs +++ b/src/options.rs @@ -49,8 +49,12 @@ pub(crate) struct Options { pub(crate) index_runes: bool, #[arg(long, help = "Track location of all satoshis.")] pub(crate) index_sats: bool, - #[clap(long, help = "Track transfers of inscriptions.")] + #[clap(long, help = "Index which inscriptions are transferred in each block.")] pub(crate) index_transfers: bool, + #[clap(long, help = "Index all the inscription movements in each block.")] + pub(crate) index_transfer_history: bool, + #[clap(long, help = "Only track the first transfer of each inscription.")] + pub(crate) index_only_first_transfer: bool, #[arg(long, help = "Inhibit the display of the progress bar while updating the index.")] pub(crate) no_progress_bar: bool, #[arg(long, short, help = "Use regtest. Equivalent to `--chain regtest`.")] diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index a11e58eb2d..22fe7dab6f 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -15,7 +15,7 @@ use { PreviewAudioHtml, PreviewCodeHtml, PreviewFontHtml, PreviewImageHtml, PreviewMarkdownHtml, PreviewModelHtml, PreviewPdfHtml, PreviewTextHtml, PreviewUnknownHtml, PreviewVideoHtml, RangeHtml, RareTxt, RuneHtml, RunesHtml, SatHtml, SatInscriptionJson, SatInscriptionsJson, - SatJson, StatusHtml, TransactionHtml, + SatJson, StatusHtml, TransactionHtml, TransfersJson, TransfersHtml, }, }, axum::{ @@ -335,6 +335,9 @@ impl Server { .route("/static/*path", get(Self::static_asset)) .route("/stats", get(Self::stats)) .route("/status", get(Self::status)) + .route("/transfer_history/:height", get(Self::inscriptionid_history_from_height)) + .route("/transfer_history/:height/:start", get(Self::inscriptionid_history_from_height_start)) + .route("/transfer_history/:height/:start/:end", get(Self::inscriptionid_history_from_height_start_end)) .route("/transfers/:height", get(Self::inscriptionids_from_height)) .route("/transfers/:height/:start", get(Self::inscriptionids_from_height_start)) .route("/transfers/:height/:start/:end", get(Self::inscriptionids_from_height_start_end)) @@ -1025,6 +1028,147 @@ impl Server { Ok(ret) } + async fn inscriptionid_history_from_height( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(height): Path, + accept_json: AcceptJson, + ) -> ServerResult { + log::info!("GET /transfer_history/{height}"); + Self::inscriptionid_history_from_height_inner( + server_config, + index.clone(), + height, + index.get_sequence_numbers_by_height(height)?, + accept_json, + ).await + } + + async fn inscriptionid_history_from_height_start( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(path): Path<(u32, usize)>, + accept_json: AcceptJson, + ) -> ServerResult { + let height = path.0; + let start = path.1; + log::info!("GET /transfer_history/{height}/{start}"); + + let sequence_numbers = index.get_sequence_numbers_by_height(height)?; + let end = sequence_numbers.len(); + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + Self::inscriptionid_history_from_height_inner( + server_config, + index.clone(), + height, + sequence_numbers[start..end].to_vec(), + accept_json, + ).await + } + } + } + + async fn inscriptionid_history_from_height_start_end( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(path): Path<(u32, usize, usize)>, + accept_json: AcceptJson, + ) -> ServerResult { + let height = path.0; + let start = path.1; + let mut end = path.2; + log::info!("GET /transfer_history/{height}/{start}/{end}"); + + let sequence_numbers = index.get_sequence_numbers_by_height(height)?; + end = usize::min(end, sequence_numbers.len()); + + match start.cmp(&end) { + Ordering::Equal => Err(ServerError::BadRequest("range length == 0".to_string())), + Ordering::Greater => Err(ServerError::BadRequest("range length < 0".to_string())), + Ordering::Less => { + Self::inscriptionid_history_from_height_inner(server_config, + index.clone(), + height, + sequence_numbers[start..end].to_vec(), + accept_json, + ).await + } + } + } + + async fn inscriptionid_history_from_height_inner( + server_config: Arc, + index: Arc, + height: u32, + sequence_numbers: Vec, + accept_json: AcceptJson, + ) -> ServerResult { + let mut ret = Vec::new(); + let mut tx_cache = HashMap::new(); + + for sequence_number in sequence_numbers { + let inscription_id = index.get_inscription_id_by_sequence_number(sequence_number)?.unwrap(); + let mut transfers_vec = Vec::new(); + let transfers = index.get_transfers_by_sequence_number(sequence_number)?; + for transfer in transfers { + sleep(Duration::from_millis(0)).await; + if transfer.height != height { + continue; + } + + let old_address = Self::satpoint_to_address(server_config.chain, &index, transfer.old_satpoint, &mut tx_cache)?; + let new_address = Self::satpoint_to_address(server_config.chain, &index, transfer.new_satpoint, &mut tx_cache)?; + + transfers_vec.push((old_address, transfer.old_satpoint, new_address, transfer.new_satpoint)); + } + + if !transfers_vec.is_empty() { + ret.push((inscription_id, transfers_vec)); + } + } + + Ok(if accept_json.0 { + Json(ret.iter().map(|ins| TransfersJson::new(ins.clone())).collect::>()).into_response() + } else { + TransfersHtml::new(Height(height), ret) + .page(server_config) + .into_response() + }) + } + + fn satpoint_to_address( + chain: Chain, + index: &Arc, + satpoint: SatPoint, + tx_cache: &mut HashMap) -> Result { + + let address = if satpoint.outpoint == unbound_outpoint() { + String::from("unbound") + } else { + if !tx_cache.contains_key(&satpoint.outpoint.txid) { + tx_cache.insert(satpoint.outpoint.txid, + index + .get_transaction(satpoint.outpoint.txid)?.unwrap()); + } + + let output = tx_cache.get(&satpoint.outpoint.txid).unwrap().clone() + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()).unwrap(); + if let Ok(address) = chain.address_from_script(&output.script_pubkey) { + address.to_string() + } else { + String::from("error") + } + }; + + Ok(address) + } + async fn transaction( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/src/templates.rs b/src/templates.rs index aae0b5f953..4379f49409 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -26,6 +26,7 @@ pub(crate) use { server_config::ServerConfig, status::StatusHtml, transaction::TransactionHtml, + transfers::{TransfersJson, TransfersHtml}, }; pub mod block; @@ -49,6 +50,7 @@ mod runes; pub mod sat; mod status; mod transaction; +mod transfers; #[derive(Boilerplate)] pub(crate) struct PageHtml { diff --git a/src/templates/transfers.rs b/src/templates/transfers.rs new file mode 100644 index 0000000000..8dff464952 --- /dev/null +++ b/src/templates/transfers.rs @@ -0,0 +1,55 @@ +use super::*; + +#[derive(Boilerplate)] +pub(crate) struct TransfersHtml { + height: Height, + data: Vec<(InscriptionId, Vec<(String, SatPoint, String, SatPoint)>)>, +} + +impl TransfersHtml { + pub(crate) fn new( + height: Height, + data: Vec<(InscriptionId, Vec<(String, SatPoint, String, SatPoint)>)>, + ) -> Self { + Self { + height, + data, + } + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct TransferJson { + from_address: String, + from_satpoint: SatPoint, + to_address: String, + to_satpoint: SatPoint, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct TransfersJson { + inscriptionid: InscriptionId, + transfers: Vec, +} + +impl TransfersJson { + pub(crate) fn new( + data: (InscriptionId, Vec<(String, SatPoint, String, SatPoint)>), + ) -> Self { + Self { + inscriptionid: data.0, + transfers: data.1.iter().map(|transfer| TransferJson { + from_address: transfer.0.clone(), + from_satpoint: transfer.1, + to_address: transfer.2.clone(), + to_satpoint: transfer.3, + }).collect() + } + } +} + +impl PageContent for TransfersHtml { + fn title(&self) -> String { + format!("Transfers for block {}", self.height) + } +} diff --git a/templates/transfers.html b/templates/transfers.html new file mode 100644 index 0000000000..80319261f1 --- /dev/null +++ b/templates/transfers.html @@ -0,0 +1,13 @@ +

Transfers in block {{ self.height }}

+
    +%% for transfers in &self.data { +
  • {{transfers.0}} +
      +%% for transfer in transfers.1.clone() { +
    • {{transfer.0}} -> + {{transfer.2}}
    • +%% } +
    +
  • +%% } +
From 5e39e834365b48e135d22bc1169a8d6f13959ffc Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 14:45:58 +0000 Subject: [PATCH 050/109] Have the new flags imply each other where it makes sense. --- src/index.rs | 15 +++++++++++++-- src/options.rs | 4 ++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/index.rs b/src/index.rs index 2a3779c7ae..2b0d3d87cc 100644 --- a/src/index.rs +++ b/src/index.rs @@ -343,6 +343,17 @@ impl Index { let genesis_block_coinbase_transaction = options.chain().genesis_block().coinbase().unwrap().clone(); + let mut index_transfers = options.index_transfers; + let mut index_transfer_history = options.index_transfer_history; + + if options.index_only_first_transfer { + index_transfer_history = true; + } + + if index_transfer_history { + index_transfers = true; + } + Ok(Self { genesis_block_coinbase_txid: genesis_block_coinbase_transaction.txid(), client, @@ -353,8 +364,8 @@ impl Index { height_limit: options.height_limit, index_runes, index_sats, - index_transfers: options.index_transfers || options.index_transfer_history, - index_transfer_history: options.index_transfer_history, + index_transfers, + index_transfer_history, no_progress_bar: options.no_progress_bar, options: options.clone(), path, diff --git a/src/options.rs b/src/options.rs index ca7bdf45c1..e19a54f5ea 100644 --- a/src/options.rs +++ b/src/options.rs @@ -51,9 +51,9 @@ pub(crate) struct Options { pub(crate) index_sats: bool, #[clap(long, help = "Index which inscriptions are transferred in each block.")] pub(crate) index_transfers: bool, - #[clap(long, help = "Index all the inscription movements in each block.")] + #[clap(long, help = "Index all the inscription movements in each block. Implies `--index-transfers`.")] pub(crate) index_transfer_history: bool, - #[clap(long, help = "Only track the first transfer of each inscription.")] + #[clap(long, help = "Only track the first transfer of each inscription. Implies `--index-transfer-history`.")] pub(crate) index_only_first_transfer: bool, #[arg(long, help = "Inhibit the display of the progress bar while updating the index.")] pub(crate) no_progress_bar: bool, From 09538b5fde4bc175fe2764106b82056770177b80 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 15:08:33 +0000 Subject: [PATCH 051/109] Handle 'lost' inscriptions correctly. Use `address_from_script()` for the old /transfers/ endpoint. --- src/subcommand/server.rs | 36 +++++++++--------------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 22fe7dab6f..29fff65086 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1000,28 +1000,7 @@ impl Server { let satpoint = index .get_inscription_satpoint_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let address = if satpoint.outpoint == unbound_outpoint() { - String::from("unbound") - } else { - if !tx_cache.contains_key(&satpoint.outpoint.txid) { - tx_cache.insert(satpoint.outpoint.txid, - index - .get_transaction(satpoint.outpoint.txid)? - .ok_or_not_found(|| format!("inscription {inscription_id} current transaction"))?); - } - - let output = tx_cache.get(&satpoint.outpoint.txid).unwrap().clone() - .output - .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()) - .ok_or_not_found(|| format!("inscription {inscription_id} current transaction output"))?; - if let Ok(address) = chain.address_from_script(&output.script_pubkey) { - address.to_string() - } else { - String::from("error") - } - }; - + let address = Self::satpoint_to_address(chain, &index, satpoint, &mut tx_cache)?; ret += &format!("{} {}\n", inscription_id, address); } @@ -1149,13 +1128,16 @@ impl Server { let address = if satpoint.outpoint == unbound_outpoint() { String::from("unbound") } else { - if !tx_cache.contains_key(&satpoint.outpoint.txid) { - tx_cache.insert(satpoint.outpoint.txid, - index - .get_transaction(satpoint.outpoint.txid)?.unwrap()); + let txid = satpoint.outpoint.txid; + if !tx_cache.contains_key(&txid) { + if let Ok(tx) = index.get_transaction(txid) { + tx_cache.insert(txid, tx.unwrap()); + } else { + return Ok(String::from("lost")); + } } - let output = tx_cache.get(&satpoint.outpoint.txid).unwrap().clone() + let output = tx_cache.get(&txid).unwrap().clone() .output .into_iter() .nth(satpoint.outpoint.vout.try_into().unwrap()).unwrap(); From beb1455babdd10b73fde2532a47c7a3f0259290c Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 17:42:51 +0000 Subject: [PATCH 052/109] Add `--filter-metaprotocol` flag. It only indexes inscriptions that have a metaprotocol that starts with the given string. --- src/envelope.rs | 9 ++++++++- src/index.rs | 2 +- src/index/updater.rs | 1 + src/index/updater/inscription_updater.rs | 3 ++- src/options.rs | 2 ++ src/subcommand/decode.rs | 2 +- 6 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/envelope.rs b/src/envelope.rs index 98352320b7..0b3d128893 100644 --- a/src/envelope.rs +++ b/src/envelope.rs @@ -123,10 +123,17 @@ impl From for ParsedEnvelope { } impl ParsedEnvelope { - pub(crate) fn from_transaction(transaction: &Transaction) -> Vec { + pub(crate) fn from_transaction(transaction: &Transaction, filter: Option) -> Vec { RawEnvelope::from_transaction(transaction) .into_iter() .map(|envelope| envelope.into()) + .filter(|envelope: &ParsedEnvelope| match &filter { + Some(filter) => match envelope.payload.metaprotocol.clone() { + Some(metaprotocol) => metaprotocol.to_vec().starts_with(filter.as_bytes()), + None => false, + } + None => true, + }) .collect() } } diff --git a/src/index.rs b/src/index.rs index 2b0d3d87cc..2decb2f65f 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1472,7 +1472,7 @@ impl Index { } Ok(self.get_transaction(inscription_id.txid)?.and_then(|tx| { - ParsedEnvelope::from_transaction(&tx) + ParsedEnvelope::from_transaction(&tx, None) .into_iter() .nth(inscription_id.index as usize) .map(|envelope| envelope.payload) diff --git a/src/index/updater.rs b/src/index/updater.rs index 7627467d0a..e9c3d4a57b 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -440,6 +440,7 @@ impl<'index> Updater<'_> { blessed_inscription_count, chain: self.index.options.chain(), cursed_inscription_count, + filter_metaprotocol: index.options.filter_metaprotocol.clone(), flotsam: Vec::new(), height: self.height, height_to_sequence_number: &mut height_to_sequence_number, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 88c7e2c94f..5c8cdf9230 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -40,6 +40,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) blessed_inscription_count: u64, pub(super) chain: Chain, pub(super) cursed_inscription_count: u64, + pub(super) filter_metaprotocol: Option, pub(super) flotsam: Vec, pub(super) height: u32, pub(super) height_to_sequence_number: &'a mut Option>, @@ -74,7 +75,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { txid: Txid, input_sat_ranges: Option<&VecDeque<(u64, u64)>>, ) -> Result { - let mut envelopes = ParsedEnvelope::from_transaction(tx).into_iter().peekable(); + let mut envelopes = ParsedEnvelope::from_transaction(tx, self.filter_metaprotocol.clone()).into_iter().peekable(); let mut floating_inscriptions = Vec::new(); let mut id_counter = 0; let mut inscribed_offsets = BTreeMap::new(); diff --git a/src/options.rs b/src/options.rs index e19a54f5ea..9815060813 100644 --- a/src/options.rs +++ b/src/options.rs @@ -33,6 +33,8 @@ pub(crate) struct Options { help = "Set index cache to bytes. By default takes 1/4 of available RAM." )] pub(crate) db_cache_size: Option, + #[arg(long, help = "Only index inscriptions that have a metaprotocol that starts with the given string.")] + pub(crate) filter_metaprotocol: Option, #[arg( long, help = "Don't look for inscriptions below ." diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index 61258778c5..e055eab3e3 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -18,7 +18,7 @@ impl Decode { Transaction::consensus_decode(&mut io::stdin())? }; - let inscriptions = ParsedEnvelope::from_transaction(&transaction); + let inscriptions = ParsedEnvelope::from_transaction(&transaction, None); Ok(Box::new(Output { inscriptions: inscriptions From 80f36551f05271ec226d1330a9e62a304d9abf7a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 17:45:28 +0000 Subject: [PATCH 053/109] When `--index-only-first-transfer` is in effect, don't index other transfers at all, even for the `/transfers` endpoint. --- src/index/updater/inscription_updater.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 5c8cdf9230..bc5d13f34d 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -377,9 +377,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { .get(&inscription_id.store())? .unwrap() .value(); - if let Some(height_to_sequence_number) = &mut self.height_to_sequence_number { - height_to_sequence_number.insert(&self.height, &sequence_number)?; - } + let mut skip_this_transfer = false; if let Some(sequence_number_to_transfers) = &mut self.sequence_number_to_transfers { if !self.index_only_first_transfer || sequence_number_to_transfers.get(sequence_number)?.next().is_none() { sequence_number_to_transfers.insert(&sequence_number, TransferEntry { @@ -388,6 +386,13 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { new_satpoint, old_satpoint, }.store())?; + } else { + skip_this_transfer = true; + } + } + if !skip_this_transfer { + if let Some(height_to_sequence_number) = &mut self.height_to_sequence_number { + height_to_sequence_number.insert(&self.height, &sequence_number)?; } } self From a06ea382cca5b0fa10bb61a503f616be012c0cf1 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 28 Dec 2023 19:02:42 +0000 Subject: [PATCH 054/109] Revert the change to how satpoints are stored. It was really slow the other way. --- src/index.rs | 20 +++---- src/index/entry.rs | 67 +++++++++++++----------- src/index/updater.rs | 2 +- src/index/updater/inscription_updater.rs | 10 ++-- src/subcommand/server.rs | 17 +++--- src/templates/transfers.rs | 18 +++---- templates/transfers.html | 3 +- 7 files changed, 70 insertions(+), 67 deletions(-) diff --git a/src/index.rs b/src/index.rs index 2decb2f65f..0db5c6a486 100644 --- a/src/index.rs +++ b/src/index.rs @@ -54,7 +54,7 @@ macro_rules! define_multimap_table { }; } -define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, SatPointValue, u32 } +define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 } define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 } define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 } define_multimap_table! { HEIGHT_TO_SEQUENCE_NUMBER, u32, u32 } @@ -69,10 +69,10 @@ define_table! { OUTPOINT_TO_SAT_RANGES, &OutPointValue, &[u8] } define_table! { OUTPOINT_TO_VALUE, &OutPointValue, u64} 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! { SAT_TO_SATPOINT, u64, &SatPointValue } define_table! { SEQUENCE_NUMBER_TO_INSCRIPTION_ENTRY, u32, InscriptionEntryValue } define_table! { SEQUENCE_NUMBER_TO_RUNE, u32, u128 } -define_table! { SEQUENCE_NUMBER_TO_SATPOINT, u32, SatPointValue } +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! { WRITE_TRANSACTION_STARTING_BLOCK_COUNT_TO_TIMESTAMP, u32, u128 } @@ -740,7 +740,7 @@ impl Index { let sequence_number = entry.0.value(); let entry = InscriptionEntry::load(entry.1.value()); let satpoint = SatPoint::load( - sequence_number_to_satpoint + *sequence_number_to_satpoint .get(sequence_number)? .unwrap() .value(), @@ -855,7 +855,7 @@ impl Index { for range in sat_to_satpoint.range(0..)? { let (sat, satpoint) = range?; - result.push((Sat(sat.value()), Entry::load(satpoint.value()))); + result.push((Sat(sat.value()), Entry::load(*satpoint.value()))); } Ok(result) @@ -868,7 +868,7 @@ impl Index { .begin_read()? .open_table(SAT_TO_SATPOINT)? .get(&sat.n())? - .map(|satpoint| Entry::load(satpoint.value())), + .map(|satpoint| Entry::load(*satpoint.value())), ) } @@ -1458,7 +1458,7 @@ impl Index { let satpoint = rtx .open_table(SEQUENCE_NUMBER_TO_SATPOINT)? .get(sequence_number)? - .map(|satpoint| Entry::load(satpoint.value())); + .map(|satpoint| Entry::load(*satpoint.value())); Ok(satpoint) } @@ -2077,7 +2077,7 @@ impl Index { } fn inscriptions_on_output<'a: 'tx, 'tx>( - satpoint_to_sequence_number: &'a impl ReadableMultimapTable, + satpoint_to_sequence_number: &'a impl ReadableMultimapTable<&'static SatPointValue, u32>, sequence_number_to_inscription_entry: &'a impl ReadableTable, outpoint: OutPoint, ) -> Result> { @@ -2095,7 +2095,7 @@ impl Index { let mut inscriptions = Vec::new(); - for range in satpoint_to_sequence_number.range::<(u128, u128, u64, u32)>(&start..=&end)? { + for range in satpoint_to_sequence_number.range::<&[u8; 44]>(&start..=&end)? { let (satpoint, sequence_numbers) = range?; for sequence_number_result in sequence_numbers { let sequence_number = sequence_number_result?.value(); @@ -2104,7 +2104,7 @@ impl Index { .unwrap(); inscriptions.push(( sequence_number, - SatPoint::load(satpoint.value()), + SatPoint::load(*satpoint.value()), InscriptionEntry::load(entry.value()).id, )); } diff --git a/src/index/entry.rs b/src/index/entry.rs index 056432f015..cf9ca7f1d6 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -155,15 +155,14 @@ impl Entry for RuneId { pub(crate) struct TransferEntry { pub(crate) height: u32, pub(crate) tx_count: u32, - pub(crate) new_satpoint: SatPoint, - pub(crate) old_satpoint: SatPoint, + pub(crate) outpoint: OutPoint, } pub(crate) type TransferEntryValue = ( - u32, // height - u32, // tx_count - SatPointValue, // new_satpoint - SatPointValue, // old_satpoint + u32, // height + u32, // tx_count + (u128, u128), // txid + u32, // vout ); impl Entry for TransferEntry { @@ -174,15 +173,26 @@ impl Entry for TransferEntry { ( height, tx_count, - new_satpoint, - old_satpoint, + txid, + vout, ): TransferEntryValue, ) -> Self { Self { height, tx_count, - new_satpoint: SatPoint::load(new_satpoint), - old_satpoint: SatPoint::load(old_satpoint), + outpoint: OutPoint { + txid: { + let low = txid.0.to_le_bytes(); + let high = txid.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], + ]) + }, + vout, + } } } @@ -190,8 +200,20 @@ impl Entry for TransferEntry { ( self.height, self.tx_count, - self.new_satpoint.store(), - self.old_satpoint.store(), + { + let bytes = self.outpoint.txid.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.outpoint.vout, ) } } @@ -340,34 +362,19 @@ impl Entry for OutPoint { } } -pub(super) type SatPointValue = (u128, u128, u64, u32); +pub(super) type SatPointValue = [u8; 44]; impl Entry for SatPoint { type Value = SatPointValue; fn load(value: Self::Value) -> Self { - let (a, b, c, d) = value; - let a_array = a.to_le_bytes(); - let b_array = b.to_le_bytes(); - let c_array = c.to_le_bytes(); - let d_array = d.to_le_bytes(); - let mut array = Vec::new(); - array.extend(a_array); - array.extend(b_array); - array.extend(c_array); - array.extend(d_array); - let result: SatPoint = Decodable::consensus_decode(&mut io::Cursor::new(array)).unwrap(); - result + Decodable::consensus_decode(&mut io::Cursor::new(value)).unwrap() } fn store(self) -> Self::Value { let mut value = [0; 44]; self.consensus_encode(&mut value.as_mut_slice()).unwrap(); - (u128::from_le_bytes(value[ 0..16].try_into().unwrap()), - u128::from_le_bytes(value[16..32].try_into().unwrap()), - u64::from_le_bytes(value[32..40].try_into().unwrap()), - u32::from_le_bytes(value[40..44].try_into().unwrap()), - ) + value } } diff --git a/src/index/updater.rs b/src/index/updater.rs index e9c3d4a57b..d4d473c662 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -640,7 +640,7 @@ impl<'index> Updater<'_> { &mut self, tx: &Transaction, txid: Txid, - sat_to_satpoint: &mut Table, + sat_to_satpoint: &mut Table, input_sat_ranges: &mut VecDeque<(u64, u64)>, sat_ranges_written: &mut u64, outputs_traversed: &mut u64, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index bc5d13f34d..43b97bef89 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -56,10 +56,10 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) reward: u64, pub(super) sat_to_sequence_number: &'a mut MultimapTable<'db, 'tx, u64, u32>, pub(super) satpoint_to_sequence_number: - &'a mut MultimapTable<'db, 'tx, SatPointValue, u32>, + &'a mut MultimapTable<'db, 'tx, &'static SatPointValue, u32>, pub(super) sequence_number_to_children: &'a mut MultimapTable<'db, 'tx, u32, u32>, pub(super) sequence_number_to_entry: &'a mut Table<'db, 'tx, u32, InscriptionEntryValue>, - pub(super) sequence_number_to_satpoint: &'a mut Table<'db, 'tx, u32, SatPointValue>, + pub(super) sequence_number_to_satpoint: &'a mut Table<'db, 'tx, u32, &'static SatPointValue>, pub(super) sequence_number_to_transfers: &'a mut Option>, pub(super) timestamp: u32, pub(super) tx_count: u32, @@ -380,18 +380,20 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let mut skip_this_transfer = false; if let Some(sequence_number_to_transfers) = &mut self.sequence_number_to_transfers { if !self.index_only_first_transfer || sequence_number_to_transfers.get(sequence_number)?.next().is_none() { + // eprintln!("insert seq {} point {}", sequence_number, new_satpoint.outpoint); sequence_number_to_transfers.insert(&sequence_number, TransferEntry { height: self.height, tx_count: self.tx_count, - new_satpoint, - old_satpoint, + outpoint: new_satpoint.outpoint, }.store())?; } else { + // eprintln!("skip seq {}", sequence_number); skip_this_transfer = true; } } if !skip_this_transfer { if let Some(height_to_sequence_number) = &mut self.height_to_sequence_number { + // eprintln!("insert height {} seq {}", self.height, sequence_number); height_to_sequence_number.insert(&self.height, &sequence_number)?; } } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 29fff65086..bb6a221755 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1000,7 +1000,7 @@ impl Server { let satpoint = index .get_inscription_satpoint_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let address = Self::satpoint_to_address(chain, &index, satpoint, &mut tx_cache)?; + let address = Self::outpoint_to_address(chain, &index, satpoint.outpoint, &mut tx_cache)?; ret += &format!("{} {}\n", inscription_id, address); } @@ -1099,10 +1099,9 @@ impl Server { continue; } - let old_address = Self::satpoint_to_address(server_config.chain, &index, transfer.old_satpoint, &mut tx_cache)?; - let new_address = Self::satpoint_to_address(server_config.chain, &index, transfer.new_satpoint, &mut tx_cache)?; + let address = Self::outpoint_to_address(server_config.chain, &index, transfer.outpoint, &mut tx_cache)?; - transfers_vec.push((old_address, transfer.old_satpoint, new_address, transfer.new_satpoint)); + transfers_vec.push((address, transfer.outpoint)); } if !transfers_vec.is_empty() { @@ -1119,16 +1118,16 @@ impl Server { }) } - fn satpoint_to_address( + fn outpoint_to_address( chain: Chain, index: &Arc, - satpoint: SatPoint, + outpoint: OutPoint, tx_cache: &mut HashMap) -> Result { - let address = if satpoint.outpoint == unbound_outpoint() { + let address = if outpoint == unbound_outpoint() { String::from("unbound") } else { - let txid = satpoint.outpoint.txid; + let txid = outpoint.txid; if !tx_cache.contains_key(&txid) { if let Ok(tx) = index.get_transaction(txid) { tx_cache.insert(txid, tx.unwrap()); @@ -1140,7 +1139,7 @@ impl Server { let output = tx_cache.get(&txid).unwrap().clone() .output .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()).unwrap(); + .nth(outpoint.vout.try_into().unwrap()).unwrap(); if let Ok(address) = chain.address_from_script(&output.script_pubkey) { address.to_string() } else { diff --git a/src/templates/transfers.rs b/src/templates/transfers.rs index 8dff464952..b421de8370 100644 --- a/src/templates/transfers.rs +++ b/src/templates/transfers.rs @@ -3,13 +3,13 @@ use super::*; #[derive(Boilerplate)] pub(crate) struct TransfersHtml { height: Height, - data: Vec<(InscriptionId, Vec<(String, SatPoint, String, SatPoint)>)>, + data: Vec<(InscriptionId, Vec<(String, OutPoint)>)>, } impl TransfersHtml { pub(crate) fn new( height: Height, - data: Vec<(InscriptionId, Vec<(String, SatPoint, String, SatPoint)>)>, + data: Vec<(InscriptionId, Vec<(String, OutPoint)>)>, ) -> Self { Self { height, @@ -20,10 +20,8 @@ impl TransfersHtml { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct TransferJson { - from_address: String, - from_satpoint: SatPoint, - to_address: String, - to_satpoint: SatPoint, + address: String, + outpoint: OutPoint, } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -34,15 +32,13 @@ pub struct TransfersJson { impl TransfersJson { pub(crate) fn new( - data: (InscriptionId, Vec<(String, SatPoint, String, SatPoint)>), + data: (InscriptionId, Vec<(String, OutPoint)>), ) -> Self { Self { inscriptionid: data.0, transfers: data.1.iter().map(|transfer| TransferJson { - from_address: transfer.0.clone(), - from_satpoint: transfer.1, - to_address: transfer.2.clone(), - to_satpoint: transfer.3, + address: transfer.0.clone(), + outpoint: transfer.1, }).collect() } } diff --git a/templates/transfers.html b/templates/transfers.html index 80319261f1..ab3b1a3408 100644 --- a/templates/transfers.html +++ b/templates/transfers.html @@ -4,8 +4,7 @@

Transfers in block {{ self.height }}

  • {{transfers.0}}
      %% for transfer in transfers.1.clone() { -
    • {{transfer.0}} -> - {{transfer.2}}
    • +
    • {{transfer.0}}
    • %% }
  • From dd8dd6054a72832692000b5c1a09db3e2dba8be5 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 29 Dec 2023 15:48:06 +0000 Subject: [PATCH 055/109] Fix previous commit. It wasn't possible to interrupt indexing. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index c5782223fb..07cf466142 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,6 +221,7 @@ pub fn main() { env_logger::init(); ctrlc::set_handler(move || { + SHUTTING_DOWN.fetch_or(true, atomic::Ordering::Relaxed); println!("Shutting down gracefully. Please wait. Pressing again won't do anything."); LISTENERS From 2ab4f2cb4ac14118b2932a491217b394904a1ec4 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 31 Dec 2023 14:13:37 +0000 Subject: [PATCH 056/109] Add metaprotocol to the `/inscription/` JSON endpoint. --- src/subcommand/server.rs | 1 + src/templates/inscription.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index bb6a221755..9a41835ed9 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1725,6 +1725,7 @@ impl Server { inscription_id, children, inscription_number: entry.inscription_number, + metaprotocol: inscription.metaprotocol().map(String::from), genesis_height: entry.height, parent, genesis_fee: entry.fee, diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 4faff866ab..117050463b 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -30,6 +30,7 @@ pub struct InscriptionJson { pub genesis_height: u32, pub inscription_id: InscriptionId, pub inscription_number: i32, + pub metaprotocol: Option, pub next: Option, pub output_value: Option, pub parent: Option, From a177626071c9d1338068de1d4a2a111703825b32 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 31 Dec 2023 22:13:13 +0000 Subject: [PATCH 057/109] Add `/inscription_funder/:inscription_id` endpoint. --- src/subcommand/server.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 9a41835ed9..bec250db32 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -275,6 +275,7 @@ impl Server { .route("/feed.xml", get(Self::feed)) .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) + .route("/inscription_funder/:inscription_id", get(Self::inscription_funder)) .route("/inscriptions", get(Self::inscriptions)) .route("/inscriptions/:page", get(Self::inscriptions_paginated)) .route( @@ -1773,6 +1774,33 @@ impl Server { }) } + async fn inscription_funder( + Extension(index): Extension>, + Extension(server_config): Extension>, + Path(inscription_id): Path, + ) -> ServerResult { + log::info!("GET /inscription_funder/{inscription_id}"); + + let reveal_txid = inscription_id.txid; + let reveal_tx = index.get_transaction(reveal_txid)?.unwrap(); + let reveal_input_count = reveal_tx.input.len(); + if reveal_input_count == 1 { + let commit_txid = reveal_tx.input[0].previous_output.txid; + let commit_tx = index.get_transaction(commit_txid)?.unwrap(); + let commit_input_count = commit_tx.input.len(); + if commit_input_count == 1 { + let funder_txid = reveal_tx.input[0].previous_output.txid; + let funder_tx = index.get_transaction(funder_txid)?.unwrap(); + let funder_vout = reveal_tx.input[0].previous_output.vout; + Ok(server_config.chain.address_from_script(&funder_tx.output[funder_vout as usize].script_pubkey).unwrap().to_string()) + } else { + Err(ServerError::BadRequest(format!("commit tx has {} inputs", commit_input_count))) + } + } else { + Err(ServerError::BadRequest(format!("reveal tx has {} inputs", reveal_input_count))) + } + } + async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, From 770912e4dba9779ee705d8b28e2d3cb11b8e1874 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 31 Dec 2023 23:04:54 +0000 Subject: [PATCH 058/109] Rename `/inscription_funder/` to `/inscription_with_funder/`. It shows all the inscription data, plus the funder, or why there's no single funder available. --- src/subcommand/server.rs | 74 ++++++++++++++++++++++-------------- src/templates/inscription.rs | 6 +++ templates/inscription.html | 8 ++++ 3 files changed, 60 insertions(+), 28 deletions(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index bec250db32..9036e3e569 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -275,7 +275,7 @@ impl Server { .route("/feed.xml", get(Self::feed)) .route("/input/:block/:transaction/:input", get(Self::input)) .route("/inscription/:inscription_query", get(Self::inscription)) - .route("/inscription_funder/:inscription_id", get(Self::inscription_funder)) + .route("/inscription_with_funder/:inscription_id", get(Self::inscription_with_funder)) .route("/inscriptions", get(Self::inscriptions)) .route("/inscriptions/:page", get(Self::inscriptions_paginated)) .route( @@ -1670,6 +1670,47 @@ impl Server { }, }; + Self::inscription_inner(server_config, index, inscription_id, accept_json, None, None).await + } + + async fn inscription_with_funder( + Extension(index): Extension>, + Extension(server_config): Extension>, + Path(inscription_id): Path, + accept_json: AcceptJson, + ) -> ServerResult { + log::info!("GET /inscription_with_funder/{inscription_id}"); + + let reveal_txid = inscription_id.txid; + let reveal_tx = index.get_transaction(reveal_txid)?.unwrap(); + let reveal_input_count = reveal_tx.input.len(); + let (funder, funder_error) = if reveal_input_count == 1 { + let commit_txid = reveal_tx.input[0].previous_output.txid; + let commit_tx = index.get_transaction(commit_txid)?.unwrap(); + let commit_input_count = commit_tx.input.len(); + if commit_input_count == 1 { + let funder_txid = reveal_tx.input[0].previous_output.txid; + let funder_tx = index.get_transaction(funder_txid)?.unwrap(); + let funder_vout = reveal_tx.input[0].previous_output.vout; + (Some(server_config.chain.address_from_script(&funder_tx.output[funder_vout as usize].script_pubkey).unwrap().to_string()), None) + } else { + (None, Some(format!("commit tx {} has {} inputs", commit_txid, commit_input_count))) + } + } else { + (None, Some(format!("reveal tx {} has {} inputs", reveal_txid, reveal_input_count))) + }; + + Self::inscription_inner(server_config, index, inscription_id, accept_json, funder, funder_error).await + } + + async fn inscription_inner( + server_config: Arc, + index: Arc, + inscription_id: InscriptionId, + accept_json: AcceptJson, + funder: Option, + funder_error: Option, + ) -> ServerResult { let entry = index .get_inscription_entry(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; @@ -1727,6 +1768,8 @@ impl Server { children, inscription_number: entry.inscription_number, metaprotocol: inscription.metaprotocol().map(String::from), + funder, + funder_error, genesis_height: entry.height, parent, genesis_fee: entry.fee, @@ -1755,6 +1798,8 @@ impl Server { chain: server_config.chain, charms, children, + funder, + funder_error, genesis_fee: entry.fee, genesis_height: entry.height, inscription, @@ -1774,33 +1819,6 @@ impl Server { }) } - async fn inscription_funder( - Extension(index): Extension>, - Extension(server_config): Extension>, - Path(inscription_id): Path, - ) -> ServerResult { - log::info!("GET /inscription_funder/{inscription_id}"); - - let reveal_txid = inscription_id.txid; - let reveal_tx = index.get_transaction(reveal_txid)?.unwrap(); - let reveal_input_count = reveal_tx.input.len(); - if reveal_input_count == 1 { - let commit_txid = reveal_tx.input[0].previous_output.txid; - let commit_tx = index.get_transaction(commit_txid)?.unwrap(); - let commit_input_count = commit_tx.input.len(); - if commit_input_count == 1 { - let funder_txid = reveal_tx.input[0].previous_output.txid; - let funder_tx = index.get_transaction(funder_txid)?.unwrap(); - let funder_vout = reveal_tx.input[0].previous_output.vout; - Ok(server_config.chain.address_from_script(&funder_tx.output[funder_vout as usize].script_pubkey).unwrap().to_string()) - } else { - Err(ServerError::BadRequest(format!("commit tx has {} inputs", commit_input_count))) - } - } else { - Err(ServerError::BadRequest(format!("reveal tx has {} inputs", reveal_input_count))) - } - } - async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/src/templates/inscription.rs b/src/templates/inscription.rs index 117050463b..95acdee3e0 100644 --- a/src/templates/inscription.rs +++ b/src/templates/inscription.rs @@ -4,6 +4,8 @@ use super::*; pub(crate) struct InscriptionHtml { pub(crate) chain: Chain, pub(crate) children: Vec, + pub(crate) funder: Option, + pub(crate) funder_error: Option, pub(crate) genesis_fee: u64, pub(crate) genesis_height: u32, pub(crate) inscription: Inscription, @@ -26,6 +28,10 @@ pub struct InscriptionJson { pub children: Vec, pub content_length: Option, pub content_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub funder: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub funder_error: Option, pub genesis_fee: u64, pub genesis_height: u32, pub inscription_id: InscriptionId, diff --git a/templates/inscription.html b/templates/inscription.html index 72917c36a4..8124546af0 100644 --- a/templates/inscription.html +++ b/templates/inscription.html @@ -82,6 +82,14 @@

    Inscription {{ self.inscription_number }}

    %% }
    timestamp
    +%% if let Some(funder) = &self.funder { +
    funder
    +
    {{ funder }}
    +%% } +%% if let Some(funder_error) = &self.funder_error { +
    no funder
    +
    {{ funder_error }}
    +%% }
    genesis height
    {{ self.genesis_height }}
    genesis fee
    From 43df02e3de4a7b2706242741f2381923f4d6c4c6 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 1 Jan 2024 15:40:21 +0000 Subject: [PATCH 059/109] Release 0.13.1-gm5 --- CHANGELOG.md | 10 ++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23fa2e71a1..711406fdad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ Changelog ========= +[0.13.1-gm5](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm5) - 2024-01-01 +---------------------------------------------------------------------------------- + +### Added +- Add `/inscription_with_funder/` endpoint. It shows all the inscription data, plus the funder, or why there's no single funder available. +- Add metaprotocol to the `/inscription/` JSON endpoint. +- Add flag `--index-transfer-history` to index all the inscription movements in each block. Implies `--index-transfers`. +- Add flag `--index-only-first-transfer` to only track the first transfer of each inscription. Implies `--index-transfer-history`. +- Add flag `--filter-metaprotocol` to only index inscriptions that have a metaprotocol that starts with the given string. + [0.13.1-gm4](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm4) - 2023-12-21 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 2696a43d2f..583e1dda08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,7 +2095,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.13.1-gm4" +version = "0.13.1-gm5" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 0826e18377..8288c1db2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1-gm4" +version = "0.13.1-gm5" license = "CC0-1.0" edition = "2021" autotests = false From 66970e6b9b39350d834b1913e26afcf31a89e98e Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 3 Jan 2024 21:27:03 +0000 Subject: [PATCH 060/109] Keep inscriptions with unrecognized even fields unbound after jubilee (#2894) --- src/index/updater/inscription_updater.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index 43b97bef89..49e9fc9a5d 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -188,7 +188,9 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { None }; - let unbound = current_input_value == 0 || curse == Some(Curse::UnrecognizedEvenField); + let unbound = current_input_value == 0 + || curse == Some(Curse::UnrecognizedEvenField) + || inscription.payload.unrecognized_even_field; let offset = inscription .payload From 4e0b98294655d3be8dd3f0778afb72f5c7e920e1 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 3 Jan 2024 21:28:53 +0000 Subject: [PATCH 061/109] Release 0.13.1-gm6 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 711406fdad..304193275f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.13.1-gm6](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm6) - 2024-01-03 +---------------------------------------------------------------------------------- + +### Fixed +- Keep inscriptions with unrecognized even fields unbound after jubilee (#2894) + [0.13.1-gm5](https://github.com/gmart7t2/ord/releases/tag/0.13.1-gm5) - 2024-01-01 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 583e1dda08..e010f67516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2095,7 +2095,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.13.1-gm5" +version = "0.13.1-gm6" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 8288c1db2a..26ffbd0956 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.13.1-gm5" +version = "0.13.1-gm6" license = "CC0-1.0" edition = "2021" autotests = false From 621112778511470bcbd138ad09e28de14a3dc3fb Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 5 Jan 2024 17:45:59 +0000 Subject: [PATCH 062/109] Fix merge --- src/inscriptions/envelope.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index fa3e5e1df3..76836f7733 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -89,17 +89,10 @@ impl From for ParsedEnvelope { } impl ParsedEnvelope { - pub(crate) fn from_transaction(transaction: &Transaction, filter: Option) -> Vec { + pub(crate) fn from_transaction(transaction: &Transaction) -> Vec { RawEnvelope::from_transaction(transaction) .into_iter() .map(|envelope| envelope.into()) - .filter(|envelope: &ParsedEnvelope| match &filter { - Some(filter) => match envelope.payload.metaprotocol.clone() { - Some(metaprotocol) => metaprotocol.to_vec().starts_with(filter.as_bytes()), - None => false, - } - None => true, - }) .collect() } } From 92b26ed88f7ea5dab8aee363dfebcf9913cc38b9 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 5 Jan 2024 17:51:12 +0000 Subject: [PATCH 063/109] Add /inscription/ endpoint logging back in --- src/subcommand/server.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 669a21edce..ed977884e2 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1573,6 +1573,11 @@ impl Server { Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, ) -> ServerResult { + match query { + InscriptionQuery::Id(id) => log::info!("GET /inscription/{id}"), + InscriptionQuery::Number(inscription_number) => log::info!("GET /inscription/{inscription_number}"), + }; + let info = Index::inscription_info(&index, query)?.ok_or_not_found(|| format!("inscription {query}"))?; From 7dce0d8f138bc985529af10a9d0b1a144de8d6aa Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 5 Jan 2024 18:34:25 +0000 Subject: [PATCH 064/109] Fix `/inscriptions_json/` endpoint for lost inscriptions. --- src/subcommand/server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index ed977884e2..b9bf2b2983 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1891,7 +1891,7 @@ impl Server { .get_inscription_satpoint_by_id(inscription_id)? .ok_or_not_found(|| format!("inscription {inscription_id}"))?; - let output = if satpoint.outpoint == unbound_outpoint() { + let output = if satpoint.outpoint.txid == unbound_outpoint().txid { None } else { Some( From 21d70d24ff9fd2d0a8427941e8d375c2d7281a50 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 5 Jan 2024 18:34:29 +0000 Subject: [PATCH 065/109] Release 0.14.1-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a232f71a5c..cca7b3b170 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.14.1-gm1](https://github.com/gmart7t2/ord/releases/tag/0.14.1-gm1) - 2024-01-05 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.13.1 from upstream. + [0.14.1](https://github.com/ordinals/ord/releases/tag/0.14.1) - 2023-01-03 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 25ff414367..1ae0b37272 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2148,7 +2148,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.14.1" +version = "0.14.1-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 293755d3e5..c1980ca6fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.14.1" +version = "0.14.1-gm1" license = "CC0-1.0" edition = "2021" autotests = false From e11bbacedf0b6fa38f5ab16ade8348b965de4718 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 12 Jan 2024 00:31:38 +0000 Subject: [PATCH 066/109] Release 0.15.0-gm1 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 112b7292c3..2d19db9fce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm1) - 2024-01-11 +---------------------------------------------------------------------------------- + +### Added +- Merged 0.15.0 from upstream. + [0.15.0](https://github.com/ordinals/ord/releases/tag/0.15.0) - 2023-01-08 -------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 4882ab9a53..f7664dd236 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0" +version = "0.15.0-gm1" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index ff97614705..b731c03736 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0" +version = "0.15.0-gm1" license = "CC0-1.0" edition = "2021" autotests = false From b5a23631b831a4ac6a27d920f953ce4da484a43d Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 14 Jan 2024 17:46:27 +0000 Subject: [PATCH 067/109] Add `--parent-satpoint` to help with using an unconfirmed parent inscription. --- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 19 ++++++++++++++----- src/subcommand/wallet/inscribe/batch.rs | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 7c1439aebd..ffdfb68497 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -128,6 +128,7 @@ impl Preview { no_broadcast: false, no_limit: false, parent: None, + parent_satpoint: None, postage: Some(TARGET_POSTAGE), reinscribe: false, reveal_input: Vec::new(), @@ -172,6 +173,7 @@ impl Preview { no_broadcast: false, no_limit: false, parent: None, + parent_satpoint: None, postage: Some(TARGET_POSTAGE), reinscribe: false, reveal_input: Vec::new(), diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 85a203df98..72c6d6bdc4 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -115,6 +115,8 @@ pub(crate) struct Inscribe { pub(crate) no_limit: bool, #[clap(long, help = "Make inscription a child of .")] pub(crate) parent: Option, + #[clap(long, help = "The satpoint of the parent inscription, in case it isn't confirmed yet.")] + pub(crate) parent_satpoint: Option, #[arg( long, help = "Amount of postage to include in the inscription. Default `10000sat`." @@ -219,7 +221,7 @@ impl Inscribe { match (self.file, self.batch) { (Some(file), None) => { - parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain)?; + parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint)?; postage = self.postage.unwrap_or(TARGET_POSTAGE); @@ -258,7 +260,7 @@ impl Inscribe { (None, Some(batch)) => { let batchfile = Batchfile::load(&batch)?; - parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain)?; + parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint)?; postage = batchfile .postage @@ -354,9 +356,19 @@ impl Inscribe { utxos: &BTreeMap, client: &Client, chain: Chain, + satpoint: Option, ) -> Result> { if let Some(parent_id) = parent { + let satpoint = if let Some(satpoint) = satpoint { + satpoint + } else { if let Some(satpoint) = index.get_inscription_satpoint_by_id(parent_id)? { + satpoint + } else { + return Err(anyhow!(format!("parent {parent_id} does not exist"))); + } + }; + if !utxos.contains_key(&satpoint.outpoint) { return Err(anyhow!(format!("parent {parent_id} not in wallet"))); } @@ -373,9 +385,6 @@ impl Inscribe { .nth(satpoint.outpoint.vout.try_into().unwrap()) .expect("current transaction output"), })) - } else { - Err(anyhow!(format!("parent {parent_id} does not exist"))) - } } else { Ok(None) } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index e33862b8e6..71e9c481eb 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -137,7 +137,7 @@ impl Batch { }); } - let signed_reveal_tx = if reveal_input_info.is_empty() { + let signed_reveal_tx = if reveal_input_info.is_empty() && self.parent_info.is_none() { consensus::encode::serialize(&reveal_tx) } else { client @@ -764,6 +764,7 @@ pub(crate) struct Batchfile { pub(crate) inscriptions: Vec, pub(crate) mode: Mode, pub(crate) parent: Option, + pub(crate) parent_satpoint: Option, pub(crate) postage: Option, pub(crate) sat: Option, } From 92965e3eff310c114dc3a002484bff126c09a649 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 14 Jan 2024 17:49:35 +0000 Subject: [PATCH 068/109] Release 0.15.0-gm2 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d19db9fce..212798bd83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm2) - 2024-01-14 +---------------------------------------------------------------------------------- + +### Added +- Add `wallet inscribe --parent-satpoint` to help with using an unconfirmed parent inscription. + [0.15.0-gm1](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm1) - 2024-01-11 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index f7664dd236..d32a203255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm1" +version = "0.15.0-gm2" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b731c03736..2f6e6e1441 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm1" +version = "0.15.0-gm2" license = "CC0-1.0" edition = "2021" autotests = false From 6158b82e0fa1357d8317044129a3eb262f7646bd Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 26 Jan 2024 14:46:39 +0000 Subject: [PATCH 069/109] Show progress bar while committing so it doesn't look like the indexer has frozen. --- src/index/updater.rs | 51 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index 4c64bf2b0a..60191f0d3c 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -118,7 +118,7 @@ impl<'index> Updater<'_> { if uncommitted == self.index.options.commit { // eprintln!("\ncommitting after {} blocks at {}", uncommitted, self.height); - self.commit(wtx, value_cache)?; + self.commit(wtx, value_cache, progress_bar.is_some())?; value_cache = HashMap::new(); uncommitted = 0; wtx = self.index.begin_write()?; @@ -151,7 +151,7 @@ impl<'index> Updater<'_> { } if uncommitted > 0 { - self.commit(wtx, value_cache)?; + self.commit(wtx, value_cache, progress_bar.is_some())?; } if let Some(progress_bar) = &mut progress_bar { @@ -715,7 +715,7 @@ impl<'index> Updater<'_> { Ok(()) } - fn commit(&mut self, wtx: WriteTransaction, value_cache: HashMap) -> Result { + fn commit(&mut self, wtx: WriteTransaction, value_cache: HashMap, use_progress_bar: bool) -> Result { log::info!( "Committing at block height {}, {} outputs traversed, {} in map, {} cached", self.height, @@ -734,8 +734,22 @@ impl<'index> Updater<'_> { let mut outpoint_to_sat_ranges = wtx.open_table(OUTPOINT_TO_SAT_RANGES)?; + let progress_bar = if use_progress_bar { + let progress_bar = ProgressBar::new(self.range_cache.len() as u64); + progress_bar.set_position(0); + progress_bar.set_style( + ProgressStyle::with_template(format!("[committing ranges at block {}] {{wide_bar}} {{pos}}/{{len}}", self.height).as_str()).unwrap(), + ); + Some(progress_bar) + } else { + None + }; + for (outpoint, sat_range) in self.range_cache.drain() { outpoint_to_sat_ranges.insert(&outpoint, sat_range.as_slice())?; + if let Some(progress_bar) = &progress_bar { + progress_bar.inc(1); + } } self.outputs_inserted_since_flush = 0; @@ -744,8 +758,22 @@ impl<'index> Updater<'_> { { let mut outpoint_to_value = wtx.open_table(OUTPOINT_TO_VALUE)?; + let progress_bar = if use_progress_bar { + let progress_bar = ProgressBar::new(value_cache.len() as u64); + progress_bar.set_position(0); + progress_bar.set_style( + ProgressStyle::with_template(format!("[committing values at block {}] {{wide_bar}} {{pos}}/{{len}}", self.height).as_str()).unwrap(), + ); + Some(progress_bar) + } else { + None + }; + for (outpoint, value) in value_cache { outpoint_to_value.insert(&outpoint.store(), &value)?; + if let Some(progress_bar) = &progress_bar { + progress_bar.inc(1); + } } } @@ -754,8 +782,25 @@ impl<'index> Updater<'_> { Index::increment_statistic(&wtx, Statistic::SatRanges, self.sat_ranges_since_flush)?; self.sat_ranges_since_flush = 0; Index::increment_statistic(&wtx, Statistic::Commits, 1)?; + + let progress_bar = if use_progress_bar { + let progress_bar = ProgressBar::new(1); + progress_bar.set_position(0); + progress_bar.set_style( + ProgressStyle::with_template(format!("[committing db at block {}] {{wide_bar}} {{pos}}/{{len}}", self.height).as_str()).unwrap(), + ); + progress_bar.inc(0); + Some(progress_bar) + } else { + None + }; + wtx.commit()?; + if let Some(progress_bar) = &progress_bar { + progress_bar.inc(1); + } + Reorg::update_savepoints(self.index, self.height)?; Ok(()) From 9a050b967be806f4797a50982f04929f6d58df56 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 26 Jan 2024 14:49:31 +0000 Subject: [PATCH 070/109] Release 0.15.0-gm3 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 212798bd83..eabf86c496 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm3](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm3) - 2024-01-26 +---------------------------------------------------------------------------------- + +### Added +- Show progress bar while committing so it doesn't look like the indexer has frozen. + [0.15.0-gm2](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm2) - 2024-01-14 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index d32a203255..a74cef96f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm2" +version = "0.15.0-gm3" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 2f6e6e1441..a4a84b9da5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm2" +version = "0.15.0-gm3" license = "CC0-1.0" edition = "2021" autotests = false From 04b25012a8625d8e2e17a0a6cdd3ff342a262093 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 26 Jan 2024 15:26:02 +0000 Subject: [PATCH 071/109] Suspend indexer progress bar while showing commit progress bars to avoid interference between them. --- src/index/updater.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index 60191f0d3c..d13063d0f7 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -118,7 +118,13 @@ impl<'index> Updater<'_> { if uncommitted == self.index.options.commit { // eprintln!("\ncommitting after {} blocks at {}", uncommitted, self.height); - self.commit(wtx, value_cache, progress_bar.is_some())?; + if progress_bar.is_some() { + progress_bar.clone().unwrap().suspend(|| { + self.commit(wtx, value_cache, true) + })?; + } else { + self.commit(wtx, value_cache, false)? + } value_cache = HashMap::new(); uncommitted = 0; wtx = self.index.begin_write()?; From 2d56027468fbc6090e2fa566679fe0d5882efb00 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 26 Jan 2024 15:27:05 +0000 Subject: [PATCH 072/109] Release 0.15.0-gm4 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eabf86c496..e2fc67cdde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm4](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm4) - 2024-01-26 +---------------------------------------------------------------------------------- + +### Added +- Suspend indexer progress bar while showing commit progress bars to avoid interference between them. + [0.15.0-gm3](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm3) - 2024-01-26 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index a74cef96f7..2d34af0e47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm3" +version = "0.15.0-gm4" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index a4a84b9da5..4206c8b702 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm3" +version = "0.15.0-gm4" license = "CC0-1.0" edition = "2021" autotests = false From 892545138bf39b54fe45e52f74cd79951ab3b46a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 28 Jan 2024 11:46:12 +0000 Subject: [PATCH 073/109] Suspend indexer progress bar while committing at end of indexing too. --- src/index/updater.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index/updater.rs b/src/index/updater.rs index d13063d0f7..7a6df6437d 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -157,7 +157,13 @@ impl<'index> Updater<'_> { } if uncommitted > 0 { - self.commit(wtx, value_cache, progress_bar.is_some())?; + if progress_bar.is_some() { + progress_bar.clone().unwrap().suspend(|| { + self.commit(wtx, value_cache, true) + })?; + } else { + self.commit(wtx, value_cache, false)? + } } if let Some(progress_bar) = &mut progress_bar { From e1e394f3a89b14ed1d4b586e03009a8b8a2fbf46 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 28 Jan 2024 11:47:05 +0000 Subject: [PATCH 074/109] Release 0.15.0-gm5 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2fc67cdde..dcfbfc6acb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm5](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm5) - 2024-01-28 +---------------------------------------------------------------------------------- + +### Added +- Suspend indexer progress bar while committing at end of indexing too. + [0.15.0-gm4](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm4) - 2024-01-26 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 2d34af0e47..49b180c665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm4" +version = "0.15.0-gm5" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 4206c8b702..ad250b16d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm4" +version = "0.15.0-gm5" license = "CC0-1.0" edition = "2021" autotests = false From d7b9bc3e79d996f322e0a2e87da7287608f0f6a4 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 28 Jan 2024 14:01:49 +0000 Subject: [PATCH 075/109] Add flag `--ignore-txt-and-json` to ignore text and json inscriptions. --- src/index.rs | 4 ++-- src/index/updater.rs | 1 + src/index/updater/inscription_updater.rs | 3 ++- src/inscriptions/envelope.rs | 8 +++++++- src/options.rs | 2 ++ src/subcommand/decode.rs | 2 +- 6 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/index.rs b/src/index.rs index ce2395594d..de20de5f97 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1438,7 +1438,7 @@ impl Index { } Ok(self.get_transaction(inscription_id.txid)?.and_then(|tx| { - ParsedEnvelope::from_transaction(&tx) + ParsedEnvelope::from_transaction(&tx, false) .into_iter() .nth(inscription_id.index as usize) .map(|envelope| envelope.payload) @@ -2033,7 +2033,7 @@ impl Index { return Ok(None); }; - let Some(inscription) = ParsedEnvelope::from_transaction(&transaction) + let Some(inscription) = ParsedEnvelope::from_transaction(&transaction, false) .into_iter() .nth(entry.id.index as usize) .map(|envelope| envelope.payload) diff --git a/src/index/updater.rs b/src/index/updater.rs index 7a6df6437d..27fb17ed2c 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -456,6 +456,7 @@ impl<'index> Updater<'_> { home_inscriptions: &mut home_inscriptions, id_to_sequence_number: &mut inscription_id_to_sequence_number, ignore_cursed: index.options.ignore_cursed, + ignore_txt_and_json: index.options.ignore_txt_and_json, index_transactions: self.index.index_transactions, inscription_number_to_sequence_number: &mut inscription_number_to_sequence_number, lost_sats, diff --git a/src/index/updater/inscription_updater.rs b/src/index/updater/inscription_updater.rs index b81a3ae9ca..1038ebd7c6 100644 --- a/src/index/updater/inscription_updater.rs +++ b/src/index/updater/inscription_updater.rs @@ -48,6 +48,7 @@ pub(super) struct InscriptionUpdater<'a, 'db, 'tx> { pub(super) home_inscriptions: &'a mut Table<'db, 'tx, u32, InscriptionIdValue>, pub(super) id_to_sequence_number: &'a mut Table<'db, 'tx, InscriptionIdValue, u32>, pub(super) ignore_cursed: bool, + pub(super) ignore_txt_and_json: bool, pub(super) index_transactions: bool, pub(super) inscription_number_to_sequence_number: &'a mut Table<'db, 'tx, i32, u32>, pub(super) lost_sats: u64, @@ -85,7 +86,7 @@ impl<'a, 'db, 'tx> InscriptionUpdater<'a, 'db, 'tx> { let total_output_value = tx.output.iter().map(|txout| txout.value).sum::(); self.tx_count += 1; - let envelopes = ParsedEnvelope::from_transaction(tx); + let envelopes = ParsedEnvelope::from_transaction(tx, self.ignore_txt_and_json); let inscriptions = !envelopes.is_empty(); let mut envelopes = envelopes.into_iter().peekable(); diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 76836f7733..0ae38a74e9 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -89,10 +89,16 @@ impl From for ParsedEnvelope { } impl ParsedEnvelope { - pub(crate) fn from_transaction(transaction: &Transaction) -> Vec { + pub(crate) fn from_transaction(transaction: &Transaction, ignore_txt_and_json: bool) -> Vec { RawEnvelope::from_transaction(transaction) .into_iter() .map(|envelope| envelope.into()) + .filter(|envelope: &ParsedEnvelope| !ignore_txt_and_json || match envelope.payload.content_type.clone() { + Some(content_type) => + !content_type.to_vec().starts_with("text/plain".as_bytes()) && + !content_type.to_vec().starts_with("application/json".as_bytes()), + None => true, + }) .collect() } } diff --git a/src/options.rs b/src/options.rs index afe14af88a..b24fcd4d63 100644 --- a/src/options.rs +++ b/src/options.rs @@ -78,6 +78,8 @@ pub struct Options { pub(crate) ignore_cursed: bool, #[arg(long, default_value = "5000", help = "Commit changes to the index file on disk every blocks.")] pub(crate) commit: usize, + #[arg(long, help = "Ignore text and json inscriptions.")] + pub(crate) ignore_txt_and_json: bool, } impl Options { diff --git a/src/subcommand/decode.rs b/src/subcommand/decode.rs index 38d2275524..e31d1b8cfc 100644 --- a/src/subcommand/decode.rs +++ b/src/subcommand/decode.rs @@ -85,7 +85,7 @@ impl Decode { Transaction::consensus_decode(&mut io::stdin())? }; - let inscriptions = ParsedEnvelope::from_transaction(&transaction); + let inscriptions = ParsedEnvelope::from_transaction(&transaction, false); if self.compact { Ok(Box::new(CompactOutput { From b6c4053ab553f164ea0edd6909dc841bbd933749 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 28 Jan 2024 14:03:49 +0000 Subject: [PATCH 076/109] Release 0.15.0-gm6 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcfbfc6acb..b70db81af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm6](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm6) - 2024-01-28 +---------------------------------------------------------------------------------- + +### Added +- Add flag `--ignore-txt-and-json` to ignore text and json inscriptions. + [0.15.0-gm5](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm5) - 2024-01-28 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 49b180c665..1668b6bcd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm5" +version = "0.15.0-gm6" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index ad250b16d5..6b09e45ebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm5" +version = "0.15.0-gm6" license = "CC0-1.0" edition = "2021" autotests = false From 406a5ed5400c587df22fb2b8d36dbc473ec786b9 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 29 Jan 2024 21:47:07 +0000 Subject: [PATCH 077/109] No need to convert content_type to a vector. --- src/inscriptions/envelope.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 0ae38a74e9..435e22ad94 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -95,8 +95,8 @@ impl ParsedEnvelope { .map(|envelope| envelope.into()) .filter(|envelope: &ParsedEnvelope| !ignore_txt_and_json || match envelope.payload.content_type.clone() { Some(content_type) => - !content_type.to_vec().starts_with("text/plain".as_bytes()) && - !content_type.to_vec().starts_with("application/json".as_bytes()), + !content_type.starts_with("text/plain".as_bytes()) && + !content_type.starts_with("application/json".as_bytes()), None => true, }) .collect() From b8fbd56796832ac7592185ebe8f77d312b9a8b82 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 14:20:20 +0000 Subject: [PATCH 078/109] Add `/inscribe` endpoint. --- Cargo.lock | 1 + Cargo.toml | 2 + src/index.rs | 4 + src/inscriptions/envelope.rs | 1 + src/inscriptions/inscription.rs | 3 + src/subcommand/preview.rs | 4 + src/subcommand/server.rs | 19 +- src/subcommand/wallet/inscribe.rs | 352 +++++++++++++++++-- src/subcommand/wallet/inscribe/batch.rs | 299 +++++++++++++--- src/subcommand/wallet/send.rs | 5 +- src/subcommand/wallet/transaction_builder.rs | 98 ++++-- 11 files changed, 685 insertions(+), 103 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1668b6bcd6..d0d2dc5225 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2198,6 +2198,7 @@ dependencies = [ "tokio-util 0.7.10", "tower-http", "unindent", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6b09e45ebd..627dd70757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ mp4 = "0.14.0" ord-bitcoincore-rpc = "0.17.1" redb = "1.4.0" regex = "1.6.0" +reqwest = { version = "0.11.10", features = ["blocking"] } rss = "2.0.1" rust-embed = "8.0.0" rustls = "0.22.0" @@ -65,6 +66,7 @@ tokio = { version = "1.17.0", features = ["rt-multi-thread"] } tokio-stream = "0.1.9" tokio-util = {version = "0.7.3", features = ["compat"] } tower-http = { version = "0.4.0", features = ["compression-br", "compression-gzip", "cors", "set-header"] } +url = "2.5.0" [dev-dependencies] criterion = "0.5.1" diff --git a/src/index.rs b/src/index.rs index de20de5f97..a31f434432 100644 --- a/src/index.rs +++ b/src/index.rs @@ -1060,6 +1060,10 @@ impl Index { Ok(result) } + pub(crate) fn client(&self) -> &Client { + &self.client + } + pub(crate) fn block_header(&self, hash: BlockHash) -> Result> { self.client.get_block_header(&hash).into_option() } diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 435e22ad94..e18b3f9586 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -79,6 +79,7 @@ impl From for ParsedEnvelope { parent, pointer, unrecognized_even_field, + utxo: None, }, input: envelope.input, offset: envelope.offset, diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 42eaa2d2b1..acb8dd7ec6 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -27,6 +27,7 @@ pub struct Inscription { pub parent: Option>, pub pointer: Option>, pub unrecognized_even_field: bool, + pub utxo: Option, } impl Inscription { @@ -47,6 +48,7 @@ impl Inscription { metaprotocol: Option, metadata: Option>, compress: bool, + utxo: Option, ) -> Result { let path = path.as_ref(); @@ -105,6 +107,7 @@ impl Inscription { metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), parent: parent.map(|id| id.value()), pointer: pointer.map(Self::pointer_value), + utxo, ..Default::default() }) } diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index ffdfb68497..b5bac14240 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -113,6 +113,7 @@ impl Preview { commit_fee_rate: None, commit_input: Vec::new(), commit_only: false, + commit_vsize: None, commitment: None, compress: false, destination: None, @@ -127,6 +128,7 @@ impl Preview { no_backup: true, no_broadcast: false, no_limit: false, + no_wallet: false, parent: None, parent_satpoint: None, postage: Some(TARGET_POSTAGE), @@ -158,6 +160,7 @@ impl Preview { commit_fee_rate: None, commit_input: Vec::new(), commit_only: false, + commit_vsize: None, commitment: None, compress: false, destination: None, @@ -172,6 +175,7 @@ impl Preview { no_backup: true, no_broadcast: false, no_limit: false, + no_wallet: false, parent: None, parent_satpoint: None, postage: Some(TARGET_POSTAGE), diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 7b4bb9e0a7..6f4d42bab9 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1,4 +1,5 @@ use { + self::wallet::inscribe::Inscribe, self::{ accept_encoding::AcceptEncoding, accept_json::AcceptJson, @@ -254,7 +255,7 @@ impl Server { log::warn!("Updating index: {error}"); } } - thread::sleep(Duration::from_millis(5000)); + thread::sleep(Duration::from_millis(500)); }); INDEXER.lock().unwrap().replace(index_thread); @@ -294,6 +295,7 @@ impl Server { .route("/favicon.ico", get(Self::favicon)) .route("/feed.xml", get(Self::feed)) .route("/input/:block/:transaction/:input", get(Self::input)) + .route("/inscribe", post(Self::inscribe)) .route("/inscription/:inscription_query", get(Self::inscription)) .route("/inscriptions", get(Self::inscriptions)) .route("/inscriptions/:page", get(Self::inscriptions_paginated)) @@ -1645,6 +1647,21 @@ impl Server { }) } + async fn inscribe( + Extension(server_config): Extension>, + Extension(index): Extension>, + Json(data): Json + ) -> ServerResult { + task::block_in_place(|| { + log::info!("POST /inscribe"); + + match Inscribe::inscribe_for_server(data.clone(), server_config.chain, index) { + Ok(result) => Ok(Json(result).into_response()), + Err(str) => Err(ServerError::BadRequest(format!("error: {str}"))), + } + }) + } + async fn inscription( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 72c6d6bdc4..b35c160347 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,12 +1,14 @@ use { - self::batch::{Batch, Batchfile, Mode}, + self::batch::{Batch, BatchEntry, Batchfile, Mode}, super::*, crate::subcommand::wallet::transaction_builder::Target, + base64::{Engine as _, engine::general_purpose}, bitcoin::{ blockdata::{opcodes, script}, key::PrivateKey, key::{TapTweak, TweakedKeyPair, TweakedPublicKey, UntweakedKeyPair}, policy::MAX_STANDARD_TX_WEIGHT, + psbt::Psbt, secp256k1::{self, constants::SCHNORR_SIGNATURE_SIZE, rand, Secp256k1, XOnlyPublicKey}, sighash::{Prevouts, SighashCache, TapSighashType}, taproot::Signature, @@ -16,6 +18,9 @@ use { bitcoincore_rpc::Client, bitcoincore_rpc::RawTx, std::collections::BTreeSet, + std::io::Write, + tempfile::tempdir, + url::Url, }; mod batch; @@ -26,14 +31,23 @@ pub struct InscriptionInfo { pub location: SatPoint, } -#[derive(Serialize, Deserialize)] +fn is_zero(n: &u64) -> bool { + *n == 0 +} + +#[derive(Serialize, Deserialize, Debug)] pub struct Output { #[serde(skip_serializing_if = "Option::is_none")] pub commit: Option, #[serde(skip_serializing_if = "Option::is_none")] pub commit_hex: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub commit_psbt: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] pub inscriptions: Vec, #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub parent: Option, #[serde(skip_serializing_if = "Option::is_none")] pub recovery_descriptor: Option, @@ -41,6 +55,7 @@ pub struct Output { pub reveal: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reveal_hex: Option, + #[serde(skip_serializing_if = "is_zero")] pub total_fees: u64, } @@ -144,6 +159,10 @@ pub(crate) struct Inscribe { pub(crate) commit_input: Vec, #[arg(long, help = "Inscribe .", conflicts_with = "satpoint")] pub(crate) sat: Option, + #[arg(long, help = "Don't use a local wallet. Leave the commit transaction unsigned instead.")] + pub(crate) no_wallet: bool, + #[arg(long, help = "Specify the vsize of the commit tx, for when we don't have a local wallet to sign with.")] + pub(crate) commit_vsize: Option, } impl Inscribe { @@ -179,7 +198,14 @@ impl Inscribe { let index = Index::open(&options)?; index.update()?; - let client = bitcoin_rpc_client_for_wallet_command(wallet, &options)?; + let (mut utxos, locked_utxos, runic_utxos, client) = if self.no_wallet { + let utxos = BTreeMap::new(); + let locked_utxos = BTreeSet::new(); + let runic_utxos = BTreeSet::new(); + let client = check_version(options.bitcoin_rpc_client(None)?)?; + (utxos, locked_utxos, runic_utxos, client) + } else { + let client = bitcoin_rpc_client_for_wallet_command(wallet, &options)?; let mut utxos = if self.coin_control { BTreeMap::new() @@ -204,6 +230,9 @@ impl Inscribe { ); } + (utxos, locked_utxos, runic_utxos, client) + }; + let chain = options.chain(); let change = match self.change { @@ -213,6 +242,8 @@ impl Inscribe { let postage; let destinations; + let fee_utxos; + let inscribe_on_specific_utxos; let inscriptions; let mode; let parent_info; @@ -221,7 +252,7 @@ impl Inscribe { match (self.file, self.batch) { (Some(file), None) => { - parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint)?; + parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint, self.no_wallet)?; postage = self.postage.unwrap_or(TARGET_POSTAGE); @@ -233,6 +264,7 @@ impl Inscribe { self.metaprotocol.clone(), metadata.clone(), self.compress, + None, )?]; next_inscription = if self.next_file.is_some() { Some(Inscription::from_file( @@ -243,6 +275,7 @@ impl Inscribe { self.metaprotocol, metadata, self.compress, + None, )?) } else { None @@ -256,24 +289,28 @@ impl Inscribe { Some(destination) => destination.require_network(chain.network())?, None => get_change_address(&client, chain)?, }]; + + inscribe_on_specific_utxos = false; + fee_utxos = Vec::new(); } (None, Some(batch)) => { let batchfile = Batchfile::load(&batch)?; - parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint)?; + parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet)?; postage = batchfile .postage .map(Amount::from_sat) .unwrap_or(TARGET_POSTAGE); - (inscriptions, destinations) = batchfile.inscriptions( + (inscriptions, destinations, inscribe_on_specific_utxos, fee_utxos) = batchfile.inscriptions( &client, chain, parent_info.as_ref().map(|info| info.tx_out.value), metadata, postage, self.compress, + &mut utxos, )?; next_inscription = None; @@ -302,9 +339,10 @@ impl Inscribe { self.satpoint }; - Batch { + let result = Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), commit_only: self.commit_only, + commit_vsize: self.commit_vsize, commitment: self.commitment, commitment_output: if self.commitment.is_some() { Some(client.get_raw_transaction_info(&self.commitment.unwrap().txid, None)?.vout[self.commitment.unwrap().vout as usize].clone()) @@ -314,6 +352,8 @@ impl Inscribe { destinations, dump, dry_run: self.dry_run, + fee_utxos, + inscribe_on_specific_utxos, inscriptions, key: self.key, mode, @@ -321,6 +361,7 @@ impl Inscribe { no_backup, no_broadcast: self.no_broadcast, no_limit: self.no_limit, + no_wallet: self.no_wallet, parent_info, postage, reinscribe: self.reinscribe, @@ -328,7 +369,8 @@ impl Inscribe { reveal_input: self.reveal_input, satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &utxos, self.commit_input, change) + .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, self.commit_input, change); + Ok(Box::new(result.unwrap())) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { @@ -357,37 +399,293 @@ impl Inscribe { client: &Client, chain: Chain, satpoint: Option, + no_wallet: bool, ) -> Result> { if let Some(parent_id) = parent { let satpoint = if let Some(satpoint) = satpoint { satpoint } else { - if let Some(satpoint) = index.get_inscription_satpoint_by_id(parent_id)? { - satpoint + if let Some(satpoint) = index.get_inscription_satpoint_by_id(parent_id)? { + satpoint + } else { + return Err(anyhow!(format!("parent {parent_id} does not exist"))); + } + }; + + let tx_out = index + .get_transaction(satpoint.outpoint.txid)? + .expect("parent transaction not found in index") + .output + .into_iter() + .nth(satpoint.outpoint.vout.try_into().unwrap()) + .expect("current transaction output"); + + if !no_wallet && !utxos.contains_key(&satpoint.outpoint) { + return Err(anyhow!(format!("parent {parent_id} not in wallet"))); + } + + let destination = if no_wallet { + chain.address_from_script(&tx_out.script_pubkey)? } else { - return Err(anyhow!(format!("parent {parent_id} does not exist"))); + get_change_address(client, chain)? + }; + + Ok(Some(ParentInfo { + destination, + id: parent_id, + location: satpoint, + tx_out, + })) + } else { + Ok(None) + } + } + + fn fetch_url_into_file( + client: &reqwest::blocking::Client, + url: &str + ) -> Result { + let res = client.get(url).send()?; + + if !res.status().is_success() { + bail!(res.status()); + } + + Ok(res.text().unwrap()) + } + + pub(crate) fn inscribe_for_server( + data: serde_json::Value, + chain: Chain, + index: Arc, + ) -> Result { + let no_wallet = true; + + if !data.is_object() { + return Err(anyhow!("expected object, not {:?}", data)); + } + + let data = data.as_object().unwrap(); + + if !data.contains_key("inscriptions") { + return Err(anyhow!("expected object to contain `inscriptions`")); + } + + if !data.contains_key("fees_utxos") { + return Err(anyhow!("expected object to contain `fees_utxos`")); + } + + let inscriptions = data.get("inscriptions").unwrap(); + let fees_utxos = data.get("fees_utxos").unwrap(); + + let commit_vsize = if data.contains_key("commit_vsize") { + let commit_vsize = data.get("commit_vsize").unwrap(); + if !commit_vsize.is_u64() { + return Err(anyhow!("expected `commit_vsize` to be a u64, not {:?}", commit_vsize)); + } + Some(commit_vsize.as_u64().unwrap()) + } else { + None + }; + + let parent = if data.contains_key("parent") { + let parent = data.get("parent").unwrap(); + if !parent.is_string() { + return Err(anyhow!("expected `parent` to be a string, not {:?}", parent)); + } + let parent = parent.as_str().unwrap(); + match InscriptionId::from_str(parent) { + Ok(parent) => Some(parent), + _ => return Err(anyhow!("expected `parent` to contain valid inscriptionid, not {:?}", parent)), + } + } else { + None + }; + + if !inscriptions.is_array() { + return Err(anyhow!("expected `inscriptions` to be an array, not {:?}", inscriptions)); + } + + if !fees_utxos.is_array() { + return Err(anyhow!("expected `fees_utxos` to be an array, not {:?}", fees_utxos)); + } + + let inscriptions = inscriptions.as_array().unwrap(); + let fees_utxos = fees_utxos.as_array().unwrap(); + + let mut entries = Vec::new(); + let tmpdir = tempdir().unwrap(); + let request_client = reqwest::blocking::Client::builder().build().unwrap(); + + for (i, inscription) in inscriptions.iter().enumerate() { + if !inscription.is_object() { + return Err(anyhow!("expected `inscriptions` to only contain objects, not {:?}", inscription)); + } + + let inscription = inscription.as_object().unwrap(); + + if !inscription.contains_key("file") { + return Err(anyhow!("expected `inscription` to contain `file`")); + } + let file = inscription.get("file").unwrap(); + if !file.is_string() { + return Err(anyhow!("expected `inscriptions[].file` to be a string, not {:?}", file)); + } + let file = file.as_str().unwrap(); + let url = Url::parse(file)?; + let path = PathBuf::from(url.path()); + let ext = match path.extension() { + Some(ext) => ext, + None => return Err(anyhow!("expected URL {:?} path {:?} to have a file extension", file, path)), + }; + let tmpfile = tmpdir.path().join(format!("{i}.{}", ext.to_str().unwrap())); + match Self::fetch_url_into_file(&request_client, file) { + Ok(body) => { + match File::create(tmpfile.clone())?.write(body.as_bytes()) { + Ok(_) => (), + Err(x) => return Err(anyhow!("write error: {}", x)), + } + } + Err(e) => return Err(anyhow!("error fetching {} : {}", file, e)), + }; + + if !inscription.contains_key("utxo") { + return Err(anyhow!("expected `inscription` to contain `utxo`")); + } + let utxo = inscription.get("utxo").unwrap(); + if !utxo.is_string() { + return Err(anyhow!("expected `inscriptions[].utxo` to be a string, not {:?}", utxo)); + } + let utxo = utxo.as_str().unwrap(); + let utxo = match OutPoint::from_str(utxo) { + Ok(utxo) => utxo, + _ => return Err(anyhow!("expected `inscriptions[].utxo` to be a valid utxo, not {:?}", utxo)), + }; + + if !inscription.contains_key("destination") { + return Err(anyhow!("expected `inscription` to contain `destination`")); + } + let destination = inscription.get("destination").unwrap(); + if !destination.is_string() { + return Err(anyhow!("expected `inscriptions[].destination` to be a string, not {:?}", destination)); } + let destination = destination.as_str().unwrap(); + let destination: Address = match destination.parse() { + Ok(destination) => destination, + Err(_) => return Err(anyhow!("expected `inscriptions[].destination` to be a valid address, not {:?}", destination)), }; - if !utxos.contains_key(&satpoint.outpoint) { - return Err(anyhow!(format!("parent {parent_id} not in wallet"))); + /* we don't need to check addresses for the correct network type here, because the batch file expects unchecked addresses + let destination: Address = match destination.clone().require_network(chain.network()) { + Ok(destination) => destination, + Err(_) => return Err(anyhow!("expected `inscriptions[].destination` to be valid for the current chain, not {:?}", destination)), + }; + */ + + entries.push(BatchEntry { + destination: Some(destination), + file: tmpfile.into(), + metadata: None, + metaprotocol: None, + utxo: Some(utxo), + }); + } + + let mut fees = Vec::new(); + + for fees_utxo in fees_utxos { + if !fees_utxo.is_string() { + return Err(anyhow!("expected `fees_utxos` to only contain strings, not {:?}", fees_utxo)); + } + + let fees_utxo = fees_utxo.as_str().unwrap(); + let fees_utxo = match OutPoint::from_str(fees_utxo) { + Ok(fees_utxo) => fees_utxo, + _ => return Err(anyhow!("expected `fees_utxos` to contain valid utxos, not {:?}", fees_utxo)), + }; + + fees.push(fees_utxo); + } + + let batchfile = Batchfile { + fees: Some(fees), + inscriptions: entries, + mode: Mode::SeparateOutputs, + parent, + ..Default::default() + }; + + let mut utxos = BTreeMap::new(); + let locked_utxos = BTreeSet::new(); + let runic_utxos = BTreeSet::new(); + let client = index.client(); + + let change = None; + + let postage; + let destinations; + let fee_utxos; + let inscribe_on_specific_utxos; + let inscriptions; + let mode; + let parent_info; + let next_inscription; + + let compress = false; + + parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, no_wallet)?; + + postage = batchfile + .postage + .map(Amount::from_sat) + .unwrap_or(TARGET_POSTAGE); + + (inscriptions, destinations, inscribe_on_specific_utxos, fee_utxos) = batchfile.inscriptions( + &client, + chain, + parent_info.as_ref().map(|info| info.tx_out.value), + None, + Amount::from_sat(0), + compress, + &mut utxos, + )?; + next_inscription = None; + + mode = batchfile.mode; + + if batchfile.sat.is_some() && mode != Mode::SameSat { + return Err(anyhow!("`sat` can only be set in `same-sat` mode")); } - Ok(Some(ParentInfo { - destination: get_change_address(client, chain)?, - id: parent_id, - location: satpoint, - tx_out: index - .get_transaction(satpoint.outpoint.txid)? - .expect("parent transaction not found in index") - .output - .into_iter() - .nth(satpoint.outpoint.vout.try_into().unwrap()) - .expect("current transaction output"), - })) - } else { - Ok(None) + let satpoint = None; + + Batch { + commit_fee_rate: FeeRate::try_from(0.0).unwrap(), + commit_only: false, + commit_vsize, + commitment: None, + commitment_output: None, + destinations, + dump: true, + dry_run: false, + fee_utxos, + inscribe_on_specific_utxos, + inscriptions, + key: None, + mode, + next_inscription, + no_backup: true, + no_broadcast: true, + no_limit: false, + no_wallet, + parent_info, + postage, + reinscribe: false, + reveal_fee_rate: FeeRate::try_from(0.0).unwrap(), + reveal_input: Vec::new(), + satpoint, } + .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, Vec::new(), change) } } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 71e9c481eb..ae1b943300 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -3,11 +3,14 @@ use super::*; pub(super) struct Batch { pub(super) commit_fee_rate: FeeRate, pub(super) commit_only: bool, + pub(super) commit_vsize: Option, pub(super) commitment: Option, pub(super) commitment_output: Option, pub(super) destinations: Vec
    , pub(super) dump: bool, pub(super) dry_run: bool, + pub(super) fee_utxos: Vec, + pub(super) inscribe_on_specific_utxos: bool, pub(super) inscriptions: Vec, pub(super) key: Option, pub(super) mode: Mode, @@ -15,6 +18,7 @@ pub(super) struct Batch { pub(super) no_backup: bool, pub(super) no_broadcast: bool, pub(super) no_limit: bool, + pub(super) no_wallet: bool, pub(super) parent_info: Option, pub(super) postage: Amount, pub(super) reinscribe: bool, @@ -28,11 +32,14 @@ impl Default for Batch { Batch { commit_fee_rate: 1.0.try_into().unwrap(), commit_only: false, + commit_vsize: None, commitment: None, commitment_output: None, destinations: Vec::new(), dump: false, dry_run: false, + fee_utxos: Vec::new(), + inscribe_on_specific_utxos: false, inscriptions: Vec::new(), key: None, mode: Mode::SharedOutput, @@ -40,6 +47,7 @@ impl Default for Batch { no_backup: false, no_broadcast: false, no_limit: false, + no_wallet: false, parent_info: None, postage: Amount::from_sat(10_000), reinscribe: false, @@ -58,21 +66,50 @@ impl Batch { client: &Client, locked_utxos: &BTreeSet, runic_utxos: BTreeSet, - utxos: &BTreeMap, + utxos: &mut BTreeMap, force_input: Vec, change: Option
    , - ) -> SubcommandResult { + ) -> Result { + let use_psbt_for_commit = true; // when not signing the commit, should we use psbt or hex for the unsigned commit tx? + let wallet_inscriptions = index.get_inscriptions(utxos)?; - let commit_tx_change = [ + if !self.fee_utxos.is_empty() { + if self.reveal_fee_rate != FeeRate::try_from(0.0)? { + return Err(anyhow!("use `--fee-rate 0` when using specific utxos to pay fees; the rate will be calculated from the size of the fee utxo(s)")); + } + if self.commit_fee_rate != FeeRate::try_from(0.0)? { + return Err(anyhow!("don't use `--commit-fee-rate` when using specific utxos to pay fees; the rate will be calculated from the size of the fee utxo(s)")); + } + if !force_input.is_empty() { + return Err(anyhow!("don't use `--commit-input` when using specific utxos to pay fees")); + } + + for outpoint in &self.fee_utxos { + if !utxos.contains_key(&outpoint) { + utxos.insert(*outpoint, Amount::from_sat(client.get_raw_transaction(&outpoint.txid, None)?.output[outpoint.vout as usize].value)); + } + } + } + + let force_input = if self.fee_utxos.is_empty() { + force_input + } else { + self.fee_utxos.clone() + }; + + let commit_tx_change = if self.no_wallet { + None + } else { + Some([ get_change_address(client, chain)?, match change { Some(change) => change, None => get_change_address(client, chain)?, }, - ]; + ])}; - let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self + let (commit_tx, reveal_tx, recovery_key_pair, total_fees, dummy_commit_psbt) = self .create_batch_inscription_transactions( wallet_inscriptions, index, @@ -82,10 +119,24 @@ impl Batch { utxos.clone(), commit_tx_change, force_input, + client, )?; + if dummy_commit_psbt.is_some() { + let dummy_commit_psbt = dummy_commit_psbt.unwrap(); + return Ok(self.output(None, None, None, + Some(dummy_commit_psbt), + Some("sign commit_psbt then re-run the /inscribe endpoint with `commit_vsize` in the input JSON set to the vsize of the signed tx; the tx has 0 fees so you can't accidentally broadcast it".to_string()), + None, None, 0, Vec::new(), &BTreeMap::new())); + } + + let commit_tx = commit_tx.unwrap(); + let reveal_tx = reveal_tx.unwrap(); + let recovery_key_pair = recovery_key_pair.unwrap(); + let total_fees = total_fees.unwrap(); + if self.dry_run { - return Ok(Box::new(self.output( + return Ok(self.output( if self.commitment.is_some() { None } else { @@ -99,12 +150,15 @@ impl Batch { None, None, None, + None, + None, total_fees, self.inscriptions.clone(), - ))); + utxos, + )); } - let signed_commit_tx = if self.commitment.is_some() { + let signed_commit_tx = if self.commitment.is_some() || self.no_wallet { Vec::new() } else { client @@ -137,7 +191,7 @@ impl Batch { }); } - let signed_reveal_tx = if reveal_input_info.is_empty() && self.parent_info.is_none() { + let signed_reveal_tx = if (reveal_input_info.is_empty() && self.parent_info.is_none()) || self.no_wallet { consensus::encode::serialize(&reveal_tx) } else { client @@ -149,6 +203,25 @@ impl Batch { .hex }; + if self.no_wallet { + let commit_tx = if use_psbt_for_commit { + general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(commit_tx)?.serialize()) + } else { + commit_tx.raw_hex() + }; + + let reveal_tx = signed_reveal_tx.raw_hex(); + return Ok(self.output(None, None, None, + Some(commit_tx), + Some(if self.parent_info.is_none() { + "sign commit_psbt, then broadcast the signed result and reveal_hex" + } else { + "sign commit_psbt and reveal_hex, then broadcast them both" + }.to_string()), + Some(reveal_tx), + None, 0, Vec::new(), &BTreeMap::new())); + } + if !self.no_backup && self.key.is_none() { Self::backup_recovery_key(client, recovery_key_pair, chain.network())?; } @@ -181,15 +254,17 @@ impl Batch { (commit, reveal) }; - Ok(Box::new(self.output( + Ok(self.output( commit, reveal, if self.dump && self.commitment.is_none() { Some(signed_commit_tx.raw_hex()) } else { None }, + None, None, if self.dump && !self.commit_only { Some(signed_reveal_tx.raw_hex()) } else { None }, if self.dump { Some(Self::get_recovery_key(&client, recovery_key_pair, chain.network())?.to_string()) } else { None }, total_fees, self.inscriptions.clone(), - ))) + utxos, + )) } fn output( @@ -197,12 +272,31 @@ impl Batch { commit: Option, reveal: Option, commit_hex: Option, + commit_psbt: Option, + message: Option, reveal_hex: Option, recovery_descriptor: Option, total_fees: u64, inscriptions: Vec, + utxos: &BTreeMap, ) -> super::Output { + if commit_psbt.is_some() { + return super::Output { + commit: None, + commit_hex: None, + commit_psbt, + inscriptions: Vec::new(), + message, + parent: None, + recovery_descriptor: None, + reveal: None, + reveal_hex, + total_fees: 0, + }; + } + let mut inscriptions_output = Vec::new(); + let mut offset = 0; for index in 0..inscriptions.len() { let index = u32::try_from(index).unwrap(); @@ -223,11 +317,6 @@ impl Batch { } }; - let offset = match self.mode { - Mode::SharedOutput => u64::from(index) * self.postage.to_sat(), - Mode::SeparateOutputs | Mode::SameSat => 0, - }; - if !self.commit_only { inscriptions_output.push(InscriptionInfo { id: InscriptionId { @@ -239,12 +328,22 @@ impl Batch { offset, }, }); - } + } + + if self.mode == Mode::SharedOutput { + offset += if self.inscribe_on_specific_utxos { + utxos[&self.inscriptions[index as usize].utxo.unwrap()] + } else { + self.postage + }.to_sat() + } } super::Output { commit, commit_hex, + commit_psbt: None, + message: None, reveal, reveal_hex, recovery_descriptor, @@ -262,9 +361,10 @@ impl Batch { locked_utxos: BTreeSet, runic_utxos: BTreeSet, mut utxos: BTreeMap, - change: [Address; 2], + change: Option<[Address; 2]>, force_input: Vec, - ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { + client: &Client, + ) -> Result<(Option, Option, Option, Option, Option)> { if let Some(parent_info) = &self.parent_info { assert!(self .inscriptions @@ -272,6 +372,10 @@ impl Batch { .all(|inscription| inscription.parent().unwrap() == parent_info.id)) } + if !self.fee_utxos.is_empty() && !self.inscribe_on_specific_utxos { + return Err(anyhow!("listing utxos to use as fees only works when inscribing on specified utxos")); + } + if self.next_inscription.is_some() && self.commitment.is_none() { return Err(anyhow!("--next-file doesn't work without --commitment")); } @@ -294,6 +398,9 @@ impl Batch { ), } + let satpoints = if self.inscribe_on_specific_utxos { + self.inscriptions.iter().map(|inscription| SatPoint {outpoint: inscription.utxo.unwrap(), offset: 0}).collect::>() + } else { let satpoint = if self.commitment.is_some() { SatPoint::from_str("0000000000000000000000000000000000000000000000000000000000000000:0:0")? } else if let Some(satpoint) = self.satpoint { @@ -311,6 +418,7 @@ impl Batch { && !inscribed_utxos.contains(outpoint) && !locked_utxos.contains(outpoint) && !runic_utxos.contains(outpoint) + && !self.fee_utxos.contains(outpoint) }) .map(|(outpoint, _amount)| SatPoint { outpoint: *outpoint, @@ -318,9 +426,12 @@ impl Batch { }) .ok_or_else(|| anyhow!("wallet contains no cardinal utxos"))? }; + vec![satpoint] + }; let mut reinscription = false; + for satpoint in satpoints.clone() { for (inscribed_satpoint, inscription_id) in &wallet_inscriptions { if *inscribed_satpoint == satpoint { reinscription = true; @@ -338,6 +449,7 @@ impl Batch { )); } } + } if self.reinscribe && !reinscription { return Err(anyhow!( @@ -391,29 +503,43 @@ impl Batch { .finalize(&secp256k1, public_key) .expect("finalizing taproot builder should work"); - Address::p2tr_tweaked(next_taproot_spend_info.output_key(), chain.network()) + Some(Address::p2tr_tweaked(next_taproot_spend_info.output_key(), chain.network())) + } else if change.is_some() { + Some(change.clone().unwrap()[0].clone()) } else { - change[0].clone() + None }; - let total_postage = match self.mode { + let total_postage = if self.inscribe_on_specific_utxos { + self.inscriptions.iter().map(|entry| utxos[&entry.utxo.unwrap()]).sum::() + } else { + match self.mode { Mode::SameSat => self.postage, Mode::SharedOutput | Mode::SeparateOutputs => { self.postage * u64::try_from(self.inscriptions.len()).unwrap() } + } }; let mut reveal_inputs = self.reveal_input.clone(); reveal_inputs.insert(0, OutPoint::null()); + let mut count = 0; let mut reveal_outputs = self .destinations .iter() - .map(|destination| TxOut { - script_pubkey: destination.script_pubkey(), - value: match self.mode { - Mode::SeparateOutputs => self.postage.to_sat(), - Mode::SharedOutput | Mode::SameSat => total_postage.to_sat(), - }, + .map(|destination| { + count += 1; + TxOut { + script_pubkey: destination.script_pubkey(), + value: match self.mode { + Mode::SeparateOutputs => if self.inscribe_on_specific_utxos { + utxos[&self.inscriptions[count - 1].utxo.unwrap()].to_sat() + } else { + self.postage.to_sat() + }, + Mode::SharedOutput | Mode::SameSat => total_postage.to_sat(), + } + } }) .collect::>(); @@ -438,12 +564,12 @@ impl Batch { if self.commitment.is_some() { reveal_outputs.push(TxOut { - script_pubkey: reveal_change_address.script_pubkey(), + script_pubkey: reveal_change_address.unwrap().script_pubkey(), value: 0, }); } - let (_, reveal_fee) = Self::build_reveal_transaction( + let (_, mut reveal_fee, reveal_vsize) = Self::build_reveal_transaction( &control_block, self.reveal_fee_rate, reveal_inputs.clone(), @@ -452,6 +578,51 @@ impl Batch { &reveal_script, ); + let commit_vsize = if self.fee_utxos.is_empty() { + 0 + } else { + let dummy_commit_tx = TransactionBuilder::new( + satpoints.clone(), + wallet_inscriptions.clone(), + utxos.clone(), + locked_utxos.clone(), + runic_utxos.clone(), + commit_tx_address.clone(), + change.clone(), + self.commit_fee_rate, + Target::NoChange(Amount::from_sat(0)), + force_input.clone(), + self.no_wallet, + ).build_transaction()?; + + if self.no_wallet { + if let Some(commit_vsize) = self.commit_vsize { + commit_vsize + } else { + // todo - can we figure out how big this will be after signing without signing it? + let dummy_commit_psbt = general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(dummy_commit_tx)?.serialize()); + return Ok((None, None, None, None, Some(dummy_commit_psbt))); + } + } else { + let dummy_commit_signed = client.sign_raw_transaction_with_wallet(&dummy_commit_tx, None, None)?; + if !dummy_commit_signed.complete { + for error in dummy_commit_signed.errors.unwrap() { + eprintln!("{:#?}", error); + } + bail!("failed to sign dummy commit tx"); + } + client.decode_raw_transaction(&dummy_commit_signed.hex, None)?.vsize as u64 + } + }; + + if !self.fee_utxos.is_empty() { + let fee_utxos_value = self.fee_utxos.iter().map(|outpoint| utxos[&outpoint]).sum::(); + let total_vsize = commit_vsize + reveal_vsize; + // eprintln!("total_vsize {} = commit_vsize {} + reveal_vsize {}", total_vsize, commit_vsize, reveal_vsize); + reveal_fee = (fee_utxos_value * reveal_vsize + Amount::from_sat(total_vsize - 1)) / total_vsize; + // eprintln!("reveal_fee = (fee_utxos {} * reveal_vsize {} + total_vsize {} - 1) / total_vsize {} = reveal_fee {}", fee_utxos_value.to_sat(), reveal_vsize, total_vsize, total_vsize, reveal_fee.to_sat()); + } + let unsigned_commit_tx = if self.commitment.is_some() { Transaction { version: 0, @@ -461,7 +632,7 @@ impl Batch { } } else { TransactionBuilder::new( - satpoint, + satpoints, wallet_inscriptions, utxos.clone(), locked_utxos.clone(), @@ -471,10 +642,13 @@ impl Batch { self.commit_fee_rate, if self.commit_only { Target::NoChange(reveal_fee + total_postage) + } else if !self.fee_utxos.is_empty() { + Target::ChangeIsFee(reveal_fee + total_postage) } else { Target::Value(reveal_fee + total_postage) }, force_input, + self.no_wallet, ) .build_transaction()? }; @@ -512,7 +686,7 @@ impl Batch { vout }; - let (mut reveal_tx, _fee) = Self::build_reveal_transaction( + let (mut reveal_tx, _fee, _vsize) = Self::build_reveal_transaction( &control_block, self.reveal_fee_rate, reveal_inputs, @@ -542,7 +716,10 @@ impl Batch { ]; if let Some(parent_info) = self.parent_info.clone() { - prevouts.insert(0, parent_info.tx_out); + prevouts.insert(0, parent_info.clone().tx_out); + if self.no_wallet { + utxos.insert(parent_info.location.outpoint, Amount::from_sat(parent_info.tx_out.value)); + } } prevouts.extend(reveal_input_prevouts); @@ -621,7 +798,7 @@ impl Batch { Self::calculate_fee(&reveal_tx, &utxos) }; - Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) + Ok((Some(unsigned_commit_tx), Some(reveal_tx), Some(recovery_key_pair), Some(total_fees), None)) } fn get_recovery_key( @@ -675,7 +852,7 @@ impl Batch { commit_input_index: usize, outputs: Vec, script: &Script, - ) -> (Transaction, Amount) { + ) -> (Transaction, Amount, u64) { let reveal_tx = Transaction { input: inputs .iter() @@ -691,7 +868,7 @@ impl Batch { version: 2, }; - let fee = { + let (fee, vsize) = { let mut reveal_tx = reveal_tx.clone(); for (current_index, txin) in reveal_tx.input.iter_mut().enumerate() { @@ -709,10 +886,11 @@ impl Batch { } } - fee_rate.fee(reveal_tx.vsize()) + let vsize = reveal_tx.vsize(); + (fee_rate.fee(vsize), vsize as u64) }; - (reveal_tx, fee) + (reveal_tx, fee, vsize) } fn calculate_fee(tx: &Transaction, utxos: &BTreeMap) -> u64 { @@ -743,6 +921,7 @@ pub(crate) struct BatchEntry { pub(crate) file: PathBuf, pub(crate) metadata: Option, pub(crate) metaprotocol: Option, + pub(crate) utxo: Option, } impl BatchEntry { @@ -761,6 +940,7 @@ impl BatchEntry { #[derive(Deserialize, PartialEq, Debug, Clone, Default)] #[serde(deny_unknown_fields)] pub(crate) struct Batchfile { + pub(crate) fees: Option>, pub(crate) inscriptions: Vec, pub(crate) mode: Mode, pub(crate) parent: Option, @@ -788,7 +968,8 @@ impl Batchfile { metadata: Option>, postage: Amount, compress: bool, - ) -> Result<(Vec, Vec
    )> { + utxos: &mut BTreeMap, + ) -> Result<(Vec, Vec
    , bool, Vec)> { assert!(!self.inscriptions.is_empty()); if self @@ -802,6 +983,32 @@ impl Batchfile { )); } + let inscribe_on_specific_utxos = if self.inscriptions.iter().any(|entry| entry.utxo.is_some()) { + if self.inscriptions.iter().all(|entry| entry.utxo.is_some()) { + true + } else { + return Err(anyhow!("if utxo is set for any inscription it must be set for all inscriptions")) + } + } else { + false + }; + + if inscribe_on_specific_utxos { + if self.postage.is_some() { + return Err(anyhow!("postage size cannot be set when specifying the utxo to inscribe on for each inscription")) + } + + if self.mode == Mode::SameSat { + return Err(anyhow!("Inscription utxos can't be specified in `same-sat` mode")); + } + + for outpoint in self.inscriptions.iter().map(|entry| entry.utxo.unwrap()) { + if !utxos.contains_key(&outpoint) { + utxos.insert(outpoint, Amount::from_sat(client.get_raw_transaction(&outpoint.txid, None)?.output[outpoint.vout as usize].value)); + } + } + } + if metadata.is_some() { assert!(self .inscriptions @@ -824,9 +1031,14 @@ impl Batchfile { None => entry.metadata()?, }, compress, + entry.utxo, )?); - pointer += postage.to_sat(); + if inscribe_on_specific_utxos { + pointer += utxos[&entry.utxo.unwrap()].to_sat(); + } else { + pointer += postage.to_sat(); + } } let destinations = match self.mode { @@ -848,6 +1060,11 @@ impl Batchfile { .collect::, _>>()?, }; - Ok((inscriptions, destinations)) + let fees = match self.fees.clone() { + Some(fees) => fees, + None => Vec::new(), + }; + + Ok((inscriptions, destinations, inscribe_on_specific_utxos, fees)) } } diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 03c64de679..41b53c1372 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -136,16 +136,17 @@ impl Send { }; let unsigned_transaction = TransactionBuilder::new( - satpoint, + vec![satpoint], inscriptions, unspent_outputs, locked_outputs, runic_outputs, address.clone(), - change, + Some(change), self.fee_rate, postage, self.force_input, + false, ) .build_transaction()?; diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index 3e87d04857..c9d52eefbc 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -60,6 +60,7 @@ pub enum Target { Postage, ExactPostage(Amount), NoChange(Amount), + ChangeIsFee(Amount), } impl fmt::Display for Error { @@ -100,7 +101,8 @@ pub struct TransactionBuilder { inputs: Vec, inscriptions: BTreeMap, locked_utxos: BTreeSet, - outgoing: SatPoint, + no_wallet: bool, + outgoing: Vec, outputs: Vec<(Address, Amount)>, recipient: Address, runic_utxos: BTreeSet, @@ -118,37 +120,45 @@ impl TransactionBuilder { pub(crate) const MAX_POSTAGE: Amount = Amount::from_sat(2 * 10_000); pub fn new( - outgoing: SatPoint, + outgoing: Vec, inscriptions: BTreeMap, amounts: BTreeMap, locked_utxos: BTreeSet, runic_utxos: BTreeSet, recipient: Address, - change: [Address; 2], + change: Option<[Address; 2]>, fee_rate: FeeRate, target: Target, force_input: Vec, + no_wallet: bool, ) -> Self { Self { utxos: amounts.keys().cloned().collect(), amounts, - change_addresses: change.iter().cloned().collect(), + change_addresses: match change.clone() { + Some(change) => change.iter().cloned().collect(), + None => BTreeSet::new(), + }, fee_rate, inputs: Vec::new(), force_input: force_input, inscriptions, locked_utxos, + no_wallet, outgoing, outputs: Vec::new(), recipient, runic_utxos, target, - unused_change_addresses: change.to_vec(), + unused_change_addresses: match change { + Some(change) => change.to_vec(), + None => Vec::new(), + }, } } pub fn build_transaction(self) -> Result { - if self.change_addresses.len() < 2 { + if !self.no_wallet && self.change_addresses.len() < 2 { return Err(Error::DuplicateAddress( self.change_addresses.first().unwrap().clone(), )); @@ -183,38 +193,44 @@ impl TransactionBuilder { } fn select_outgoing(mut self) -> Result { - let dust_limit = self + let dust_limit = if self.no_wallet { + 0 + } else { + self .unused_change_addresses .last() .unwrap() .script_pubkey() .dust_value() - .to_sat(); + .to_sat() + }; + for outgoing in self.outgoing.clone() { 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 + if outgoing.outpoint == inscribed_satpoint.outpoint + && outgoing.offset != inscribed_satpoint.offset + && outgoing.offset < inscribed_satpoint.offset + dust_limit { return Err(Error::UtxoContainsAdditionalInscription { - outgoing_satpoint: self.outgoing, + outgoing_satpoint: outgoing, inscribed_satpoint: *inscribed_satpoint, inscription_id: *inscription_id, }); } } + } + + let mut amount = self.outgoing.iter().map(|outgoing| self.amounts[&outgoing.outpoint]).sum::(); - let mut amount = *self - .amounts - .get(&self.outgoing.outpoint) - .ok_or(Error::NotInWallet(self.outgoing))?; + if self.outgoing[0].offset >= amount.to_sat() { + return Err(Error::OutOfRange(self.outgoing[0], amount.to_sat() - 1)); + } - if self.outgoing.offset >= amount.to_sat() { - return Err(Error::OutOfRange(self.outgoing, amount.to_sat() - 1)); + for outgoing in self.outgoing.clone() { + self.utxos.remove(&outgoing.outpoint); + self.inputs.push(outgoing.outpoint); } - self.utxos.remove(&self.outgoing.outpoint); - self.inputs.push(self.outgoing.outpoint); for input in &self.force_input { self.inputs.push(*input); amount += *self.amounts.get(&input).unwrap(); @@ -223,8 +239,8 @@ impl TransactionBuilder { self.outputs.push((self.recipient.clone(), amount)); tprintln!( - "selected outgoing outpoint {} with value {}", - self.outgoing.outpoint, + "selected {} outgoing outpoint(s) with value {}", + self.outgoing.len(), amount.to_sat() ); @@ -294,7 +310,7 @@ impl TransactionBuilder { let min_value = match self.target { Target::Postage => self.outputs.last().unwrap().0.script_pubkey().dust_value(), - Target::Value(value) | Target::ExactPostage(value) | Target::NoChange(value) => value, + Target::Value(value) | Target::ExactPostage(value) | Target::NoChange(value) | Target::ChangeIsFee(value) => value, }; let total = min_value @@ -333,6 +349,12 @@ impl TransactionBuilder { } fn strip_value(mut self) -> Self { + if let Target::ChangeIsFee(value) = self.target { + // for ChangeIsFee, set the output value to be the target value, and don't assign the extra sats to any output, so they end up as fee + self.outputs.last_mut().expect("no outputs found").1 = value; + return self; + } + let sat_offset = self.calculate_sat_offset(); let total_output_amount = self @@ -355,6 +377,7 @@ impl TransactionBuilder { Target::Postage => (Self::MAX_POSTAGE, TARGET_POSTAGE), Target::Value(value) => (value, value), Target::NoChange(_) => (excess, excess), + Target::ChangeIsFee(value) => (value, value), }; if excess > max @@ -482,12 +505,16 @@ impl TransactionBuilder { .collect(), }; + let mut count = 0; + let mut first_sat_offset = 0; + + for outgoing in self.outgoing { assert_eq!( self .amounts .iter() - .filter(|(outpoint, amount)| *outpoint == &self.outgoing.outpoint - && self.outgoing.offset < amount.to_sat()) + .filter(|(outpoint, amount)| *outpoint == &outgoing.outpoint + && outgoing.offset < amount.to_sat()) .count(), 1, "invariant: outgoing sat is contained in utxos" @@ -497,7 +524,7 @@ impl TransactionBuilder { transaction .input .iter() - .filter(|tx_in| tx_in.previous_output == self.outgoing.outpoint) + .filter(|tx_in| tx_in.previous_output == outgoing.outpoint) .count(), 1, "invariant: inputs spend outgoing sat" @@ -506,8 +533,8 @@ impl TransactionBuilder { 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; + if tx_in.previous_output == outgoing.outpoint { + sat_offset += outgoing.offset; found = true; break; } else { @@ -515,6 +542,9 @@ impl TransactionBuilder { } } assert!(found, "invariant: outgoing sat is found in inputs"); + if count == 0 { + first_sat_offset = sat_offset; + } let mut output_end = 0; let mut found = false; @@ -530,6 +560,8 @@ impl TransactionBuilder { } } assert!(found, "invariant: outgoing sat is found in outputs"); + count += 1; + } assert_eq!( transaction @@ -572,7 +604,7 @@ impl TransactionBuilder { "invariant: excess postage is stripped" ); } - Target::Value(value) => { + Target::Value(value) | Target::ChangeIsFee(value) => { assert!( Amount::from_sat(output.value).checked_sub(value).unwrap() <= self @@ -593,7 +625,7 @@ impl TransactionBuilder { } } assert_eq!( - offset, sat_offset, + offset, first_sat_offset, "invariant: sat is at first position in recipient output" ); } else { @@ -623,10 +655,12 @@ impl TransactionBuilder { } let expected_fee = self.fee_rate.fee(modified_tx.vsize()); + if false { // todo assert_eq!( actual_fee, expected_fee, "invariant: fee estimation is correct", ); + } for tx_out in &transaction.output { assert!( @@ -641,8 +675,8 @@ impl TransactionBuilder { 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; + if *outpoint == self.outgoing[0].outpoint { + return sat_offset + self.outgoing[0].offset; } else { sat_offset += self.amounts[outpoint].to_sat(); } From 74856183613640fc818ae7a338c5a22b377d31c9 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 15:27:54 +0000 Subject: [PATCH 079/109] Fix http download error. It needs to be treated as binary, not text. --- src/subcommand/wallet/inscribe.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b35c160347..5e6e8551eb 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -18,7 +18,6 @@ use { bitcoincore_rpc::Client, bitcoincore_rpc::RawTx, std::collections::BTreeSet, - std::io::Write, tempfile::tempdir, url::Url, }; @@ -443,15 +442,23 @@ impl Inscribe { fn fetch_url_into_file( client: &reqwest::blocking::Client, - url: &str - ) -> Result { - let res = client.get(url).send()?; + url: &str, + file: &PathBuf, + ) -> Result { + let mut res = client.get(url).send()?; if !res.status().is_success() { bail!(res.status()); } - Ok(res.text().unwrap()) + match File::create(file) { + Ok(mut fp) => + match res.copy_to(&mut fp) { + Ok(n) => Ok(n), + Err(x) => return Err(anyhow!("write error: {}", x)), + } + Err(x) => return Err(anyhow!("create file error: {}", x)), + } } pub(crate) fn inscribe_for_server( @@ -539,12 +546,10 @@ impl Inscribe { None => return Err(anyhow!("expected URL {:?} path {:?} to have a file extension", file, path)), }; let tmpfile = tmpdir.path().join(format!("{i}.{}", ext.to_str().unwrap())); - match Self::fetch_url_into_file(&request_client, file) { + match Self::fetch_url_into_file(&request_client, file, &tmpfile) { Ok(body) => { - match File::create(tmpfile.clone())?.write(body.as_bytes()) { - Ok(_) => (), - Err(x) => return Err(anyhow!("write error: {}", x)), - } + eprintln!("body is {} bytes", body); + let _ = fs::copy(&tmpfile, "/tmp/file"); } Err(e) => return Err(anyhow!("error fetching {} : {}", file, e)), }; From 2f6887badf37a51f5603c4d5b7125e2c2242b29a Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 15:28:26 +0000 Subject: [PATCH 080/109] Set a user agent header. --- src/subcommand/wallet/inscribe.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 5e6e8551eb..02d7cb08c4 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,3 +1,5 @@ +use reqwest::header::USER_AGENT; +use reqwest::header; use { self::batch::{Batch, BatchEntry, Batchfile, Mode}, super::*, @@ -522,7 +524,9 @@ impl Inscribe { let mut entries = Vec::new(); let tmpdir = tempdir().unwrap(); - let request_client = reqwest::blocking::Client::builder().build().unwrap(); + let mut headers = header::HeaderMap::new(); + headers.insert(USER_AGENT, header::HeaderValue::from_static("ord inscribe endpoint")); + let request_client = reqwest::blocking::Client::builder().default_headers(headers).build().unwrap(); for (i, inscription) in inscriptions.iter().enumerate() { if !inscription.is_object() { From 6d82274feeb1fb653abc8a77fb3c0a9adbb3a929 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 15:30:46 +0000 Subject: [PATCH 081/109] Release 0.15.0-gm7 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70db81af3..f67c595921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm7](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm7) - 2024-01-31 +---------------------------------------------------------------------------------- + +### Added +- Add `/inscribe` server endpoint. + [0.15.0-gm6](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm6) - 2024-01-28 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index d0d2dc5225..048ebc2310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm6" +version = "0.15.0-gm7" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 627dd70757..b50fe70272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm6" +version = "0.15.0-gm7" license = "CC0-1.0" edition = "2021" autotests = false From edf76ef7ebd7958d0176a7d5679a8809561e4858 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 16:34:53 +0000 Subject: [PATCH 082/109] Add metadata for inscription endpoint. --- src/subcommand/wallet/inscribe.rs | 7 +++++++ src/subcommand/wallet/inscribe/batch.rs | 10 +++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 02d7cb08c4..fcbdb0abae 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -571,6 +571,12 @@ impl Inscribe { _ => return Err(anyhow!("expected `inscriptions[].utxo` to be a valid utxo, not {:?}", utxo)), }; + let metadata = if inscription.contains_key("metadata") { + Some(inscription.get("metadata").unwrap().clone()) + } else { + None + }; + if !inscription.contains_key("destination") { return Err(anyhow!("expected `inscription` to contain `destination`")); } @@ -595,6 +601,7 @@ impl Inscribe { destination: Some(destination), file: tmpfile.into(), metadata: None, + metadata_json: metadata, metaprotocol: None, utxo: Some(utxo), }); diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index ae1b943300..5f25bce623 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -920,6 +920,7 @@ pub(crate) struct BatchEntry { pub(crate) destination: Option>, pub(crate) file: PathBuf, pub(crate) metadata: Option, + pub(crate) metadata_json: Option, pub(crate) metaprotocol: Option, pub(crate) utxo: Option, } @@ -927,7 +928,14 @@ pub(crate) struct BatchEntry { impl BatchEntry { pub(crate) fn metadata(&self) -> Result>> { Ok(match &self.metadata { - None => None, + None => match &self.metadata_json { + Some(metadata) => { + let mut cbor = Vec::new(); + ciborium::into_writer(&metadata, &mut cbor)?; + Some(cbor) + } + None => None, + } Some(metadata) => { let mut cbor = Vec::new(); ciborium::into_writer(&metadata, &mut cbor)?; From ce4193c2a6d2e081845ccade9bc74f6256f4f550 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 31 Jan 2024 16:36:12 +0000 Subject: [PATCH 083/109] Release 0.15.0-gm8 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f67c595921..3709d5f1ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm8](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm8) - 2024-01-31 +---------------------------------------------------------------------------------- + +### Added +- Allow setting metadata via `/inscribe` endpoint. + [0.15.0-gm7](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm7) - 2024-01-31 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 048ebc2310..054973666d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2142,7 +2142,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm7" +version = "0.15.0-gm8" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index b50fe70272..21515f233f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm7" +version = "0.15.0-gm8" license = "CC0-1.0" edition = "2021" autotests = false From 81a88b1f8edf456dce32ee16bd7667897c56b81c Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 6 Feb 2024 13:38:10 +0000 Subject: [PATCH 084/109] Include a PSBT version of the reveal tx in the /inscribe output, with no witness data. --- src/subcommand/wallet/inscribe.rs | 2 ++ src/subcommand/wallet/inscribe/batch.rs | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index fcbdb0abae..cb1e216cd9 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -56,6 +56,8 @@ pub struct Output { pub reveal: Option, #[serde(skip_serializing_if = "Option::is_none")] pub reveal_hex: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub reveal_psbt: Option, #[serde(skip_serializing_if = "is_zero")] pub total_fees: u64, } diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 5f25bce623..4a6b60cae1 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -127,7 +127,7 @@ impl Batch { return Ok(self.output(None, None, None, Some(dummy_commit_psbt), Some("sign commit_psbt then re-run the /inscribe endpoint with `commit_vsize` in the input JSON set to the vsize of the signed tx; the tx has 0 fees so you can't accidentally broadcast it".to_string()), - None, None, 0, Vec::new(), &BTreeMap::new())); + None, None, None, 0, Vec::new(), &BTreeMap::new())); } let commit_tx = commit_tx.unwrap(); @@ -152,6 +152,7 @@ impl Batch { None, None, None, + None, total_fees, self.inscriptions.clone(), utxos, @@ -210,7 +211,14 @@ impl Batch { commit_tx.raw_hex() }; - let reveal_tx = signed_reveal_tx.raw_hex(); + let signed_reveal_tx_hex = signed_reveal_tx.raw_hex(); + + let mut blank_reveal_tx = reveal_tx.clone(); + for input in &mut blank_reveal_tx.input { + input.witness = Witness::new(); + } + let blank_reveal_tx = general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(blank_reveal_tx)?.serialize()); + return Ok(self.output(None, None, None, Some(commit_tx), Some(if self.parent_info.is_none() { @@ -218,7 +226,8 @@ impl Batch { } else { "sign commit_psbt and reveal_hex, then broadcast them both" }.to_string()), - Some(reveal_tx), + Some(signed_reveal_tx_hex), + Some(blank_reveal_tx), None, 0, Vec::new(), &BTreeMap::new())); } @@ -260,6 +269,7 @@ impl Batch { if self.dump && self.commitment.is_none() { Some(signed_commit_tx.raw_hex()) } else { None }, None, None, if self.dump && !self.commit_only { Some(signed_reveal_tx.raw_hex()) } else { None }, + None, if self.dump { Some(Self::get_recovery_key(&client, recovery_key_pair, chain.network())?.to_string()) } else { None }, total_fees, self.inscriptions.clone(), @@ -275,6 +285,7 @@ impl Batch { commit_psbt: Option, message: Option, reveal_hex: Option, + reveal_psbt: Option, recovery_descriptor: Option, total_fees: u64, inscriptions: Vec, @@ -291,6 +302,7 @@ impl Batch { recovery_descriptor: None, reveal: None, reveal_hex, + reveal_psbt, total_fees: 0, }; } @@ -346,6 +358,7 @@ impl Batch { message: None, reveal, reveal_hex, + reveal_psbt: None, recovery_descriptor, total_fees, parent: self.parent_info.clone().map(|info| info.id), From aa92dd871edbe6368fd0ce731bb1bd1692e174a1 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 6 Feb 2024 23:54:11 +0000 Subject: [PATCH 085/109] Have the /inscribe endpoint generate and reuse the same temporary key per network. --- src/index.rs | 4 ++++ src/subcommand/server.rs | 2 +- src/subcommand/wallet/inscribe.rs | 32 ++++++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/index.rs b/src/index.rs index a31f434432..e811925996 100644 --- a/src/index.rs +++ b/src/index.rs @@ -405,6 +405,10 @@ impl Index { Ok(true) } + pub(crate) fn data_dir(&self) -> PathBuf { + self.options.data_dir() + } + pub(crate) fn has_rune_index(&self) -> bool { self.index_runes } diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 6f4d42bab9..147641649a 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -1655,7 +1655,7 @@ impl Server { task::block_in_place(|| { log::info!("POST /inscribe"); - match Inscribe::inscribe_for_server(data.clone(), server_config.chain, index) { + match Inscribe::inscribe_for_server(data.clone(), server_config.chain, &index) { Ok(result) => Ok(Json(result).into_response()), Err(str) => Err(ServerError::BadRequest(format!("error: {str}"))), } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index cb1e216cd9..13e8b67651 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -19,7 +19,7 @@ use { bitcoincore_rpc::bitcoincore_rpc_json::{GetRawTransactionResultVout, ImportDescriptors, SignRawTransactionInput, Timestamp}, bitcoincore_rpc::Client, bitcoincore_rpc::RawTx, - std::collections::BTreeSet, + std::{collections::BTreeSet, io::Write}, tempfile::tempdir, url::Url, }; @@ -465,10 +465,33 @@ impl Inscribe { } } + pub(crate) fn get_temporary_key( + index: &Index, + chain: Chain, + ) -> Result { + let key_path = index.data_dir().join("key.txt"); + if let Err(err) = fs::create_dir_all(key_path.parent().unwrap()) { + eprintln!("error"); + bail!("failed to create data dir `{}`: {err}", key_path.parent().unwrap().display()); + } + + match fs::read_to_string(key_path.clone()) { + Ok(key) => Ok(key.trim_end().to_string()), + Err(_) => { + let secp256k1 = Secp256k1::new(); + let key_pair = UntweakedKeyPair::new(&secp256k1, &mut rand::thread_rng()); + let key = PrivateKey::new(key_pair.secret_key(), chain.network()).to_wif(); + let mut file = File::create(key_path)?; + file.write(format!("{}\n", key).as_bytes())?; + Ok(key) + } + } + } + pub(crate) fn inscribe_for_server( data: serde_json::Value, chain: Chain, - index: Arc, + index: &Index, ) -> Result { let no_wallet = true; @@ -677,6 +700,9 @@ impl Inscribe { let satpoint = None; + let key = Some(Self::get_temporary_key(index, chain)?); + key.clone().map(|key| eprintln!("using key {key}")); + Batch { commit_fee_rate: FeeRate::try_from(0.0).unwrap(), commit_only: false, @@ -689,7 +715,7 @@ impl Inscribe { fee_utxos, inscribe_on_specific_utxos, inscriptions, - key: None, + key, mode, next_inscription, no_backup: true, From 40787a32cf7dcad0650cebde6a4dc06925bdb69d Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 8 Feb 2024 03:50:14 +0000 Subject: [PATCH 086/109] Try merging user-provided reveal_psbt with our reveal_tx to make a fully signed reveal tx. --- Cargo.lock | 1 + Cargo.toml | 2 +- src/subcommand/wallet/inscribe.rs | 20 +++++- src/subcommand/wallet/inscribe/batch.rs | 82 ++++++++++++++++++++++--- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 054973666d..c921ce1430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -470,6 +470,7 @@ version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1945a5048598e4189e239d3f809b19bdad4845c4b2ba400d304d2dcf26d2c462" dependencies = [ + "base64 0.13.1", "bech32", "bitcoin-private", "bitcoin_hashes 0.12.0", diff --git a/Cargo.toml b/Cargo.toml index 21515f233f..c8124c1fc6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ axum-server = "0.5.0" base64 = "0.21.0" bech32 = "0.9.1" bip39 = "2.0.0" -bitcoin = { version = "0.30.1", features = ["rand"] } +bitcoin = { version = "0.30.1", features = ["base64", "rand"] } boilerplate = { version = "1.0.0", features = ["axum"] } brotli = "3.4.0" chrono = { version = "0.4.19", features = ["serde"] } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 13e8b67651..3d6b2538e0 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -1,5 +1,3 @@ -use reqwest::header::USER_AGENT; -use reqwest::header; use { self::batch::{Batch, BatchEntry, Batchfile, Mode}, super::*, @@ -19,6 +17,7 @@ use { bitcoincore_rpc::bitcoincore_rpc_json::{GetRawTransactionResultVout, ImportDescriptors, SignRawTransactionInput, Timestamp}, bitcoincore_rpc::Client, bitcoincore_rpc::RawTx, + reqwest::{header, header::USER_AGENT}, std::{collections::BTreeSet, io::Write}, tempfile::tempdir, url::Url, @@ -370,6 +369,7 @@ impl Inscribe { reinscribe: self.reinscribe, reveal_fee_rate: self.fee_rate, reveal_input: self.reveal_input, + reveal_psbt: None, satpoint, } .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, self.commit_input, change); @@ -703,6 +703,21 @@ impl Inscribe { let key = Some(Self::get_temporary_key(index, chain)?); key.clone().map(|key| eprintln!("using key {key}")); + let reveal_psbt = if data.contains_key("reveal_psbt") { + let reveal_psbt = data.get("reveal_psbt").unwrap(); + if !reveal_psbt.is_string() { + return Err(anyhow!("expected `reveal_psbt` to be a string, not {:?}", reveal_psbt)); + } + let reveal_psbt = reveal_psbt.as_str().unwrap(); + eprintln!("got reveal_psbt: {reveal_psbt}"); + match Psbt::from_str(reveal_psbt) { + Ok(psbt) => Some(psbt), + Err(e) => return Err(anyhow!("reveal_psbt {}", e)), + } + } else { + None + }; + Batch { commit_fee_rate: FeeRate::try_from(0.0).unwrap(), commit_only: false, @@ -727,6 +742,7 @@ impl Inscribe { reinscribe: false, reveal_fee_rate: FeeRate::try_from(0.0).unwrap(), reveal_input: Vec::new(), + reveal_psbt, satpoint, } .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, Vec::new(), change) diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 4a6b60cae1..a701538e99 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -24,6 +24,7 @@ pub(super) struct Batch { pub(super) reinscribe: bool, pub(super) reveal_fee_rate: FeeRate, pub(super) reveal_input: Vec, + pub(super) reveal_psbt: Option, pub(super) satpoint: Option, } @@ -53,6 +54,7 @@ impl Default for Batch { reinscribe: false, reveal_fee_rate: 1.0.try_into().unwrap(), reveal_input: Vec::new(), + reveal_psbt: None, satpoint: None, } } @@ -131,7 +133,7 @@ impl Batch { } let commit_tx = commit_tx.unwrap(); - let reveal_tx = reveal_tx.unwrap(); + let mut reveal_tx = reveal_tx.unwrap(); let recovery_key_pair = recovery_key_pair.unwrap(); let total_fees = total_fees.unwrap(); @@ -211,23 +213,83 @@ impl Batch { commit_tx.raw_hex() }; - let signed_reveal_tx_hex = signed_reveal_tx.raw_hex(); + let blank_reveal_psbt = if let Some(reveal_psbt) = self.reveal_psbt.clone() { + eprintln!("\nwe have been given a reveal psbt:\n{:#?}\ncopy its signature(s) to our reveal_tx", reveal_psbt); + let extracted_tx = reveal_psbt.extract_tx(); + eprintln!("\nextracted tx {:?}", extracted_tx); - let mut blank_reveal_tx = reveal_tx.clone(); - for input in &mut blank_reveal_tx.input { - input.witness = Witness::new(); - } - let blank_reveal_tx = general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(blank_reveal_tx)?.serialize()); + for (i, input) in extracted_tx.input.iter().enumerate() { + eprintln!("\ninput {i}: {:?}", input); + eprintln!(" prevout outpoint: {:?}", input.previous_output); + eprintln!(" witness: {:?}", input.witness); + } + + eprintln!("\n---"); + + eprintln!("\nour reveal tx {:?}", reveal_tx); + + for (i, input) in reveal_tx.input.iter().enumerate() { + eprintln!("\ninput {i}: {:?}", input); + eprintln!(" prevout outpoint: {:?}", input.previous_output); + eprintln!(" witness: {:?}", input.witness); + } + + eprintln!("\n---"); + + if extracted_tx.input.len() != reveal_tx.input.len() { + return Err(anyhow!("supplied reveal_psbt has {} inputs but should have {}", extracted_tx.input.len(), reveal_tx.input.len())); + } + + for (i, input) in extracted_tx.input.iter().enumerate() { + if input.previous_output != reveal_tx.input[i].previous_output { + return Err(anyhow!("prevout of input {i} of reveal_psbt is incorrect")); + } + + if reveal_tx.input[i].witness.len() == 0 { + if input.witness.len() > 0 { + reveal_tx.input[i] = input.clone(); + } else { + return Err(anyhow!("input {i} of reveal_psbt isn't signed")); + } + } + } + + eprintln!("\n---"); + eprintln!("\nmerged txs:"); + + for (i, input) in reveal_tx.input.iter().enumerate() { + eprintln!("\ninput {i}: {:?}", input); + eprintln!(" prevout outpoint: {:?}", input.previous_output); + eprintln!(" witness: {:?}", input.witness); + } + + None + } else { + let mut blank_reveal_tx = reveal_tx.clone(); + let mut any_unsigned = false; + for input in &mut blank_reveal_tx.input { + if input.witness.len() == 0 { + any_unsigned = true; + } + input.witness = Witness::new(); + } + + if any_unsigned { + Some(general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(blank_reveal_tx)?.serialize())) + } else { + None + } + }; return Ok(self.output(None, None, None, Some(commit_tx), Some(if self.parent_info.is_none() { "sign commit_psbt, then broadcast the signed result and reveal_hex" } else { - "sign commit_psbt and reveal_hex, then broadcast them both" + "sign commit_psbt and reveal_hex, then broadcast them both. or sign the reveal_psbt, add it to the input json, and run the /inscribe endpoint again" }.to_string()), - Some(signed_reveal_tx_hex), - Some(blank_reveal_tx), + Some(consensus::encode::serialize(&reveal_tx).raw_hex()), + blank_reveal_psbt, None, 0, Vec::new(), &BTreeMap::new())); } From 499b7214dd575d2104816c0614d1ec9da50cc0f2 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 9 Feb 2024 18:24:05 +0000 Subject: [PATCH 087/109] Fill in `witness_utxo` for the input coming from the commitx in the reveal_psbt. Remove debug. --- src/subcommand/wallet/inscribe/batch.rs | 36 ++++++++++++++++++------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index a701538e99..7757283198 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -207,17 +207,17 @@ impl Batch { }; if self.no_wallet { - let commit_tx = if use_psbt_for_commit { - general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(commit_tx)?.serialize()) + let commit_tx_hex = if use_psbt_for_commit { + general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(commit_tx.clone())?.serialize()) } else { commit_tx.raw_hex() }; let blank_reveal_psbt = if let Some(reveal_psbt) = self.reveal_psbt.clone() { - eprintln!("\nwe have been given a reveal psbt:\n{:#?}\ncopy its signature(s) to our reveal_tx", reveal_psbt); + // eprintln!("\nwe have been given a reveal psbt:\n{:#?}\ncopy its signature(s) to our reveal_tx", reveal_psbt); let extracted_tx = reveal_psbt.extract_tx(); - eprintln!("\nextracted tx {:?}", extracted_tx); - + // eprintln!("\nextracted tx {:?}", extracted_tx); +/* for (i, input) in extracted_tx.input.iter().enumerate() { eprintln!("\ninput {i}: {:?}", input); eprintln!(" prevout outpoint: {:?}", input.previous_output); @@ -235,7 +235,7 @@ impl Batch { } eprintln!("\n---"); - +*/ if extracted_tx.input.len() != reveal_tx.input.len() { return Err(anyhow!("supplied reveal_psbt has {} inputs but should have {}", extracted_tx.input.len(), reveal_tx.input.len())); } @@ -253,7 +253,7 @@ impl Batch { } } } - +/* eprintln!("\n---"); eprintln!("\nmerged txs:"); @@ -262,9 +262,10 @@ impl Batch { eprintln!(" prevout outpoint: {:?}", input.previous_output); eprintln!(" witness: {:?}", input.witness); } - +*/ None } else { + // copy the reveal_tx, and blank out all the witnesses so we can convert it to a Psbt let mut blank_reveal_tx = reveal_tx.clone(); let mut any_unsigned = false; for input in &mut blank_reveal_tx.input { @@ -275,14 +276,29 @@ impl Batch { } if any_unsigned { - Some(general_purpose::STANDARD.encode(Psbt::from_unsigned_tx(blank_reveal_tx)?.serialize())) + let commit_txid = commit_tx.txid(); + let mut blank_reveal_psbt = Psbt::from_unsigned_tx(blank_reveal_tx.clone())?; + let mut found_commit_output = false; + for (i, input) in blank_reveal_tx.input.iter().enumerate() { + if commit_txid == input.previous_output.txid { + if found_commit_output { + return Err(anyhow!("reveal has multiple inputs from the commit tx")); + } + found_commit_output = true; + blank_reveal_psbt.inputs[i].witness_utxo = Some(commit_tx.output[input.previous_output.vout as usize].clone()) + } + } + if !found_commit_output { + return Err(anyhow!("reveal has no inputs from the commit tx")); + } + Some(general_purpose::STANDARD.encode(blank_reveal_psbt.serialize())) } else { None } }; return Ok(self.output(None, None, None, - Some(commit_tx), + Some(commit_tx_hex), Some(if self.parent_info.is_none() { "sign commit_psbt, then broadcast the signed result and reveal_hex" } else { From 490835d8cd9c9a61d60edd49b34e51d1b489bd42 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 13 Feb 2024 17:40:48 +0000 Subject: [PATCH 088/109] Allow `--index-runes` to add a runes index to an existing index if runes aren't active yet. --- src/index.rs | 16 ++++++++++++++-- src/options.rs | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/index.rs b/src/index.rs index e811925996..e155a1d257 100644 --- a/src/index.rs +++ b/src/index.rs @@ -240,7 +240,7 @@ impl Index { redb::Durability::Immediate }; - let index_runes; + let mut index_runes; let index_sats; let index_transactions; @@ -299,8 +299,20 @@ impl Index { 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)?; - } + // if --index-runes is on the command line, and the index doesn't have the runes index, and runes aren't active yet, add the rune index + if options.index_runes() && !index_runes && tx + .open_table(HEIGHT_TO_BLOCK_HEADER)? + .range(0..)? + .next_back() + .transpose()? + .map(|(height, _header)| height.value()).unwrap() < options.first_rune_height() { + index_runes = true; + let tx = database.begin_write()?; + Self::set_statistic(&mut tx.open_table(STATISTIC_TO_COUNT)?, Statistic::IndexRunes, u64::from(index_runes))?; + tx.commit()?; + } + } database } Err(DatabaseError::Storage(StorageError::Io(error))) diff --git a/src/options.rs b/src/options.rs index b24fcd4d63..88b7a36de6 100644 --- a/src/options.rs +++ b/src/options.rs @@ -114,7 +114,7 @@ impl Options { } pub(crate) fn index_runes(&self) -> bool { - self.index_runes && self.chain() != Chain::Mainnet + self.index_runes } pub(crate) fn rpc_url(&self, wallet_name: Option) -> String { From cabdcbcccd98bd4d619371ab047dddd0e0ddde90 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 15 Feb 2024 11:49:30 +0000 Subject: [PATCH 089/109] Add `/sendtx` endpoint to broadcast a raw transaction. POST the raw tx as a JSON string. --- src/subcommand/server.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 147641649a..1adf48f1a0 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -354,6 +354,7 @@ impl Server { .route("/sat/:sat", get(Self::sat)) .route("/search", get(Self::search_by_query)) .route("/search/*query", get(Self::search_by_path)) + .route("/sendtx", post(Self::send_transaction)) .route("/static/*path", get(Self::static_asset)) .route("/stats", get(Self::stats)) .route("/status", get(Self::status)) @@ -778,6 +779,24 @@ impl Server { }) } + async fn send_transaction( + Extension(index): Extension>, + Json(data): Json + ) -> ServerResult { + task::block_in_place(|| { + log::info!("POST /sendtx"); + + if !data.is_string() { + return Err(ServerError::BadRequest("expected string".to_string())); + } + + match index.client().send_raw_transaction(data.as_str().unwrap()) { + Ok(ok) => Ok(Json(ok).into_response()), + Err(e) => Err(ServerError::BadRequest(e.to_string())) + } + }) + } + async fn range( Extension(server_config): Extension>, Path((DeserializeFromStr(start), DeserializeFromStr(end))): Path<( From 08dfb1003b0377908c992e4f1eaf43b85a02c70c Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 18 Feb 2024 14:45:06 +0000 Subject: [PATCH 090/109] Add `--reveal-fee` flag to `wallet inscribe`. --- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 4 ++++ src/subcommand/wallet/inscribe/batch.rs | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index b5bac14240..4fa223dac6 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -133,6 +133,7 @@ impl Preview { parent_satpoint: None, postage: Some(TARGET_POSTAGE), reinscribe: false, + reveal_fee: None, reveal_input: Vec::new(), satpoint: None, sat: None, @@ -180,6 +181,7 @@ impl Preview { parent_satpoint: None, postage: Some(TARGET_POSTAGE), reinscribe: false, + reveal_fee: None, reveal_input: Vec::new(), satpoint: None, sat: None, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 3d6b2538e0..614eaefe1b 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -141,6 +141,8 @@ pub(crate) struct Inscribe { pub(crate) postage: Option, #[clap(long, help = "Allow reinscription.")] pub(crate) reinscribe: bool, + #[arg(long, help = "Specify the reveal tx fee.")] + pub(crate) reveal_fee: Option, #[arg(long, help = "Inscribe .")] pub(crate) satpoint: Option, #[clap(long, help = "Use provided recovery key instead of a random one.")] @@ -367,6 +369,7 @@ impl Inscribe { parent_info, postage, reinscribe: self.reinscribe, + reveal_fee: self.reveal_fee, reveal_fee_rate: self.fee_rate, reveal_input: self.reveal_input, reveal_psbt: None, @@ -740,6 +743,7 @@ impl Inscribe { parent_info, postage, reinscribe: false, + reveal_fee: None, reveal_fee_rate: FeeRate::try_from(0.0).unwrap(), reveal_input: Vec::new(), reveal_psbt, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 7757283198..1d555af654 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -22,6 +22,7 @@ pub(super) struct Batch { pub(super) parent_info: Option, pub(super) postage: Amount, pub(super) reinscribe: bool, + pub(super) reveal_fee: Option, pub(super) reveal_fee_rate: FeeRate, pub(super) reveal_input: Vec, pub(super) reveal_psbt: Option, @@ -52,6 +53,7 @@ impl Default for Batch { parent_info: None, postage: Amount::from_sat(10_000), reinscribe: false, + reveal_fee: None, reveal_fee_rate: 1.0.try_into().unwrap(), reveal_input: Vec::new(), reveal_psbt: None, @@ -471,6 +473,10 @@ impl Batch { return Err(anyhow!("--next-file doesn't work without --commitment")); } + if !self.fee_utxos.is_empty() && self.reveal_fee.is_some() { + return Err(anyhow!("--reveal-fee doesn't work when specifying fee_utxos")); + } + match self.mode { Mode::SameSat => assert_eq!( self.destinations.len(), @@ -712,6 +718,12 @@ impl Batch { // eprintln!("total_vsize {} = commit_vsize {} + reveal_vsize {}", total_vsize, commit_vsize, reveal_vsize); reveal_fee = (fee_utxos_value * reveal_vsize + Amount::from_sat(total_vsize - 1)) / total_vsize; // eprintln!("reveal_fee = (fee_utxos {} * reveal_vsize {} + total_vsize {} - 1) / total_vsize {} = reveal_fee {}", fee_utxos_value.to_sat(), reveal_vsize, total_vsize, total_vsize, reveal_fee.to_sat()); + } else if let Some(r) = self.reveal_fee { + if r < reveal_fee { + return Err(anyhow!("requested reveal_fee is too small; should be at least {} sats", reveal_fee.to_sat())); + } + + reveal_fee = r; } let unsigned_commit_tx = if self.commitment.is_some() { From 26dab6a9b4e0721dadd7f10996d152ff38eacd01 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 18 Feb 2024 18:53:33 +0000 Subject: [PATCH 091/109] Add `--next-batch` flag, analogous to `--next-file`, but for batches of inscriptions. --- src/subcommand/preview.rs | 2 + src/subcommand/wallet/inscribe.rs | 68 +++++++++++++++++-------- src/subcommand/wallet/inscribe/batch.rs | 13 +++-- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index 4fa223dac6..dc07fc95ec 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -124,6 +124,7 @@ impl Preview { json_metadata: None, key: None, metaprotocol: None, + next_batch: None, next_file: None, no_backup: true, no_broadcast: false, @@ -172,6 +173,7 @@ impl Preview { json_metadata: None, key: None, metaprotocol: None, + next_batch: None, next_file: None, no_backup: true, no_broadcast: false, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 614eaefe1b..b5a06c1f9f 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -151,6 +151,8 @@ pub(crate) struct Inscribe { pub(crate) commit_only: bool, #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires the same --key as was used to make the commitment. Implies --no-backup. This doesn't work if the --key has ever been backed up to the wallet.")] pub(crate) commitment: Option, + #[arg(long, help = "Make the change of the reveal tx commit to the contents of multiple inscriptions defined in a yaml .")] + pub(crate) next_batch: Option, #[clap(long, help = "Make the change of the reveal tx commit to the contents of .")] pub(crate) next_file: Option, #[clap(long, help = "Use as an extra input to the reveal tx. For use with `--commitment`.")] @@ -179,8 +181,16 @@ impl Inscribe { return Err(anyhow!("--commit-only and --commitment don't work together")); } + if self.next_batch.is_some() && self.next_file.is_some() { + return Err(anyhow!("--next-batch and --next-file don't work together")); + } + + if self.commit_only && self.next_batch.is_some() { + return Err(anyhow!("--commit-only and --next-batch don't work together")); + } + if self.commit_only && self.next_file.is_some() { - return Err(anyhow!("--commit-only and --next_file don't work together")); + return Err(anyhow!("--commit-only and --next-file don't work together")); } if self.commitment.is_none() && !self.reveal_input.is_empty() { @@ -251,9 +261,40 @@ impl Inscribe { let inscriptions; let mode; let parent_info; - let next_inscription; let sat; + let next_inscriptions = if self.next_file.is_some() { + vec![Inscription::from_file( + chain, + self.next_file.unwrap(), + self.parent, + None, + self.metaprotocol.clone(), + metadata.clone(), + self.compress, + None, + )?] + } else if self.next_batch.is_some() { + let batchfile = Batchfile::load(&self.next_batch.unwrap())?; + let parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet)?; + let postage = batchfile + .postage + .map(Amount::from_sat) + .unwrap_or(TARGET_POSTAGE); + + batchfile.inscriptions( + &client, + chain, + parent_info.as_ref().map(|info| info.tx_out.value), + metadata.clone(), + postage, + self.compress, + &mut utxos, + )?.0 + } else { + Vec::new() + }; + match (self.file, self.batch) { (Some(file), None) => { parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint, self.no_wallet)?; @@ -270,20 +311,6 @@ impl Inscribe { self.compress, None, )?]; - next_inscription = if self.next_file.is_some() { - Some(Inscription::from_file( - chain, - self.next_file.unwrap(), - self.parent, - None, - self.metaprotocol, - metadata, - self.compress, - None, - )?) - } else { - None - }; mode = Mode::SeparateOutputs; @@ -316,7 +343,6 @@ impl Inscribe { self.compress, &mut utxos, )?; - next_inscription = None; mode = batchfile.mode; @@ -361,7 +387,7 @@ impl Inscribe { inscriptions, key: self.key, mode, - next_inscription, + next_inscriptions, no_backup, no_broadcast: self.no_broadcast, no_limit: self.no_limit, @@ -673,7 +699,7 @@ impl Inscribe { let inscriptions; let mode; let parent_info; - let next_inscription; + let next_inscriptions; let compress = false; @@ -693,7 +719,7 @@ impl Inscribe { compress, &mut utxos, )?; - next_inscription = None; + next_inscriptions = Vec::new(); mode = batchfile.mode; @@ -735,7 +761,7 @@ impl Inscribe { inscriptions, key, mode, - next_inscription, + next_inscriptions, no_backup: true, no_broadcast: true, no_limit: false, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 1d555af654..4ade3b472f 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -14,7 +14,7 @@ pub(super) struct Batch { pub(super) inscriptions: Vec, pub(super) key: Option, pub(super) mode: Mode, - pub(super) next_inscription: Option, + pub(super) next_inscriptions: Vec, pub(super) no_backup: bool, pub(super) no_broadcast: bool, pub(super) no_limit: bool, @@ -45,7 +45,7 @@ impl Default for Batch { inscriptions: Vec::new(), key: None, mode: Mode::SharedOutput, - next_inscription: None, + next_inscriptions: Vec::new(), no_backup: false, no_broadcast: false, no_limit: false, @@ -469,8 +469,8 @@ impl Batch { return Err(anyhow!("listing utxos to use as fees only works when inscribing on specified utxos")); } - if self.next_inscription.is_some() && self.commitment.is_none() { - return Err(anyhow!("--next-file doesn't work without --commitment")); + if !self.next_inscriptions.is_empty() && self.commitment.is_none() { + return Err(anyhow!("--next-batch and --next-file don't work without --commitment")); } if !self.fee_utxos.is_empty() && self.reveal_fee.is_some() { @@ -585,10 +585,9 @@ impl Batch { let commit_tx_address = Address::p2tr_tweaked(taproot_spend_info.output_key(), chain.network()); - let reveal_change_address = if self.next_inscription.is_some() { - let next_inscriptions = vec![self.next_inscription.clone().unwrap()]; + let reveal_change_address = if !self.next_inscriptions.is_empty() { let next_reveal_script = Inscription::append_batch_reveal_script( - &next_inscriptions, + &self.next_inscriptions, ScriptBuf::builder() .push_slice(public_key.serialize()) .push_opcode(opcodes::all::OP_CHECKSIG), From 0127ac91ff9069dda7d6ce46ec8d4b43d46036f8 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 19 Feb 2024 21:26:01 +0000 Subject: [PATCH 092/109] Add `--parent-destination` flag to control where the parent inscription ends up when using it to inscribe a child. --- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index dc07fc95ec..fa1226df70 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -132,6 +132,7 @@ impl Preview { no_wallet: false, parent: None, parent_satpoint: None, + parent_destination: None, postage: Some(TARGET_POSTAGE), reinscribe: false, reveal_fee: None, @@ -180,6 +181,7 @@ impl Preview { no_limit: false, no_wallet: false, parent: None, + parent_destination: None, parent_satpoint: None, postage: Some(TARGET_POSTAGE), reinscribe: false, diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index b5a06c1f9f..aad12dc290 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -132,6 +132,8 @@ pub(crate) struct Inscribe { pub(crate) no_limit: bool, #[clap(long, help = "Make inscription a child of .")] pub(crate) parent: Option, + #[clap(long, help = "Address to return parent inscription to.")] + pub(crate) parent_destination: Option>, #[clap(long, help = "The satpoint of the parent inscription, in case it isn't confirmed yet.")] pub(crate) parent_satpoint: Option, #[arg( @@ -276,7 +278,7 @@ impl Inscribe { )?] } else if self.next_batch.is_some() { let batchfile = Batchfile::load(&self.next_batch.unwrap())?; - let parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet)?; + let parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet, self.parent_destination.clone())?; let postage = batchfile .postage .map(Amount::from_sat) @@ -297,7 +299,7 @@ impl Inscribe { match (self.file, self.batch) { (Some(file), None) => { - parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint, self.no_wallet)?; + parent_info = Inscribe::get_parent_info(self.parent, &index, &utxos, &client, chain, self.parent_satpoint, self.no_wallet, self.parent_destination)?; postage = self.postage.unwrap_or(TARGET_POSTAGE); @@ -327,7 +329,7 @@ impl Inscribe { (None, Some(batch)) => { let batchfile = Batchfile::load(&batch)?; - parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet)?; + parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, self.no_wallet, self.parent_destination)?; postage = batchfile .postage @@ -432,6 +434,7 @@ impl Inscribe { chain: Chain, satpoint: Option, no_wallet: bool, + destination: Option>, ) -> Result> { if let Some(parent_id) = parent { let satpoint = if let Some(satpoint) = satpoint { @@ -458,6 +461,8 @@ impl Inscribe { let destination = if no_wallet { chain.address_from_script(&tx_out.script_pubkey)? + } else if let Some(destination) = destination { + destination.require_network(chain.network())? } else { get_change_address(client, chain)? }; @@ -703,7 +708,7 @@ impl Inscribe { let compress = false; - parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, no_wallet)?; + parent_info = Inscribe::get_parent_info(batchfile.parent, &index, &utxos, &client, chain, batchfile.parent_satpoint, no_wallet, None)?; postage = batchfile .postage From 639f8bf38f89f387bf09b9aa765d90fbd981bbb2 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 20 Feb 2024 12:52:27 +0000 Subject: [PATCH 093/109] Until now, using --commitment caused the reveal tx to create a change output. This change allows us to skip the change output by setting --reveal-fee to 0sat. --- src/subcommand/wallet/inscribe.rs | 2 +- src/subcommand/wallet/inscribe/batch.rs | 28 +++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index aad12dc290..2f75e51784 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -151,7 +151,7 @@ pub(crate) struct Inscribe { pub(crate) key: Option, #[clap(long, help = "Don't make a reveal tx; just create a commit tx that sends all the sats to a new commitment. Either specify --key if you have one, or note the --key it generates for you. Implies --no-backup.")] pub(crate) commit_only: bool, - #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires the same --key as was used to make the commitment. Implies --no-backup. This doesn't work if the --key has ever been backed up to the wallet.")] + #[clap(long, help = "Don't make a commit transaction; just create a reveal tx that reveals the inscription committed to by output . Requires the same --key as was used to make the commitment. Implies --no-backup. This doesn't work if the --key has ever been backed up to the wallet. When using --commitment, the reveal tx will create a change output unless --reveal-fee is set to '0 sats', in which case the whole commitment will go to postage and fees.")] pub(crate) commitment: Option, #[arg(long, help = "Make the change of the reveal tx commit to the contents of multiple inscriptions defined in a yaml .")] pub(crate) next_batch: Option, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 4ade3b472f..65519c0928 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -658,11 +658,13 @@ impl Batch { let commit_input = if self.parent_info.is_some() { 1 } else { 0 }; - if self.commitment.is_some() { - reveal_outputs.push(TxOut { - script_pubkey: reveal_change_address.unwrap().script_pubkey(), - value: 0, - }); + if self.reveal_fee != Some(Amount::from_sat(0)) { + if self.commitment.is_some() { + reveal_outputs.push(TxOut { + script_pubkey: reveal_change_address.unwrap().script_pubkey(), + value: 0, + }); + } } let (_, mut reveal_fee, reveal_vsize) = Self::build_reveal_transaction( @@ -718,11 +720,13 @@ impl Batch { reveal_fee = (fee_utxos_value * reveal_vsize + Amount::from_sat(total_vsize - 1)) / total_vsize; // eprintln!("reveal_fee = (fee_utxos {} * reveal_vsize {} + total_vsize {} - 1) / total_vsize {} = reveal_fee {}", fee_utxos_value.to_sat(), reveal_vsize, total_vsize, total_vsize, reveal_fee.to_sat()); } else if let Some(r) = self.reveal_fee { - if r < reveal_fee { - return Err(anyhow!("requested reveal_fee is too small; should be at least {} sats", reveal_fee.to_sat())); - } + if r != Amount::from_sat(0) { + if r < reveal_fee { + return Err(anyhow!("requested reveal_fee is too small; should be at least {} sats", reveal_fee.to_sat())); + } - reveal_fee = r; + reveal_fee = r; + } } let unsigned_commit_tx = if self.commitment.is_some() { @@ -767,8 +771,10 @@ impl Batch { let vout = if self.commitment.is_some() { reveal_inputs[commit_input] = self.commitment.unwrap(); - if let Some(last) = reveal_outputs.last_mut() { - (*last).value = (reveal_input_value + self.commitment_output.clone().unwrap().value - total_postage - reveal_fee).to_sat(); + if self.reveal_fee != Some(Amount::from_sat(0)) { + if let Some(last) = reveal_outputs.last_mut() { + (*last).value = (reveal_input_value + self.commitment_output.clone().unwrap().value - total_postage - reveal_fee).to_sat(); + } } 0 From 0fb1c04d5e9f6896a198afb0fb23acb1f772cb08 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Tue, 20 Feb 2024 13:19:37 +0000 Subject: [PATCH 094/109] Release 0.15.0-gm9 --- CHANGELOG.md | 15 +++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3709d5f1ab..5db72fe5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ Changelog ========= +[0.15.0-gm9](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm9) - 2024-02-20 +---------------------------------------------------------------------------------- + +### Added +- Include a PSBT version of the reveal tx in the /inscribe output, with no witness data. +- Have the `/inscribe` endpoint generate and reuse the same temporary key per network. +- Try merging the user-provided `reveal_psbt` with our `reveal_tx` to make a fully signed reveal tx. +- Fill in `witness_utxo` for the input coming from the commit tx in the `reveal_psbt`. +- Allow `--index-runes` to add a runes index to an existing index if runes aren't active yet. +- Add `/sendtx` endpoint to broadcast a raw transaction. POST the raw tx as a JSON string. +- Add `--reveal-fee` flag to `wallet inscribe`. +- Add `--next-batch` flag, analogous to `--next-file`, but for batches of inscriptions. +- Add `--parent-destination` flag to control where the parent inscription ends up when using it to inscribe a child. +- Set `--reveal-fee` to `0 sats` when using `--commitment` to avoid creating a change output in the reveal tx. + [0.15.0-gm8](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm8) - 2024-01-31 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index c921ce1430..d2f0c3eaf1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,7 +2143,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm8" +version = "0.15.0-gm9" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index c8124c1fc6..3f3260c526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm8" +version = "0.15.0-gm9" license = "CC0-1.0" edition = "2021" autotests = false From 8f42561f8bda4f82fd176f2c3677655dc52ef92b Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 23 Feb 2024 12:56:42 +0000 Subject: [PATCH 095/109] Use `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1` whenever possible to save space. --- src/inscriptions/tag.rs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index f6b95e4a48..1f46480d36 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -36,6 +36,23 @@ impl Tag { } } + pub(crate) fn push_bytes(self, tmp: script::Builder) -> script::Builder { + // if it's a single byte between 1 and 16, use a PUSHNUM opcode + let bytes = self.bytes(); + if bytes.len() == 1 && (1..17).contains(&bytes[0]) { + tmp.push_opcode( + match bytes[0] { + 1 => opcodes::all::OP_PUSHNUM_1, 2 => opcodes::all::OP_PUSHNUM_2, 3 => opcodes::all::OP_PUSHNUM_3, 4 => opcodes::all::OP_PUSHNUM_4, + 5 => opcodes::all::OP_PUSHNUM_5, 6 => opcodes::all::OP_PUSHNUM_6, 7 => opcodes::all::OP_PUSHNUM_7, 8 => opcodes::all::OP_PUSHNUM_8, + 9 => opcodes::all::OP_PUSHNUM_9, 10 => opcodes::all::OP_PUSHNUM_10, 11 => opcodes::all::OP_PUSHNUM_11, 12 => opcodes::all::OP_PUSHNUM_12, + 13 => opcodes::all::OP_PUSHNUM_13, 14 => opcodes::all::OP_PUSHNUM_14, 15 => opcodes::all::OP_PUSHNUM_15, 16 => opcodes::all::OP_PUSHNUM_16, + _ => panic!("unreachable"), + }) + } else { + tmp.push_slice::<&script::PushBytes>(bytes.try_into().unwrap()) + } + } + pub(crate) fn encode(self, builder: &mut script::Builder, value: &Option>) { if let Some(value) = value { let mut tmp = script::Builder::new(); @@ -43,13 +60,11 @@ impl Tag { if self.is_chunked() { for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) { - tmp = tmp - .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) + tmp = self.push_bytes(tmp) .push_slice::<&script::PushBytes>(chunk.try_into().unwrap()); } } else { - tmp = tmp - .push_slice::<&script::PushBytes>(self.bytes().try_into().unwrap()) + tmp = self.push_bytes(tmp) .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); } From d397527128fcd8dd6f599a51ecdd43d7790e1852 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Fri, 23 Feb 2024 13:19:40 +0000 Subject: [PATCH 096/109] Release 0.15.0-gm10 --- CHANGELOG.md | 6 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db72fe5f9..a05c2ed73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +[0.15.0-gm10](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm10) - 2024-02-23 +------------------------------------------------------------------------------------ + +### Added +- Use `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1` whenever possible to save space. + [0.15.0-gm9](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm9) - 2024-02-20 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index d2f0c3eaf1..09a496c28f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,7 +2143,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm9" +version = "0.15.0-gm10" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index 3f3260c526..e1ddaa79bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm9" +version = "0.15.0-gm10" license = "CC0-1.0" edition = "2021" autotests = false From 2b26d2be749798b0ddd382bedf78b06d0b8bc3ae Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 24 Feb 2024 20:55:31 +0000 Subject: [PATCH 097/109] Better function name, and extra comment. --- src/inscriptions/tag.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index 1f46480d36..1576c550cf 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -36,10 +36,11 @@ impl Tag { } } - pub(crate) fn push_bytes(self, tmp: script::Builder) -> script::Builder { - // if it's a single byte between 1 and 16, use a PUSHNUM opcode + pub(crate) fn push_tag(self, tmp: script::Builder) -> script::Builder { let bytes = self.bytes(); + if bytes.len() == 1 && (1..17).contains(&bytes[0]) { + // if it's a single byte between 1 and 16, use a PUSHNUM opcode tmp.push_opcode( match bytes[0] { 1 => opcodes::all::OP_PUSHNUM_1, 2 => opcodes::all::OP_PUSHNUM_2, 3 => opcodes::all::OP_PUSHNUM_3, 4 => opcodes::all::OP_PUSHNUM_4, @@ -49,6 +50,7 @@ impl Tag { _ => panic!("unreachable"), }) } else { + // otherwise use a PUSHBYTES opcode tmp.push_slice::<&script::PushBytes>(bytes.try_into().unwrap()) } } @@ -60,11 +62,11 @@ impl Tag { if self.is_chunked() { for chunk in value.chunks(MAX_SCRIPT_ELEMENT_SIZE) { - tmp = self.push_bytes(tmp) + tmp = self.push_tag(tmp) .push_slice::<&script::PushBytes>(chunk.try_into().unwrap()); } } else { - tmp = self.push_bytes(tmp) + tmp = self.push_tag(tmp) .push_slice::<&script::PushBytes>(value.as_slice().try_into().unwrap()); } From 9c206d434739a92c56b916ed81d223d20b8a8345 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sat, 24 Feb 2024 21:01:28 +0000 Subject: [PATCH 098/109] Disable the single-byte push, but leave it easy to enable. --- src/inscriptions/tag.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index 1576c550cf..c44ce389a0 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -37,9 +37,11 @@ impl Tag { } pub(crate) fn push_tag(self, tmp: script::Builder) -> script::Builder { + let use_single_byte_push_when_possible = false; + let bytes = self.bytes(); - if bytes.len() == 1 && (1..17).contains(&bytes[0]) { + if use_single_byte_push_when_possible && bytes.len() == 1 && (1..17).contains(&bytes[0]) { // if it's a single byte between 1 and 16, use a PUSHNUM opcode tmp.push_opcode( match bytes[0] { From ff0e6f34a939a5938397612713072fe93d5ab136 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Mon, 26 Feb 2024 12:35:03 +0000 Subject: [PATCH 099/109] Allow inscribing .avif files. --- src/inscriptions/media.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inscriptions/media.rs b/src/inscriptions/media.rs index c4859bd38b..d2ac81acff 100644 --- a/src/inscriptions/media.rs +++ b/src/inscriptions/media.rs @@ -66,7 +66,7 @@ impl Media { ("font/woff", BROTLI_MODE_GENERIC, Media::Font, &["woff"]), ("font/woff2", BROTLI_MODE_FONT, Media::Font, &["woff2"]), ("image/apng", BROTLI_MODE_GENERIC, Media::Image, &["apng"]), - ("image/avif", BROTLI_MODE_GENERIC, Media::Image, &[]), + ("image/avif", BROTLI_MODE_GENERIC, Media::Image, &["avif"]), ("image/gif", BROTLI_MODE_GENERIC, Media::Image, &["gif"]), ("image/jpeg", BROTLI_MODE_GENERIC, Media::Image, &["jpg", "jpeg"]), ("image/png", BROTLI_MODE_GENERIC, Media::Image, &["png"]), From fe17e64925d16c7bcbcfd05384874685d0d85ac2 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 28 Feb 2024 03:00:08 +0000 Subject: [PATCH 100/109] Better error checking for inputs to transaction builder. --- src/subcommand/wallet/transaction_builder.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/subcommand/wallet/transaction_builder.rs b/src/subcommand/wallet/transaction_builder.rs index c9d52eefbc..33bb65b31f 100644 --- a/src/subcommand/wallet/transaction_builder.rs +++ b/src/subcommand/wallet/transaction_builder.rs @@ -46,6 +46,7 @@ pub enum Error { NotEnoughCardinalUtxos, NotInWallet(SatPoint), OutOfRange(SatPoint, u64), + OutputNotInWallet(OutPoint), UtxoContainsAdditionalInscription { outgoing_satpoint: SatPoint, inscribed_satpoint: SatPoint, @@ -72,6 +73,7 @@ impl fmt::Display for Error { } => write!(f, "output value is below dust value: {output_value} < {dust_value}"), 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::OutputNotInWallet(outpoint) => write!(f, "outpoint {outpoint} not in wallet"), Error::NotEnoughCardinalUtxos => write!( f, "wallet does not contain enough cardinal UTXOs, please add additional funds to wallet." @@ -220,7 +222,13 @@ impl TransactionBuilder { } } - let mut amount = self.outgoing.iter().map(|outgoing| self.amounts[&outgoing.outpoint]).sum::(); + let mut amount = Amount::from_sat(0); + for outgoing in &self.outgoing { + amount += match self.amounts.get(&outgoing.outpoint) { + Some(amount) => *amount, + None => return Err(Error::NotInWallet(*outgoing)), + } + } if self.outgoing[0].offset >= amount.to_sat() { return Err(Error::OutOfRange(self.outgoing[0], amount.to_sat() - 1)); @@ -233,7 +241,10 @@ impl TransactionBuilder { for input in &self.force_input { self.inputs.push(*input); - amount += *self.amounts.get(&input).unwrap(); + amount += match self.amounts.get(&input) { + Some(amount) => *amount, + None => return Err(Error::OutputNotInWallet(*input)), + }; self.utxos.remove(&input); } self.outputs.push((self.recipient.clone(), amount)); From f40868c09e3b29125d71b064c91185b29df851b2 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Wed, 28 Feb 2024 03:47:22 +0000 Subject: [PATCH 101/109] Better error checking when inscribing. --- src/subcommand/wallet/inscribe.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 2f75e51784..6fba6f38bf 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -371,7 +371,7 @@ impl Inscribe { self.satpoint }; - let result = Batch { + Ok(Box::new(Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), commit_only: self.commit_only, commit_vsize: self.commit_vsize, @@ -403,8 +403,7 @@ impl Inscribe { reveal_psbt: None, satpoint, } - .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, self.commit_input, change); - Ok(Box::new(result.unwrap())) + .inscribe(chain, &index, &client, &locked_utxos, runic_utxos, &mut utxos, self.commit_input, change)?)) } fn parse_metadata(cbor: Option, json: Option) -> Result>> { From f1b165d4c9049be04269b6a9567457eceb94b03e Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 29 Feb 2024 12:21:32 +0000 Subject: [PATCH 102/109] Release 0.15.0-gm11 --- CHANGELOG.md | 9 +++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a05c2ed73e..99b8c53092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ Changelog ### Added - Use `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1` whenever possible to save space. +[0.15.0-gm11](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm10) - 2024-02-23 +------------------------------------------------------------------------------------ + +### Changed +- Better error checking when inscribing. +- Better error checking for inputs to transaction builder. +- Allow inscribing .avif files. +- Don't `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1`, but leave the code there if people want to enable it. + [0.15.0-gm9](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm9) - 2024-02-20 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index 09a496c28f..a2faccc591 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,7 +2143,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm10" +version = "0.15.0-gm11" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index e1ddaa79bf..bc67b79b10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm10" +version = "0.15.0-gm11" license = "CC0-1.0" edition = "2021" autotests = false From 589b8f95864baaf99b5ac1f0e36868f852be173f Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 3 Mar 2024 01:18:27 +0000 Subject: [PATCH 103/109] Add inscribing delegates. --- src/inscriptions/inscription.rs | 6 ++++++ src/subcommand/wallet/inscribe.rs | 3 +++ src/subcommand/wallet/inscribe/batch.rs | 2 ++ 3 files changed, 11 insertions(+) diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index acb8dd7ec6..328470f36a 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -42,6 +42,7 @@ impl Inscription { pub(crate) fn from_file( chain: Chain, + delegate: Option, path: impl AsRef, parent: Option, pointer: Option, @@ -103,6 +104,7 @@ impl Inscription { body: Some(body), content_type: Some(content_type.into()), content_encoding, + delegate: delegate.map(|id| id.value()), metadata, metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), parent: parent.map(|id| id.value()), @@ -131,20 +133,24 @@ impl Inscription { .push_opcode(opcodes::all::OP_IF) .push_slice(envelope::PROTOCOL_ID); + if self.delegate.is_none() { Tag::ContentType.encode(&mut builder, &self.content_type); Tag::ContentEncoding.encode(&mut builder, &self.content_encoding); + } Tag::Metaprotocol.encode(&mut builder, &self.metaprotocol); Tag::Parent.encode(&mut builder, &self.parent); Tag::Delegate.encode(&mut builder, &self.delegate); Tag::Pointer.encode(&mut builder, &self.pointer); Tag::Metadata.encode(&mut builder, &self.metadata); + if self.delegate.is_none() { 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(PushBytesBuf::try_from(chunk.to_vec()).unwrap()); } } + } builder.push_opcode(opcodes::all::OP_ENDIF) } diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 6fba6f38bf..8575ad002f 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -268,6 +268,7 @@ impl Inscribe { let next_inscriptions = if self.next_file.is_some() { vec![Inscription::from_file( chain, + None, self.next_file.unwrap(), self.parent, None, @@ -305,6 +306,7 @@ impl Inscribe { inscriptions = vec![Inscription::from_file( chain, + None, file, self.parent, None, @@ -656,6 +658,7 @@ impl Inscribe { */ entries.push(BatchEntry { + delegate: None, destination: Some(destination), file: tmpfile.into(), metadata: None, diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 65519c0928..13bce5bd36 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -1025,6 +1025,7 @@ pub(crate) enum Mode { #[derive(Deserialize, Default, PartialEq, Debug, Clone)] #[serde(deny_unknown_fields)] pub(crate) struct BatchEntry { + pub(crate) delegate: Option, pub(crate) destination: Option>, pub(crate) file: PathBuf, pub(crate) metadata: Option, @@ -1138,6 +1139,7 @@ impl Batchfile { for (i, entry) in self.inscriptions.iter().enumerate() { inscriptions.push(Inscription::from_file( chain, + entry.delegate, &entry.file, self.parent, if i == 0 { None } else { Some(pointer) }, From bccf31e43a29a837a6d01af29ce881a2e8ea94ec Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 3 Mar 2024 01:19:06 +0000 Subject: [PATCH 104/109] Add the ability to set a pointer offset per inscription in the batch. --- src/inscriptions/envelope.rs | 1 + src/inscriptions/inscription.rs | 21 ++++++++++++++++++++- src/subcommand/preview.rs | 2 ++ src/subcommand/wallet/inscribe.rs | 8 ++++++++ src/subcommand/wallet/inscribe/batch.rs | 8 +++++++- 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index e18b3f9586..bc0190a486 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -78,6 +78,7 @@ impl From for ParsedEnvelope { metaprotocol, parent, pointer, + skip_pointer: false, unrecognized_even_field, utxo: None, }, diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 328470f36a..26c2dfb14c 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -26,6 +26,7 @@ pub struct Inscription { pub metaprotocol: Option>, pub parent: Option>, pub pointer: Option>, + pub skip_pointer: bool, pub unrecognized_even_field: bool, pub utxo: Option, } @@ -49,10 +50,25 @@ impl Inscription { metaprotocol: Option, metadata: Option>, compress: bool, + skip_pointer_for_none: bool, utxo: Option, ) -> Result { let path = path.as_ref(); + if path == PathBuf::from("none") { + return Ok(Self { + body: None, + content_type: None, + content_encoding: None, + metadata, + metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), + parent: parent.map(|id| id.value()), + pointer: pointer.map(Self::pointer_value), + skip_pointer: skip_pointer_for_none, + ..Default::default() + }); + } + let body = fs::read(path).with_context(|| format!("io error reading {}", path.display()))?; let (content_type, compression_mode) = Media::content_type_for_path(path)?; @@ -109,6 +125,7 @@ impl Inscription { metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), parent: parent.map(|id| id.value()), pointer: pointer.map(Self::pointer_value), + skip_pointer: false, utxo, ..Default::default() }) @@ -140,7 +157,9 @@ impl Inscription { Tag::Metaprotocol.encode(&mut builder, &self.metaprotocol); Tag::Parent.encode(&mut builder, &self.parent); Tag::Delegate.encode(&mut builder, &self.delegate); - Tag::Pointer.encode(&mut builder, &self.pointer); + if !self.skip_pointer { + Tag::Pointer.encode(&mut builder, &self.pointer); + } Tag::Metadata.encode(&mut builder, &self.metadata); if self.delegate.is_none() { diff --git a/src/subcommand/preview.rs b/src/subcommand/preview.rs index fa1226df70..06f938b922 100644 --- a/src/subcommand/preview.rs +++ b/src/subcommand/preview.rs @@ -139,6 +139,7 @@ impl Preview { reveal_input: Vec::new(), satpoint: None, sat: None, + skip_pointer_for_none: false, utxo: Vec::new(), }), }), @@ -189,6 +190,7 @@ impl Preview { reveal_input: Vec::new(), satpoint: None, sat: None, + skip_pointer_for_none: false, utxo: Vec::new(), }), }), diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index 8575ad002f..d45c805ac1 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -171,6 +171,8 @@ pub(crate) struct Inscribe { pub(crate) no_wallet: bool, #[arg(long, help = "Specify the vsize of the commit tx, for when we don't have a local wallet to sign with.")] pub(crate) commit_vsize: Option, + #[arg(long, help = "Whether to omit pointer from the envelope of blank inscriptions.")] + pub(crate) skip_pointer_for_none: bool, } impl Inscribe { @@ -275,6 +277,7 @@ impl Inscribe { self.metaprotocol.clone(), metadata.clone(), self.compress, + self.skip_pointer_for_none, None, )?] } else if self.next_batch.is_some() { @@ -292,6 +295,7 @@ impl Inscribe { metadata.clone(), postage, self.compress, + self.skip_pointer_for_none, &mut utxos, )?.0 } else { @@ -313,6 +317,7 @@ impl Inscribe { self.metaprotocol.clone(), metadata.clone(), self.compress, + self.skip_pointer_for_none, None, )?]; @@ -345,6 +350,7 @@ impl Inscribe { metadata, postage, self.compress, + self.skip_pointer_for_none, &mut utxos, )?; @@ -664,6 +670,7 @@ impl Inscribe { metadata: None, metadata_json: metadata, metaprotocol: None, + pointer: None, utxo: Some(utxo), }); } @@ -724,6 +731,7 @@ impl Inscribe { None, Amount::from_sat(0), compress, + false, &mut utxos, )?; next_inscriptions = Vec::new(); diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 13bce5bd36..3a8eb5ad69 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -1031,6 +1031,7 @@ pub(crate) struct BatchEntry { pub(crate) metadata: Option, pub(crate) metadata_json: Option, pub(crate) metaprotocol: Option, + pub(crate) pointer: Option, pub(crate) utxo: Option, } @@ -1085,6 +1086,7 @@ impl Batchfile { metadata: Option>, postage: Amount, compress: bool, + skip_pointer_for_none: bool, utxos: &mut BTreeMap, ) -> Result<(Vec, Vec
    , bool, Vec)> { assert!(!self.inscriptions.is_empty()); @@ -1142,13 +1144,17 @@ impl Batchfile { entry.delegate, &entry.file, self.parent, - if i == 0 { None } else { Some(pointer) }, + match entry.pointer { + Some(pointer) => Some(pointer), + None => if i == 0 { None } else { Some(pointer) }, + }, entry.metaprotocol.clone(), match &metadata { Some(metadata) => Some(metadata.clone()), None => entry.metadata()?, }, compress, + skip_pointer_for_none, entry.utxo, )?); From 495fd07a4d1476c72ce949f4475ce1be0a806549 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 3 Mar 2024 01:19:48 +0000 Subject: [PATCH 105/109] Add inscription `offset` to yaml field to allow inscribing on somewhere other than sat 0 of a utxo. --- src/subcommand/wallet/inscribe.rs | 1 + src/subcommand/wallet/inscribe/batch.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index d45c805ac1..fb20a006d0 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -670,6 +670,7 @@ impl Inscribe { metadata: None, metadata_json: metadata, metaprotocol: None, + offset: None, pointer: None, utxo: Some(utxo), }); diff --git a/src/subcommand/wallet/inscribe/batch.rs b/src/subcommand/wallet/inscribe/batch.rs index 3a8eb5ad69..d7a8c2ddc3 100644 --- a/src/subcommand/wallet/inscribe/batch.rs +++ b/src/subcommand/wallet/inscribe/batch.rs @@ -1031,6 +1031,7 @@ pub(crate) struct BatchEntry { pub(crate) metadata: Option, pub(crate) metadata_json: Option, pub(crate) metaprotocol: Option, + pub(crate) offset: Option, pub(crate) pointer: Option, pub(crate) utxo: Option, } @@ -1139,6 +1140,9 @@ impl Batchfile { let mut inscriptions = Vec::new(); for (i, entry) in self.inscriptions.iter().enumerate() { + if entry.offset.is_some() && entry.pointer.is_some() { + return Err(anyhow!("you can't specify `offset` and `pointer` for the same inscription (inscription {i})")); + } inscriptions.push(Inscription::from_file( chain, entry.delegate, @@ -1146,7 +1150,10 @@ impl Batchfile { self.parent, match entry.pointer { Some(pointer) => Some(pointer), - None => if i == 0 { None } else { Some(pointer) }, + None => match entry.offset { + Some(offset) => Some(pointer + offset), + None => if i == 0 { None } else { Some(pointer) }, + }, }, entry.metaprotocol.clone(), match &metadata { From 1f55d20b7bd17a080486a6e33d016ae58b3f1c1d Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Sun, 3 Mar 2024 01:29:10 +0000 Subject: [PATCH 106/109] Release 0.15.0-gm12 --- CHANGELOG.md | 14 +++++++++++--- Cargo.lock | 2 +- Cargo.toml | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b8c53092..36a02ca3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ Changelog ========= -[0.15.0-gm10](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm10) - 2024-02-23 +[0.15.0-gm12](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm12) - 2024-03-02 ------------------------------------------------------------------------------------ ### Added -- Use `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1` whenever possible to save space. +- Add inscription `offset` to yaml field to allow inscribing on somewhere other than sat 0 of a utxo. +- Add the ability to set a pointer offset per inscription in the batch. +- Add inscribing delegates. -[0.15.0-gm11](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm10) - 2024-02-23 +[0.15.0-gm11](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm11) - 2024-02-29 ------------------------------------------------------------------------------------ ### Changed @@ -16,6 +18,12 @@ Changelog - Allow inscribing .avif files. - Don't `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1`, but leave the code there if people want to enable it. +[0.15.0-gm10](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm10) - 2024-02-23 +------------------------------------------------------------------------------------ + +### Added +- Use `OP_PUSHNUM_x` instead of `OP_PUSHBYTES_1` whenever possible to save space. + [0.15.0-gm9](https://github.com/gmart7t2/ord/releases/tag/0.15.0-gm9) - 2024-02-20 ---------------------------------------------------------------------------------- diff --git a/Cargo.lock b/Cargo.lock index a2faccc591..87587d883b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2143,7 +2143,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.15.0-gm11" +version = "0.15.0-gm12" dependencies = [ "anyhow", "async-trait", diff --git a/Cargo.toml b/Cargo.toml index bc67b79b10..66c6ac9271 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ord" description = "◉ Ordinal wallet and block explorer" -version = "0.15.0-gm11" +version = "0.15.0-gm12" license = "CC0-1.0" edition = "2021" autotests = false From 77f5a5b09c14e52ddf778cb6ec58b7e51074c23f Mon Sep 17 00:00:00 2001 From: terp <95259598+terpdoctor@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:42:22 -0800 Subject: [PATCH 107/109] Update Cargo.lock --- Cargo.lock | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 87587d883b..5c63a0042f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,6 +479,12 @@ dependencies = [ "serde", ] +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + [[package]] name = "bitcoin-private" version = "0.1.0" @@ -2150,6 +2156,7 @@ dependencies = [ "axum", "axum-server", "base64 0.21.6", + "base58", "bech32", "bip39", "bitcoin", From 09a1f09838c41de7bffb581fdfb789fcaef402a8 Mon Sep 17 00:00:00 2001 From: terp <95259598+terpdoctor@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:42:58 -0800 Subject: [PATCH 108/109] Update Cargo.toml --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 66c6ac9271..b020392ec5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ async-trait = "0.1.72" axum = { version = "0.6.1", features = ["headers", "http2"] } axum-server = "0.5.0" base64 = "0.21.0" +base58 = "0.2.0" bech32 = "0.9.1" bip39 = "2.0.0" bitcoin = { version = "0.30.1", features = ["base64", "rand"] } From 12750d6f0310332e4c245f8978775f385a7f225a Mon Sep 17 00:00:00 2001 From: terp <95259598+terpdoctor@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:44:57 -0800 Subject: [PATCH 109/109] Update teleburn.rs --- src/subcommand/teleburn.rs | 53 ++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/src/subcommand/teleburn.rs b/src/subcommand/teleburn.rs index e13d5d57e3..342cd01e7c 100644 --- a/src/subcommand/teleburn.rs +++ b/src/subcommand/teleburn.rs @@ -1,20 +1,51 @@ -use super::*; +use {super::*, crate::index::entry::Entry}; +use base58::ToBase58; #[derive(Debug, Parser)] pub(crate) struct Teleburn { - #[arg(help = "Generate teleburn addresses for inscription .")] - destination: InscriptionId, +@@ -8,11 +9,15 @@ pub(crate) struct Teleburn { +#[derive(Debug, PartialEq, Serialize)] +pub struct Output { + ethereum: EthereumTeleburnAddress, + solana: SolanaTeleburnAddress, } -#[derive(Debug, PartialEq, Serialize, Deserialize)] -pub struct Output { - pub ethereum: crate::teleburn::Ethereum, +#[derive(Debug, PartialEq)] +struct EthereumTeleburnAddress([u8; 20]); + +#[derive(Debug, PartialEq)] +struct SolanaTeleburnAddress([u8; 32]); + +impl Serialize for EthereumTeleburnAddress { + fn serialize(&self, serializer: S) -> Result + where +@@ -34,11 +39,29 @@ impl Display for EthereumTeleburnAddress { + } } -impl Teleburn { - pub(crate) fn run(self) -> SubcommandResult { - Ok(Box::new(Output { - ethereum: self.destination.into(), - })) +impl Serialize for SolanaTeleburnAddress { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.collect_str(self) } } + +impl Display for SolanaTeleburnAddress { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.0.to_base58())?; + + Ok(()) + } +} + +impl Teleburn { + pub(crate) fn run(self) -> Result { + let digest = bitcoin::hashes::sha256::Hash::hash(&self.recipient.store()); + print_json(Output { + ethereum: EthereumTeleburnAddress(digest[0..20].try_into().unwrap()), + solana: SolanaTeleburnAddress(digest[0..32].try_into().unwrap()), + })?; + Ok(()) + }