1
0
Fork 0
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:
Roman Zeyde 2018-05-02 13:42:13 +03:00
parent 92ac75e50d
commit 4a5fc5e83b
No known key found for this signature in database
GPG key ID: 87CAE5FA46917CBB
5 changed files with 189 additions and 63 deletions

View file

@ -17,7 +17,7 @@ pub struct Daemon {
url: String,
}
#[derive(Eq, PartialEq)]
#[derive(Eq, PartialEq, Clone)]
pub struct HeaderEntry {
height: usize,
hash: Sha256dHash,

View file

@ -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 {

View file

@ -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())
}
}

View file

@ -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(&params).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(&params).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(&params).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(&params),
"blockchain.estimatefee" => self.blockchain_estimatefee(&params),
"blockchain.relayfee" => self.blockchain_relayfee(),
"blockchain.scripthash.subscribe" => self.blockchain_scripthash_subscribe(&params),
"blockchain.scripthash.get_balance" => self.blockchain_scripthash_get_balance(&params),
"blockchain.scripthash.get_history" => self.blockchain_scripthash_get_history(&params),

View file

@ -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__':