mirror of
https://github.com/romanz/electrs.git
synced 2025-02-24 06:57:53 +01:00
Add very simple Electrum API
This commit is contained in:
parent
92ac75e50d
commit
4a5fc5e83b
5 changed files with 189 additions and 63 deletions
|
@ -17,7 +17,7 @@ pub struct Daemon {
|
|||
url: String,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Eq, PartialEq, Clone)]
|
||||
pub struct HeaderEntry {
|
||||
height: usize,
|
||||
hash: Sha256dHash,
|
||||
|
|
|
@ -20,7 +20,7 @@ use types::{Bytes, HeaderMap};
|
|||
const HASH_LEN: usize = 32;
|
||||
pub const HASH_PREFIX_LEN: usize = 8;
|
||||
|
||||
type FullHash = [u8; HASH_LEN];
|
||||
pub type FullHash = [u8; HASH_LEN];
|
||||
pub type HashPrefix = [u8; HASH_PREFIX_LEN];
|
||||
|
||||
pub fn hash_prefix(hash: &[u8]) -> HashPrefix {
|
||||
|
|
113
src/query.rs
113
src/query.rs
|
@ -5,7 +5,7 @@ use bitcoin::network::serialize::{deserialize, serialize};
|
|||
use bitcoin::util::hash::Sha256dHash;
|
||||
use itertools::enumerate;
|
||||
|
||||
use daemon::Daemon;
|
||||
use daemon::{Daemon, HeaderEntry};
|
||||
use index::{compute_script_hash, hash_prefix, HashPrefix, Index, TxInKey, TxInRow, TxKey,
|
||||
TxOutRow, HASH_PREFIX_LEN};
|
||||
use store::Store;
|
||||
|
@ -17,6 +17,30 @@ pub struct Query<'a> {
|
|||
index: &'a Index,
|
||||
}
|
||||
|
||||
pub struct FundingOutput {
|
||||
pub txn_id: Sha256dHash,
|
||||
pub height: u32,
|
||||
pub output_index: usize,
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
pub struct SpendingInput {
|
||||
pub txn_id: Sha256dHash,
|
||||
pub height: u32,
|
||||
pub input_index: usize,
|
||||
}
|
||||
|
||||
pub struct Status {
|
||||
pub balance: u64,
|
||||
pub funding: Vec<FundingOutput>,
|
||||
pub spending: Vec<SpendingInput>,
|
||||
}
|
||||
|
||||
struct TxnHeight {
|
||||
txn: Transaction,
|
||||
height: u32,
|
||||
}
|
||||
|
||||
impl<'a> Query<'a> {
|
||||
pub fn new(store: &'a Store, daemon: &'a Daemon, index: &'a Index) -> Query<'a> {
|
||||
Query {
|
||||
|
@ -26,7 +50,7 @@ impl<'a> Query<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn load_txns(&self, prefixes: Vec<HashPrefix>) -> Vec<Transaction> {
|
||||
fn load_txns(&self, prefixes: Vec<HashPrefix>) -> Vec<TxnHeight> {
|
||||
let mut txns = Vec::new();
|
||||
for txid_prefix in prefixes {
|
||||
for row in self.store.scan(&[b"T", &txid_prefix[..]].concat()) {
|
||||
|
@ -34,19 +58,20 @@ impl<'a> Query<'a> {
|
|||
let txid: Sha256dHash = deserialize(&key.txid).unwrap();
|
||||
let txn_bytes = self.daemon.get(&format!("tx/{}.bin", txid.be_hex_string()));
|
||||
let txn: Transaction = deserialize(&txn_bytes).unwrap();
|
||||
txns.push(txn)
|
||||
let height: u32 = bincode::deserialize(&row.value).unwrap();
|
||||
txns.push(TxnHeight { txn, height })
|
||||
}
|
||||
}
|
||||
txns
|
||||
}
|
||||
|
||||
fn find_spending_txn(&self, txid: &Sha256dHash, output_index: u32) -> Option<Transaction> {
|
||||
fn find_spending_input(&self, funding: &FundingOutput) -> Option<SpendingInput> {
|
||||
let spend_key = bincode::serialize(&TxInKey {
|
||||
code: b'I',
|
||||
prev_hash_prefix: hash_prefix(&txid[..]),
|
||||
prev_index: output_index as u16,
|
||||
prev_hash_prefix: hash_prefix(&funding.txn_id[..]),
|
||||
prev_index: funding.output_index as u16,
|
||||
}).unwrap();
|
||||
let mut spending: Vec<Transaction> = self.load_txns(
|
||||
let spending_txns: Vec<TxnHeight> = self.load_txns(
|
||||
self.store
|
||||
.scan(&spend_key)
|
||||
.iter()
|
||||
|
@ -57,21 +82,36 @@ impl<'a> Query<'a> {
|
|||
})
|
||||
.collect(),
|
||||
);
|
||||
spending.retain(|item| {
|
||||
item.input
|
||||
.iter()
|
||||
.any(|input| input.prev_hash == *txid && input.prev_index == output_index)
|
||||
});
|
||||
assert!(spending.len() <= 1);
|
||||
if spending.len() == 1 {
|
||||
Some(spending.remove(0))
|
||||
let mut spending_inputs = Vec::new();
|
||||
for t in &spending_txns {
|
||||
for (index, input) in enumerate(&t.txn.input) {
|
||||
if input.prev_hash == funding.txn_id
|
||||
&& input.prev_index == funding.output_index as u32
|
||||
{
|
||||
spending_inputs.push(SpendingInput {
|
||||
txn_id: t.txn.txid(),
|
||||
height: t.height,
|
||||
input_index: index,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(spending_inputs.len() <= 1);
|
||||
if spending_inputs.len() == 1 {
|
||||
Some(spending_inputs.remove(0))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn balance(&self, script_hash: &[u8]) -> f64 {
|
||||
let mut funding: Vec<Transaction> = self.load_txns(
|
||||
pub fn status(&self, script_hash: &[u8]) -> Status {
|
||||
let mut status = Status {
|
||||
balance: 0,
|
||||
funding: vec![],
|
||||
spending: vec![],
|
||||
};
|
||||
|
||||
let funding_txns = self.load_txns(
|
||||
self.store
|
||||
.scan(&[b"O", &script_hash[..HASH_PREFIX_LEN]].concat())
|
||||
.iter()
|
||||
|
@ -82,25 +122,27 @@ impl<'a> Query<'a> {
|
|||
})
|
||||
.collect(),
|
||||
);
|
||||
funding.retain(|item| {
|
||||
item.output
|
||||
.iter()
|
||||
.any(|output| compute_script_hash(&output.script_pubkey[..]) == script_hash)
|
||||
});
|
||||
|
||||
let mut balance = 0u64;
|
||||
let mut spending = Vec::<Transaction>::new();
|
||||
for txn in &funding {
|
||||
let txid = txn.txid();
|
||||
for (index, output) in enumerate(&txn.output) {
|
||||
if let Some(spent) = self.find_spending_txn(&txid, index as u32) {
|
||||
spending.push(spent); // TODO: may contain duplicate TXNs
|
||||
} else {
|
||||
balance += output.value;
|
||||
for t in funding_txns {
|
||||
let txn_id = t.txn.txid();
|
||||
for (index, output) in enumerate(&t.txn.output) {
|
||||
if compute_script_hash(&output.script_pubkey[..]) == script_hash {
|
||||
status.funding.push(FundingOutput {
|
||||
txn_id: txn_id,
|
||||
height: t.height,
|
||||
output_index: index,
|
||||
value: output.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
balance as f64 / 100_000_000f64
|
||||
for funding_output in &status.funding {
|
||||
if let Some(spent) = self.find_spending_input(&funding_output) {
|
||||
status.spending.push(spent);
|
||||
} else {
|
||||
status.balance += funding_output.value;
|
||||
}
|
||||
}
|
||||
status
|
||||
}
|
||||
|
||||
pub fn get_tx(&self, tx_hash: &Sha256dHash) -> Bytes {
|
||||
|
@ -118,4 +160,9 @@ impl<'a> Query<'a> {
|
|||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn get_best_header(&self) -> Option<HeaderEntry> {
|
||||
let header_list = self.index.headers_list();
|
||||
Some(header_list.headers().last()?.clone())
|
||||
}
|
||||
}
|
||||
|
|
96
src/rpc.rs
96
src/rpc.rs
|
@ -1,11 +1,15 @@
|
|||
use bitcoin::util::hash::Sha256dHash;
|
||||
use itertools;
|
||||
use serde_json::{from_str, Number, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream};
|
||||
|
||||
use query::Query;
|
||||
use query::{Query, Status};
|
||||
use index::FullHash;
|
||||
use util;
|
||||
use crypto::digest::Digest;
|
||||
use crypto::sha2::Sha256;
|
||||
|
||||
error_chain!{}
|
||||
|
||||
|
@ -13,9 +17,59 @@ struct Handler<'a> {
|
|||
query: &'a Query<'a>,
|
||||
}
|
||||
|
||||
// TODO: Sha256dHash should be a generic hash-container (since script hash is single SHA256)
|
||||
fn hash_from_params(params: &[Value]) -> Result<Sha256dHash> {
|
||||
let script_hash = params.get(0).chain_err(|| "missing hash")?;
|
||||
let script_hash = script_hash.as_str().chain_err(|| "non-string hash")?;
|
||||
let script_hash = Sha256dHash::from_hex(script_hash).chain_err(|| "non-hex hash")?;
|
||||
Ok(script_hash)
|
||||
}
|
||||
|
||||
fn history_from_status(status: &Status) -> Vec<(u32, Sha256dHash)> {
|
||||
let mut txns_map = HashMap::<Sha256dHash, u32>::new();
|
||||
for f in &status.funding {
|
||||
txns_map.insert(f.txn_id, f.height);
|
||||
}
|
||||
for s in &status.spending {
|
||||
txns_map.insert(s.txn_id, s.height);
|
||||
}
|
||||
let mut txns: Vec<(u32, Sha256dHash)> =
|
||||
txns_map.into_iter().map(|item| (item.1, item.0)).collect();
|
||||
txns.sort();
|
||||
txns
|
||||
}
|
||||
|
||||
fn hash_from_status(status: &Status) -> Option<FullHash> {
|
||||
let txns = history_from_status(status);
|
||||
if txns.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut hash = FullHash::default();
|
||||
let mut sha2 = Sha256::new();
|
||||
for (height, txn_id) in txns {
|
||||
let part = format!("{}:{}:", txn_id.be_hex_string(), height);
|
||||
sha2.input(part.as_bytes());
|
||||
}
|
||||
sha2.result(&mut hash);
|
||||
Some(hash)
|
||||
}
|
||||
|
||||
impl<'a> Handler<'a> {
|
||||
fn blockchain_headers_subscribe(&self) -> Result<Value> {
|
||||
Ok(json!({}))
|
||||
let entry = self.query
|
||||
.get_best_header()
|
||||
.chain_err(|| "no headers found")?;
|
||||
let header = entry.header();
|
||||
Ok(json!({
|
||||
"block_height": entry.height(),
|
||||
"version": header.version,
|
||||
"prev_block_hash": header.prev_blockhash.be_hex_string(),
|
||||
"merkle_root": header.merkle_root.be_hex_string(),
|
||||
"timestamp": header.time,
|
||||
"bits": header.bits,
|
||||
"nonce": header.nonce
|
||||
}))
|
||||
}
|
||||
|
||||
fn server_version(&self) -> Result<Value> {
|
||||
|
@ -52,27 +106,40 @@ impl<'a> Handler<'a> {
|
|||
Ok(json!(1e-5)) // TODO: consult with actual mempool
|
||||
}
|
||||
|
||||
fn blockchain_scripthash_subscribe(&self, _params: &[Value]) -> Result<Value> {
|
||||
Ok(json!("HEX_STATUS"))
|
||||
fn blockchain_relayfee(&self) -> Result<Value> {
|
||||
Ok(json!(1e-5)) // TODO: consult with actual node
|
||||
}
|
||||
|
||||
fn blockchain_scripthash_subscribe(&self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?;
|
||||
let status = self.query.status(&script_hash[..]);
|
||||
|
||||
Ok(match hash_from_status(&status) {
|
||||
Some(hash) => Value::String(util::hexlify(&hash)),
|
||||
None => Value::Null,
|
||||
})
|
||||
}
|
||||
|
||||
fn blockchain_scripthash_get_balance(&self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = params.get(0).chain_err(|| "missing scripthash")?;
|
||||
let script_hash = script_hash.as_str().chain_err(|| "non-string scripthash")?;
|
||||
let script_hash = Sha256dHash::from_hex(script_hash).chain_err(|| "non-hex scripthash")?;
|
||||
let confirmed = self.query.balance(&script_hash[..]);
|
||||
Ok(json!({ "confirmed": confirmed })) // TODO: "unconfirmed"
|
||||
let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?;
|
||||
let status = self.query.status(&script_hash[..]);
|
||||
Ok(json!({ "confirmed": status.balance })) // TODO: "unconfirmed"
|
||||
}
|
||||
|
||||
fn blockchain_scripthash_get_history(&self, _params: &[Value]) -> Result<Value> {
|
||||
Ok(json!([])) // TODO: list of {tx_hash: "ABC", height: 123}
|
||||
fn blockchain_scripthash_get_history(&self, params: &[Value]) -> Result<Value> {
|
||||
let script_hash = hash_from_params(¶ms).chain_err(|| "bad script_hash")?;
|
||||
let status = self.query.status(&script_hash[..]);
|
||||
Ok(json!(Value::Array(
|
||||
history_from_status(&status)
|
||||
.into_iter()
|
||||
.map(|item| json!({"height": item.0, "tx_hash": item.1.be_hex_string()}))
|
||||
.collect()
|
||||
)))
|
||||
}
|
||||
|
||||
fn blockchain_transaction_get(&self, params: &[Value]) -> Result<Value> {
|
||||
// TODO: handle 'verbose' param
|
||||
let tx_hash = params.get(0).chain_err(|| "missing tx_hash")?;
|
||||
let tx_hash = tx_hash.as_str().chain_err(|| "non-string tx_hash")?;
|
||||
let tx_hash = Sha256dHash::from_hex(tx_hash).chain_err(|| "non-hex tx_hash")?;
|
||||
let tx_hash = hash_from_params(params).chain_err(|| "bad tx_hash")?;
|
||||
let tx_hex = util::hexlify(&self.query.get_tx(&tx_hash));
|
||||
Ok(json!(tx_hex))
|
||||
}
|
||||
|
@ -91,6 +158,7 @@ impl<'a> Handler<'a> {
|
|||
"mempool.get_fee_histogram" => self.mempool_get_fee_histogram(),
|
||||
"blockchain.block.get_chunk" => self.blockchain_block_get_chunk(¶ms),
|
||||
"blockchain.estimatefee" => self.blockchain_estimatefee(¶ms),
|
||||
"blockchain.relayfee" => self.blockchain_relayfee(),
|
||||
"blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(¶ms),
|
||||
"blockchain.scripthash.get_balance" => self.blockchain_scripthash_get_balance(¶ms),
|
||||
"blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(¶ms),
|
||||
|
|
|
@ -14,11 +14,25 @@ script_for_address = BitcoinMainnet.ui.script_for_address
|
|||
|
||||
log = Logger(__name__)
|
||||
|
||||
class Connection:
|
||||
def __init__(self, addr):
|
||||
self.s = socket.create_connection(addr)
|
||||
self.f = self.s.makefile('r')
|
||||
self.id = 0
|
||||
|
||||
def call(self, method, *args):
|
||||
req = {
|
||||
'id': self.id,
|
||||
'method': method,
|
||||
'params': list(args),
|
||||
}
|
||||
msg = json.dumps(req) + '\n'
|
||||
self.s.sendall(msg.encode('ascii'))
|
||||
return json.loads(self.f.readline())
|
||||
|
||||
|
||||
def main():
|
||||
s = socket.create_connection(('localhost', 50001))
|
||||
f = s.makefile('r')
|
||||
|
||||
conn = Connection(('localhost', 50001))
|
||||
xpub, = sys.argv[1:]
|
||||
total = 0
|
||||
k = pycoin.ui.key_from_text.key_from_text(xpub)
|
||||
|
@ -28,23 +42,20 @@ def main():
|
|||
address = k.subkey(change).subkey(n).address()
|
||||
script = script_for_address(address)
|
||||
script_hash = hashlib.sha256(script).digest()
|
||||
req = {
|
||||
'id': 1,
|
||||
'method': 'blockchain.scripthash.get_balance',
|
||||
'params': [script_hash[::-1].hex()]
|
||||
}
|
||||
msg = json.dumps(req) + '\n'
|
||||
s.sendall(msg.encode('ascii'))
|
||||
res = json.loads(f.readline())['result']
|
||||
total += res['confirmed']
|
||||
if res['confirmed']:
|
||||
reply = conn.call('blockchain.scripthash.get_balance',
|
||||
script_hash[::-1].hex())
|
||||
res = reply['result']
|
||||
confirmed = res['confirmed'] / 1e8
|
||||
total += confirmed
|
||||
if confirmed:
|
||||
log.info('{}/{} => {} has {:11.8f} BTC',
|
||||
change, n, address, res['confirmed'])
|
||||
change, n, address, confirmed)
|
||||
empty = 0
|
||||
else:
|
||||
empty += 1
|
||||
if empty >= 10:
|
||||
break
|
||||
log.info('{}', conn.call('blockchain.scripthash.get_history', script_hash[::-1].hex()))
|
||||
log.info('total balance: {} BTC', total)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Add table
Reference in a new issue