Skip to content

Commit

Permalink
bip21: support bip21 uri
Browse files Browse the repository at this point in the history
Signed-off-by: lvaccaro <[email protected]>
  • Loading branch information
lvaccaro committed Nov 28, 2023
1 parent 1226ad4 commit ac6e2fe
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 50 deletions.
125 changes: 125 additions & 0 deletions bitcoin/src/bip21.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use bdk::bitcoin::{Address, Amount, Denomination};
use percent_encoding::{utf8_percent_encode, AsciiSet, CONTROLS};
use std::collections::BTreeMap;
use std::str::FromStr;
use url::{ParseError, Url};

pub struct Bip21 {
pub scheme: String,
pub address: Address,
pub amount: Option<Amount>,
pub label: Option<String>,
pub message: Option<String>,
}

impl Bip21 {
pub fn as_str(&self) -> Result<String, ParseError> {
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ');
let mut query = BTreeMap::new();
if let Some(a) = &self.amount {
query.insert("amount", a.as_btc().to_string());
}
if let Some(l) = &self.label {
let encoded = utf8_percent_encode(l.as_str(), FRAGMENT).to_string();
query.insert("label", encoded);
}
if let Some(m) = &self.message {
let encoded = utf8_percent_encode(m.as_str(), FRAGMENT).to_string();
query.insert("message", encoded);
}
let params = query
.into_iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>();
let url = format!(
"{}:{}?{}",
self.scheme,
self.address.to_string(),
params.join("&")
);
Ok(url.trim_end_matches("?").to_string())
}

pub fn parse(string: &str) -> Result<Self, ParseError> {
let url = Url::parse(string)?;
let mut params: BTreeMap<String, String> = BTreeMap::new();
for (k, v) in url.query_pairs().into_owned() {
params.insert(k, v);
}
let scheme = url.scheme().to_string();
let address = Address::from_str(url.path()).map_err(|_| ParseError::IdnaError)?;
let amount = match params.get("amount") {
None => None,
Some(amount) => Some(
Amount::from_str_in(amount.as_str(), Denomination::Bitcoin)
.map_err(|_| ParseError::IdnaError)?,
),
};
let label = params.get("label").cloned();
let message = params.get("message").cloned();
Ok(Bip21 {
scheme,
address,
amount,
label,
message,
})
}
}

#[cfg(test)]
mod test {
use crate::bip21::Bip21;
use bdk::bitcoin::{Address, Amount, Denomination};
use std::str::FromStr;

#[test]
fn serialize() {
let mut bip21 = Bip21 {
scheme: "bitcoin".to_string(),
address: Address::from_str("2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK").unwrap(),
amount: None,
label: None,
message: None,
};
assert_eq!(
"bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK",
bip21.as_str().unwrap()
);
bip21.label = Some("Luke-Jr".to_string());
assert_eq!(
"bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK?label=Luke-Jr",
bip21.as_str().unwrap()
);
bip21.amount = Some(Amount::from_str_in("20.3", Denomination::Bitcoin).unwrap());
assert_eq!(
"bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK?amount=20.3&label=Luke-Jr",
bip21.as_str().unwrap()
);
bip21.amount = Some(Amount::from_str_in("50", Denomination::Bitcoin).unwrap());
bip21.message = Some("Donation for project xyz".to_string());
assert_eq!(
"bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz",
bip21.as_str().unwrap()
);
}

#[test]
fn deserialize() {
let url1 = Bip21::parse("bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK").unwrap();
assert_eq!(
"bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK",
url1.as_str().unwrap()
);
assert_eq!("bitcoin", url1.scheme);
assert_eq!(
"2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK",
url1.address.to_string()
);
let url2 = Bip21::parse("bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz").unwrap();
assert_eq!("bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK?amount=50&label=Luke-Jr&message=Donation%20for%20project%20xyz", url2.as_str().unwrap());
assert_eq!(50 as f64, url2.amount.unwrap().as_btc());
assert_eq!("Luke-Jr", url2.label.unwrap().as_str());
assert_eq!("Donation for project xyz", url2.message.unwrap().as_str());
}
}
4 changes: 3 additions & 1 deletion bitcoin/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
pub mod config;
pub mod bip21;

pub extern crate bdk;
extern crate ini;
extern crate structopt;
extern crate percent_encoding;
extern crate url;

use bdk::bitcoin::Address;
use bdk::blockchain::{
Expand Down
55 changes: 32 additions & 23 deletions server/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ use std::str::FromStr;
use crate::wallet;
use wallet::{Error, gen_err};

#[derive(Default)]
pub struct Page {
pub url: String,
pub network: String,
pub address: String,
pub amount: Option<String>,
pub label: Option<String>,
pub message: Option<String>,
pub status: Option<String>,
}

const CSS2: &str = include_str!("../../assets/css/style.css");
const CSS1: &str = include_str!("../../assets/css/styles.css");

Expand All @@ -29,22 +40,11 @@ fn inner_header(title: &str) -> Markup {
return header
}

fn inner_address(address: &str) -> Markup {
let partial = html! {
div class="media text-muted pt-3" {
p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray" {
span { (address) }
}
}
};
partial
}

