Skip to content

Commit 8fcecbb

Browse files
tomproyukibtc
authored andcommitted
nostr: add NIP-73 blockchain address and transaction
Closes #879 Pull-Request: #879 Signed-off-by: Yuki Kishimoto <[email protected]>
1 parent 23ab667 commit 8fcecbb

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
- nostr: add `UnsignedEvent::id` method ([Yuki Kishimoto] at https://github.com/rust-nostr/nostr/pull/868)
4444
- nostr: add `TagKind::single_letter` constructor ([awiteb] at https://github.com/rust-nostr/nostr/pull/871)
45+
- nostr: add NIP-73 blockchain address and transaction ([Thomas Profelt] at https://github.com/rust-nostr/nostr/pull/879)
4546
- blossom: add new crate with Blossom support ([Daniel D’Aquino] at https://github.com/rust-nostr/nostr/pull/838)
4647
- mls-storage: add new crate with traits and types for mls storage implementations ([JeffG] at https://github.com/rust-nostr/nostr/pull/836)
4748
- mls-memory-storage: add an in-memory implementation for MLS ([JeffG] at https://github.com/rust-nostr/nostr/pull/839)
@@ -1267,3 +1268,4 @@ added `nostrdb` storage backend, added NIP32 and completed NIP51 support and mor
12671268
[magine]: https://github.com/ma233 (?)
12681269
[daywalker90]: https://github.com/daywalker90 (nostr:npub1kuemsj7xryp0uje36dr53scn9mxxh8ema90hw9snu46633n9n2hqp3drjt)
12691270
[Daniel D’Aquino]: https://github.com/danieldaquino (nostr:npub13v47pg9dxjq96an8jfev9znhm0k7ntwtlh9y335paj9kyjsjpznqzzl3l8)
1271+
[Thomas Profelt]: https://github.com/tompro (nostr:npub1rf0lc5dpyvpl6q3dfq0n0mtqc0maxa0kdehcj9nc5884fzufuzxqv67gj6)

crates/nostr/src/nips/nip73.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const PODCAST_EPISODE: &str = "podcast:item:guid:";
2020
const PODCAST_PUBLISHER: &str = "podcast:publisher:guid:";
2121
const MOVIE: &str = "isan:";
2222
const PAPER: &str = "doi:";
23+
const BLOCKCHAIN_TX: &str = ":tx:";
24+
const BLOCKCHAIN_ADDR: &str = ":address:";
2325

2426
/// NIP73 error
2527
#[derive(Debug, PartialEq, Eq)]
@@ -60,6 +62,24 @@ pub enum ExternalContentId {
6062
Movie(String),
6163
/// Paper
6264
Paper(String),
65+
/// Blockchain Transaction
66+
BlockchainTransaction {
67+
/// The blockchain name (e.g., "bitcoin", "ethereum")
68+
chain: String,
69+
/// A lower case hex transaction id
70+
transaction_hash: String,
71+
/// The chain id if one is required
72+
chain_id: Option<String>,
73+
},
74+
/// Blockchain Address
75+
BlockchainAddress {
76+
/// The blockchain name (e.g., "bitcoin", "ethereum")
77+
chain: String,
78+
/// The on-chain address
79+
address: String,
80+
/// The chain id if one is required
81+
chain_id: Option<String>,
82+
},
6383
}
6484

6585
impl fmt::Display for ExternalContentId {
@@ -74,6 +94,34 @@ impl fmt::Display for ExternalContentId {
7494
Self::PodcastPublisher(guid) => write!(f, "{PODCAST_PUBLISHER}{guid}"),
7595
Self::Movie(movie) => write!(f, "{MOVIE}{movie}"),
7696
Self::Paper(paper) => write!(f, "{PAPER}{paper}"),
97+
Self::BlockchainTransaction {
98+
chain,
99+
transaction_hash,
100+
chain_id,
101+
} => {
102+
write!(
103+
f,
104+
"{chain}{}{BLOCKCHAIN_TX}{transaction_hash}",
105+
chain_id
106+
.as_ref()
107+
.map(|id| format!(":{id}"))
108+
.unwrap_or_default()
109+
)
110+
}
111+
Self::BlockchainAddress {
112+
chain,
113+
address,
114+
chain_id,
115+
} => {
116+
write!(
117+
f,
118+
"{chain}{}{BLOCKCHAIN_ADDR}{address}",
119+
chain_id
120+
.as_ref()
121+
.map(|id| format!(":{id}"))
122+
.unwrap_or_default()
123+
)
124+
}
77125
}
78126
}
79127
}
@@ -114,6 +162,24 @@ impl FromStr for ExternalContentId {
114162
return Ok(Self::Paper(stripped.to_string()));
115163
}
116164

165+
if let Some((chain, hash)) = content.split_once(BLOCKCHAIN_TX) {
166+
let (chain, chain_id) = extract_chain_id(chain);
167+
return Ok(Self::BlockchainTransaction {
168+
chain,
169+
transaction_hash: hash.to_string(),
170+
chain_id,
171+
});
172+
}
173+
174+
if let Some((chain, address)) = content.split_once(BLOCKCHAIN_ADDR) {
175+
let (chain, chain_id) = extract_chain_id(chain);
176+
return Ok(Self::BlockchainAddress {
177+
chain,
178+
address: address.to_string(),
179+
chain_id,
180+
});
181+
}
182+
117183
if let Ok(url) = Url::parse(content) {
118184
return Ok(Self::Url(url));
119185
}
@@ -122,6 +188,15 @@ impl FromStr for ExternalContentId {
122188
}
123189
}
124190

191+
/// Given a blockchain name returns the chain and the optional chain id if any.
192+
fn extract_chain_id(chain: &str) -> (String, Option<String>) {
193+
match chain.split_once(':') {
194+
None => (chain.to_string(), None),
195+
Some((chain, "")) => (chain.to_string(), None),
196+
Some((chain, chain_id)) => (chain.to_string(), Some(chain_id.to_string())),
197+
}
198+
}
199+
125200
#[cfg(test)]
126201
mod tests {
127202
use super::*;
@@ -164,6 +239,33 @@ mod tests {
164239
ExternalContentId::Paper("10.1000/182".to_string()).to_string(),
165240
"doi:10.1000/182"
166241
);
242+
assert_eq!(
243+
ExternalContentId::BlockchainTransaction {
244+
chain: "bitcoin".to_string(),
245+
transaction_hash: "txid".to_string(),
246+
chain_id: None,
247+
}
248+
.to_string(),
249+
"bitcoin:tx:txid"
250+
);
251+
assert_eq!(
252+
ExternalContentId::BlockchainTransaction {
253+
chain: "ethereum".to_string(),
254+
transaction_hash: "txid".to_string(),
255+
chain_id: Some("100".to_string()),
256+
}
257+
.to_string(),
258+
"ethereum:100:tx:txid"
259+
);
260+
assert_eq!(
261+
ExternalContentId::BlockchainAddress {
262+
chain: "ethereum".to_string(),
263+
address: "onchain_address".to_string(),
264+
chain_id: Some("100".to_string()),
265+
}
266+
.to_string(),
267+
"ethereum:100:address:onchain_address"
268+
);
167269
}
168270

169271
#[test]
@@ -204,6 +306,46 @@ mod tests {
204306
ExternalContentId::from_str("doi:10.1000/182").unwrap(),
205307
ExternalContentId::Paper("10.1000/182".to_string())
206308
);
309+
assert_eq!(
310+
ExternalContentId::from_str(
311+
"bitcoin:tx:a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"
312+
)
313+
.unwrap(),
314+
ExternalContentId::BlockchainTransaction {
315+
chain: "bitcoin".to_string(),
316+
transaction_hash:
317+
"a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d".to_string(),
318+
chain_id: None,
319+
}
320+
);
321+
assert_eq!(
322+
ExternalContentId::from_str("ethereum:100:tx:0x98f7812be496f97f80e2e98d66358d1fc733cf34176a8356d171ea7fbbe97ccd").unwrap(),
323+
ExternalContentId::BlockchainTransaction {
324+
chain: "ethereum".to_string(),
325+
transaction_hash: "0x98f7812be496f97f80e2e98d66358d1fc733cf34176a8356d171ea7fbbe97ccd".to_string(),
326+
chain_id: Some("100".to_string()),
327+
}
328+
);
329+
assert_eq!(
330+
ExternalContentId::from_str("bitcoin:address:1HQ3Go3ggs8pFnXuHVHRytPCq5fGG8Hbhx")
331+
.unwrap(),
332+
ExternalContentId::BlockchainAddress {
333+
chain: "bitcoin".to_string(),
334+
address: "1HQ3Go3ggs8pFnXuHVHRytPCq5fGG8Hbhx".to_string(),
335+
chain_id: None,
336+
}
337+
);
338+
assert_eq!(
339+
ExternalContentId::from_str(
340+
"ethereum:100:address:0xd8da6bf26964af9d7eed9e03e53415d37aa96045"
341+
)
342+
.unwrap(),
343+
ExternalContentId::BlockchainAddress {
344+
chain: "ethereum".to_string(),
345+
address: "0xd8da6bf26964af9d7eed9e03e53415d37aa96045".to_string(),
346+
chain_id: Some("100".to_string()),
347+
}
348+
);
207349
}
208350

209351
#[test]

0 commit comments

Comments
 (0)