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

Add /address/:address/ JSON API endpoints #4008

Closed
Closed
2 changes: 2 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ pub struct Output {
pub address: Option<Address<NetworkUnchecked>>,
pub indexed: bool,
pub inscriptions: Vec<InscriptionId>,
pub outpoint: OutPoint,
pub runes: BTreeMap<SpacedRune, Pile>,
pub sat_ranges: Option<Vec<(u64, u64)>>,
pub script_pubkey: ScriptBuf,
Expand All @@ -181,6 +182,7 @@ impl Output {
.map(|address| uncheck(&address)),
indexed,
inscriptions,
outpoint,
runes,
sat_ranges,
script_pubkey: tx_out.script_pubkey,
Expand Down
137 changes: 137 additions & 0 deletions src/subcommand/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ impl Server {
let router = Router::new()
.route("/", get(Self::home))
.route("/address/:address", get(Self::address))
.route("/address/:address/cardinals", get(Self::address_cardinals))
.route("/address/:address/runes", get(Self::address_runes))
.route(
"/address/:address/inscriptions",
get(Self::address_inscriptions),
)
.route("/block/:query", get(Self::block))
.route("/blockcount", get(Self::block_count))
.route("/blockhash", get(Self::block_hash))
Expand Down Expand Up @@ -866,6 +872,136 @@ impl Server {
})
}

async fn address_cardinals(
Extension(server_config): Extension<Arc<ServerConfig>>,
Extension(index): Extension<Arc<Index>>,
Path(address): Path<Address<NetworkUnchecked>>,
AcceptJson(accept_json): AcceptJson,
) -> ServerResult {
task::block_in_place(|| {
Ok(if accept_json {
let address = address
.require_network(server_config.chain.network())
.map_err(|err| ServerError::BadRequest(err.to_string()))?;

let outputs = index.get_address_info(&address)?;

let cardinal_outputs: Vec<OutPoint> = outputs
.into_iter()
.filter(|output| {
let has_inscriptions = index
.get_inscriptions_on_output_with_satpoints(*output)
.map(|inscriptions| !inscriptions.is_empty())
.unwrap_or(false);

let has_runes = index
.get_rune_balances_for_output(*output)
.map(|runes| !runes.is_empty())
.unwrap_or(false);

!has_inscriptions && !has_runes
})
.collect();

let mut response = Vec::new();

for outpoint in cardinal_outputs {
let (output_info, _) = index
.get_output_info(outpoint)?
.ok_or_not_found(|| format!("output {outpoint}"))?;

response.push(output_info);
}

Json(response).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
})
})
}

async fn address_runes(
Extension(server_config): Extension<Arc<ServerConfig>>,
Extension(index): Extension<Arc<Index>>,
Path(address): Path<Address<NetworkUnchecked>>,
AcceptJson(accept_json): AcceptJson,
) -> ServerResult {
task::block_in_place(|| {
Ok(if accept_json {
let address = address
.require_network(server_config.chain.network())
.map_err(|err| ServerError::BadRequest(err.to_string()))?;

let outputs = index.get_address_info(&address)?;

let runic_outputs: Vec<OutPoint> = outputs
.into_iter()
.filter(|output| {
index
.get_rune_balances_for_output(*output)
.map(|runes| !runes.is_empty())
.unwrap_or(false)
})
.collect();

let mut response = Vec::new();

for outpoint in runic_outputs {
let (output_info, _txout) = index
.get_output_info(outpoint)?
.ok_or_not_found(|| format!("output {outpoint}"))?;

response.push(output_info);
}

Json(response).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
})
})
}

async fn address_inscriptions(
Extension(server_config): Extension<Arc<ServerConfig>>,
Extension(index): Extension<Arc<Index>>,
Path(address): Path<Address<NetworkUnchecked>>,
AcceptJson(accept_json): AcceptJson,
) -> ServerResult {
task::block_in_place(|| {
Ok(if accept_json {
let address = address
.require_network(server_config.chain.network())
.map_err(|err| ServerError::BadRequest(err.to_string()))?;

let outputs = index.get_address_info(&address)?;

let inscription_outputs: Vec<OutPoint> = outputs
.into_iter()
.filter(|output| {
index
.get_inscriptions_on_output_with_satpoints(*output)
.map(|inscriptions| !inscriptions.is_empty())
.unwrap_or(false)
})
.collect();

let mut response = Vec::new();

for outpoint in inscription_outputs {
let (output_info, _txout) = index
.get_output_info(outpoint)?
.ok_or_not_found(|| format!("output {outpoint}"))?;

response.push(output_info);
}

Json(response).into_response()
} else {
StatusCode::NOT_FOUND.into_response()
})
})
}

async fn block(
Extension(server_config): Extension<Arc<ServerConfig>>,
Extension(index): Extension<Arc<Index>>,
Expand Down Expand Up @@ -3562,6 +3698,7 @@ mod tests {
sat_ranges: None,
indexed: true,
inscriptions: Vec::new(),
outpoint: output,
runes: vec![(
SpacedRune {
rune: Rune(RUNE),
Expand Down
154 changes: 154 additions & 0 deletions tests/json_api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use {
super::*,
bitcoin::{BlockHash, ScriptBuf},
ord::subcommand::wallet::send::Output,
ord::{Envelope, Inscription},
};

Expand Down Expand Up @@ -343,6 +344,7 @@ fn get_output() {
InscriptionId { txid, index: 1 },
InscriptionId { txid, index: 2 },
],
outpoint: OutPoint { txid, vout: 0 },
indexed: true,
runes: BTreeMap::new(),
sat_ranges: Some(vec![
Expand All @@ -363,6 +365,158 @@ fn get_output() {
);
}

#[test]
fn address_cardinals_ordinals_runes_api() {
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 address = "bcrt1qs758ursh4q9z627kt3pp5yysm78ddny6txaqgw";

let (inscription_id, reveal) = inscribe(&core, &ord);

let inscription_send = CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 1 {address} {inscription_id}",
))
.core(&core)
.ord(&ord)
.stdout_regex(".*")
.run_and_deserialize_output::<Output>();

core.mine_blocks(1);

etch(&core, &ord, Rune(RUNE));

let rune_send = 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>();

let cardinal_send = CommandBuilder::new(format!(
"--chain regtest --index-runes wallet send --fee-rate 13.3 {address} 2btc"
))
.core(&core)
.ord(&ord)
.run_and_deserialize_output::<Send>();

core.mine_blocks(6);

let cardinals_response = ord.json_request(format!("/address/{}/cardinals", address));

assert_eq!(cardinals_response.status(), StatusCode::OK);

let cardinals_json: Vec<api::Output> =
serde_json::from_str(&cardinals_response.text().unwrap()).unwrap();

pretty_assert_eq!(
cardinals_json,
vec![api::Output {
address: Some(address.parse().unwrap()),
inscriptions: vec![],
outpoint: OutPoint {
txid: cardinal_send.txid,
vout: 0
},
indexed: true,
runes: BTreeMap::new(),
sat_ranges: None,
script_pubkey: ScriptBuf::from(
address
.parse::<Address<NetworkUnchecked>>()
.unwrap()
.assume_checked()
),
spent: false,
transaction: cardinal_send.txid,
value: 2 * COIN_VALUE,
}]
);

let runes_response = ord.json_request(format!("/address/{}/runes", address));

assert_eq!(runes_response.status(), StatusCode::OK);

let runes_json: Vec<api::Output> = serde_json::from_str(&runes_response.text().unwrap()).unwrap();

let mut expected_runes = BTreeMap::new();

expected_runes.insert(
SpacedRune {
rune: Rune(RUNE),
spacers: 0,
},
Pile {
amount: 1000,
divisibility: 0,
symbol: Some('¢'),
},
);

pretty_assert_eq!(
runes_json,
vec![api::Output {
address: Some(address.parse().unwrap()),
inscriptions: vec![],
outpoint: OutPoint {
txid: rune_send.txid,
vout: 0
},
indexed: true,
runes: expected_runes,
sat_ranges: None,
script_pubkey: ScriptBuf::from(
address
.parse::<Address<NetworkUnchecked>>()
.unwrap()
.assume_checked()
),
spent: false,
transaction: rune_send.txid,
value: 9901,
}]
);

let inscriptions_response = ord.json_request(format!("/address/{}/inscriptions", address));

assert_eq!(inscriptions_response.status(), StatusCode::OK);

let inscriptions_json: Vec<api::Output> =
serde_json::from_str(&inscriptions_response.text().unwrap()).unwrap();

pretty_assert_eq!(
inscriptions_json,
vec![api::Output {
address: Some(address.parse().unwrap()),
inscriptions: vec![InscriptionId {
txid: reveal,
index: 0
},],
outpoint: OutPoint {
txid: inscription_send.txid,
vout: 0
},
indexed: true,
runes: BTreeMap::new(),
sat_ranges: None,
script_pubkey: ScriptBuf::from(
address
.parse::<Address<NetworkUnchecked>>()
.unwrap()
.assume_checked()
),
spent: false,
transaction: inscription_send.txid,
value: 9901,
}]
);
}

#[test]
fn json_request_fails_when_disabled() {
let core = mockcore::spawn();
Expand Down
4 changes: 4 additions & 0 deletions tests/wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,10 @@ inscriptions:
index: 2
},
],
outpoint: OutPoint {
txid: reveal_txid,
vout: 0
},
indexed: true,
runes: BTreeMap::new(),
sat_ranges: Some(vec![(5_000_000_000, 5_000_030_000)]),
Expand Down
Loading