Skip to content

Commit b366277

Browse files
authored
Use npub as id, fix sending to nostr only contact (#544)
* Use npub as id, fix sending to nostr only contact * Review fixes * Review fixes
1 parent b0c2185 commit b366277

File tree

12 files changed

+194
-144
lines changed

12 files changed

+194
-144
lines changed

CHANGELOG.md

Lines changed: 55 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,39 @@
11
# 0.3.16
22

33
* Set the BTC network in the identity and check, if the persisted network is the same as the one configured in the application, failing if it doesn't.
4+
* Nostr npub as primary key in Nostr contacts (breaking DB change)
45

56
# 0.3.15
67

78
* Upload and download files to and from Nostr using Blossom
8-
* Add `nostr_hash` to `File` (breaking DB change)
9+
* Add `nostr_hash` to `File` (breaking DB change)
910
* Fix MintRequestResponse return type
1011
* Bill Caching for multiple identities (breaking DB change)
1112

1213
# 0.3.14
1314

1415
* Minting
15-
* Added notifications and offers
16-
* Added timestamps for status
17-
* Call mint endpoint for cancelling
18-
* Use mint nostr relays from network and fall back to identity ones
19-
* Add endpoints to accept, or reject an offer from a mint
20-
* Add logic to check keyset info, mint and create proofs
21-
* Add logic to recover proofs if something goes wrong
22-
* Add logic to check if proofs were spent
16+
* Added notifications and offers
17+
* Added timestamps for status
18+
* Call mint endpoint for cancelling
19+
* Use mint nostr relays from network and fall back to identity ones
20+
* Add endpoints to accept, or reject an offer from a mint
21+
* Add logic to check keyset info, mint and create proofs
22+
* Add logic to recover proofs if something goes wrong
23+
* Add logic to check if proofs were spent
2324

2425
# 0.3.13
2526

2627
* Minting
27-
* Add default mint configuration options
28-
* `default_mint_url`
29-
* `default_mint_node_id`
30-
* Implement `request_to_mint`
31-
* Add minting status flag to bill
32-
* Add endpoint to fetch minting status for a bill
33-
* Add logic for checking mint request status on the mint
34-
* Add cronjob to check mint requests
35-
* Add endpoint to cancel mint requests
28+
* Add default mint configuration options
29+
* `default_mint_url`
30+
* `default_mint_node_id`
31+
* Implement `request_to_mint`
32+
* Add minting status flag to bill
33+
* Add endpoint to fetch minting status for a bill
34+
* Add logic for checking mint request status on the mint
35+
* Add cronjob to check mint requests
36+
* Add endpoint to cancel mint requests
3637
* Change bitcoin addresses and descriptor to p2wpkh
3738
* Suppress logging from crates we don't control
3839

@@ -48,36 +49,36 @@
4849
* Remove the `bcr-ebill-web` crate
4950
* Use a list of Nostr relays everywhere, instead of a single one, including in the config
5051
* Add Blank Endorse Bill data model implementation
51-
* Rename `IdentityPublicData` to `BillIdentParticipant`
52-
* same for `LightIdentityPublicData`
53-
* Introduce the concept of `BillParticipant`, with the variants `Ident` and `Anon`
54-
* `Anon` includes a `BillAnonParticipant`
55-
* `Ident` includes a `BillIdentParticipant`
56-
* Use `BillParticipant` in parts of the bill where a participant can be anonymous
52+
* Rename `IdentityPublicData` to `BillIdentParticipant`
53+
* same for `LightIdentityPublicData`
54+
* Introduce the concept of `BillParticipant`, with the variants `Ident` and `Anon`
55+
* `Anon` includes a `BillAnonParticipant`
56+
* `Ident` includes a `BillIdentParticipant`
57+
* Use `BillParticipant` in parts of the bill where a participant can be anonymous
5758
* Add the possibility to add anonymous contacts
58-
* They only have Node Id, Name and E-Mail as fields
59-
* E-Mail is optional
60-
* This changes the data model for contacts, `email` and `postal_address` are now optional
61-
* Additional validation rules ensure the fields can only be set for non-anon contacts
62-
* Adds an endpoint `Api.contact().deanonymize()` to de-anonymize a contact by adding all necessary fields for a personal, or company contact
63-
* It takes the same payload as creating a contact and fails for non-anon contacts
59+
* They only have Node Id, Name and E-Mail as fields
60+
* E-Mail is optional
61+
* This changes the data model for contacts, `email` and `postal_address` are now optional
62+
* Additional validation rules ensure the fields can only be set for non-anon contacts
63+
* Adds an endpoint `Api.contact().deanonymize()` to de-anonymize a contact by adding all necessary fields for a personal, or company contact
64+
* It takes the same payload as creating a contact and fails for non-anon contacts
6465
* Add the possibility to add an anonymous identity
65-
* They only have Node Id, (nick)name and E-Mail as fields
66-
* E-Mail is optional
67-
* Adds an endpoint `Api.identity().deanonymize()` to de-anonymize an identity by adding all necessary fields for a personal identity
68-
* It takes the same payload as creating an identity and fails for a non-anon identity
69-
* Anon identity can't issue bills, or create a company
66+
* They only have Node Id, (nick)name and E-Mail as fields
67+
* E-Mail is optional
68+
* Adds an endpoint `Api.identity().deanonymize()` to de-anonymize an identity by adding all necessary fields for a personal identity
69+
* It takes the same payload as creating an identity and fails for a non-anon identity
70+
* Anon identity can't issue bills, or create a company
7071
* Add the possibility to issue and endorse blank
71-
* New `Api.bill()` methods for blank endorsements
72-
* `issue_blank`
73-
* `offer_to_sell_blank`
74-
* `endorse_bill_blank`
75-
* `mint_bill_blank`
76-
* Can issue (non-self-drafted) blank bills (payee is anon)
77-
* Can endorse/mint/offer to sell to anon endorsee/mint/buyer
78-
* If caller of a bill action is anonymous in the bill, any action they take stay anonymous (e.g. endorse)
72+
* New `Api.bill()` methods for blank endorsements
73+
* `issue_blank`
74+
* `offer_to_sell_blank`
75+
* `endorse_bill_blank`
76+
* `mint_bill_blank`
77+
* Can issue (non-self-drafted) blank bills (payee is anon)
78+
* Can endorse/mint/offer to sell to anon endorsee/mint/buyer
79+
* If caller of a bill action is anonymous in the bill, any action they take stay anonymous (e.g. endorse)
7980
* Add endpoint to check payment for singular bill
80-
* `Api.bill().check_payment_for_bill(id)`
81+
* `Api.bill().check_payment_for_bill(id)`
8182
* Fix TS type for identity detail
8283
* Return identity on `create` and `deanonymize` identity for consistency
8384

@@ -88,14 +89,14 @@
8889
* Reduce size of the WASM binary
8990
* Fix a small issue, where bills were recalculated instead of taken from cache, once their payment/sell/recourse/accept requests expired
9091
* Change behaviour of request to pay
91-
* it's now possible to req to pay before the maturity date
92-
* The actual payment expiry still only happens 2 workdays after the end of the maturity date,
92+
* it's now possible to req to pay before the maturity date
93+
* The actual payment expiry still only happens 2 workdays after the end of the maturity date,
9394
or end of the req to pay end of day if that was after the maturity date
94-
* the `request_to_pay_timed_out` flag is set after payment expired, not after the req to pay expired
95-
* The waiting state for payment is only active during the req to pay (while it's blocked)
96-
* Afterwards, the bill is not blocked anymore, can still be rejected to pay and paid
97-
* But recourse is only possible after the payment expired (after maturity date)
98-
* An expired req to pay, which expired before the maturity date does not show up in `past_payments`
95+
* the `request_to_pay_timed_out` flag is set after payment expired, not after the req to pay expired
96+
* The waiting state for payment is only active during the req to pay (while it's blocked)
97+
* Afterwards, the bill is not blocked anymore, can still be rejected to pay and paid
98+
* But recourse is only possible after the payment expired (after maturity date)
99+
* An expired req to pay, which expired before the maturity date does not show up in `past_payments`
99100

100101
# 0.3.9
101102

@@ -130,7 +131,7 @@
130131

131132
* Add in-depth tests for bill validation
132133
* Add recourse reason to `Recourse` block data
133-
* (breaks existing persisted bills, if they had a recourse block)
134+
* (breaks existing persisted bills, if they had a recourse block)
134135
* Added `has_requested_funds` flag to `BillStatusWeb`, indicating the caller has requested funds (req to pay, req to recourse, offer to sell) at some point
135136
* Added `past_payments` endpoint to `Api.bill()`, which returns data about past payments and payment requests where the caller was the beneficiary
136137

@@ -142,7 +143,7 @@
142143
* Add bill action validation for incoming blocks
143144
* Add signer verification for incoming blocks
144145
* Add recourse reason to `RequestRecourse` block data
145-
* (breaks existing persisted bills, if they had a request recourse block)
146+
* (breaks existing persisted bills, if they had a request recourse block)
146147
* Move bill validation logic to `bcr-ebill-core`
147148

148149
# 0.3.2
@@ -172,7 +173,7 @@ can be passed - a list of file upload ids, to upload multiple files for one bill
172173
* Restructured `BitcreditBillWeb` to a more structured approach, separating `status`,
173174
`data` and `participants` and adding the concept of `current_waiting_state`, to
174175
have all data available, if the bill is in a waiting state.
175-
* Added the concept of `redeemed_funds_available` on `status`, to indicate if
176+
* Added the concept of `redeemed_funds_available` on `status`, to indicate if
176177
the caller has funds available (e.g. from a sale, or a paid bill)
177178

178179
# 0.3.0

crates/bcr-ebill-api/src/service/contact_service.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ use bcr_ebill_core::{
77
BillParticipant,
88
validation::{validate_create_contact, validate_update_contact},
99
},
10-
nostr_contact::{NostrContact, TrustLevel},
10+
nostr_contact::{NostrContact, NostrPublicKey, TrustLevel},
1111
};
1212
use bcr_ebill_persistence::nostr::NostrContactStoreApi;
1313
#[cfg(test)]
1414
use mockall::automock;
15-
pub use nostr::key::PublicKey;
1615

1716
use crate::{
1817
Config,
@@ -100,10 +99,13 @@ pub trait ContactServiceApi: ServiceTraitBounds {
10099
) -> Result<Contact>;
101100

102101
/// Returns whether a given npub (as hex) is in our contact list.
103-
async fn is_known_npub(&self, npub: &PublicKey) -> Result<bool>;
102+
async fn is_known_npub(&self, npub: &NostrPublicKey) -> Result<bool>;
104103

105104
/// Returns the Npubs we want to subscribe to on Nostr.
106-
async fn get_nostr_npubs(&self) -> Result<Vec<PublicKey>>;
105+
async fn get_nostr_npubs(&self) -> Result<Vec<NostrPublicKey>>;
106+
107+
/// Returns a Nostr contact by node id if we have a trusted one.
108+
async fn get_nostr_contact_by_node_id(&self, node_id: &str) -> Result<Option<NostrContact>>;
107109

108110
/// opens and decrypts the attached file from the given contact
109111
async fn open_and_decrypt_file(
@@ -193,7 +195,7 @@ impl ContactService {
193195
.await?
194196
{
195197
Some(nostr_contact) => nostr_contact.merge_contact(contact),
196-
None => NostrContact::from_contact(contact),
198+
None => NostrContact::from_contact(contact)?,
197199
};
198200
self.nostr_contact_store.upsert(&nostr_contact).await?;
199201
Ok(())
@@ -570,7 +572,7 @@ impl ContactServiceApi for ContactService {
570572
Ok(contact)
571573
}
572574

573-
async fn is_known_npub(&self, npub: &PublicKey) -> Result<bool> {
575+
async fn is_known_npub(&self, npub: &NostrPublicKey) -> Result<bool> {
574576
Ok(!self.config.nostr_config.only_known_contacts
575577
|| self
576578
.nostr_contact_store
@@ -581,13 +583,21 @@ impl ContactServiceApi for ContactService {
581583
}
582584

583585
/// Returns the Npubs we want to subscribe to on Nostr.
584-
async fn get_nostr_npubs(&self) -> Result<Vec<PublicKey>> {
586+
async fn get_nostr_npubs(&self) -> Result<Vec<NostrPublicKey>> {
585587
Ok(self
586588
.nostr_contact_store
587589
.get_npubs(vec![TrustLevel::Trusted, TrustLevel::Participant])
588590
.await?)
589591
}
590592

593+
/// Returns a Nostr contact by node id if we have a trusted or participant one.
594+
async fn get_nostr_contact_by_node_id(&self, node_id: &str) -> Result<Option<NostrContact>> {
595+
match self.nostr_contact_store.by_node_id(node_id).await {
596+
Ok(Some(c)) if c.trust_level != TrustLevel::None => Ok(Some(c)),
597+
_ => Ok(None),
598+
}
599+
}
600+
591601
async fn open_and_decrypt_file(
592602
&self,
593603
contact: Contact,
@@ -645,7 +655,7 @@ pub mod tests {
645655
empty_optional_address, init_test_cfg,
646656
},
647657
};
648-
use bcr_ebill_core::nostr_contact::HandshakeStatus;
658+
use bcr_ebill_core::nostr_contact::{HandshakeStatus, NostrPublicKey};
649659
use std::collections::HashMap;
650660
use util::BcrKeys;
651661

@@ -1040,10 +1050,10 @@ pub mod tests {
10401050
async fn is_known_npub_calls_store() {
10411051
let (store, file_upload_store, file_upload_client, identity_store, mut nostr_contact) =
10421052
get_storages();
1043-
let pub_key = PublicKey::from_hex(TEST_NODE_ID_SECP_AS_NPUB_HEX).unwrap();
1053+
let pub_key = NostrPublicKey::from_hex(TEST_NODE_ID_SECP_AS_NPUB_HEX).unwrap();
10441054
nostr_contact.expect_by_npub().returning(|_| {
10451055
Ok(Some(NostrContact {
1046-
node_id: TEST_NODE_ID_SECP.to_string(),
1056+
npub: NostrPublicKey::parse(TEST_NODE_ID_SECP_AS_NPUB_HEX).unwrap(),
10471057
name: None,
10481058
relays: vec![],
10491059
trust_level: TrustLevel::Participant,

crates/bcr-ebill-api/src/service/notification_service/default_service.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::sync::Arc;
44
use async_trait::async_trait;
55
use bcr_ebill_core::blockchain::BlockchainType;
66
use bcr_ebill_core::blockchain::bill::block::NodeId;
7-
use bcr_ebill_core::contact::{BillParticipant, ContactType};
7+
use bcr_ebill_core::contact::{BillAnonParticipant, BillParticipant, ContactType};
88
use bcr_ebill_persistence::nostr::{
99
NostrChainEvent, NostrChainEventStoreApi, NostrQueuedMessage, NostrQueuedMessageStoreApi,
1010
};
@@ -87,6 +87,17 @@ impl DefaultNotificationService {
8787
self.contact_service.get_identity_by_node_id(node_id).await
8888
{
8989
Some(identity)
90+
} else if let Ok(Some(nostr)) = self
91+
.contact_service
92+
.get_nostr_contact_by_node_id(node_id)
93+
.await
94+
{
95+
// we have no contact but a nostr contact of a participant
96+
Some(BillParticipant::Anon(BillAnonParticipant {
97+
node_id: node_id.to_string(),
98+
email: None,
99+
nostr_relays: nostr.relays,
100+
}))
90101
} else {
91102
None
92103
}

crates/bcr-ebill-api/src/service/notification_service/nostr.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -372,11 +372,7 @@ impl NotificationJsonTransportApi for NostrClient {
372372
&self,
373373
node_id: &str,
374374
) -> Result<Option<bcr_ebill_transport::transport::NostrContactData>> {
375-
if let Ok(npub) = crypto::get_nostr_npub_as_hex_from_node_id(node_id) {
376-
let public_key = PublicKey::from_str(&npub).map_err(|e| {
377-
error!("Failed to parse Nostr npub when sending a notification: {e}");
378-
Error::Crypto("Failed to parse Nostr npub".to_string())
379-
})?;
375+
if let Ok(public_key) = crypto::get_npub_from_node_id(node_id) {
380376
match self.fetch_metadata(public_key).await? {
381377
Some(meta) => {
382378
let relays = self

crates/bcr-ebill-api/src/tests/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pub mod tests {
1616
contact::{BillIdentParticipant, BillParticipant, Contact, ContactType},
1717
identity::{ActiveIdentityState, Identity, IdentityType, IdentityWithAll},
1818
mint::{MintOffer, MintRequest, MintRequestStatus},
19-
nostr_contact::{HandshakeStatus, NostrContact, TrustLevel},
19+
nostr_contact::{HandshakeStatus, NostrContact, NostrPublicKey, TrustLevel},
2020
notification::{ActionType, Notification, NotificationType},
2121
util::crypto::BcrKeys,
2222
};
@@ -119,12 +119,12 @@ pub mod tests {
119119
#[async_trait]
120120
impl NostrContactStoreApi for NostrContactStore {
121121
async fn by_node_id(&self, node_id: &str) -> Result<Option<NostrContact>>;
122-
async fn by_npub(&self, npub: &nostr::key::PublicKey) -> Result<Option<NostrContact>>;
122+
async fn by_npub(&self, npub: &NostrPublicKey) -> Result<Option<NostrContact>>;
123123
async fn upsert(&self, data: &NostrContact) -> Result<()>;
124124
async fn delete(&self, node_id: &str) -> Result<()>;
125125
async fn set_handshake_status(&self, node_id: &str, status: HandshakeStatus) -> Result<()>;
126126
async fn set_trust_level(&self, node_id: &str, trust_level: TrustLevel) -> Result<()>;
127-
async fn get_npubs(&self, levels: Vec<TrustLevel>) -> Result<Vec<nostr::key::PublicKey>>;
127+
async fn get_npubs(&self, levels: Vec<TrustLevel>) -> Result<Vec<NostrPublicKey>>;
128128
}
129129
}
130130

crates/bcr-ebill-core/src/nostr_contact.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ use std::collections::BTreeSet;
22

33
use serde::{Deserialize, Serialize};
44

5-
use crate::contact::Contact;
5+
use crate::{contact::Contact, util::crypto::get_npub_from_node_id};
6+
7+
/// Make key type clear
8+
pub type NostrPublicKey = nostr::key::PublicKey;
69

710
/// Data we need to communicate with a Nostr contact.
811
#[derive(Debug, Clone, Serialize, Deserialize)]
912
pub struct NostrContact {
1013
/// Our node id. This is the node id and acts as the primary key.
11-
pub node_id: String,
14+
pub npub: NostrPublicKey,
1215
/// The Nostr name of the contact as retreived via Nostr metadata.
1316
pub name: Option<String>,
1417
/// The relays we found for this contact either from a message or the result of a relay list
@@ -23,14 +26,15 @@ pub struct NostrContact {
2326
impl NostrContact {
2427
/// Creates a new Nostr contact from a contact. This is used when we have a contact and want to
2528
/// create the Nostr contact from it. Handshake is set to complete and we trust the contact.
26-
pub fn from_contact(contact: &Contact) -> Self {
27-
Self {
28-
node_id: contact.node_id.clone(),
29+
pub fn from_contact(contact: &Contact) -> crate::util::crypto::Result<Self> {
30+
let npub = get_npub_from_node_id(contact.node_id.as_str())?;
31+
Ok(Self {
32+
npub,
2933
name: Some(contact.name.clone()),
3034
relays: contact.nostr_relays.clone(),
3135
trust_level: TrustLevel::Trusted,
3236
handshake_status: HandshakeStatus::Added,
33-
}
37+
})
3438
}
3539

3640
/// Merges contact data into a nostr contact. This assumes at that point the handskake is
@@ -39,7 +43,7 @@ impl NostrContact {
3943
let mut relays: BTreeSet<String> = BTreeSet::from_iter(self.relays.clone());
4044
relays.extend(contact.nostr_relays.clone());
4145
Self {
42-
node_id: self.node_id.clone(),
46+
npub: self.npub,
4347
name: Some(contact.name.clone()),
4448
relays: relays.into_iter().collect(),
4549
trust_level: TrustLevel::Trusted,

0 commit comments

Comments
 (0)