1use crate::codecs::decode::enums::enum_proto_name;
4use crate::codecs::scalars::format_uint64_id;
5use crate::errors::{Error, Result};
6use crate::models::{LifecycleFlowSummary, LifecycleFlowsList, ZipperReasonDetails};
7use crate::proto::chain::lifecycle::v1::{
8 FlowKind as FlowKindEnum, FlowStep, FlowSummaryView, FlowTxMatchView, GetFlowResponse,
9 LifecycleReason, ListFlowsByTxResponse, ListFlowsResponse,
10};
11use crate::proto::chain::zipper::v1::ZipperReasonDetails as ProtoZipperReasonDetails;
12
13fn lifecycle_reason_label(value: &buffa::EnumValue<LifecycleReason>) -> String {
14 match value.as_known() {
15 Some(LifecycleReason::REASON_UNSPECIFIED) => "unspecified".to_owned(),
16 Some(LifecycleReason::ZIPPER_VALIDATION_REJECTED) => {
17 "zipper_validation_rejected".to_owned()
18 }
19 Some(LifecycleReason::ZIPPER_EXECUTION_REJECTED) => "zipper_execution_rejected".to_owned(),
20 Some(LifecycleReason::ZIPPER_WITHDRAW_EXECUTION_FAILED) => {
21 "zipper_withdraw_execution_failed".to_owned()
22 }
23 Some(LifecycleReason::ZIPPER_DEPOSIT_REFUND_FAILED) => {
24 "zipper_deposit_refund_failed".to_owned()
25 }
26 Some(LifecycleReason::LEDGER_MIRROR_REJECTED) => "ledger_mirror_rejected".to_owned(),
27 Some(LifecycleReason::LEDGER_MIRROR_TRANSFER_EXCEEDS_CREDITS) => {
28 "ledger_mirror_transfer_exceeds_credits".to_owned()
29 }
30 Some(LifecycleReason::LEDGER_MIRROR_TRANSFER_EXISTS) => {
31 "ledger_mirror_transfer_exists".to_owned()
32 }
33 Some(LifecycleReason::LEDGER_MIRROR_PENDING_TRANSFER_NOT_FOUND) => {
34 "ledger_mirror_pending_transfer_not_found".to_owned()
35 }
36 Some(LifecycleReason::LEDGER_MIRROR_TRANSFER_ID_ALREADY_FAILED) => {
37 "ledger_mirror_transfer_id_already_failed".to_owned()
38 }
39 None => format!("unknown_reason_{}", value.to_i32()),
40 }
41}
42
43fn zipper_reason_from_proto(msg: &ProtoZipperReasonDetails) -> ZipperReasonDetails {
44 ZipperReasonDetails {
45 code: msg.code.to_i32(),
46 reason_id: msg.reason_id.clone(),
47 message: msg.message.clone(),
48 }
49}
50
51fn flow_kind_label(value: &buffa::EnumValue<FlowKindEnum>) -> String {
52 match value.as_known() {
53 Some(FlowKindEnum::KIND_DEPOSIT) => "deposit".to_owned(),
54 Some(FlowKindEnum::KIND_WITHDRAW) => "withdraw".to_owned(),
55 Some(FlowKindEnum::KIND_TRANSFER) => "transfer".to_owned(),
56 Some(FlowKindEnum::KIND_UNSPECIFIED) => "unspecified".to_owned(),
57 None => {
58 let raw = enum_proto_name(value);
59 if raw.is_empty() { String::new() } else { raw }
60 }
61 }
62}
63
64fn flow_step_label(value: &buffa::EnumValue<FlowStep>) -> String {
65 match value.as_known() {
66 Some(FlowStep::FLOW_STEP_SOURCE) => "source".to_owned(),
67 Some(FlowStep::FLOW_STEP_TRANSFER) => "transfer".to_owned(),
68 Some(FlowStep::FLOW_STEP_REQUEST) => "request".to_owned(),
69 Some(FlowStep::FLOW_STEP_VALIDATION) => "validation".to_owned(),
70 Some(FlowStep::FLOW_STEP_EXECUTION) => "execution".to_owned(),
71 Some(FlowStep::FLOW_STEP_BRIDGE_FULFILLMENT) => "bridge_fulfillment".to_owned(),
72 Some(FlowStep::FLOW_STEP_DROPPED) => "dropped".to_owned(),
73 Some(FlowStep::FLOW_STEP_FAILED) => "failed".to_owned(),
74 Some(FlowStep::FLOW_STEP_REFUNDED) => "refunded".to_owned(),
75 Some(FlowStep::FLOW_STEP_FULFILLING) => "fulfilling".to_owned(),
76 Some(FlowStep::FLOW_STEP_SETTLEMENT) => "settlement".to_owned(),
77 Some(FlowStep::FLOW_STEP_UNSPECIFIED) => "unspecified".to_owned(),
78 None => {
79 let raw = enum_proto_name(value);
80 if raw.is_empty() { String::new() } else { raw }
81 }
82 }
83}
84
85fn flow_summary_from_proto(msg: &FlowSummaryView) -> LifecycleFlowSummary {
86 LifecycleFlowSummary {
87 intent_id: msg.flow_id.clone(),
88 flow_kind: flow_kind_label(&msg.flow_kind),
89 latest_step: flow_step_label(&msg.current_step),
90 is_open: msg.is_open,
91 is_terminal: msg.is_terminal,
92 owner_account_id: format_uint64_id(msg.owner_account_id),
93 smart_account_address: msg.smart_account_address.clone(),
94 lifecycle_reason: lifecycle_reason_label(&msg.lifecycle_reason),
95 zipper_reason: msg.zipper_reason.as_option().map(zipper_reason_from_proto),
96 }
97}
98
99pub fn flow_summary_message_from_proto(msg: &FlowSummaryView) -> LifecycleFlowSummary {
100 flow_summary_from_proto(msg)
101}
102
103pub fn flows_list_from_proto(msg: &ListFlowsResponse) -> LifecycleFlowsList {
104 LifecycleFlowsList {
105 flows: msg.flows.iter().map(flow_summary_from_proto).collect(),
106 next_page_token: msg.next_page_token.clone(),
107 }
108}
109
110fn flow_tx_match_from_proto(msg: &FlowTxMatchView) -> LifecycleFlowSummary {
111 LifecycleFlowSummary {
112 intent_id: msg.flow_id.clone(),
113 flow_kind: flow_kind_label(&msg.flow_kind),
114 latest_step: flow_step_label(&msg.current_step),
115 is_open: msg.is_open,
116 is_terminal: msg.is_terminal,
117 owner_account_id: format_uint64_id(msg.owner_account_id),
118 smart_account_address: msg.smart_account_address.clone(),
119 lifecycle_reason: lifecycle_reason_label(&msg.lifecycle_reason),
120 zipper_reason: msg.zipper_reason.as_option().map(zipper_reason_from_proto),
121 }
122}
123
124pub fn flows_by_tx_list_from_proto(msg: &ListFlowsByTxResponse) -> LifecycleFlowsList {
125 LifecycleFlowsList {
126 flows: msg.matches.iter().map(flow_tx_match_from_proto).collect(),
127 next_page_token: msg.next_page_token.clone(),
128 }
129}
130
131pub fn flow_from_get_response(msg: &GetFlowResponse) -> Result<LifecycleFlowSummary> {
132 let detail = msg
133 .flow
134 .as_option()
135 .ok_or_else(|| Error::transport("invalid GetFlow response: missing flow"))?;
136 detail
137 .summary
138 .as_option()
139 .map(flow_summary_from_proto)
140 .ok_or_else(|| Error::transport("invalid GetFlow response: missing flow summary"))
141}
142
143pub fn flow_from_get_by_tx_response(msg: &ListFlowsByTxResponse) -> Result<LifecycleFlowSummary> {
144 msg.matches
145 .first()
146 .map(flow_tx_match_from_proto)
147 .ok_or_else(|| Error::transport("invalid GetFlowByTx response: no matching flow"))
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::proto::chain::lifecycle::v1::{FlowKind as FlowKindEnum, FlowStep};
154 use crate::proto::chain::zipper::v1::ZipperReasonCode;
155
156 #[test]
157 fn flow_summary_maps_fields() {
158 let msg = FlowSummaryView {
159 flow_id: "flow-abc".into(),
160 flow_kind: FlowKindEnum::KindDeposit.into(),
161 current_step: FlowStep::Settlement.into(),
162 is_open: false,
163 is_terminal: true,
164 owner_account_id: 99,
165 smart_account_address: "0xabc".into(),
166 source_address: "0xsource".into(),
167 lifecycle_reason: LifecycleReason::LedgerMirrorTransferExceedsCredits.into(),
168 zipper_reason: ProtoZipperReasonDetails {
169 code: ZipperReasonCode::DepositAmountBelowMinimum.into(),
170 reason_id: "deposit_amount_below_minimum".into(),
171 message: "Deposit amount is below the minimum".into(),
172 ..Default::default()
173 }
174 .into(),
175 ..Default::default()
176 };
177 let flow = flow_summary_message_from_proto(&msg);
178 assert_eq!(flow.intent_id, "flow-abc");
179 assert!(flow.is_terminal);
180 assert_eq!(flow.flow_kind, "deposit");
181 assert_eq!(flow.latest_step, "settlement");
182 assert_eq!(flow.smart_account_address, "0xabc");
183 assert_eq!(flow.owner_account_id, format_uint64_id(99));
184 assert_eq!(
185 flow.lifecycle_reason,
186 "ledger_mirror_transfer_exceeds_credits"
187 );
188 let zipper = flow.zipper_reason.expect("zipper_reason");
189 assert_eq!(zipper.code, 1003);
190 assert_eq!(zipper.reason_id, "deposit_amount_below_minimum");
191 assert_eq!(zipper.message, "Deposit amount is below the minimum");
192 }
193
194 #[test]
195 fn lifecycle_reason_preserves_unknown_codes() {
196 let msg = FlowSummaryView {
197 flow_id: "flow-unknown".into(),
198 lifecycle_reason: buffa::EnumValue::from(2001),
199 ..Default::default()
200 };
201 let flow = flow_summary_message_from_proto(&msg);
202 assert_eq!(flow.flow_kind, "unspecified");
203 assert_eq!(flow.latest_step, "unspecified");
204 assert_eq!(flow.lifecycle_reason, "unknown_reason_2001");
205 assert!(flow.zipper_reason.is_none());
206 }
207
208 #[test]
209 fn flows_list_maps_pagination() {
210 let msg = ListFlowsResponse {
211 flows: vec![
212 FlowSummaryView {
213 flow_id: "a".into(),
214 ..Default::default()
215 },
216 FlowSummaryView {
217 flow_id: "b".into(),
218 ..Default::default()
219 },
220 ],
221 next_page_token: "next".into(),
222 ..Default::default()
223 };
224 let result = flows_list_from_proto(&msg);
225 assert_eq!(result.flows.len(), 2);
226 assert_eq!(result.next_page_token, "next");
227 assert_eq!(result.flows[0].lifecycle_reason, "unspecified");
228 }
229
230 #[test]
231 fn flow_tx_match_preserves_owner_identity() {
232 let msg = ListFlowsByTxResponse {
233 matches: vec![FlowTxMatchView {
234 flow_id: "flow-tx".into(),
235 owner_account_id: 99,
236 smart_account_address: "0xsmart".into(),
237 ..Default::default()
238 }],
239 ..Default::default()
240 };
241 let flow = flow_from_get_by_tx_response(&msg).expect("matching flow");
242 assert_eq!(flow.owner_account_id, format_uint64_id(99));
243 assert_eq!(flow.smart_account_address, "0xsmart");
244 }
245
246 #[test]
247 fn singular_flow_responses_reject_missing_required_entities() {
248 assert!(flow_from_get_response(&GetFlowResponse::default()).is_err());
249 assert!(flow_from_get_by_tx_response(&ListFlowsByTxResponse::default()).is_err());
250 }
251}