Skip to content

Commit

Permalink
Implement wallet identify (#586)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphjaph committed Oct 1, 2022
1 parent 8ff39ad commit 5546505
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 3 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ mod range;
mod server;
mod supply;
mod traits;
mod wallet;

#[derive(Debug, Parser)]
pub(crate) enum Subcommand {
Expand All @@ -23,6 +24,8 @@ pub(crate) enum Subcommand {
Server(server::Server),
Supply,
Traits(traits::Traits),
#[clap(subcommand)]
Wallet(wallet::Wallet),
}

impl Subcommand {
Expand All @@ -43,6 +46,7 @@ impl Subcommand {
}
Self::Supply => supply::run(),
Self::Traits(traits) => traits.run(),
Self::Wallet(wallet) => wallet.run(options),
}
}
}
19 changes: 19 additions & 0 deletions src/subcommand/wallet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use super::*;

mod identify;

#[derive(Debug, Parser)]
pub(crate) enum Wallet {
Identify,
}

impl Wallet {
pub(crate) fn run(self, options: Options) -> Result<()> {
match self {
Self::Identify => identify::run(options),
}
}
}

#[cfg(test)]
mod tests {}
141 changes: 141 additions & 0 deletions src/subcommand/wallet/identify.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use {
super::*,
bitcoincore_rpc::{Auth, Client, RpcApi},
};

pub(crate) fn run(options: Options) -> Result {
let index = Index::open(&options)?;
index.index()?;

let cookie_file = options.cookie_file()?;
let rpc_url = options.rpc_url();
log::info!(
"Connecting to Bitcoin Core RPC server at {rpc_url} using credentials from `{}`",
cookie_file.display()
);
let client = Client::new(&rpc_url, Auth::CookieFile(cookie_file))
.context("Failed to connect to Bitcoin Core RPC at {rpc_url}")?;

let unspent = client.list_unspent(None, None, None, None, None)?;

let mut utxos = Vec::new();
for utxo in unspent {
let output = OutPoint::new(utxo.txid, utxo.vout);
match index.list(output)? {
Some(List::Unspent(ordinal_ranges)) => {
utxos.push((output, ordinal_ranges));
}
Some(List::Spent) => {
bail!("Output {output} in wallet but is spent according to index")
}
None => bail!("Ordinals index has not seen {output}"),
}
}

for (ordinal, output, offset, rarity) in identify(utxos) {
println!("{ordinal}\t{output}\t{offset}\t{rarity}");
}

Ok(())
}

fn identify(utxos: Vec<(OutPoint, Vec<(u64, u64)>)>) -> Vec<(Ordinal, OutPoint, u64, Rarity)> {
utxos
.into_iter()
.flat_map(|(outpoint, ordinal_ranges)| {
let mut offset = 0;
ordinal_ranges.into_iter().filter_map(move |(start, end)| {
let ordinal = Ordinal(start);
let rarity = ordinal.rarity();
let start_offset = offset;
offset += end - start;
if rarity > Rarity::Common {
Some((ordinal, outpoint, start_offset, rarity))
} else {
None
}
})
})
.collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn identify_no_rare_ordinals() {
let utxos = vec![(
OutPoint::null(),
vec![(51 * COIN_VALUE, 100 * COIN_VALUE), (1234, 5678)],
)];
assert_eq!(identify(utxos), vec![])
}

#[test]
fn identify_one_rare_ordinal() {
let utxos = vec![(
OutPoint::null(),
vec![(10, 80), (50 * COIN_VALUE, 100 * COIN_VALUE)],
)];
assert_eq!(
identify(utxos),
vec![(
Ordinal(50 * COIN_VALUE),
OutPoint::null(),
70,
Rarity::Uncommon
)]
)
}

#[test]
fn identify_two_rare_ordinals() {
let utxos = vec![(
OutPoint::null(),
vec![(0, 100), (1050000000000000, 1150000000000000)],
)];
assert_eq!(
identify(utxos),
vec![
(Ordinal(0), OutPoint::null(), 0, Rarity::Mythic),
(
Ordinal(1050000000000000),
OutPoint::null(),
100,
Rarity::Epic
)
]
)
}

#[test]
fn identify_rare_ordinals_in_different_outpoints() {
let utxos = vec![
(OutPoint::null(), vec![(50 * COIN_VALUE, 55 * COIN_VALUE)]),
(
OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5")
.unwrap(),
vec![(100 * COIN_VALUE, 111 * COIN_VALUE)],
),
];
assert_eq!(
identify(utxos),
vec![
(
Ordinal(50 * COIN_VALUE),
OutPoint::null(),
0,
Rarity::Uncommon
),
(
Ordinal(100 * COIN_VALUE),
OutPoint::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b:5")
.unwrap(),
0,
Rarity::Uncommon
)
]
)
}
}
1 change: 1 addition & 0 deletions test-bitcoincore-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository = "https://github.com/casey/ord"

