From 1640a9c4b4d8ea1bc03828769713cd95e19f7077 Mon Sep 17 00:00:00 2001 From: Greg Martin Date: Thu, 9 Nov 2023 22:19:42 +0000 Subject: [PATCH 01/41] 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 02/41] 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 03/41] 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 04/41] 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 05/41] 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 06/41] 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 07/41] 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 08/41] 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 09/41] 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 10/41] 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 11/41] 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 12/41] 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 13/41] 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 14/41] 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 15/41] 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 16/41] 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 17/41] 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 18/41] 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 19/41] 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 20/41] 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 21/41] 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 22/41] 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 23/41] 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 24/41] 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 25/41] 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 26/41] 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 27/41] 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 28/41] 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 29/41] 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 30/41] 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 31/41] 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 32/41] 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 33/41] 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 34/41] 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 35/41] 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 36/41] 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 37/41] 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 38/41] 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 39/41] 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 40/41] 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 41/41] 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