Skip to content

Commit c45fd95

Browse files
authored
Adapt signatories, Get Invites, Accept and Reject Invites, Local (#751)
1 parent d9e05e2 commit c45fd95

File tree

22 files changed

+2591
-674
lines changed

22 files changed

+2591
-674
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
* Split up `update_identity` and `update_email` for identity and create identity proof block on email update
2626
* Change flow for company creation to first call `create_company_keys` to get a key pair and node id, then confirm email of creator, then create company
2727
* Add `email` to signatory and use a data structure for signatories (breaking API and DB change)
28+
* Adapt signatory handling for companies
29+
* API for inviting signatories
30+
* API to accept/reject company invites
31+
* Restructured company persistence - `company` table is now a cache, calculated from the chain (similar to bills)
32+
* Added possibility to locally hide past invites
2833

2934
# 0.4.12
3035

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

Lines changed: 1049 additions & 354 deletions
Large diffs are not rendered by default.

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub mod tests {
2222
use bcr_ebill_core::{
2323
application::ServiceTraitBounds,
2424
application::bill::{BitcreditBillResult, PaymentState},
25-
application::company::Company,
25+
application::company::{Company, LocalSignatoryOverride, LocalSignatoryOverrideStatus},
2626
application::contact::Contact,
2727
application::identity::{ActiveIdentityState, Identity, IdentityWithAll},
2828
application::nostr_contact::{HandshakeStatus, NostrContact, NostrPublicKey, TrustLevel},
@@ -254,6 +254,18 @@ pub mod tests {
254254
proof: &SignedIdentityProof,
255255
data: &EmailIdentityProofData,
256256
) -> Result<()>;
257+
async fn get_local_signatory_overrides(
258+
&self,
259+
id: &NodeId,
260+
) -> Result<Vec<LocalSignatoryOverride>>;
261+
async fn set_local_signatory_override(
262+
&self,
263+
id: &NodeId,
264+
signatory: &NodeId,
265+
status: LocalSignatoryOverrideStatus,
266+
) -> Result<()>;
267+
async fn delete_local_signatory_override(&self, id: &NodeId, signatory: &NodeId) -> Result<()>;
268+
async fn get_active_company_invites(&self) -> Result<HashMap<NodeId, (Company, BcrKeys)>>;
257269
}
258270
}
259271

Lines changed: 188 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
use crate::protocol::{
2-
City, Country, Date, Email, File, Identification, Name, PostalAddress,
3-
blockchain::company::{
4-
CompanyBlockPayload, CompanyCreateBlockData, CompanySignatoryBlockData, SignatoryType,
5-
},
2+
City, Country, Date, Email, EmailIdentityProofData, File, Identification, Name, PostalAddress,
3+
SignedIdentityProof, Timestamp,
4+
blockchain::company::{CompanyBlockPayload, CompanyCreateBlockData, SignatoryType},
65
};
76
use bcr_common::core::NodeId;
87

@@ -20,57 +19,71 @@ pub struct Company {
2019
pub registration_date: Option<Date>,
2120
pub proof_of_registration_file: Option<File>,
2221
pub logo_file: Option<File>,
22+
pub creation_time: Timestamp,
2323
pub signatories: Vec<CompanySignatory>,
24-
pub active: bool,
24+
pub status: CompanyStatus,
2525
}
2626

27-
impl From<Company> for CompanyCreateBlockData {
28-
fn from(value: Company) -> Self {
29-
Self {
30-
id: value.id,
31-
name: value.name,
32-
country_of_registration: value.country_of_registration,
33-
city_of_registration: value.city_of_registration,
34-
postal_address: value.postal_address,
35-
email: value.email,
36-
registration_number: value.registration_number,
37-
registration_date: value.registration_date,
38-
proof_of_registration_file: value.proof_of_registration_file,
39-
logo_file: value.logo_file,
40-
signatories: value.signatories.into_iter().map(|s| s.into()).collect(),
41-
}
27+
impl Company {
28+
// checks if the given node id is an authorized signer for this company
29+
pub fn is_authorized_signer(&self, node_id: &NodeId) -> bool {
30+
self.signatories.iter().any(|s| {
31+
&s.node_id == node_id
32+
&& matches!(
33+
s.status,
34+
CompanySignatoryStatus::InviteAcceptedIdentityProven { .. }
35+
)
36+
})
4237
}
4338
}
4439

40+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Copy)]
41+
pub enum CompanyStatus {
42+
Invited,
43+
Active,
44+
None,
45+
}
46+
4547
#[derive(Debug, Serialize, Deserialize, Clone)]
4648
pub struct CompanySignatory {
49+
pub t: SignatoryType,
4750
pub node_id: NodeId,
48-
pub email: Email,
51+
pub status: CompanySignatoryStatus,
4952
}
5053

51-
impl From<CompanySignatory> for CompanySignatoryBlockData {
52-
fn from(value: CompanySignatory) -> Self {
53-
Self {
54-
t: SignatoryType::Solo,
55-
node_id: value.node_id,
56-
email: value.email,
57-
}
58-
}
59-
}
60-
61-
impl From<CompanySignatoryBlockData> for CompanySignatory {
62-
fn from(value: CompanySignatoryBlockData) -> Self {
63-
Self {
64-
node_id: value.node_id,
65-
email: value.email,
66-
}
67-
}
54+
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
55+
pub enum CompanySignatoryStatus {
56+
Invited {
57+
ts: Timestamp,
58+
inviter: NodeId,
59+
},
60+
InviteAccepted {
61+
ts: Timestamp,
62+
},
63+
InviteRejected {
64+
ts: Timestamp,
65+
},
66+
InviteAcceptedIdentityProven {
67+
ts: Timestamp,
68+
data: EmailIdentityProofData,
69+
proof: SignedIdentityProof,
70+
},
71+
Removed {
72+
ts: Timestamp,
73+
remover: NodeId,
74+
},
6875
}
6976

7077
impl Company {
7178
/// Creates a new company from a block data payload
7279
pub fn from_block_data(data: CompanyCreateBlockData, our_node_id: &NodeId) -> Self {
73-
let active = data.signatories.iter().any(|s| &s.node_id == our_node_id);
80+
// if we're the creator, the company is active, otherwise we have no status in the company at this point
81+
let status = if our_node_id == &data.creator {
82+
CompanyStatus::Active
83+
} else {
84+
CompanyStatus::None
85+
};
86+
7487
Self {
7588
id: data.id,
7689
name: data.name,
@@ -82,12 +95,24 @@ impl Company {
8295
registration_date: data.registration_date,
8396
proof_of_registration_file: data.proof_of_registration_file,
8497
logo_file: data.logo_file,
85-
signatories: data.signatories.into_iter().map(|s| s.into()).collect(),
86-
active,
98+
creation_time: data.creation_time,
99+
signatories: vec![CompanySignatory {
100+
t: SignatoryType::Solo,
101+
node_id: data.creator,
102+
status: CompanySignatoryStatus::InviteAccepted {
103+
ts: data.creation_time,
104+
},
105+
}],
106+
status,
87107
}
88108
}
89109
/// Applies data from a block to this company.
90-
pub fn apply_block_data(&mut self, data: &CompanyBlockPayload, our_node_id: &NodeId) {
110+
pub fn apply_block_data(
111+
&mut self,
112+
data: &CompanyBlockPayload,
113+
our_node_id: &NodeId,
114+
timestamp: Timestamp,
115+
) {
91116
match data {
92117
CompanyBlockPayload::Update(payload) => {
93118
self.name = payload.name.to_owned().unwrap_or(self.name.to_owned());
@@ -134,32 +159,137 @@ impl Company {
134159
.to_owned()
135160
.or(self.proof_of_registration_file.to_owned());
136161
}
137-
CompanyBlockPayload::AddSignatory(payload) => {
138-
if !self
162+
CompanyBlockPayload::InviteSignatory(payload) => {
163+
// if we're invited, set our status to invited, if we're not already invited or accepted
164+
if CompanyStatus::None == self.status && our_node_id == &payload.invitee {
165+
self.status = CompanyStatus::Invited;
166+
}
167+
168+
// update signatory data
169+
if let Some(signatory) = self
139170
.signatories
140-
.iter()
141-
.any(|s| s.node_id == payload.signatory)
171+
.iter_mut()
172+
.find(|s| s.node_id == payload.invitee)
142173
{
143-
self.signatories.push(
144-
CompanySignatoryBlockData {
145-
t: SignatoryType::Solo,
146-
node_id: payload.signatory.to_owned(),
147-
email: payload.signatory_email.to_owned(),
174+
match signatory.status {
175+
CompanySignatoryStatus::InviteRejected { .. }
176+
| CompanySignatoryStatus::Removed { .. } => {
177+
// invite again
178+
signatory.status = CompanySignatoryStatus::Invited {
179+
ts: timestamp,
180+
inviter: payload.inviter.clone(),
181+
}
148182
}
149-
.into(),
150-
);
151-
if &payload.signatory == our_node_id {
152-
self.active = true;
183+
_ => (), // already invited / accepted - ignore,
153184
}
185+
} else {
186+
// if the signatory wasn't in the list before - add as invited
187+
self.signatories.push(CompanySignatory {
188+
t: SignatoryType::Solo,
189+
node_id: payload.invitee.to_owned(),
190+
status: CompanySignatoryStatus::Invited {
191+
ts: timestamp,
192+
inviter: payload.inviter.clone(),
193+
},
194+
});
195+
}
196+
}
197+
CompanyBlockPayload::SignatoryAcceptInvite(payload) => {
198+
// if we're invited, set our status to active
199+
if CompanyStatus::Invited == self.status && our_node_id == &payload.accepter {
200+
self.status = CompanyStatus::Active;
201+
}
202+
203+
// update signatory data
204+
if let Some(signatory) = self
205+
.signatories
206+
.iter_mut()
207+
.find(|s| s.node_id == payload.accepter)
208+
{
209+
signatory.status = CompanySignatoryStatus::InviteAccepted { ts: timestamp };
210+
}
211+
}
212+
CompanyBlockPayload::SignatoryRejectInvite(payload) => {
213+
// if we're invited, set our status to None
214+
if CompanyStatus::Invited == self.status && our_node_id == &payload.rejecter {
215+
self.status = CompanyStatus::None;
216+
}
217+
218+
// update signatory data
219+
if let Some(signatory) = self
220+
.signatories
221+
.iter_mut()
222+
.find(|s| s.node_id == payload.rejecter)
223+
{
224+
signatory.status = CompanySignatoryStatus::InviteRejected { ts: timestamp };
154225
}
155226
}
156227
CompanyBlockPayload::RemoveSignatory(payload) => {
157-
self.signatories.retain(|i| i.node_id != payload.signatory);
158-
if &payload.signatory == our_node_id {
159-
self.active = false;
228+
// if we're removed, set our status to none
229+
if our_node_id == &payload.removee {
230+
self.status = CompanyStatus::None;
231+
}
232+
233+
// update signatory data
234+
if let Some(signatory) = self
235+
.signatories
236+
.iter_mut()
237+
.find(|s| s.node_id == payload.removee)
238+
{
239+
signatory.status = CompanySignatoryStatus::Removed {
240+
ts: timestamp,
241+
remover: payload.remover.clone(),
242+
};
243+
}
244+
}
245+
CompanyBlockPayload::IdentityProof(payload) => {
246+
if let Some(signatory) = self
247+
.signatories
248+
.iter_mut()
249+
.find(|s| s.node_id == payload.data.node_id)
250+
{
251+
// Part of adding a signatory via Accept, or Create
252+
if payload.reference_block.is_some() {
253+
match signatory.status {
254+
CompanySignatoryStatus::InviteAccepted { .. }
255+
| CompanySignatoryStatus::InviteAcceptedIdentityProven { .. } => {
256+
signatory.status =
257+
CompanySignatoryStatus::InviteAcceptedIdentityProven {
258+
ts: timestamp,
259+
data: payload.data.clone(),
260+
proof: payload.proof.clone(),
261+
}
262+
}
263+
_ => (), // invalid / irrelevant cases
264+
}
265+
} else {
266+
// only update data
267+
if let CompanySignatoryStatus::InviteAcceptedIdentityProven { ts, .. } =
268+
signatory.status
269+
{
270+
signatory.status =
271+
CompanySignatoryStatus::InviteAcceptedIdentityProven {
272+
ts,
273+
data: payload.data.clone(),
274+
proof: payload.proof.clone(),
275+
}
276+
}
277+
}
160278
}
161279
}
162280
_ => {}
163281
}
164282
}
165283
}
284+
285+
#[derive(Debug, Clone)]
286+
pub struct LocalSignatoryOverride {
287+
pub company_id: NodeId,
288+
pub node_id: NodeId,
289+
pub status: LocalSignatoryOverrideStatus,
290+
}
291+
292+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
293+
pub enum LocalSignatoryOverrideStatus {
294+
Hidden, // hide a company signatory locally
295+
}

crates/bcr-ebill-core/src/protocol/base/identity_proof.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::protocol::{
99
};
1010

1111
/// The signature and witness of an identity proof
12-
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)]
12+
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq)]
1313
pub struct SignedIdentityProof {
1414
/// The signature of the sha256 hashed borsh-payload (EmailIdentityProofData) by the mint
1515
pub signature: SchnorrSignature,
@@ -27,7 +27,7 @@ impl SignedIdentityProof {
2727
}
2828

2929
/// Mapping from (node_id/option<company_node_id>) => email, to be signed by a witness (mint)
30-
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq)]
30+
#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, Serialize, Deserialize, PartialEq, Eq)]
3131
pub struct EmailIdentityProofData {
3232
/// Identity node id
3333
pub node_id: NodeId,

0 commit comments

Comments
 (0)