[dependencies]
bitcoin = { version = "0.29.1", features = ["serde"] }
bitcoincore-rpc = "0.16.0"
bitcoincore-rpc-json = "0.16.0"
hex = "0.4.3"
jsonrpc-core = "18.0.0"
Expand Down
57 changes: 54 additions & 3 deletions test-bitcoincore-rpc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use {
bitcoin::{
blockdata::constants::COIN_VALUE, blockdata::script, consensus::encode::serialize,
hash_types::BlockHash, hashes::Hash, Block, BlockHeader, Network, OutPoint, PackedLockTime,
Script, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
hash_types::BlockHash, hashes::Hash, Amount, Block, BlockHeader, Network, OutPoint,
PackedLockTime, Script, Sequence, Transaction, TxIn, TxMerkleNode, TxOut, Txid, Witness, Wtxid,
},
bitcoincore_rpc_json::GetRawTransactionResult,
bitcoincore_rpc_json::{GetRawTransactionResult, ListUnspentResultEntry},
jsonrpc_core::{IoHandler, Value},
jsonrpc_http_server::{CloseHandle, ServerBuilder},
std::collections::BTreeMap,
Expand Down Expand Up @@ -205,6 +205,16 @@ pub trait Api {
verbose: bool,
blockhash: Option<BlockHash>,
) -> Result<Value, jsonrpc_core::Error>;

#[rpc(name = "listunspent")]
fn list_unspent(
&self,
minconf: Option<usize>,
maxconf: Option<usize>,
address: Option<bitcoin::Address>,
include_unsafe: Option<bool>,
query_options: Option<String>,
) -> Result<Vec<ListUnspentResultEntry>, jsonrpc_core::Error>;
}

impl Api for Server {
Expand Down Expand Up @@ -282,6 +292,47 @@ impl Api for Server {
}
}
}

fn list_unspent(
&self,
minconf: Option<usize>,
maxconf: Option<usize>,
address: Option<bitcoin::Address>,
include_unsafe: Option<bool>,
query_options: Option<String>,
) -> Result<Vec<ListUnspentResultEntry>, jsonrpc_core::Error> {
assert_eq!(minconf, None, "minconf param not supported");
assert_eq!(maxconf, None, "maxconf param not supported");
assert_eq!(address, None, "address param not supported");
assert_eq!(include_unsafe, None, "include_unsafe param not supported");
assert_eq!(query_options, None, "query_options param not supported");
Ok(
self
.state
.lock()
.unwrap()
.transactions
.iter()
.flat_map(|(txid, tx)| {
(0..tx.output.len()).map(|vout| ListUnspentResultEntry {
txid: *txid,
vout: vout as u32,
address: None,
label: None,
redeem_script: None,
witness_script: None,
script_pub_key: Script::new(),
amount: Amount::default(),
confirmations: 0,
spendable: true,
solvable: true,
descriptor: None,
safe: true,
})
})
.collect(),
)
}
}

pub struct Handle {
Expand Down
1 change: 1 addition & 0 deletions tests/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ mod server;
mod supply;
mod traits;
mod version;
mod wallet;
19 changes: 19 additions & 0 deletions tests/wallet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use {
super::*,
bitcoin::{blockdata::constants::COIN_VALUE, OutPoint},
};

#[test]
fn identify() {
let rpc_server = test_bitcoincore_rpc::spawn();
let second_coinbase = rpc_server.mine_blocks(1)[0].txdata[0].txid();

CommandBuilder::new("wallet identify")
.rpc_server(&rpc_server)
.expected_stdout(format!(
"{}\t{}\t0\tuncommon\n",
50 * COIN_VALUE,
OutPoint::new(second_coinbase, 0)
))
.run();
}

0 comments on commit 5546505

Please sign in to comment.