Skip to content

Commit

Permalink
feat (sdk-common): Add BIP21 URI parsing for Liquid (#1058)
Browse files Browse the repository at this point in the history
  • Loading branch information
hydra-yse authored Aug 8, 2024
1 parent 0eb0f6a commit 84ce07c
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 29 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ jobs:
working-directory: libs/sdk-common
run: cargo test

- name: Run sdk-common tests with 'liquid' feature
working-directory: libs/sdk-common
run: cargo test --features=liquid

clippy:
name: Clippy
runs-on: ubuntu-latest
Expand Down
111 changes: 107 additions & 4 deletions libs/Cargo.lock

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

2 changes: 1 addition & 1 deletion libs/sdk-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ tonic = { workspace = true, features = [
uniffi_build = { version = "0.23.0" }
uniffi_bindgen = "0.23.0"
anyhow = { workspace = true }
glob = "0.3.1"
glob = "0.3.1"
7 changes: 6 additions & 1 deletion libs/sdk-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ tonic = { workspace = true, features = [
"tls-webpki-roots",
] }
url = "2.5.0"
elements = { version = "0.24.1", optional = true }
urlencoding = { version = "2.1.3" }

[dev-dependencies]
bitcoin = { workspace = true, features = ["rand"] }
Expand All @@ -37,4 +39,7 @@ tokio = { workspace = true }
once_cell = { workspace = true }

[build-dependencies]
tonic-build = { workspace = true }
tonic-build = { workspace = true }

[features]
liquid = ["dep:elements"]
108 changes: 108 additions & 0 deletions libs/sdk-common/src/input_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ use bip21::Uri;
use bitcoin::bech32;
use bitcoin::bech32::FromBase32;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use LnUrlRequestData::*;

use crate::prelude::*;

#[cfg(feature = "liquid")]
use self::liquid::bip21::LiquidAddressData;

/// Parses generic user input, typically pasted from clipboard or scanned from a QR.
///
/// # Examples
Expand Down Expand Up @@ -175,6 +179,11 @@ pub async fn parse(input: &str) -> Result<InputType> {
};
}

#[cfg(feature = "liquid")]
if let Ok(address) = parse_liquid_address(input) {
return Ok(InputType::LiquidAddress { address });
}

if let Ok(invoice) = parse_invoice(input) {
return Ok(InputType::Bolt11 { invoice });
}
Expand Down Expand Up @@ -391,6 +400,15 @@ pub enum InputType {
address: BitcoinAddressData,
},

/// # Supported standards
///
/// - plain on-chain liquid address
/// - BIP21 on liquid/liquidtestnet
#[cfg(feature = "liquid")]
LiquidAddress {
address: LiquidAddressData,
},

/// Also covers URIs like `bitcoin:...&lightning=bolt11`. In this case, it returns the BOLT11
/// and discards all other data.
Bolt11 {
Expand Down Expand Up @@ -577,6 +595,51 @@ pub struct BitcoinAddressData {
pub message: Option<String>,
}

#[derive(Debug)]
pub enum URISerializationError {
UnsupportedNetwork,
AssetIdMissing,
InvalidAddress,
}

impl BitcoinAddressData {
/// Converts the structure to a BIP21 URI while also
/// ensuring that all the fields are valid
pub fn to_uri(&self) -> Result<String, URISerializationError> {
self.address
.parse::<bitcoin::Address>()
.map_err(|_| URISerializationError::InvalidAddress)?;

let mut optional_keys = HashMap::new();

if let Some(amount_sat) = self.amount_sat {
let amount_btc = amount_sat as f64 / 100_000_000.0;
optional_keys.insert("amount", format!("{amount_btc:.8}"));
}

if let Some(message) = &self.message {
optional_keys.insert("message", urlencoding::encode(message).to_string());
}

if let Some(label) = &self.label {
optional_keys.insert("label", urlencoding::encode(label).to_string());
}

match optional_keys.is_empty() {
true => Ok(self.address.clone()),
false => {
let scheme = "bitcoin";
let suffix_str = optional_keys
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<String>>()
.join("&");
Ok(format!("{scheme}:{}{suffix_str}", self.address))
}
}
}
}

impl From<Uri<'_>> for BitcoinAddressData {
fn from(uri: Uri) -> Self {
BitcoinAddressData {
Expand Down Expand Up @@ -722,6 +785,51 @@ pub(crate) mod tests {
Ok(())
}

#[tokio::test]
#[cfg(feature = "liquid")]
async fn test_liquid_address() -> Result<()> {
assert!(parse("tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk")
.await
.is_ok());
assert!(parse("liquidnetwork:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk")
.await
.is_ok());
assert!(parse("wrong-net:tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk").await.is_err());
assert!(parse("liquidnetwork:testinvalidaddress").await.is_err());

let address: elements::Address = "tlq1qqw5ur50rnvcx33vmljjtnez3hrtl6n7vs44tdj2c9fmnxrrgzgwnhw6jtpn8cljkmlr8tgfw9hemrr5y8u2nu024hhak3tpdk".parse()?;
let amount_btc = 0.00001; // 1000 sats
let label = "label";
let message = "this%20is%20a%20message";
let asset_id = elements::issuance::AssetId::LIQUID_BTC.to_string();
let output = parse(&format!(
"liquidnetwork:{}?amount={amount_btc}&assetid={asset_id}&label={label}&message={message}",
address
))
.await?;

if let InputType::LiquidAddress {
address: liquid_address_data,
} = output
{
assert_eq!(Network::Bitcoin, liquid_address_data.network);
assert_eq!(address.to_string(), liquid_address_data.address.to_string());
assert_eq!(
Some((amount_btc * 100_000_000.0) as u64),
liquid_address_data.amount_sat
);
assert_eq!(Some(label.to_string()), liquid_address_data.label);
assert_eq!(
Some(urlencoding::decode(message).unwrap().into_owned()),
liquid_address_data.message
);
} else {
panic!("Invalid input type received");
}

Ok(())
}

#[tokio::test]
async fn test_bolt11() -> Result<()> {
let bolt11 = "lnbc110n1p38q3gtpp5ypz09jrd8p993snjwnm68cph4ftwp22le34xd4r8ftspwshxhmnsdqqxqyjw5qcqpxsp5htlg8ydpywvsa7h3u4hdn77ehs4z4e844em0apjyvmqfkzqhhd2q9qgsqqqyssqszpxzxt9uuqzymr7zxcdccj5g69s8q7zzjs7sgxn9ejhnvdh6gqjcy22mss2yexunagm5r2gqczh8k24cwrqml3njskm548aruhpwssq9nvrvz";
Expand Down
Loading

0 comments on commit 84ce07c

Please sign in to comment.