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>, 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 *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 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 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}