use {super::*, ciborium::value::Integer, ord::Properties, ord::subcommand::wallet::send::Output};
#[test]
fn run() {
let core = mockcore::spawn();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let builder =
CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}")).core(&core);
let mut command = builder.command();
let mut child = command.spawn().unwrap();
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/status"))
&& response.status() == 200
{
break;
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn address_page_shows_outputs_and_sat_balance() {
let core = mockcore::spawn();
let ord = TestServer::spawn_with_args(&core, &["--index-addresses"]);
create_wallet(&core, &ord);
core.mine_blocks(1);
let address = "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4";
let send = CommandBuilder::new(format!("wallet send --fee-rate 13.3 {address} 2btc"))
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Send>();
core.mine_blocks(1);
ord.assert_response_regex(
format!("/address/{address}"),
format!(
".*<h1>Address {address}</h1>.*<dd>200000000</dd>.*<a class=collapse href=/output/{}.*",
OutPoint {
txid: send.txid,
vout: 0
}
),
);
}
#[test]
fn address_page_shows_single_rune() {
let core = mockcore::builder().network(Network::Regtest).build();
let ord =
TestServer::spawn_with_args(&core, &["--index-runes", "--index-addresses", "--regtest"]);
create_wallet(&core, &ord);
etch(&core, &ord, Rune(RUNE));
let address = "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw";
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} 1000:{}",
Rune(RUNE)
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(6);
ord.assert_response_regex(
format!("/address/{address}"),
format!(".*<dd>.*{}.*: 1000¢</dd>.*", Rune(RUNE)),
);
}
#[test]
fn address_page_shows_multiple_runes() {
let core = mockcore::builder().network(Network::Regtest).build();
let ord =
TestServer::spawn_with_args(&core, &["--index-runes", "--index-addresses", "--regtest"]);
create_wallet(&core, &ord);
etch(&core, &ord, Rune(RUNE));
etch(&core, &ord, Rune(RUNE + 1));
let address = "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw";
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} 1000:{}",
Rune(RUNE)
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(6);
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} 1000:{}",
Rune(RUNE + 1)
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(6);
ord.assert_response_regex(
format!("/address/{address}"),
format!(
".*<dd>.*{}.*: 1000¢</dd>.*<dd>.*{}.*: 1000¢</dd>.*",
Rune(RUNE),
Rune(RUNE + 1)
),
);
}
#[test]
fn address_page_shows_aggregated_runes_balance() {
let core = mockcore::builder().network(Network::Regtest).build();
let ord =
TestServer::spawn_with_args(&core, &["--index-runes", "--index-addresses", "--regtest"]);
create_wallet(&core, &ord);
etch(&core, &ord, Rune(RUNE));
let address = "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw";
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} 250:{}",
Rune(RUNE)
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(6);
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} 250:{}",
Rune(RUNE)
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(6);
ord.assert_response_regex(
format!("/address/{address}"),
format!(".*<dd>.*{}.*: 500¢</dd>.*", Rune(RUNE)),
);
}
#[test]
fn address_page_shows_aggregated_inscriptions() {
let core = mockcore::builder().network(Network::Regtest).build();
let ord =
TestServer::spawn_with_args(&core, &["--index-runes", "--index-addresses", "--regtest"]);
create_wallet(&core, &ord);
let (inscription_id_1, _reveal) = inscribe(&core, &ord);
let address = "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw";
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} {inscription_id_1}",
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(1);
let (inscription_id_2, _reveal) = inscribe(&core, &ord);
CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} {inscription_id_2}",
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();
core.mine_blocks(1);
ord.assert_response_regex(
format!("/address/{address}"),
r".*
<dl>.*
<dt>inscriptions</dt>
<dd class=thumbnails>
<a href=/inscription/[[:xdigit:]]{64}i\d><iframe .* src=/preview/[[:xdigit:]]{64}i\d></iframe></a>
<a href=/inscription/[[:xdigit:]]{64}i\d><iframe .* src=/preview/[[:xdigit:]]{64}i\d></iframe></a>
</dd>.*"
,
);
}
#[test]
fn inscription_page() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (inscription, reveal) = inscribe(&core, &ord);
let response = ord.json_request(format!(
"/output/{}",
OutPoint {
txid: reveal,
vout: 0
}
));
assert_eq!(response.status(), StatusCode::OK);
let output: api::Output = serde_json::from_str(&response.text().unwrap()).unwrap();
TestServer::spawn_with_args(&core, &[]).assert_html(
format!("/inscription/{inscription}"),
Chain::Mainnet,
InscriptionHtml {
chain: Chain::Mainnet,
charms: 0,
child_count: 0,
children: Vec::new(),
fee: 138,
height: 2,
inscription: Inscription {
content_type: Some("text/plain;charset=utf-8".as_bytes().into()),
body: Some("foo".as_bytes().into()),
..default()
},
id: inscription,
number: 0,
next: None,
output: Some(TxOut {
value: Amount::from_sat(10000),
script_pubkey: output.script_pubkey,
}),
properties: Properties::default(),
parents: Vec::new(),
previous: None,
rune: None,
sat: None,
satpoint: SatPoint {
outpoint: OutPoint {
txid: reveal,
vout: 0,
},
offset: 0,
},
timestamp: "1970-01-01 00:00:02+00:00"
.parse::<DateTime<Utc>>()
.unwrap(),
},
);
}
#[test]
fn inscription_appears_on_reveal_transaction_page() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (_, reveal) = inscribe(&core, &ord);
core.mine_blocks(1);
TestServer::spawn_with_args(&core, &[]).assert_response_regex(
format!("/tx/{reveal}"),
format!(".*<h1>Transaction .*</h1>.*<a href=/inscription/{reveal}.*"),
);
}
#[test]
fn multiple_inscriptions_appear_on_reveal_transaction_page() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
core.mine_blocks(1);
let output = CommandBuilder::new("wallet batch --batch batch.yaml --fee-rate 55")
.write("inscription.txt", "Hello World")
.write("meow.wav", [0; 2048])
.write(
"batch.yaml",
"mode: shared-output\ninscriptions:\n- file: inscription.txt\n- file: meow.wav\n",
)
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Batch>();
core.mine_blocks(1);
let id0 = output.inscriptions[0].id;
let id1 = output.inscriptions[1].id;
let reveal = output.reveal;
ord.assert_response_regex(
format!("/tx/{reveal}"),
format!(".*<h1>Transaction .*</h1>.*<a href=/inscription/{id0}.*<a href=/inscription/{id1}.*"),
);
}
#[test]
fn inscription_appears_on_output_page() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (inscription, reveal) = inscribe(&core, &ord);
core.mine_blocks(1);
ord.assert_response_regex(
format!("/output/{reveal}:0"),
format!(".*<h1>Output <span class=monospace>{reveal}:0</span></h1>.*<a href=/inscription/{inscription}.*"),
);
}
#[test]
fn inscription_page_after_send() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (inscription, reveal) = inscribe(&core, &ord);
core.mine_blocks(1);
ord.assert_response_regex(
format!("/inscription/{inscription}"),
format!(
r".*<h1>Inscription 0</h1>.*<dt>location</dt>\s*<dd><a class=collapse href=/satpoint/{reveal}:0:0>{reveal}:0:0</a></dd>.*",
),
);
let txid = CommandBuilder::new(format!(
"wallet send --fee-rate 1 bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv {inscription}"
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>()
.txid;
core.mine_blocks(1);
ord.assert_response_regex(
format!("/inscription/{inscription}"),
format!(
r".*<h1>Inscription 0</h1>.*<dt>address</dt>\s*<dd><a class=collapse href=/address/bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv>bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv</a></dd>.*<dt>location</dt>\s*<dd><a class=collapse href=/satpoint/{txid}:0:0>{txid}:0:0</a></dd>.*",
),
)
}
#[test]
fn inscription_content() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (inscription, _) = inscribe(&core, &ord);
core.mine_blocks(1);
let response = ord.request(format!("/content/{inscription}"));
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain;charset=utf-8"
);
assert_eq!(
response
.headers()
.get_all("content-security-policy")
.into_iter()
.collect::<Vec<&reqwest::header::HeaderValue>>(),
&[
"default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
"default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:",
]
);
assert_eq!(response.bytes().unwrap(), "FOO");
}
#[test]
fn inscription_metadata() {
let metadata = r#"{"foo":"bar","baz":1}"#;
let mut encoded_metadata = Vec::new();
let cbor_map = ciborium::value::Value::Map(vec![
(
ciborium::value::Value::Text("foo".into()),
ciborium::value::Value::Text("bar".into()),
),
(
ciborium::value::Value::Text("baz".into()),
ciborium::value::Value::Integer(Integer::from(1)),
),
]);
ciborium::ser::into_writer(&cbor_map, &mut encoded_metadata).unwrap();
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
core.mine_blocks(1);
let inscription_id = CommandBuilder::new(
"wallet inscribe --fee-rate 1 --json-metadata metadata.json --file foo.txt",
)
.write("foo.txt", "FOO")
.write("metadata.json", metadata)
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Batch>()
.inscriptions
.first()
.unwrap()
.id;
core.mine_blocks(1);
let response = ord.request(format!("/r/metadata/{inscription_id}"));
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/json"
);
assert_eq!(
response.text().unwrap(),
format!("\"{}\"", hex::encode(encoded_metadata))
);
}
#[test]
fn recursive_inscription_endpoint() {
let core = mockcore::spawn();
let ord = TestServer::spawn_with_server_args(&core, &["--index-sats"], &[]);
create_wallet(&core, &ord);
core.mine_blocks(1);
let output = CommandBuilder::new("wallet inscribe --fee-rate 1 --file foo.txt")
.write("foo.txt", "FOO")
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Batch>();
core.mine_blocks(1);
let inscription = output.inscriptions.first().unwrap();
let response = ord.request(format!("/r/inscription/{}", inscription.id));
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"application/json"
);
let mut inscription_recursive_json: api::InscriptionRecursive =
serde_json::from_str(&response.text().unwrap()).unwrap();
assert_regex_match!(inscription_recursive_json.address.unwrap(), r"bc1p.*");
inscription_recursive_json.address = None;
pretty_assert_eq!(
inscription_recursive_json,
api::InscriptionRecursive {
charms: vec![Charm::Coin, Charm::Uncommon],
content_type: Some("text/plain;charset=utf-8".to_string()),
content_length: Some(3),
delegate: None,
fee: 138,
height: 2,
id: inscription.id,
number: 0,
output: inscription.location.outpoint,
sat: Some(Sat(50 * COIN_VALUE)),
satpoint: SatPoint {
outpoint: inscription.location.outpoint,
offset: 0,
},
timestamp: 2,
value: Some(10000),
address: None,
}
)
}
#[test]
fn inscriptions_page() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (inscription, _) = inscribe(&core, &ord);
ord.assert_response_regex(
"/inscriptions",
format!(
".*<h1>All Inscriptions</h1>
<div class=thumbnails>
<a href=/inscription/{inscription}>.*</a>
</div>
.*",
),
);
}
#[test]
fn inscriptions_page_is_sorted() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let mut regex = String::new();
for _ in 0..8 {
let (inscription, _) = inscribe(&core, &ord);
regex.insert_str(0, &format!(".*<a href=/inscription/{inscription}>.*"));
}
ord.assert_response_regex("/inscriptions", ®ex);
}
#[test]
fn inscriptions_page_has_next_and_previous() {
let core = mockcore::spawn();
let ord = TestServer::spawn(&core);
create_wallet(&core, &ord);
let (a, _) = inscribe(&core, &ord);
let (b, _) = inscribe(&core, &ord);
let (c, _) = inscribe(&core, &ord);
ord.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 core = mockcore::spawn();
TestServer::spawn_with_args(&core, &[]).assert_response_regex(
"/sat/2099999997689999",
r".*<dt>timestamp</dt><dd><time>.* \d+:\d+:\d+ UTC</time> \(expected\)</dd>.*",
);
}
#[test]
fn missing_credentials() {
let core = mockcore::spawn();
CommandBuilder::new("--bitcoin-rpc-username foo server")
.core(&core)
.expected_exit_code(1)
.expected_stderr("error: no bitcoin RPC password specified\n")
.run_and_extract_stdout();
CommandBuilder::new("--bitcoin-rpc-password bar server")
.core(&core)
.expected_exit_code(1)
.expected_stderr("error: no bitcoin RPC username specified\n")
.run_and_extract_stdout();
}
#[test]
fn all_endpoints_in_recursive_directory_return_json() {
let core = mockcore::spawn();
core.mine_blocks(2);
let ord_server = TestServer::spawn_with_args(&core, &[]);
assert_eq!(
ord_server.request("/r/blockheight").json::<u64>().unwrap(),
2
);
assert_eq!(ord_server.request("/r/blocktime").json::<u64>().unwrap(), 2);
assert_eq!(
ord_server.request("/r/blockhash").json::<String>().unwrap(),
"70a93647a8d559c7e7ff2df9bd875f5b726a2ff8ca3562003d257df5a4c47ae2"
);
assert_eq!(
ord_server
.request("/r/blockhash/0")
.json::<String>()
.unwrap(),
"000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
);
assert!(ord_server.request("/blockhash").json::<String>().is_err());
assert!(ord_server.request("/blockhash/2").json::<String>().is_err());
}
#[test]
fn sat_recursive_endpoints_without_sat_index_return_404() {
let core = mockcore::spawn();
core.mine_blocks(1);
let server = TestServer::spawn_with_args(&core, &[""]);
assert_eq!(
server.request("/r/sat/5000000000").status(),
StatusCode::NOT_FOUND,
);
assert_eq!(
server.request("/r/sat/5000000000/at/1").status(),
StatusCode::NOT_FOUND,
);
}
#[test]
fn inscription_transactions_are_stored_with_transaction_index() {
let core = mockcore::spawn();
let ord = TestServer::spawn_with_server_args(&core, &["--index-transactions"], &[]);
create_wallet(&core, &ord);
let (_inscription, reveal) = inscribe(&core, &ord);
let coinbase = core.tx(1, 0).compute_txid();
assert_eq!(
ord.request(format!("/tx/{reveal}")).status(),
StatusCode::OK,
);
assert_eq!(
ord.request(format!("/tx/{coinbase}")).status(),
StatusCode::OK,
);
core.clear_state();
assert_eq!(
ord.request(format!("/tx/{reveal}")).status(),
StatusCode::OK,
);
assert_eq!(
ord.request(format!("/tx/{coinbase}")).status(),
StatusCode::NOT_FOUND,
);
}
#[test]
fn run_no_sync() {
let core = mockcore::spawn();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let tempdir = Arc::new(TempDir::new().unwrap());
let builder = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}",))
.core(&core)
.temp_dir(tempdir.clone());
let mut command = builder.command();
let mut child = command.spawn().unwrap();
core.mine_blocks(1);
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockheight"))
&& response.status() == 200
{
assert_eq!(response.text().unwrap(), "1");
break;
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
child.kill().unwrap();
child.wait().unwrap();
let builder = CommandBuilder::new(format!(
"server --no-sync --address 127.0.0.1 --http-port {port}",
))
.core(&core)
.temp_dir(tempdir);
let mut command = builder.command();
let mut child = command.spawn().unwrap();
core.mine_blocks(2);
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockheight"))
&& response.status() == 200
{
assert_eq!(response.text().unwrap(), "1");
break;
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn authentication() {
let core = mockcore::spawn();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let builder = CommandBuilder::new(format!(
" --server-username foo --server-password bar server --address 127.0.0.1 --http-port {port}"
))
.core(&core);
let mut command = builder.command();
let mut child = command.spawn().unwrap();
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}"))
&& response.status() == 401
{
break;
}
if attempt == 100 {
panic!("Server did not respond");
}
thread::sleep(Duration::from_millis(50));
}
let response = reqwest::blocking::Client::new()
.get(format!("http://localhost:{port}"))
.basic_auth("foo", Some("bar"))
.send()
.unwrap();
assert_eq!(response.status(), 200);
child.kill().unwrap();
child.wait().unwrap();
}
#[cfg(unix)]
#[test]
fn ctrl_c() {
use nix::{
sys::signal::{self, Signal},
unistd::Pid,
};
let core = mockcore::spawn();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let tempdir = Arc::new(TempDir::new().unwrap());
core.mine_blocks(3);
let mut spawn = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}"))
.temp_dir(tempdir.clone())
.core(&core)
.spawn();
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockcount"))
&& (response.status() == 200 || response.text().unwrap() == *"3")
{
break;
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
signal::kill(
Pid::from_raw(spawn.child.id().try_into().unwrap()),
Signal::SIGINT,
)
.unwrap();
let mut buffer = String::new();
BufReader::new(spawn.child.stderr.as_mut().unwrap())
.read_line(&mut buffer)
.unwrap();
assert_eq!(
buffer,
"Shutting down gracefully. Press <CTRL-C> again to shutdown immediately.\n"
);
spawn.child.wait().unwrap();
CommandBuilder::new(format!(
"server --no-sync --address 127.0.0.1 --http-port {port}"
))
.temp_dir(tempdir)
.core(&core)
.spawn();
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/blockcount"))
&& (response.status() == 200 || response.text().unwrap() == *"3")
{
break;
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
}