[Unit] After=network.target Description=Ord server StartLimitBurst=120 StartLimitIntervalSec=10m [Service] AmbientCapabilities=CAP_NET_BIND_SERVICE Environment=RUST_BACKTRACE=1 Environment=RUST_LOG=info ExecStart=/usr/local/bin/ord \ --bitcoin-data-dir /var/lib/bitcoind \ --data-dir /var/lib/ord \ --config-dir /var/lib/ord \ --chain ${CHAIN} \ --index-sats \ server \ --acme-contact mailto:[email protected] \ --http \ --https Group=ord LimitNOFILE=65536 MemoryDenyWriteExecute=true NoNewPrivileges=true PrivateDevices=true PrivateTmp=true ProtectHome=true ProtectSystem=full Restart=on-failure RestartSec=5s StateDirectory=ord StateDirectoryMode=0700 TimeoutStopSec=10m Type=simple User=ord WorkingDirectory=/var/lib/ord [Install] WantedBy=multi-user.target #44,993,089text### Fixed - Respect locked coins (#2618) - Set `Cache-Control: no-store` header on 404 responses (#2637) - Fix statistics table and increment schema version (#2624) - Fix broken link in README (#2621) - Speed up indexing of re-inscriptions (#2608) - Fix docs rendering (#2612) - Update docs with new position of --enable-json-api (#2601) - Move `--enable-json-api` flag to server options (#2599) - Make server_runs_with_rpc_user_and_pass_as_env_vars test less flaky (#2580) ### Runes - Implement open etchings (#2548) - Add more info to /rune page and link to rune from /tx (#2528) - Display inscription on /rune (#2542) - Add rune numbers (#2557) - Ignore non push opcodes in runestones (#2553) - Improve rune minimum at height (#2546) [0.10.0](https://github.com/ordinals/ord/releases/tag/0.10.0) - 2023-10-23 -------------------------------------------------------------------------- ### Added - Batch inscriptions (#2504) - Add teleburn command to generate Ethereum teleburn addresses (#1680) - Add Korean version of handbook (#2560)#44,993,088text- Add --version flag (#236) - Bump version: 0.0.2 → 0.0.3 (#234) - Change deploy target in recipe (#232) - Use default port and set ambient capabilities in ord service (#230) - Test deploy on vagrant (#229) - Update slide deck (#227) - Add link to video (#226) - Separate deck pages (#225) - Fix docs HTML (#224) - Add side deck (#223) - Change slot notation to AxBxCxD (#222) - Improve NFT encoding (#221) - Remove use of sha256d in signature algorithm (#219) - Use standard formats (#218) - Use CBOR for serialization/deserialization (#217) - Add nix flake (#214) - Build binaries for releases (#213) [0.0.1](https://github.com/ordinals/ord/releases/tag/0.0.1) - 2022-06-05 --------------------------------------------------------------------- - Add commands to mint and verify NFTs (#211) - Add legendary sat location hints (#208) - Re-implement find (#206) - Add explanation to bounty page (#205) - Change bounty dir to bounties (#204) - Add ordinal bounty page (#203) - Add drawbacks section to BIP (#202) - Remove log spam (#200) #44,993,087text- Add links to homepage (#335) - Styling (#333) - Remove fluff from BIP (#336) - Remove old comment from bitcoind.service (#334) - Add viewport meta tag (#332) - Add rarity colors (#330) - Don't let ordinals become telephone numbers (#331) - Add next and prev links to /ordinal (#329) - Fix broken link (#328) - Add header to /range (#325) - Fix off by one bug in index::blocks (#326) - Add header to /output (#324) - Limit blocks (#320) - Add header to /tx (#322) - Add header to /block/HASH (#321) - Convert / to boilerplate template (#317) - Return BlockHash from Index::all (#319) - Don't warn about installing bitcoind in deploy/setup (#318) - Improvements (#298) - Update rust toolchain when deploying (#311) - Fix forbidden word check (#313) - Don't run integration tests on MacOS CI (#316) - Disable redb checksums (#315) - Pay a fixed fee when sending transactions (#314) - Refactor duplicate blockchain code in purse (#312) - Add `ord wallet send` (#305) - Add wallet balance subcommand (#271) - Add wallet utxos subcommand (#259)#44,993,086textchecksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys 0.48.0", ] [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" dependencies = [ "strum_macros", ] [[package]] name = "strum_macros" version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index"#44,993,085text- Use bitcoin core node for integration tests (#263) - List transaction outputs (#292) - Add `/output/:outpoint` endpoint (#293) - Add /range/:start/:end endpoint (#291) - Move /list endpoint to /api/list (#288) - List block transactions at `/block/:hash` (#286) - Display ordinals at `/ordinal/:ordinal` (#287) - Wait for bitcoind and ord to become available (#285) - List blocks on root page (#276) - Remove user-facing list page (#275) - Add network option (#274) - Serve HTTPS with ACME certs (#256) - Remove unused functionality (#270) - Revise homepage (#268) - Link to blog post (#267) - Use hour, minute, second, and third terminology (#262) - Allow passing ordinals in degree and decimal notation (#261) - Update dependencies (#258) - Make genesis sat mythic (#260) - Add wallet (#233) - Overhaul traits (#255) - Clarify duplicate transaction rule in BIP (#254) - Purge LMDB (#231) - Add justfile with commands for moving ordinals around manually (#238) - Add links to discord server (#237) - Make `nft verify` take input as argument (#235) #44,993,084textname = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "770d968249b5d99410d61f5bf89057f3199a077a04d087092f58e7d10692baae" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" dependencies = [ "event-listener 4.0.0", "pin-project-lite", ] [[package]] name = "executable-path" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ebc5a6d89e3c90b84e8f33c8737933dda8f1c106b5415900b38b9d433841478" [[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index"#44,993,083textuse super::*; #[derive(Debug)] pub(crate) enum Expected { String(String), Regex(Regex), } impl Expected { pub(crate) fn regex(pattern: &str) -> Self { Self::Regex(Regex::new(&format!("^(?s){pattern}$")).unwrap()) } pub(crate) fn assert_match(&self, output: &str) { match self { Self::String(string) => pretty_assert_eq!(output, string), Self::Regex(regex) => assert!( regex.is_match(output), "regex:\n{regex}\ndid not match output:\n{output}", ), } } } #44,986,643text #[test] fn get_inscriptions_in_block() { let rpc_server = test_bitcoincore_rpc::spawn(); create_wallet(&rpc_server); rpc_server.mine_blocks(10); let envelope = envelope(&[b"ord", &[1], b"text/plain;charset=utf-8", &[], b"bar"]); let txid = rpc_server.broadcast_tx(TransactionTemplate { inputs: &[ (1, 0, 0, envelope.clone()), (2, 0, 0, envelope.clone()), (3, 0, 0, envelope.clone()), ], ..Default::default() }); rpc_server.mine_blocks(1); let _ = rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(4, 0, 0, envelope.clone()), (5, 0, 0, envelope.clone())], ..Default::default() }); rpc_server.mine_blocks(1); let _ = rpc_server.broadcast_tx(TransactionTemplate { inputs: &[(6, 0, 0, envelope.clone())], ..Default::default() }); rpc_server.mine_blocks(1); let server = TestServer::spawn_with_server_args( &rpc_server, &["--index-sats", "--first-inscription-height", "0"], &["--enable-json-api"], ); // get all inscriptions from block 11 #44,986,641text confirmations: 0, difficulty: 0.0, hash: block_hash, height, median_time: None, merkle_root: TxMerkleNode::all_zeros(), n_tx: 0, next_block_hash: None, nonce: 0, previous_block_hash: None, time: 0, version: Version::ONE, version_hex: Some(vec![0, 0, 0, 0]), }) .unwrap(), ) } else { match self.state().blocks.get(&block_hash) { Some(block) => Ok(serde_json::to_value(hex::encode(serialize(&block.header))).unwrap()), None => Err(Self::not_found()), } } } fn get_block( &self, block_hash: BlockHash, verbosity: u64, ) -> Result<String, jsonrpc_core::Error> { assert_eq!(verbosity, 0, "Verbosity level {verbosity} is unsupported"); match self.state().blocks.get(&block_hash) { Some(block) => Ok(hex::encode(serialize(block))), None => Err(Self::not_found()), } } fn get_block_count(&self) -> Result<u64, jsonrpc_core::Error> { #44,986,640text} #[test] fn inscriptions_page_has_next_and_previous() { let rpc_server = test_bitcoincore_rpc::spawn(); create_wallet(&rpc_server); let (a, _) = inscribe(&rpc_server); let (b, _) = inscribe(&rpc_server); let (c, _) = inscribe(&rpc_server); TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( format!("/inscription/{b}"), format!( ".*<h1>Inscription 1</h1>.* <div class=inscription> <a class=prev href=/inscription/{a}>❮</a> <iframe .* src=/preview/{b}></iframe> <a class=next href=/inscription/{c}>❯</a> </div>.*", ), ); } #[test] fn expected_sat_time_is_rounded() { let rpc_server = test_bitcoincore_rpc::spawn(); TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( "/sat/2099999997689999", r".*<dt>timestamp</dt><dd><time>.* \d+:\d+:\d+ UTC</time> \(expected\)</dd>.*", ); } #[test] #[ignore] fn server_runs_with_rpc_user_and_pass_as_env_vars() { let rpc_server = test_bitcoincore_rpc::spawn(); rpc_server.mine_blocks(1); let tempdir = TempDir::new().unwrap(); #44,986,639text rpc_server.mine_blocks(1); assert_eq!(ord.wait_with_output().unwrap().status.code(), Some(0)); } #[test] fn export_inscription_number_to_id_tsv() { let rpc_server = test_bitcoincore_rpc::spawn(); let temp_dir = TempDir::new().unwrap(); create_wallet(&rpc_server); inscribe(&rpc_server); inscribe(&rpc_server); let (inscription, _) = inscribe(&rpc_server); rpc_server.mine_blocks(1); let tsv = CommandBuilder::new("index export --tsv foo.tsv") .rpc_server(&rpc_server) .temp_dir(temp_dir) .stdout_regex(r"\{\}\n") .run_and_extract_file("foo.tsv"); let entries: std::collections::BTreeMap<i64, ord::Object> = tsv .lines() .filter(|line| !line.is_empty() && !line.starts_with('#')) .map(|line| { let value = line.split('\t').collect::<Vec<&str>>(); let inscription_number = i64::from_str(value[0]).unwrap(); let inscription_id = ord::Object::from_str(value[1]).unwrap(); (inscription_number, inscription_id) }) .collect(); assert_eq!( #44,986,638text rpc_server.mine_blocks(1); let response = TestServer::spawn_with_server_args(&rpc_server, &[], &["--enable-json-api"]) .json_request("/block/0"); assert_eq!(response.status(), StatusCode::OK); let block_json: BlockJson = serde_json::from_str(&response.text().unwrap()).unwrap(); assert_eq!( block_json, BlockJson { hash: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" .parse::<BlockHash>() .unwrap(), target: "00000000ffff0000000000000000000000000000000000000000000000000000" .parse::<BlockHash>() .unwrap(), best_height: 1, height: 0, inscriptions: vec![], } ); } #44,986,637text ..self } } pub(crate) fn expected_exit_code(self, expected_exit_code: i32) -> Self { Self { expected_exit_code, ..self } } pub(crate) fn temp_dir(self, tempdir: TempDir) -> Self { Self { tempdir, ..self } } pub(crate) fn command(&self) -> Command { let mut command = Command::new(executable_path("ord")); if let Some(rpc_server_url) = &self.rpc_server_url { let cookiefile = self.tempdir.path().join("cookie"); fs::write(&cookiefile, "username:password").unwrap(); command.args([ "--rpc-url", rpc_server_url, "--cookie-file", cookiefile.to_str().unwrap(), ]); } command .env("ORD_INTEGRATION_TEST", "1") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .current_dir(&self.tempdir) .arg("--data-dir") .arg(self.tempdir.path()) .args(&self.args); command } fn run(self) -> (TempDir, String) { let child = self.command().spawn().unwrap(); #44,986,636text let ethereum_teleburn_address = CommandBuilder::new(format!("teleburn {inscription}")) .rpc_server(&rpc_server) .run_and_deserialize_output::<ord::subcommand::teleburn::Output>() .ethereum; TestServer::spawn_with_args(&rpc_server, &[]).assert_response_regex( format!("/inscription/{inscription}"), format!( ".*<meta property=og:title content='Inscription 0'>.* .*<meta property=og:image content='https://.*/favicon.png'>.* .*<meta property=twitter:card content=summary>.* <h1>Inscription 0</h1> .*<iframe .* src=/preview/{inscription}></iframe>.* <dl> <dt>id</dt> <dd class=monospace>{inscription}</dd> <dt>address</dt> <dd class=monospace>bc1.*</dd> <dt>output value</dt> <dd>10000</dd> <dt>preview</dt> <dd><a href=/preview/{inscription}>link</a></dd> <dt>content</dt> <dd><a href=/content/{inscription}>link</a></dd> <dt>content length</dt> <dd>3 bytes</dd> <dt>content type</dt> <dd>text/plain;charset=utf-8</dd> <dt>timestamp</dt> <dd><time>1970-01-01 00:00:02 UTC</time></dd> #44,986,635text tempdir.path().display(), tempdir.path().display(), ord_args.join(" "), server_args.join(" "), ).to_args()) .env("ORD_INTEGRATION_TEST", "1") .current_dir(&tempdir) .spawn().unwrap(); for i in 0.. { match reqwest::blocking::get(format!("http://127.0.0.1:{port}/status")) { Ok(_) => break, Err(err) => { if i == 400 { panic!("Server failed to start: {err}"); } } } thread::sleep(Duration::from_millis(25)); } Self { child, tempdir, port, rpc_url: rpc_server.url(), } } pub(crate) fn url(&self) -> Url { format!("http://127.0.0.1:{}", self.port).parse().unwrap() } pub(crate) fn assert_response_regex(&self, path: impl AsRef<str>, regex: impl AsRef<str>) { self.sync_server(); 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()); #44,986,634text local_relay: false, time_offset: 0, connections: 0, connections_in: None, connections_out: None, network_active: true, networks: Vec::new(), relay_fee: Amount::from_sat(0), incremental_fee: Amount::from_sat(0), local_addresses: Vec::new(), warnings: String::new(), }) } fn get_block_hash(&self, height: usize) -> Result<BlockHash, jsonrpc_core::Error> { match self.state().hashes.get(height) { Some(block_hash) => Ok(*block_hash), None => Err(Self::not_found()), } } fn get_block_header( &self, block_hash: BlockHash, verbose: bool, ) -> Result<Value, jsonrpc_core::Error> { if verbose { let height = match self .state() .hashes .iter() .position(|hash| *hash == block_hash) { Some(height) => height, None => return Err(Self::not_found()), }; Ok( serde_json::to_value(GetBlockHeaderResult { bits: String::new(), chainwork: Vec::new(), #44,986,633textdmt-mint.magic#44,743,717tapdmt-mint.magic#44,741,400tapdmt-mint.magic#44,740,697tapdmt-mint.magic#44,740,691tapdmt-mint.magic#44,740,666tapdmt-mint.magic#44,739,942tapdmt-mint.magic#44,739,855tapdmt-mint.magic#44,739,854tapdmt-mint.magic#44,739,853tapdmt-mint.magic#44,739,841tapdmt-mint.magic#44,739,782tapdmt-mint.magic#44,739,727tap