Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

Commit f8c75ae

Browse files
authored
Merge pull request #25 from benthecarman/bark
Add Bark adress parsing
2 parents 50e16a7 + fdf53ff commit f8c75ae

5 files changed

Lines changed: 441 additions & 6 deletions

File tree

‎src/ark.rs‎

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
//! Bark address parsing.
2+
//!
3+
//! Bark addresses are Bech32m strings with `ark`/`tark` HRPs, a policy-address version byte, and
4+
//! an opaque Bark payload. This module validates the address envelope while preserving the payload
5+
//! for wallets that understand Bark payments.
6+
7+
use alloc::vec::Vec;
8+
use core::fmt;
9+
use core::str::FromStr;
10+
11+
use bitcoin::bech32::primitives::decode::CheckedHrpstring;
12+
use bitcoin::bech32::{Bech32m, ByteIterExt, Fe32, Fe32IterExt, Hrp};
13+
use bitcoin::Network;
14+
15+
const HRP_MAINNET: Hrp = Hrp::parse_unchecked("ark");
16+
const HRP_TESTNET: Hrp = Hrp::parse_unchecked("tark");
17+
const VERSION_POLICY: Fe32 = Fe32::P;
18+
19+
/// A Bark policy address.
20+
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21+
pub struct BarkAddress {
22+
testnet: bool,
23+
payload: Vec<u8>,
24+
}
25+
26+
impl BarkAddress {
27+
/// Whether this address uses Bark's test-network HRP (`tark`).
28+
pub fn is_testnet(&self) -> bool {
29+
self.testnet
30+
}
31+
32+
/// The raw address payload after the Bark address version field.
33+
pub fn payload(&self) -> &[u8] {
34+
&self.payload
35+
}
36+
37+
/// Requires that this address matches the given Bitcoin network.
38+
pub fn require_network(self, network: Network) -> Result<Self, NetworkValidationError> {
39+
if self.testnet == (network != Network::Bitcoin) {
40+
Ok(self)
41+
} else {
42+
Err(NetworkValidationError)
43+
}
44+
}
45+
}
46+
47+
impl FromStr for BarkAddress {
48+
type Err = ParseAddressError;
49+
50+
fn from_str(s: &str) -> Result<Self, Self::Err> {
51+
let mut checked =
52+
CheckedHrpstring::new::<Bech32m>(s).map_err(|_| ParseAddressError::Bech32)?;
53+
let testnet = hrp_is_testnet(checked.hrp())?;
54+
55+
let version = checked.remove_witness_version().ok_or(ParseAddressError::Empty)?;
56+
if version != VERSION_POLICY {
57+
return Err(ParseAddressError::UnknownVersion);
58+
}
59+
60+
let payload: Vec<_> = checked.byte_iter().collect();
61+
if payload.is_empty() {
62+
return Err(ParseAddressError::Invalid("empty Bark address payload"));
63+
}
64+
Ok(BarkAddress { testnet, payload })
65+
}
66+
}
67+
68+
impl fmt::Display for BarkAddress {
69+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70+
let hrp = hrp_for_testnet(self.testnet);
71+
let chars = [VERSION_POLICY]
72+
.into_iter()
73+
.chain(self.payload.iter().copied().bytes_to_fes())
74+
.with_checksum::<Bech32m>(&hrp)
75+
.chars();
76+
for c in chars {
77+
let mut buf = [0; 4];
78+
f.write_str(c.encode_utf8(&mut buf))?;
79+
}
80+
Ok(())
81+
}
82+
}
83+
84+
impl fmt::Debug for BarkAddress {
85+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86+
fmt::Display::fmt(self, f)
87+
}
88+
}
89+
90+
/// Error parsing a Bark address.
91+
#[derive(Clone, Debug, PartialEq, Eq)]
92+
pub enum ParseAddressError {
93+
/// The address was not a valid Bech32m string.
94+
Bech32,
95+
/// The address HRP was not `ark` or `tark`.
96+
Hrp,
97+
/// The address had no version field.
98+
Empty,
99+
/// The address version was not supported.
100+
UnknownVersion,
101+
/// The address payload was malformed.
102+
Invalid(&'static str),
103+
}
104+
105+
impl fmt::Display for ParseAddressError {
106+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107+
match self {
108+
Self::Bech32 => f.write_str("bech32m decoding error"),
109+
Self::Hrp => f.write_str("invalid Bark address HRP"),
110+
Self::Empty => f.write_str("empty Bark address"),
111+
Self::UnknownVersion => f.write_str("unknown Bark address version"),
112+
Self::Invalid(msg) => f.write_str(msg),
113+
}
114+
}
115+
}
116+
117+
/// Error returned when an address does not match the requested network.
118+
#[derive(Clone, Debug, PartialEq, Eq)]
119+
pub struct NetworkValidationError;
120+
121+
fn hrp_is_testnet(hrp: Hrp) -> Result<bool, ParseAddressError> {
122+
if hrp == HRP_MAINNET {
123+
Ok(false)
124+
} else if hrp == HRP_TESTNET {
125+
Ok(true)
126+
} else {
127+
Err(ParseAddressError::Hrp)
128+
}
129+
}
130+
131+
fn hrp_for_testnet(testnet: bool) -> Hrp {
132+
if testnet {
133+
HRP_TESTNET
134+
} else {
135+
HRP_MAINNET
136+
}
137+
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use alloc::string::ToString;
142+
143+
use super::*;
144+
145+
const BARK_MAINNET: &str = "ark1pwh9vsmezqqpharv69q4z8m6x364d5m5prnmcalcalq9pdmzw0y7mpveck4pcfhezqypczkrrj3lkx5ue4qrf4jc7ztpt9htdttmh2judhqnu7aue8p0y9mqkr4cf5";
146+
const BARK_TESTNET: &str = "tark1pwh9vsmezqqpharv69q4z8m6x364d5m5prnmcalcalq9pdmzw0y7mpveck4pcfhezqypczkrrj3lkx5ue4qrf4jc7ztpt9htdttmh2judhqnu7aue8p0y9mq47jn9z";
147+
148+
#[test]
149+
fn parse_bark_address() {
150+
let address = BarkAddress::from_str(BARK_MAINNET).unwrap();
151+
assert!(!address.is_testnet());
152+
assert_eq!(address.to_string(), BARK_MAINNET);
153+
assert_eq!(BarkAddress::from_str(&BARK_MAINNET.to_ascii_uppercase()).unwrap(), address);
154+
assert!(address.payload().len() > 4);
155+
assert!(address.clone().require_network(Network::Bitcoin).is_ok());
156+
assert!(address.require_network(Network::Signet).is_err());
157+
}
158+
159+
#[test]
160+
fn parse_testnet_bark_address() {
161+
let address = BarkAddress::from_str(BARK_TESTNET).unwrap();
162+
assert!(address.is_testnet());
163+
assert_eq!(address.to_string(), BARK_TESTNET);
164+
assert_eq!(BarkAddress::from_str(&BARK_TESTNET.to_ascii_uppercase()).unwrap(), address);
165+
assert!(address.clone().require_network(Network::Signet).is_ok());
166+
assert!(address.require_network(Network::Bitcoin).is_err());
167+
}
168+
169+
#[test]
170+
fn reject_empty_bark_payload() {
171+
let address = BarkAddress { testnet: false, payload: Vec::new() }.to_string();
172+
173+
assert_eq!(
174+
BarkAddress::from_str(&address),
175+
Err(ParseAddressError::Invalid("empty Bark address payload"))
176+
);
177+
}
178+
}

