Skip to content

Commit 9a5669c

Browse files
Copilotlarp0
andcommitted
Implement transaction page with data fetching and display
Co-authored-by: larp0 <[email protected]>
1 parent 266ce31 commit 9a5669c

File tree

5 files changed

+458
-4
lines changed

5 files changed

+458
-4
lines changed

opensvm-dioxus/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ web-sys = { version = "0.3.64", features = [
3434
"ClipboardEvent",
3535
"Location",
3636
"MediaQueryList",
37+
"Request",
38+
"RequestInit",
39+
"RequestMode",
40+
"Response",
41+
"Headers",
3742
], optional = true }
3843
gloo = { version = "0.10.0", optional = true, features = ["futures"] }
3944
console_log = { version = "1.0.0", optional = true }

opensvm-dioxus/src/app.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,12 @@ fn AI(cx: Scope) -> Element {
6767
}
6868

6969
#[component]
70-
fn Transaction(cx: Scope, #[allow(unused_variables)] id: String) -> Element {
71-
cx.render(rsx! { TransactionPage {} })
70+
fn Transaction(cx: Scope, id: String) -> Element {
71+
cx.render(rsx! {
72+
TransactionPage {
73+
transaction_id: id.clone()
74+
}
75+
})
7276
}
7377

7478
#[component]

opensvm-dioxus/src/assets/styles.css

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,129 @@ p {
105105

106106
.text-tertiary {
107107
color: var(--text-tertiary);
108+
}
109+
110+
/* Transaction page specific styles */
111+
.transaction-details {
112+
max-width: 1000px;
113+
margin: 0 auto;
114+
}
115+
116+
.section {
117+
background-color: var(--surface);
118+
border: 1px solid var(--border);
119+
border-radius: 8px;
120+
padding: 1.5rem;
121+
margin-bottom: 1.5rem;
122+
}
123+
124+
.section h2 {
125+
color: var(--primary);
126+
margin-bottom: 1rem;
127+
font-size: 1.25rem;
128+
}
129+
130+
.info-grid {
131+
display: grid;
132+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
133+
gap: 1rem;
134+
}
135+
136+
.info-item {
137+
display: flex;
138+
flex-direction: column;
139+
gap: 0.25rem;
140+
}
141+
142+
.info-item .label {
143+
font-weight: 600;
144+
color: var(--text-secondary);
145+
font-size: 0.875rem;
146+
}
147+
148+
.info-item .value {
149+
color: var(--text);
150+
font-size: 0.875rem;
151+
word-break: break-all;
152+
}
153+
154+
.mono {
155+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
156+
}
157+
158+
.small {
159+
font-size: 0.75rem;
160+
}
161+
162+
.account-list, .instruction-list {
163+
display: flex;
164+
flex-direction: column;
165+
gap: 0.75rem;
166+
}
167+
168+
.account-item {
169+
display: flex;
170+
align-items: center;
171+
gap: 1rem;
172+
padding: 0.75rem;
173+
background-color: var(--surface-light);
174+
border-radius: 6px;
175+
}
176+
177+
.account-item .index {
178+
font-weight: 600;
179+
color: var(--primary);
180+
min-width: 3rem;
181+
}
182+
183+
.account-item .address {
184+
color: var(--text);
185+
font-size: 0.875rem;
186+
}
187+
188+
.instruction-item {
189+
padding: 1rem;
190+
background-color: var(--surface-light);
191+
border-radius: 6px;
192+
}
193+
194+
.instruction-item h3 {
195+
color: var(--primary);
196+
margin-bottom: 0.75rem;
197+
font-size: 1rem;
198+
}
199+
200+
.instruction-details {
201+
display: flex;
202+
flex-direction: column;
203+
gap: 0.5rem;
204+
}
205+
206+
.logs {
207+
background-color: var(--surface-light);
208+
border-radius: 6px;
209+
padding: 1rem;
210+
max-height: 300px;
211+
overflow-y: auto;
212+
}
213+
214+
.log-item {
215+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
216+
font-size: 0.75rem;
217+
color: var(--text);
218+
margin-bottom: 0.25rem;
219+
line-height: 1.4;
220+
}
221+
222+
.loading, .error, .not-found {
223+
text-align: center;
224+
padding: 2rem;
225+
background-color: var(--surface);
226+
border-radius: 8px;
227+
margin: 2rem 0;
228+
}
229+
230+
.error {
231+
color: var(--error);
232+
border: 1px solid var(--error);
108233
}
Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,219 @@
11
//! Transaction page
22
33
use dioxus::prelude::*;
4+
use crate::utils::api::{TransactionDetails};
5+
6+
#[derive(PartialEq, Props)]
7+
pub struct TransactionPageProps {
8+
pub transaction_id: String,
9+
}
410

511
/// Transaction page component
6-
pub fn TransactionPage(cx: Scope) -> Element {
12+
pub fn TransactionPage(cx: Scope<TransactionPageProps>) -> Element {
13+
let transaction_data = use_state(cx, || None::<TransactionDetails>);
14+
let loading = use_state(cx, || true);
15+
let error = use_state(cx, || None::<String>);
16+
17+
// Fetch transaction data when component mounts or transaction_id changes
18+
use_effect(cx, (&cx.props.transaction_id,), |(transaction_id,)| {
19+
let transaction_data = transaction_data.clone();
20+
let loading = loading.clone();
21+
let error = error.clone();
22+
let transaction_id = transaction_id.clone();
23+
24+
async move {
25+
loading.set(true);
26+
error.set(None);
27+
28+
#[cfg(feature = "web")]
29+
{
30+
use crate::utils::api::web::fetch_transaction;
31+
match fetch_transaction(&transaction_id).await {
32+
Ok(Some(data)) => {
33+
transaction_data.set(Some(data));
34+
}
35+
Ok(None) => {
36+
error.set(Some("Transaction not found".to_string()));
37+
}
38+
Err(e) => {
39+
error.set(Some(format!("Error fetching transaction: {:?}", e)));
40+
}
41+
}
42+
}
43+
44+
#[cfg(not(feature = "web"))]
45+
{
46+
// For desktop/mobile, implement later or use mock data
47+
error.set(Some("Transaction fetching not implemented for this platform".to_string()));
48+
}
49+
50+
loading.set(false);
51+
}
52+
});
53+
754
cx.render(rsx! {
855
div { class: "transaction-page",
956
h1 { "Transaction Details" }
10-
p { "View detailed information about Solana blockchain transactions." }
57+
58+
if *loading.get() {
59+
rsx! {
60+
div { class: "loading",
61+
p { "Loading transaction..." }
62+
}
63+
}
64+
} else if let Some(error_msg) = error.get() {
65+
rsx! {
66+
div { class: "error",
67+
p { "Error: {error_msg}" }
68+
p { "Transaction ID: {cx.props.transaction_id}" }
69+
}
70+
}
71+
} else if let Some(transaction) = transaction_data.get() {
72+
rsx! {
73+
div { class: "transaction-details",
74+
render_transaction_info { transaction: transaction.clone() }
75+
}
76+
}
77+
} else {
78+
rsx! {
79+
div { class: "not-found",
80+
p { "Transaction not found" }
81+
p { "Transaction ID: {cx.props.transaction_id}" }
82+
}
83+
}
84+
}
1185
}
1286
})
1387
}
88+
89+
#[derive(PartialEq, Props)]
90+
struct TransactionInfoProps {
91+
transaction: TransactionDetails,
92+
}
93+
94+
fn render_transaction_info(cx: Scope<TransactionInfoProps>) -> Element {
95+
let tx = &cx.props.transaction;
96+
97+
cx.render(rsx! {
98+
div { class: "transaction-info",
99+
div { class: "section",
100+
h2 { "Overview" }
101+
div { class: "info-grid",
102+
div { class: "info-item",
103+
span { class: "label", "Signature:" },
104+
span { class: "value mono",
105+
if !tx.transaction.signatures.is_empty() {
106+
&tx.transaction.signatures[0]
107+
} else {
108+
"N/A"
109+
}
110+
}
111+
},
112+
113+
match tx.slot {
114+
Some(slot) => rsx! {
115+
div { class: "info-item",
116+
span { class: "label", "Slot:" },
117+
span { class: "value", "{slot}" }
118+
}
119+
},
120+
None => rsx! { div {} }
121+
},
122+
123+
match tx.block_time {
124+
Some(block_time) => rsx! {
125+
div { class: "info-item",
126+
span { class: "label", "Block Time:" },
127+
span { class: "value", "{format_timestamp(block_time)}" }
128+
}
129+
},
130+
None => rsx! { div {} }
131+
},
132+
133+
match &tx.meta {
134+
Some(meta) => rsx! {
135+
div { class: "info-item",
136+
span { class: "label", "Fee:" },
137+
span { class: "value", "{meta.fee} lamports" }
138+
},
139+
div { class: "info-item",
140+
span { class: "label", "Status:" },
141+
span { class: "value",
142+
if meta.err.is_some() {
143+
"Failed"
144+
} else {
145+
"Success"
146+
}
147+
}
148+
}
149+
},
150+
None => rsx! { div {} }
151+
}
152+
}
153+
}
154+
155+
div { class: "section",
156+
h2 { "Account Keys" }
157+
div { class: "account-list",
158+
for (i, account) in tx.transaction.message.account_keys.iter().enumerate() {
159+
div { class: "account-item",
160+
span { class: "index", "#{i}" },
161+
span { class: "address mono", "{account}" }
162+
}
163+
}
164+
}
165+
}
166+
167+
div { class: "section",
168+
h2 { "Instructions" }
169+
div { class: "instruction-list",
170+
for (i, instruction) in tx.transaction.message.instructions.iter().enumerate() {
171+
div { class: "instruction-item",
172+
h3 { "Instruction #{i + 1}" },
173+
div { class: "instruction-details",
174+
div { class: "info-item",
175+
span { class: "label", "Program:" },
176+
span { class: "value mono",
177+
if let Some(program_key) = tx.transaction.message.account_keys.get(instruction.program_id_index as usize) {
178+
program_key
179+
} else {
180+
"Unknown"
181+
}
182+
}
183+
},
184+
div { class: "info-item",
185+
span { class: "label", "Data:" },
186+
span { class: "value mono small", "{instruction.data}" }
187+
}
188+
}
189+
}
190+
}
191+
}
192+
}
193+
194+
match &tx.meta {
195+
Some(meta) => match &meta.log_messages {
196+
Some(logs) => rsx! {
197+
div { class: "section",
198+
h2 { "Log Messages" },
199+
div { class: "logs",
200+
for log in logs {
201+
div { class: "log-item", "{log}" }
202+
}
203+
}
204+
}
205+
},
206+
None => rsx! { div {} }
207+
},
208+
None => rsx! { div {} }
209+
}
210+
}
211+
})
212+
}
213+
214+
fn format_timestamp(timestamp: i64) -> String {
215+
use chrono::{DateTime, Utc};
216+
DateTime::<Utc>::from_timestamp(timestamp, 0)
217+
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
218+
.unwrap_or_else(|| timestamp.to_string())
219+
}

0 commit comments

Comments
 (0)