fn inner_status(status: &str) -> Markup {
fn inner_section(text: &str) -> Markup {
let partial = html! {
div class="media text-muted pt-3" {
p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray" {
span { (status) }
span { (text) }
}
}
};
Expand Down Expand Up @@ -107,12 +107,11 @@ fn address_qr(network: &str, address: &str) -> Result<String, Error> {
}
}

pub fn page(network: &str, address: &str, status: &str) -> Result<String, Error> {
let meta_http_content = format!("{}; URL=/?{}", 10, address);
let address_link = address_link(network, address)?;
let address_qr = address_qr(network, address)?;
pub fn render(page: Page) -> Result<String, Error> {
let meta_http_content = format!("{}; URL=/?{}", 10, page.url);
let address_link = address_link(page.network.as_str(), page.address.as_str())?;
let address_qr = address_qr(page.network.as_str(), page.address.as_str())?;
let qr = create_bmp_base64_qr(&address_qr).map_err(|_| gen_err())?;
println!("{}",network);

let html = html! {
(DOCTYPE)
Expand All @@ -121,25 +120,35 @@ pub fn page(network: &str, address: &str, status: &str) -> Result<String, Error>
meta charset="UTF-8";
meta name="robots" content="noindex";
meta http-equiv="Refresh" content=(meta_http_content);
title { (address) }
title { (page.address) }
style { (CSS1) }
style { (CSS2) }
}
body {
div.container.center.headings--one-size {
(inner_header(network))
(inner_header(page.network.as_str()))
div.content {
div.index-content {

div.framed.framed-paragraph {
div class="center" {
img class="qr" src=(qr) { }
br { }
(inner_address(address))
(inner_section(page.address.as_str()))
}
}

(inner_status(status))
@if let Some(amount) = &page.amount {
(inner_section(format!("Amount {} sats", amount.to_string().as_str()).as_str()))
}
@if let Some(label) = &page.label {
(inner_section(format!("Label {}", label.to_string().as_str()).as_str()))
}
@if let Some(message) = &page.message {
(inner_section(format!("Message {}", message.to_string().as_str()).as_str()))
}
@if let Some(status) = &page.status {
(inner_section(format!("{}", status.to_string().as_str()).as_str()))
}
a href=(address_link) { "Open in wallet app" }
}
}
Expand Down
50 changes: 24 additions & 26 deletions server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ use uriparse;
use std::convert::TryFrom;

use crate::{html, wallet};
use crate::html::not_found;
use crate::html::{not_found, Page};
use wallet::{Wallet, Error, gen_err};

use btctipserver_bitcoin::BTCWallet;

pub fn run_server(url: &str, wallet: Wallet) {
let wallet_mutex = Arc::new(Mutex::new(wallet));
let server = Server::http(url).unwrap();
Expand Down Expand Up @@ -63,30 +61,30 @@ pub fn page(
wallet: &mut Wallet,
uri: &str,
) -> Result<String, Error> {
let network = wallet.network()?;
let mut address = uri;

BTCWallet::Bip21::parse(uri).unwrap();


Bip21::parse("bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK")
let address = match parsed.query().unwrap() {

let mut page = Page {
network: wallet.network()?,
url: format!("{}", uri),
address: format!("{}", uri),
..Default::default()
};



if parsed.query().unwrap().starts_with(wallet.schema()) {
address = Bip21::parse("bitcoin:2NDxuABdg2uqk9MouV6f53acAwaY2GwKVHK").unwrap();
}

let address = uri;
let mine = wallet.is_my_address(address)?;
if uri.starts_with(wallet.schema()) {
println!("{}",wallet.schema());
if let Ok(bip21) = btctipserver_bitcoin::bip21::Bip21::parse(uri) {
page.address = bip21.address.to_string();
if let Some(amount) = bip21.amount {
page.amount = Some(amount.as_sat().to_string());
}
page.label = bip21.label;
page.label = bip21.message;
}
}
let mine = wallet.is_my_address(page.address.as_str())?;
if !mine {
return Ok(format!("Address {} is not mine", address));
return Ok(format!("Address {} is not mine", page.address));
}
let results = wallet
.balance_address(&address, Option::from(0))
.balance_address(&page.address, Option::from(0))
.map_err(|_| gen_err())?
.into_iter()
.filter(|(_, v)| *v > 0)
Expand All @@ -95,9 +93,9 @@ pub fn page(
.collect::<Vec<String>>()
.join(", ");

let txt = match results.is_empty() {
true => "No tx found yet".to_string(),
_ => results,
page.status = match results.is_empty() {
true => Some("No tx found yet".to_string()),
_ => Some(results),
};
html::page(network.as_str(), address, txt.as_str())
html::render(page)
}

0 comments on commit ac6e2fe

Please sign in to comment.