Skip to main content

polyester/codecs/decode/
sub_accounts.rs

1//! Sub-account decoders.
2
3use crate::codecs::decode::enums::enum_proto_name;
4use crate::codecs::scalars::format_uint64_id;
5use crate::models::{
6    CreateSubaccountResult, GetSubaccountResult, SubAccount, SubAccountActivityEvent,
7    SubAccountActivityList, SubAccountInvite, SubAccountInvitesList, SubAccountMember,
8    SubAccountMembersList, SubAccountsList,
9};
10use crate::proto::auth::v1::{
11    CreateSubaccountResponse, GetSubaccountResponse, InviteSubaccountMemberResponse,
12    ListSubaccountEventsResponse, ListSubaccountInvitesResponse, ListSubaccountMembersResponse,
13    ListSubaccountsResponse, RespondSubaccountInviteResponse, Subaccount, SubaccountInvite,
14    SubaccountMemberView,
15};
16
17fn timestamp_ms(ts: Option<&buffa_types::google::protobuf::Timestamp>) -> i64 {
18    match ts {
19        Some(t) => t.seconds.saturating_mul(1000) + (t.nanos as i64) / 1_000_000,
20        None => 0,
21    }
22}
23
24fn activity_label<T: buffa::Enumeration>(value: &buffa::EnumValue<T>, prefix: &str) -> String {
25    let label = enum_proto_name(value);
26    match label.strip_prefix(prefix) {
27        Some(known) => known.to_ascii_lowercase(),
28        None => label,
29    }
30}
31
32pub fn subaccount_from_proto(msg: &Subaccount) -> SubAccount {
33    SubAccount {
34        subaccount_id: format_uint64_id(msg.id),
35        label: msg.label.clone(),
36        smart_account_address: msg.smart_account_address.clone(),
37        status: msg.status.clone(),
38        updated_at: msg.updated_at.as_option().cloned(),
39        revision: msg.revision,
40    }
41}
42
43pub fn subaccounts_list_from_proto(msg: &ListSubaccountsResponse) -> SubAccountsList {
44    SubAccountsList {
45        subaccounts: msg.subaccounts.iter().map(subaccount_from_proto).collect(),
46    }
47}
48
49pub fn get_subaccount_from_proto(msg: &GetSubaccountResponse) -> GetSubaccountResult {
50    GetSubaccountResult {
51        subaccount: msg.subaccount.as_option().map(subaccount_from_proto),
52    }
53}
54
55pub fn create_subaccount_from_proto(msg: &CreateSubaccountResponse) -> CreateSubaccountResult {
56    CreateSubaccountResult {
57        subaccount_id: format_uint64_id(msg.subaccount_id),
58        revision: msg.revision,
59    }
60}
61
62fn member_from_proto(msg: &SubaccountMemberView) -> SubAccountMember {
63    SubAccountMember {
64        grantee_account_id: format_uint64_id(msg.account_id),
65        role: enum_proto_name(&msg.role),
66    }
67}
68
69pub fn subaccount_members_list_from_proto(
70    msg: &ListSubaccountMembersResponse,
71) -> SubAccountMembersList {
72    SubAccountMembersList {
73        members: msg.members.iter().map(member_from_proto).collect(),
74    }
75}
76
77fn invite_from_proto(msg: &SubaccountInvite) -> SubAccountInvite {
78    SubAccountInvite {
79        invite_id: format_uint64_id(msg.id),
80        grantee_account_id: format_uint64_id(msg.grantee_account_id),
81        role: enum_proto_name(&msg.role),
82        status: enum_proto_name(&msg.status),
83    }
84}
85
86pub fn invite_subaccount_member_from_proto(
87    msg: &InviteSubaccountMemberResponse,
88) -> Option<SubAccountInvite> {
89    msg.invite.as_option().map(invite_from_proto)
90}
91
92pub fn respond_subaccount_invite_from_proto(
93    msg: &RespondSubaccountInviteResponse,
94) -> Option<SubAccountInvite> {
95    msg.invite.as_option().map(invite_from_proto)
96}
97
98pub fn subaccount_invites_list_from_proto(
99    msg: &ListSubaccountInvitesResponse,
100) -> SubAccountInvitesList {
101    SubAccountInvitesList {
102        invites: msg.invites.iter().map(invite_from_proto).collect(),
103    }
104}
105
106pub fn subaccount_activity_list_from_proto(
107    msg: &ListSubaccountEventsResponse,
108) -> SubAccountActivityList {
109    SubAccountActivityList {
110        events: msg
111            .events
112            .iter()
113            .map(|e| SubAccountActivityEvent {
114                event_type: format!(
115                    "{}:{}",
116                    activity_label(&e.entity_kind, "ACTIVITY_ENTITY_"),
117                    activity_label(&e.event_action, "ACTIVITY_ACTION_")
118                ),
119                ts_ms: timestamp_ms(e.created_at.as_option()),
120            })
121            .collect(),
122        next_page_token: msg.next_page_token.clone(),
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::proto::auth::v1::{
130        ActivityEntityKind, ActivityEvent, ActivityEventAction, SubaccountRole,
131    };
132    use buffa_types::google::protobuf::Timestamp;
133
134    #[test]
135    fn subaccounts_list_maps_fields() {
136        let msg = ListSubaccountsResponse {
137            subaccounts: vec![Subaccount {
138                id: 12,
139                label: "trading".into(),
140                smart_account_address: "0xabc".into(),
141                status: "active".into(),
142                updated_at: Timestamp {
143                    seconds: 3,
144                    nanos: 123_456_000,
145                    ..Default::default()
146                }
147                .into(),
148                revision: 4,
149                ..Default::default()
150            }],
151            ..Default::default()
152        };
153        let result = subaccounts_list_from_proto(&msg);
154        assert_eq!(result.subaccounts.len(), 1);
155        assert_eq!(result.subaccounts[0].subaccount_id, format_uint64_id(12));
156        assert_eq!(result.subaccounts[0].smart_account_address, "0xabc");
157        assert_eq!(
158            result.subaccounts[0].updated_at.as_ref().unwrap().nanos,
159            123_456_000
160        );
161        assert_eq!(result.subaccounts[0].revision, 4);
162    }
163
164    #[test]
165    fn members_list_maps_grantee_and_role() {
166        let msg = ListSubaccountMembersResponse {
167            members: vec![SubaccountMemberView {
168                account_id: 99,
169                role: SubaccountRole::TRADER.into(),
170                ..Default::default()
171            }],
172            ..Default::default()
173        };
174        let result = subaccount_members_list_from_proto(&msg);
175        assert_eq!(result.members.len(), 1);
176        assert_eq!(result.members[0].grantee_account_id, format_uint64_id(99));
177        assert!(!result.members[0].role.is_empty());
178    }
179
180    #[test]
181    fn activity_list_joins_entity_and_action() {
182        let msg = ListSubaccountEventsResponse {
183            events: vec![ActivityEvent {
184                entity_kind: ActivityEntityKind::ACTIVITY_ENTITY_INVITE.into(),
185                event_action: ActivityEventAction::ACTIVITY_ACTION_CREATED.into(),
186                ..Default::default()
187            }],
188            next_page_token: "n".into(),
189            ..Default::default()
190        };
191        let result = subaccount_activity_list_from_proto(&msg);
192        assert_eq!(result.events[0].event_type, "invite:created");
193        assert_eq!(result.next_page_token, "n");
194    }
195
196    #[test]
197    fn create_subaccount_maps_revision() {
198        let result = create_subaccount_from_proto(&CreateSubaccountResponse {
199            subaccount_id: 42,
200            revision: 1,
201            ..Default::default()
202        });
203        assert_eq!(result.subaccount_id, format_uint64_id(42));
204        assert_eq!(result.revision, 1);
205    }
206}