diff --git a/src/api.rs b/src/api.rs index cbc92cad91..961ebbec85 100644 --- a/src/api.rs +++ b/src/api.rs @@ -89,6 +89,14 @@ pub struct ChildInscriptions { pub page: usize, } +#[derive(Serialize)] +pub struct AddressRecursive { + pub outputs: Vec, + pub inscriptions: Vec, + pub sat_balance: u64, + pub runes_balances: Vec<(SpacedRune, Decimal, Option)>, +} + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct Inscription { pub address: Option, diff --git a/src/subcommand/server.rs b/src/subcommand/server.rs index 79cb0bf86c..3ff936a72c 100644 --- a/src/subcommand/server.rs +++ b/src/subcommand/server.rs @@ -211,6 +211,7 @@ impl Server { get(Self::parents_paginated), ) .route("/preview/:inscription_id", get(Self::preview)) + .route("/r/address/:address", get(Self::address_recursive)) .route("/r/blockhash", get(Self::block_hash_json)) .route( "/r/blockhash/:height", @@ -862,6 +863,44 @@ impl Server { }) } + async fn address_recursive( + Extension(server_config): Extension>, + Extension(index): Extension>, + Path(address): Path>, + ) -> ServerResult { + task::block_in_place(|| { + if !index.has_address_index() { + return Err(ServerError::NotFound( + "this server has no address index".to_string(), + )); + } + + let address = address + .require_network(server_config.chain.network()) + .map_err(|err| ServerError::BadRequest(err.to_string()))?; + + let mut outputs = index.get_address_info(&address)?; + + outputs.sort(); + + let sat_balance = index.get_sat_balances_for_outputs(&outputs)?; + + let inscriptions = index.get_inscriptions_for_outputs(&outputs)?; + + let runes_balances = index.get_aggregated_rune_balances_for_outputs(&outputs)?; + + Ok( + Json(api::AddressRecursive { + outputs, + inscriptions, + sat_balance, + runes_balances, + }) + .into_response(), + ) + }) + } + async fn block( Extension(server_config): Extension>, Extension(index): Extension>, diff --git a/tests/server.rs b/tests/server.rs index d92e8104d8..a24d0b6b6f 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -63,6 +63,44 @@ fn address_page_shows_outputs_and_sat_balance() { ); } +#[test] +fn address_recursive_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 8.8 {address} 2btc")) + .core(&core) + .ord(&ord) + .run_and_deserialize_output::(); + + core.mine_blocks(1); + use serde_json::json; + + let response = ord.request(format!("/r/address/{address}")); + println!("Response: {:?}", response.text().unwrap()); + + let expected_json = json!({ + "outputs": [ OutPoint { + txid: send.txid, + vout: 0 + }], + "inscriptions": [], + "sat_balance": 200000000, + "runes_balances": [] + }) + .to_string(); + + ord.assert_response_regex( + format!("/r/address/{address}"), + regex::escape(&expected_json), + ); +} + #[test] fn address_page_shows_single_rune() { let core = mockcore::builder().network(Network::Regtest).build(); @@ -91,6 +129,33 @@ fn address_page_shows_single_rune() { format!(".*
.*{}.*: 1000¢
.*", Rune(RUNE)), ); } +#[test] +fn address_recursive_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 8.8 {address} 1000:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(6); + + let expected_regex = r#"(?s)\{"outputs":\["[^"]*"\],"inscriptions":\[\],"sat_balance":\d+,"runes_balances":\[\[\"AAAAAAAAAAAAA\",\"1000\",\"¢\"\]\]\}"#; + + ord.assert_response_regex(format!("/r/address/{address}"), expected_regex); +} #[test] fn address_page_shows_multiple_runes() { @@ -137,6 +202,45 @@ fn address_page_shows_multiple_runes() { ); } +#[test] +fn address_recursive_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 8.8 {address} 1000:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(6); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet send --fee-rate 8.8 {address} 1000:{}", + Rune(RUNE + 1) + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(6); + + let expected_regex = r#"(?s)\{"outputs":\["[^"]+:0","[^"]+:0"\],"inscriptions":\[\],"sat_balance":\d+,"runes_balances":\[\["AAAAAAAAAAAAA","1000","¢"\],\["AAAAAAAAAAAAB","1000","¢"\]\]\}"#; + ord.assert_response_regex(format!("/r/address/{address}"), expected_regex); +} + #[test] fn address_page_shows_aggregated_runes_balance() { let core = mockcore::builder().network(Network::Regtest).build(); @@ -177,6 +281,45 @@ fn address_page_shows_aggregated_runes_balance() { ); } +#[test] +fn address_recusive_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 8.8 {address} 250:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(6); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet send --fee-rate 8.8 {address} 250:{}", + Rune(RUNE) + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(6); + + let expected_regex = r#"(?s)\{"outputs":\["[^"]+:2","[^"]+:2"\],"inscriptions":\[\],"sat_balance":\d+,"runes_balances":\[\["AAAAAAAAAAAAA","500","¢"\]\]\}"#; + + ord.assert_response_regex(format!("/r/address/{address}"), expected_regex); +} + #[test] fn address_page_shows_aggregated_inscriptions() { let core = mockcore::builder().network(Network::Regtest).build(); @@ -224,6 +367,44 @@ fn address_page_shows_aggregated_inscriptions() { ); } +#[test] +fn address_recursive_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 8.8 {address} {inscription_id_1}", + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let (inscription_id_2, _reveal) = inscribe(&core, &ord); + + CommandBuilder::new(format!( + "--chain regtest --index-runes wallet send --fee-rate 8.8 {address} {inscription_id_2}", + )) + .core(&core) + .ord(&ord) + .stdout_regex(".*") + .run_and_deserialize_output::(); + + core.mine_blocks(1); + + let expected_regex = r#"(?s)\{"outputs":\["[^"]+:0","[^"]+:0"\],"inscriptions":\["[a-f0-9]{64}i\d","[a-f0-9]{64}i\d"\],"sat_balance":\d+,"runes_balances":\[\]\}"#; + ord.assert_response_regex(format!("/r/address/{address}"), expected_regex); +} + #[test] fn inscription_page() { let core = mockcore::spawn();