transfer100.pizza#71,242,683brc-20Inscription #70548439#70,548,439pngInscription #70544675#70,544,675pngInscription #70544669#70,544,669pngInscription #70489973#70,489,973pngInscription #70489230#70,489,230pngInscription #70465296#70,465,296pngInscription #70395433#70,395,433webp["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,114text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,113text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,112text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,111text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,110text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,109text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,108text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,107text["/content/5ac5662894624ebde8ce122739c5f00f7eedd7125617491ce3598e326273d924i0",{"p":"brc-20","op":"mint","tick":"cpos","amt":"40"},{"p":"ocf","op":"call","name":"forest_launch","params":["0"]}]#69,843,106text#67,757,160mint1,000.rwas#60,398,163brc-339mint1,000.rwas#60,398,162brc-339mint1,000.rwas#60,398,161brc-339mint1,000.rwas#60,398,160brc-339mint1,000.rwas#60,398,159brc-339mint1,000.rwas#60,398,158brc-339mint1,000.rwas#60,398,157brc-339mint1,000.rwas#60,398,156brc-339mint1,000.rwas#60,398,155brc-339mint1,000.rwas#60,398,154brc-339Inscription #50401482#50,401,482html#50,183,418veda#50,119,955vedaInscription #46104214#46,104,214pngInscription #46102688#46,102,688pngInscription #46100869#46,100,869pngInscription #46100594#46,100,594pngInscription #46098741#46,098,741pngInscription #46098680#46,098,680pngInscription #45256798#45,256,798pngInscription #45256258#45,256,258pngInscription #45240493#45,240,493pngInscription #45234599#45,234,599pngInscription #45234590#45,234,590pngInscription #45234396#45,234,396png let context = Context::builder() .arg("--index-runes-pre-alpha-i-agree-to-get-rekt") .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: Rune(RUNE), 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, }; context.assert_runes( [( id, RuneEntry { etching, rune: Rune(RUNE), limit: Some(1000), timestamp: 2, supply: 1000, ..Default::default() }, )], [( OutPoint { #45,051,221text let split_coinbase_output = TransactionTemplate { inputs: &[(1, 0, 0, Default::default())], outputs: 2, fee: 0, ..Default::default() }; let txid = context.rpc_server.broadcast_tx(split_coinbase_output); context.mine_blocks(1); assert_eq!( context.index.list(OutPoint::new(txid, 0)).unwrap().unwrap(), List::Unspent(vec![(50 * COIN_VALUE, 75 * COIN_VALUE)]) ); assert_eq!( context.index.list(OutPoint::new(txid, 1)).unwrap().unwrap(), List::Unspent(vec![(75 * COIN_VALUE, 100 * COIN_VALUE)]) ); } #[test] fn list_merge_ranges_are_tracked_correctly() { let context = Context::builder().arg("--index-sats").build(); context.mine_blocks(2); let merge_coinbase_outputs = TransactionTemplate { inputs: &[(1, 0, 0, Default::default()), (2, 0, 0, Default::default())], fee: 0, ..Default::default() }; let txid = context.rpc_server.broadcast_tx(merge_coinbase_outputs); context.mine_blocks(1); assert_eq!( #45,017,568textuse {super::*, crate::wallet::Wallet}; #[derive(Debug, Parser)] pub(crate) struct Sats { #[arg( long, help = "Find satoshis listed in first column of tab-separated value file <TSV>." )] tsv: Option<PathBuf>, } #[derive(Serialize, Deserialize)] pub struct OutputTsv { pub sat: String, pub output: OutPoint, } #[derive(Serialize, Deserialize)] pub struct OutputRare { pub sat: Sat, pub output: OutPoint, pub offset: u64, pub rarity: Rarity, } impl Sats { pub(crate) fn run(&self, options: Options) -> SubcommandResult { let index = Index::open(&options)?; if !index.has_sat_index() { bail!("sats requires index created with `--index-sats` flag"); } index.update()?; let utxos = index.get_unspent_output_ranges(Wallet::load(&options)?)?; if let Some(path) = &self.tsv { let mut output = Vec::new(); for (outpoint, sat) in sats_from_tsv( utxos, &fs::read_to_string(path) .with_context(|| format!("I/O error reading `{}`", path.display()))?,#44,990,050text }; let unsigned_transaction = TransactionBuilder::new( satpoint, inscriptions, unspent_outputs, locked_outputs, address.clone(), change, self.fee_rate, postage, ) .build_transaction()?; let signed_tx = client .sign_raw_transaction_with_wallet(&unsigned_transaction, None, None)? .hex; let txid = client.send_raw_transaction(&signed_tx)?; Ok(Box::new(Output { transaction: txid })) } fn lock_inscriptions( client: &Client, inscriptions: BTreeMap<SatPoint, InscriptionId>, unspent_outputs: BTreeMap<bitcoin::OutPoint, bitcoin::Amount>, ) -> Result { let all_inscription_outputs = inscriptions .keys() .map(|satpoint| satpoint.outpoint) .collect::<HashSet<OutPoint>>(); let wallet_inscription_outputs = unspent_outputs .keys() .filter(|utxo| all_inscription_outputs.contains(utxo)) .cloned() .collect::<Vec<OutPoint>>(); if !client.lock_unspent(&wallet_inscription_outputs)? {#44,990,049text match config { SpawnConfig::Https(_) => "https", _ => "http", } ); } Ok(tokio::spawn(async move { match config { SpawnConfig::Https(acceptor) => { axum_server::Server::bind(addr) .handle(handle) .acceptor(acceptor) .serve(router.into_make_service()) .await } SpawnConfig::Redirect(destination) => { axum_server::Server::bind(addr) .handle(handle) .serve( Router::new() .fallback(Self::redirect_http_to_https) .layer(Extension(destination)) .into_make_service(), ) .await } SpawnConfig::Http => { axum_server::Server::bind(addr) .handle(handle) .serve(router.into_make_service()) .await } } })) } fn acme_cache(acme_cache: Option<&PathBuf>, options: &Options) -> Result<PathBuf> { #44,989,399text x = x * 26 + c as u64 - 'a' as u64 + 1; if x > Self::SUPPLY { bail!("sat name out of range"); } } _ => bail!("invalid character in sat name: {c}"), } } Ok(Sat(Self::SUPPLY - x)) } fn from_degree(degree: &str) -> Result<Self> { let (cycle_number, rest) = degree .split_once('°') .ok_or_else(|| anyhow!("missing degree symbol"))?; let cycle_number = cycle_number.parse::<u32>()?; let (epoch_offset, rest) = rest .split_once('′') .ok_or_else(|| anyhow!("missing minute symbol"))?; let epoch_offset = epoch_offset.parse::<u32>()?; if epoch_offset >= SUBSIDY_HALVING_INTERVAL { bail!("invalid epoch offset"); } let (period_offset, rest) = rest .split_once('″') .ok_or_else(|| anyhow!("missing second symbol"))?; let period_offset = period_offset.parse::<u32>()?; if period_offset >= DIFFCHANGE_INTERVAL { bail!("invalid period offset"); } let cycle_start_epoch = cycle_number * CYCLE_EPOCHS; #44,989,398textuse {super::*, fee_rate::FeeRate}; #[derive(Debug, Parser)] pub(crate) struct Preview { #[command(flatten)] server: super::server::Server, inscriptions: Vec<PathBuf>, } struct KillOnDrop(process::Child); impl Drop for KillOnDrop { fn drop(&mut self) { self.0.kill().unwrap() } } impl Preview { pub(crate) fn run(self) -> SubcommandResult { let tmpdir = TempDir::new()?; let rpc_port = TcpListener::bind("127.0.0.1:0")?.local_addr()?.port(); let bitcoin_data_dir = tmpdir.path().join("bitcoin"); fs::create_dir(&bitcoin_data_dir)?; let _bitcoind = KillOnDrop( Command::new("bitcoind") .arg({ let mut arg = OsString::from("-datadir="); arg.push(&bitcoin_data_dir); arg }) .arg("-regtest") .arg("-txindex") .arg("-listen=0") .arg(format!("-rpcport={rpc_port}")) .spawn() .context("failed to spawn `bitcoind`")?, ); let options = Options { chain_argument: Chain::Regtest, #44,989,397text </dd> .* </dl> .* " ), ); } #[test] fn charm_nineball() { let server = TestServer::new_with_regtest_with_index_sats(); server.mine_blocks(9); let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(9, 0, 0, inscription("text/plain", "foo").to_witness())], ..Default::default() }); let id = InscriptionId { txid, index: 0 }; server.mine_blocks(1); server.assert_response_regex( format!("/inscription/{id}"), StatusCode::OK, format!( ".*<h1>Inscription 0</h1>.* <dl> <dt>id</dt> <dd class=monospace>{id}</dd> <dt>charms</dt> <dd> <span title=uncommon>🌱</span> <span title=nineball>9️⃣</span> </dd> .* </dl> .* " ), ); } #[test] fn charm_reinscription() { let server = TestServer::new_with_regtest(); server.mine_blocks(1); server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(1, 0, 0, inscription("text/plain", "foo").to_witness())], #44,989,396text value: 0, }], lock_time: locktime::absolute::LockTime::ZERO, version: 0, }); match result { Ok(_) => panic!("expected error"), Err(Error::Script(_)) => {} Err(err) => panic!("unexpected error: {err}"), } } #[test] fn deciphering_runestone_with_invalid_varint_returns_varint_error() { let result = Runestone::decipher(&Transaction { input: Vec::new(), output: vec![TxOut { script_pubkey: script::Builder::new() .push_opcode(opcodes::all::OP_RETURN) .push_slice(b"RUNE_TEST") .push_slice([128]) .into_script(), value: 0, }], lock_time: locktime::absolute::LockTime::ZERO, version: 0, }); match result { Ok(_) => panic!("expected error"), Err(Error::Varint) => {} Err(err) => panic!("unexpected error: {err}"), } } #[test] fn non_push_opcodes_in_runestone_are_ignored() { assert_eq!( Runestone::decipher(&Transaction { input: Vec::new(), #44,986,956text where D: Deserializer<'de>, { Ok(DeserializeFromStr::deserialize(deserializer)?.0) } } impl FromStr for SatPoint { type Err = Error; fn from_str(s: &str) -> Result<Self, Self::Err> { let (outpoint, offset) = s .rsplit_once(':') .ok_or_else(|| anyhow!("invalid satpoint: {s}"))?; Ok(SatPoint { outpoint: outpoint.parse()?, offset: offset.parse()?, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn from_str_ok() { assert_eq!( "1111111111111111111111111111111111111111111111111111111111111111:1:1" .parse::<SatPoint>() .unwrap(), SatPoint { outpoint: "1111111111111111111111111111111111111111111111111111111111111111:1" .parse() .unwrap(), offset: 1, } ); } #[test] fn from_str_err() { "abc".parse::<SatPoint>().unwrap_err(); "abc:xyz".parse::<SatPoint>().unwrap_err(); "1111111111111111111111111111111111111111111111111111111111111111:1" .parse::<SatPoint>() #44,986,955textuse {super::*, std::num::TryFromIntError}; #[derive(Debug, PartialEq, Copy, Clone, Hash, Eq, Ord, PartialOrd)] pub(crate) struct RuneId { pub(crate) height: u32, pub(crate) index: u16, } impl TryFrom<u128> for RuneId { type Error = TryFromIntError; fn try_from(n: u128) -> Result<Self, Self::Error> { Ok(Self { height: u32::try_from(n >> 16)?, index: u16::try_from(n & 0xFFFF).unwrap(), }) } } impl From<RuneId> for u128 { fn from(id: RuneId) -> Self { u128::from(id.height) << 16 | u128::from(id.index) } } impl Display for RuneId { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "{}/{}", self.height, self.index,) } } impl FromStr for RuneId { type Err = crate::Error; fn from_str(s: &str) -> Result<Self, Self::Err> { let (height, index) = s .split_once('/') .ok_or_else(|| anyhow!("invalid rune ID: {s}"))?; Ok(Self { height: height.parse()?, index: index.parse()?, }) } } #[cfg(test)] mod tests { use super::*; #44,986,954text let curse = if inscription.payload.unrecognized_even_field { Some(Curse::UnrecognizedEvenField) } else if inscription.payload.duplicate_field { Some(Curse::DuplicateField) } else if inscription.payload.incomplete_field { Some(Curse::IncompleteField) } else if inscription.input != 0 { Some(Curse::NotInFirstInput) } else if inscription.offset != 0 { Some(Curse::NotAtOffsetZero) } else if inscription.payload.pointer.is_some() { Some(Curse::Pointer) } else if inscription.pushnum { Some(Curse::Pushnum) } else if let Some((id, count)) = inscribed_offset { if *count > 1 { Some(Curse::Reinscription) } else { let initial_inscription_sequence_number = self.id_to_sequence_number.get(id.store())?.unwrap().value(); let initial_inscription_is_cursed = InscriptionEntry::load( self .sequence_number_to_entry #44,986,547textdmt-mint.nat#42,522,321tapdmt-mint.nat#42,522,293tapdmt-mint.nat#42,522,276tapdmt-mint.nat#42,522,274tapdmt-mint.nat#42,522,255tap