Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add query endpoints for balances of specific runes #3675

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
83 changes: 82 additions & 1 deletion src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,12 @@ mod updater;
#[cfg(test)]
pub(crate) mod testing;

const SCHEMA_VERSION: u64 = 26;
const SCHEMA_VERSION: u64 = 27;

define_multimap_table! { SATPOINT_TO_SEQUENCE_NUMBER, &SatPointValue, u32 }
define_multimap_table! { SAT_TO_SEQUENCE_NUMBER, u64, u32 }
define_multimap_table! { SEQUENCE_NUMBER_TO_CHILDREN, u32, u32 }
define_multimap_table! { RUNE_ID_TO_OUTPOINTS_BALANCE, RuneIdValue, &[u8] }
define_multimap_table! { SCRIPT_PUBKEY_TO_OUTPOINT, &[u8], OutPointValue }
define_table! { CONTENT_TYPE_TO_COUNT, Option<&[u8]>, u64 }
define_table! { HEIGHT_TO_BLOCK_HEADER, u32, &HeaderValue }
Expand Down Expand Up @@ -300,6 +301,7 @@ impl Index {
tx.open_multimap_table(SAT_TO_SEQUENCE_NUMBER)?;
tx.open_multimap_table(SCRIPT_PUBKEY_TO_OUTPOINT)?;
tx.open_multimap_table(SEQUENCE_NUMBER_TO_CHILDREN)?;
tx.open_multimap_table(RUNE_ID_TO_OUTPOINTS_BALANCE)?;
tx.open_table(CONTENT_TYPE_TO_COUNT)?;
tx.open_table(HEIGHT_TO_BLOCK_HEADER)?;
tx.open_table(HEIGHT_TO_LAST_SEQUENCE_NUMBER)?;
Expand Down Expand Up @@ -982,6 +984,26 @@ impl Index {
Ok(((id, balance), len))
}

// a buffer with size: 36 + balance size
pub(crate) fn encode_rune_outpoints_balance(
outpoint: OutPoint,
balance: u128,
buffer: &mut Vec<u8>,
) {
buffer.extend_from_slice(&outpoint.store());
varint::encode_to_vec(balance, buffer);
}

pub(crate) fn decode_rune_outpoints_balance(buffer: &[u8]) -> Result<((OutPoint, u128), usize)> {
let mut len = 0;
let outpoint_value: [u8; 36] = buffer[len..len + 36].try_into().unwrap();
len += 36;
let outpoint = OutPoint::load(outpoint_value);
let (balance, balance_len) = varint::decode(&buffer[len..]).unwrap();
len += balance_len;
Ok(((outpoint, balance), len))
}

pub fn get_rune_balances_for_output(
&self,
outpoint: OutPoint,
Expand Down Expand Up @@ -1074,6 +1096,38 @@ impl Index {
Ok(rune_balances)
}

pub(crate) fn get_rune_specific_balances(&self, rune: Rune) -> Result<BTreeMap<OutPoint, u128>> {
let mut result: BTreeMap<OutPoint, u128> = BTreeMap::new();
let rtx = self.database.begin_read()?;

// get rune id
let Some(id) = self
.database
.begin_read()?
.open_table(RUNE_TO_RUNE_ID)?
.get(rune.0)?
.map(|guard| guard.value())
else {
return Ok(BTreeMap::new());
};

// get a list of outpoints of a specific rune
let outpoints_balance = rtx
.open_multimap_table(RUNE_ID_TO_OUTPOINTS_BALANCE)?
.get(id)?;

// retrieve rune balances
for i in outpoints_balance {
let guard = i?;
let buffer = guard.value();
let ((outpoint, amount), _) = Index::decode_rune_outpoints_balance(buffer).unwrap();

*result.entry(outpoint).or_default() += amount
}

Ok(result)
}

pub fn get_rune_balances(&self) -> Result<Vec<(OutPoint, Vec<(RuneId, u128)>)>> {
let mut result = Vec::new();

Expand Down Expand Up @@ -2306,6 +2360,33 @@ impl Index {
mod tests {
use {super::*, crate::index::testing::Context};

#[test]
fn test_encode_decode_rune_to_outpoints_balance() {
let outpoint = OutPoint {
txid: Txid::all_zeros(),
vout: 2,
};

// 5 outpoints
let mut buffer: Vec<u8> = Vec::new();
Index::encode_rune_outpoints_balance(outpoint, 1000, &mut buffer);
Index::encode_rune_outpoints_balance(outpoint, 1000, &mut buffer);
Index::encode_rune_outpoints_balance(outpoint, 1000, &mut buffer);
Index::encode_rune_outpoints_balance(outpoint, 1000, &mut buffer);
Index::encode_rune_outpoints_balance(outpoint, 1000, &mut buffer);

let mut i = 0;
let arr_buffer = buffer.as_slice();
while i < buffer.len() {
let ((d_outpoint, d_balance), len) =
Index::decode_rune_outpoints_balance(&arr_buffer[i..]).unwrap();
i += len;

assert_eq!(outpoint, d_outpoint);
assert_eq!(1000, d_balance);
}
}

#[test]
fn height_limit() {
{
Expand Down
22 changes: 22 additions & 0 deletions src/index/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,28 @@ impl Context {

pretty_assert_eq!(balances, self.index.get_rune_balances().unwrap());

// convert from outpoint -> <rune, balance> to rune -> <outpoint, balance>
let mut rune_outpoints_balance: HashMap<RuneId, BTreeMap<OutPoint, u128>> = HashMap::new();
balances.iter().for_each(|outpoint| {
for r in outpoint.1.iter() {
*rune_outpoints_balance
.entry(r.0)
.or_default()
.entry(outpoint.0)
.or_default() += r.1;
}
});

for i in rune_outpoints_balance {
pretty_assert_eq!(
i.1,
self
.index
.get_rune_specific_balances(self.index.get_rune_by_id(i.0).unwrap().unwrap())
.unwrap()
);
}

let mut outstanding: HashMap<RuneId, u128> = HashMap::new();

for (_, balances) in balances {
Expand Down
3 changes: 3 additions & 0 deletions src/index/updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,8 @@ impl<'index> Updater<'index> {

if self.index.index_runes && self.height >= self.index.settings.first_rune_height() {
let mut outpoint_to_rune_balances = wtx.open_table(OUTPOINT_TO_RUNE_BALANCES)?;
let mut rune_id_to_outpoints_balance =
wtx.open_multimap_table(RUNE_ID_TO_OUTPOINTS_BALANCE)?;
let mut rune_id_to_rune_entry = wtx.open_table(RUNE_ID_TO_RUNE_ENTRY)?;
let mut rune_to_rune_id = wtx.open_table(RUNE_TO_RUNE_ID)?;
let mut sequence_number_to_rune_id = wtx.open_table(SEQUENCE_NUMBER_TO_RUNE_ID)?;
Expand All @@ -649,6 +651,7 @@ impl<'index> Updater<'index> {
Height(self.height),
),
outpoint_to_balances: &mut outpoint_to_rune_balances,
rune_id_to_outpoints_balance: &mut rune_id_to_outpoints_balance,
rune_to_id: &mut rune_to_rune_id,
runes,
sequence_number_to_rune_id: &mut sequence_number_to_rune_id,
Expand Down
20 changes: 20 additions & 0 deletions src/index/updater/rune_updater.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub(super) struct RuneUpdater<'a, 'tx, 'client> {
pub(super) inscription_id_to_sequence_number: &'a Table<'tx, InscriptionIdValue, u32>,
pub(super) minimum: Rune,
pub(super) outpoint_to_balances: &'a mut Table<'tx, &'static OutPointValue, &'static [u8]>,
pub(super) rune_id_to_outpoints_balance: &'a mut MultimapTable<'tx, RuneIdValue, &'static [u8]>,
pub(super) rune_to_id: &'a mut Table<'tx, u128, RuneIdValue>,
pub(super) runes: u64,
pub(super) sequence_number_to_rune_id: &'a mut Table<'tx, u32, RuneIdValue>,
Expand Down Expand Up @@ -170,6 +171,7 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> {

// update outpoint balances
let mut buffer: Vec<u8> = Vec::new();
// update rune to outpoints, balance
for (vout, balances) in allocated.into_iter().enumerate() {
if balances.is_empty() {
continue;
Expand Down Expand Up @@ -198,6 +200,9 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> {
for (id, balance) in balances {
Index::encode_rune_balance(id, balance.n(), &mut buffer);

let mut outpoints_balance_buffer: Vec<u8> = Vec::new();
Index::encode_rune_outpoints_balance(outpoint, balance.n(), &mut outpoints_balance_buffer);

if let Some(sender) = self.event_sender {
sender.blocking_send(Event::RuneTransferred {
outpoint,
Expand All @@ -207,6 +212,10 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> {
amount: balance.0,
})?;
}

self
.rune_id_to_outpoints_balance
.insert(id.store(), outpoints_balance_buffer.as_slice())?;
}

self
Expand Down Expand Up @@ -482,6 +491,17 @@ impl<'a, 'tx, 'client> RuneUpdater<'a, 'tx, 'client> {
let ((id, balance), len) = Index::decode_rune_balance(&buffer[i..]).unwrap();
i += len;
*unallocated.entry(id).or_default() += balance;

let mut outpoints_balance_buffer: Vec<u8> = Vec::new();
Index::encode_rune_outpoints_balance(
input.previous_output,
balance,
&mut outpoints_balance_buffer,
);
// this is supposed to have no error as rune_id_to_outpoints_balance should have the outpoint like in outpoint_to_balances
let _ = self
.rune_id_to_outpoints_balance
.remove(id.store(), outpoints_balance_buffer.as_slice());
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ impl Server {
.route("/range/:start/:end", get(Self::range))
.route("/rare.txt", get(Self::rare_txt))
.route("/rune/:rune", get(Self::rune))
.route("/rune/:rune/balances", get(Self::rune_specific_balances))
.route("/runes", get(Self::runes))
.route("/runes/:page", get(Self::runes_paginated))
.route("/runes/balances", get(Self::runes_balances))
Expand Down Expand Up @@ -798,6 +799,36 @@ impl Server {
})
}

async fn rune_specific_balances(
Extension(index): Extension<Arc<Index>>,
Path(DeserializeFromStr(rune_query)): Path<DeserializeFromStr<query::Rune>>,
AcceptJson(accept_json): AcceptJson,
) -> ServerResult {
task::block_in_place(|| {
if !index.has_rune_index() {
return Err(ServerError::NotFound(
"this server has no rune index".to_string(),
));
}

let rune = match rune_query {
query::Rune::Spaced(spaced_rune) => spaced_rune.rune,
query::Rune::Id(rune_id) => index
.get_rune_by_id(rune_id)?
.ok_or_not_found(|| format!("rune {rune_id}"))?,
query::Rune::Number(number) => index
.get_rune_by_number(usize::try_from(number).unwrap())?
.ok_or_not_found(|| format!("rune number {number}"))?,
};

Ok(if accept_json {
Json(index.get_rune_specific_balances(rune).unwrap()).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
})
})
}

async fn home(
Extension(server_config): Extension<Arc<ServerConfig>>,
Extension(index): Extension<Arc<Index>>,
Expand Down
74 changes: 74 additions & 0 deletions tests/json_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,3 +754,77 @@ fn get_decode_tx() {
}
);
}

#[test]
fn get_rune_specific_balances() {
let core = mockcore::builder().network(Network::Regtest).build();

let ord = TestServer::spawn_with_server_args(&core, &["--index-runes", "--regtest"], &[]);

create_wallet(&core, &ord);

core.mine_blocks(3);

let runes = [Rune(RUNE), Rune(RUNE + 1), Rune(RUNE + 2)];
let mut etched: Vec<Etched> = Vec::new();

runes
.iter()
.for_each(|rune| etched.push(etch(&core, &ord, *rune)));

core.mine_blocks(1);

let mut rune_balances: BTreeMap<Rune, BTreeMap<OutPoint, u128>> = vec![
(
runes[0],
vec![(
OutPoint {
txid: etched[0].output.reveal,
vout: 1,
},
1000,
)]
.into_iter()
.collect(),
),
(
runes[1],
vec![(
OutPoint {
txid: etched[1].output.reveal,
vout: 1,
},
1000,
)]
.into_iter()
.collect(),
),
(
runes[2],
vec![(
OutPoint {
txid: etched[2].output.reveal,
vout: 1,
},
1000,
)]
.into_iter()
.collect(),
),
]
.into_iter()
.collect();

etched.iter().enumerate().for_each(|(i, e)| {
let response = ord.json_request(format!("/rune/{}/balances", e.id));
assert_eq!(response.status(), StatusCode::OK);

let runes_balance_json: BTreeMap<OutPoint, u128> =
serde_json::from_str(&response.text().unwrap()).unwrap();

pretty_assert_eq!(
runes_balance_json,
rune_balances.entry(runes[i]).or_default().to_owned()
);
});
}
Loading