Skip to main content

gm_lib/tui/app/pages/
asset_transfer.rs

1use crate::network::NetworkStore;
2use crate::tui::app::widgets::address_book_popup::AddressBookPopup;
3use crate::tui::app::widgets::assets_popup::AssetsPopup;
4use crate::tui::app::widgets::form::FormItemIndex;
5use crate::tui::app::widgets::tx_popup::TxPopup;
6use crate::tui::app::SharedState;
7use crate::tui::{
8    app::widgets::form::{Form, FormWidget},
9    events::Event,
10    traits::{Component, HandleResult},
11};
12use crate::utils::assets::{Asset, TokenAddress};
13use crate::utils::erc20;
14use crate::Result;
15use alloy::primitives::utils::parse_units;
16use alloy::primitives::{Bytes, U256};
17use alloy::rpc::types::TransactionRequest;
18use std::sync::mpsc;
19use std::sync::{atomic::AtomicBool, Arc};
20use strum::EnumIter;
21
22use super::address_book::AddressBookMenuItem;
23
24#[derive(EnumIter, PartialEq)]
25pub enum FormItem {
26    Heading,
27    To,
28    AssetType,
29    Amount,
30    ErrorText,
31    TransferButton,
32}
33impl FormItemIndex for FormItem {
34    fn index(self) -> usize {
35        self as usize
36    }
37}
38impl TryFrom<FormItem> for FormWidget {
39    type Error = crate::Error;
40    fn try_from(value: FormItem) -> crate::Result<Self> {
41        let widget = match value {
42            FormItem::Heading => FormWidget::Heading("Transfer Assets"),
43            FormItem::To => FormWidget::InputBox {
44                label: "To",
45                text: String::new(),
46                empty_text: Some("<press SPACE to select from address book>"),
47                currency: None,
48            },
49            FormItem::AssetType => FormWidget::DisplayBox {
50                label: "Asset Type",
51                text: String::new(),
52                empty_text: Some("<press SPACE to select from your assets>"),
53            },
54            FormItem::Amount => FormWidget::InputBox {
55                label: "Amount",
56                text: String::new(),
57                empty_text: None,
58                currency: None,
59            },
60            FormItem::ErrorText => FormWidget::ErrorText(String::new()),
61            FormItem::TransferButton => FormWidget::Button { label: "Transfer" },
62        };
63        Ok(widget)
64    }
65}
66
67pub struct AssetTransferPage {
68    pub form: Form<FormItem>,
69    pub asset: Option<Asset>, // TODO see if we can avoid this here
70    pub address_book_popup: AddressBookPopup,
71    pub asset_popup: AssetsPopup,
72    pub tx_popup: TxPopup,
73}
74
75impl AssetTransferPage {
76    fn try_default() -> crate::Result<Self> {
77        Ok(Self {
78            form: Form::init(|_| Ok(()))?,
79            asset: None,
80            address_book_popup: AddressBookPopup::default(),
81            asset_popup: AssetsPopup::default(),
82            tx_popup: TxPopup::default(),
83        })
84    }
85}
86
87impl AssetTransferPage {
88    #[allow(clippy::field_reassign_with_default)]
89    pub fn new(asset: &Asset) -> crate::Result<Self> {
90        let mut page = Self::try_default()?;
91        page.asset = Some(asset.clone());
92
93        // Update the form with the asset type, this is because the `asset` is
94        // not directly linked to the ASSET_TYPE in form state
95        *page.form.get_text_mut(FormItem::AssetType) = format!("{}", asset.r#type);
96        *page
97            .form
98            .get_currency_mut(FormItem::Amount)
99            .expect("currency not found in this input entry, please check idx") =
100            Some(asset.r#type.symbol.clone());
101
102        Ok(page)
103    }
104}
105
106impl Component for AssetTransferPage {
107    fn set_focus(&mut self, focus: bool) {
108        self.form.set_form_focus(focus);
109    }
110
111    fn handle_event(
112        &mut self,
113        event: &Event,
114        area: ratatui::prelude::Rect,
115        tr: &mpsc::Sender<Event>,
116        sd: &Arc<AtomicBool>,
117        ss: &SharedState,
118    ) -> Result<HandleResult> {
119        let mut result = HandleResult::default();
120
121        if self.address_book_popup.is_open() {
122            result.merge(self.address_book_popup.handle_event(event, |entry| {
123                *self.form.get_text_mut(FormItem::To) = entry.address_unwrap().to_string();
124                self.form.advance_cursor();
125                Ok(())
126            })?);
127        } else if self.asset_popup.is_open() {
128            result.merge(self.asset_popup.handle_event(event, |asset| {
129                self.asset = Some(asset.clone());
130                *self.form.get_text_mut(FormItem::AssetType) = format!("{}", asset.r#type);
131                *self
132                    .form
133                    .get_currency_mut(FormItem::Amount)
134                    .expect("currency not found in this input entry, please check idx") =
135                    Some(asset.r#type.symbol.clone());
136                self.form.advance_cursor();
137                Ok(())
138            })?);
139        } else if self.tx_popup.is_open() {
140            let is_confirmed = self.tx_popup.is_confirmed();
141            let r = self.tx_popup.handle_event(
142                (event, area, tr, sd, ss),
143                |_| Ok(()),
144                |_| Ok(()),
145                || Ok(()),
146                || {
147                    if is_confirmed {
148                        result.page_pops = 1;
149                    }
150                    Ok(())
151                },
152            )?;
153            result.merge(r);
154        } else {
155            // Handle form events
156            if self.form.is_focused(FormItem::To)
157                && self.form.get_text(FormItem::To).is_empty()
158                && event.is_space_or_enter_pressed()
159            {
160                self.address_book_popup.open();
161                self.address_book_popup
162                    .set_items(Some(AddressBookMenuItem::get_menu(
163                        false,
164                        ss.recent_addresses.clone(),
165                    )?));
166                result.esc_ignores = 1;
167            } else if self.form.is_focused(FormItem::AssetType) && event.is_space_or_enter_pressed()
168            {
169                self.asset_popup.open();
170                self.asset_popup.set_items(ss.assets_read()?);
171                result.esc_ignores = 1;
172            } else {
173                self.form.handle_event(
174                    event,
175                    |_, _| Ok(()),
176                    |label, form| {
177                        if label == FormItem::TransferButton {
178                            let to = form.get_text(FormItem::To);
179                            let asset = self
180                                .asset
181                                .as_ref()
182                                .ok_or(crate::Error::InternalErrorStr("No asset selected"))?;
183                            let amount = parse_units(
184                                form.get_text(FormItem::Amount),
185                                asset.r#type.decimals,
186                            )?;
187
188                            let (to, calldata, value) = match asset.r#type.token_address {
189                                TokenAddress::Native => {
190                                    (to.parse()?, Bytes::new(), amount.get_absolute())
191                                }
192                                TokenAddress::Contract(address) => (
193                                    address,
194                                    erc20::encode_transfer(to.parse()?, amount.get_absolute()),
195                                    U256::ZERO,
196                                ),
197                            };
198
199                            if self.tx_popup.is_not_sent() || self.tx_popup.is_confirmed() {
200                                self.tx_popup.set_tx_req(
201                                    NetworkStore::from_name(&asset.r#type.network)?,
202                                    TransactionRequest::default()
203                                        .to(to)
204                                        .value(value)
205                                        .input(calldata.into()),
206                                );
207                            }
208
209                            self.tx_popup.open();
210                        }
211                        Ok(())
212                    },
213                )?;
214            }
215        }
216
217        // Check for amount to be greateer than balance
218        if let Some(asset) = &self.asset {
219            let amount = self.form.get_text(FormItem::Amount);
220            match parse_units(amount, asset.r#type.decimals) {
221                Err(e) => {
222                    *self.form.get_text_mut(FormItem::ErrorText) = format!("Invalid amount: {e}");
223                }
224                Ok(amount) => {
225                    if amount.get_absolute() > asset.value {
226                        *self.form.get_text_mut(FormItem::ErrorText) =
227                            "Amount exceeds balance".to_string();
228                    } else {
229                        self.form.get_text_mut(FormItem::ErrorText).clear();
230                    }
231                }
232            }
233        }
234
235        Ok(result)
236    }
237
238    fn render_component(
239        &self,
240        area: ratatui::prelude::Rect,
241        buf: &mut ratatui::prelude::Buffer,
242        shared_state: &SharedState,
243    ) -> ratatui::prelude::Rect
244    where
245        Self: Sized,
246    {
247        self.form.render(area, buf, &shared_state.theme);
248
249        self.address_book_popup
250            .render(area, buf, &shared_state.theme);
251
252        self.asset_popup.render(area, buf, &shared_state.theme);
253
254        self.tx_popup.render(area, buf, &shared_state.theme);
255
256        area
257    }
258}