From 208e626b17056639ef37c40439c0a9fbcb8d09ec Mon Sep 17 00:00:00 2001 From: Mario Zupan Date: Tue, 18 Nov 2025 11:26:54 +0100 Subject: [PATCH 1/3] Adapt Identity Create / Deanonymize and Identity Proofs --- CHANGELOG.md | 6 + crates/bcr-ebill-api/src/external/email.rs | 147 +++---- crates/bcr-ebill-api/src/lib.rs | 2 + .../src/service/identity_service.rs | 370 +++++++++++++++--- crates/bcr-ebill-api/src/tests/mod.rs | 23 +- .../src/application/identity_proof.rs | 55 --- crates/bcr-ebill-core/src/application/mod.rs | 5 +- .../src/protocol/base/identity_proof.rs | 157 +++----- .../src/protocol/blockchain/company/mod.rs | 32 +- .../src/protocol/blockchain/identity/mod.rs | 19 +- .../bcr-ebill-core/src/protocol/mint/mod.rs | 15 +- crates/bcr-ebill-core/src/protocol/mod.rs | 2 +- .../bcr-ebill-core/src/protocol/tests/mod.rs | 13 + .../src/db/email_notification.rs | 6 +- .../bcr-ebill-persistence/src/db/identity.rs | 98 ++++- crates/bcr-ebill-persistence/src/identity.rs | 23 +- crates/bcr-ebill-persistence/src/lib.rs | 3 + crates/bcr-ebill-persistence/src/tests/mod.rs | 13 +- .../handler/company_chain_event_processor.rs | 9 +- .../handler/identity_chain_event_processor.rs | 21 +- crates/bcr-ebill-transport/src/handler/mod.rs | 20 + crates/bcr-ebill-transport/src/test_utils.rs | 9 +- crates/bcr-ebill-wasm/index.html | 6 + crates/bcr-ebill-wasm/main.js | 221 ++++++----- crates/bcr-ebill-wasm/src/api/identity.rs | 44 ++- crates/bcr-ebill-wasm/src/context.rs | 4 +- crates/bcr-ebill-wasm/src/data/identity.rs | 32 +- crates/bcr-ebill-wasm/src/error.rs | 4 + crates/bcr-ebill-wasm/src/lib.rs | 2 + 29 files changed, 929 insertions(+), 432 deletions(-) delete mode 100644 crates/bcr-ebill-core/src/application/identity_proof.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index ada2e138..e620978e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,12 @@ * Change document max file size to 10 MB and max files on bill to 20 * Add request deadlines to BillHistoryBlock * Remove `identity_proof` API and adapt and move to new email confirmation API +* Add dev mode flag `disable_mandatory_email_confirmations`, to make it easier for testing +* Identity Confirmation via Email + * Add persistence + * Adapt `create_identity` and `deanonymize` to require a confirmed email for identified users + * Add endpoints to `confirm`, `verify` an email address and to `get_email_confirmations` + * Adapt `IdentityProof` Block to include the email confirmation signed by the mint # 0.4.12 diff --git a/crates/bcr-ebill-api/src/external/email.rs b/crates/bcr-ebill-api/src/external/email.rs index 5cb88211..f3e8d4a3 100644 --- a/crates/bcr-ebill-api/src/external/email.rs +++ b/crates/bcr-ebill-api/src/external/email.rs @@ -1,14 +1,15 @@ use async_trait::async_trait; use bcr_common::core::{BillId, NodeId}; use bcr_ebill_core::application::ServiceTraitBounds; -use bcr_ebill_core::protocol::{Email, event::bill_events::BillEventType, mint::MintSignature}; -use bitcoin::{XOnlyPublicKey, base58}; +use bcr_ebill_core::protocol::Sha256Hash; +use bcr_ebill_core::protocol::{ + Email, SchnorrSignature, SignedEmailIdentityData, SignedIdentityProof, + crypto::Error as CryptoError, event::bill_events::BillEventType, +}; +use bitcoin::base58; use borsh_derive::BorshSerialize; -use nostr::hashes::Hash; -use nostr::hashes::sha256; -use nostr::util::SECP256K1; use secp256k1::schnorr::Signature; -use secp256k1::{Keypair, Message, SecretKey}; +use secp256k1::{Keypair, Message, SECP256K1, SecretKey}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -27,9 +28,6 @@ pub enum Error { /// all hex errors #[error("External Email Base58 Error: {0}")] Base58(#[from] base58::InvalidCharacterError), - /// all signature errors - #[error("External Email Signature Error: {0}")] - Signature(#[from] secp256k1::Error), /// all borsh errors #[error("External Email Borsh Error")] Borsh(#[from] borsh::io::Error), @@ -37,6 +35,8 @@ pub enum Error { InvalidMintId, #[error("External Email Invalid Mint Signature Error")] InvalidMintSignature, + #[error("External Email crypto Error")] + Crypto(#[from] CryptoError), } #[cfg(test)] @@ -66,7 +66,7 @@ pub trait EmailClientApi: ServiceTraitBounds { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> Result; + ) -> Result<(SignedIdentityProof, SignedEmailIdentityData)>; /// Send a bill notification email async fn send_bill_notification( &self, @@ -100,38 +100,54 @@ impl EmailClient { } } - async fn get_eic_challenge(&self, mint_url: &url::Url, node_id: &NodeId) -> Result { - let req = StartEmailRegisterRequest { - node_id: node_id.to_owned(), - }; - - let resp: StartEmailRegisterResponse = self - .cl - .post(to_url(mint_url, "v1/eic/challenge")?) - .json(&req) - .send() - .await? - .json() - .await?; + async fn get_eic_challenge( + &self, + mint_url: &url::Url, + node_id: &NodeId, + private_key: &SecretKey, + ) -> Result { + self.get_challenge("eic", mint_url, node_id, private_key) + .await + } - Ok(resp.challenge) + async fn get_ens_challenge( + &self, + mint_url: &url::Url, + node_id: &NodeId, + private_key: &SecretKey, + ) -> Result { + self.get_challenge("ens", mint_url, node_id, private_key) + .await } - async fn get_ens_challenge(&self, mint_url: &url::Url, node_id: &NodeId) -> Result { + async fn get_challenge( + &self, + prefix: &str, + mint_url: &url::Url, + node_id: &NodeId, + private_key: &SecretKey, + ) -> Result { let req = StartEmailRegisterRequest { node_id: node_id.to_owned(), }; let resp: StartEmailRegisterResponse = self .cl - .post(to_url(mint_url, "v1/ens/challenge")?) + .post(to_url(mint_url, &format!("v1/{}/challenge", prefix))?) .json(&req) .send() .await? .json() .await?; - Ok(resp.challenge) + let decoded_challenge = base58::decode(&resp.challenge).map_err(Error::Base58)?; + + let key_pair = Keypair::from_secret_key(SECP256K1, private_key); + let msg = Message::from_digest_slice(&decoded_challenge) + .map_err(|e| Error::Crypto(CryptoError::Signature(e.to_string())))?; + let signature = SECP256K1.sign_schnorr(&msg, &key_pair); + + Ok(signature) } } @@ -151,12 +167,9 @@ impl EmailClientApi for EmailClient { email: &Email, private_key: &SecretKey, ) -> Result<()> { - let challenge = self.get_eic_challenge(mint_url, node_id).await?; - let decoded_challenge = base58::decode(&challenge).map_err(Error::Base58)?; - - let key_pair = Keypair::from_secret_key(SECP256K1, private_key); - let msg = Message::from_digest_slice(&decoded_challenge).map_err(Error::Signature)?; - let signed_challenge = SECP256K1.sign_schnorr(&msg, &key_pair); + let signed_challenge = self + .get_eic_challenge(mint_url, node_id, private_key) + .await?; let req = RegisterEmailRequest { node_id: node_id.to_owned(), @@ -183,7 +196,7 @@ impl EmailClientApi for EmailClient { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> Result { + ) -> Result<(SignedIdentityProof, SignedEmailIdentityData)> { let pl = EmailConfirmPayload { node_id: node_id.to_owned(), company_node_id: company_node_id.to_owned(), @@ -191,10 +204,14 @@ impl EmailClientApi for EmailClient { }; let serialized = borsh::to_vec(&pl).map_err(Error::Borsh)?; - let signature = sign_payload(&serialized, private_key); + let hash = Sha256Hash::from_bytes(&serialized); + let signature = SchnorrSignature::sign(&hash, private_key).map_err(Error::Crypto)?; let payload = base58::encode(&serialized); - let req = EmailConfirmRequest { payload, signature }; + let req = EmailConfirmRequest { + payload, + signature: signature.as_sig(), + }; let res: EmailConfirmResponse = self .cl @@ -210,18 +227,25 @@ impl EmailClientApi for EmailClient { } let decoded_mint_sig = base58::decode(&res.payload).map_err(Error::Base58)?; + let hash = Sha256Hash::from_bytes(&decoded_mint_sig); + + let signature = SchnorrSignature::from(res.signature); - if !verify_request( - &decoded_mint_sig, - &res.signature, - &mint_node_id.pub_key().x_only_public_key().0, - )? { + if !signature + .verify(&hash, &mint_node_id.pub_key()) + .map_err(Error::Crypto)? + { return Err(Error::InvalidMintSignature.into()); } - let mint_sig: MintSignature = borsh::from_slice(&decoded_mint_sig).map_err(Error::Borsh)?; + let proof = SignedIdentityProof { + signature, + witness: res.mint_node_id, + }; + let data: SignedEmailIdentityData = + borsh::from_slice(&decoded_mint_sig).map_err(Error::Borsh)?; - Ok(mint_sig) + Ok((proof, data)) } async fn send_bill_notification( @@ -243,13 +267,17 @@ impl EmailClientApi for EmailClient { }; let serialized = borsh::to_vec(&pl).map_err(Error::Borsh)?; - let signature = sign_payload(&serialized, private_key); + let hash = Sha256Hash::from_bytes(&serialized); + let signature = SchnorrSignature::sign(&hash, private_key).map_err(Error::Crypto)?; let payload = base58::encode(&serialized); - let req = NotificationSendRequest { payload, signature }; + let req = NotificationSendRequest { + payload, + signature: signature.as_sig(), + }; self.cl - .post(to_url(mint_url, "notifications/v1/send")?) + .post(to_url(mint_url, "v1/ens/email/send")?) .json(&req) .send() .await? @@ -265,12 +293,9 @@ impl EmailClientApi for EmailClient { company_node_id: &Option, private_key: &SecretKey, ) -> Result { - let challenge = self.get_ens_challenge(mint_url, node_id).await?; - let decoded_challenge = base58::decode(&challenge).map_err(Error::Base58)?; - - let key_pair = Keypair::from_secret_key(SECP256K1, private_key); - let msg = Message::from_digest_slice(&decoded_challenge).map_err(Error::Signature)?; - let signed_challenge = SECP256K1.sign_schnorr(&msg, &key_pair); + let signed_challenge = self + .get_ens_challenge(mint_url, node_id, private_key) + .await?; let req = GetEmailPreferencesLinkRequest { node_id: node_id.to_owned(), @@ -291,20 +316,6 @@ impl EmailClientApi for EmailClient { } } -pub fn sign_payload(req: &[u8], private_key: &SecretKey) -> Signature { - let key_pair = Keypair::from_secret_key(SECP256K1, private_key); - let hash: sha256::Hash = sha256::Hash::hash(req); - let req = Message::from_digest(*hash.as_ref()); - - SECP256K1.sign_schnorr(&req, &key_pair) -} - -pub fn verify_request(payload: &[u8], signature: &Signature, key: &XOnlyPublicKey) -> Result { - let hash = sha256::Hash::hash(payload); - let msg = Message::from_digest(*hash.as_ref()); - Ok(SECP256K1.verify_schnorr(signature, &msg, key).is_ok()) -} - #[derive(Debug, Serialize)] pub struct StartEmailRegisterRequest { pub node_id: NodeId, @@ -313,7 +324,7 @@ pub struct StartEmailRegisterRequest { #[derive(Debug, Deserialize)] pub struct StartEmailRegisterResponse { pub challenge: String, - pub ttl_seconds: u32, + pub ttl: u32, } #[derive(Debug, Serialize)] @@ -341,7 +352,7 @@ pub struct EmailConfirmPayload { #[derive(Debug, Deserialize)] pub struct EmailConfirmResponse { - /// A borsh-encoded MintSignature + /// A borsh-encoded SignedEmailIdentityData pub payload: String, /// The mint signature of the payload pub signature: Signature, diff --git a/crates/bcr-ebill-api/src/lib.rs b/crates/bcr-ebill-api/src/lib.rs index 2e272e58..3029c39b 100644 --- a/crates/bcr-ebill-api/src/lib.rs +++ b/crates/bcr-ebill-api/src/lib.rs @@ -72,6 +72,8 @@ pub struct CourtConfig { pub struct DevModeConfig { /// Whether dev mode is on pub on: bool, + /// Whether mandatory email confirmations should be enabled (disable for easier testing) + pub disable_mandatory_email_confirmations: bool, } /// Payment specific configuration diff --git a/crates/bcr-ebill-api/src/service/identity_service.rs b/crates/bcr-ebill-api/src/service/identity_service.rs index cd48be52..41c9ef79 100644 --- a/crates/bcr-ebill-api/src/service/identity_service.rs +++ b/crates/bcr-ebill-api/src/service/identity_service.rs @@ -1,4 +1,5 @@ use super::Result; +use crate::external::email::EmailClientApi; use crate::external::file_storage::FileStorageClientApi; use crate::service::Error; use crate::service::file_upload_service::UploadFileType; @@ -11,9 +12,6 @@ use bcr_common::core::NodeId; use bcr_ebill_core::application::identity::validation::validate_create_identity; use bcr_ebill_core::application::identity::{ActiveIdentityState, Identity, IdentityWithAll}; use bcr_ebill_core::application::{ServiceTraitBounds, ValidationError}; -use bcr_ebill_core::protocol::Country; -use bcr_ebill_core::protocol::Date; -use bcr_ebill_core::protocol::Email; use bcr_ebill_core::protocol::Identification; use bcr_ebill_core::protocol::Name; use bcr_ebill_core::protocol::Sha256Hash; @@ -21,13 +19,17 @@ use bcr_ebill_core::protocol::Timestamp; use bcr_ebill_core::protocol::blockchain::Blockchain; use bcr_ebill_core::protocol::blockchain::identity::{ IdentityBlock, IdentityBlockPlaintextWrapper, IdentityBlockchain, IdentityCreateBlockData, - IdentityType, IdentityUpdateBlockData, + IdentityProofBlockData, IdentityType, IdentityUpdateBlockData, }; use bcr_ebill_core::protocol::crypto::{self, BcrKeys, DeriveKeypair}; use bcr_ebill_core::protocol::{City, ProtocolValidationError}; +use bcr_ebill_core::protocol::{Country, SignedEmailIdentityData, SignedIdentityProof}; +use bcr_ebill_core::protocol::{Date, Field}; +use bcr_ebill_core::protocol::{Email, blockchain}; use bcr_ebill_core::protocol::{File, OptionalPostalAddress, Validate, event::IdentityChainEvent}; use bcr_ebill_persistence::file_upload::FileUploadStoreApi; use bcr_ebill_persistence::identity::{IdentityChainStoreApi, IdentityStoreApi}; +use bcr_ebill_persistence::notification::EmailNotificationStoreApi; use bitcoin::base58; use log::{debug, error, info}; use secp256k1::{PublicKey, SecretKey}; @@ -122,6 +124,17 @@ pub trait IdentityServiceApi: ServiceTraitBounds { /// If dev mode is on, return the full identity chain with decrypted data async fn dev_mode_get_full_identity_chain(&self) -> Result>; + + /// Confirm a new email address + async fn confirm_email(&self, email: &Email) -> Result<()>; + + /// Verify confirmation of an email address with the sent confirmation code + async fn verify_email(&self, confirmation_code: &str) -> Result<()>; + + /// Get email confirmations for the identity + async fn get_email_confirmations( + &self, + ) -> Result>; } /// The identity service is responsible for managing the local identity @@ -132,6 +145,8 @@ pub struct IdentityService { file_upload_client: Arc, blockchain_store: Arc, block_transport: Arc, + email_client: Arc, + email_notification_store: Arc, } impl IdentityService { @@ -141,6 +156,8 @@ impl IdentityService { file_upload_client: Arc, blockchain_store: Arc, block_transport: Arc, + email_client: Arc, + email_notification_store: Arc, ) -> Self { Self { store, @@ -148,6 +165,8 @@ impl IdentityService { file_upload_client, blockchain_store, block_transport, + email_client, + email_notification_store, } } @@ -218,6 +237,103 @@ impl IdentityService { debug!("Identity change"); self.publish_contact(identity, keys).await } + + async fn validate_and_add_block( + &self, + chain: &mut IdentityBlockchain, + new_block: IdentityBlock, + ) -> Result<()> { + let try_add_block = chain.try_add_block(new_block.clone()); + if try_add_block && chain.is_chain_valid() { + self.blockchain_store.add_block(&new_block).await?; + Ok(()) + } else { + Err(Error::Protocol(blockchain::Error::BlockchainInvalid.into())) + } + } + + async fn create_identity_proof_block( + &self, + proof: SignedIdentityProof, + data: SignedEmailIdentityData, + identity: &Identity, + keys: &BcrKeys, + chain: &mut IdentityBlockchain, + ) -> Result<()> { + let new_block = IdentityBlock::create_block_for_identity_proof( + chain.get_latest_block(), + &IdentityProofBlockData { proof, data }, + keys, + Timestamp::now(), + ) + .map_err(|e| Error::Protocol(e.into()))?; + + self.validate_and_add_block(chain, new_block.clone()) + .await?; + self.populate_block(identity, &new_block, keys).await?; + + Ok(()) + } + + /// If mandatory email confirmations are not disabled, check that there is a confirmed email and the + /// given email is a confirmed one for the identity + async fn check_confirmed_email( + &self, + email: &Option, + t: &IdentityType, + node_id: &NodeId, + ) -> Result> { + match t { + IdentityType::Ident => { + // Email has to be checked before + let Some(em) = email else { + return Err(ProtocolValidationError::FieldEmpty(Field::Email).into()); + }; + + if !get_config() + .dev_mode_config + .disable_mandatory_email_confirmations + { + // Make sure there is a confirmed email + let email_confirmations = self.store.get_email_confirmations().await?; + if email_confirmations.is_empty() { + return Err(Error::Validation( + ValidationError::NoConfirmedEmailForIdentIdentity, + )); + } + + // Given email has to be a confirmed email + for ec in email_confirmations.iter() { + if &ec.1.email == em { + return Ok(Some((ec.0.to_owned(), ec.1.to_owned()))); + } + } + + // No email found - fail + Err(Error::Validation( + ValidationError::NoConfirmedEmailForIdentIdentity, + )) + } else { + // if mandatory email confirmations are disabled, create self-signed email confirmation + let keys = self.get_keys().await?; + + let self_signed_identity = SignedEmailIdentityData { + node_id: node_id.to_owned(), + company_node_id: None, + email: em.to_owned(), + created_at: Timestamp::now(), + }; + let proof = self_signed_identity.sign(node_id, &keys.get_private_key())?; + self.store + .set_email_confirmation(&proof, &self_signed_identity) + .await?; + + Ok(Some((proof, self_signed_identity))) + } + } + IdentityType::Anon => Ok(None), + } + } } /// Derives a child key, encrypts the contact data with it and returns the bcr metadata @@ -384,7 +500,8 @@ impl IdentityServiceApi for IdentityService { }; } - let previous_block = self.blockchain_store.get_latest_block().await?; + let mut identity_chain = self.blockchain_store.get_chain().await?; + let previous_block = identity_chain.get_latest_block(); let block_data = IdentityUpdateBlockData { t: None, name, @@ -399,9 +516,11 @@ impl IdentityServiceApi for IdentityService { }; block_data.validate()?; let new_block = - IdentityBlock::create_block_for_update(&previous_block, &block_data, &keys, timestamp) + IdentityBlock::create_block_for_update(previous_block, &block_data, &keys, timestamp) .map_err(|e| Error::Protocol(e.into()))?; - self.blockchain_store.add_block(&new_block).await?; + self.validate_and_add_block(&mut identity_chain, new_block.clone()) + .await?; + self.store.save(&identity).await?; self.populate_block(&identity, &new_block, &keys).await?; self.on_identity_contact_change(&identity, &keys).await?; @@ -438,6 +557,8 @@ impl IdentityServiceApi for IdentityService { validate_create_identity(t.clone(), &email, &postal_address)?; let nostr_relays = get_config().nostr_config.relays.clone(); + let email_confirmation = self.check_confirmed_email(&email, &t, &node_id).await?; + let identity = match t { IdentityType::Ident => { // TODO(multi-relay): don't default to first @@ -501,7 +622,7 @@ impl IdentityServiceApi for IdentityService { // create new identity chain and persist it let block_data: IdentityCreateBlockData = identity.clone().into(); block_data.validate()?; - let identity_chain = IdentityBlockchain::new(&block_data, &keys, timestamp) + let mut identity_chain = IdentityBlockchain::new(&block_data, &keys, timestamp) .map_err(|e| Error::Protocol(e.into()))?; let first_block = identity_chain.get_first_block(); self.blockchain_store.add_block(first_block).await?; @@ -511,6 +632,12 @@ impl IdentityServiceApi for IdentityService { self.populate_block(&identity, first_block, &keys).await?; self.on_identity_contact_change(&identity, &keys).await?; + // Create and populate identity proof block + if let Some((proof, data)) = email_confirmation { + self.create_identity_proof_block(proof, data, &identity, &keys, &mut identity_chain) + .await?; + } + debug!("created identity"); Ok(()) } @@ -550,6 +677,10 @@ impl IdentityServiceApi for IdentityService { validate_create_identity(t.clone(), &email, &postal_address)?; + let email_confirmation = self + .check_confirmed_email(&email, &t, &existing_identity.node_id) + .await?; + // TODO(multi-relay): don't default to first let (profile_picture_file, identity_document_file) = match nostr_relays.first() { Some(nostr_relay) => { @@ -592,7 +723,8 @@ impl IdentityServiceApi for IdentityService { nostr_relays: get_config().nostr_config.relays.to_owned(), }; - let previous_block = self.blockchain_store.get_latest_block().await?; + let mut identity_chain = self.blockchain_store.get_chain().await?; + let previous_block = identity_chain.get_latest_block(); let block_data = IdentityUpdateBlockData { t: Some(t.clone()), name: Some(name), @@ -607,12 +739,20 @@ impl IdentityServiceApi for IdentityService { }; block_data.validate()?; let new_block = - IdentityBlock::create_block_for_update(&previous_block, &block_data, &keys, timestamp) + IdentityBlock::create_block_for_update(previous_block, &block_data, &keys, timestamp) .map_err(|e| Error::Protocol(e.into()))?; - self.blockchain_store.add_block(&new_block).await?; + self.validate_and_add_block(&mut identity_chain, new_block.clone()) + .await?; self.store.save(&identity).await?; self.populate_block(&identity, &new_block, &keys).await?; self.on_identity_contact_change(&identity, &keys).await?; + + // Create and populate identity proof block + if let Some((proof, data)) = email_confirmation { + self.create_identity_proof_block(proof, data, &identity, &keys, &mut identity_chain) + .await?; + } + debug!("deanonymized identity"); Ok(()) } @@ -757,6 +897,62 @@ impl IdentityServiceApi for IdentityService { .await; Ok(()) } + + async fn confirm_email(&self, email: &Email) -> Result<()> { + let keys = self.store.get_or_create_key_pair().await?; + let node_id = NodeId::new(keys.pub_key(), get_config().bitcoin_network()); + + // use default mint URL for now, until we support multiple mints + let mint_url = get_config().mint_config.default_mint_url.to_owned(); + + self.email_client + .register(&mint_url, &node_id, &None, email, &keys.get_private_key()) + .await?; + + Ok(()) + } + + async fn verify_email(&self, confirmation_code: &str) -> Result<()> { + let keys = self.store.get_or_create_key_pair().await?; + let node_id = NodeId::new(keys.pub_key(), get_config().bitcoin_network()); + + // use default mint URL for now, until we support multiple mints + let mint_url = get_config().mint_config.default_mint_url.to_owned(); + let mint_node_id = get_config().mint_config.default_mint_node_id.to_owned(); + + let (signed_proof, signed_email_identity_data) = self + .email_client + .confirm( + &mint_url, + &mint_node_id, + &node_id, + &None, + confirmation_code, + &keys.get_private_key(), + ) + .await?; + + self.store + .set_email_confirmation(&signed_proof, &signed_email_identity_data) + .await?; + + let email_preferences_link = self + .email_client + .get_email_preferences_link(&mint_url, &node_id, &None, &keys.get_private_key()) + .await?; + self.email_notification_store + .add_email_preferences_link_for_node_id(&email_preferences_link, &node_id) + .await?; + + Ok(()) + } + + async fn get_email_confirmations( + &self, + ) -> Result> { + let email_confirmations = self.store.get_email_confirmations().await?; + Ok(email_confirmations) + } } #[cfg(test)] @@ -765,11 +961,13 @@ mod tests { use super::*; use crate::{ - external::file_storage::MockFileStorageClientApi, + external::{email::MockEmailClientApi, file_storage::MockFileStorageClientApi}, service::transport_service::MockTransportServiceApi, tests::tests::{ - MockFileUploadStoreApiMock, MockIdentityChainStoreApiMock, MockIdentityStoreApiMock, - empty_identity, empty_optional_address, filled_optional_address, init_test_cfg, + MockEmailNotificationStoreApiMock, MockFileUploadStoreApiMock, + MockIdentityChainStoreApiMock, MockIdentityStoreApiMock, empty_identity, + empty_optional_address, filled_optional_address, init_test_cfg, + signed_identity_proof_test, }, }; use mockall::predicate::eq; @@ -781,6 +979,8 @@ mod tests { Arc::new(MockFileStorageClientApi::new()), Arc::new(MockIdentityChainStoreApiMock::new()), Arc::new(MockTransportServiceApi::new()), + Arc::new(MockEmailClientApi::new()), + Arc::new(MockEmailNotificationStoreApiMock::new()), ) } @@ -795,6 +995,24 @@ mod tests { Arc::new(MockFileStorageClientApi::new()), Arc::new(mock_chain_storage), Arc::new(transport), + Arc::new(MockEmailClientApi::new()), + Arc::new(MockEmailNotificationStoreApiMock::new()), + ) + } + + fn get_service_with_email_client_and_email_notif_store( + mock_storage: MockIdentityStoreApiMock, + mock_email_client: MockEmailClientApi, + mock_notif_store: MockEmailNotificationStoreApiMock, + ) -> IdentityService { + IdentityService::new( + Arc::new(mock_storage), + Arc::new(MockFileUploadStoreApiMock::new()), + Arc::new(MockFileStorageClientApi::new()), + Arc::new(MockIdentityChainStoreApiMock::new()), + Arc::new(MockTransportServiceApi::new()), + Arc::new(mock_email_client), + Arc::new(mock_notif_store), ) } @@ -805,6 +1023,9 @@ mod tests { storage .expect_get_or_create_key_pair() .returning(|| Ok(BcrKeys::new())); + storage + .expect_get_email_confirmations() + .returning(|| Ok(vec![signed_identity_proof_test()])); storage.expect_save().returning(move |_| Ok(())); storage .expect_get_key_pair() @@ -815,7 +1036,7 @@ mod tests { transport.expect_on_block_transport(|t| { t.expect_send_identity_chain_events() .returning(|_| Ok(())) - .once(); + .times(2); // create and identity proof }); transport.expect_on_contact_transport(|t| { // publishes contact info to nostr @@ -907,24 +1128,19 @@ mod tests { identity.t = IdentityType::Anon; Ok(identity) }); + storage + .expect_get_email_confirmations() + .returning(|| Ok(vec![signed_identity_proof_test()])); let mut chain_storage = MockIdentityChainStoreApiMock::new(); chain_storage.expect_add_block().returning(|_| Ok(())); - chain_storage.expect_get_latest_block().returning(|| { - let identity = empty_identity(); - Ok(IdentityBlockchain::new( - &identity.into(), - &BcrKeys::new(), - Timestamp::new(1731593928).unwrap(), - ) - .unwrap() - .get_latest_block() - .clone()) - }); + chain_storage + .expect_get_chain() + .returning(|| Ok(get_genesis_chain(None))); let mut transport = MockTransportServiceApi::new(); transport.expect_on_block_transport(|t| { t.expect_send_identity_chain_events() .returning(|_| Ok(())) - .once(); + .times(2); // update and identity proof }); // publishes contact info to nostr @@ -1065,17 +1281,9 @@ mod tests { Ok(identity) }); let mut chain_storage = MockIdentityChainStoreApiMock::new(); - chain_storage.expect_get_latest_block().returning(|| { - let identity = empty_identity(); - Ok(IdentityBlockchain::new( - &identity.into(), - &BcrKeys::new(), - Timestamp::new(1731593928).unwrap(), - ) - .unwrap() - .get_latest_block() - .clone()) - }); + chain_storage + .expect_get_chain() + .returning(|| Ok(get_genesis_chain(None))); chain_storage.expect_add_block().returning(|_| Ok(())); let mut transport = MockTransportServiceApi::new(); transport.expect_on_block_transport(|t| { @@ -1159,17 +1367,9 @@ mod tests { .returning(|_| Err(bcr_ebill_persistence::Error::EncodingError)) .times(1); let mut chain_storage = MockIdentityChainStoreApiMock::new(); - chain_storage.expect_get_latest_block().returning(|| { - let identity = empty_identity(); - Ok(IdentityBlockchain::new( - &identity.into(), - &BcrKeys::new(), - Timestamp::new(1731593928).unwrap(), - ) - .unwrap() - .get_latest_block() - .clone()) - }); + chain_storage + .expect_get_chain() + .returning(|| Ok(get_genesis_chain(None))); chain_storage .expect_add_block() .returning(|_| Ok(())) @@ -1313,4 +1513,76 @@ mod tests { .is_err() ); } + + #[tokio::test] + async fn test_get_email_confirmations() { + let mut storage = MockIdentityStoreApiMock::new(); + storage + .expect_get_email_confirmations() + .returning(|| Ok(vec![])); + let service = get_service(storage); + let res = service.get_email_confirmations().await.expect("works"); + assert_eq!(res.len(), 0); + } + + #[tokio::test] + async fn test_verify_email() { + let mut storage = MockIdentityStoreApiMock::new(); + storage + .expect_get_or_create_key_pair() + .returning(|| Ok(BcrKeys::new())); + storage + .expect_set_email_confirmation() + .returning(|_, _| Ok(())); + let mut email_client = MockEmailClientApi::new(); + email_client + .expect_get_email_preferences_link() + .returning(|_, _, _, _| Ok(url::Url::parse("https://bit.cr").unwrap())); + email_client + .expect_confirm() + .returning(|_, _, _, _, _, _| Ok(signed_identity_proof_test())); + let mut email_notif_storage = MockEmailNotificationStoreApiMock::new(); + email_notif_storage + .expect_add_email_preferences_link_for_node_id() + .returning(|_, _| Ok(())); + let service = get_service_with_email_client_and_email_notif_store( + storage, + email_client, + email_notif_storage, + ); + let res = service.verify_email("123456").await; + assert!(res.is_ok()); + } + + #[tokio::test] + async fn test_confirm_email() { + let mut storage = MockIdentityStoreApiMock::new(); + storage + .expect_get_or_create_key_pair() + .returning(|| Ok(BcrKeys::new())); + let mut email_client = MockEmailClientApi::new(); + email_client + .expect_register() + .returning(|_, _, _, _, _| Ok(())); + let email_notif_storage = MockEmailNotificationStoreApiMock::new(); + let service = get_service_with_email_client_and_email_notif_store( + storage, + email_client, + email_notif_storage, + ); + let res = service + .confirm_email(&Email::new("test@example.com").unwrap()) + .await; + assert!(res.is_ok()); + } + + fn get_genesis_chain(identity: Option) -> IdentityBlockchain { + let identity = identity.unwrap_or(empty_identity()); + IdentityBlockchain::new( + &identity.into(), + &BcrKeys::new(), + Timestamp::new(1731593928).unwrap(), + ) + .unwrap() + } } diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index f4f77130..998369cb 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -18,6 +18,7 @@ pub mod tests { use bcr_ebill_core::protocol::Sum; use bcr_ebill_core::protocol::Timestamp; use bcr_ebill_core::protocol::blockchain::bill::BitcreditBill; + use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; use bcr_ebill_core::{ application::ServiceTraitBounds, application::bill::{BitcreditBillResult, PaymentState}, @@ -278,6 +279,14 @@ pub mod tests { async fn get_current_identity(&self) -> Result; async fn set_current_identity(&self, identity_state: &ActiveIdentityState) -> Result<()>; async fn set_or_check_network(&self, configured_network: bitcoin::Network) -> Result<()>; + async fn get_email_confirmations( + &self, + ) -> Result>; + async fn set_email_confirmation( + &self, + proof: &SignedIdentityProof, + data: &SignedEmailIdentityData, + ) -> Result<()>; } } @@ -465,7 +474,8 @@ pub mod tests { num_confirmations_for_payment: 6, }, dev_mode_config: DevModeConfig { - on: false + on: false, + disable_mandatory_email_confirmations: false }, court_config: CourtConfig { default_url: url::Url::parse("https://court-dev.minibill.tech").unwrap() @@ -585,6 +595,17 @@ pub mod tests { .unwrap() } + pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { + let data = SignedEmailIdentityData { + node_id: node_id_test(), + company_node_id: None, + email: Email::new("test@example.com").unwrap(), + created_at: Timestamp::new(1731593929).unwrap(), + }; + let proof = data.sign(&node_id_test(), &private_key_test()).unwrap(); + (proof, data) + } + // bitcrt285psGq4Lz4fEQwfM3We5HPznJq8p1YvRaddszFaU5dY pub fn bill_id_test() -> BillId { BillId::new( diff --git a/crates/bcr-ebill-core/src/application/identity_proof.rs b/crates/bcr-ebill-core/src/application/identity_proof.rs deleted file mode 100644 index 14afdf86..00000000 --- a/crates/bcr-ebill-core/src/application/identity_proof.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::fmt; - -use url::Url; - -use crate::protocol::{BlockId, IdentityProofStamp, Sha256Hash, Timestamp}; -use bcr_common::core::NodeId; - -#[derive(Debug, Clone)] -pub enum IdentityProofStatus { - /// The request succeeded and we found the signature we were looking for in the response - Success, - /// The request succeeded, but we didn't find the signature we were looking for in the response - NotFound, - /// The request failed with a connection error - FailureConnect, - /// The request failed with a client error (4xx) - FailureClient, - /// The request failed with a server error (5xx) - FailureServer, -} - -impl fmt::Display for IdentityProofStatus { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - IdentityProofStatus::Success => "Success", - IdentityProofStatus::NotFound => "NotFound", - IdentityProofStatus::FailureConnect => "FailureConnect", - IdentityProofStatus::FailureClient => "FailureClient", - IdentityProofStatus::FailureServer => "FailureServer", - }; - write!(f, "{}", s) - } -} - -/// An identity proof -#[derive(Debug, Clone)] -pub struct IdentityProof { - pub node_id: NodeId, - pub stamp: IdentityProofStamp, - pub url: Url, - pub timestamp: Timestamp, - pub status: IdentityProofStatus, - pub status_last_checked_timestamp: Timestamp, - pub block_id: BlockId, -} - -impl IdentityProof { - pub fn id(&self) -> String { - // The id is the base58 sha256 hash of the node_id:url:timestamp triple - Sha256Hash::from_bytes( - format!("{}:{}:{}", &self.node_id, &self.url, self.timestamp).as_bytes(), - ) - .to_string() - } -} diff --git a/crates/bcr-ebill-core/src/application/mod.rs b/crates/bcr-ebill-core/src/application/mod.rs index c1032783..e061413a 100644 --- a/crates/bcr-ebill-core/src/application/mod.rs +++ b/crates/bcr-ebill-core/src/application/mod.rs @@ -9,7 +9,6 @@ pub mod company; pub mod contact; mod event; pub mod identity; -pub mod identity_proof; pub mod nostr_contact; pub mod notification; @@ -138,4 +137,8 @@ pub enum ValidationError { /// errors that stem from trying to accept a mint request that's expired #[error("Mint request can only be accepted if it's not expired.")] AcceptMintOfferExpired, + + /// errors that stem from trying to create, or deanonymize an identity without a confirmed email + #[error("Ident identity can only be created with a confirmed email.")] + NoConfirmedEmailForIdentIdentity, } diff --git a/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs b/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs index 8139a16c..fbf80db3 100644 --- a/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs +++ b/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs @@ -1,121 +1,82 @@ -use std::{fmt, str::FromStr}; - -use log::warn; +use borsh::{BorshDeserialize, BorshSerialize}; use secp256k1::{SECP256K1, SecretKey}; +use serde::{Deserialize, Serialize}; use bcr_common::core::NodeId; -use crate::protocol::{ProtocolValidationError, SchnorrSignature, Sha256Hash}; - -/// This is the string users are supposed to post on their social media to prove their identity -#[derive(Debug, Clone, PartialEq)] -pub struct IdentityProofStamp { - inner: SchnorrSignature, +use crate::protocol::{ + Email, ProtocolError, SchnorrSignature, Sha256Hash, Timestamp, crypto::Error as CryptoError, +}; + +/// The signature and witness of an identity proof +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)] +pub struct SignedIdentityProof { + /// The signature of the sha256 hashed borsh-payload (SignedEmailIdentityData) by the mint + pub signature: SchnorrSignature, + /// The mint (signer) node id + pub witness: NodeId, } -impl IdentityProofStamp { - /// Sign the base58 sha256 hash of the given node_id using the given key and returns the resulting signature - pub fn new(node_id: &NodeId, private_key: &SecretKey) -> Result { - // check that the node id and the private key match - if node_id.pub_key() != private_key.public_key(SECP256K1) { - return Err(ProtocolValidationError::InvalidNodeId); - } - // hash the node id - let hash = Sha256Hash::from_bytes(node_id.to_string().as_bytes()); - // sign it - let signature = SchnorrSignature::sign(&hash, private_key) - .map_err(|_| ProtocolValidationError::InvalidSignature)?; - Ok(IdentityProofStamp::from(signature)) - } - - /// Checks if the identity proof signature string is within the given body of text - pub fn is_contained_in(&self, body: &str) -> bool { - let self_str = self.to_string(); - body.contains(&self_str) - } - - pub fn verify_against_node_id(&self, node_id: &NodeId) -> bool { - let hash = Sha256Hash::from_bytes(node_id.to_string().as_bytes()); - match self.inner.verify(&hash, &node_id.pub_key()) { - Ok(verified) => verified, - Err(e) => { - warn!( - "could not verify identity proof stamp {self} against node id {node_id}: {e}" - ); - false - } - } - } -} - -impl fmt::Display for IdentityProofStamp { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.inner.fmt(f) +impl SignedIdentityProof { + pub fn verify(&self, data: &SignedEmailIdentityData) -> Result { + let serialized = borsh::to_vec(&data)?; + let hash = Sha256Hash::from_bytes(&serialized); + let res = self.signature.verify(&hash, &self.witness.pub_key())?; + Ok(res) } } -impl From for IdentityProofStamp { - fn from(value: SchnorrSignature) -> Self { - Self { inner: value } - } +/// Mapping from (node_id/option) => email, to be signed by a witness (mint) +#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)] +pub struct SignedEmailIdentityData { + /// Identity node id + pub node_id: NodeId, + /// Optional company node id + pub company_node_id: Option, + /// The mapped email + pub email: Email, + /// The time of signing + pub created_at: Timestamp, } -impl FromStr for IdentityProofStamp { - type Err = ProtocolValidationError; - fn from_str(s: &str) -> std::result::Result { - Ok(Self { - inner: SchnorrSignature::from_str(s)?, +impl SignedEmailIdentityData { + pub fn sign( + &self, + witness: &NodeId, + witness_private_key: &SecretKey, + ) -> Result { + if witness.pub_key() != witness_private_key.public_key(SECP256K1) { + return Err(ProtocolError::Crypto(CryptoError::Crypto( + "Keys don't match".into(), + ))); + } + let serialized = borsh::to_vec(&self)?; + let hash = Sha256Hash::from_bytes(&serialized); + let sig = SchnorrSignature::sign(&hash, witness_private_key)?; + Ok(SignedIdentityProof { + signature: sig, + witness: witness.to_owned(), }) } } -impl serde::Serialize for IdentityProofStamp { - fn serialize(&self, s: S) -> Result - where - S: serde::Serializer, - { - s.collect_str(self) - } -} - -impl<'de> serde::Deserialize<'de> for IdentityProofStamp { - fn deserialize(d: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(d)?; - IdentityProofStamp::from_str(&s).map_err(serde::de::Error::custom) - } -} - -impl borsh::BorshSerialize for IdentityProofStamp { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - let stamp_str = self.to_string(); - borsh::BorshSerialize::serialize(&stamp_str, writer) - } -} - -impl borsh::BorshDeserialize for IdentityProofStamp { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let stamp_str: String = borsh::BorshDeserialize::deserialize_reader(reader)?; - IdentityProofStamp::from_str(&stamp_str) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e)) - } -} - #[cfg(test)] -pub mod tests { +mod tests { use crate::protocol::tests::tests::{node_id_test, private_key_test}; use super::*; #[test] - fn test_create_and_verify() { - let node_id = node_id_test(); - let private_key = private_key_test(); - - let identity_proof_stamp = - IdentityProofStamp::new(&node_id, &private_key).expect("is valid"); - assert!(identity_proof_stamp.verify_against_node_id(&node_id)); + fn test_sign_verify() { + let data = SignedEmailIdentityData { + node_id: node_id_test(), + company_node_id: None, + email: Email::new("test@example.com").unwrap(), + created_at: Timestamp::new(1731593929).unwrap(), + }; + let proof = data + .sign(&node_id_test(), &private_key_test()) + .expect("works"); + assert!(proof.verify(&data).expect("works")); } } diff --git a/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs b/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs index 93afb2d4..15fb27c3 100644 --- a/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs @@ -1,6 +1,7 @@ use super::Result; use super::bill::BillOpCode; use super::{Block, Blockchain}; +use crate::protocol::BlockId; use crate::protocol::City; use crate::protocol::Country; use crate::protocol::Date; @@ -10,9 +11,9 @@ use crate::protocol::Name; use crate::protocol::SchnorrSignature; use crate::protocol::Sha256Hash; use crate::protocol::Timestamp; +use crate::protocol::base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; use crate::protocol::blockchain::{Error, borsh_to_json_value}; use crate::protocol::crypto::{self, BcrKeys}; -use crate::protocol::{BlockId, IdentityProofStamp}; use crate::protocol::{File, OptionalPostalAddress, PostalAddress}; use bcr_common::core::BillId; use bcr_common::core::NodeId; @@ -186,12 +187,8 @@ pub struct CompanyRemoveSignatoryBlockData { #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct CompanyIdentityProofBlockData { - pub stamp: IdentityProofStamp, - #[borsh( - serialize_with = "crate::protocol::serialization::serialize_url", - deserialize_with = "crate::protocol::serialization::deserialize_url" - )] - pub url: url::Url, + pub proof: SignedIdentityProof, + pub data: SignedEmailIdentityData, } #[derive(Debug)] @@ -660,7 +657,8 @@ mod tests { use crate::protocol::{ Country, tests::tests::{ - bill_id_test, node_id_test, private_key_test, valid_address, valid_optional_address, + bill_id_test, node_id_test, private_key_test, signed_identity_proof_test, + valid_address, valid_optional_address, }, }; @@ -802,6 +800,24 @@ mod tests { assert_eq!(chain.blocks().len(), 5); assert!(chain.is_chain_valid()); + let test_signed_identity = signed_identity_proof_test(); + let identity_proof_block = CompanyBlock::create_block_for_identity_proof( + id.clone(), + chain.get_latest_block(), + &CompanyIdentityProofBlockData { + proof: test_signed_identity.0, + data: test_signed_identity.1, + }, + &identity_keys, + &company_keys, + Timestamp::new(1731593933).unwrap(), + ); + assert!(identity_proof_block.is_ok()); + chain.try_add_block(identity_proof_block.unwrap()); + + assert_eq!(chain.blocks().len(), 6); + assert!(chain.is_chain_valid()); + let new_chain_from_empty_blocks = CompanyBlockchain::new_from_blocks(vec![]); assert!(new_chain_from_empty_blocks.is_err()); diff --git a/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs b/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs index 0572b3fe..53da21bc 100644 --- a/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs @@ -1,6 +1,7 @@ use super::Result; use super::bill::BillOpCode; use super::{Block, Blockchain}; +use crate::protocol::BlockId; use crate::protocol::City; use crate::protocol::Country; use crate::protocol::Date; @@ -10,9 +11,9 @@ use crate::protocol::Name; use crate::protocol::SchnorrSignature; use crate::protocol::Sha256Hash; use crate::protocol::Timestamp; +use crate::protocol::base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; use crate::protocol::blockchain::{Error, borsh_to_json_value}; use crate::protocol::crypto::{self, BcrKeys}; -use crate::protocol::{BlockId, IdentityProofStamp}; use crate::protocol::{Field, ProtocolValidationError, Validate}; use crate::protocol::{File, OptionalPostalAddress}; use bcr_common::core::{BillId, NodeId}; @@ -262,12 +263,8 @@ pub struct IdentityRemoveSignatoryBlockData { #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct IdentityProofBlockData { - pub stamp: IdentityProofStamp, - #[borsh( - serialize_with = "crate::protocol::serialization::serialize_url", - deserialize_with = "crate::protocol::serialization::deserialize_url" - )] - pub url: url::Url, + pub proof: SignedIdentityProof, + pub data: SignedEmailIdentityData, } #[derive(Debug)] @@ -678,7 +675,8 @@ impl IdentityBlockchain { mod tests { use super::*; use crate::protocol::tests::tests::{ - bill_id_test, empty_identity, node_id_test, private_key_test, valid_optional_address, + bill_id_test, empty_identity, node_id_test, private_key_test, signed_identity_proof_test, + valid_optional_address, }; #[test] @@ -808,11 +806,12 @@ mod tests { assert!(remove_signatory_block.is_ok()); chain.try_add_block(remove_signatory_block.unwrap()); + let test_signed_identity = signed_identity_proof_test(); let identity_proof_block = IdentityBlock::create_block_for_identity_proof( chain.get_latest_block(), &IdentityProofBlockData { - stamp: IdentityProofStamp::new(&node_id_test(), &private_key_test()).unwrap(), - url: url::Url::parse("https://bit.cr").unwrap(), + proof: test_signed_identity.0, + data: test_signed_identity.1, }, &keys, Timestamp::new(1731593929).unwrap(), diff --git a/crates/bcr-ebill-core/src/protocol/mint/mod.rs b/crates/bcr-ebill-core/src/protocol/mint/mod.rs index 94c4a846..e95b07ee 100644 --- a/crates/bcr-ebill-core/src/protocol/mint/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/mint/mod.rs @@ -1,8 +1,6 @@ -use borsh::{BorshDeserialize, BorshSerialize}; -use uuid::Uuid; - -use crate::protocol::{Email, Sum, Timestamp}; +use crate::protocol::{Sum, Timestamp}; use bcr_common::core::{BillId, NodeId}; +use uuid::Uuid; /// A request to mint #[derive(Debug, Clone)] @@ -75,12 +73,3 @@ pub struct MintRequestState { /// There might be an offer pub offer: Option, } - -/// node_id/company_node_id => email mapping, signed by a mint -#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)] -pub struct MintSignature { - pub node_id: NodeId, - pub company_node_id: Option, - pub email: Email, - pub created_at: Timestamp, -} diff --git a/crates/bcr-ebill-core/src/protocol/mod.rs b/crates/bcr-ebill-core/src/protocol/mod.rs index dcadacae..4e7da90a 100644 --- a/crates/bcr-ebill-core/src/protocol/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/mod.rs @@ -25,7 +25,7 @@ pub use base::date::Date; pub use base::email::Email; pub use base::hash::Sha256Hash; pub use base::identification::Identification; -pub use base::identity_proof::IdentityProofStamp; +pub use base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; pub use base::name::Name; pub use base::signature::SchnorrSignature; pub use base::sum::Currency; diff --git a/crates/bcr-ebill-core/src/protocol/tests/mod.rs b/crates/bcr-ebill-core/src/protocol/tests/mod.rs index ec1ec58a..c4cc8358 100644 --- a/crates/bcr-ebill-core/src/protocol/tests/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/tests/mod.rs @@ -11,6 +11,8 @@ pub mod tests { use crate::protocol::Sum; use crate::protocol::Timestamp; use crate::protocol::Zip; + use crate::protocol::base::identity_proof::SignedEmailIdentityData; + use crate::protocol::base::identity_proof::SignedIdentityProof; use crate::protocol::blockchain::bill::BitcreditBill; use crate::protocol::blockchain::bill::ContactType; use crate::protocol::blockchain::bill::participant::BillIdentParticipant; @@ -189,6 +191,17 @@ pub mod tests { .unwrap() } + pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { + let data = SignedEmailIdentityData { + node_id: node_id_test(), + company_node_id: None, + email: Email::new("test@example.com").unwrap(), + created_at: Timestamp::new(1731593929).unwrap(), + }; + let proof = data.sign(&node_id_test(), &private_key_test()).unwrap(); + (proof, data) + } + // bitcrt285psGq4Lz4fEQwfM3We5HPznJq8p1YvRaddszFaU5dY pub fn bill_id_test() -> BillId { BillId::new( diff --git a/crates/bcr-ebill-persistence/src/db/email_notification.rs b/crates/bcr-ebill-persistence/src/db/email_notification.rs index 33277e98..6991d7b3 100644 --- a/crates/bcr-ebill-persistence/src/db/email_notification.rs +++ b/crates/bcr-ebill-persistence/src/db/email_notification.rs @@ -33,10 +33,8 @@ impl EmailNotificationStoreApi for SurrealEmailNotificationStore { node_id: node_id.to_owned(), email_preferences_link: email_preferences_link.to_owned(), }; - let _: Option = self - .db - .create(Self::TABLE, Some(node_id.to_string()), db) - .await?; + let _: Option = + self.db.upsert(Self::TABLE, node_id.to_string(), db).await?; Ok(()) } diff --git a/crates/bcr-ebill-persistence/src/db/identity.rs b/crates/bcr-ebill-persistence/src/db/identity.rs index 142f71f2..c3145b71 100644 --- a/crates/bcr-ebill-persistence/src/db/identity.rs +++ b/crates/bcr-ebill-persistence/src/db/identity.rs @@ -8,7 +8,8 @@ use bcr_ebill_core::{ identity::{ActiveIdentityState, Identity, IdentityWithAll}, }, protocol::{ - City, Country, Date, Email, Identification, Name, SecretKey, + City, Country, Date, Email, Identification, Name, SchnorrSignature, SecretKey, + SignedEmailIdentityData, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; @@ -24,6 +25,7 @@ impl SurrealIdentityStore { const ACTIVE_IDENTITY_TABLE: &'static str = "active_identity"; const KEY_TABLE: &'static str = "identity_key"; const NETWORK_TABLE: &'static str = "identity_network"; + const EMAIL_CONFIRMATION_TABLE: &'static str = "email_confirmation"; const UNIQUE_ID: &'static str = "unique_record"; pub fn new(db: SurrealWrapper) -> Self { @@ -180,6 +182,79 @@ impl IdentityStoreApi for SurrealIdentityStore { .await?; Ok(()) } + + async fn get_email_confirmations( + &self, + ) -> Result> { + let result: Vec = + self.db.select_all(Self::EMAIL_CONFIRMATION_TABLE).await?; + Ok(result + .into_iter() + .map(|confirmation| confirmation.into()) + .collect()) + } + + async fn set_email_confirmation( + &self, + proof: &SignedIdentityProof, + data: &SignedEmailIdentityData, + ) -> Result<()> { + let keys = self.get_key_pair().await?; + if keys.pub_key() != data.node_id.pub_key() { + return Err(Error::PublicKeyDoesNotMatch); + } + + let entity: IdentityEmailConfirmationDb = (proof.to_owned(), data.to_owned()).into(); + let _: Option = self + .db + .upsert( + Self::EMAIL_CONFIRMATION_TABLE, + proof.witness.to_string(), + entity, + ) + .await?; + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdentityEmailConfirmationDb { + pub signature: SchnorrSignature, + pub witness: NodeId, + pub node_id: NodeId, + pub company_node_id: Option, + pub email: Email, + pub created_at: Timestamp, +} + +impl From<(SignedIdentityProof, SignedEmailIdentityData)> for IdentityEmailConfirmationDb { + fn from((proof, data): (SignedIdentityProof, SignedEmailIdentityData)) -> Self { + IdentityEmailConfirmationDb { + signature: proof.signature, + witness: proof.witness, + node_id: data.node_id, + company_node_id: data.company_node_id, + email: data.email, + created_at: data.created_at, + } + } +} + +impl From for (SignedIdentityProof, SignedEmailIdentityData) { + fn from(value: IdentityEmailConfirmationDb) -> Self { + ( + SignedIdentityProof { + signature: value.signature, + witness: value.witness, + }, + SignedEmailIdentityData { + node_id: value.node_id, + company_node_id: value.company_node_id, + email: value.email, + created_at: value.created_at, + }, + ) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -290,7 +365,10 @@ impl From for BcrKeys { #[cfg(test)] mod tests { use super::*; - use crate::{db::get_memory_db, tests::tests::empty_identity}; + use crate::{ + db::get_memory_db, + tests::tests::{empty_identity, private_key_test, signed_identity_proof_test}, + }; async fn get_store() -> SurrealIdentityStore { let mem_db = get_memory_db("test", "identity") @@ -344,4 +422,20 @@ mod tests { let fetched_key_pair = store.get_key_pair().await.unwrap(); assert_eq!(keys.get_public_key(), fetched_key_pair.get_public_key()); } + + #[tokio::test] + async fn test_set_get_email_confirmation() { + let store = get_store().await; + let key = BcrKeys::from_private_key(&private_key_test()); + store.save_key_pair(&key, "").await.unwrap(); + + let (proof, data) = signed_identity_proof_test(); + store + .set_email_confirmation(&proof, &data) + .await + .expect("works"); + let email_confirmations = store.get_email_confirmations().await.expect("works"); + assert_eq!(email_confirmations.len(), 1); + assert_eq!(email_confirmations[0].0.signature, proof.signature); + } } diff --git a/crates/bcr-ebill-persistence/src/identity.rs b/crates/bcr-ebill-persistence/src/identity.rs index 69ef90fc..d8d14c76 100644 --- a/crates/bcr-ebill-persistence/src/identity.rs +++ b/crates/bcr-ebill-persistence/src/identity.rs @@ -2,10 +2,15 @@ use super::Result; use async_trait::async_trait; use bcr_ebill_core::{ - application::ServiceTraitBounds, - application::identity::{ActiveIdentityState, Identity, IdentityWithAll}, - protocol::blockchain::identity::{IdentityBlock, IdentityBlockchain}, - protocol::crypto::BcrKeys, + application::{ + ServiceTraitBounds, + identity::{ActiveIdentityState, Identity, IdentityWithAll}, + }, + protocol::{ + SignedEmailIdentityData, SignedIdentityProof, + blockchain::identity::{IdentityBlock, IdentityBlockchain}, + crypto::BcrKeys, + }, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -34,6 +39,16 @@ pub trait IdentityStoreApi: ServiceTraitBounds { async fn set_current_identity(&self, identity_state: &ActiveIdentityState) -> Result<()>; /// Sets the network for this identity, or, if it's set, checks if the set network is the same as the configured one async fn set_or_check_network(&self, configured_network: bitcoin::Network) -> Result<()>; + /// Gets the email confirmation state for this identity + async fn get_email_confirmations( + &self, + ) -> Result>; + /// Sets the email confirmation state for this identity + async fn set_email_confirmation( + &self, + proof: &SignedIdentityProof, + data: &SignedEmailIdentityData, + ) -> Result<()>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/crates/bcr-ebill-persistence/src/lib.rs b/crates/bcr-ebill-persistence/src/lib.rs index 787e63a9..4312c14d 100644 --- a/crates/bcr-ebill-persistence/src/lib.rs +++ b/crates/bcr-ebill-persistence/src/lib.rs @@ -44,6 +44,9 @@ pub enum Error { #[error("Network does not match")] NetworkDoesNotMatch, + #[error("Public Key does not match")] + PublicKeyDoesNotMatch, + #[error("Error with encoding, or decoding")] EncodingError, } diff --git a/crates/bcr-ebill-persistence/src/tests/mod.rs b/crates/bcr-ebill-persistence/src/tests/mod.rs index a19f84e3..6f546aee 100644 --- a/crates/bcr-ebill-persistence/src/tests/mod.rs +++ b/crates/bcr-ebill-persistence/src/tests/mod.rs @@ -15,7 +15,7 @@ pub mod tests { }, protocol::{ Address, City, Country, Date, Email, Name, OptionalPostalAddress, PostalAddress, - PublicKey, SecretKey, Sum, Timestamp, + PublicKey, SecretKey, SignedEmailIdentityData, SignedIdentityProof, Sum, Timestamp, blockchain::{ bill::{ BillHistory, BitcreditBill, ContactType, @@ -204,6 +204,17 @@ pub mod tests { .unwrap() } + pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { + let data = SignedEmailIdentityData { + node_id: node_id_test(), + company_node_id: None, + email: Email::new("test@example.com").unwrap(), + created_at: Timestamp::new(1731593929).unwrap(), + }; + let proof = data.sign(&node_id_test(), &private_key_test()).unwrap(); + (proof, data) + } + // bitcrt285psGq4Lz4fEQwfM3We5HPznJq8p1YvRaddszFaU5dY pub fn bill_id_test() -> BillId { BillId::new( diff --git a/crates/bcr-ebill-transport/src/handler/company_chain_event_processor.rs b/crates/bcr-ebill-transport/src/handler/company_chain_event_processor.rs index ab7aa786..c007b39f 100644 --- a/crates/bcr-ebill-transport/src/handler/company_chain_event_processor.rs +++ b/crates/bcr-ebill-transport/src/handler/company_chain_event_processor.rs @@ -481,7 +481,6 @@ pub mod tests { use bcr_ebill_core::protocol::event::{CompanyBlockEvent, Event, EventEnvelope}; use bcr_ebill_core::{ application::company::Company, - protocol::IdentityProofStamp, protocol::Name, protocol::Sha256Hash, protocol::Timestamp, @@ -497,13 +496,14 @@ pub mod tests { }; use mockall::predicate::{always, eq}; + use crate::handler::test_utils::signed_identity_proof_test; use crate::{ handler::{ CompanyChainEventProcessor, CompanyChainEventProcessorApi, MockNostrContactProcessorApi, MockNotificationHandlerApi, test_utils::{ MockCompanyChainStore, MockCompanyStore, MockIdentityStore, get_company_data, - node_id_test, private_key_test, + node_id_test, }, }, test_utils::{MockNotificationJsonTransport, get_baseline_identity}, @@ -1195,9 +1195,10 @@ pub mod tests { &keys, )]; let chain = CompanyBlockchain::new_from_blocks(blocks).expect("could not create chain"); + let test_signed_identity = signed_identity_proof_test(); let data = CompanyIdentityProofBlockData { - stamp: IdentityProofStamp::new(&node_id_test(), &private_key_test()).unwrap(), - url: url::Url::parse("https://bit.cr").unwrap(), + proof: test_signed_identity.0, + data: test_signed_identity.1, }; let update_block = get_company_identity_proof_block( node_id.clone(), diff --git a/crates/bcr-ebill-transport/src/handler/identity_chain_event_processor.rs b/crates/bcr-ebill-transport/src/handler/identity_chain_event_processor.rs index 0bc9e976..11c03a1b 100644 --- a/crates/bcr-ebill-transport/src/handler/identity_chain_event_processor.rs +++ b/crates/bcr-ebill-transport/src/handler/identity_chain_event_processor.rs @@ -299,8 +299,11 @@ impl IdentityChainEventProcessor { IdentityBlockPayload::SignCompanyBill(_) => { /* handled in company chain */ } IdentityBlockPayload::RemoveSignatory(_) => { /* no action needed */ } IdentityBlockPayload::Create(_) => { /* creates are handled on validation */ } - IdentityBlockPayload::IdentityProof(_) => { - // TODO: handle identity proof block + IdentityBlockPayload::IdentityProof(data) => { + self.identity_store + .set_email_confirmation(&data.proof, &data.data) + .await + .map_err(|e| Error::Persistence(e.to_string()))?; } } @@ -379,7 +382,6 @@ pub mod tests { use bcr_ebill_core::protocol::event::{Event, EventEnvelope, IdentityBlockEvent}; use bcr_ebill_core::{ application::identity::Identity, - protocol::IdentityProofStamp, protocol::Name, protocol::Sha256Hash, protocol::Timestamp, @@ -393,6 +395,7 @@ pub mod tests { }; use mockall::predicate::{always, eq}; + use crate::handler::test_utils::signed_identity_proof_test; use crate::{ handler::{ IdentityChainEventProcessorApi, MockNostrContactProcessorApi, @@ -400,7 +403,6 @@ pub mod tests { identity_chain_event_processor::IdentityChainEventProcessor, test_utils::{ MockIdentityChainStore, MockIdentityStore, get_baseline_identity, node_id_test, - private_key_test, }, }, test_utils::MockNotificationJsonTransport, @@ -733,9 +735,10 @@ pub mod tests { let keys = full.key_pair.clone(); let blocks = vec![get_identity_create_block(full.identity, &full.key_pair)]; let chain = IdentityBlockchain::new_from_blocks(blocks).expect("could not create chain"); + let test_signed_identity = signed_identity_proof_test(); let data = IdentityProofBlockData { - stamp: IdentityProofStamp::new(&node_id_test(), &private_key_test()).unwrap(), - url: url::Url::parse("https://bit.cr").unwrap(), + proof: test_signed_identity.0, + data: test_signed_identity.1, }; let identity_proof_block = get_identity_proof_block(chain.get_latest_block(), &keys, &data); @@ -746,6 +749,12 @@ pub mod tests { .returning(move || Ok(expected_identity.clone())) .once(); + // incoming identity proof is set + store + .expect_set_email_confirmation() + .returning(|_, _| Ok(())) + .times(1); + // checks if we already have the chain chain_store .expect_get_chain() diff --git a/crates/bcr-ebill-transport/src/handler/mod.rs b/crates/bcr-ebill-transport/src/handler/mod.rs index bd00db82..703a68ff 100644 --- a/crates/bcr-ebill-transport/src/handler/mod.rs +++ b/crates/bcr-ebill-transport/src/handler/mod.rs @@ -358,6 +358,7 @@ mod test_utils { crypto::BcrKeys, event::ActionType, }; + use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; use bcr_ebill_persistence::{ NostrChainEventStoreApi, NotificationStoreApi, Result, bill::{BillChainStoreApi, BillStoreApi}, @@ -530,6 +531,14 @@ mod test_utils { async fn get_current_identity(&self) -> Result; async fn set_current_identity(&self, identity_state: &bcr_ebill_core::application::identity::ActiveIdentityState) -> Result<()>; async fn set_or_check_network(&self, configured_network: bitcoin::Network) -> Result<()>; + async fn get_email_confirmations( + &self, + ) -> Result>; + async fn set_email_confirmation( + &self, + proof: &SignedIdentityProof, + data: &SignedEmailIdentityData, + ) -> Result<()>; } } @@ -780,4 +789,15 @@ mod test_utils { NodeId::from_str("bitcrt03f9f94d1fdc2090d46f3524807e3f58618c36988e69577d70d5d4d1e9e9645a4f") .unwrap() } + + pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { + let data = SignedEmailIdentityData { + node_id: node_id_test(), + company_node_id: None, + email: Email::new("test@example.com").unwrap(), + created_at: Timestamp::new(1731593929).unwrap(), + }; + let proof = data.sign(&node_id_test(), &private_key_test()).unwrap(); + (proof, data) + } } diff --git a/crates/bcr-ebill-transport/src/test_utils.rs b/crates/bcr-ebill-transport/src/test_utils.rs index f761161e..24407980 100644 --- a/crates/bcr-ebill-transport/src/test_utils.rs +++ b/crates/bcr-ebill-transport/src/test_utils.rs @@ -19,7 +19,7 @@ use bcr_ebill_core::protocol::blockchain::bill::BillBlockchain; use bcr_ebill_core::protocol::blockchain::bill::block::BillIssueBlockData; use bcr_ebill_core::protocol::crypto::BcrKeys; use bcr_ebill_core::protocol::event::{EventEnvelope, EventType}; -use bcr_ebill_core::protocol::mint::MintSignature; +use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; use bcr_ebill_core::{ application::ServiceTraitBounds, application::contact::Contact, @@ -145,7 +145,10 @@ pub fn init_test_cfg() { payment_config: bcr_ebill_api::PaymentConfig { num_confirmations_for_payment: 6, }, - dev_mode_config: DevModeConfig { on: false }, + dev_mode_config: DevModeConfig { + on: false, + disable_mandatory_email_confirmations: false, + }, court_config: CourtConfig { default_url: url::Url::parse("https://court-dev.minibill.tech").unwrap(), }, @@ -771,7 +774,7 @@ mockall::mock! { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> bcr_ebill_api::external::email::Result; + ) -> bcr_ebill_api::external::email::Result<(SignedIdentityProof, SignedEmailIdentityData)>; async fn send_bill_notification( &self, mint_url: &url::Url, diff --git a/crates/bcr-ebill-wasm/index.html b/crates/bcr-ebill-wasm/index.html index d78d1d4f..4b02b4c4 100644 --- a/crates/bcr-ebill-wasm/index.html +++ b/crates/bcr-ebill-wasm/index.html @@ -45,6 +45,12 @@

