diff --git a/Cargo.lock b/Cargo.lock index f14f43dfaf..7e19b23f73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" dependencies = [ "backtrace", ] @@ -702,9 +702,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.2" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", "clap_derive", @@ -724,11 +724,11 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.0" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.52", @@ -1484,9 +1484,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" dependencies = [ "bytes", "fnv", @@ -1523,6 +1523,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2484,9 +2490,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -2660,9 +2666,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.25" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ "async-compression", "base64 0.21.7", @@ -3196,7 +3202,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -3266,20 +3272,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ - "bitflags 2.4.2", + "bitflags 1.3.2", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.6.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ "core-foundation-sys", "libc", @@ -3316,18 +3322,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -3440,9 +3446,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", diff --git a/crates/test-bitcoincore-rpc/src/lib.rs b/crates/test-bitcoincore-rpc/src/lib.rs index e13a1e0c81..8e9e6ea83a 100644 --- a/crates/test-bitcoincore-rpc/src/lib.rs +++ b/crates/test-bitcoincore-rpc/src/lib.rs @@ -135,9 +135,10 @@ pub struct TransactionTemplate<'a> { pub op_return_index: Option, pub output_values: &'a [u64], pub outputs: usize, + pub p2tr: bool, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct JsonOutPoint { txid: Txid, vout: u32, @@ -181,6 +182,7 @@ impl<'a> Default for TransactionTemplate<'a> { op_return_index: None, output_values: &[], outputs: 1, + p2tr: false, } } } @@ -197,7 +199,17 @@ impl Handle { format!("http://127.0.0.1:{}", self.port) } - fn state(&self) -> MutexGuard { + pub fn address(&self, output: OutPoint) -> Address { + let state = self.state(); + + Address::from_script( + &state.transactions.get(&output.txid).unwrap().output[output.vout as usize].script_pubkey, + state.network, + ) + .unwrap() + } + + pub fn state(&self) -> MutexGuard { self.state.lock().unwrap() } @@ -209,15 +221,19 @@ impl Handle { self.state().wallets.clone() } + #[track_caller] pub fn mine_blocks(&self, n: u64) -> Vec { self.mine_blocks_with_subsidy(n, 50 * COIN_VALUE) } + #[track_caller] pub fn mine_blocks_with_subsidy(&self, n: u64, subsidy: u64) -> Vec { let mut bitcoin_rpc_data = self.state(); - (0..n) - .map(|_| bitcoin_rpc_data.push_block(subsidy)) - .collect() + let mut blocks = Vec::new(); + for _ in 0..n { + blocks.push(bitcoin_rpc_data.mine_block(subsidy)); + } + blocks } pub fn broadcast_tx(&self, template: TransactionTemplate) -> Txid { @@ -236,9 +252,25 @@ impl Handle { self.state().utxos.get(outpoint).cloned() } - pub fn tx(&self, bi: usize, ti: usize) -> Transaction { + #[track_caller] + pub fn tx(&self, block: usize, transaction: usize) -> Transaction { let state = self.state(); - state.blocks[&state.hashes[bi]].txdata[ti].clone() + let blockhash = state.hashes.get(block).expect("block index out of bounds"); + state.blocks[blockhash] + .txdata + .get(transaction) + .expect("transaction index out of bounds") + .clone() + } + + #[track_caller] + pub fn tx_by_id(&self, txid: Txid) -> Transaction { + self + .state() + .transactions + .get(&txid) + .expect("unknown transaction") + .clone() } pub fn mempool(&self) -> Vec { @@ -271,10 +303,6 @@ impl Handle { self.state().loaded_wallets.clone() } - pub fn change_addresses(&self) -> Vec
{ - self.state().change_addresses.clone() - } - pub fn cookie_file(&self) -> PathBuf { self.tempdir.path().join(".cookie") } diff --git a/crates/test-bitcoincore-rpc/src/server.rs b/crates/test-bitcoincore-rpc/src/server.rs index f7eb7f05ab..c1bbbd34fe 100644 --- a/crates/test-bitcoincore-rpc/src/server.rs +++ b/crates/test-bitcoincore-rpc/src/server.rs @@ -1,12 +1,7 @@ use { super::*, base64::Engine, - bitcoin::{ - consensus::Decodable, - psbt::Psbt, - secp256k1::{rand, KeyPair, Secp256k1, XOnlyPublicKey}, - Witness, - }, + bitcoin::{consensus::Decodable, psbt::Psbt, Witness}, std::io::Cursor, }; @@ -228,26 +223,36 @@ impl Api for Server { vout: u32, _include_mempool: Option, ) -> Result, jsonrpc_core::Error> { - Ok( - self - .state() - .utxos - .get(&OutPoint { txid, vout }) - .map(|&value| GetTxOutResult { - bestblock: BlockHash::all_zeros(), - confirmations: 0, - value, - script_pub_key: GetRawTransactionResultVoutScriptPubKey { - asm: String::new(), - hex: Vec::new(), - req_sigs: None, - type_: None, - addresses: Vec::new(), - address: None, - }, - coinbase: false, - }), - ) + let state = self.state(); + + let Some(value) = state.utxos.get(&OutPoint { txid, vout }) else { + return Ok(None); + }; + + let mut confirmations = None; + + for (height, hash) in state.hashes.iter().enumerate() { + for tx in &state.blocks[hash].txdata { + if tx.txid() == txid { + confirmations = Some(state.hashes.len() - height); + } + } + } + + Ok(Some(GetTxOutResult { + bestblock: BlockHash::all_zeros(), + coinbase: false, + confirmations: confirmations.unwrap().try_into().unwrap(), + script_pub_key: GetRawTransactionResultVoutScriptPubKey { + asm: String::new(), + hex: Vec::new(), + req_sigs: None, + type_: None, + addresses: Vec::new(), + address: None, + }, + value: *value, + })) } fn get_wallet_info(&self) -> Result { @@ -350,7 +355,7 @@ impl Api for Server { Some(transaction.output.len().try_into().unwrap()) ); - let state = self.state(); + let mut state = self.state(); let output_value = transaction .output @@ -371,35 +376,60 @@ impl Api for Server { .map(|txin| state.utxos.get(&txin.previous_output).unwrap().to_sat()) .sum::(); - let shortfall = output_value.saturating_sub(input_value); - utxos.sort(); utxos.reverse(); - if shortfall > 0 { - let (additional_input_value, outpoint) = utxos - .iter() - .find(|(value, outpoint)| value.to_sat() >= shortfall && !state.locked.contains(outpoint)) - .ok_or_else(|| { - jsonrpc_core::Error::new(jsonrpc_core::types::error::ErrorCode::ServerError(-6)) - })?; + if output_value > input_value { + for (value, outpoint) in utxos { + if state.locked.contains(&outpoint) { + continue; + } - transaction.input.push(TxIn { - previous_output: *outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - witness: Witness::default(), - }); + let tx = state.transactions.get(&outpoint.txid).unwrap(); + + let tx_out = &tx.output[usize::try_from(outpoint.vout).unwrap()]; + + let Ok(address) = Address::from_script(&tx_out.script_pubkey, state.network) else { + continue; + }; + + if !state.is_wallet_address(&address) { + continue; + } + + transaction.input.push(TxIn { + previous_output: outpoint, + script_sig: ScriptBuf::new(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + witness: Witness::default(), + }); + + input_value += value.to_sat(); - input_value += additional_input_value.to_sat(); + if input_value > output_value { + break; + } + } + + if output_value > input_value { + return Err(jsonrpc_core::Error { + code: jsonrpc_core::ErrorCode::ServerError(-6), + message: "insufficent funds".into(), + data: None, + }); + } } let change_position = transaction.output.len() as i32; - transaction.output.push(TxOut { - value: input_value - output_value, - script_pubkey: ScriptBuf::new(), - }); + let change = input_value - output_value; + + if change > 0 { + transaction.output.push(TxOut { + value: change, + script_pubkey: state.new_address(true).into(), + }); + } let fee = if let Some(fee_rate) = options.fee_rate { // increase vsize to account for the witness that `fundrawtransaction` will add @@ -528,32 +558,46 @@ impl Api for Server { txid: Txid, _include_watchonly: Option, ) -> Result { - match self.state.lock().unwrap().transactions.get(&txid) { - Some(tx) => Ok( - serde_json::to_value(GetTransactionResult { - info: WalletTxInfo { - txid, - confirmations: 0, - time: 0, - timereceived: 0, - blockhash: None, - blockindex: None, - blockheight: None, - blocktime: None, - wallet_conflicts: Vec::new(), - bip125_replaceable: Bip125Replaceable::Unknown, - }, - amount: SignedAmount::from_sat(0), - fee: None, - details: Vec::new(), - hex: serialize(tx), - }) - .unwrap(), - ), - None => Err(jsonrpc_core::Error::new( + let state = self.state(); + + let Some(tx) = state.transactions.get(&txid) else { + return Err(jsonrpc_core::Error::new( jsonrpc_core::types::error::ErrorCode::ServerError(-8), - )), + )); + }; + + let mut confirmations = None; + + 'outer: for (height, hash) in state.hashes.iter().enumerate() { + for tx in &state.blocks[hash].txdata { + if tx.txid() == txid { + confirmations = Some(state.hashes.len() - height); + break 'outer; + } + } } + + Ok( + serde_json::to_value(GetTransactionResult { + info: WalletTxInfo { + txid, + confirmations: confirmations.unwrap().try_into().unwrap(), + time: 0, + timereceived: 0, + blockhash: None, + blockindex: None, + blockheight: None, + blocktime: None, + wallet_conflicts: Vec::new(), + bip125_replaceable: Bip125Replaceable::Unknown, + }, + amount: SignedAmount::from_sat(0), + fee: None, + details: Vec::new(), + hex: serialize(tx), + }) + .unwrap(), + ) } fn get_raw_transaction( @@ -626,28 +670,43 @@ impl Api for Server { let state = self.state(); - Ok( - state - .utxos - .iter() - .filter(|(outpoint, _amount)| !state.locked.contains(outpoint)) - .map(|(outpoint, &amount)| ListUnspentResultEntry { - txid: outpoint.txid, - vout: outpoint.vout, - address: None, - label: None, - redeem_script: None, - witness_script: None, - script_pub_key: ScriptBuf::new(), - amount, - confirmations: 0, - spendable: true, - solvable: true, - descriptor: None, - safe: true, - }) - .collect(), - ) + let mut unspent = Vec::new(); + + for (outpoint, &amount) in &state.utxos { + if state.locked.contains(outpoint) { + continue; + } + + let tx = state.transactions.get(&outpoint.txid).unwrap(); + + let tx_out = &tx.output[usize::try_from(outpoint.vout).unwrap()]; + + let Ok(address) = Address::from_script(&tx_out.script_pubkey, state.network) else { + continue; + }; + + if !state.is_wallet_address(&address) { + continue; + } + + unspent.push(ListUnspentResultEntry { + txid: outpoint.txid, + vout: outpoint.vout, + address: None, + label: None, + redeem_script: None, + witness_script: None, + script_pub_key: ScriptBuf::new(), + amount, + confirmations: 0, + spendable: true, + solvable: true, + descriptor: None, + safe: true, + }); + } + + Ok(unspent) } fn list_lock_unspent(&self) -> Result, jsonrpc_core::Error> { @@ -665,13 +724,7 @@ impl Api for Server { &self, _address_type: Option, ) -> Result { - let secp256k1 = Secp256k1::new(); - let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); - let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); - let address = Address::p2tr(&secp256k1, public_key, None, self.network); - self.state().change_addresses.push(address.clone()); - - Ok(address) + Ok(self.state().new_address(true)) } fn get_descriptor_info( @@ -708,12 +761,7 @@ impl Api for Server { _label: Option, _address_type: Option, ) -> Result { - let secp256k1 = Secp256k1::new(); - let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); - let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); - let address = Address::p2tr(&secp256k1, public_key, None, self.network); - - Ok(address) + Ok(self.state().new_address(false)) } fn list_transactions( diff --git a/crates/test-bitcoincore-rpc/src/state.rs b/crates/test-bitcoincore-rpc/src/state.rs index 72bf46bd7c..aaac2849a9 100644 --- a/crates/test-bitcoincore-rpc/src/state.rs +++ b/crates/test-bitcoincore-rpc/src/state.rs @@ -1,21 +1,29 @@ -use super::*; +use { + super::*, + bitcoin::{ + key::{KeyPair, Secp256k1, XOnlyPublicKey}, + secp256k1::rand, + WPubkeyHash, + }, +}; #[derive(Debug)] -pub(crate) struct State { - pub(crate) blocks: BTreeMap, - pub(crate) change_addresses: Vec
, - pub(crate) descriptors: Vec, - pub(crate) fail_lock_unspent: bool, - pub(crate) hashes: Vec, - pub(crate) loaded_wallets: BTreeSet, - pub(crate) locked: BTreeSet, - pub(crate) mempool: Vec, - pub(crate) network: Network, - pub(crate) nonce: u32, - pub(crate) transactions: BTreeMap, - pub(crate) utxos: BTreeMap, - pub(crate) version: usize, - pub(crate) wallets: BTreeSet, +pub struct State { + pub blocks: BTreeMap, + pub descriptors: Vec, + pub fail_lock_unspent: bool, + pub hashes: Vec, + pub loaded_wallets: BTreeSet, + pub locked: BTreeSet, + pub mempool: Vec, + pub network: Network, + pub nonce: u32, + pub transactions: BTreeMap, + pub utxos: BTreeMap, + pub version: usize, + pub receive_addresses: Vec
, + pub change_addresses: Vec
, + pub wallets: BTreeSet, } impl State { @@ -34,23 +42,66 @@ impl State { descriptors: Vec::new(), fail_lock_unspent, hashes, + loaded_wallets: BTreeSet::new(), locked: BTreeSet::new(), mempool: Vec::new(), network, nonce: 0, + receive_addresses: Vec::new(), transactions: BTreeMap::new(), utxos: BTreeMap::new(), version, wallets: BTreeSet::new(), - loaded_wallets: BTreeSet::new(), } } + pub(crate) fn new_address(&mut self, change: bool) -> Address { + let secp256k1 = Secp256k1::new(); + let key_pair = KeyPair::new(&secp256k1, &mut rand::thread_rng()); + let (public_key, _parity) = XOnlyPublicKey::from_keypair(&key_pair); + let address = Address::p2tr(&secp256k1, public_key, None, self.network); + if change { + &mut self.change_addresses + } else { + &mut self.receive_addresses + } + .push(address.clone()); + address + } + + pub fn is_wallet_address(&self, address: &Address) -> bool { + self.receive_addresses.contains(address) || self.change_addresses.contains(address) + } + pub(crate) fn clear(&mut self) { *self = Self::new(self.network, self.version, self.fail_lock_unspent); } - pub(crate) fn push_block(&mut self, subsidy: u64) -> Block { + #[track_caller] + pub fn balances(&self) -> BTreeMap> { + let mut addresses: BTreeMap> = BTreeMap::new(); + + for (&outpoint, &amount) in &self.utxos { + let transaction = self.transactions.get(&outpoint.txid).unwrap(); + let tx_out = &transaction.output[usize::try_from(outpoint.vout).unwrap()]; + + if tx_out.script_pubkey == ScriptBuf::new() { + continue; + } + + let address = Address::from_script(&tx_out.script_pubkey, self.network).unwrap(); + + addresses + .entry(address) + .or_default() + .push((outpoint, amount)); + } + + addresses + } + + #[track_caller] + pub(crate) fn mine_block(&mut self, subsidy: u64) -> Block { let coinbase = Transaction { version: 2, lock_time: LockTime::ZERO, @@ -83,7 +134,7 @@ impl State { fee }) .sum::(), - script_pubkey: ScriptBuf::new(), + script_pubkey: self.new_address(false).into(), }], }; @@ -105,7 +156,9 @@ impl State { for tx in block.txdata.iter() { for input in tx.input.iter() { - self.utxos.remove(&input.previous_output); + if !input.previous_output.is_null() { + assert!(self.utxos.remove(&input.previous_output).is_some()); + } } for (vout, txout) in tx.output.iter().enumerate() { @@ -173,7 +226,14 @@ impl State { .get(i) .cloned() .unwrap_or(value_per_output), - script_pubkey: script::Builder::new().into_script(), + script_pubkey: if template.p2tr { + let secp = Secp256k1::new(); + let keypair = KeyPair::new(&secp, &mut rand::thread_rng()); + let internal_key = XOnlyPublicKey::from_keypair(&keypair); + ScriptBuf::new_v1_p2tr(&secp, internal_key.0, None) + } else { + ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::all_zeros()) + }, }) .collect(), }; diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 4bde2e7bb7..0f21e93626 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.4" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -128,6 +128,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "array-init" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" +dependencies = [ + "nodrop", +] + [[package]] name = "asn1-rs" version = "0.3.1" @@ -273,7 +282,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -357,7 +366,6 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "headers", "http 0.2.9", "http-body", "hyper", @@ -443,12 +451,24 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bech32" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bip39" version = "2.0.0" @@ -466,7 +486,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e99ff7289b20a7385f66a0feda78af2fc119d28fb56aea8886a9cd0a4abdd75" dependencies = [ - "bech32", + "bech32 0.9.1", "bitcoin-private", "bitcoin_hashes 0.12.0", "hex_lit", @@ -544,7 +564,7 @@ dependencies = [ "new_mime_guess", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -676,7 +696,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -691,6 +711,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "concurrent-queue" version = "2.2.0" @@ -719,11 +749,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -743,16 +783,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -851,7 +881,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -873,7 +903,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" dependencies = [ "darling_core 0.20.3", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -904,7 +934,7 @@ checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -999,7 +1029,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -1023,17 +1053,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.0" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -1121,6 +1161,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1211,7 +1266,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -1372,31 +1427,6 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -[[package]] -name = "headers" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "bytes", - "headers-core", - "http 0.2.9", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.9", -] - [[package]] name = "heck" version = "0.4.1" @@ -1508,6 +1538,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1519,7 +1562,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows 0.48.0", ] [[package]] @@ -1537,6 +1580,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1591,15 +1644,10 @@ dependencies = [ ] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "ipnet" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix 0.38.4", - "windows-sys 0.48.0", -] +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" @@ -1653,9 +1701,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libfuzzer-sys" @@ -1682,9 +1730,9 @@ checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "log" -version = "0.4.19" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "matchit" @@ -1692,6 +1740,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.5.0" @@ -1773,6 +1827,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "never" version = "0.1.0" @@ -1801,6 +1873,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + [[package]] name = "nom" version = "7.1.3" @@ -1903,6 +1981,50 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.3.3", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -1911,14 +2033,14 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ord" -version = "0.12.3" +version = "0.16.0" dependencies = [ "anyhow", "async-trait", "axum", "axum-server", - "base64 0.21.2", - "bech32", + "base64 0.22.0", + "bech32 0.11.0", "bip39", "bitcoin", "boilerplate", @@ -1926,8 +2048,8 @@ dependencies = [ "chrono", "ciborium", "clap", + "colored", "ctrlc", - "derive_more", "dirs", "env_logger", "futures", @@ -1944,14 +2066,17 @@ dependencies = [ "miniscript", "mp4", "ord-bitcoincore-rpc", + "ordinals", "pulldown-cmark", "redb", "regex", + "reqwest", "rss", "rust-embed", "rustls 0.22.1", "rustls-acme", "serde", + "serde-hex", "serde_json", "serde_yaml", "sha3", @@ -1961,13 +2086,14 @@ dependencies = [ "tokio-stream", "tokio-util", "tower-http", + "urlencoding", ] [[package]] name = "ord-bitcoincore-rpc" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d57a4297d466506cde088e020b33819f9a496d50272b14d7890e6fde5595a3e" +checksum = "16b622f69d68d7201d5186615978aca36ceb7e7c57d7771491d3e261c64ff4d8" dependencies = [ "bitcoin-private", "jsonrpc", @@ -1989,6 +2115,16 @@ dependencies = [ "serde_json", ] +[[package]] +name = "ordinals" +version = "0.0.4" +dependencies = [ + "bitcoin", + "derive_more", + "serde", + "thiserror", +] + [[package]] name = "parking" version = "2.1.0" @@ -2027,7 +2163,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -2053,6 +2189,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "polling" version = "2.8.0" @@ -2083,25 +2225,32 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] [[package]] name = "pulldown-cmark" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.3.3", "getopts", "memchr", + "pulldown-cmark-escape", "unicase", ] +[[package]] +name = "pulldown-cmark-escape" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8f9aa0e3cbcfaf8bf00300004ee3b72f74770f9cbac93f6928771f613276b" + [[package]] name = "quick-xml" version = "0.30.0" @@ -2114,9 +2263,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2153,9 +2302,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.7.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -2163,14 +2312,12 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] @@ -2187,9 +2334,9 @@ dependencies = [ [[package]] name = "redb" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08837f9a129bde83c51953b8c96cbb3422b940166b730caa954836106eb1dfd2" +checksum = "72623e6275cd430215b741f41ebda34db93a13ebde253f908b70871c46afc5ba" dependencies = [ "libc", ] @@ -2252,6 +2399,46 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "reqwest" +version = "0.11.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eea5a9eb898d3783f17c6407670e3592fd174cb81a10e51d4c37f49450b9946" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.9", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -2313,7 +2500,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.27", + "syn 2.0.52", "walkdir", ] @@ -2490,6 +2677,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2527,6 +2723,29 @@ dependencies = [ "cc", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.18" @@ -2542,6 +2761,17 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-hex" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" +dependencies = [ + "array-init", + "serde", + "smallvec", +] + [[package]] name = "serde_derive" version = "1.0.177" @@ -2550,7 +2780,7 @@ checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -2600,17 +2830,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.7" @@ -2641,6 +2860,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "socket2" version = "0.4.9" @@ -2716,9 +2944,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -2745,9 +2973,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.7" +version = "0.30.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165d6d8539689e3d3bc8b98ac59541e1f21c7de7c85d60dc80e43ae0ed2113db" +checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2755,7 +2983,28 @@ dependencies = [ "ntapi", "once_cell", "rayon", - "winapi", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" +dependencies = [ + "bitflags 2.3.3", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2771,33 +3020,24 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", ] [[package]] @@ -2879,7 +3119,17 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -2941,6 +3191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ae70283aba8d2a8b411c695c437fe25b8b5e44e23e780662002fc72fb47a82" dependencies = [ "async-compression", + "base64 0.21.2", "bitflags 2.3.3", "bytes", "futures-core", @@ -2948,6 +3199,7 @@ dependencies = [ "http 0.2.9", "http-body", "http-range-header", + "mime", "pin-project-lite", "tokio", "tokio-util", @@ -3009,6 +3261,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + [[package]] name = "unicode-ident" version = "1.0.11" @@ -3054,6 +3312,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -3066,6 +3341,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -3130,7 +3411,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", "wasm-bindgen-shared", ] @@ -3164,7 +3445,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3231,6 +3512,25 @@ dependencies = [ "windows-targets 0.48.1", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -3249,6 +3549,15 @@ dependencies = [ "windows-targets 0.48.1", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3279,6 +3588,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.0", ] +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3291,6 +3615,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3303,6 +3633,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3315,6 +3651,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3327,6 +3669,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3339,6 +3687,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3351,6 +3705,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3363,6 +3723,22 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "x509-parser" version = "0.13.2" diff --git a/src/api.rs b/src/api.rs index a1b9c5127b..5f66506956 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,8 +1,5 @@ use { - super::{ - target_as_block_hash, BlockHash, Chain, Deserialize, Height, InscriptionId, OutPoint, Pile, - Rarity, SatPoint, Serialize, SpacedRune, TxMerkleNode, TxOut, - }, + super::*, serde_hex::{SerHex, Strict}, }; @@ -122,7 +119,7 @@ pub struct Inscriptions { #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { - pub address: Option, + pub address: Option>, pub indexed: bool, pub inscriptions: Vec, pub runes: Vec<(SpacedRune, Pile)>, @@ -148,7 +145,7 @@ impl Output { address: chain .address_from_script(&output.script_pubkey) .ok() - .map(|address| address.to_string()), + .map(|address| uncheck(&address)), indexed, inscriptions, runes, diff --git a/src/decimal.rs b/src/decimal.rs index f66b533ddc..ac102a5d96 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -1,13 +1,13 @@ use super::*; -#[derive(Debug, PartialEq, Copy, Clone)] +#[derive(Debug, PartialEq, Copy, Clone, Default)] pub struct Decimal { value: u128, scale: u8, } impl Decimal { - pub(crate) fn to_amount(self, divisibility: u8) -> Result { + pub fn to_amount(self, divisibility: u8) -> Result { match divisibility.checked_sub(self.scale) { Some(difference) => Ok( self @@ -196,6 +196,7 @@ mod tests { assert_eq!(decimal, string.parse::().unwrap()); } + case(Decimal { value: 0, scale: 0 }, "0"); case(Decimal { value: 1, scale: 0 }, "1"); case(Decimal { value: 1, scale: 1 }, "0.1"); case( diff --git a/src/index.rs b/src/index.rs index 8099467768..99b214b8d9 100644 --- a/src/index.rs +++ b/src/index.rs @@ -2,7 +2,8 @@ use { self::{ entry::{ Entry, HeaderValue, InscriptionEntry, InscriptionEntryValue, InscriptionIdValue, - OutPointValue, RuneEntryValue, RuneIdValue, SatPointValue, SatRange, TxidValue, + OutPointValue, OutputEntry, OutputValue, RuneEntryValue, RuneIdValue, SatPointValue, + SatRange, TxidValue, }, event::Event, reorg::*, @@ -47,7 +48,7 @@ mod updater; #[cfg(test)] pub(crate) mod testing; -const SCHEMA_VERSION: u64 = 19; +const SCHEMA_VERSION: u64 = 20; macro_rules! define_table { ($name:ident, $key:ty, $value:ty) => { @@ -71,6 +72,7 @@ define_table! { HEIGHT_TO_LAST_SEQUENCE_NUMBER, u32, u32 } define_table! { HOME_INSCRIPTIONS, u32, InscriptionIdValue } define_table! { INSCRIPTION_ID_TO_SEQUENCE_NUMBER, InscriptionIdValue, u32 } define_table! { INSCRIPTION_NUMBER_TO_SEQUENCE_NUMBER, i32, u32 } +define_table! { OUTPOINT_TO_OUTPUT, &OutPointValue, OutputValue } define_table! { OUTPOINT_TO_RUNE_BALANCES, &OutPointValue, &[u8] } define_table! { OUTPOINT_TO_SAT_RANGES, &OutPointValue, &[u8] } define_table! { OUTPOINT_TO_VALUE, &OutPointValue, u64} @@ -929,33 +931,58 @@ impl Index { Ok(balances) } - pub(crate) fn get_rune_balance_map(&self) -> Result>> { + pub(crate) fn get_rune_balance_map(&self) -> Result>> { let outpoint_balances = self.get_rune_balances()?; let rtx = self.database.begin_read()?; let rune_id_to_rune_entry = rtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; - let mut rune_balances: BTreeMap> = BTreeMap::new(); + let mut rune_balances_by_id: BTreeMap> = BTreeMap::new(); for (outpoint, balances) in outpoint_balances { for (rune_id, amount) in balances { - let rune = RuneEntry::load( - rune_id_to_rune_entry - .get(&rune_id.store())? - .unwrap() - .value(), - ) - .rune; - - *rune_balances - .entry(rune) + *rune_balances_by_id + .entry(rune_id) .or_default() .entry(outpoint) .or_default() += amount; } } + let mut rune_balances = BTreeMap::new(); + + for (rune_id, balances) in rune_balances_by_id { + let RuneEntry { + rune, + divisibility, + symbol, + .. + } = RuneEntry::load( + rune_id_to_rune_entry + .get(&rune_id.store())? + .unwrap() + .value(), + ); + + rune_balances.insert( + rune, + balances + .into_iter() + .map(|(outpoint, amount)| { + ( + outpoint, + Pile { + amount, + divisibility, + symbol, + }, + ) + }) + .collect(), + ); + } + Ok(rune_balances) } @@ -2414,12 +2441,14 @@ mod tests { fn find_first_sat_of_second_block() { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(1); + let tx = context.rpc_server.tx(1, 0); assert_eq!( context.index.find(Sat(50 * COIN_VALUE)).unwrap().unwrap(), SatPoint { - outpoint: "84aca0d43f45ac753d4744f40b2f54edec3a496b298951735d450e601386089d:0" - .parse() - .unwrap(), + outpoint: OutPoint { + txid: tx.txid(), + vout: 0, + }, offset: 0, } ) diff --git a/src/index/entry.rs b/src/index/entry.rs index 3981bbd268..50726896f0 100644 --- a/src/index/entry.rs +++ b/src/index/entry.rs @@ -28,6 +28,26 @@ impl Entry for Header { } } +#[derive(Debug, PartialEq, Copy, Clone)] +pub(super) struct OutputEntry { + pub(super) height: u32, + pub(super) taproot: bool, +} + +pub(super) type OutputValue = (u32, bool); + +impl Entry for OutputEntry { + type Value = OutputValue; + + fn load((height, taproot): Self::Value) -> Self { + Self { height, taproot } + } + + fn store(self) -> Self::Value { + (self.height, self.taproot) + } +} + #[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)] pub struct RuneEntry { pub burned: u128, @@ -566,4 +586,16 @@ mod tests { assert_eq!(actual, expected); } + + #[test] + fn output() { + let value = (0, true); + let entry = OutputEntry { + height: 0, + taproot: true, + }; + + assert_eq!(entry.store(), value); + assert_eq!(OutputEntry::load(value), entry); + } } diff --git a/src/index/testing.rs b/src/index/testing.rs index 701e24c681..00ada3fc23 100644 --- a/src/index/testing.rs +++ b/src/index/testing.rs @@ -1,4 +1,4 @@ -use {super::*, std::ffi::OsString, tempfile::TempDir}; +use {super::*, bitcoin::script::PushBytes, std::ffi::OsString, tempfile::TempDir}; pub(crate) struct ContextBuilder { args: Vec, @@ -89,10 +89,12 @@ impl Context { } } + #[track_caller] pub(crate) fn mine_blocks(&self, n: u64) -> Vec { self.mine_blocks_with_update(n, true) } + #[track_caller] pub(crate) fn mine_blocks_with_update(&self, n: u64, update: bool) -> Vec { let blocks = self.rpc_server.mine_blocks(n); if update { @@ -149,4 +151,58 @@ impl Context { ); } } + + pub(crate) fn etch(&self, runestone: Runestone, outputs: usize) -> (Txid, RuneId) { + let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap(); + + self.mine_blocks(1); + + self.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..Default::default() + }); + + self.mine_blocks(RUNE_COMMIT_INTERVAL.into()); + + let mut witness = Witness::new(); + + if let Some(etching) = runestone.etching { + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + etching + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + } else { + witness.push(ScriptBuf::new()); + } + + witness.push([]); + + let txid = self.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs, + ..Default::default() + }); + + self.mine_blocks(1); + + ( + txid, + RuneId { + height: u32::try_from(block_count + usize::try_from(RUNE_COMMIT_INTERVAL).unwrap() + 1) + .unwrap(), + index: 1, + }, + ) + } } diff --git a/src/index/updater.rs b/src/index/updater.rs index b6ca59fd70..9e620b170b 100644 --- a/src/index/updater.rs +++ b/src/index/updater.rs @@ -580,6 +580,7 @@ impl<'index> Updater<'index> { if self.index.index_runes && self.height >= self.index.settings.first_rune_height() { let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?; + let mut outpoint_to_output = wtx.open_table(OUTPOINT_TO_OUTPUT)?; let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?; let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?; let mut sequence_number_to_rune_id = wtx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?; @@ -596,6 +597,7 @@ impl<'index> Updater<'index> { inscription_id_to_sequence_number: &mut inscription_id_to_sequence_number, minimum: Rune::minimum_at_height(self.index.settings.chain(), Height(self.height)), outpoint_to_balances: &mut outpoint_to_rune_balances, + outpoint_to_output: &mut outpoint_to_output, rune_to_id: &mut rune_to_rune_id, runes, sequence_number_to_rune_id: &mut sequence_number_to_rune_id, diff --git a/src/index/updater/rune_updater.rs b/src/index/updater/rune_updater.rs index 555e6c495c..33ec8f4749 100644 --- a/src/index/updater/rune_updater.rs +++ b/src/index/updater/rune_updater.rs @@ -31,6 +31,7 @@ pub(super) struct RuneUpdater<'a, 'db, 'tx> { pub(super) inscription_id_to_sequence_number: &'a Table<'db, 'tx, InscriptionIdValue, u32>, pub(super) minimum: Rune, pub(super) outpoint_to_balances: &'a mut Table<'db, 'tx, &'static OutPointValue, &'static [u8]>, + pub(super) outpoint_to_output: &'a mut Table<'db, 'tx, &'static OutPointValue, OutputValue>, pub(super) rune_to_id: &'a mut Table<'db, 'tx, u128, RuneIdValue>, pub(super) runes: u64, pub(super) sequence_number_to_rune_id: &'a mut Table<'db, 'tx, u32, RuneIdValue>, @@ -73,7 +74,7 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { update.supply += claim.limit; } - let mut etched = self.etched(index, &runestone)?; + let mut etched = self.etched(index, tx, &runestone)?; if !burn { for Edict { id, amount, output } in runestone.edicts { @@ -228,6 +229,33 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { )?; } + for input in tx.input.iter() { + if input.previous_output.is_null() { + continue; + } + + self + .outpoint_to_output + .remove(&input.previous_output.store())? + .unwrap(); + } + + for (vout, output) in tx.output.iter().enumerate() { + let outpoint = OutPoint { + txid, + vout: vout.try_into().unwrap(), + }; + + self.outpoint_to_output.insert( + &outpoint.store(), + OutputEntry { + height: self.height, + taproot: output.script_pubkey.is_v1_p2tr(), + } + .store(), + )?; + } + // increment entries with burned runes for (id, amount) in burned { self @@ -299,25 +327,24 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { Ok(()) } - fn etched(&mut self, index: usize, runestone: &Runestone) -> Result> { + fn etched( + &mut self, + index: usize, + tx: &Transaction, + runestone: &Runestone, + ) -> Result> { let Some(etching) = runestone.etching else { return Ok(None); }; - if etching - .rune - .map(|rune| rune < self.minimum || rune.is_reserved()) - .unwrap_or_default() - || etching - .rune - .and_then(|rune| self.rune_to_id.get(rune.0).transpose()) - .transpose()? - .is_some() - { - return Ok(None); - } - let rune = if let Some(rune) = etching.rune { + if rune < self.minimum + || rune.is_reserved() + || self.rune_to_id.get(rune.0)?.is_some() + || !self.tx_commits_to_rune(tx, rune)? + { + return Ok(None); + } rune } else { let reserved_runes = self @@ -378,6 +405,43 @@ impl<'a, 'db, 'tx> RuneUpdater<'a, 'db, 'tx> { Ok(Some(Claim { id, limit })) } + fn tx_commits_to_rune(&self, tx: &Transaction, rune: Rune) -> Result { + let commitment = rune.commitment(); + + for input in &tx.input { + let Some(tapscript) = input.witness.tapscript() else { + continue; + }; + + for instruction in tapscript.instructions() { + let instruction = instruction?; + + let Some(pushbytes) = instruction.push_bytes() else { + continue; + }; + + if pushbytes.as_bytes() != commitment { + continue; + } + + let Some(output) = self + .outpoint_to_output + .get(&input.previous_output.store())? + else { + panic!("input not in UTXO set: {}", input.previous_output); + }; + + let output = OutputEntry::load(output.value()); + + if output.taproot && self.height >= output.height + RUNE_COMMIT_INTERVAL { + return Ok(true); + } + } + } + + Ok(false) + } + fn unallocated(&mut self, tx: &Transaction) -> Result> { // map of rune ID to un-allocated balance of that rune let mut unallocated: HashMap = HashMap::new(); diff --git a/src/inscriptions/envelope.rs b/src/inscriptions/envelope.rs index 699059dd12..da54535ec6 100644 --- a/src/inscriptions/envelope.rs +++ b/src/inscriptions/envelope.rs @@ -55,6 +55,7 @@ impl From for ParsedEnvelope { let metaprotocol = Tag::Metaprotocol.take(&mut fields); let parents = Tag::Parent.take_array(&mut fields); let pointer = Tag::Pointer.take(&mut fields); + let rune = Tag::Rune.take(&mut fields); let unrecognized_even_field = fields .keys() @@ -78,6 +79,7 @@ impl From for ParsedEnvelope { metaprotocol, parents, pointer, + rune, unrecognized_even_field, }, input: envelope.input, diff --git a/src/inscriptions/inscription.rs b/src/inscriptions/inscription.rs index 39179693a8..f764f5ed96 100644 --- a/src/inscriptions/inscription.rs +++ b/src/inscriptions/inscription.rs @@ -23,6 +23,7 @@ pub struct Inscription { pub metaprotocol: Option>, pub parents: Vec>, pub pointer: Option>, + pub rune: Option>, pub unrecognized_even_field: bool, } @@ -45,6 +46,7 @@ impl Inscription { parents: Vec, path: impl AsRef, pointer: Option, + rune: Option, ) -> Result { let path = path.as_ref(); @@ -104,6 +106,7 @@ impl Inscription { metaprotocol: metaprotocol.map(|metaprotocol| metaprotocol.into_bytes()), parents: parents.iter().map(|parent| parent.value()).collect(), pointer: pointer.map(Self::pointer_value), + rune: rune.map(|rune| rune.commitment()), ..Default::default() }) } @@ -134,6 +137,7 @@ impl Inscription { Tag::Delegate.append(&mut builder, &self.delegate); Tag::Pointer.append(&mut builder, &self.pointer); Tag::Metadata.append(&mut builder, &self.metadata); + Tag::Rune.append(&mut builder, &self.rune); if let Some(body) = &self.body { builder = builder.push_slice(envelope::BODY_TAG); @@ -779,6 +783,7 @@ mod tests { Vec::new(), file.path(), None, + None, ) .unwrap(); @@ -793,6 +798,7 @@ mod tests { Vec::new(), file.path(), Some(0), + None, ) .unwrap(); @@ -807,6 +813,7 @@ mod tests { Vec::new(), file.path(), Some(1), + None, ) .unwrap(); @@ -821,6 +828,7 @@ mod tests { Vec::new(), file.path(), Some(256), + None, ) .unwrap(); diff --git a/src/inscriptions/tag.rs b/src/inscriptions/tag.rs index 86c086afaa..ad86f0ebdf 100644 --- a/src/inscriptions/tag.rs +++ b/src/inscriptions/tag.rs @@ -13,6 +13,7 @@ pub(crate) enum Tag { Metaprotocol = 7, ContentEncoding = 9, Delegate = 11, + Rune = 13, #[allow(unused)] Note = 15, #[allow(unused)] diff --git a/src/lib.rs b/src/lib.rs index ddc4417a06..6068dcd9db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,13 +16,14 @@ use { arguments::Arguments, blocktime::Blocktime, decimal::Decimal, + index::BitcoinCoreRpcResultExt, inscriptions::{ inscription_id, media::{self, ImageRendering, Media}, teleburn, Charm, ParsedEnvelope, }, representation::Representation, - runes::{Etching, Pile, SpacedRune}, + runes::Etching, settings::Settings, subcommand::{Subcommand, SubcommandResult}, tally::Tally, @@ -84,7 +85,7 @@ pub use self::{ inscriptions::{Envelope, Inscription, InscriptionId}, object::Object, options::Options, - runes::{Edict, Rune, RuneId, Runestone}, + runes::{Edict, Pile, Rune, RuneId, Runestone, SpacedRune}, wallet::transaction_builder::{Target, TransactionBuilder}, }; @@ -126,12 +127,13 @@ pub mod wallet; type Result = std::result::Result; +const RUNE_COMMIT_INTERVAL: u32 = 6; +const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); + static SHUTTING_DOWN: AtomicBool = AtomicBool::new(false); static LISTENERS: Mutex> = Mutex::new(Vec::new()); static INDEXER: Mutex>> = Mutex::new(None); -const TARGET_POSTAGE: Amount = Amount::from_sat(10_000); - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn fund_raw_transaction( client: &Client, @@ -194,6 +196,10 @@ fn unbound_outpoint() -> OutPoint { } } +fn uncheck(address: &Address) -> Address { + address.to_string().parse().unwrap() +} + pub fn parse_ord_server_args(args: &str) -> (Settings, subcommand::server::Server) { match Arguments::try_parse_from(args.split_whitespace()) { Ok(arguments) => match arguments.subcommand { diff --git a/src/runes.rs b/src/runes.rs index e2b6e8c754..389639bc6c 100644 --- a/src/runes.rs +++ b/src/runes.rs @@ -3,9 +3,10 @@ use { super::*, }; -pub use {edict::Edict, rune::Rune, rune_id::RuneId, runestone::Runestone}; - -pub use {etching::Etching, mint::Mint, pile::Pile, spaced_rune::SpacedRune}; +pub use { + edict::Edict, etching::Etching, mint::Mint, pile::Pile, rune::Rune, rune_id::RuneId, + runestone::Runestone, spaced_rune::SpacedRune, +}; pub const MAX_DIVISIBILITY: u8 = 38; pub const MAX_LIMIT: u128 = 1 << 64; @@ -62,27 +63,16 @@ mod tests { context.mine_blocks(1); - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + }), + ..Default::default() + }, + 1, + ); context.assert_runes([], []); } @@ -93,6 +83,8 @@ mod tests { context.mine_blocks(1); + context.etch(Default::default(), 1); + context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), @@ -108,29 +100,16 @@ mod tests { fn etching_with_no_edicts_creates_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -138,7 +117,7 @@ mod tests { RuneEntry { etching: txid, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -150,34 +129,21 @@ mod tests { fn etching_with_edict_creates_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -186,7 +152,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -196,7 +162,7 @@ mod tests { #[test] fn runes_must_be_greater_than_or_equal_to_minimum_for_height() { - let block_two_minimum: u128 = Rune::minimum_at_height(Chain::Regtest, Height(2)).0; + let minimum = Rune::minimum_at_height(Chain::Regtest, Height(RUNE_COMMIT_INTERVAL + 2)).0; { let context = Context::builder() @@ -204,73 +170,55 @@ mod tests { .arg("--index-runes") .build(); - context.mine_blocks(1); - - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(block_two_minimum - 1)), - ..Default::default() - }), + context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(minimum - 1)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + }), + ..Default::default() + }, + 1, + ); context.assert_runes([], []); } { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); + let context = Context::builder() + .chain(Chain::Regtest) + .arg("--index-runes") + .build(); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(block_two_minimum)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(minimum)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { etching: txid, - rune: Rune(block_two_minimum), + rune: Rune(minimum), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -284,29 +232,21 @@ mod tests { { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RESERVED)), - ..Default::default() - }), + context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RESERVED)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); + }), + ..Default::default() + }, + 1, + ); context.assert_runes([], []); } @@ -314,34 +254,21 @@ mod tests { { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RESERVED - 1)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RESERVED - 1)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -350,7 +277,7 @@ mod tests { etching: txid, rune: Rune(RESERVED - 1), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -416,7 +343,7 @@ mod tests { context.mine_blocks(1); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -489,36 +416,25 @@ mod tests { fn etching_with_non_zero_divisibility_and_rune() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - divisibility: 1, - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + divisibility: 1, + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + }), + ..Default::default() + }, + 1, + ); context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, @@ -527,7 +443,7 @@ mod tests { etching: txid, divisibility: 1, supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -539,41 +455,28 @@ mod tests { fn allocations_over_max_supply_are_ignored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: u128::MAX, + output: 0, + }, + Edict { + id: 0, + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -582,7 +485,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -594,41 +497,28 @@ mod tests { fn allocations_partially_over_max_supply_are_honored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX / 2, - output: 0, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: u128::MAX / 2, + output: 0, + }, + Edict { + id: 0, + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -638,7 +528,7 @@ mod tests { rune: Rune(RUNE), supply: u128::MAX, symbol: None, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -652,32 +542,21 @@ mod tests { context.mine_blocks(1); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 100, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 100, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -686,7 +565,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: 100, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -698,41 +577,28 @@ mod tests { fn etching_may_allocate_to_multiple_outputs() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 100, - output: 0, - }, - Edict { - id: 0, - amount: 100, - output: 1, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 100, + output: 0, + }, + Edict { + id: 0, + amount: 100, + output: 1, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -742,7 +608,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: 200, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -754,41 +620,28 @@ mod tests { fn allocations_to_invalid_outputs_are_ignored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 100, - output: 0, - }, - Edict { - id: 0, - amount: 100, - output: 3, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 100, + output: 0, + }, + Edict { + id: 0, + amount: 100, + output: 3, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -797,7 +650,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: 100, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -809,34 +662,21 @@ mod tests { fn input_runes_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -845,7 +685,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -859,7 +699,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -883,7 +723,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -901,36 +741,23 @@ mod tests { fn etched_rune_is_allocated_with_zero_supply_for_burned_runestone() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - default_output: None, - burn: true, - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + default_output: None, + burn: true, + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -938,7 +765,7 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -950,42 +777,29 @@ mod tests { fn etched_rune_open_etching_parameters_are_unset_for_burned_runestone() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - deadline: Some(1), - limit: Some(1), - term: Some(1), - }), - divisibility: 1, - symbol: Some('$'), - spacers: 1, + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + deadline: Some(1), + limit: Some(1), + term: Some(1), }), - burn: true, - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + divisibility: 1, + symbol: Some('$'), + spacers: 1, + }), + burn: true, + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1001,7 +815,7 @@ mod tests { spacers: 1, supply: 0, symbol: Some('$'), - timestamp: 2, + timestamp: id.height, }, )], [], @@ -1057,43 +871,30 @@ mod tests { fn input_runes_are_burned_if_an_unrecognized_even_tag_is_encountered() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid0, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, + context.assert_runes( + [( + id, + RuneEntry { + etching: txid0, + rune: Rune(RUNE), + supply: u128::MAX, + timestamp: id.height, ..Default::default() }, )], @@ -1107,7 +908,7 @@ mod tests { ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { burn: true, @@ -1128,7 +929,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1140,34 +941,21 @@ mod tests { fn unallocated_runes_are_assigned_to_first_non_op_return_output() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1176,7 +964,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1190,7 +978,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), ..Default::default() }); @@ -1204,7 +992,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1222,34 +1010,21 @@ mod tests { fn unallocated_runes_are_burned_if_no_non_op_return_output_is_present() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1258,7 +1033,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1272,7 +1047,7 @@ mod tests { ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some(Runestone::default().encipher()), outputs: 0, ..Default::default() @@ -1287,7 +1062,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, burned: u128::MAX, ..Default::default() }, @@ -1300,34 +1075,21 @@ mod tests { fn unallocated_runes_are_assigned_to_default_output() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1336,7 +1098,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1350,7 +1112,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -1371,7 +1133,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1389,34 +1151,21 @@ mod tests { fn unallocated_runes_are_assigned_to_first_non_op_return_output_if_default_is_too_large() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1425,7 +1174,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1439,7 +1188,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -1460,7 +1209,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1478,34 +1227,21 @@ mod tests { fn unallocated_runes_are_burned_if_default_output_is_op_return() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1514,7 +1250,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1528,7 +1264,7 @@ mod tests { ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -1550,7 +1286,7 @@ mod tests { rune: Rune(RUNE), supply: u128::MAX, burned: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1563,34 +1299,21 @@ mod tests { ) { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1599,7 +1322,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1613,7 +1336,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: None, ..Default::default() }); @@ -1627,7 +1350,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -1645,34 +1368,53 @@ mod tests { fn duplicate_runes_are_forbidden() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + context.assert_runes( + [( + id, + RuneEntry { + etching: txid, + rune: Rune(RUNE), + supply: u128::MAX, + timestamp: id.height, ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], + ); - context.mine_blocks(1); + context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); - let id = RuneId { - height: 2, - index: 1, - }; + context.mine_blocks(1); context.assert_runes( [( @@ -1681,82 +1423,33 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], ); - - context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - context.assert_runes( - [( - id, - RuneEntry { - etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() - }, - )], - [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], - ); - } + } #[test] - fn outpoint_may_hold_multiple_runes() { + fn output_may_hold_multiple_runes() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1765,7 +1458,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, )], @@ -1778,32 +1471,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [ @@ -1813,7 +1495,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -1823,7 +1505,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -1848,7 +1530,10 @@ mod tests { ); let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], + inputs: &[ + (id0.height.try_into().unwrap(), 1, 0, Witness::new()), + (id1.height.try_into().unwrap(), 1, 0, Witness::new()), + ], ..Default::default() }); @@ -1862,7 +1547,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -1872,7 +1557,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -1892,34 +1577,21 @@ mod tests { fn multiple_input_runes_on_the_same_input_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -1928,7 +1600,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, )], @@ -1941,32 +1613,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [ @@ -1976,7 +1637,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -1986,7 +1647,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2011,7 +1672,10 @@ mod tests { ); let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], + inputs: &[ + (id0.height.try_into().unwrap(), 1, 0, Witness::new()), + (id1.height.try_into().unwrap(), 1, 0, Witness::new()), + ], ..Default::default() }); @@ -2025,7 +1689,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2035,7 +1699,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2051,7 +1715,7 @@ mod tests { ); let txid3 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(4, 1, 0, Witness::new())], + inputs: &[((id1.height + 1).try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -2084,7 +1748,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2094,7 +1758,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2123,34 +1787,21 @@ mod tests { fn multiple_input_runes_on_different_inputs_may_be_allocated() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2159,7 +1810,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, )], @@ -2172,32 +1823,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [ @@ -2207,7 +1847,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2217,7 +1857,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2242,7 +1882,10 @@ mod tests { ); let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new()), (3, 1, 0, Witness::new())], + inputs: &[ + (id0.height.try_into().unwrap(), 1, 0, Witness::new()), + (id1.height.try_into().unwrap(), 1, 0, Witness::new()), + ], op_return: Some( Runestone { edicts: vec![ @@ -2274,7 +1917,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2284,7 +1927,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2305,34 +1948,21 @@ mod tests { ) { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2341,7 +1971,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2355,7 +1985,7 @@ mod tests { ); let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) @@ -2374,7 +2004,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2386,59 +2016,37 @@ mod tests { fn rune_rarity_is_assigned_correctly() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(2); + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - let id0 = RuneId { - height: 3, - index: 1, - }; - - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 2, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [ @@ -2448,7 +2056,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 3, + timestamp: id0.height, ..Default::default() }, ), @@ -2458,7 +2066,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2489,32 +2097,21 @@ mod tests { context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2523,7 +2120,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2537,7 +2134,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ @@ -2568,7 +2165,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2586,34 +2183,21 @@ mod tests { fn edicts_which_refer_to_input_rune_with_no_balance_are_skipped() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id0 = RuneId { - height: 2, - index: 1, - }; + let (txid0, id0) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2622,7 +2206,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, )], @@ -2635,32 +2219,21 @@ mod tests { )], ); - let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE + 1)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id1 = RuneId { - height: 3, - index: 1, - }; + let (txid1, id1) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE + 1)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [ @@ -2670,7 +2243,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2680,7 +2253,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2705,7 +2278,7 @@ mod tests { ); let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id0.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ @@ -2737,7 +2310,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id0.height, ..Default::default() }, ), @@ -2747,7 +2320,7 @@ mod tests { etching: txid1, rune: Rune(RUNE + 1), supply: u128::MAX, - timestamp: 3, + timestamp: id1.height, number: 1, ..Default::default() }, @@ -2776,34 +2349,21 @@ mod tests { fn edicts_over_max_inputs_are_ignored() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX / 2, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX / 2, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2812,7 +2372,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX / 2, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2826,7 +2386,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -2850,7 +2410,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX / 2, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2868,34 +2428,21 @@ mod tests { fn edicts_may_transfer_runes_to_op_return_outputs() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 1, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 1, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2905,7 +2452,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2917,35 +2464,21 @@ mod tests { fn outputs_with_no_runes_have_no_balance() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 2, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -2954,7 +2487,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -2966,42 +2499,28 @@ mod tests { fn edicts_which_transfer_no_runes_to_output_create_no_balance_entry() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 2, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - Edict { - id: 0, - amount: 0, - output: 1, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: u128::MAX, + output: 0, + }, + Edict { + id: 0, + amount: 0, + output: 1, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3010,7 +2529,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3022,35 +2541,21 @@ mod tests { fn split_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 0, - output: 5, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 0, + output: 5, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3059,7 +2564,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3076,42 +2581,28 @@ mod tests { fn split_in_etching_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 1000, - output: 0, - }, - Edict { - id: 0, - amount: 0, - output: 5, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 1000, + output: 0, + }, + Edict { + id: 0, + amount: 0, + output: 5, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3120,7 +2611,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3149,42 +2640,28 @@ mod tests { fn split_in_etching_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 0, - output: 5, - }, - Edict { - id: 0, - amount: 1000, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 0, + output: 5, + }, + Edict { + id: 0, + amount: 1000, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3193,7 +2670,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3210,35 +2687,21 @@ mod tests { fn split_with_amount_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 1000, - output: 5, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 1000, + output: 5, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3247,7 +2710,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: 4000, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3264,42 +2727,28 @@ mod tests { fn split_in_etching_with_amount_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: u128::MAX - 3000, - output: 0, - }, - Edict { - id: 0, - amount: 1000, - output: 5, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: u128::MAX - 3000, + output: 0, + }, + Edict { + id: 0, + amount: 1000, + output: 5, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3308,7 +2757,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3324,42 +2773,28 @@ mod tests { fn split_in_etching_with_amount_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - outputs: 4, - op_return: Some( - Runestone { - edicts: vec![ - Edict { - id: 0, - amount: 1000, - output: 5, - }, - Edict { - id: 0, - amount: u128::MAX, - output: 0, - }, - ], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![ + Edict { + id: 0, + amount: 1000, + output: 5, + }, + Edict { + id: 0, + amount: u128::MAX, + output: 0, + }, + ], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 4, + ); context.assert_runes( [( @@ -3368,7 +2803,7 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3381,41 +2816,28 @@ mod tests { (OutPoint { txid, vout: 2 }, vec![(id, 1000)]), (OutPoint { txid, vout: 3 }, vec![(id, 1000)]), ], - ); - } - - #[test] - fn split() { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + ); + } - context.mine_blocks(1); + #[test] + fn split() { + let context = Context::builder().arg("--index-runes").build(); - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3424,7 +2846,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3438,7 +2860,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -3463,7 +2885,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3490,34 +2912,21 @@ mod tests { fn split_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3526,7 +2935,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3540,7 +2949,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -3572,7 +2981,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3599,34 +3008,21 @@ mod tests { fn split_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3635,7 +3031,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3649,7 +3045,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -3681,7 +3077,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3708,34 +3104,21 @@ mod tests { fn split_with_amount() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3744,7 +3127,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3758,7 +3141,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -3783,7 +3166,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3810,34 +3193,21 @@ mod tests { fn split_with_amount_with_preceding_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3846,7 +3216,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3860,7 +3230,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 4, op_return: Some( Runestone { @@ -3892,7 +3262,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3919,34 +3289,21 @@ mod tests { fn split_with_amount_with_following_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -3955,7 +3312,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -3969,7 +3326,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 4, op_return: Some( Runestone { @@ -4001,7 +3358,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4042,35 +3399,22 @@ mod tests { fn etching_may_specify_symbol() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - symbol: Some('$'), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + symbol: Some('$'), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4080,7 +3424,7 @@ mod tests { rune: Rune(RUNE), supply: u128::MAX, symbol: Some('$'), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4092,82 +3436,56 @@ mod tests { fn allocate_all_remaining_runes_in_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 0, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 0, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { etching: txid, - rune: Rune(RUNE), - supply: u128::MAX, - timestamp: 2, - ..Default::default() - }, - )], - [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], - ); - } - - #[test] - fn allocate_all_remaining_runes_in_inputs() { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + rune: Rune(RUNE), + supply: u128::MAX, + timestamp: id.height, ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + }, + )], + [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])], + ); + } - context.mine_blocks(1); + #[test] + fn allocate_all_remaining_runes_in_inputs() { + let context = Context::builder().arg("--index-runes").build(); - let id = RuneId { - height: 2, - index: 1, - }; + let (txid0, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4176,7 +3494,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4190,7 +3508,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 1, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 1, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -4215,7 +3533,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4232,7 +3550,7 @@ mod tests { #[test] fn max_limit() { MAX_LIMIT - .checked_mul(u128::from(u16::MAX) * 144 * 365 * 1_000_000_000) + .checked_mul(u128::from(u16::MAX) * u128::from(RUNE_COMMIT_INTERVAL) * 365 * 1_000_000_000) .unwrap(); } @@ -4240,34 +3558,21 @@ mod tests { fn rune_can_be_minted_without_edict() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - // etch the rune - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4275,7 +3580,7 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mints: 0, mint: Some(MintEntry { limit: Some(1000), @@ -4314,7 +3619,7 @@ mod tests { mints: 1, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4332,34 +3637,20 @@ mod tests { fn etching_with_limit_can_be_minted() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - // etch the rune - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4367,7 +3658,7 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mints: 0, mint: Some(MintEntry { limit: Some(1000), @@ -4381,7 +3672,7 @@ mod tests { // claim the rune let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -4411,7 +3702,7 @@ mod tests { mints: 1, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4426,7 +3717,7 @@ mod tests { // claim the rune let txid2 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(3, 0, 0, Witness::new())], + inputs: &[(4, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -4456,7 +3747,7 @@ mod tests { mints: 2, rune: Rune(RUNE), supply: 2000, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4480,7 +3771,7 @@ mod tests { // claim the rune in a burn runestone context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(4, 0, 0, Witness::new())], + inputs: &[(5, 0, 0, Witness::new())], op_return: Some( Runestone { burn: true, @@ -4512,7 +3803,7 @@ mod tests { mints: 3, rune: Rune(RUNE), supply: 3000, - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4539,34 +3830,21 @@ mod tests { fn open_etchings_can_be_limited_to_term() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - term: Some(2), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + term: Some(2), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4576,10 +3854,10 @@ mod tests { rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), - end: Some(4), + end: Some(id.height + 2), ..Default::default() }), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4613,11 +3891,11 @@ mod tests { rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), - end: Some(4), + end: Some(id.height + 2), ..Default::default() }), supply: 1000, - timestamp: 2, + timestamp: id.height, mints: 1, ..Default::default() }, @@ -4657,10 +3935,10 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(1000), - end: Some(4), + end: Some(id.height + 2), ..Default::default() }), mints: 1, @@ -4681,39 +3959,26 @@ mod tests { fn open_etchings_with_term_zero_cannot_be_minted() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: 1000, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - term: Some(0), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: 1000, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + term: Some(0), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4723,10 +3988,10 @@ mod tests { rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), - end: Some(2), + end: Some(id.height), ..Default::default() }), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -4759,10 +4024,10 @@ mod tests { RuneEntry { etching: txid, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(1000), - end: Some(2), + end: Some(id.height), ..Default::default() }), ..Default::default() @@ -4778,32 +4043,21 @@ mod tests { context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - deadline: Some(5), - term: Some(2), - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + deadline: Some(12), + term: Some(2), }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4811,10 +4065,10 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: 9, mint: Some(MintEntry { - deadline: Some(5), - end: Some(4), + deadline: Some(12), + end: Some(11), limit: Some(1000), }), ..Default::default() @@ -4824,14 +4078,9 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + inputs: &[(id.height.try_into().unwrap(), 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], claim: Some(id), ..Default::default() } @@ -4848,12 +4097,12 @@ mod tests { RuneEntry { rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: 9, mints: 1, etching: txid0, mint: Some(MintEntry { - deadline: Some(5), - end: Some(4), + deadline: Some(12), + end: Some(11), limit: Some(1000), }), ..Default::default() @@ -4872,11 +4121,6 @@ mod tests { inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], claim: Some(id), ..Default::default() } @@ -4894,11 +4138,11 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: 9, mint: Some(MintEntry { limit: Some(1000), - deadline: Some(5), - end: Some(4), + deadline: Some(12), + end: Some(11), }), mints: 1, ..Default::default() @@ -4920,32 +4164,21 @@ mod tests { context.mine_blocks(1); - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - deadline: Some(4), - term: Some(3), - }), - ..Default::default() + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + deadline: Some(11), + term: Some(3), }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -4953,10 +4186,10 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { - deadline: Some(4), - end: Some(5), + deadline: Some(11), + end: Some(12), limit: Some(1000), }), ..Default::default() @@ -4966,14 +4199,9 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + inputs: &[(3, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], claim: Some(id), ..Default::default() } @@ -4990,12 +4218,12 @@ mod tests { RuneEntry { rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mints: 1, etching: txid0, mint: Some(MintEntry { - deadline: Some(4), - end: Some(5), + deadline: Some(11), + end: Some(12), limit: Some(1000), }), ..Default::default() @@ -5011,14 +4239,9 @@ mod tests { ); context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(3, 0, 0, Witness::new())], + inputs: &[(4, 0, 0, Witness::new())], op_return: Some( Runestone { - edicts: vec![Edict { - id: u128::from(id), - amount: 1000, - output: 0, - }], claim: Some(id), ..Default::default() } @@ -5036,11 +4259,11 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(1000), - deadline: Some(4), - end: Some(5), + deadline: Some(11), + end: Some(12), }), mints: 1, ..Default::default() @@ -5060,34 +4283,21 @@ mod tests { fn open_etchings_can_be_limited_to_deadline() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - deadline: Some(4), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + deadline: Some(RUNE_COMMIT_INTERVAL + 4), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -5095,9 +4305,9 @@ mod tests { RuneEntry { etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { - deadline: Some(4), + deadline: Some(id.height + 2), limit: Some(1000), ..Default::default() }), @@ -5132,11 +4342,11 @@ mod tests { RuneEntry { rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mints: 1, etching: txid0, mint: Some(MintEntry { - deadline: Some(4), + deadline: Some(id.height + 2), limit: Some(1000), ..Default::default() }), @@ -5178,10 +4388,10 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(1000), - deadline: Some(4), + deadline: Some(id.height + 2), ..Default::default() }), mints: 1, @@ -5200,35 +4410,22 @@ mod tests { #[test] fn open_etching_claims_can_use_split() { - let context = Context::builder().arg("--index-runes").build(); - - context.mine_blocks(1); - - let txid0 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + let context = Context::builder().arg("--index-runes").build(); + + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -5240,7 +4437,7 @@ mod tests { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -5248,7 +4445,7 @@ mod tests { ); let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(2, 0, 0, Witness::new())], + inputs: &[(3, 0, 0, Witness::new())], outputs: 2, op_return: Some( Runestone { @@ -5274,7 +4471,7 @@ mod tests { etching: txid0, rune: Rune(RUNE), supply: 1000, - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(1000), ..Default::default() @@ -5306,38 +4503,25 @@ mod tests { fn runes_can_be_etched_and_claimed_in_the_same_transaction() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), ..Default::default() }), - edicts: vec![Edict { - id: 0, - amount: 2000, - output: 0, - }], ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + edicts: vec![Edict { + id: 0, + amount: 2000, + output: 0, + }], + ..Default::default() + }, + 1, + ); context.assert_runes( [( @@ -5349,7 +4533,7 @@ mod tests { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, supply: 1000, ..Default::default() }, @@ -5362,41 +4546,28 @@ mod tests { fn limit_over_max_is_clamped() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(MAX_LIMIT + 1), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(MAX_LIMIT + 1), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mint: Some(MintEntry { limit: Some(MAX_LIMIT), deadline: None, @@ -5408,7 +4579,7 @@ mod tests { [], ); - let txid = context.rpc_server.broadcast_tx(TransactionTemplate { + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { @@ -5431,9 +4602,9 @@ mod tests { [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), - timestamp: 2, + timestamp: id.height, mints: 1, supply: MAX_LIMIT, mint: Some(MintEntry { @@ -5444,7 +4615,13 @@ mod tests { ..Default::default() }, )], - [(OutPoint { txid, vout: 0 }, vec![(id, MAX_LIMIT)])], + [( + OutPoint { + txid: txid1, + vout: 0, + }, + vec![(id, MAX_LIMIT)], + )], ); } @@ -5452,46 +4629,33 @@ mod tests { fn omitted_limit_defaults_to_max_limit() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - term: Some(1), - ..Default::default() - }), + let (txid, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + term: Some(1), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, + etching: txid, rune: Rune(RUNE), mint: Some(MintEntry { limit: None, - end: Some(3), + end: Some(id.height + 1), ..Default::default() }), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], @@ -5503,65 +4667,52 @@ mod tests { fn transactions_cannot_claim_more_than_limit() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), ..Default::default() }), - edicts: vec![Edict { - id: 0, - amount: 2000, - output: 0, - }], ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + edicts: vec![Edict { + id: 0, + amount: 2000, + output: 0, + }], + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, supply: 1000, ..Default::default() }, )], [( OutPoint { - txid: etching, + txid: txid0, vout: 0, }, vec![(id, 1000)], )], ); - let edict = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![Edict { @@ -5579,22 +4730,17 @@ mod tests { context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, supply: 2000, mints: 1, ..Default::default() @@ -5603,14 +4749,14 @@ mod tests { [ ( OutPoint { - txid: etching, + txid: txid0, vout: 0, }, vec![(id, 1000)], ), ( OutPoint { - txid: edict, + txid: txid1, vout: 0, }, vec![(id, 1000)], @@ -5623,53 +4769,40 @@ mod tests { fn multiple_edicts_in_one_transaction_may_claim_open_etching() { let context = Context::builder().arg("--index-runes").build(); - context.mine_blocks(1); - - let etching = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: Some(Mint { - limit: Some(1000), - ..Default::default() - }), + let (txid0, id) = context.etch( + Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), ..Default::default() }), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - context.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + ); context.assert_runes( [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, ..Default::default() }, )], [], ); - let edict = context.rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], + let txid1 = context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(2, 0, 0, Witness::new())], op_return: Some( Runestone { edicts: vec![ @@ -5699,22 +4832,17 @@ mod tests { context.mine_blocks(1); - let id = RuneId { - height: 2, - index: 1, - }; - context.assert_runes( [( id, RuneEntry { - etching, + etching: txid0, rune: Rune(RUNE), mint: Some(MintEntry { limit: Some(1000), ..Default::default() }), - timestamp: 2, + timestamp: id.height, supply: 1000, mints: 1, ..Default::default() @@ -5722,11 +4850,181 @@ mod tests { )], [( OutPoint { - txid: edict, + txid: txid1, vout: 0, }, vec![(id, 1000)], )], ); } + + #[test] + fn commits_are_not_valid_in_non_taproot_witnesses() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = usize::try_from(context.index.block_count().unwrap()).unwrap(); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: false, + ..Default::default() + }); + + context.mine_blocks(RUNE_COMMIT_INTERVAL.into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn immature_commits_are_not_valid() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = usize::try_from(context.index.block_count().unwrap()).unwrap(); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..Default::default() + }); + + context.mine_blocks((RUNE_COMMIT_INTERVAL - 1).into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } + + #[test] + fn etchings_are_not_valid_without_commitment() { + let context = Context::builder().arg("--index-runes").build(); + + let block_count = usize::try_from(context.index.block_count().unwrap()).unwrap(); + + context.mine_blocks(1); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Witness::new())], + p2tr: true, + ..Default::default() + }); + + context.mine_blocks((RUNE_COMMIT_INTERVAL).into()); + + let mut witness = Witness::new(); + + let runestone = Runestone { + etching: Some(Etching { + rune: Some(Rune(RUNE)), + mint: Some(Mint { + limit: Some(1000), + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }; + + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>([].as_slice().try_into().unwrap()) + .into_script(); + + witness.push(tapscript); + + witness.push([]); + + context.rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs: 1, + ..Default::default() + }); + + context.mine_blocks(1); + + context.assert_runes([], []); + } } diff --git a/src/runes/rune.rs b/src/runes/rune.rs index 04bbc3e00e..8688ec530a 100644 --- a/src/runes/rune.rs +++ b/src/runes/rune.rs @@ -69,9 +69,21 @@ impl Rune { self.0 >= RESERVED } - pub(crate) fn reserved(n: u128) -> Self { + pub fn reserved(n: u128) -> Self { Rune(RESERVED.checked_add(n).unwrap()) } + + pub(crate) fn commitment(self) -> Vec { + let bytes = self.0.to_le_bytes(); + + let mut end = bytes.len(); + + while end > 0 && bytes[end - 1] == 0 { + end -= 1; + } + + bytes[..end].into() + } } impl Serialize for Rune { @@ -358,4 +370,20 @@ mod tests { } } } + + #[test] + fn commitment() { + #[track_caller] + fn case(rune: u128, bytes: &[u8]) { + assert_eq!(Rune(rune).commitment(), bytes); + } + + case(0, &[]); + case(1, &[1]); + case(255, &[255]); + case(256, &[0, 1]); + case(65535, &[255, 255]); + case(65536, &[0, 0, 1]); + case(u128::MAX, &[255; 16]); + } } diff --git a/src/runes/spaced_rune.rs b/src/runes/spaced_rune.rs index 1251ecb491..c71d671a91 100644 --- a/src/runes/spaced_rune.rs +++ b/src/runes/spaced_rune.rs @@ -1,6 +1,6 @@ use super::*; -#[derive(Copy, Clone, Debug, PartialEq, Ord, PartialOrd, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Default)] pub struct SpacedRune { pub rune: Rune, pub spacers: u32, diff --git a/src/subcommand/balances.rs b/src/subcommand/balances.rs index 8643c926ac..3418fc0290 100644 --- a/src/subcommand/balances.rs +++ b/src/subcommand/balances.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Output { - pub runes: BTreeMap>, + pub runes: BTreeMap>, } pub(crate) fn run(settings: Settings) -> SubcommandResult { diff --git a/src/subcommand/env.rs b/src/subcommand/env.rs index a8931d6c5a..7b97183fd0 100644 --- a/src/subcommand/env.rs +++ b/src/subcommand/env.rs @@ -160,13 +160,22 @@ rpcport={bitcoind_port} }, )?; + let datadir = if relative + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') + { + relative + } else { + format!("'{relative}'") + }; + eprintln!( "{} {server_url} {} -bitcoin-cli -datadir='{relative}' getblockchaininfo +bitcoin-cli -datadir={datadir} getblockchaininfo {} -{} --datadir '{relative}' wallet balance", +{} --datadir '{datadir}' wallet balance", "`ord` server URL:".blue().bold(), "Example `bitcoin-cli` command:".blue().bold(), "Example `ord` command:".blue().bold(), diff --git a/src/subcommand/find.rs b/src/subcommand/find.rs index 602710f656..7715cfefd0 100644 --- a/src/subcommand/find.rs +++ b/src/subcommand/find.rs @@ -32,7 +32,10 @@ impl Find { match self.end { Some(end) => match index.find_range(self.sat, end)? { - Some(result) => Ok(Some(Box::new(result))), + Some(mut results) => { + results.sort_by_key(|find_range_output| find_range_output.start); + Ok(Some(Box::new(results))) + } None => Err(anyhow!("range has not been mined as of index height")), }, None => match index.find(self.sat)? { diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 1ac3f2b6ac..3eb2e527aa 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -260,6 +260,7 @@ impl Server { .route("/static/*path", get(Self::static_asset)) .route("/status", get(Self::status)) .route("/tx/:txid", get(Self::transaction)) + .route("/update", get(Self::update)) .fallback(Self::fallback) .layer(Extension(index)) .layer(Extension(server_config.clone())) @@ -475,7 +476,7 @@ impl Server { index.block_height()?.ok_or_not_found(|| "genesis block") } - async fn clock(Extension(index): Extension>) -> ServerResult { + async fn clock(Extension(index): Extension>) -> ServerResult { task::block_in_place(|| { Ok( ( @@ -491,26 +492,28 @@ impl Server { } async fn fallback(Extension(index): Extension>, uri: Uri) -> ServerResult { - let path = urlencoding::decode(uri.path().trim_matches('/')) - .map_err(|err| ServerError::BadRequest(err.to_string()))?; - - let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) { - "inscription" - } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) { - "rune" - } else if re::OUTPOINT.is_match(&path) { - "output" - } else if re::HASH.is_match(&path) { - if index.block_header(path.parse().unwrap())?.is_some() { - "block" + task::block_in_place(|| { + let path = urlencoding::decode(uri.path().trim_matches('/')) + .map_err(|err| ServerError::BadRequest(err.to_string()))?; + + let prefix = if re::INSCRIPTION_ID.is_match(&path) || re::INSCRIPTION_NUMBER.is_match(&path) { + "inscription" + } else if re::RUNE_ID.is_match(&path) || re::SPACED_RUNE.is_match(&path) { + "rune" + } else if re::OUTPOINT.is_match(&path) { + "output" + } else if re::HASH.is_match(&path) { + if index.block_header(path.parse().unwrap())?.is_some() { + "block" + } else { + "tx" + } } else { - "tx" - } - } else { - return Ok(StatusCode::NOT_FOUND.into_response()); - }; + return Ok(StatusCode::NOT_FOUND.into_response()); + }; - Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response()) + Ok(Redirect::to(&format!("/{prefix}/{path}")).into_response()) + }) } async fn sat( @@ -518,7 +521,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(sat)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let inscriptions = index.get_inscription_ids_by_sat(sat)?; let satpoint = index.rare_sat_satpoint(sat)?.or_else(|| { @@ -570,7 +573,7 @@ impl Server { Extension(index): Extension>, Path(outpoint): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let sat_ranges = index.list(outpoint)?; @@ -662,7 +665,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(rune_query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if !index.has_rune_index() { return Err(ServerError::NotFound( @@ -695,7 +698,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { Ok(if accept_json { Json(api::Runes { @@ -716,11 +719,25 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let balances = index.get_rune_balance_map()?; Ok(if accept_json { - Json(balances).into_response() + Json( + balances + .into_iter() + .map(|(rune, balances)| { + ( + rune, + balances + .into_iter() + .map(|(outpoint, pile)| (outpoint, pile.amount)) + .collect(), + ) + }) + .collect::>>(), + ) + .into_response() } else { RuneBalancesHtml { balances } .page(server_config) @@ -747,7 +764,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let blocks = index.blocks(100)?; let mut featured_blocks = BTreeMap::new(); @@ -777,7 +794,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (block, height) = match query { query::Block::Height(height) => { @@ -830,7 +847,7 @@ impl Server { Extension(index): Extension>, Path(txid): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let transaction = index .get_transaction(txid)? @@ -861,6 +878,20 @@ impl Server { }) } + async fn update( + Extension(settings): Extension>, + Extension(index): Extension>, + ) -> ServerResult { + task::block_in_place(|| { + if settings.integration_test() { + index.update()?; + Ok(index.block_count()?.to_string().into_response()) + } else { + Ok(StatusCode::NOT_FOUND.into_response()) + } + }) + } + async fn metadata( Extension(index): Extension>, Path(inscription_id): Path, @@ -879,7 +910,7 @@ impl Server { async fn inscription_recursive( Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let inscription = index .get_inscription_by_id(inscription_id)? @@ -940,7 +971,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { Ok(if accept_json { Json(index.status()?).into_response() @@ -998,7 +1029,7 @@ impl Server { }) } - async fn favicon() -> ServerResult { + async fn favicon() -> ServerResult { Ok( Self::static_asset(Path("/favicon.png".to_string())) .await @@ -1009,7 +1040,7 @@ impl Server { async fn feed( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let mut builder = rss::ChannelBuilder::default(); @@ -1050,7 +1081,7 @@ impl Server { }) } - async fn static_asset(Path(path): Path) -> ServerResult { + async fn static_asset(Path(path): Path) -> ServerResult { let content = StaticAssets::get(if let Some(stripped) = path.strip_prefix('/') { stripped } else { @@ -1270,7 +1301,7 @@ impl Server { Extension(server_config): Extension>, Path(inscription_id): Path, accept_encoding: AcceptEncoding, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if settings.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); @@ -1377,7 +1408,7 @@ impl Server { Extension(server_config): Extension>, Path(inscription_id): Path, accept_encoding: AcceptEncoding, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if settings.is_hidden(inscription_id) { return Ok(PreviewUnknownHtml.into_response()); @@ -1462,7 +1493,7 @@ impl Server { Extension(index): Extension>, Path(DeserializeFromStr(query)): Path>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { if let query::Inscription::Sat(_) = query { if !index.has_sat_index() { @@ -1535,7 +1566,7 @@ impl Server { async fn collections( Extension(server_config): Extension>, Extension(index): Extension>, - ) -> ServerResult { + ) -> ServerResult { Self::collections_paginated(Extension(server_config), Extension(index), Path(0)).await } @@ -1543,7 +1574,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path(page_index): Path, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (collections, more_collections) = index.get_collections_paginated(100, page_index)?; @@ -1567,7 +1598,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { Self::children_paginated( Extension(server_config), Extension(index), @@ -1580,7 +1611,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let entry = index .get_inscription_entry(parent)? @@ -1612,14 +1643,14 @@ impl Server { async fn children_recursive( Extension(index): Extension>, Path(inscription_id): Path, - ) -> ServerResult { + ) -> ServerResult { Self::children_recursive_paginated(Extension(index), Path((inscription_id, 0))).await } async fn children_recursive_paginated( Extension(index): Extension>, Path((parent, page)): Path<(InscriptionId, usize)>, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let parent_sequence_number = index .get_inscription_entry(parent)? @@ -1637,7 +1668,7 @@ impl Server { Extension(server_config): Extension>, Extension(index): Extension>, accept_json: AcceptJson, - ) -> ServerResult { + ) -> ServerResult { Self::inscriptions_paginated( Extension(server_config), Extension(index), @@ -1652,7 +1683,7 @@ impl Server { Extension(index): Extension>, Path(page_index): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let (inscriptions, more) = index.get_inscriptions_paginated(100, page_index)?; @@ -1684,7 +1715,7 @@ impl Server { Extension(index): Extension>, Path(block_height): Path, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { Self::inscriptions_in_block_paginated( Extension(server_config), Extension(index), @@ -1699,7 +1730,7 @@ impl Server { Extension(index): Extension>, Path((block_height, page_index)): Path<(u32, u32)>, AcceptJson(accept_json): AcceptJson, - ) -> ServerResult { + ) -> ServerResult { task::block_in_place(|| { let page_size = 100; @@ -2051,6 +2082,63 @@ mod tests { Builder::default().build() } + #[track_caller] + pub(crate) fn etch( + &self, + runestone: Runestone, + outputs: usize, + witness: Option, + ) -> (Txid, RuneId) { + let block_count = usize::try_from(self.index.block_count().unwrap()).unwrap(); + + self.mine_blocks(1); + + self.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count, 0, 0, Default::default())], + p2tr: true, + ..Default::default() + }); + + self.mine_blocks(RUNE_COMMIT_INTERVAL.into()); + + let witness = witness.unwrap_or_else(|| { + let tapscript = script::Builder::new() + .push_slice::<&PushBytes>( + runestone + .etching + .unwrap() + .rune + .unwrap() + .commitment() + .as_slice() + .try_into() + .unwrap(), + ) + .into_script(); + let mut witness = Witness::default(); + witness.push(tapscript); + witness.push([]); + witness + }); + + let txid = self.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { + inputs: &[(block_count + 1, 1, 0, witness)], + op_return: Some(runestone.encipher()), + outputs, + ..Default::default() + }); + + self.mine_blocks(1); + + ( + txid, + RuneId { + height: self.index.block_count().unwrap() - 1, + index: 1, + }, + ) + } + #[track_caller] fn get(&self, path: impl AsRef) -> reqwest::blocking::Response { if let Err(error) = self.index.update() { @@ -2134,6 +2222,7 @@ mod tests { assert_eq!(response.headers().get(header::LOCATION).unwrap(), location); } + #[track_caller] fn mine_blocks(&self, n: u64) -> Vec { let blocks = self.bitcoin_rpc_server.mine_blocks(n); self.index.update().unwrap(); @@ -2448,30 +2537,27 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - ..Default::default() - }), + server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + }), + ..Default::default() + }, + 1, + None, + ); server.mine_blocks(1); - server.assert_redirect("/search/2:1", "/rune/AAAAAAAAAAAAA"); - server.assert_redirect("/search?query=2:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search/9:1", "/rune/AAAAAAAAAAAAA"); + server.assert_redirect("/search?query=9:1", "/rune/AAAAAAAAAAAAA"); server.assert_response_regex( "/search/100000000000000000000:200000000000000000", @@ -2529,32 +2615,29 @@ mod tests { let rune = Rune(RUNE); - server.assert_response_regex("/rune/2:1", StatusCode::NOT_FOUND, ".*"); - - server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - ..Default::default() - }), + server.assert_response_regex("/rune/9:1", StatusCode::NOT_FOUND, ".*"); + + server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); + }), + ..Default::default() + }, + 1, + None, + ); server.mine_blocks(1); server.assert_response_regex( - "/rune/2:1", + "/rune/9:1", StatusCode::OK, ".*Rune AAAAAAAAAAAAA.*", ); @@ -2575,34 +2658,25 @@ mod tests { ".*Runes.*

Runes

\n
    \n
.*", ); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), + symbol: Some('%'), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + Default::default(), + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, @@ -2610,7 +2684,8 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, + symbol: Some('%'), ..Default::default() } )] @@ -2645,33 +2720,31 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - symbol: Some('%'), - ..Default::default() - }), + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + symbol: Some('%'), + ..Default::default() + }), + ..Default::default() + }, + 1, + Some( + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + rune: Some(rune.commitment()), ..Default::default() } - .encipher(), + .to_witness(), ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ); assert_eq!( server.index.runes().unwrap(), @@ -2682,7 +2755,7 @@ mod tests { rune, supply: u128::MAX, symbol: Some('%'), - timestamp: 2, + timestamp: id.height, ..Default::default() } )] @@ -2704,11 +2777,11 @@ mod tests {
number
0
timestamp
-
+
id
-
2:1
+
9:1
etching block height
-
2
+
9
etching transaction index
1
mint
@@ -2756,36 +2829,34 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(rune), - symbol: Some('%'), - spacers: 1, - ..Default::default() - }), + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(rune), + symbol: Some('%'), + spacers: 1, + ..Default::default() + }), + ..Default::default() + }, + 1, + Some( + Inscription { + content_type: Some("text/plain".into()), + body: Some("hello".into()), + rune: Some(rune.commitment()), ..Default::default() } - .encipher(), + .to_witness(), ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, @@ -2794,7 +2865,7 @@ mod tests { rune, supply: u128::MAX, symbol: Some('%'), - timestamp: 2, + timestamp: id.height, spacers: 1, ..Default::default() } @@ -2858,34 +2929,24 @@ mod tests { ".*Runes.*

Runes

\n
    \n
.*", ); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - rune: Some(Rune(RUNE)), - ..Default::default() - }), + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + rune: Some(Rune(RUNE)), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + None, + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, @@ -2893,13 +2954,13 @@ mod tests { etching: txid, rune: Rune(RUNE), supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() } )] ); - assert_eq!( + pretty_assert_eq!( server.index.get_rune_balances().unwrap(), [(OutPoint { txid, vout: 0 }, vec![(id, u128::MAX)])] ); @@ -2927,35 +2988,25 @@ mod tests { server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*"); - let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Default::default())], - op_return: Some( - Runestone { - edicts: vec![Edict { - id: 0, - amount: u128::MAX, - output: 0, - }], - etching: Some(Etching { - divisibility: 1, - rune: Some(rune), - ..Default::default() - }), + let (txid, id) = server.etch( + Runestone { + edicts: vec![Edict { + id: 0, + amount: u128::MAX, + output: 0, + }], + etching: Some(Etching { + divisibility: 1, + rune: Some(rune), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - server.mine_blocks(1); - - let id = RuneId { - height: 2, - index: 1, - }; + }), + ..Default::default() + }, + 1, + None, + ); - assert_eq!( + pretty_assert_eq!( server.index.runes().unwrap(), [( id, @@ -2964,7 +3015,7 @@ mod tests { etching: txid, rune, supply: u128::MAX, - timestamp: 2, + timestamp: id.height, ..Default::default() } )] @@ -2999,12 +3050,14 @@ mod tests { ), ); - assert_eq!( + let address = default_address(Chain::Regtest); + + pretty_assert_eq!( server.get_json::(format!("/output/{output}")), api::Output { value: 5000000000, - script_pubkey: String::new(), - address: None, + script_pubkey: address.script_pubkey().to_asm_string(), + address: Some(uncheck(&address)), transaction: txid.to_string(), sat_ranges: None, indexed: true, @@ -3496,7 +3549,7 @@ mod tests { } server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, inscription("text/plain", "{}").to_witness())], + inputs: &[(102, 0, 0, inscription("text/plain", "{}").to_witness())], ..Default::default() }); @@ -3668,12 +3721,12 @@ mod tests {

1 Output

.*" @@ -4994,7 +5047,7 @@ next inputs.insert( 0, ( - 101, + 102, 0, 0, Inscription { @@ -5153,6 +5206,7 @@ next
id
{id}
+ .*
value
.*
@@ -5364,6 +5418,7 @@ next
id
{id}
+ .*
value
.*
@@ -5424,6 +5479,7 @@ next
id
{id}
+ .*
value
.*
@@ -5509,6 +5565,7 @@ next
id
{id}
+ .*
value
5000000000
.* @@ -5999,4 +6056,10 @@ next PreviewUnknownHtml.to_string(), ); } + + #[test] + fn update_endpoint_is_not_available_when_not_in_integration_test_mode() { + let server = TestServer::builder().build(); + server.assert_response("/update", StatusCode::NOT_FOUND, ""); + } } diff --git a/src/subcommand/server/error.rs b/src/subcommand/server/error.rs index 1ad0fcca6c..61efbc640a 100644 --- a/src/subcommand/server/error.rs +++ b/src/subcommand/server/error.rs @@ -11,7 +11,7 @@ pub(super) enum ServerError { NotFound(String), } -pub(crate) type ServerResult = Result; +pub(super) type ServerResult = Result; impl IntoResponse for ServerError { fn into_response(self) -> Response { diff --git a/src/subcommand/wallet.rs b/src/subcommand/wallet.rs index 0caee88a7e..cbb5484267 100644 --- a/src/subcommand/wallet.rs +++ b/src/subcommand/wallet.rs @@ -12,7 +12,6 @@ pub mod balance; pub mod cardinals; pub mod create; pub mod dump; -pub mod etch; pub mod inscribe; pub mod inscriptions; pub mod mint; @@ -47,8 +46,6 @@ pub(crate) enum Subcommand { Create(create::Create), #[command(about = "Dump wallet descriptors")] Dump, - #[command(about = "Create rune")] - Etch(etch::Etch), #[command(about = "Create inscription")] Inscribe(inscribe::Inscribe), #[command(about = "List wallet inscriptions")] @@ -96,7 +93,6 @@ impl WalletCommand { match self.subcommand { Subcommand::Balance => balance::run(wallet), Subcommand::Dump => dump::run(wallet), - Subcommand::Etch(etch) => etch.run(wallet), Subcommand::Inscribe(inscribe) => inscribe.run(wallet), Subcommand::Inscriptions => inscriptions::run(wallet), Subcommand::Mint(mint) => mint.run(wallet), diff --git a/src/subcommand/wallet/balance.rs b/src/subcommand/wallet/balance.rs index 475559aab1..90ca503eff 100644 --- a/src/subcommand/wallet/balance.rs +++ b/src/subcommand/wallet/balance.rs @@ -28,16 +28,27 @@ pub(crate) fn run(wallet: Wallet) -> SubcommandResult { for (output, txout) in unspent_outputs { let rune_balances = wallet.get_runes_balances_for_output(output)?; - if inscription_outputs.contains(output) { + let is_ordinal = inscription_outputs.contains(output); + let is_runic = !rune_balances.is_empty(); + + if is_ordinal { ordinal += txout.value; - } else if !rune_balances.is_empty() { + } + + if is_runic { for (spaced_rune, pile) in rune_balances { *runes.entry(spaced_rune.rune).or_default() += pile.amount; } runic += txout.value; - } else { + } + + if !is_ordinal && !is_runic { cardinal += txout.value; } + + if is_ordinal && is_runic { + eprintln!("warning: output {output} contains both inscriptions and runes"); + } } Ok(Some(Box::new(Output { diff --git a/src/subcommand/wallet/etch.rs b/src/subcommand/wallet/etch.rs deleted file mode 100644 index cb2c8cd088..0000000000 --- a/src/subcommand/wallet/etch.rs +++ /dev/null @@ -1,126 +0,0 @@ -use super::*; - -#[derive(Debug, Parser)] -pub(crate) struct Etch { - #[clap(long, help = "Set divisibility to .")] - divisibility: u8, - #[clap(long, help = "Etch with fee rate of sats/vB.")] - fee_rate: FeeRate, - #[clap(long, help = "Etch rune . May contain `.` or `•`as spacers.")] - rune: SpacedRune, - #[clap(long, help = "Set supply to .")] - supply: Decimal, - #[clap(long, help = "Set currency symbol to .")] - symbol: char, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Output { - pub rune: SpacedRune, - pub transaction: Txid, -} - -impl Etch { - pub(crate) fn run(self, wallet: Wallet) -> SubcommandResult { - ensure!( - wallet.has_rune_index(), - "`ord wallet etch` requires index created with `--index-runes` flag", - ); - - let SpacedRune { rune, spacers } = self.rune; - - let bitcoin_client = wallet.bitcoin_client(); - - let count = bitcoin_client.get_block_count()?; - - ensure!( - wallet.get_rune(rune)?.is_none(), - "rune `{}` has already been etched", - rune, - ); - - let minimum_at_height = - Rune::minimum_at_height(wallet.chain(), Height(u32::try_from(count).unwrap() + 1)); - - ensure!( - rune >= minimum_at_height, - "rune is less than minimum for next block: {} < {minimum_at_height}", - rune, - ); - - ensure!(!rune.is_reserved(), "rune `{}` is reserved", rune); - - ensure!( - self.divisibility <= crate::runes::MAX_DIVISIBILITY, - " must be equal to or less than 38" - ); - - let destination = wallet.get_change_address()?; - - let runestone = Runestone { - etching: Some(Etching { - divisibility: self.divisibility, - mint: None, - rune: Some(rune), - spacers, - symbol: Some(self.symbol), - }), - edicts: vec![Edict { - amount: self.supply.to_amount(self.divisibility)?, - id: 0, - output: 1, - }], - default_output: None, - burn: false, - claim: None, - }; - - let script_pubkey = runestone.encipher(); - - ensure!( - script_pubkey.len() <= 82, - "runestone greater than maximum OP_RETURN size: {} > 82", - script_pubkey.len() - ); - - let unfunded_transaction = Transaction { - version: 2, - lock_time: LockTime::ZERO, - input: Vec::new(), - output: vec![ - TxOut { - script_pubkey, - value: 0, - }, - TxOut { - script_pubkey: destination.script_pubkey(), - value: TARGET_POSTAGE.to_sat(), - }, - ], - }; - - let inscriptions = wallet - .inscriptions() - .keys() - .map(|satpoint| satpoint.outpoint) - .collect::>(); - - if !bitcoin_client.lock_unspent(&inscriptions)? { - bail!("failed to lock UTXOs"); - } - - let unsigned_transaction = - fund_raw_transaction(bitcoin_client, self.fee_rate, &unfunded_transaction)?; - - let signed_transaction = bitcoin_client - .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? - .hex; - - let transaction = bitcoin_client.send_raw_transaction(&signed_transaction)?; - - Ok(Some(Box::new(Output { - rune: self.rune, - transaction, - }))) - } -} diff --git a/src/subcommand/wallet/inscribe.rs b/src/subcommand/wallet/inscribe.rs index c8066de812..d81efae624 100644 --- a/src/subcommand/wallet/inscribe.rs +++ b/src/subcommand/wallet/inscribe.rs @@ -82,13 +82,14 @@ impl Inscribe { let chain = wallet.chain(); - let postages; let destinations; let inscriptions; let mode; let parent_info; + let postages; let reinscribe; let reveal_satpoints; + let etch; let satpoint = match (self.file, self.batch) { (Some(file), None) => { @@ -112,6 +113,7 @@ impl Inscribe { self.parent.into_iter().collect(), file, None, + None, )?]; mode = Mode::SeparateOutputs; @@ -125,6 +127,8 @@ impl Inscribe { None => wallet.get_change_address()?, }]; + etch = None; + if let Some(sat) = self.sat { Some(wallet.find_sat_in_outputs(sat)?) } else { @@ -153,6 +157,8 @@ impl Inscribe { reinscribe = batchfile.reinscribe; + etch = batchfile.etch; + if let Some(sat) = batchfile.sat { Some(wallet.find_sat_in_outputs(sat)?) } else { @@ -162,10 +168,44 @@ impl Inscribe { _ => unreachable!(), }; + if let Some(etch) = etch { + let rune = etch.rune.rune; + + ensure!(!rune.is_reserved(), "rune `{rune}` is reserved"); + + ensure!( + etch.divisibility <= crate::runes::MAX_DIVISIBILITY, + " must be less than or equal 38" + ); + + ensure!( + wallet.has_rune_index(), + "etching runes requires index created with `--index-runes`", + ); + + ensure!( + wallet.get_rune(rune)?.is_none(), + "rune `{rune}` has already been etched", + ); + + let bitcoin_client = wallet.bitcoin_client(); + + let count = bitcoin_client.get_block_count()?; + + let minimum = + Rune::minimum_at_height(wallet.chain(), Height(u32::try_from(count).unwrap() + 1)); + + ensure!( + rune >= minimum, + "rune is less than minimum for next block: {rune} < {minimum}", + ); + } + Batch { commit_fee_rate: self.commit_fee_rate.unwrap_or(self.fee_rate), destinations, dry_run: self.dry_run, + etch, inscriptions, mode, no_backup: self.no_backup, @@ -209,7 +249,7 @@ impl Inscribe { mod tests { use { super::*, - crate::wallet::inscribe::{BatchEntry, ParentInfo}, + crate::wallet::inscribe::{BatchEntry, BatchTransactions, ParentInfo}, bitcoin::policy::MAX_STANDARD_TX_WEIGHT, serde_yaml::{Mapping, Value}, tempfile::TempDir, @@ -221,9 +261,13 @@ mod tests { let inscription = inscription("text/plain", "ord"); let commit_address = change(0); let reveal_address = recipient(); - let change = [commit_address, change(1)]; + let reveal_change = [commit_address, change(1)]; - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint: Some(satpoint(1, 0)), parent_info: None, inscriptions: vec![inscription], @@ -236,13 +280,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), - change, + reveal_change, + change(2), ) .unwrap(); @@ -262,9 +307,13 @@ mod tests { let inscription = inscription("text/plain", "ord"); let commit_address = change(0); let reveal_address = recipient(); - let change = [commit_address, change(1)]; + let reveal_change = [commit_address, change(1)]; - let (commit_tx, reveal_tx, _, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint: Some(satpoint(1, 0)), parent_info: None, inscriptions: vec![inscription], @@ -277,13 +326,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), - change, + reveal_change, + change(2), ) .unwrap(); @@ -321,13 +371,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( inscriptions, Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .unwrap_err() .to_string(); @@ -372,13 +423,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( inscriptions, Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .is_ok()) } @@ -404,7 +456,11 @@ mod tests { let reveal_address = recipient(); let fee_rate = 3.3; - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint, parent_info: None, inscriptions: vec![inscription], @@ -417,13 +473,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .unwrap(); @@ -487,7 +544,11 @@ mod tests { let reveal_address = recipient(); let fee_rate = 4.0; - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint: None, parent_info: Some(parent_info.clone()), inscriptions: vec![child_inscription], @@ -500,13 +561,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(1), ) .unwrap(); @@ -569,7 +631,11 @@ mod tests { let commit_fee_rate = 3.3; let fee_rate = 1.0; - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint, parent_info: None, inscriptions: vec![inscription], @@ -582,13 +648,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .unwrap(); @@ -640,13 +707,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .unwrap_err() .to_string(); @@ -667,7 +735,7 @@ mod tests { let commit_address = change(0); let reveal_address = recipient(); - let (_commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { reveal_tx, .. } = Batch { satpoint, parent_info: None, inscriptions: vec![inscription], @@ -680,13 +748,14 @@ mod tests { mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( BTreeMap::new(), Chain::Mainnet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(1)], + change(2), ) .unwrap(); @@ -840,7 +909,11 @@ inscriptions: let fee_rate = 4.0.try_into().unwrap(); - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint: None, parent_info: Some(parent_info.clone()), inscriptions, @@ -853,13 +926,14 @@ inscriptions: mode, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(2), ) .unwrap(); @@ -962,7 +1036,11 @@ inscriptions: let fee_rate = 1.0.try_into().unwrap(); - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { reveal_satpoints: reveal_satpoints.clone(), parent_info: Some(parent_info.clone()), inscriptions, @@ -977,7 +1055,7 @@ inscriptions: mode, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, reveal_satpoints @@ -987,6 +1065,7 @@ inscriptions: BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ) .unwrap(); @@ -1076,13 +1155,14 @@ inscriptions: mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ) .unwrap_err() .to_string(); @@ -1152,13 +1232,14 @@ inscriptions: mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ); } @@ -1190,13 +1271,14 @@ inscriptions: mode: Mode::SharedOutput, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ) .unwrap_err() .to_string(); @@ -1230,7 +1312,7 @@ inscriptions: let fee_rate = 4.0.try_into().unwrap(); - let (_commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { reveal_tx, .. } = Batch { satpoint: None, parent_info: None, inscriptions, @@ -1243,13 +1325,14 @@ inscriptions: mode, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ) .unwrap(); @@ -1310,7 +1393,11 @@ inscriptions: let fee_rate = 4.0.try_into().unwrap(); - let (commit_tx, reveal_tx, _private_key, _) = Batch { + let BatchTransactions { + commit_tx, + reveal_tx, + .. + } = Batch { satpoint: None, parent_info: Some(parent_info.clone()), inscriptions, @@ -1323,13 +1410,14 @@ inscriptions: mode, ..Default::default() } - .create_batch_inscription_transactions( + .create_batch_transactions( wallet_inscriptions, Chain::Signet, BTreeSet::new(), BTreeSet::new(), utxos.into_iter().collect(), [commit_address, change(2)], + change(3), ) .unwrap(); diff --git a/src/subcommand/wallet/send.rs b/src/subcommand/wallet/send.rs index 5ffe971b3e..953b22c813 100644 --- a/src/subcommand/wallet/send.rs +++ b/src/subcommand/wallet/send.rs @@ -107,23 +107,23 @@ impl Send { ) }; + let mut fee = 0; + for txin in unsigned_transaction.input.iter() { + let Some(txout) = unspent_outputs.get(&txin.previous_output) else { + panic!("input {} not found in utxos", txin.previous_output); + }; + fee += txout.value; + } + + for txout in unsigned_transaction.output.iter() { + fee = fee.checked_sub(txout.value).unwrap(); + } + Ok(Some(Box::new(Output { txid, psbt, outgoing: self.outgoing, - fee: unsigned_transaction - .input - .iter() - .map(|txin| unspent_outputs.get(&txin.previous_output).unwrap().value) - .sum::() - .checked_sub( - unsigned_transaction - .output - .iter() - .map(|txout| txout.value) - .sum::(), - ) - .unwrap(), + fee, }))) } diff --git a/src/templates/rune_balances.rs b/src/templates/rune_balances.rs index 2ab7f4803e..343537e39c 100644 --- a/src/templates/rune_balances.rs +++ b/src/templates/rune_balances.rs @@ -2,7 +2,7 @@ use super::*; #[derive(Boilerplate, Debug, PartialEq, Serialize, Deserialize)] pub struct RuneBalancesHtml { - pub balances: BTreeMap>, + pub balances: BTreeMap>, } impl PageContent for RuneBalancesHtml { @@ -19,7 +19,7 @@ mod tests { #[test] fn display_rune_balances() { - let balances: BTreeMap> = vec![ + let balances: BTreeMap> = vec![ ( Rune(RUNE), vec![( @@ -27,7 +27,11 @@ mod tests { txid: txid(1), vout: 1, }, - 1000, + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('$'), + }, )] .into_iter() .collect(), @@ -39,7 +43,11 @@ mod tests { txid: txid(2), vout: 2, }, - 12345678, + Pile { + amount: 12345678, + divisibility: 1, + symbol: Some('¢'), + }, )] .into_iter() .collect(), @@ -65,7 +73,7 @@ mod tests { 1{64}:1 - 1000 + 1000\u{00A0}\\$ @@ -80,7 +88,7 @@ mod tests { 2{64}:2 - 12345678 + 1234567\\.8\u{00A0}¢ diff --git a/src/test.rs b/src/test.rs index 98a96e80f4..1265e905a8 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,9 +1,9 @@ pub(crate) use { super::*, bitcoin::{ - blockdata::{opcodes, script, script::PushBytesBuf}, + blockdata::script::{PushBytes, PushBytesBuf}, constants::COIN_VALUE, - ScriptBuf, Witness, + WPubkeyHash, }, pretty_assertions::assert_eq as pretty_assert_eq, std::iter, @@ -146,3 +146,11 @@ pub(crate) fn envelope(payload: &[&[u8]]) -> Witness { Witness::from_slice(&[script.into_bytes(), Vec::new()]) } + +pub(crate) fn default_address(chain: Chain) -> Address { + Address::from_script( + &ScriptBuf::new_v0_p2wpkh(&WPubkeyHash::all_zeros()), + chain.network(), + ) + .unwrap() +} diff --git a/src/wallet.rs b/src/wallet.rs index d279b18d27..1791df2c2f 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -479,6 +479,10 @@ impl Wallet { self.settings.chain() } + pub(crate) fn integration_test(&self) -> bool { + self.settings.integration_test() + } + fn check_descriptors(wallet_name: &str, descriptors: Vec) -> Result> { let tr = descriptors .iter() diff --git a/src/wallet/inscribe.rs b/src/wallet/inscribe.rs index 27f107e2c5..b27fa3ee64 100644 --- a/src/wallet/inscribe.rs +++ b/src/wallet/inscribe.rs @@ -14,20 +14,46 @@ use { wallet::transaction_builder::Target, }; -pub use {batch::Batch, batch_entry::BatchEntry, batch_file::Batchfile, mode::Mode}; +pub use {batch::Batch, batch_entry::BatchEntry, batchfile::Batchfile, etch::Etch, mode::Mode}; pub mod batch; pub mod batch_entry; -pub mod batch_file; +pub mod batchfile; +mod etch; pub mod mode; -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] +#[derive(Debug)] +pub(crate) struct BatchTransactions { + pub(crate) rune: Option, + pub(crate) commit_tx: Transaction, + pub(crate) recovery_key_pair: TweakedKeyPair, + pub(crate) reveal_tx: Transaction, + pub(crate) total_fees: u64, +} + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct BatchMint { + pub deadline: Option, + pub limit: Decimal, + pub term: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct InscriptionInfo { + pub destination: Address, pub id: InscriptionId, pub location: SatPoint, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct RuneInfo { + pub destination: Option>, + pub location: Option, + pub rune: SpacedRune, +} + +#[derive(Debug, Serialize, Deserialize)] pub struct Output { pub commit: Txid, pub commit_psbt: Option, @@ -35,6 +61,7 @@ pub struct Output { pub parent: Option, pub reveal: Txid, pub reveal_psbt: Option, + pub rune: Option, pub total_fees: u64, } diff --git a/src/wallet/inscribe/batch.rs b/src/wallet/inscribe/batch.rs index 6221ac4d40..4032b65f5e 100644 --- a/src/wallet/inscribe/batch.rs +++ b/src/wallet/inscribe/batch.rs @@ -4,6 +4,7 @@ pub struct Batch { pub(crate) commit_fee_rate: FeeRate, pub(crate) destinations: Vec
, pub(crate) dry_run: bool, + pub(crate) etch: Option, pub(crate) inscriptions: Vec, pub(crate) mode: Mode, pub(crate) no_backup: bool, @@ -22,6 +23,7 @@ impl Default for Batch { commit_fee_rate: 1.0.try_into().unwrap(), destinations: Vec::new(), dry_run: false, + etch: None, inscriptions: Vec::new(), mode: Mode::SharedOutput, no_backup: false, @@ -44,17 +46,21 @@ impl Batch { utxos: &BTreeMap, wallet: &Wallet, ) -> SubcommandResult { - let commit_tx_change = [wallet.get_change_address()?, wallet.get_change_address()?]; - - let (commit_tx, reveal_tx, recovery_key_pair, total_fees) = self - .create_batch_inscription_transactions( - wallet.inscriptions().clone(), - wallet.chain(), - locked_utxos.clone(), - runic_utxos, - utxos.clone(), - commit_tx_change, - )?; + let BatchTransactions { + commit_tx, + reveal_tx, + recovery_key_pair, + total_fees, + rune, + } = self.create_batch_transactions( + wallet.inscriptions().clone(), + wallet.chain(), + locked_utxos.clone(), + runic_utxos, + utxos.clone(), + [wallet.get_change_address()?, wallet.get_change_address()?], + wallet.get_change_address()?, + )?; if self.dry_run { let commit_psbt = wallet @@ -77,6 +83,7 @@ impl Batch { Some(base64::engine::general_purpose::STANDARD.encode(reveal_psbt.serialize())), total_fees, self.inscriptions.clone(), + rune, )))); } @@ -119,6 +126,37 @@ impl Batch { .bitcoin_client() .send_raw_transaction(&signed_commit_tx)?; + if self.etch.is_some() { + eprintln!("Waiting for rune commitment to mature…"); + + loop { + let transaction = wallet + .bitcoin_client() + .get_transaction(&commit_tx.txid(), Some(true)) + .into_option()?; + + if let Some(transaction) = transaction { + if u32::try_from(transaction.info.confirmations).unwrap() < RUNE_COMMIT_INTERVAL { + continue; + } + } + + let tx_out = wallet + .bitcoin_client() + .get_tx_out(&commit_tx.txid(), 0, Some(true))?; + + if let Some(tx_out) = tx_out { + if tx_out.confirmations >= RUNE_COMMIT_INTERVAL { + break; + } + } + + if !wallet.integration_test() { + thread::sleep(Duration::from_secs(5)); + } + } + } + let reveal = match wallet .bitcoin_client() .send_raw_transaction(&signed_reveal_tx) @@ -138,6 +176,7 @@ impl Batch { None, total_fees, self.inscriptions.clone(), + rune, )))) } @@ -157,10 +196,11 @@ impl Batch { reveal_psbt: Option, total_fees: u64, inscriptions: Vec, + rune: Option, ) -> Output { let mut inscriptions_output = Vec::new(); - for index in 0..inscriptions.len() { - let index = u32::try_from(index).unwrap(); + for i in 0..inscriptions.len() { + let index = u32::try_from(i).unwrap(); let vout = match self.mode { Mode::SharedOutput | Mode::SameSat => { @@ -180,18 +220,24 @@ impl Batch { }; let offset = match self.mode { - Mode::SharedOutput => self.postages[0..usize::try_from(index).unwrap()] + Mode::SharedOutput => self.postages[0..i] .iter() .map(|amount| amount.to_sat()) .sum(), Mode::SeparateOutputs | Mode::SameSat | Mode::SatPoints => 0, }; + let destination = match self.mode { + Mode::SameSat | Mode::SharedOutput => &self.destinations[0], + Mode::SatPoints | Mode::SeparateOutputs => &self.destinations[i], + }; + inscriptions_output.push(InscriptionInfo { id: InscriptionId { txid: reveal, index, }, + destination: uncheck(destination), location: SatPoint { outpoint: OutPoint { txid: reveal, vout }, offset, @@ -202,23 +248,25 @@ impl Batch { Output { commit, commit_psbt, + inscriptions: inscriptions_output, + parent: self.parent_info.clone().map(|info| info.id), reveal, reveal_psbt, + rune, total_fees, - parent: self.parent_info.clone().map(|info| info.id), - inscriptions: inscriptions_output, } } - pub(crate) fn create_batch_inscription_transactions( + pub(crate) fn create_batch_transactions( &self, wallet_inscriptions: BTreeMap>, chain: Chain, locked_utxos: BTreeSet, runic_utxos: BTreeSet, mut utxos: BTreeMap, - change: [Address; 2], - ) -> Result<(Transaction, Transaction, TweakedKeyPair, u64)> { + commit_change: [Address; 2], + reveal_change: Address, + ) -> Result { if let Some(parent_info) = &self.parent_info { for inscription in &self.inscriptions { assert_eq!(inscription.parents(), vec![parent_info.id]); @@ -376,22 +424,98 @@ impl Batch { }); } + let rune; + let premine; + + if let Some(etch) = self.etch { + let mut edicts = Vec::new(); + + let vout; + let destination; + premine = etch.premine.to_amount(etch.divisibility)?; + + if premine > 0 { + let output = u32::try_from(reveal_outputs.len()).unwrap(); + destination = Some(reveal_change.clone()); + + reveal_outputs.push(TxOut { + script_pubkey: reveal_change.into(), + value: TARGET_POSTAGE.to_sat(), + }); + + edicts.push(Edict { + id: 0, + amount: premine, + output: output.into(), + }); + + vout = Some(output); + } else { + vout = None; + destination = None; + } + + let script_pubkey = Runestone { + burn: false, + claim: None, + default_output: None, + edicts, + etching: Some(Etching { + divisibility: etch.divisibility, + mint: etch + .mint + .map(|mint| -> Result { + Ok(runes::Mint { + deadline: mint.deadline, + term: mint.term, + limit: Some(mint.limit.to_amount(etch.divisibility)?), + }) + }) + .transpose()?, + rune: Some(etch.rune.rune), + spacers: etch.rune.spacers, + symbol: Some(etch.symbol), + }), + } + .encipher(); + + ensure!( + script_pubkey.len() <= 82, + "runestone greater than maximum OP_RETURN size: {} > 82", + script_pubkey.len() + ); + + reveal_outputs.push(TxOut { + script_pubkey, + value: 0, + }); + + rune = Some((destination, etch.rune, vout)); + } else { + premine = 0; + rune = None; + } + let commit_input = usize::from(self.parent_info.is_some()) + self.reveal_satpoints.len(); - let (_, reveal_fee) = Self::build_reveal_transaction( + let (_reveal_tx, reveal_fee) = Self::build_reveal_transaction( + commit_input, &control_block, self.reveal_fee_rate, - reveal_inputs.clone(), - commit_input, reveal_outputs.clone(), + reveal_inputs.clone(), &reveal_script, ); - let target = if self.mode == Mode::SatPoints { - Target::Value(reveal_fee) - } else { - Target::Value(reveal_fee + Amount::from_sat(total_postage)) - }; + let mut target_value = reveal_fee; + + if self.mode != Mode::SatPoints { + target_value += Amount::from_sat(total_postage); + } + + if premine > 0 { + target_value += TARGET_POSTAGE; + } let unsigned_commit_tx = TransactionBuilder::new( satpoint, @@ -400,9 +524,9 @@ impl Batch { locked_utxos.clone(), runic_utxos, commit_tx_address.clone(), - change, + commit_change, self.commit_fee_rate, - target, + Target::Value(target_value), ) .build_transaction()?; @@ -419,11 +543,11 @@ impl Batch { }; let (mut reveal_tx, _fee) = Self::build_reveal_transaction( + commit_input, &control_block, self.reveal_fee_rate, - reveal_inputs, - commit_input, reveal_outputs.clone(), + reveal_inputs, &reveal_script, ); @@ -431,7 +555,7 @@ impl Batch { ensure!( output.value >= output.script_pubkey.dust_value().to_sat(), "commit transaction output would be dust" - ) + ); } let mut prevouts = Vec::new(); @@ -508,7 +632,22 @@ impl Batch { let total_fees = Self::calculate_fee(&unsigned_commit_tx, &utxos) + Self::calculate_fee(&reveal_tx, &utxos); - Ok((unsigned_commit_tx, reveal_tx, recovery_key_pair, total_fees)) + let rune = rune.map(|(destination, rune, vout)| RuneInfo { + destination: destination.map(|destination| uncheck(&destination)), + location: vout.map(|vout| OutPoint { + txid: reveal_tx.txid(), + vout, + }), + rune, + }); + + Ok(BatchTransactions { + commit_tx: unsigned_commit_tx, + recovery_key_pair, + reveal_tx, + rune, + total_fees, + }) } fn backup_recovery_key(wallet: &Wallet, recovery_key_pair: TweakedKeyPair) -> Result { @@ -543,24 +682,24 @@ impl Batch { } fn build_reveal_transaction( + commit_input_index: usize, control_block: &ControlBlock, fee_rate: FeeRate, - reveal_inputs: Vec, - commit_input_index: usize, - outputs: Vec, + output: Vec, + input: Vec, script: &Script, ) -> (Transaction, Amount) { let reveal_tx = Transaction { - input: reveal_inputs - .iter() - .map(|outpoint| TxIn { - previous_output: *outpoint, + input: input + .into_iter() + .map(|previous_output| TxIn { + previous_output, script_sig: script::Builder::new().into_script(), witness: Witness::new(), sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }) .collect(), - output: outputs, + output, lock_time: LockTime::ZERO, version: 2, }; diff --git a/src/wallet/inscribe/batch_entry.rs b/src/wallet/inscribe/batch_entry.rs index de4c462d47..7e24b11d06 100644 --- a/src/wallet/inscribe/batch_entry.rs +++ b/src/wallet/inscribe/batch_entry.rs @@ -1,14 +1,14 @@ use super::*; -#[derive(Deserialize, Default, PartialEq, Debug, Clone)] +#[derive(Serialize, Deserialize, Default, PartialEq, Debug, Clone)] #[serde(deny_unknown_fields)] pub struct BatchEntry { - pub(crate) delegate: Option, - pub(crate) destination: Option>, - pub(crate) file: PathBuf, - pub(crate) metadata: Option, - pub(crate) metaprotocol: Option, - pub(crate) satpoint: Option, + pub delegate: Option, + pub destination: Option>, + pub file: PathBuf, + pub metadata: Option, + pub metaprotocol: Option, + pub satpoint: Option, } impl BatchEntry { diff --git a/src/wallet/inscribe/batch_file.rs b/src/wallet/inscribe/batchfile.rs similarity index 96% rename from src/wallet/inscribe/batch_file.rs rename to src/wallet/inscribe/batchfile.rs index 7cfbe4b841..7f2dc7d490 100644 --- a/src/wallet/inscribe/batch_file.rs +++ b/src/wallet/inscribe/batchfile.rs @@ -1,16 +1,17 @@ use super::*; -#[derive(Deserialize, PartialEq, Debug, Clone, Default)] +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)] #[serde(deny_unknown_fields)] pub struct Batchfile { - pub(crate) inscriptions: Vec, - pub(crate) mode: Mode, - pub(crate) parent: Option, - pub(crate) postage: Option, + pub inscriptions: Vec, + pub mode: Mode, + pub parent: Option, + pub postage: Option, #[serde(default)] - pub(crate) reinscribe: bool, - pub(crate) sat: Option, - pub(crate) satpoint: Option, + pub reinscribe: bool, + pub etch: Option, + pub sat: Option, + pub satpoint: Option, } impl Batchfile { @@ -137,6 +138,9 @@ impl Batchfile { self.parent.into_iter().collect(), &entry.file, Some(pointer), + self + .etch + .and_then(|etch| (i == 0).then_some(etch.rune.rune)), )?); let postage = if self.mode == Mode::SatPoints { diff --git a/src/wallet/inscribe/etch.rs b/src/wallet/inscribe/etch.rs new file mode 100644 index 0000000000..5741bb29b6 --- /dev/null +++ b/src/wallet/inscribe/etch.rs @@ -0,0 +1,11 @@ +use super::*; + +#[derive(Serialize, Deserialize, PartialEq, Debug, Copy, Clone, Default)] +#[serde(deny_unknown_fields)] +pub struct Etch { + pub divisibility: u8, + pub mint: Option, + pub premine: Decimal, + pub rune: SpacedRune, + pub symbol: char, +} diff --git a/tests/balances.rs b/tests/balances.rs index 79de88aae7..d7235da67a 100644 --- a/tests/balances.rs +++ b/tests/balances.rs @@ -57,10 +57,14 @@ fn with_runes() { Rune(RUNE), vec![( OutPoint { - txid: a.transaction, + txid: a.inscribe.reveal, vout: 1 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() @@ -69,10 +73,14 @@ fn with_runes() { Rune(RUNE + 1), vec![( OutPoint { - txid: b.transaction, + txid: b.inscribe.reveal, vout: 1 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() diff --git a/tests/command_builder.rs b/tests/command_builder.rs index 5747f3aee5..4ff76af8c3 100644 --- a/tests/command_builder.rs +++ b/tests/command_builder.rs @@ -28,6 +28,54 @@ impl ToArgs for Vec { } } +pub(crate) struct Spawn { + pub(crate) child: Child, + expected_exit_code: i32, + expected_stderr: Expected, + expected_stdout: Expected, + tempdir: Arc, +} + +impl Spawn { + #[track_caller] + fn run(self) -> (TempDir, String) { + let output = self.child.wait_with_output().unwrap(); + + let stdout = str::from_utf8(&output.stdout).unwrap(); + let stderr = str::from_utf8(&output.stderr).unwrap(); + if output.status.code() != Some(self.expected_exit_code) { + panic!( + "Test failed: {}\nstdout:\n{}\nstderr:\n{}", + output.status, stdout, stderr + ); + } + + self.expected_stderr.assert_match(stderr); + self.expected_stdout.assert_match(stdout); + + (Arc::try_unwrap(self.tempdir).unwrap(), stdout.into()) + } + + #[track_caller] + pub(crate) fn run_and_deserialize_output(self) -> T { + let stdout = self.stdout_regex(".*").run_and_extract_stdout(); + serde_json::from_str(&stdout) + .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{stdout}")) + } + + #[track_caller] + pub(crate) fn run_and_extract_stdout(self) -> String { + self.run().1 + } + + pub(crate) fn stdout_regex(self, expected_stdout: impl AsRef) -> Self { + Self { + expected_stdout: Expected::regex(expected_stdout.as_ref()), + ..self + } + } +} + pub(crate) struct CommandBuilder { args: Vec, bitcoin_rpc_server_cookie_file: Option, @@ -180,7 +228,7 @@ impl CommandBuilder { } #[track_caller] - fn run(self) -> (TempDir, String) { + pub(crate) fn spawn(self) -> Spawn { let mut command = self.command(); let child = command.spawn().unwrap(); @@ -191,21 +239,18 @@ impl CommandBuilder { .write_all(&self.stdin) .unwrap(); - let output = child.wait_with_output().unwrap(); - - let stdout = str::from_utf8(&output.stdout).unwrap(); - let stderr = str::from_utf8(&output.stderr).unwrap(); - if output.status.code() != Some(self.expected_exit_code) { - panic!( - "Test failed: {}\nstdout:\n{}\nstderr:\n{}", - output.status, stdout, stderr - ); + Spawn { + child, + expected_exit_code: self.expected_exit_code, + expected_stderr: self.expected_stderr, + expected_stdout: self.expected_stdout, + tempdir: self.tempdir, } + } - self.expected_stderr.assert_match(stderr); - self.expected_stdout.assert_match(stdout); - - (Arc::try_unwrap(self.tempdir).unwrap(), stdout.into()) + #[track_caller] + fn run(self) -> (TempDir, String) { + self.spawn().run() } pub(crate) fn run_and_extract_file(self, path: impl AsRef) -> String { @@ -221,7 +266,9 @@ impl CommandBuilder { #[track_caller] pub(crate) fn run_and_deserialize_output(self) -> T { let stdout = self.stdout_regex(".*").run_and_extract_stdout(); - serde_json::from_str(&stdout) - .unwrap_or_else(|err| panic!("Failed to deserialize JSON: {err}\n{stdout}")) + match serde_json::from_str(&stdout) { + Ok(output) => output, + Err(err) => panic!("Failed to deserialize JSON: {err}\n{stdout}"), + } } } diff --git a/tests/etch.rs b/tests/etch.rs deleted file mode 100644 index 321fb9604f..0000000000 --- a/tests/etch.rs +++ /dev/null @@ -1,480 +0,0 @@ -use { - super::*, - ord::{ - subcommand::wallet::{balance, etch::Output}, - Rune, - }, -}; - -#[test] -fn flag_is_required() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - CommandBuilder::new(format!( - "--regtest wallet etch --rune {} --divisibility 39 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: `ord wallet etch` requires index created with `--index-runes` flag\n") - .run_and_extract_stdout(); -} - -#[test] -fn divisibility_over_max_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 39 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: must be equal to or less than 38\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn supply_over_max_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 340282366920938463463374607431768211456 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .stderr_regex(r"error: invalid value '\d+' for '--supply ': number too large to fit in target type\n.*") - .expected_exit_code(2) - .run_and_extract_stdout(); -} - -#[test] -fn rune_below_minimum_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(99229755678436031 - 1), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune is less than minimum for next block: ZZWZRFAGQTKY < ZZWZRFAGQTKZ\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn reserved_rune_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - "--index-runes --regtest wallet etch --rune AAAAAAAAAAAAAAAAAAAAAAAAAAA --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢" - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn trying_to_etch_an_existing_rune_is_an_error() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - - bitcoin_rpc_server.mine_blocks(1); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn runes_can_be_etched() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - "--index-runes --regtest wallet etch --rune A•A•A•A•A•A•A•A•A•A•A•A•A --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - pretty_assert_eq!( - runes(&bitcoin_rpc_server), - vec![( - Rune(RUNE), - RuneInfo { - burned: 0, - mint: None, - divisibility: 1, - etching: output.transaction, - height: 2, - id: RuneId { - height: 2, - index: 1 - }, - index: 1, - mints: 0, - number: 0, - rune: Rune(RUNE), - spacers: 0b111111111111, - supply: 10000, - symbol: Some('¢'), - timestamp: ord::timestamp(2), - } - )] - .into_iter() - .collect() - ); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.runes.unwrap()[&Rune(RUNE)], 10000); -} - -#[test] -fn etch_sets_integer_fee_rate_correctly() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let tx = bitcoin_rpc_server.tx(2, 1); - - assert_eq!(tx.txid(), output.transaction); - - let output = tx.output.iter().map(|tx_out| tx_out.value).sum::(); - - assert_eq!(output, 50 * COIN_VALUE - tx.vsize() as u64 * 100); -} - -#[test] -fn etch_sets_decimal_fee_rate_correctly() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 100.5 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); - - let tx = bitcoin_rpc_server.tx(2, 1); - - assert_eq!(tx.txid(), output.transaction); - - let output = tx.output.iter().map(|tx_out| tx_out.value).sum::(); - - assert_eq!(output, 50 * COIN_VALUE - (tx.vsize() as f64 * 100.5) as u64); -} - -#[test] -fn etch_does_not_select_inscribed_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks(1); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.cardinal, 5000000000); - - CommandBuilder::new("--regtest wallet inscribe --fee-rate 0 --file foo.txt --postage 50btc") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.cardinal, 0); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 1 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: not enough cardinal utxos\n") - .run_and_extract_stdout(); -} - -#[test] -fn inscribe_does_not_select_runic_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.cardinal, 0); - assert_eq!(output.ordinal, 0); - assert_eq!(output.runic, Some(10000)); - - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: wallet contains no cardinal utxos\n") - .run_and_extract_stdout(); -} - -#[test] -fn send_amount_does_not_select_runic_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - CommandBuilder::new("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 600sat") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: not enough cardinal utxos\n") - .run_and_extract_stdout(); -} - -#[test] -fn send_satpoint_does_not_send_runic_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - CommandBuilder::new(format!("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}:1:0", output.transaction)) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: runic outpoints may not be sent by satpoint\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} - -#[test] -fn send_inscription_does_not_select_runic_utxos() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 1000 --symbol ¢", - Rune(RUNE), - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let inscribe = - CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - let output = CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - assert_eq!(output.cardinal, 0); - assert_eq!(output.ordinal, 10000); - assert_eq!(output.runic, Some(10000)); - - CommandBuilder::new(format!("--regtest --index-runes wallet send --postage 10001sat --fee-rate 0 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}", inscribe.inscriptions[0].id)) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") - .expected_exit_code(1) - .run_and_extract_stdout(); -} diff --git a/tests/find.rs b/tests/find.rs index e313debdaf..ecb81b3ab1 100644 --- a/tests/find.rs +++ b/tests/find.rs @@ -32,16 +32,24 @@ fn find_range_command_returns_satpoints_and_ranges() { FindRangeOutput { start: 0, size: 50 * COIN_VALUE, - satpoint: "4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:0:0" - .parse() - .unwrap() + satpoint: SatPoint { + outpoint: OutPoint { + txid: rpc_server.tx(0, 0).into(), + vout: 0, + }, + offset: 0, + } }, FindRangeOutput { start: 50 * COIN_VALUE, size: 5 * COIN_VALUE, - satpoint: "84aca0d43f45ac753d4744f40b2f54edec3a496b298951735d450e601386089d:0:0" - .parse() - .unwrap() + satpoint: SatPoint { + outpoint: OutPoint { + txid: rpc_server.tx(1, 0).into(), + vout: 0, + }, + offset: 0, + } } ] ); diff --git a/tests/json_api.rs b/tests/json_api.rs index b41188dfab..c45e25943f 100644 --- a/tests/json_api.rs +++ b/tests/json_api.rs @@ -333,7 +333,11 @@ fn get_output() { pretty_assert_eq!( output_json, api::Output { - address: None, + address: Some( + "bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq9e75rs" + .parse() + .unwrap() + ), inscriptions: vec![ InscriptionId { txid, index: 0 }, InscriptionId { txid, index: 1 }, @@ -346,7 +350,7 @@ fn get_output() { (10000000000, 15000000000,), (15000000000, 20000000000,), ],), - script_pubkey: "".to_string(), + script_pubkey: "OP_0 OP_PUSHBYTES_20 0000000000000000000000000000000000000000".into(), spent: false, transaction: txid.to_string(), value: 3 * 50 * COIN_VALUE, @@ -530,7 +534,7 @@ fn get_runes() { bitcoin_rpc_server.mine_blocks(1); - let response = ord_rpc_server.json_request(format!("/rune/{}", a.rune)); + let response = ord_rpc_server.json_request(format!("/rune/{}", a.inscribe.rune.unwrap().rune)); assert_eq!(response.status(), StatusCode::OK); let rune_json: api::Rune = serde_json::from_str(&response.text().unwrap()).unwrap(); @@ -542,20 +546,23 @@ fn get_runes() { burned: 0, mint: None, divisibility: 0, - etching: a.transaction, + etching: a.inscribe.reveal, mints: 0, number: 0, rune: Rune(RUNE), spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: 5, + timestamp: 11, }, id: RuneId { - height: 5, + height: 11, index: 1 }, - parent: None, + parent: Some(InscriptionId { + txid: a.inscribe.reveal, + index: 0, + }), } ); @@ -571,59 +578,59 @@ fn get_runes() { entries: vec![ ( RuneId { - height: 5, + height: 11, index: 1 }, RuneEntry { burned: 0, mint: None, divisibility: 0, - etching: a.transaction, + etching: a.inscribe.reveal, mints: 0, number: 0, rune: Rune(RUNE), spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: 5, + timestamp: 11, } ), ( RuneId { - height: 7, + height: 19, index: 1 }, RuneEntry { burned: 0, mint: None, divisibility: 0, - etching: b.transaction, + etching: b.inscribe.reveal, mints: 0, number: 1, rune: Rune(RUNE + 1), spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: 7, + timestamp: 19, } ), ( RuneId { - height: 9, + height: 27, index: 1 }, RuneEntry { burned: 0, mint: None, divisibility: 0, - etching: c.transaction, + etching: c.inscribe.reveal, mints: 0, number: 2, rune: Rune(RUNE + 2), spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: 9, + timestamp: 27, } ) ] @@ -658,7 +665,7 @@ fn get_runes_balances() { rune0, vec![( OutPoint { - txid: e0.transaction, + txid: e0.inscribe.reveal, vout: 1, }, 1000, @@ -670,7 +677,7 @@ fn get_runes_balances() { rune1, vec![( OutPoint { - txid: e1.transaction, + txid: e1.inscribe.reveal, vout: 1, }, 1000, @@ -682,7 +689,7 @@ fn get_runes_balances() { rune2, vec![( OutPoint { - txid: e2.transaction, + txid: e2.inscribe.reveal, vout: 1, }, 1000, diff --git a/tests/lib.rs b/tests/lib.rs index 0d859cc5d5..42749c4828 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -5,14 +5,18 @@ use { bitcoin::{ address::{Address, NetworkUnchecked}, blockdata::constants::COIN_VALUE, - Network, OutPoint, Txid, + Network, OutPoint, Txid, Witness, }, bitcoincore_rpc::bitcoincore_rpc_json::ListDescriptorsResult, chrono::{DateTime, Utc}, executable_path::executable_path, ord::{ - api, chain::Chain, outgoing::Outgoing, subcommand::runes::RuneInfo, Edict, InscriptionId, Rune, - RuneEntry, RuneId, Runestone, + api, + chain::Chain, + outgoing::Outgoing, + subcommand::runes::RuneInfo, + wallet::inscribe::{BatchEntry, Batchfile, Etch}, + Edict, InscriptionId, Pile, Rune, RuneEntry, RuneId, Runestone, SpacedRune, }, ordinals::{Rarity, Sat, SatPoint}, pretty_assertions::assert_eq as pretty_assert_eq, @@ -24,10 +28,10 @@ use { collections::BTreeMap, ffi::{OsStr, OsString}, fs, - io::Write, + io::{BufRead, BufReader, Write}, net::TcpListener, path::{Path, PathBuf}, - process::{Command, Stdio}, + process::{Child, Command, Stdio}, str::{self, FromStr}, thread, time::Duration, @@ -55,7 +59,6 @@ mod test_server; mod balances; mod decode; mod epochs; -mod etch; mod find; mod index; mod info; @@ -73,9 +76,12 @@ mod wallet; const RUNE: u128 = 99246114928149462; +type Balance = ord::subcommand::wallet::balance::Output; +type Create = ord::subcommand::wallet::create::Output; type Inscribe = ord::wallet::inscribe::Output; type Inscriptions = Vec; -type Etch = ord::subcommand::wallet::etch::Output; +type Send = ord::subcommand::wallet::send::Output; +type Supply = ord::subcommand::supply::Output; fn create_wallet(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer) { CommandBuilder::new(format!( @@ -87,6 +93,24 @@ fn create_wallet(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_serv .run_and_deserialize_output::(); } +fn receive( + bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, + ord_rpc_server: &TestServer, +) -> Address { + let address = CommandBuilder::new("wallet receive") + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server) + .run_and_deserialize_output::() + .addresses + .into_iter() + .next() + .unwrap(); + + address + .require_network(bitcoin_rpc_server.state().network) + .unwrap() +} + fn inscribe( bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer, @@ -109,29 +133,249 @@ fn inscribe( (output.inscriptions[0].id, output.reveal) } +fn drain(bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer) { + let balance = CommandBuilder::new("--regtest --index-runes wallet balance") + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server) + .run_and_deserialize_output::(); + + CommandBuilder::new(format!( + " + --chain regtest + --index-runes + wallet send + --fee-rate 0 + bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k + {}sat + ", + balance.cardinal + )) + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); + + let balance = CommandBuilder::new("--regtest --index-runes wallet balance") + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server) + .run_and_deserialize_output::(); + + pretty_assert_eq!(balance.cardinal, 0); +} + +struct Etched { + id: RuneId, + inscribe: Inscribe, +} + fn etch( bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, ord_rpc_server: &TestServer, rune: Rune, -) -> Etch { +) -> Etched { + batch( + bitcoin_rpc_server, + ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 0, + mint: None, + premine: "1000".parse().unwrap(), + rune: SpacedRune { rune, spacers: 0 }, + symbol: '¢', + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), + ..Default::default() + }], + ..Default::default() + }, + ) +} + +fn batch( + bitcoin_rpc_server: &test_bitcoincore_rpc::Handle, + ord_rpc_server: &TestServer, + batchfile: Batchfile, +) -> Etched { bitcoin_rpc_server.mine_blocks(1); - let output = CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 0 --supply 1000 --symbol ¢", - rune - ) - ) - .bitcoin_rpc_server(bitcoin_rpc_server) - .ord_rpc_server(ord_rpc_server) - .run_and_deserialize_output(); + let mut builder = + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("batch.yaml", serde_yaml::to_string(&batchfile).unwrap()) + .bitcoin_rpc_server(bitcoin_rpc_server) + .ord_rpc_server(ord_rpc_server); + + for inscription in &batchfile.inscriptions { + builder = builder.write(&inscription.file, "inscription"); + } + + let mut spawn = builder.spawn(); + + let mut buffer = String::new(); + + BufReader::new(spawn.child.stderr.as_mut().unwrap()) + .read_line(&mut buffer) + .unwrap(); + + assert_eq!(buffer, "Waiting for rune commitment to mature…\n"); + + bitcoin_rpc_server.mine_blocks(6); + + let inscribe = spawn.run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); - output + let height = bitcoin_rpc_server.height(); + + let id = RuneId { + height: u32::try_from(height).unwrap(), + index: 1, + }; + + let reveal = inscribe.reveal; + let parent = inscribe.inscriptions[0].id; + + let Etch { + divisibility, + premine, + rune, + symbol, + mint, + } = batchfile.etch.unwrap(); + + let mut mint_definition = Vec::::new(); + + if let Some(mint) = mint { + mint_definition.push("
".into()); + mint_definition.push("
".into()); + mint_definition.push("
deadline
".into()); + if let Some(deadline) = mint.deadline { + mint_definition.push(format!( + "
", + ord::timestamp(deadline) + )); + } else { + mint_definition.push("
none
".into()); + } + + mint_definition.push("
end
".into()); + + if let Some(term) = mint.term { + let end = height + u64::from(term); + mint_definition.push(format!("
{end}
")); + } else { + mint_definition.push("
none
".into()); + } + + mint_definition.push("
limit
".into()); + + mint_definition.push(format!( + "
{}
", + Pile { + amount: mint.limit.to_amount(divisibility).unwrap(), + divisibility, + symbol: Some(symbol), + } + )); + + mint_definition.push("
mints
".into()); + mint_definition.push("
0
".into()); + + mint_definition.push("
".into()); + mint_definition.push("
".into()); + } else { + mint_definition.push("
no
".into()); + } + + ord_rpc_server.assert_response_regex( + format!("/rune/{rune}"), + format!( + r".*
id
+
{id}
.* +
etching transaction index
+
1
+
mint
+ {} +
supply
+
{premine} {symbol}
+
burned
+
0 {symbol}
+
divisibility
+
{divisibility}
+
symbol
+
{symbol}
+
etching
+
{reveal}
+
parent
+
{parent}
+.*", + mint_definition.join("\\s+"), + ), + ); + + let ord::wallet::inscribe::RuneInfo { + destination, + location, + rune, + } = inscribe.rune.clone().unwrap(); + + if premine.to_amount(divisibility).unwrap() > 0 { + let destination = destination + .unwrap() + .clone() + .require_network(Network::Regtest) + .unwrap(); + + assert!(bitcoin_rpc_server.state().is_wallet_address(&destination)); + + let location = location.unwrap(); + + ord_rpc_server.assert_response_regex( + "/runes/balances", + format!( + ".* + {rune} + + + + + + +
+ {location} + + {premine}\u{00A0}{symbol} +
+ + .*" + ), + ); + + assert_eq!(bitcoin_rpc_server.address(location), destination); + } else { + assert!(destination.is_none()); + assert!(location.is_none()); + } + + let response = ord_rpc_server.json_request("/inscriptions"); + + assert!(response.status().is_success()); + + for id in response.json::().unwrap().ids { + let response = ord_rpc_server.json_request(format!("/inscription/{id}")); + assert!(response.status().is_success()); + if let Some(location) = location { + let inscription = response.json::().unwrap(); + assert!(inscription.satpoint.outpoint != location); + } + } + + Etched { inscribe, id } } -fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { +fn envelope(payload: &[&[u8]]) -> Witness { let mut builder = bitcoin::script::Builder::new() .push_opcode(bitcoin::opcodes::OP_FALSE) .push_opcode(bitcoin::opcodes::all::OP_IF); @@ -146,12 +390,5 @@ fn envelope(payload: &[&[u8]]) -> bitcoin::Witness { .push_opcode(bitcoin::opcodes::all::OP_ENDIF) .into_script(); - bitcoin::Witness::from_slice(&[script.into_bytes(), Vec::new()]) -} - -fn runes(rpc_server: &test_bitcoincore_rpc::Handle) -> BTreeMap { - CommandBuilder::new("--index-runes --regtest runes") - .bitcoin_rpc_server(rpc_server) - .run_and_deserialize_output::() - .runes + Witness::from_slice(&[script.into_bytes(), Vec::new()]) } diff --git a/tests/runes.rs b/tests/runes.rs index 341279ad70..0dd114d75f 100644 --- a/tests/runes.rs +++ b/tests/runes.rs @@ -45,7 +45,7 @@ fn one_rune() { let etch = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--index-runes --regtest runes") .bitcoin_rpc_server(&bitcoin_rpc_server) .run_and_deserialize_output::(), @@ -56,10 +56,10 @@ fn one_rune() { burned: 0, mint: None, divisibility: 0, - etching: etch.transaction, - height: 2, + etching: etch.inscribe.reveal, + height: 8, id: RuneId { - height: 2, + height: 8, index: 1 }, index: 1, @@ -69,7 +69,7 @@ fn one_rune() { spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(2), + timestamp: ord::timestamp(8), } )] .into_iter() @@ -104,10 +104,10 @@ fn two_runes() { burned: 0, mint: None, divisibility: 0, - etching: a.transaction, - height: 2, + etching: a.inscribe.reveal, + height: 8, id: RuneId { - height: 2, + height: 8, index: 1 }, index: 1, @@ -117,7 +117,7 @@ fn two_runes() { spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(2), + timestamp: ord::timestamp(8), } ), ( @@ -126,10 +126,10 @@ fn two_runes() { burned: 0, mint: None, divisibility: 0, - etching: b.transaction, - height: 4, + etching: b.inscribe.reveal, + height: 16, id: RuneId { - height: 4, + height: 16, index: 1 }, index: 1, @@ -139,7 +139,7 @@ fn two_runes() { spacers: 0, supply: 1000, symbol: Some('¢'), - timestamp: ord::timestamp(4), + timestamp: ord::timestamp(16), } ) ] diff --git a/tests/supply.rs b/tests/supply.rs index 9e5f396af2..6c6366831b 100644 --- a/tests/supply.rs +++ b/tests/supply.rs @@ -1,10 +1,10 @@ -use {super::*, ord::subcommand::supply::Output}; +use super::*; #[test] fn genesis() { assert_eq!( - CommandBuilder::new("supply").run_and_deserialize_output::(), - Output { + CommandBuilder::new("supply").run_and_deserialize_output::(), + Supply { supply: 2099999997690000, first: 0, last: 2099999997689999, diff --git a/tests/test_server.rs b/tests/test_server.rs index 776f77e774..c0a0d4bbee 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -87,14 +87,18 @@ impl TestServer { format!("http://127.0.0.1:{}", self.port).parse().unwrap() } + #[track_caller] pub(crate) fn assert_response_regex(&self, path: impl AsRef, regex: impl AsRef) { self.sync_server(); - + let path = path.as_ref(); let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); - assert_eq!(response.status(), StatusCode::OK); - assert_regex_match!(response.text().unwrap(), regex.as_ref()); + let status = response.status(); + assert_eq!(status, StatusCode::OK, "bad status for {path}: {status}"); + let text = response.text().unwrap(); + assert_regex_match!(text, regex.as_ref()); } + #[track_caller] pub(crate) fn assert_response(&self, path: impl AsRef, expected_response: &str) { self.sync_server(); let response = reqwest::blocking::get(self.url().join(path.as_ref()).unwrap()).unwrap(); @@ -128,21 +132,9 @@ impl TestServer { pub(crate) fn sync_server(&self) { let client = Client::new(&self.bitcoin_rpc_url, Auth::None).unwrap(); let chain_block_count = client.get_block_count().unwrap() + 1; - - for i in 0.. { - let response = reqwest::blocking::get(self.url().join("/blockcount").unwrap()).unwrap(); - - assert_eq!(response.status(), StatusCode::OK); - - let ord_height = response.text().unwrap().parse::().unwrap(); - - if ord_height >= chain_block_count { - break; - } else if i == 20 { - panic!("index failed to synchronize with chain"); - } - thread::sleep(Duration::from_millis(50)); - } + let response = reqwest::blocking::get(self.url().join("/update").unwrap()).unwrap(); + assert_eq!(response.status(), StatusCode::OK); + assert!(response.text().unwrap().parse::().unwrap() >= chain_block_count); } } diff --git a/tests/wallet.rs b/tests/wallet.rs index 92ca17c20b..5e347c9499 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -12,5 +12,6 @@ mod outputs; mod receive; mod restore; mod sats; +mod selection; mod send; mod transactions; diff --git a/tests/wallet/balance.rs b/tests/wallet/balance.rs index d7d3e626b2..7d2645f19c 100644 --- a/tests/wallet/balance.rs +++ b/tests/wallet/balance.rs @@ -84,7 +84,7 @@ fn runic_utxos_are_deducted_from_cardinal() { create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--regtest --index-runes wallet balance") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) @@ -100,17 +100,17 @@ fn runic_utxos_are_deducted_from_cardinal() { etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); - assert_eq!( + pretty_assert_eq!( CommandBuilder::new("--regtest --index-runes wallet balance") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::(), Output { - cardinal: 100 * COIN_VALUE - 10_000, - ordinal: 0, + cardinal: 50 * COIN_VALUE * 8 - 20_000, + ordinal: 10000, runic: Some(10_000), runes: Some(vec![(Rune(RUNE), 1000)].into_iter().collect()), - total: 100 * COIN_VALUE, + total: 50 * COIN_VALUE * 8, } ); } diff --git a/tests/wallet/inscribe.rs b/tests/wallet/inscribe.rs index e0efd95879..c6c10d9c84 100644 --- a/tests/wallet/inscribe.rs +++ b/tests/wallet/inscribe.rs @@ -1578,7 +1578,15 @@ fn batch_inscribe_works_with_some_destinations_set_and_others_not() { .write("meow.wav", [0; 2048]) .write( "batch.yaml", - "mode: separate-outputs\ninscriptions:\n- file: inscription.txt\n destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4\n- file: tulip.png\n- file: meow.wav\n destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k\n" + "\ +mode: separate-outputs +inscriptions: +- file: inscription.txt + destination: bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 +- file: tulip.png +- file: meow.wav + destination: bc1pxwww0ct9ue7e8tdnlmug5m2tamfn7q06sahstg39ys4c9f3340qqxrdu9k +", ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) @@ -1601,7 +1609,7 @@ fn batch_inscribe_works_with_some_destinations_set_and_others_not() { ".*
address
{}
.*", - bitcoin_rpc_server.change_addresses()[0] + bitcoin_rpc_server.state().change_addresses[0], ), ); @@ -2401,9 +2409,9 @@ inscriptions: assert_eq!(outpoints.len(), output.inscriptions.len()); - let inscription_1 = output.inscriptions[0]; - let inscription_2 = output.inscriptions[1]; - let inscription_3 = output.inscriptions[2]; + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; ord_rpc_server.assert_response_regex( format!("/inscription/{}", inscription_1.id), @@ -2442,12 +2450,16 @@ fn batch_inscribe_with_satpoints_with_different_sizes() { create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + let address_1 = receive(&bitcoin_rpc_server, &ord_rpc_server); + let address_2 = receive(&bitcoin_rpc_server, &ord_rpc_server); + let address_3 = receive(&bitcoin_rpc_server, &ord_rpc_server); + bitcoin_rpc_server.mine_blocks(3); let outpoint_1 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 25btc", - ) + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_1} 25btc" + )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .stdout_regex(r".*") @@ -2459,9 +2471,9 @@ fn batch_inscribe_with_satpoints_with_different_sizes() { bitcoin_rpc_server.mine_blocks(1); let outpoint_2 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc", - ) + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_2} 1btc" + )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .stdout_regex(r".*") @@ -2473,9 +2485,9 @@ fn batch_inscribe_with_satpoints_with_different_sizes() { bitcoin_rpc_server.mine_blocks(1); let outpoint_3 = OutPoint { - txid: CommandBuilder::new( - "--index-sats wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 3btc", - ) + txid: CommandBuilder::new(format!( + "--index-sats wallet send --fee-rate 1 {address_3} 3btc" + )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .stdout_regex(r".*") @@ -2570,9 +2582,9 @@ inscriptions: assert_eq!(outpoints.len(), output.inscriptions.len()); - let inscription_1 = output.inscriptions[0]; - let inscription_2 = output.inscriptions[1]; - let inscription_3 = output.inscriptions[2]; + let inscription_1 = &output.inscriptions[0]; + let inscription_2 = &output.inscriptions[1]; + let inscription_3 = &output.inscriptions[2]; ord_rpc_server.assert_response_regex( format!("/inscription/{}", inscription_1.id), @@ -2604,3 +2616,284 @@ inscriptions: ), ); } + +#[test] +fn batch_inscribe_can_etch_rune() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + let batch = batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), + ..Default::default() + }], + ..Default::default() + }, + ); + + let parent = batch.inscribe.inscriptions[0].id; + + let request = ord_rpc_server.request(format!("/content/{parent}")); + + assert_eq!(request.status(), 200); + assert_eq!(request.headers().get("content-type").unwrap(), "image/jpeg"); + assert_eq!(request.text().unwrap(), "inscription"); + + ord_rpc_server.assert_response_regex( + format!("/inscription/{parent}"), + r".*
rune
\s*
AAAAAAAAAAAAA
.*", + ); + + ord_rpc_server.assert_response_regex( + "/rune/AAAAAAAAAAAAA", + format!( + r".*
parent
\s*
{parent}
.*" + ), + ); + + assert!(bitcoin_rpc_server.state().is_wallet_address( + &batch + .inscribe + .rune + .unwrap() + .destination + .unwrap() + .require_network(Network::Regtest) + .unwrap() + )); +} + +#[test] +fn etch_existing_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&Batchfile { + etch: Some(Etch { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 1, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.txt".into(), + ..Default::default() + }], + ..Default::default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAA` has already been etched\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_reserved_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&Batchfile { + etch: Some(Etch { + divisibility: 0, + rune: SpacedRune { + rune: Rune::reserved(0), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.txt".into(), + ..Default::default() + }], + ..Default::default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune `AAAAAAAAAAAAAAAAAAAAAAAAAAA` is reserved\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_sub_minimum_rune_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&Batchfile { + etch: Some(Etch { + divisibility: 0, + rune: SpacedRune { + rune: Rune(0), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.txt".into(), + ..Default::default() + }], + ..Default::default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: rune is less than minimum for next block: A < ZZWZRFAGQTKZ\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_requires_rune_index() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&Batchfile { + etch: Some(Etch { + divisibility: 0, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.txt".into(), + ..Default::default() + }], + ..Default::default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: etching runes requires index created with `--index-runes`\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn etch_divisibility_over_maximum_error() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks(1); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "foo") + .write( + "batch.yaml", + serde_yaml::to_string(&Batchfile { + etch: Some(Etch { + divisibility: 39, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.txt".into(), + ..Default::default() + }], + ..Default::default() + }) + .unwrap(), + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: must be less than or equal 38\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} diff --git a/tests/wallet/mint.rs b/tests/wallet/mint.rs index 6bc2408649..bb47b8b073 100644 --- a/tests/wallet/mint.rs +++ b/tests/wallet/mint.rs @@ -1,10 +1,6 @@ use { super::*, - bitcoin::Witness, - ord::{ - runes::{Etching, Mint, Pile}, - subcommand::wallet::{balance, mint}, - }, + ord::{runes::Pile, subcommand::wallet::mint}, }; #[test] @@ -20,39 +16,31 @@ fn minting_rune_and_fails_if_after_end() { create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", - Rune(RUNE) - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: rune AAAAAAAAAAAAA has not been etched\n") - .run_and_extract_stdout(); - - bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - symbol: Some('*'), - divisibility: 1, - mint: Some(Mint { - limit: Some(1111), - term: Some(2), - ..Default::default() - }), - ..Default::default() + batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "0".parse().unwrap(), + symbol: '¢', + mint: Some(ord::wallet::inscribe::BatchMint { + term: Some(2), + limit: "111.1".parse().unwrap(), + deadline: None, }), + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - bitcoin_rpc_server.mine_blocks(1); + }], + ..Default::default() + }, + ); let output = CommandBuilder::new(format!( "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", @@ -69,16 +57,16 @@ fn minting_rune_and_fails_if_after_end() { .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::(); - assert_eq!( + pretty_assert_eq!( output.pile, Pile { amount: 1111, divisibility: 1, - symbol: Some('*'), + symbol: Some('¢'), } ); - assert_eq!( + pretty_assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( @@ -88,7 +76,7 @@ fn minting_rune_and_fails_if_after_end() { txid: output.mint, vout: 1 }, - output.pile.amount + output.pile, )] .into_iter() .collect() @@ -107,7 +95,7 @@ fn minting_rune_and_fails_if_after_end() { .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .expected_exit_code(1) - .expected_stderr("error: rune AAAAAAAAAAAAA mint ended on block 4\n") + .expected_stderr("error: rune AAAAAAAAAAAAA mint ended on block 11\n") .run_and_extract_stdout(); } @@ -120,27 +108,29 @@ fn minting_rune_fails_if_not_mintable() { let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - bitcoin_rpc_server.mine_blocks(1); + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - mint: None, - ..Default::default() - }), + batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - bitcoin_rpc_server.mine_blocks(1); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + }], + ..Default::default() + }, + ); CommandBuilder::new(format!( "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", @@ -162,34 +152,33 @@ fn minting_rune_fails_if_after_deadline() { let ord_rpc_server = TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - bitcoin_rpc_server.mine_blocks(1); - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); let rune = Rune(RUNE); - - let deadline: u32 = 3; - - bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(rune), - mint: Some(Mint { - deadline: Some(deadline), - ..Default::default() - }), - ..Default::default() + let deadline = 9; + + batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 1, + rune: SpacedRune { rune, spacers: 0 }, + premine: "0".parse().unwrap(), + symbol: '¢', + mint: Some(ord::wallet::inscribe::BatchMint { + term: Some(2), + limit: "111.1".parse().unwrap(), + deadline: Some(deadline), }), + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - bitcoin_rpc_server.mine_blocks(1); + }], + ..Default::default() + }, + ); CommandBuilder::new(format!( "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {rune}", @@ -232,72 +221,3 @@ fn minting_rune_with_no_rune_index_fails() { .expected_stderr("error: `ord wallet etch` requires index created with `--index-runes` flag\n") .run_and_extract_stdout(); } - -#[test] -fn minting_rune_does_not_send_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[(1, 0, 0, Witness::new())], - op_return: Some( - Runestone { - etching: Some(Etching { - rune: Some(Rune(RUNE)), - symbol: Some('*'), - divisibility: 1, - mint: Some(Mint { - limit: Some(1111), - ..Default::default() - }), - ..Default::default() - }), - ..Default::default() - } - .encipher(), - ), - ..Default::default() - }); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - CommandBuilder::new("--chain regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - assert_eq!( - CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - balance::Output { - cardinal: 0, - ordinal: 10000, - runic: Some(0), - runes: Some(BTreeMap::new()), - total: 10000, - } - ); - - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet mint --fee-rate 1 --rune {}", - Rune(RUNE) - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: not enough cardinal utxos\n") - .run_and_extract_stdout(); -} diff --git a/tests/wallet/selection.rs b/tests/wallet/selection.rs new file mode 100644 index 0000000000..d937995bc3 --- /dev/null +++ b/tests/wallet/selection.rs @@ -0,0 +1,215 @@ +use super::*; + +#[test] +fn inscribe_does_not_select_runic_utxos() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + drain(&bitcoin_rpc_server, &ord_rpc_server); + + CommandBuilder::new("--regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .expected_stderr("error: wallet contains no cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn send_amount_does_not_select_runic_utxos() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + drain(&bitcoin_rpc_server, &ord_rpc_server); + + CommandBuilder::new("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 600sat") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn send_satpoint_does_not_send_runic_utxos() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); + + let etched = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + CommandBuilder::new(format!("--regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw {}:0", etched.inscribe.rune.unwrap().location.unwrap())) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: runic outpoints may not be sent by satpoint\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn send_inscription_does_not_select_runic_utxos() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--regtest", "--index-runes"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + + let (id, _) = inscribe(&bitcoin_rpc_server, &ord_rpc_server); + + drain(&bitcoin_rpc_server, &ord_rpc_server); + + CommandBuilder::new( + format!( + " + --regtest + --index-runes + wallet + send + --postage 10000sat + --fee-rate 1 + bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw + {id} + ")) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_stderr("error: wallet does not contain enough cardinal UTXOs, please add additional funds to wallet.\n") + .expected_exit_code(1) + .run_and_extract_stdout(); +} + +#[test] +fn mint_does_not_select_inscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 1, + rune: SpacedRune { + rune: Rune(RUNE), + spacers: 0, + }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: Some(ord::wallet::inscribe::BatchMint { + deadline: None, + limit: "1000".parse().unwrap(), + term: None, + }), + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), + ..Default::default() + }], + ..Default::default() + }, + ); + + drain(&bitcoin_rpc_server, &ord_rpc_server); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet mint --fee-rate 0 --rune {}", + Rune(RUNE) + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} + +#[test] +fn sending_rune_does_not_send_inscription() { + let bitcoin_rpc_server = test_bitcoincore_rpc::builder() + .network(Network::Regtest) + .build(); + + let ord_rpc_server = + TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); + + create_wallet(&bitcoin_rpc_server, &ord_rpc_server); + + bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); + + let rune = Rune(RUNE); + + CommandBuilder::new("--chain regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") + .write("foo.txt", "FOO") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); + + bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); + + pretty_assert_eq!( + CommandBuilder::new("--regtest --index-runes wallet balance") + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(), + Balance { + cardinal: 10000, + ordinal: 10000, + runic: Some(0), + runes: Some(BTreeMap::new()), + total: 20000, + } + ); + + etch(&bitcoin_rpc_server, &ord_rpc_server, rune); + + drain(&bitcoin_rpc_server, &ord_rpc_server); + + CommandBuilder::new(format!( + " + --chain regtest + --index-runes + wallet send + --fee-rate 0 + bcrt1pyrmadgg78e38ewfv0an8c6eppk2fttv5vnuvz04yza60qau5va0saknu8k + 1000{rune} + ", + )) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .expected_exit_code(1) + .expected_stderr("error: not enough cardinal utxos\n") + .run_and_extract_stdout(); +} diff --git a/tests/wallet/send.rs b/tests/wallet/send.rs index 914941bbe8..26b08db203 100644 --- a/tests/wallet/send.rs +++ b/tests/wallet/send.rs @@ -1,10 +1,4 @@ -use { - super::*, - base64::Engine, - bitcoin::psbt::Psbt, - ord::subcommand::wallet::{balance, create, send}, - std::collections::BTreeMap, -}; +use {super::*, base64::Engine, bitcoin::psbt::Psbt}; #[test] fn inscriptions_can_be_sent() { @@ -26,7 +20,7 @@ fn inscriptions_can_be_sent() { .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .stdout_regex(r".*") - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); let txid = bitcoin_rpc_server.mempool()[0].txid(); assert_eq!(txid, output.txid); @@ -92,7 +86,7 @@ fn send_inscribed_sat() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -117,14 +111,14 @@ fn send_on_mainnnet_works_with_wallet_named_foo() { CommandBuilder::new("wallet --name foo create") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); CommandBuilder::new(format!( "wallet --name foo send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 {txid}:0:0" )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); } #[test] @@ -164,7 +158,7 @@ fn send_on_mainnnet_works_with_wallet_named_ord() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); assert_eq!(bitcoin_rpc_server.mempool()[0].txid(), output.txid); } @@ -248,7 +242,7 @@ fn can_send_after_dust_limit_from_an_inscription() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); } #[test] @@ -260,20 +254,28 @@ fn splitting_merged_inscriptions_is_possible() { create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - bitcoin_rpc_server.mine_blocks(3); + bitcoin_rpc_server.mine_blocks(1); + + let inscribe = CommandBuilder::new("wallet inscribe --fee-rate 0 --batch batch.yaml") + .write("inscription.txt", "INSCRIPTION") + .write( + "batch.yaml", + "\ +mode: shared-output + +inscriptions: +- file: inscription.txt +- file: inscription.txt +- file: inscription.txt +", + ) + .bitcoin_rpc_server(&bitcoin_rpc_server) + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); - let inscription = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); + let reveal_txid = inscribe.reveal; - // merging 3 inscriptions into one utxo - let reveal_txid = bitcoin_rpc_server.broadcast_tx(TransactionTemplate { - inputs: &[ - (1, 0, 0, inscription.clone()), - (2, 0, 0, inscription.clone()), - (3, 0, 0, inscription.clone()), - ], - outputs: 1, - ..Default::default() - }); + let destination = inscribe.inscriptions[0].destination.clone(); bitcoin_rpc_server.mine_blocks(1); @@ -285,7 +287,7 @@ fn splitting_merged_inscriptions_is_possible() { pretty_assert_eq!( output_json, api::Output { - address: None, + address: Some(destination.clone()), inscriptions: vec![ InscriptionId { txid: reveal_txid, @@ -302,15 +304,11 @@ fn splitting_merged_inscriptions_is_possible() { ], indexed: true, runes: Vec::new(), - sat_ranges: Some(vec![ - (5000000000, 10000000000,), - (10000000000, 15000000000,), - (15000000000, 20000000000,), - ],), - script_pubkey: "".to_string(), + sat_ranges: Some(vec![(5_000_000_000, 5_000_030_000)]), + script_pubkey: destination.payload.script_pubkey().to_asm_string(), spent: false, transaction: reveal_txid.to_string(), - value: 3 * 50 * COIN_VALUE, + value: 30_000, } ); @@ -323,7 +321,7 @@ fn splitting_merged_inscriptions_is_possible() { .ord_rpc_server(&ord_rpc_server) .expected_exit_code(1) .expected_stderr(format!( - "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:{}\n", 100 * COIN_VALUE + "error: cannot send {reveal_txid}:0:0 without also sending inscription {reveal_txid}i2 at {reveal_txid}:0:20000\n", )) .run_and_extract_stdout(); @@ -334,7 +332,7 @@ fn splitting_merged_inscriptions_is_possible() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -345,7 +343,7 @@ fn splitting_merged_inscriptions_is_possible() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -356,7 +354,7 @@ fn splitting_merged_inscriptions_is_possible() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); } #[test] @@ -396,7 +394,7 @@ fn send_btc_with_fee_rate() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); let tx = &bitcoin_rpc_server.mempool()[0]; @@ -442,7 +440,7 @@ fn send_btc_locks_inscriptions() { CommandBuilder::new("wallet send --fee-rate 1 bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 1btc") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); assert!(bitcoin_rpc_server.get_locked().contains(&OutPoint { txid: reveal, @@ -487,7 +485,7 @@ fn wallet_send_with_fee_rate() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); let tx = &bitcoin_rpc_server.mempool()[0]; let mut fee = 0; @@ -548,7 +546,7 @@ fn wallet_send_with_fee_rate_and_target_postage() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); let tx = &bitcoin_rpc_server.mempool()[0]; let mut fee = 0; @@ -608,7 +606,7 @@ fn send_dry_run() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); assert!(bitcoin_rpc_server.mempool().is_empty()); assert_eq!( @@ -717,7 +715,7 @@ fn sending_rune_works() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -736,7 +734,11 @@ fn sending_rune_works() { txid: output.txid, vout: 2 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() @@ -765,7 +767,7 @@ fn sending_spaced_rune_works() { ) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -784,7 +786,11 @@ fn sending_spaced_rune_works() { txid: output.txid, vout: 2 }, - 1000 + Pile { + amount: 1000, + divisibility: 0, + symbol: Some('¢') + }, )] .into_iter() .collect() @@ -810,17 +816,24 @@ fn sending_rune_with_divisibility_works() { let rune = Rune(RUNE); - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 1 --fee-rate 0 --supply 100 --symbol ¢", - rune, - ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks(1); + batch( + &bitcoin_rpc_server, + &ord_rpc_server, + Batchfile { + etch: Some(Etch { + divisibility: 1, + rune: SpacedRune { rune, spacers: 0 }, + premine: "1000".parse().unwrap(), + symbol: '¢', + mint: None, + }), + inscriptions: vec![BatchEntry { + file: "inscription.jpeg".into(), + ..Default::default() + }], + ..Default::default() + }, + ); let output = CommandBuilder::new(format!( "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 10.1{}", @@ -828,7 +841,7 @@ fn sending_rune_with_divisibility_works() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -837,7 +850,7 @@ fn sending_rune_with_divisibility_works() { .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::(); - assert_eq!( + pretty_assert_eq!( balances, ord::subcommand::balances::Output { runes: vec![( @@ -848,14 +861,22 @@ fn sending_rune_with_divisibility_works() { txid: output.txid, vout: 1 }, - 899 + Pile { + amount: 9899, + divisibility: 1, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 101 + Pile { + amount: 101, + divisibility: 1, + symbol: Some('¢') + }, ) ] .into_iter() @@ -886,7 +907,7 @@ fn sending_rune_leaves_unspent_runes_in_wallet() { )) .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -906,14 +927,22 @@ fn sending_rune_leaves_unspent_runes_in_wallet() { txid: output.txid, vout: 1 }, - 250 + Pile { + amount: 250, + divisibility: 0, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 750 + Pile { + amount: 750, + divisibility: 0, + symbol: Some('¢') + }, ) ] .into_iter() @@ -924,16 +953,14 @@ fn sending_rune_leaves_unspent_runes_in_wallet() { } ); - let tx = bitcoin_rpc_server.tx(3, 1); - - assert_eq!(tx.txid(), output.txid); + let tx = bitcoin_rpc_server.tx_by_id(output.txid); let address = Address::from_script(&tx.output[1].script_pubkey, Network::Regtest).unwrap(); assert!(bitcoin_rpc_server - .change_addresses() - .iter() - .any(|change_address| change_address == &address)); + .state() + .change_addresses + .contains(&address)); } #[test] @@ -947,15 +974,22 @@ fn sending_rune_creates_transaction_with_expected_runestone() { create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); + let etch = etch(&bitcoin_rpc_server, &ord_rpc_server, Rune(RUNE)); let output = CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 1 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750{}", + " + --chain regtest + --index-runes + wallet + send + --fee-rate 1 + bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 750{} + ", Rune(RUNE), )) .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); + .ord_rpc_server(&ord_rpc_server) + .run_and_deserialize_output::(); bitcoin_rpc_server.mine_blocks(1); @@ -975,14 +1009,22 @@ fn sending_rune_creates_transaction_with_expected_runestone() { txid: output.txid, vout: 1 }, - 250 + Pile { + amount: 250, + divisibility: 0, + symbol: Some('¢') + }, ), ( OutPoint { txid: output.txid, vout: 2 }, - 750 + Pile { + amount: 750, + divisibility: 0, + symbol: Some('¢') + }, ) ] .into_iter() @@ -993,21 +1035,15 @@ fn sending_rune_creates_transaction_with_expected_runestone() { } ); - let tx = bitcoin_rpc_server.tx(3, 1); - - assert_eq!(tx.txid(), output.txid); + let tx = bitcoin_rpc_server.tx_by_id(output.txid); - assert_eq!( + pretty_assert_eq!( Runestone::from_transaction(&tx).unwrap(), Runestone { default_output: None, etching: None, edicts: vec![Edict { - id: RuneId { - height: 2, - index: 1 - } - .into(), + id: etch.id.into(), amount: 750, output: 2 }], @@ -1046,77 +1082,3 @@ fn error_messages_use_spaced_runes() { .expected_stderr("error: rune `FOO` has not been etched\n") .run_and_extract_stdout(); } - -#[test] -fn sending_rune_does_not_send_inscription() { - let bitcoin_rpc_server = test_bitcoincore_rpc::builder() - .network(Network::Regtest) - .build(); - - let ord_rpc_server = - TestServer::spawn_with_server_args(&bitcoin_rpc_server, &["--index-runes", "--regtest"], &[]); - - create_wallet(&bitcoin_rpc_server, &ord_rpc_server); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - let rune = Rune(RUNE); - - CommandBuilder::new("--chain regtest --index-runes wallet inscribe --fee-rate 0 --file foo.txt") - .write("foo.txt", "FOO") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 10000); - - assert_eq!( - CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - balance::Output { - cardinal: 10000, - ordinal: 10000, - runic: Some(0), - runes: Some(BTreeMap::new()), - total: 20000, - } - ); - - CommandBuilder::new( - format!( - "--index-runes --regtest wallet etch --rune {} --divisibility 0 --fee-rate 0 --supply 1000 --symbol ¢", - rune - ) - ) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(); - - bitcoin_rpc_server.mine_blocks_with_subsidy(1, 0); - - assert_eq!( - CommandBuilder::new("--regtest --index-runes wallet balance") - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .run_and_deserialize_output::(), - balance::Output { - cardinal: 0, - ordinal: 10000, - runic: Some(10000), - runes: Some(vec![(rune, 1000)].into_iter().collect()), - total: 20000, - } - ); - - CommandBuilder::new(format!( - "--chain regtest --index-runes wallet send --fee-rate 0 bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw 1000{}", - rune - )) - .bitcoin_rpc_server(&bitcoin_rpc_server) - .ord_rpc_server(&ord_rpc_server) - .expected_exit_code(1) - .expected_stderr("error: not enough cardinal utxos\n") - .run_and_extract_stdout(); -} diff --git a/tests/wallet/transactions.rs b/tests/wallet/transactions.rs index e728e58371..5c61320efa 100644 --- a/tests/wallet/transactions.rs +++ b/tests/wallet/transactions.rs @@ -24,7 +24,6 @@ fn transactions() { .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); assert_eq!(output[0].confirmations, 1); } @@ -48,8 +47,7 @@ fn transactions_with_limit() { .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[0].confirmations, 1); + assert_eq!(output.len(), 1); bitcoin_rpc_server.mine_blocks(1); @@ -58,14 +56,12 @@ fn transactions_with_limit() { .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::>(); - assert_regex_match!(output[1].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[1].confirmations, 2); + assert_eq!(output.len(), 2); let output = CommandBuilder::new("wallet transactions --limit 1") .bitcoin_rpc_server(&bitcoin_rpc_server) .ord_rpc_server(&ord_rpc_server) .run_and_deserialize_output::>(); - assert_regex_match!(output[0].transaction.to_string(), "[[:xdigit:]]{64}"); - assert_eq!(output[0].confirmations, 1); + assert_eq!(output.len(), 1); }