@@ -20,6 +20,8 @@ const PODCAST_EPISODE: &str = "podcast:item:guid:";
2020const PODCAST_PUBLISHER : & str = "podcast:publisher:guid:" ;
2121const MOVIE : & str = "isan:" ;
2222const 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
6585impl 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) ]
126201mod 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