Skip to content

Commit

Permalink
Display aggregated rune balances in address page (#3831)
Browse files Browse the repository at this point in the history
  • Loading branch information
yoitsyoung committed Jun 29, 2024
1 parent 41cb05c commit 10d49a0
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 24 deletions.
34 changes: 34 additions & 0 deletions src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,40 @@ impl Index {
.collect()
}

pub(crate) fn get_aggregated_rune_balances_for_outputs(
&self,
outputs: &Vec<OutPoint>,
) -> Result<Vec<(SpacedRune, Decimal, Option<char>)>> {
let mut runes = BTreeMap::new();

for output in outputs {
let rune_balances = self.get_rune_balances_for_output(*output)?;

for (spaced_rune, pile) in rune_balances {
runes
.entry(spaced_rune)
.and_modify(|(decimal, _symbol): &mut (Decimal, Option<char>)| {
assert_eq!(decimal.scale, pile.divisibility);
decimal.value += pile.amount;
})
.or_insert((
Decimal {
value: pile.amount,
scale: pile.divisibility,
},
pile.symbol,
));
}
}

Ok(
runes
.into_iter()
.map(|(spaced_rune, (decimal, symbol))| (spaced_rune, decimal, symbol))
.collect(),
)
}

pub(crate) fn get_sat_balances_for_outputs(&self, outputs: &Vec<OutPoint>) -> Result<u64> {
let outpoint_to_txout = self.database.begin_read()?.open_table(OUTPOINT_TO_TXOUT)?;

Expand Down
3 changes: 3 additions & 0 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -864,13 +864,16 @@ impl Server {

let sat_balance = index.get_sat_balances_for_outputs(&outputs)?;

let runes_balances = index.get_aggregated_rune_balances_for_outputs(&outputs)?;

Ok(if accept_json {
Json(outputs).into_response()
} else {
AddressHtml {
address,
outputs,
sat_balance,
runes_balances,
}
.page(server_config)
.into_response()
Expand Down
84 changes: 60 additions & 24 deletions src/templates/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) struct AddressHtml {
pub(crate) address: Address,
pub(crate) outputs: Vec<OutPoint>,
pub(crate) sat_balance: u64,
pub(crate) runes_balances: Vec<(SpacedRune, Decimal, Option<char>)>,
}

impl PageContent for AddressHtml {
Expand All @@ -17,32 +18,67 @@ impl PageContent for AddressHtml {
mod tests {
use super::*;

#[test]
fn display() {
assert_regex_match!(
AddressHtml {
address: Address::from_str(
"bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8"
)
fn setup() -> AddressHtml {
AddressHtml {
address: Address::from_str("bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8")
.unwrap()
.require_network(Network::Bitcoin)
.unwrap(),
outputs: vec![outpoint(1), outpoint(2)],
sat_balance: 99,
},
"<h1>Address bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8</h1>
<dl>
<dt>sat balance</dt>
<dd>99</dd>
<dt>outputs</dt>
<dd>
<ul>
<li><a class=monospace href=/output/1{64}:1>1{64}:1</a></li>
<li><a class=monospace href=/output/2{64}:2>2{64}:2</a></li>
</ul>
</dd>
</dl>.*"
.unindent()
);
outputs: vec![outpoint(1), outpoint(2)],
sat_balance: 99,
runes_balances: vec![
(
SpacedRune {
rune: Rune::from_str("TEEEEEEEEESTRUNE").unwrap(),
spacers: 0,
},
Decimal {
scale: 0,
value: 20000,
},
Some('R'),
),
(
SpacedRune {
rune: Rune::from_str("ANOTHERTEESTRUNE").unwrap(),
spacers: 0,
},
Decimal {
scale: 0,
value: 10000,
},
Some('F'),
),
],
}
}

#[test]
fn test_address_rendering() {
let address_html = setup();
let expected_pattern =
r#".*<h1>Address bc1phuq0vkls6w926zdaem6x9n02z2gg7j2xfudgwddyey7uyquarlgsh40ev8</h1>.*"#;
assert_regex_match!(address_html, expected_pattern);
}

#[test]
fn test_sat_balance_rendering() {
let address_html = setup();
let expected_pattern = r#".*<dt>sat balance</dt>\n\s*<dd>99</dd>.*"#;
assert_regex_match!(address_html, expected_pattern);
}

#[test]
fn test_runes_balances_rendering() {
let address_html = setup();
let expected_pattern = r#".*<dt>runes balances</dt>\n\s*<dd><a class=monospace href=/rune/TEEEEEEEEESTRUNE>TEEEEEEEEESTRUNE</a>: 20000R</dd>\n\s*<dd><a class=monospace href=/rune/ANOTHERTEESTRUNE>ANOTHERTEESTRUNE</a>: 10000F</dd>.*"#;
assert_regex_match!(address_html, expected_pattern);
}

#[test]
fn test_outputs_rendering() {
let address_html = setup();
let expected_pattern = r#".*<dt>outputs</dt>\n\s*<dd>\n\s*<ul>\n\s*<li><a class=monospace href=/output/1{64}:1>1{64}:1</a></li>\n\s*<li><a class=monospace href=/output/2{64}:2>2{64}:2</a></li>\n\s*</ul>\n\s*</dd>.*"#;
assert_regex_match!(address_html, expected_pattern);
}
}
8 changes: 8 additions & 0 deletions templates/address.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ <h1>Address {{ self.address }}</h1>
<dl>
<dt>sat balance</dt>
<dd>{{ self.sat_balance }}</dd>
<dt>runes balances</dt>
%% for (rune, decimal, symbol) in self.runes_balances.iter() {
%% if let Some(symbol) = symbol {
<dd><a class=monospace href=/rune/{{ rune }}>{{ rune }}</a>: {{ decimal }}{{ symbol }}</dd>
%% } else {
<dd>{{ rune }}: {{ decimal }}</dd>
%% }
%% }
<dt>outputs</dt>
<dd>
<ul>
Expand Down
114 changes: 114 additions & 0 deletions tests/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,120 @@ fn address_page_shows_outputs_and_sat_balance() {
);
}

#[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 inscription_page() {
let core = mockcore::spawn();
Expand Down

0 comments on commit 10d49a0

Please sign in to comment.