Skip to content

Commit

Permalink
fix: fix inscription/get_last_accepted API
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Jan 3, 2024
1 parent 978113f commit 2c59f86
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 30 deletions.
8 changes: 8 additions & 0 deletions crates/ns-indexer/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ fn main() -> anyhow::Result<()> {
Err(err)?;
}
}
} else {
match indexer.scan_last_accepted(shutdown.clone()).await {
Ok(_) => log::info!(target: "server", "indexer scanning finished"),
Err(err) => {
log::error!(target: "server", "indexer scanning error: {}", err);
Err(err)?;
}
}
}

Ok::<(), anyhow::Error>(())
Expand Down
59 changes: 58 additions & 1 deletion crates/ns-indexer/src/indexer.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use bitcoin::{hashes::Hash, BlockHash, Transaction};
use futures::future::Shared;
use std::{
collections::{BTreeMap, HashSet, VecDeque},
future::Future,
sync::Arc,
};
use tokio::sync::RwLock;
use tokio::{
sync::RwLock,
time::{sleep, Duration},
};

use ns_protocol::{
ns::{Name, PublicKeyParams, ThresholdLevel},
Expand Down Expand Up @@ -94,6 +99,58 @@ impl Indexer {
Ok(last_accepted_height.block_height as u64)
}

pub async fn scan_last_accepted<S>(&self, signal: Shared<S>) -> anyhow::Result<()>
where
S: Future<Output = ()>,
{
let mut height = 0i64;

loop {
tokio::select! {
_ = signal.clone() => {
log::warn!(target: "ns-indexer", "Received signal to stop indexing");
return Ok(());
},
_ = async {
sleep(Duration::from_secs(3)).await;
} => {},
};

if let Some(checkpoint) = db::Checkpoint::get_last_accepted(&self.scylla).await? {
log::info!(target: "ns-indexer",
action = "scan_last_accepted",
last_accepted = checkpoint.name.clone(),
new_last_accepted = checkpoint.height > height,
height = checkpoint.height,
block_height = checkpoint.block_height;
"",
);

if checkpoint.height > height {
let mut inscription =
db::Inscription::with_pk(checkpoint.name.clone(), checkpoint.sequence);
inscription.get_one(&self.scylla, vec![]).await?;

let last_accepted = inscription.to_index()?;
let last_checkpoint = inscription.to_checkpoint(last_accepted.hash()?)?;
if last_checkpoint != checkpoint {
anyhow::bail!(
"last accepted inscription is not match with checkpoint:\n{:#?}\n{:#?}",
last_checkpoint,
checkpoint
);
}
{
let mut last_accepted_state = self.state.last_accepted.write().await;
*last_accepted_state = Some(last_accepted.clone());
}

height = checkpoint.height;
}
}
}
}

pub async fn index(
&self,
block_hash: &BlockHash,
Expand Down
17 changes: 10 additions & 7 deletions crates/ns-indexer/src/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ pub fn new(state: Arc<api::IndexerAPI>) -> Router {
Router::new()
.route("/", routing::get(api::version))
.route("/healthz", routing::get(api::healthz))
.route("/best/utxo/list", routing::get(api::UtxoAPI::list))
.nest(
"/best",
Router::new()
.route("/inscription", routing::get(api::InscriptionAPI::get_best))
.route(
"/inscription/list",
routing::get(api::InscriptionAPI::list_best),
)
.route("/utxo/list", routing::get(api::UtxoAPI::list)),
)
.nest(
"/v1/name",
Router::new()
Expand Down Expand Up @@ -55,12 +64,6 @@ pub fn new(state: Arc<api::IndexerAPI>) -> Router {
routing::get(api::InscriptionAPI::list_by_name),
),
)
.nest(
"/best/inscription",
Router::new()
.route("/", routing::get(api::InscriptionAPI::get_best))
.route("/list", routing::get(api::InscriptionAPI::list_best)),
)
.nest(
"/v1/invalid_inscription",
Router::new().route(
Expand Down
50 changes: 29 additions & 21 deletions crates/ns-inscriber/src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use terminal_prompt::Terminal;

use ns_inscriber::{
bitcoin::BitCoinRPCOptions,
inscriber::{Inscriber, InscriberOptions, UnspentTxOut},
inscriber::{Inscriber, InscriberOptions, UnspentTxOut, UnspentTxOutJSON},
wallet::{
base64_decode, base64_encode, base64url_decode, base64url_encode, ed25519, hash_256, iana,
secp256k1, unwrap_cbor_tag, wrap_cbor_tag, DerivationPath, Encrypt0, Key,
Expand Down Expand Up @@ -128,8 +128,11 @@ pub enum Commands {
/// Inscribe names to a transaction
Inscribe {
/// Unspent transaction id to spend
#[arg(long)]
#[arg(long, default_value = "")]
txid: String,
/// Unspent txout in JSON format. If not provided, will use the first txout on txid
#[arg(long, default_value = "")]
txout_json: String,
/// Bitcoin address on tx to operate on, will be combined to "{addr}.cose.key" to read secp256k key
#[arg(long, value_name = "FILE")]
addr: String,
Expand Down Expand Up @@ -495,12 +498,12 @@ async fn main() -> anyhow::Result<()> {

Some(Commands::Inscribe {
txid,
txout_json,
addr,
fee,
key,
names,
}) => {
let txid: Txid = txid.parse()?;
let fee_rate = Amount::from_sat(*fee);
let names: Vec<String> = names.split(',').map(|n| n.trim().to_string()).collect();
for name in &names {
Expand Down Expand Up @@ -555,25 +558,30 @@ async fn main() -> anyhow::Result<()> {
let (p2wpkh_pubkey, p2tr_pubkey) = secp256k1::as_script_pubkey(&secp, &keypair);

let inscriber = get_inscriber(network).await?;
let tx = inscriber.bitcoin.get_transaction(&txid).await?;
let (vout, txout) = tx
.output
.iter()
.enumerate()
.find(|(_, o)| o.script_pubkey == p2wpkh_pubkey || o.script_pubkey == p2tr_pubkey)
.ok_or(anyhow!("no matched transaction out to spend"))?;
let unspent_txout = if !txid.is_empty() {
let txid: Txid = txid.parse()?;
let tx = inscriber.bitcoin.get_transaction(&txid).await?;
let (vout, txout) = tx
.output
.iter()
.enumerate()
.find(|(_, o)| {
o.script_pubkey == p2wpkh_pubkey || o.script_pubkey == p2tr_pubkey
})
.ok_or(anyhow!("no matched transaction out to spend"))?;
UnspentTxOut {
txid,
vout: vout as u32,
amount: txout.value,
script_pubkey: txout.script_pubkey.clone(),
}
} else {
let txout: UnspentTxOutJSON = serde_json::from_str(&txout_json)?;
txout.to()?
};

let txid = inscriber
.inscribe(
&ns,
fee_rate,
&keypair.secret_key(),
&UnspentTxOut {
txid,
vout: vout as u32,
amount: txout.value,
script_pubkey: txout.script_pubkey.clone(),
},
)
.inscribe(&ns, fee_rate, &keypair.secret_key(), &unspent_txout)
.await?;

println!("txid: {}", txid);
Expand Down
45 changes: 44 additions & 1 deletion crates/ns-inscriber/src/inscriber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use bitcoin::{
Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
Witness,
};
use std::collections::HashSet;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, str::FromStr};

use ns_protocol::ns::{Name, MAX_NAME_BYTES};

Expand Down Expand Up @@ -669,6 +670,34 @@ pub fn check_duplicate(names: &Vec<Name>) -> Option<String> {
None
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct UnspentTxOutJSON {
pub txid: String,
pub vout: u32,
pub amount: u64,
pub script_pubkey: String,
}

impl UnspentTxOutJSON {
pub fn to(&self) -> anyhow::Result<UnspentTxOut> {
Ok(UnspentTxOut {
txid: Txid::from_str(&self.txid)?,
vout: self.vout,
amount: Amount::from_sat(self.amount),
script_pubkey: ScriptBuf::from_hex(&self.script_pubkey)?,
})
}

pub fn from(tx: UnspentTxOut) -> Self {
Self {
txid: tx.txid.to_string(),
vout: tx.vout,
amount: tx.amount.to_sat(),
script_pubkey: tx.script_pubkey.to_hex_string(),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -712,6 +741,20 @@ mod tests {
name
}

#[test]
fn unspent_txout_json_works() {
let json_str = serde_json::json!({
"txid": "8e9d3e0d762c1d2348a2ca046b36f8de001f740c976b09c046ee1f09a8680131",
"vout": 0,
"amount": 4929400,
"script_pubkey": "0014d37960b3783772f0b6e5a0917f163fa642b3a7fc"
});
let json: UnspentTxOutJSON = serde_json::from_value(json_str).unwrap();
let tx = json.to().unwrap();
let json2 = UnspentTxOutJSON::from(tx);
assert_eq!(json, json2)
}

#[test]
fn preview_inscription_transactions_works() {
let names = vec![get_name("0"), get_name("a")];
Expand Down

0 comments on commit 2c59f86

Please sign in to comment.