‎src/dns_resolver.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ mod tests {
9999
},
100100
PaymentMethod::LightningBolt12(_) => {},
101101
PaymentMethod::OnChain { .. } => {},
102+
PaymentMethod::Bark(_) => {},
102103
PaymentMethod::Cashu(_) => {},
103104
}
104105
}

‎src/http_resolver.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ mod tests {
341341
},
342342
PaymentMethod::LightningBolt12(_) => {},
343343
PaymentMethod::OnChain { .. } => {},
344+
PaymentMethod::Bark(_) => {},
344345
PaymentMethod::Cashu(_) => panic!("Should only resolve to BOLT 11"),
345346
}
346347
}
@@ -388,6 +389,7 @@ mod tests {
388389
},
389390
PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"),
390391
PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"),
392+
PaymentMethod::Bark(_) => panic!("Should only resolve to BOLT 11"),
391393
PaymentMethod::Cashu(_) => panic!("Should only resolve to BOLT 11"),
392394
}
393395
}
@@ -428,6 +430,7 @@ mod tests {
428430
},
429431
PaymentMethod::LightningBolt12(_) => panic!("Should only resolve to BOLT 11"),
430432
PaymentMethod::OnChain(_) => panic!("Should only resolve to BOLT 11"),
433+
PaymentMethod::Bark(_) => panic!("Should only resolve to BOLT 11"),
431434
PaymentMethod::Cashu(_) => panic!("Should only resolve to BOLT 11"),
432435
}
433436
}

0 commit comments

Comments
 (0)