Skip to content

Commit

Permalink
Merge pull request #13 from notmandatory/feat/get_unused_addr
Browse files Browse the repository at this point in the history
Return last_unused addresses, parse .ini and create online wallet at startup
  • Loading branch information
lvaccaro authored Mar 21, 2021
2 parents 93851e6 + 4f2e6a6 commit 488f775
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 97 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bdk = { version = "0.3.0", default-features = false }
bdk = { git = "https://github.com/bitcoindevkit/bdk.git", rev = "c456a25", default-features = false }
bdk-macros = "^0.2"
simple-server = "0.4.0"
log = "0.4.0"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Another Bitcoin payment service, based on [bdk](https://github.com/bitcoindevkit
### Get it start
Build and run service (default port is 8080):
```
cargo run server
RUST_LOG=info cargo run
```

Open the local web page on your browser using url [localhost:8080/bitcoin](http://localhost:8080/bitcoin).
Expand Down
2 changes: 1 addition & 1 deletion assets/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ <h6 class="mb-0 text-white lh-100">Bitcoin</h6>
</center>
</div>
<small class="d-block text-right mt-3">
<a href="/bitcoin">Generate new address</a>
<a href="/bitcoin">Get unused address</a>
</small>
</div>
</main>
Expand Down
5 changes: 4 additions & 1 deletion config_example.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
[BDK]
datadir = ~/.bdk-bitcoin
datadir = .bdk-bitcoin
network = testnet
wallet = test
descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)"

# electrum server URL must support network specified above
electrum = "ssl://electrum.blockstream.info:60002"
199 changes: 106 additions & 93 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
#[macro_use]
extern crate log;
extern crate env_logger;
extern crate simple_server;
extern crate bdk;
extern crate serde_json;
extern crate bdk_macros;
extern crate env_logger;
extern crate ini;
extern crate serde_json;
extern crate simple_server;

use ini::Ini;

use std::env;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use std::str;
use std::env;
use std::str::FromStr;

use bdk::bitcoin::{Address, Network};
use bdk::sled;
use bdk::{Wallet};
use bdk::bitcoin::Address;
use bdk::Wallet;

use bdk::electrum_client::{Client, ElectrumApi, ListUnspentRes};
use simple_server::{Method, Server, StatusCode};
use bdk::electrum_client::{Client, ElectrumApi, ListUnspentRes, Error};

fn prepare_home_dir() -> PathBuf {
use bdk::blockchain::{
log_progress, AnyBlockchain, AnyBlockchainConfig, ConfigurableBlockchain,
ElectrumBlockchainConfig,
};
use bdk::sled::Tree;
use bdk::wallet::AddressIndex::LastUnused;
use std::sync::{Arc, Mutex};

fn prepare_home_dir(datadir: &str) -> PathBuf {
let mut dir = PathBuf::new();
dir.push(&dirs_next::home_dir().unwrap());
dir.push(".bdk-bitcoin");
dir.push(datadir);

if !dir.exists() {
info!("Creating home directory {}", dir.as_path().display());
Expand All @@ -36,64 +44,34 @@ fn prepare_home_dir() -> PathBuf {
dir
}

fn new_address() -> Result<Address, bdk::Error> {
let conf = Ini::load_from_file("config.ini").unwrap();

let section_bdk = conf.section(Some("BDK")).unwrap();
// let dir = section_bdk.get("datadir").unwrap();
let descriptor = section_bdk.get("descriptor").unwrap();
let network = section_bdk.get("network").unwrap();
let wallet = section_bdk.get("wallet").unwrap();

let database = sled::open(prepare_home_dir().to_str().unwrap()).unwrap();
let tree = database.open_tree(wallet).unwrap();

let wallet = Wallet::new_offline(
descriptor,
None,
network.parse().unwrap(),
tree,
)?;

let addr = wallet.get_new_address()?;
Ok(addr)
fn last_unused_address(wallet: &Wallet<AnyBlockchain, Tree>) -> Result<Address, bdk::Error> {
wallet.sync(log_progress(), None)?;
wallet.get_address(LastUnused)
}

fn client() -> Result<Client, Error> {
let conf = Ini::load_from_file("config.ini").unwrap();
let section_bdk = conf.section(Some("BDK")).unwrap();
let network = section_bdk.get("network").unwrap();
let url = match network.parse().unwrap() {
bdk::bitcoin::Network::Bitcoin => { "ssl://electrum.blockstream.info:50002" }
bdk::bitcoin::Network::Testnet => { "ssl://electrum.blockstream.info:60002"}
_ => { "" }
};
Client::new(url)
}

fn check_address(client: &Client, addr: &str, from_height: Option<usize>) -> Result<Vec<ListUnspentRes>, bdk::Error> {

let monitor_script = Address::from_str(addr)
.unwrap()
.script_pubkey();
fn check_address(
client: &Client,
addr: &str,
from_height: Option<usize>,
) -> Result<Vec<ListUnspentRes>, bdk::Error> {
let monitor_script = Address::from_str(addr).unwrap().script_pubkey();

let unspents = client
.script_list_unspent(&monitor_script)
.unwrap();
let unspents = client.script_list_unspent(&monitor_script).unwrap();

let array = unspents.into_iter()
let array = unspents
.into_iter()
.filter(|x| x.height >= from_height.unwrap_or(0))
.collect();

Ok(array)
}

fn html(address: &str) -> Result<String, std::io::Error> {
let client = client().unwrap();
fn html(electrum: &str, address: &str) -> Result<String, std::io::Error> {
let client = Client::new(electrum).unwrap();
let list = check_address(&client, &address, Option::from(0)).unwrap();

let status = match list.last() {
None => { "No onchain tx found yet".to_string() }
None => "No onchain tx found yet".to_string(),
Some(unspent) => {
let location = match unspent.height {
0 => "in mempool".to_string(),
Expand All @@ -114,43 +92,81 @@ fn html(address: &str) -> Result<String, std::io::Error> {
Ok(txt)
}

fn redirect() -> Result<String, std::io::Error> {
let address = new_address().unwrap();
fn redirect(address: Address) -> Result<String, std::io::Error> {
let link = format!("/bitcoin/?{}", address);
let html = format!("<head><meta name='robots' content='noindex'><meta http-equiv=\"Refresh\" content=\"0; URL={}\"></head>", link);
Ok(html)
}

/// Look up our server port number in PORT, for compatibility with Heroku.
fn get_server_port() -> u16 {
env::var("PORT").ok().and_then(|p| p.parse().ok()).unwrap_or(8080)
env::var("PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(8080)
}

fn main() {
env_logger::init();

// load config from ini file
let conf = Ini::load_from_file("config.ini").unwrap();
let section_bdk = conf.section(Some("BDK")).unwrap();
let datadir = section_bdk.get("datadir").unwrap();
let descriptor = section_bdk.get("descriptor").unwrap();
let network = section_bdk.get("network").unwrap();
let network = Network::from_str(network).unwrap();
let wallet = section_bdk.get("wallet").unwrap();
let electrum = section_bdk.get("electrum").unwrap().to_string();

let server = Server::new(|request, mut response| {
println!("Request: {} {}", request.method(), request.uri());
println!("Body: {}", str::from_utf8(request.body()).unwrap());
println!("Headers:");
// setup database
let database = sled::open(prepare_home_dir(datadir).to_str().unwrap()).unwrap();
let tree = database.open_tree(wallet).unwrap();

// setup electrum blockchain client
let electrum_config = AnyBlockchainConfig::Electrum(ElectrumBlockchainConfig {
url: electrum.clone(),
socks5: None,
retry: 3,
timeout: Some(2),
});

// create wallet shared by all requests
let wallet = Wallet::new(
descriptor,
None,
network,
tree,
AnyBlockchain::from_config(&electrum_config).unwrap(),
)
.unwrap();
wallet.sync(log_progress(), None).unwrap();
let wallet_mutex = Arc::new(Mutex::new(wallet));

let server = Server::new(move |request, mut response| {
debug!("Request: {} {}", request.method(), request.uri());
debug!("Body: {}", str::from_utf8(request.body()).unwrap());
debug!("Headers:");
for (key, value) in request.headers() {
println!("{}: {}", key, value.to_str().unwrap());
debug!("{}: {}", key, value.to_str().unwrap());
}

// unlock wallet mutex for this request
let wallet = wallet_mutex.lock().unwrap();

match (request.method(), request.uri().path()) {
(&Method::GET, "/bitcoin/api/new") => {
// curl 127.0.0.1:7878/bitcoin/api/new
let addr = new_address();
//info!("addr {}", addr.to_string());
return match addr {
(&Method::GET, "/bitcoin/api/last_unused") => {
let address = last_unused_address(&*wallet);
return match address {
Ok(a) => {
info!("new addr {}", a.to_string());
info!("last unused addr {}", a.to_string());
let value = serde_json::json!({
"network": a.network.to_string(),
"address": a.to_string()
});
"network": a.network.to_string(),
"address": a.to_string()
});
Ok(response.body(value.to_string().as_bytes().to_vec())?)
},
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?)
}
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?),
};
}
(&Method::GET, "/bitcoin/api/check") => {
Expand All @@ -160,40 +176,37 @@ fn main() {
let height = query.next().unwrap();
let h: usize = height.parse::<usize>().unwrap();

let client = client().unwrap();
let client = Client::new(electrum.as_str()).unwrap();
let list = check_address(&client, &addr, Option::from(h));
return match list {
Ok(list) => {
println!("addr {} height {}", addr, h);
debug!("addr {} height {}", addr, h);
for item in list.iter() {
println!("{} {}", item.value, item.height);
debug!("{} {}", item.value, item.height);
let _value = serde_json::json!({
"value": item.value,
"height": item.height,
"tx_hash": item.tx_hash,
});
}
Ok(response.body("".as_bytes().to_vec())?)
},
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?)
}
}
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?),
};
}
(&Method::GET, "/bitcoin/") => {
let address = request.uri().query().unwrap();
return match html(address) {
Ok(txt) => {
Ok(response.body(txt.as_bytes().to_vec())?)
},
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?)
}
let address = request.uri().query().unwrap(); // TODO handle missing address
return match html(electrum.as_str(), address) {
Ok(txt) => Ok(response.body(txt.as_bytes().to_vec())?),
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?),
};
}
(&Method::GET, "/bitcoin") => {
return match redirect() {
Ok(txt) => {
Ok(response.body(txt.as_bytes().to_vec())?)
},
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?)
}
let address = last_unused_address(&*wallet).unwrap();
return match redirect(address) {
Ok(txt) => Ok(response.body(txt.as_bytes().to_vec())?),
Err(e) => Ok(response.body(e.to_string().as_bytes().to_vec())?),
};
}
(&Method::GET, "/") => {
let link = "/bitcoin";
Expand Down

0 comments on commit 488f775

Please sign in to comment.