Identity Testing

+
Identity Proof
+ Email: + + Confirmation Code: + +

Identity backup restore

diff --git a/crates/bcr-ebill-wasm/main.js b/crates/bcr-ebill-wasm/main.js index 588618bc..0d2dc11e 100644 --- a/crates/bcr-ebill-wasm/main.js +++ b/crates/bcr-ebill-wasm/main.js @@ -24,6 +24,9 @@ document.getElementById("switch_identity").addEventListener("click", switchIdent document.getElementById("share_contact_to").addEventListener("click", shareContact); document.getElementById("dev_mode_get_identity_chain").addEventListener("click", devModeGetIdentityChain); document.getElementById("sync_identity_chain").addEventListener("click", syncIdentityChain); +document.getElementById("confirm_email").addEventListener("click", confirmEmail); +document.getElementById("verify_email").addEventListener("click", verifyEmail); +document.getElementById("get_confirmations").addEventListener("click", getIdentityConfirmations); // bill actions document.getElementById("bill_fetch_detail").addEventListener("click", fetchBillDetail); @@ -91,46 +94,50 @@ let config = { job_runner_initial_delay_seconds: 5, job_runner_check_interval_seconds: 600, transport_initial_subscription_delay_seconds: 1, - // default_mint_url: "http://localhost:4343", - default_mint_url: "https://wildcat-dev-docker.minibill.tech", + default_mint_url: "http://localhost:4343", + // default_mint_url: "https://wildcat-dev-docker.minibill.tech", // default_mint_node_id: "bitcrt038d1bd3e2e3a01f20c861f18eb456cc33f869c9aaa5dec685f7f7d8c40ea3b3c7", - default_mint_node_id: "bitcrt02a2e6ecd9dfee6f88e6a0eb8ebdcfa4dae9905158889586fc18bbcccbd9fac5e7", // dev mint + default_mint_node_id: "bitcrt02c18f94838c95754478c14a7c90db417d7a1dd0099add2002b31b4513480b3e99", // dev mint num_confirmations_for_payment: 1, dev_mode: true, + disable_mandatory_email_confirmations: true, // default_court_url: "http://localhost:8000", default_court_url: "https://bcr-court-dev.minibill.tech" }; + async function start(create_identity) { await wasm.default(); await wasm.initialize_api(config); - let notificationApi = wasm.Api.notification(); - let identityApi = wasm.Api.identity(); - let contactApi = wasm.Api.contact(); - let companyApi = wasm.Api.company(); - let billApi = wasm.Api.bill(); - let generalApi = wasm.Api.general(); + window.notifApi = wasm.Api.notification(); + window.identityApi = wasm.Api.identity(); + window.contactApi = wasm.Api.contact(); + window.companyApi = wasm.Api.company(); + window.billApi = wasm.Api.bill(); + window.generalApi = wasm.Api.general(); + + console.log("Apis initialized.."); // Identity let identity; try { - identity = success_or_fail(await identityApi.detail()); + identity = success_or_fail(await window.identityApi.detail()); console.log("local identity:", identity); } catch (err) { if (create_identity) { await sleep(2000); // sleep to let Nostr connect before first setup console.log("No local identity found - creating anon identity.."); - fail_on_error(await identityApi.create({ + fail_on_error(await window.identityApi.create({ t: 1, name: "Cypherpunk", postal_address: {}, })); - identity = success_or_fail(await identityApi.detail()); + identity = success_or_fail(await window.identityApi.detail()); console.log("Deanonymizing identity.."); - fail_on_error(await identityApi.deanonymize({ + fail_on_error(await window.identityApi.deanonymize({ t: 0, name: "Johanna Smith", email: "jsmith@example.com", @@ -143,7 +150,7 @@ async function start(create_identity) { })); // add self to contacts - fail_on_error(await contactApi.create({ + fail_on_error(await window.contactApi.create({ t: 0, node_id: identity.node_id, name: "Self Contact", @@ -162,19 +169,19 @@ async function start(create_identity) { if (identity) { document.getElementById("identity").innerHTML = identity.node_id; - await notificationApi.subscribe((evt) => { + await window.notifApi.subscribe((evt) => { console.log("Received event in JS: ", evt); }); - current_identity = success_or_fail(await identityApi.active()); + current_identity = success_or_fail(await window.identityApi.active()); console.log(current_identity); document.getElementById("current_identity").innerHTML = current_identity.node_id; // Company - let companies = success_or_fail(await companyApi.list()); + let companies = success_or_fail(await window.companyApi.list()); console.log("companies:", companies.companies.length, companies); if (companies.companies.length == 0 && create_identity) { - let company = success_or_fail(await companyApi.create({ + let company = success_or_fail(await window.companyApi.create({ name: "hayek Ltd", email: "test@example.com", postal_address: { @@ -185,11 +192,11 @@ async function start(create_identity) { } })); console.log("company: ", company); - fail_on_error(await companyApi.edit({ id: company.id, email: "different@example.com", postal_address: {} })); - let detail = success_or_fail(await companyApi.detail(company.id)); + fail_on_error(await window.companyApi.edit({ id: company.id, email: "different@example.com", postal_address: {} })); + let detail = success_or_fail(await window.companyApi.detail(company.id)); console.log("company detail: ", detail); // add company to contacts - fail_on_error(await contactApi.create({ + fail_on_error(await window.contactApi.create({ t: 1, node_id: detail.id, name: "Company Contact", @@ -207,39 +214,27 @@ async function start(create_identity) { } // General - let currencies = success_or_fail(await generalApi.currencies()); + let currencies = success_or_fail(await window.generalApi.currencies()); console.log("currencies: ", currencies); - let status = success_or_fail(await generalApi.status()); + let status = success_or_fail(await window.generalApi.status()); console.log("status: ", status); if (identity) { - let search = success_or_fail(await generalApi.search({ filter: { search_term: "Test", currency: "SAT", item_types: ["Contact"] } })); + let search = success_or_fail(await window.generalApi.search({ filter: { search_term: "Test", currency: "SAT", item_types: ["Contact"] } })); console.log("search: ", search); } // Notifications if (current_identity) { let filter = current_identity ? { node_ids: [current_identity.node_id] } : null; - let notifications = success_or_fail(await notificationApi.list(filter)); + let notifications = success_or_fail(await window.notifApi.list(filter)); console.log("notifications: ", notifications); } console.log("Returning apis.."); - return { companyApi, generalApi, identityApi, billApi, contactApi, notificationApi }; } -let apis = await start(generateIdentity()); -let contactApi = apis.contactApi; -let companyApi = apis.companyApi; -let generalApi = apis.generalApi; -let identityApi = apis.identityApi; -let billApi = apis.billApi; -window.billApi = billApi; -window.identityApi = identityApi; -window.generalApi = generalApi; -let notificationTriggerApi = apis.notificationApi; -console.log("Apis initialized.."); - +await start(generateIdentity()); async function uploadFile(event) { const file = event.target.files[0]; @@ -257,7 +252,7 @@ async function uploadFile(event) { console.log("File Extension:", uploadedFile.extension); console.log("File Bytes:", uploadedFile.data); try { - let file_upload_response = success_or_fail(await contactApi.upload(uploadedFile)); + let file_upload_response = success_or_fail(await window.contactApi.upload(uploadedFile)); console.log("success uploading:", file_upload_response); document.getElementById("file_upload_id").value = file_upload_response.file_upload_id; } catch (err) { @@ -266,17 +261,17 @@ async function uploadFile(event) { } async function getSeedPhrase() { - let seed_phrase = success_or_fail(await identityApi.seed_backup()); + let seed_phrase = success_or_fail(await window.identityApi.seed_backup()); document.getElementById("current_seed").innerHTML = seed_phrase.seed_phrase; } async function restoreFromSeedPhrase() { let seed_phrase = document.getElementById("restore_seed_phrase").value; - fail_on_error(await identityApi.seed_recover({ seed_phrase })); + fail_on_error(await window.identityApi.seed_recover({ seed_phrase })); } async function createCompany() { - let company = success_or_fail(await companyApi.create({ + let company = success_or_fail(await window.companyApi.create({ name: "hayek Ltd", email: "test@example.com", postal_address: { @@ -292,7 +287,7 @@ async function createCompany() { async function updateCompany() { let company_id = document.getElementById("company_update_id").value; let name = document.getElementById("company_update_name").value; - fail_on_error(await companyApi.edit({ + fail_on_error(await window.companyApi.edit({ id: company_id, name: name, postal_address: {} @@ -303,7 +298,7 @@ async function updateCompany() { async function addSignatory() { let company_id = document.getElementById("company_update_id").value; let signatory_node_id = document.getElementById("company_signatory_id").value; - fail_on_error(await companyApi.add_signatory({ + fail_on_error(await window.companyApi.add_signatory({ id: company_id, signatory_node_id: signatory_node_id, })); @@ -313,7 +308,7 @@ async function addSignatory() { async function removeSignatory() { let company_id = document.getElementById("company_update_id").value; let signatory_node_id = document.getElementById("company_signatory_id").value; - fail_on_error(await companyApi.remove_signatory({ + fail_on_error(await window.companyApi.remove_signatory({ id: company_id, signatory_node_id: signatory_node_id, })); @@ -324,17 +319,17 @@ async function shareCompanyContact() { let node_id = document.getElementById("company_update_id").value; let share_to_node_id = document.getElementById("company_signatory_id").value; console.log("sharing contact details to identity: ", node_id); - fail_on_error(await companyApi.share_contact_details({ recipient: share_to_node_id, company_id: node_id })); + fail_on_error(await window.companyApi.share_contact_details({ recipient: share_to_node_id, company_id: node_id })); } async function listCompanies() { - let companies = success_or_fail(await companyApi.list()); + let companies = success_or_fail(await window.companyApi.list()); console.log("companies:", companies.companies.length, companies); } async function listSignatories() { let measured = measure(async () => { - return success_or_fail(await companyApi.list_signatories(document.getElementById("company_update_id").value)); + return success_or_fail(await window.companyApi.list_signatories(document.getElementById("company_update_id").value)); }); await measured(); } @@ -342,12 +337,12 @@ async function listSignatories() { async function triggerContact() { let node_id = document.getElementById("node_id_contact").value; try { - let contact = success_or_fail(await contactApi.detail(node_id)); + let contact = success_or_fail(await window.contactApi.detail(node_id)); console.log("contact:", contact); } catch (err) { console.log("No contact found - creating.."); let file_upload_id = document.getElementById("file_upload_id").value || undefined; - fail_on_error(await contactApi.create({ + fail_on_error(await window.contactApi.create({ t: 0, node_id: node_id, name: "Test Contact", @@ -361,7 +356,7 @@ async function triggerContact() { avatar_file_upload_id: file_upload_id, })); } - let contact = success_or_fail(await contactApi.detail(node_id)); + let contact = success_or_fail(await window.contactApi.detail(node_id)); console.log("contact:", contact); document.getElementById("contact_id").value = node_id; document.getElementById("node_id_bill").value = node_id; @@ -373,11 +368,11 @@ async function triggerContact() { async function triggerAnonContact() { let node_id = document.getElementById("node_id_contact").value; try { - let contact = success_or_fail(await contactApi.detail(node_id)); + let contact = success_or_fail(await window.contactApi.detail(node_id)); console.log("anon contact:", contact); } catch (err) { console.log("No contact found - creating.."); - fail_on_error(await contactApi.create({ + fail_on_error(await window.contactApi.create({ t: 2, node_id: node_id, name: "some anon dude", @@ -400,7 +395,7 @@ async function triggerBill(t, blank) { let file_upload_id = document.getElementById("file_upload_id").value || undefined; let node_id = document.getElementById("node_id_bill").value; - let identity = success_or_fail(await identityApi.detail()); + let identity = success_or_fail(await window.identityApi.detail()); let bill_issue_data = { t, country_of_issuing: "at", @@ -417,9 +412,9 @@ async function triggerBill(t, blank) { }; let bill; if (blank) { - bill = success_or_fail(await billApi.issue_blank(bill_issue_data)); + bill = success_or_fail(await window.billApi.issue_blank(bill_issue_data)); } else { - bill = success_or_fail(await billApi.issue(bill_issue_data)); + bill = success_or_fail(await window.billApi.issue(bill_issue_data)); } let bill_id = bill.id; console.log("created bill with id: ", bill_id); @@ -433,7 +428,7 @@ async function triggerNotif() { async function fetchTempFile() { let file_upload_id = document.getElementById("file_upload_id").value; - let temp_file = success_or_fail(await generalApi.temp_file(file_upload_id)); + let temp_file = success_or_fail(await window.generalApi.temp_file(file_upload_id)); let file_bytes = temp_file.data; let arr = new Uint8Array(file_bytes); let blob = new Blob([arr], { type: temp_file.content_type }); @@ -446,27 +441,27 @@ async function fetchTempFile() { async function fetchContactFile() { let node_id = document.getElementById("contact_id").value; let file_name = document.getElementById("contact_file_name").value; - let file = success_or_fail(await contactApi.file_base64(node_id, file_name)); + let file = success_or_fail(await window.contactApi.file_base64(node_id, file_name)); document.getElementById("attached_file").src = `data:${file.content_type};base64,${file.data}`; } async function switchIdentity() { let node_id = document.getElementById("node_id_identity").value; - fail_on_error(await identityApi.switch({ t: 1, node_id })); + fail_on_error(await window.identityApi.switch({ t: 1, node_id })); document.getElementById("current_identity").textContent = node_id; } async function shareContact() { let node_id = document.getElementById("node_id_identity").value; console.log("sharing contact details to identity: ", node_id); - fail_on_error(await identityApi.share_contact_details({ recipient: node_id })); + fail_on_error(await window.identityApi.share_contact_details({ recipient: node_id })); } async function endorseBill() { let bill_id = document.getElementById("endorse_bill_id").value; let endorsee = document.getElementById("endorsee_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.endorse_bill({ bill_id, endorsee })); + return success_or_fail(await window.billApi.endorse_bill({ bill_id, endorsee })); }); await measured(); } @@ -475,7 +470,7 @@ async function endorseBillBlank() { let bill_id = document.getElementById("endorse_bill_id").value; let endorsee = document.getElementById("endorsee_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.endorse_bill_blank({ bill_id, endorsee })); + return success_or_fail(await window.billApi.endorse_bill_blank({ bill_id, endorsee })); }); await measured(); } @@ -483,7 +478,7 @@ async function endorseBillBlank() { async function requestToAcceptBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.request_to_accept({ bill_id, acceptance_deadline: getDeadlineDate() })); + return success_or_fail(await window.billApi.request_to_accept({ bill_id, acceptance_deadline: getDeadlineDate() })); }); await measured(); } @@ -491,7 +486,7 @@ async function requestToAcceptBill() { async function acceptBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.accept({ bill_id })); + return success_or_fail(await window.billApi.accept({ bill_id })); }); await measured(); } @@ -499,7 +494,7 @@ async function acceptBill() { async function requestToPayBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.request_to_pay({ bill_id, currency: "SAT", payment_deadline: getDeadlineDate() })); + return success_or_fail(await window.billApi.request_to_pay({ bill_id, currency: "SAT", payment_deadline: getDeadlineDate() })); }); await measured(); } @@ -508,7 +503,7 @@ async function offerToSellBill() { let bill_id = document.getElementById("endorse_bill_id").value; let endorsee = document.getElementById("endorsee_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.offer_to_sell({ bill_id, sum: "500", currency: "SAT", buyer: endorsee, buying_deadline: getDeadlineDate() })); + return success_or_fail(await window.billApi.offer_to_sell({ bill_id, sum: "500", currency: "SAT", buyer: endorsee, buying_deadline: getDeadlineDate() })); }); await measured(); } @@ -517,7 +512,7 @@ async function requestToRecourseBill() { let bill_id = document.getElementById("endorse_bill_id").value; let endorsee = document.getElementById("endorsee_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.request_to_recourse_bill_acceptance({ bill_id, recoursee: endorsee, recourse_deadline: getDeadlineDate() })); + return success_or_fail(await window.billApi.request_to_recourse_bill_acceptance({ bill_id, recoursee: endorsee, recourse_deadline: getDeadlineDate() })); }); await measured(); } @@ -526,7 +521,7 @@ async function requestToRecourseBillPayment() { let bill_id = document.getElementById("endorse_bill_id").value; let endorsee = document.getElementById("endorsee_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.request_to_recourse_bill_payment({ bill_id, recoursee: endorsee, currency: "SAT", sum: "1500", recourse_deadline: getDeadlineDate() })); + return success_or_fail(await window.billApi.request_to_recourse_bill_payment({ bill_id, recoursee: endorsee, currency: "SAT", sum: "1500", recourse_deadline: getDeadlineDate() })); }); await measured(); } @@ -534,7 +529,7 @@ async function requestToRecourseBillPayment() { async function rejectAcceptBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.reject_to_accept({ bill_id })); + return success_or_fail(await window.billApi.reject_to_accept({ bill_id })); }); await measured(); } @@ -542,7 +537,7 @@ async function rejectAcceptBill() { async function rejectPayBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.reject_to_pay({ bill_id })); + return success_or_fail(await window.billApi.reject_to_pay({ bill_id })); }); await measured(); } @@ -550,7 +545,7 @@ async function rejectPayBill() { async function rejectBuyingBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.reject_to_buy({ bill_id })); + return success_or_fail(await window.billApi.reject_to_buy({ bill_id })); }); await measured(); } @@ -558,7 +553,7 @@ async function rejectBuyingBill() { async function rejectRecourseBill() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.reject_to_pay_recourse({ bill_id })); + return success_or_fail(await window.billApi.reject_to_pay_recourse({ bill_id })); }); await measured(); } @@ -566,7 +561,7 @@ async function rejectRecourseBill() { async function requestToMint() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.request_to_mint({ bill_id, mint_node: config.default_mint_node_id })); + return success_or_fail(await window.billApi.request_to_mint({ bill_id, mint_node: config.default_mint_node_id })); }); await measured(); } @@ -574,7 +569,7 @@ async function requestToMint() { async function getMintState() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.mint_state(bill_id)); + return success_or_fail(await window.billApi.mint_state(bill_id)); }); await measured(); } @@ -582,7 +577,7 @@ async function getMintState() { async function checkMintState() { let bill_id = document.getElementById("endorse_bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.check_mint_state(bill_id)); + return success_or_fail(await window.billApi.check_mint_state(bill_id)); }); await measured(); } @@ -590,7 +585,7 @@ async function checkMintState() { async function cancelRegToMint() { let mint_request_id = document.getElementById("mint_req_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.cancel_request_to_mint(mint_request_id)); + return success_or_fail(await window.billApi.cancel_request_to_mint(mint_request_id)); }); await measured(); } @@ -598,7 +593,7 @@ async function cancelRegToMint() { async function acceptMintOffer() { let mint_request_id = document.getElementById("mint_req_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.accept_mint_offer(mint_request_id)); + return success_or_fail(await window.billApi.accept_mint_offer(mint_request_id)); }); await measured(); } @@ -606,45 +601,45 @@ async function acceptMintOffer() { async function rejectMintOffer() { let mint_request_id = document.getElementById("mint_req_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.reject_mint_offer(mint_request_id)); + return success_or_fail(await window.billApi.reject_mint_offer(mint_request_id)); }); await measured(); } async function fetchBillDetail() { let measured = measure(async () => { - return success_or_fail(await billApi.detail(document.getElementById("bill_id").value)); + return success_or_fail(await window.billApi.detail(document.getElementById("bill_id").value)); }); await measured(); } async function fetchBillEndorsements() { let measured = measure(async () => { - return success_or_fail(await billApi.endorsements(document.getElementById("bill_id").value)); + return success_or_fail(await window.billApi.endorsements(document.getElementById("bill_id").value)); }); await measured(); } async function fetchBillPastEndorsees() { let measured = measure(async () => { - return success_or_fail(await billApi.past_endorsees(document.getElementById("bill_id").value)); + return success_or_fail(await window.billApi.past_endorsees(document.getElementById("bill_id").value)); }); await measured(); } async function fetchBillPastPayments() { let measured = measure(async () => { - return success_or_fail(await billApi.past_payments(document.getElementById("bill_id").value)); + return success_or_fail(await window.billApi.past_payments(document.getElementById("bill_id").value)); }); await measured(); } async function fetchBillFile() { let bill_id = document.getElementById("bill_id").value; - let detail = success_or_fail(await billApi.detail(bill_id)); + let detail = success_or_fail(await window.billApi.detail(bill_id)); if (detail.data.files.length > 0) { - let file = success_or_fail(await billApi.attachment_base64(bill_id, detail.data.files[0].name)); + let file = success_or_fail(await window.billApi.attachment_base64(bill_id, detail.data.files[0].name)); document.getElementById("bill_attached_file").src = `data:${file.content_type};base64,${file.data}`; } else { console.log("Bill has no file"); @@ -653,21 +648,21 @@ async function fetchBillFile() { async function fetchBillBills() { let measured = measure(async () => { - return success_or_fail(await billApi.list()); + return success_or_fail(await window.billApi.list()); }); await measured(); } async function fetchBillBalances() { let measured = measure(async () => { - return success_or_fail(await generalApi.overview("SAT")); + return success_or_fail(await window.generalApi.overview("SAT")); }); await measured(); } async function fetchBillSearch() { let measured = measure(async () => { - return success_or_fail(await billApi.search({ filter: { currency: "SAT", role: "All" } })); + return success_or_fail(await window.billApi.search({ filter: { currency: "SAT", role: "All" } })); }); await measured(); } @@ -675,14 +670,14 @@ async function fetchBillSearch() { async function fetchBillHistory() { let bill_id = document.getElementById("bill_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.bill_history(bill_id)); + return success_or_fail(await window.billApi.bill_history(bill_id)); }); await measured(); } async function clearBillCache() { let measured = measure(async () => { - return success_or_fail(await billApi.clear_bill_cache()); + return success_or_fail(await window.billApi.clear_bill_cache()); }); await measured(); } @@ -691,7 +686,7 @@ async function syncBillChain() { let bill_id = document.getElementById("bill_id").value; console.log("syncBillChain", bill_id); let measured = measure(async () => { - return success_or_fail(await billApi.sync_bill_chain({ bill_id: bill_id })); + return success_or_fail(await window.billApi.sync_bill_chain({ bill_id: bill_id })); }); await measured(); } @@ -700,7 +695,7 @@ async function syncCompanyChain() { let node_id = document.getElementById("company_update_id").value; console.log("syncCompanyChain", node_id); let measured = measure(async () => { - return success_or_fail(await companyApi.sync_company_chain({ node_id: node_id })); + return success_or_fail(await window.companyApi.sync_company_chain({ node_id: node_id })); }); await measured(); } @@ -708,7 +703,33 @@ async function syncCompanyChain() { async function syncIdentityChain() { console.log("syncIdentityChain"); let measured = measure(async () => { - return success_or_fail(await identityApi.sync_identity_chain()); + return success_or_fail(await window.identityApi.sync_identity_chain()); + }); + await measured(); +} + +async function confirmEmail() { + console.log("confirmEmail"); + let email = document.getElementById("email_to_confirm").value; + let measured = measure(async () => { + return success_or_fail(await window.identityApi.confirm_email(email)); + }); + await measured(); +} + +async function verifyEmail() { + console.log("verifyEmail"); + let code = document.getElementById("confirmation_code").value; + let measured = measure(async () => { + return success_or_fail(await window.identityApi.verify_email(code)); + }); + await measured(); +} + +async function getIdentityConfirmations() { + console.log("getIdentityConfirmations"); + let measured = measure(async () => { + return success_or_fail(await window.identityApi.get_email_confirmations()); }); await measured(); } @@ -717,7 +738,7 @@ async function shareBillWithCourt() { let bill_id = document.getElementById("court_bill_id").value; let court_node_id = document.getElementById("court_node_id").value; let measured = measure(async () => { - return success_or_fail(await billApi.share_bill_with_court({ bill_id: bill_id, court_node_id: court_node_id })); + return success_or_fail(await window.billApi.share_bill_with_court({ bill_id: bill_id, court_node_id: court_node_id })); }); await measured(); } @@ -726,7 +747,7 @@ async function devModeGetBillChain() { let bill_id = document.getElementById("dev_mode_bill_id").value; console.log("devModeGetBillChain", bill_id); let measured = measure(async () => { - let res = success_or_fail(await billApi.dev_mode_get_full_bill_chain(bill_id)); + let res = success_or_fail(await window.billApi.dev_mode_get_full_bill_chain(bill_id)); return res.map((b) => { return JSON.parse(b); }) @@ -737,7 +758,7 @@ async function devModeGetBillChain() { async function devModeGetIdentityChain() { console.log("devModeGetIdentityChain"); let measured = measure(async () => { - let res = success_or_fail(await identityApi.dev_mode_get_full_identity_chain()); + let res = success_or_fail(await window.identityApi.dev_mode_get_full_identity_chain()); return res.map((b) => { return JSON.parse(b); }) @@ -749,7 +770,7 @@ async function devModeGetCompanyChain() { let company_id = document.getElementById("dev_mode_company_id").value; console.log("devModeGetCompanyChain", company_id); let measured = measure(async () => { - let res = success_or_fail(await companyApi.dev_mode_get_full_company_chain(company_id)); + let res = success_or_fail(await window.companyApi.dev_mode_get_full_company_chain(company_id)); return res.map((b) => { return JSON.parse(b); }) @@ -774,14 +795,14 @@ function measure(promiseFunction) { async function fetchContacts() { let measured = measure(async () => { - return success_or_fail(await contactApi.list()); + return success_or_fail(await window.contactApi.list()); }); await measured(); } async function searchContacts() { let measured = measure(async () => { - return success_or_fail(await contactApi.search({ + return success_or_fail(await window.contactApi.search({ search_term: document.getElementById("contact_search_term").value, include_logical: true, include_contact: true @@ -793,7 +814,7 @@ async function searchContacts() { async function removeContactAvatar() { let node_id = document.getElementById("node_id_contact").value; let measured = measure(async () => { - return success_or_fail(await contactApi.edit({ node_id: node_id, avatar_file_upload_id: undefined, postal_address: {} })); + return success_or_fail(await window.contactApi.edit({ node_id: node_id, avatar_file_upload_id: undefined, postal_address: {} })); }); await measured(); } @@ -801,7 +822,7 @@ async function removeContactAvatar() { async function deleteContact() { let node_id = document.getElementById("node_id_contact").value; let measured = measure(async () => { - return success_or_fail(await contactApi.remove(node_id)); + return success_or_fail(await window.contactApi.remove(node_id)); }); await measured(); } diff --git a/crates/bcr-ebill-wasm/src/api/identity.rs b/crates/bcr-ebill-wasm/src/api/identity.rs index d2128ff8..737256f5 100644 --- a/crates/bcr-ebill-wasm/src/api/identity.rs +++ b/crates/bcr-ebill-wasm/src/api/identity.rs @@ -7,8 +7,8 @@ use crate::{ Base64FileResponse, BinaryFileResponse, OptionalPostalAddressWeb, UploadFile, UploadFileResponse, has_field, identity::{ - ChangeIdentityPayload, IdentityTypeWeb, IdentityWeb, NewIdentityPayload, SeedPhrase, - ShareContactTo, SwitchIdentity, + ChangeIdentityPayload, IdentityEmailConfirmationWeb, IdentityTypeWeb, IdentityWeb, + NewIdentityPayload, SeedPhrase, ShareContactTo, SwitchIdentity, }, }, error::WasmError, @@ -478,6 +478,46 @@ impl Identity { .await; TSResult::res_to_js(res) } + + #[wasm_bindgen(unchecked_return_type = "TSResult")] + pub async fn confirm_email(&self, email: &str) -> JsValue { + let res: Result<()> = async { + let parsed_email = Email::new(email)?; + get_ctx() + .identity_service + .confirm_email(&parsed_email) + .await?; + Ok(()) + } + .await; + TSResult::res_to_js(res) + } + + #[wasm_bindgen(unchecked_return_type = "TSResult")] + pub async fn verify_email(&self, confirmation_code: &str) -> JsValue { + let res: Result<()> = async { + get_ctx() + .identity_service + .verify_email(confirmation_code) + .await?; + Ok(()) + } + .await; + TSResult::res_to_js(res) + } + + #[wasm_bindgen(unchecked_return_type = "TSResult")] + pub async fn get_email_confirmations(&self) -> JsValue { + let res: Result> = async { + let email_confirmations = get_ctx().identity_service.get_email_confirmations().await?; + Ok(email_confirmations + .into_iter() + .map(|ec| ec.into()) + .collect()) + } + .await; + TSResult::res_to_js(res) + } } impl Default for Identity { diff --git a/crates/bcr-ebill-wasm/src/context.rs b/crates/bcr-ebill-wasm/src/context.rs index 16a3d6c8..8f70a8cf 100644 --- a/crates/bcr-ebill-wasm/src/context.rs +++ b/crates/bcr-ebill-wasm/src/context.rs @@ -53,7 +53,7 @@ impl Context { let transport_service = create_transport_service( nostr_clients.clone(), db.clone(), - email_client, + email_client.clone(), cfg.nostr_config.relays.to_owned(), ) .await?; @@ -92,6 +92,8 @@ impl Context { file_upload_client.clone(), db.identity_chain_store.clone(), transport_service.clone(), + email_client.clone(), + db.email_notification_store.clone(), ); let company_service = CompanyService::new( diff --git a/crates/bcr-ebill-wasm/src/data/identity.rs b/crates/bcr-ebill-wasm/src/data/identity.rs index 3c6c5cb6..5bd3b0de 100644 --- a/crates/bcr-ebill-wasm/src/data/identity.rs +++ b/crates/bcr-ebill-wasm/src/data/identity.rs @@ -7,7 +7,8 @@ use bcr_ebill_core::{ nostr_contact::NostrPublicKey, }, protocol::{ - City, Country, Date, Email, Identification, Name, PublicKey, + City, Country, Date, Email, Identification, Name, PublicKey, SchnorrSignature, + SignedEmailIdentityData, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; @@ -190,3 +191,32 @@ pub struct ShareCompanyContactTo { #[tsify(type = "string")] pub company_id: NodeId, } + +#[derive(Tsify, Debug, Serialize)] +pub struct IdentityEmailConfirmationWeb { + #[tsify(type = "string")] + pub signature: SchnorrSignature, + #[tsify(type = "string")] + pub witness: NodeId, + #[tsify(type = "string")] + pub node_id: NodeId, + #[tsify(type = "string | undefined")] + pub company_node_id: Option, + #[tsify(type = "string")] + pub email: Email, + #[tsify(type = "number")] + pub created_at: Timestamp, +} + +impl From<(SignedIdentityProof, SignedEmailIdentityData)> for IdentityEmailConfirmationWeb { + fn from((proof, data): (SignedIdentityProof, SignedEmailIdentityData)) -> Self { + Self { + signature: proof.signature, + witness: proof.witness, + node_id: data.node_id, + company_node_id: data.company_node_id, + email: data.email, + created_at: data.created_at, + } + } +} diff --git a/crates/bcr-ebill-wasm/src/error.rs b/crates/bcr-ebill-wasm/src/error.rs index f4fd27d0..39f8631c 100644 --- a/crates/bcr-ebill-wasm/src/error.rs +++ b/crates/bcr-ebill-wasm/src/error.rs @@ -81,6 +81,7 @@ enum JsErrorType { RejectMintRequestNotOffered, AcceptMintRequestNotOffered, AcceptMintOfferExpired, + NoConfirmedEmailForIdentIdentity, NoFileForFileUploadId, NotFound, ExternalApi, @@ -246,6 +247,9 @@ fn validation_error_data(e: ValidationError) -> JsErrorData { err_400(e, JsErrorType::AcceptMintRequestNotOffered) } ValidationError::AcceptMintOfferExpired => err_400(e, JsErrorType::AcceptMintOfferExpired), + ValidationError::NoConfirmedEmailForIdentIdentity => { + err_400(e, JsErrorType::NoConfirmedEmailForIdentIdentity) + } } } diff --git a/crates/bcr-ebill-wasm/src/lib.rs b/crates/bcr-ebill-wasm/src/lib.rs index c6c822bd..dd17aa73 100644 --- a/crates/bcr-ebill-wasm/src/lib.rs +++ b/crates/bcr-ebill-wasm/src/lib.rs @@ -44,6 +44,7 @@ pub struct Config { pub default_mint_node_id: String, pub num_confirmations_for_payment: usize, pub dev_mode: bool, + pub disable_mandatory_email_confirmations: bool, pub default_court_url: String, } @@ -137,6 +138,7 @@ pub async fn initialize_api( }, dev_mode_config: DevModeConfig { on: config.dev_mode, + disable_mandatory_email_confirmations: config.disable_mandatory_email_confirmations, }, court_config: CourtConfig { default_url: url::Url::parse(&config.default_court_url) From 2e6e7e9500764a9b943a242558683a3abb70db04 Mon Sep 17 00:00:00 2001 From: Mario Zupan Date: Thu, 20 Nov 2025 09:45:33 +0100 Subject: [PATCH 2/3] review fixes --- crates/bcr-ebill-api/src/external/email.rs | 10 +++++----- .../bcr-ebill-api/src/service/identity_service.rs | 12 ++++++------ crates/bcr-ebill-api/src/tests/mod.rs | 10 +++++----- .../src/protocol/base/identity_proof.rs | 10 +++++----- .../src/protocol/blockchain/company/mod.rs | 4 ++-- .../src/protocol/blockchain/identity/mod.rs | 4 ++-- crates/bcr-ebill-core/src/protocol/mod.rs | 2 +- crates/bcr-ebill-core/src/protocol/tests/mod.rs | 6 +++--- crates/bcr-ebill-persistence/src/db/identity.rs | 14 +++++++------- crates/bcr-ebill-persistence/src/identity.rs | 6 +++--- crates/bcr-ebill-persistence/src/tests/mod.rs | 6 +++--- crates/bcr-ebill-transport/src/handler/mod.rs | 10 +++++----- crates/bcr-ebill-transport/src/test_utils.rs | 4 ++-- crates/bcr-ebill-wasm/src/data/identity.rs | 6 +++--- 14 files changed, 52 insertions(+), 52 deletions(-) diff --git a/crates/bcr-ebill-api/src/external/email.rs b/crates/bcr-ebill-api/src/external/email.rs index f3e8d4a3..adf422c7 100644 --- a/crates/bcr-ebill-api/src/external/email.rs +++ b/crates/bcr-ebill-api/src/external/email.rs @@ -3,7 +3,7 @@ use bcr_common::core::{BillId, NodeId}; use bcr_ebill_core::application::ServiceTraitBounds; use bcr_ebill_core::protocol::Sha256Hash; use bcr_ebill_core::protocol::{ - Email, SchnorrSignature, SignedEmailIdentityData, SignedIdentityProof, + Email, SchnorrSignature, EmailIdentityProofData, SignedIdentityProof, crypto::Error as CryptoError, event::bill_events::BillEventType, }; use bitcoin::base58; @@ -66,7 +66,7 @@ pub trait EmailClientApi: ServiceTraitBounds { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> Result<(SignedIdentityProof, SignedEmailIdentityData)>; + ) -> Result<(SignedIdentityProof, EmailIdentityProofData)>; /// Send a bill notification email async fn send_bill_notification( &self, @@ -196,7 +196,7 @@ impl EmailClientApi for EmailClient { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> Result<(SignedIdentityProof, SignedEmailIdentityData)> { + ) -> Result<(SignedIdentityProof, EmailIdentityProofData)> { let pl = EmailConfirmPayload { node_id: node_id.to_owned(), company_node_id: company_node_id.to_owned(), @@ -242,7 +242,7 @@ impl EmailClientApi for EmailClient { signature, witness: res.mint_node_id, }; - let data: SignedEmailIdentityData = + let data: EmailIdentityProofData = borsh::from_slice(&decoded_mint_sig).map_err(Error::Borsh)?; Ok((proof, data)) @@ -352,7 +352,7 @@ pub struct EmailConfirmPayload { #[derive(Debug, Deserialize)] pub struct EmailConfirmResponse { - /// A borsh-encoded SignedEmailIdentityData + /// A borsh-encoded EmailIdentityProofData pub payload: String, /// The mint signature of the payload pub signature: Signature, diff --git a/crates/bcr-ebill-api/src/service/identity_service.rs b/crates/bcr-ebill-api/src/service/identity_service.rs index 41c9ef79..a17ea88d 100644 --- a/crates/bcr-ebill-api/src/service/identity_service.rs +++ b/crates/bcr-ebill-api/src/service/identity_service.rs @@ -23,7 +23,7 @@ use bcr_ebill_core::protocol::blockchain::identity::{ }; use bcr_ebill_core::protocol::crypto::{self, BcrKeys, DeriveKeypair}; use bcr_ebill_core::protocol::{City, ProtocolValidationError}; -use bcr_ebill_core::protocol::{Country, SignedEmailIdentityData, SignedIdentityProof}; +use bcr_ebill_core::protocol::{Country, EmailIdentityProofData, SignedIdentityProof}; use bcr_ebill_core::protocol::{Date, Field}; use bcr_ebill_core::protocol::{Email, blockchain}; use bcr_ebill_core::protocol::{File, OptionalPostalAddress, Validate, event::IdentityChainEvent}; @@ -134,7 +134,7 @@ pub trait IdentityServiceApi: ServiceTraitBounds { /// Get email confirmations for the identity async fn get_email_confirmations( &self, - ) -> Result>; + ) -> Result>; } /// The identity service is responsible for managing the local identity @@ -255,7 +255,7 @@ impl IdentityService { async fn create_identity_proof_block( &self, proof: SignedIdentityProof, - data: SignedEmailIdentityData, + data: EmailIdentityProofData, identity: &Identity, keys: &BcrKeys, chain: &mut IdentityBlockchain, @@ -282,7 +282,7 @@ impl IdentityService { email: &Option, t: &IdentityType, node_id: &NodeId, - ) -> Result> { + ) -> Result> { match t { IdentityType::Ident => { // Email has to be checked before @@ -317,7 +317,7 @@ impl IdentityService { // if mandatory email confirmations are disabled, create self-signed email confirmation let keys = self.get_keys().await?; - let self_signed_identity = SignedEmailIdentityData { + let self_signed_identity = EmailIdentityProofData { node_id: node_id.to_owned(), company_node_id: None, email: em.to_owned(), @@ -949,7 +949,7 @@ impl IdentityServiceApi for IdentityService { async fn get_email_confirmations( &self, - ) -> Result> { + ) -> Result> { let email_confirmations = self.store.get_email_confirmations().await?; Ok(email_confirmations) } diff --git a/crates/bcr-ebill-api/src/tests/mod.rs b/crates/bcr-ebill-api/src/tests/mod.rs index 998369cb..8f15cbb3 100644 --- a/crates/bcr-ebill-api/src/tests/mod.rs +++ b/crates/bcr-ebill-api/src/tests/mod.rs @@ -18,7 +18,7 @@ pub mod tests { use bcr_ebill_core::protocol::Sum; use bcr_ebill_core::protocol::Timestamp; use bcr_ebill_core::protocol::blockchain::bill::BitcreditBill; - use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; + use bcr_ebill_core::protocol::{EmailIdentityProofData, SignedIdentityProof}; use bcr_ebill_core::{ application::ServiceTraitBounds, application::bill::{BitcreditBillResult, PaymentState}, @@ -281,11 +281,11 @@ pub mod tests { async fn set_or_check_network(&self, configured_network: bitcoin::Network) -> Result<()>; async fn get_email_confirmations( &self, - ) -> Result>; + ) -> Result>; async fn set_email_confirmation( &self, proof: &SignedIdentityProof, - data: &SignedEmailIdentityData, + data: &EmailIdentityProofData, ) -> Result<()>; } } @@ -595,8 +595,8 @@ pub mod tests { .unwrap() } - pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { - let data = SignedEmailIdentityData { + pub fn signed_identity_proof_test() -> (SignedIdentityProof, EmailIdentityProofData) { + let data = EmailIdentityProofData { node_id: node_id_test(), company_node_id: None, email: Email::new("test@example.com").unwrap(), diff --git a/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs b/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs index fbf80db3..99fc3451 100644 --- a/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs +++ b/crates/bcr-ebill-core/src/protocol/base/identity_proof.rs @@ -11,14 +11,14 @@ use crate::protocol::{ /// The signature and witness of an identity proof #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)] pub struct SignedIdentityProof { - /// The signature of the sha256 hashed borsh-payload (SignedEmailIdentityData) by the mint + /// The signature of the sha256 hashed borsh-payload (EmailIdentityProofData) by the mint pub signature: SchnorrSignature, /// The mint (signer) node id pub witness: NodeId, } impl SignedIdentityProof { - pub fn verify(&self, data: &SignedEmailIdentityData) -> Result { + pub fn verify(&self, data: &EmailIdentityProofData) -> Result { let serialized = borsh::to_vec(&data)?; let hash = Sha256Hash::from_bytes(&serialized); let res = self.signature.verify(&hash, &self.witness.pub_key())?; @@ -28,7 +28,7 @@ impl SignedIdentityProof { /// Mapping from (node_id/option) => email, to be signed by a witness (mint) #[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)] -pub struct SignedEmailIdentityData { +pub struct EmailIdentityProofData { /// Identity node id pub node_id: NodeId, /// Optional company node id @@ -39,7 +39,7 @@ pub struct SignedEmailIdentityData { pub created_at: Timestamp, } -impl SignedEmailIdentityData { +impl EmailIdentityProofData { pub fn sign( &self, witness: &NodeId, @@ -68,7 +68,7 @@ mod tests { #[test] fn test_sign_verify() { - let data = SignedEmailIdentityData { + let data = EmailIdentityProofData { node_id: node_id_test(), company_node_id: None, email: Email::new("test@example.com").unwrap(), diff --git a/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs b/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs index 15fb27c3..b900a972 100644 --- a/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/blockchain/company/mod.rs @@ -11,7 +11,7 @@ use crate::protocol::Name; use crate::protocol::SchnorrSignature; use crate::protocol::Sha256Hash; use crate::protocol::Timestamp; -use crate::protocol::base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; +use crate::protocol::base::identity_proof::{EmailIdentityProofData, SignedIdentityProof}; use crate::protocol::blockchain::{Error, borsh_to_json_value}; use crate::protocol::crypto::{self, BcrKeys}; use crate::protocol::{File, OptionalPostalAddress, PostalAddress}; @@ -188,7 +188,7 @@ pub struct CompanyRemoveSignatoryBlockData { #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct CompanyIdentityProofBlockData { pub proof: SignedIdentityProof, - pub data: SignedEmailIdentityData, + pub data: EmailIdentityProofData, } #[derive(Debug)] diff --git a/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs b/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs index 53da21bc..79c709f3 100644 --- a/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/blockchain/identity/mod.rs @@ -11,7 +11,7 @@ use crate::protocol::Name; use crate::protocol::SchnorrSignature; use crate::protocol::Sha256Hash; use crate::protocol::Timestamp; -use crate::protocol::base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; +use crate::protocol::base::identity_proof::{EmailIdentityProofData, SignedIdentityProof}; use crate::protocol::blockchain::{Error, borsh_to_json_value}; use crate::protocol::crypto::{self, BcrKeys}; use crate::protocol::{Field, ProtocolValidationError, Validate}; @@ -264,7 +264,7 @@ pub struct IdentityRemoveSignatoryBlockData { #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct IdentityProofBlockData { pub proof: SignedIdentityProof, - pub data: SignedEmailIdentityData, + pub data: EmailIdentityProofData, } #[derive(Debug)] diff --git a/crates/bcr-ebill-core/src/protocol/mod.rs b/crates/bcr-ebill-core/src/protocol/mod.rs index 4e7da90a..b2e3a565 100644 --- a/crates/bcr-ebill-core/src/protocol/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/mod.rs @@ -25,7 +25,7 @@ pub use base::date::Date; pub use base::email::Email; pub use base::hash::Sha256Hash; pub use base::identification::Identification; -pub use base::identity_proof::{SignedEmailIdentityData, SignedIdentityProof}; +pub use base::identity_proof::{EmailIdentityProofData, SignedIdentityProof}; pub use base::name::Name; pub use base::signature::SchnorrSignature; pub use base::sum::Currency; diff --git a/crates/bcr-ebill-core/src/protocol/tests/mod.rs b/crates/bcr-ebill-core/src/protocol/tests/mod.rs index c4cc8358..c6789637 100644 --- a/crates/bcr-ebill-core/src/protocol/tests/mod.rs +++ b/crates/bcr-ebill-core/src/protocol/tests/mod.rs @@ -11,7 +11,7 @@ pub mod tests { use crate::protocol::Sum; use crate::protocol::Timestamp; use crate::protocol::Zip; - use crate::protocol::base::identity_proof::SignedEmailIdentityData; + use crate::protocol::base::identity_proof::EmailIdentityProofData; use crate::protocol::base::identity_proof::SignedIdentityProof; use crate::protocol::blockchain::bill::BitcreditBill; use crate::protocol::blockchain::bill::ContactType; @@ -191,8 +191,8 @@ pub mod tests { .unwrap() } - pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { - let data = SignedEmailIdentityData { + pub fn signed_identity_proof_test() -> (SignedIdentityProof, EmailIdentityProofData) { + let data = EmailIdentityProofData { node_id: node_id_test(), company_node_id: None, email: Email::new("test@example.com").unwrap(), diff --git a/crates/bcr-ebill-persistence/src/db/identity.rs b/crates/bcr-ebill-persistence/src/db/identity.rs index c3145b71..1d438de2 100644 --- a/crates/bcr-ebill-persistence/src/db/identity.rs +++ b/crates/bcr-ebill-persistence/src/db/identity.rs @@ -9,7 +9,7 @@ use bcr_ebill_core::{ }, protocol::{ City, Country, Date, Email, Identification, Name, SchnorrSignature, SecretKey, - SignedEmailIdentityData, SignedIdentityProof, Timestamp, + EmailIdentityProofData, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; @@ -185,7 +185,7 @@ impl IdentityStoreApi for SurrealIdentityStore { async fn get_email_confirmations( &self, - ) -> Result> { + ) -> Result> { let result: Vec = self.db.select_all(Self::EMAIL_CONFIRMATION_TABLE).await?; Ok(result @@ -197,7 +197,7 @@ impl IdentityStoreApi for SurrealIdentityStore { async fn set_email_confirmation( &self, proof: &SignedIdentityProof, - data: &SignedEmailIdentityData, + data: &EmailIdentityProofData, ) -> Result<()> { let keys = self.get_key_pair().await?; if keys.pub_key() != data.node_id.pub_key() { @@ -227,8 +227,8 @@ pub struct IdentityEmailConfirmationDb { pub created_at: Timestamp, } -impl From<(SignedIdentityProof, SignedEmailIdentityData)> for IdentityEmailConfirmationDb { - fn from((proof, data): (SignedIdentityProof, SignedEmailIdentityData)) -> Self { +impl From<(SignedIdentityProof, EmailIdentityProofData)> for IdentityEmailConfirmationDb { + fn from((proof, data): (SignedIdentityProof, EmailIdentityProofData)) -> Self { IdentityEmailConfirmationDb { signature: proof.signature, witness: proof.witness, @@ -240,14 +240,14 @@ impl From<(SignedIdentityProof, SignedEmailIdentityData)> for IdentityEmailConfi } } -impl From for (SignedIdentityProof, SignedEmailIdentityData) { +impl From for (SignedIdentityProof, EmailIdentityProofData) { fn from(value: IdentityEmailConfirmationDb) -> Self { ( SignedIdentityProof { signature: value.signature, witness: value.witness, }, - SignedEmailIdentityData { + EmailIdentityProofData { node_id: value.node_id, company_node_id: value.company_node_id, email: value.email, diff --git a/crates/bcr-ebill-persistence/src/identity.rs b/crates/bcr-ebill-persistence/src/identity.rs index d8d14c76..6852b284 100644 --- a/crates/bcr-ebill-persistence/src/identity.rs +++ b/crates/bcr-ebill-persistence/src/identity.rs @@ -7,7 +7,7 @@ use bcr_ebill_core::{ identity::{ActiveIdentityState, Identity, IdentityWithAll}, }, protocol::{ - SignedEmailIdentityData, SignedIdentityProof, + EmailIdentityProofData, SignedIdentityProof, blockchain::identity::{IdentityBlock, IdentityBlockchain}, crypto::BcrKeys, }, @@ -42,12 +42,12 @@ pub trait IdentityStoreApi: ServiceTraitBounds { /// Gets the email confirmation state for this identity async fn get_email_confirmations( &self, - ) -> Result>; + ) -> Result>; /// Sets the email confirmation state for this identity async fn set_email_confirmation( &self, proof: &SignedIdentityProof, - data: &SignedEmailIdentityData, + data: &EmailIdentityProofData, ) -> Result<()>; } diff --git a/crates/bcr-ebill-persistence/src/tests/mod.rs b/crates/bcr-ebill-persistence/src/tests/mod.rs index 6f546aee..359f5b07 100644 --- a/crates/bcr-ebill-persistence/src/tests/mod.rs +++ b/crates/bcr-ebill-persistence/src/tests/mod.rs @@ -15,7 +15,7 @@ pub mod tests { }, protocol::{ Address, City, Country, Date, Email, Name, OptionalPostalAddress, PostalAddress, - PublicKey, SecretKey, SignedEmailIdentityData, SignedIdentityProof, Sum, Timestamp, + PublicKey, SecretKey, EmailIdentityProofData, SignedIdentityProof, Sum, Timestamp, blockchain::{ bill::{ BillHistory, BitcreditBill, ContactType, @@ -204,8 +204,8 @@ pub mod tests { .unwrap() } - pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { - let data = SignedEmailIdentityData { + pub fn signed_identity_proof_test() -> (SignedIdentityProof, EmailIdentityProofData) { + let data = EmailIdentityProofData { node_id: node_id_test(), company_node_id: None, email: Email::new("test@example.com").unwrap(), diff --git a/crates/bcr-ebill-transport/src/handler/mod.rs b/crates/bcr-ebill-transport/src/handler/mod.rs index 703a68ff..9910b56d 100644 --- a/crates/bcr-ebill-transport/src/handler/mod.rs +++ b/crates/bcr-ebill-transport/src/handler/mod.rs @@ -358,7 +358,7 @@ mod test_utils { crypto::BcrKeys, event::ActionType, }; - use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; + use bcr_ebill_core::protocol::{EmailIdentityProofData, SignedIdentityProof}; use bcr_ebill_persistence::{ NostrChainEventStoreApi, NotificationStoreApi, Result, bill::{BillChainStoreApi, BillStoreApi}, @@ -533,11 +533,11 @@ mod test_utils { async fn set_or_check_network(&self, configured_network: bitcoin::Network) -> Result<()>; async fn get_email_confirmations( &self, - ) -> Result>; + ) -> Result>; async fn set_email_confirmation( &self, proof: &SignedIdentityProof, - data: &SignedEmailIdentityData, + data: &EmailIdentityProofData, ) -> Result<()>; } } @@ -790,8 +790,8 @@ mod test_utils { .unwrap() } - pub fn signed_identity_proof_test() -> (SignedIdentityProof, SignedEmailIdentityData) { - let data = SignedEmailIdentityData { + pub fn signed_identity_proof_test() -> (SignedIdentityProof, EmailIdentityProofData) { + let data = EmailIdentityProofData { node_id: node_id_test(), company_node_id: None, email: Email::new("test@example.com").unwrap(), diff --git a/crates/bcr-ebill-transport/src/test_utils.rs b/crates/bcr-ebill-transport/src/test_utils.rs index 24407980..b2677c18 100644 --- a/crates/bcr-ebill-transport/src/test_utils.rs +++ b/crates/bcr-ebill-transport/src/test_utils.rs @@ -19,7 +19,7 @@ use bcr_ebill_core::protocol::blockchain::bill::BillBlockchain; use bcr_ebill_core::protocol::blockchain::bill::block::BillIssueBlockData; use bcr_ebill_core::protocol::crypto::BcrKeys; use bcr_ebill_core::protocol::event::{EventEnvelope, EventType}; -use bcr_ebill_core::protocol::{SignedEmailIdentityData, SignedIdentityProof}; +use bcr_ebill_core::protocol::{EmailIdentityProofData, SignedIdentityProof}; use bcr_ebill_core::{ application::ServiceTraitBounds, application::contact::Contact, @@ -774,7 +774,7 @@ mockall::mock! { company_node_id: &Option, confirmation_code: &str, private_key: &SecretKey, - ) -> bcr_ebill_api::external::email::Result<(SignedIdentityProof, SignedEmailIdentityData)>; + ) -> bcr_ebill_api::external::email::Result<(SignedIdentityProof, EmailIdentityProofData)>; async fn send_bill_notification( &self, mint_url: &url::Url, diff --git a/crates/bcr-ebill-wasm/src/data/identity.rs b/crates/bcr-ebill-wasm/src/data/identity.rs index 5bd3b0de..17d95a94 100644 --- a/crates/bcr-ebill-wasm/src/data/identity.rs +++ b/crates/bcr-ebill-wasm/src/data/identity.rs @@ -8,7 +8,7 @@ use bcr_ebill_core::{ }, protocol::{ City, Country, Date, Email, Identification, Name, PublicKey, SchnorrSignature, - SignedEmailIdentityData, SignedIdentityProof, Timestamp, + EmailIdentityProofData, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; @@ -208,8 +208,8 @@ pub struct IdentityEmailConfirmationWeb { pub created_at: Timestamp, } -impl From<(SignedIdentityProof, SignedEmailIdentityData)> for IdentityEmailConfirmationWeb { - fn from((proof, data): (SignedIdentityProof, SignedEmailIdentityData)) -> Self { +impl From<(SignedIdentityProof, EmailIdentityProofData)> for IdentityEmailConfirmationWeb { + fn from((proof, data): (SignedIdentityProof, EmailIdentityProofData)) -> Self { Self { signature: proof.signature, witness: proof.witness, From aac2746163103ba2aa370b8161ac1c03b8b550d9 Mon Sep 17 00:00:00 2001 From: Mario Zupan Date: Thu, 20 Nov 2025 10:00:53 +0100 Subject: [PATCH 3/3] fix formatting --- crates/bcr-ebill-api/src/external/email.rs | 2 +- crates/bcr-ebill-persistence/src/db/identity.rs | 5 ++--- crates/bcr-ebill-persistence/src/tests/mod.rs | 5 +++-- crates/bcr-ebill-wasm/src/data/identity.rs | 5 ++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/bcr-ebill-api/src/external/email.rs b/crates/bcr-ebill-api/src/external/email.rs index adf422c7..f9764e2d 100644 --- a/crates/bcr-ebill-api/src/external/email.rs +++ b/crates/bcr-ebill-api/src/external/email.rs @@ -3,7 +3,7 @@ use bcr_common::core::{BillId, NodeId}; use bcr_ebill_core::application::ServiceTraitBounds; use bcr_ebill_core::protocol::Sha256Hash; use bcr_ebill_core::protocol::{ - Email, SchnorrSignature, EmailIdentityProofData, SignedIdentityProof, + Email, EmailIdentityProofData, SchnorrSignature, SignedIdentityProof, crypto::Error as CryptoError, event::bill_events::BillEventType, }; use bitcoin::base58; diff --git a/crates/bcr-ebill-persistence/src/db/identity.rs b/crates/bcr-ebill-persistence/src/db/identity.rs index 1d438de2..1e9b15ca 100644 --- a/crates/bcr-ebill-persistence/src/db/identity.rs +++ b/crates/bcr-ebill-persistence/src/db/identity.rs @@ -8,9 +8,8 @@ use bcr_ebill_core::{ identity::{ActiveIdentityState, Identity, IdentityWithAll}, }, protocol::{ - City, Country, Date, Email, Identification, Name, SchnorrSignature, SecretKey, - EmailIdentityProofData, SignedIdentityProof, Timestamp, - blockchain::identity::IdentityType, + City, Country, Date, Email, EmailIdentityProofData, Identification, Name, SchnorrSignature, + SecretKey, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; use serde::{Deserialize, Serialize}; diff --git a/crates/bcr-ebill-persistence/src/tests/mod.rs b/crates/bcr-ebill-persistence/src/tests/mod.rs index 359f5b07..ecd63ea9 100644 --- a/crates/bcr-ebill-persistence/src/tests/mod.rs +++ b/crates/bcr-ebill-persistence/src/tests/mod.rs @@ -14,8 +14,9 @@ pub mod tests { identity::Identity, }, protocol::{ - Address, City, Country, Date, Email, Name, OptionalPostalAddress, PostalAddress, - PublicKey, SecretKey, EmailIdentityProofData, SignedIdentityProof, Sum, Timestamp, + Address, City, Country, Date, Email, EmailIdentityProofData, Name, + OptionalPostalAddress, PostalAddress, PublicKey, SecretKey, SignedIdentityProof, Sum, + Timestamp, blockchain::{ bill::{ BillHistory, BitcreditBill, ContactType, diff --git a/crates/bcr-ebill-wasm/src/data/identity.rs b/crates/bcr-ebill-wasm/src/data/identity.rs index 17d95a94..35e87731 100644 --- a/crates/bcr-ebill-wasm/src/data/identity.rs +++ b/crates/bcr-ebill-wasm/src/data/identity.rs @@ -7,9 +7,8 @@ use bcr_ebill_core::{ nostr_contact::NostrPublicKey, }, protocol::{ - City, Country, Date, Email, Identification, Name, PublicKey, SchnorrSignature, - EmailIdentityProofData, SignedIdentityProof, Timestamp, - blockchain::identity::IdentityType, + City, Country, Date, Email, EmailIdentityProofData, Identification, Name, PublicKey, + SchnorrSignature, SignedIdentityProof, Timestamp, blockchain::identity::IdentityType, }, }; use serde::{Deserialize, Serialize};