Skip to content

Commit ade9219

Browse files
authored
Merge pull request #350 from Grace-CODE-D/feature/react-app-stress-mobile-privacy
Feature/react app stress mobile privacy
2 parents 46ca353 + 420388b commit ade9219

10 files changed

Lines changed: 448 additions & 14 deletions

File tree

app/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
"dependencies": {
1212
"@stellar-escrow/components": "*",
1313
"@stellar-escrow/state": "*",
14+
"@emotion/react": "^11.11.3",
15+
"@emotion/styled": "^11.11.0",
16+
"@mui/icons-material": "^5.15.3",
17+
"@mui/material": "^5.15.3",
1418
"@reduxjs/toolkit": "^1.9.7",
1519
"react": "^18.2.0",
1620
"react-dom": "^18.2.0",

app/src/App.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,38 @@
1-
import { Routes, Route, NavLink } from 'react-router-dom';
1+
import { Routes, Route } from 'react-router-dom';
2+
import { AppBar, Toolbar, Typography, Button, Box, Container } from '@mui/material';
3+
import { NavLink } from 'react-router-dom';
24
import Dashboard from './pages/Dashboard';
35
import TradeDetail from './pages/TradeDetail';
46
import CreateTrade from './pages/CreateTrade';
57
import { ErrorBoundary } from './ErrorBoundary';
6-
import './App.css';
78

89
export default function App() {
910
return (
1011
<ErrorBoundary>
11-
<div className="app">
12-
<nav className="nav">
13-
<span className="nav-brand">StellarEscrow</span>
14-
<NavLink to="/" end>Dashboard</NavLink>
15-
<NavLink to="/trades/new">New Trade</NavLink>
16-
</nav>
17-
<main className="main">
12+
<Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
13+
<AppBar position="static" color="primary">
14+
<Toolbar sx={{ gap: 2 }}>
15+
<Typography variant="h6" sx={{ fontWeight: 700, flexGrow: 1 }}>
16+
StellarEscrow
17+
</Typography>
18+
<Button color="inherit" component={NavLink} to="/" end>
19+
Dashboard
20+
</Button>
21+
<Button color="inherit" component={NavLink} to="/trades/new">
22+
New Trade
23+
</Button>
24+
</Toolbar>
25+
</AppBar>
26+
<Container component="main" sx={{ flex: 1, py: 4 }}>
1827
<ErrorBoundary>
1928
<Routes>
2029
<Route path="/" element={<Dashboard />} />
2130
<Route path="/trades/new" element={<CreateTrade />} />
2231
<Route path="/trades/:id" element={<TradeDetail />} />
2332
</Routes>
2433
</ErrorBoundary>
25-
</main>
26-
</div>
34+
</Container>
35+
</Box>
2736
</ErrorBoundary>
2837
);
2938
}

app/src/main.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import ReactDOM from 'react-dom/client';
33
import { Provider } from 'react-redux';
44
import { PersistGate } from 'redux-persist/integration/react';
55
import { BrowserRouter } from 'react-router-dom';
6+
import { ThemeProvider, CssBaseline } from '@mui/material';
67
import { store, persistor } from '@stellar-escrow/state';
8+
import theme from './theme';
79
import App from './App';
810
import './index.css';
911

@@ -12,7 +14,10 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
1214
<Provider store={store}>
1315
<PersistGate loading={null} persistor={persistor}>
1416
<BrowserRouter>
15-
<App />
17+
<ThemeProvider theme={theme}>
18+
<CssBaseline />
19+
<App />
20+
</ThemeProvider>
1621
</BrowserRouter>
1722
</PersistGate>
1823
</Provider>

app/src/theme.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { createTheme } from '@mui/material/styles';
2+
3+
const theme = createTheme({
4+
palette: {
5+
primary: { main: '#1a1a2e' },
6+
secondary: { main: '#e94560' },
7+
background: { default: '#f5f5f5' },
8+
},
9+
typography: {
10+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
11+
},
12+
components: {
13+
MuiButton: {
14+
defaultProps: { disableElevation: true },
15+
styleOverrides: { root: { textTransform: 'none', borderRadius: 6 } },
16+
},
17+
},
18+
});
19+
20+
export default theme;

contract/STRESS_TESTING.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ Validates StellarEscrow contract performance under load using Soroban's built-in
55
## Running
66

77
```bash
8-
cd contract
8+
# Automated runner (from repo root) — runs all tests and prints a summary
9+
./scripts/stress-test.sh
10+
11+
# Release build (faster execution, closer to production)
12+
./scripts/stress-test.sh --release
913

10-
# All stress tests with output
14+
# Directly via cargo
15+
cd contract
1116
cargo test --test stress -- --nocapture
1217

1318
# Single scenario

contract/src/privacy.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,46 @@ pub fn get_grant(
8686
get_disclosure_grant(env, trade_id, grantee)
8787
.ok_or(ContractError::DisclosureGrantNotFound)
8888
}
89+
90+
/// Check whether a trade has privacy settings configured.
91+
pub fn has_privacy(env: &Env, trade_id: u64) -> bool {
92+
get_trade_privacy(env, trade_id).is_some()
93+
}
94+
95+
/// Check whether a specific grantee has an active disclosure grant for a trade.
96+
pub fn has_disclosure_grant(env: &Env, trade_id: u64, grantee: &Address) -> bool {
97+
get_disclosure_grant(env, trade_id, grantee).is_some()
98+
}
99+
100+
/// Verify that a data hash matches the stored privacy commitment.
101+
/// Returns `Ok(true)` if the hash matches, `Ok(false)` if it doesn't,
102+
/// or `Err(DisclosureGrantNotFound)` if no privacy record exists.
103+
pub fn verify_data_hash(
104+
env: &Env,
105+
trade_id: u64,
106+
claimed_hash: &String,
107+
) -> Result<bool, ContractError> {
108+
let privacy = get_trade_privacy(env, trade_id)
109+
.ok_or(ContractError::DisclosureGrantNotFound)?;
110+
Ok(privacy.data_hash == *claimed_hash)
111+
}
112+
113+
/// Privacy compliance check: ensure a trade has a privacy record before
114+
/// allowing sensitive operations (e.g. private arbitration).
115+
/// Returns `Err(DisclosureUnauthorized)` if private_arbitration is requested
116+
/// but no privacy record has been set.
117+
pub fn require_privacy_compliance(
118+
env: &Env,
119+
trade_id: u64,
120+
requires_private_arbitration: bool,
121+
) -> Result<(), ContractError> {
122+
if !requires_private_arbitration {
123+
return Ok(());
124+
}
125+
let privacy = get_trade_privacy(env, trade_id)
126+
.ok_or(ContractError::DisclosureUnauthorized)?;
127+
if !privacy.private_arbitration {
128+
return Err(ContractError::DisclosureUnauthorized);
129+
}
130+
Ok(())
131+
}

mobile-sdk/src/flows.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/// Mobile-optimized transaction flows for common escrow operations.
2+
///
3+
/// Each flow bundles the steps a mobile user takes into a single function:
4+
/// build → (optionally queue offline) → sign → submit.
5+
use crate::error::MobileSdkError;
6+
use crate::offline_queue::OfflineQueue;
7+
use crate::signing::sign_transaction;
8+
use crate::transaction_builder::{
9+
build_cancel_trade, build_confirm_receipt, build_fund_trade, build_raise_dispute,
10+
};
11+
use crate::types::{QueuedTransaction, SignedTransaction};
12+
13+
/// Result of a mobile flow step.
14+
pub enum FlowResult {
15+
/// Transaction was signed and is ready for submission.
16+
Ready(SignedTransaction),
17+
/// Device is offline; transaction was queued for later.
18+
Queued(String),
19+
}
20+
21+
/// Fund a trade: build + sign in one call.
22+
/// If `secret_key_hex` is provided, signs immediately (offline-capable).
23+
/// If the device is offline (`is_online = false`), queues instead.
24+
pub fn flow_fund_trade(
25+
contract_id: &str,
26+
trade_id: u64,
27+
sequence: i64,
28+
network_passphrase: &str,
29+
secret_key_hex: &str,
30+
is_online: bool,
31+
queue: &mut OfflineQueue,
32+
) -> Result<FlowResult, MobileSdkError> {
33+
let unsigned = build_fund_trade(contract_id, trade_id, sequence, network_passphrase)?;
34+
35+
if !is_online {
36+
let queued_id = format!("fund_trade_{trade_id}_{sequence}");
37+
queue.enqueue(QueuedTransaction {
38+
id: queued_id.clone(),
39+
unsigned_xdr: unsigned.xdr,
40+
operation: format!("fund_trade:{trade_id}"),
41+
created_at: 0, // caller should set epoch seconds
42+
});
43+
return Ok(FlowResult::Queued(queued_id));
44+
}
45+
46+
let signed = sign_transaction(&unsigned, secret_key_hex)?;
47+
Ok(FlowResult::Ready(signed))
48+
}
49+
50+
/// Confirm receipt: build + sign in one call.
51+
pub fn flow_confirm_receipt(
52+
contract_id: &str,
53+
trade_id: u64,
54+
sequence: i64,
55+
network_passphrase: &str,
56+
secret_key_hex: &str,
57+
is_online: bool,
58+
queue: &mut OfflineQueue,
59+
) -> Result<FlowResult, MobileSdkError> {
60+
let unsigned = build_confirm_receipt(contract_id, trade_id, sequence, network_passphrase)?;
61+
62+
if !is_online {
63+
let queued_id = format!("confirm_receipt_{trade_id}_{sequence}");
64+
queue.enqueue(QueuedTransaction {
65+
id: queued_id.clone(),
66+
unsigned_xdr: unsigned.xdr,
67+
operation: format!("confirm_receipt:{trade_id}"),
68+
created_at: 0,
69+
});
70+
return Ok(FlowResult::Queued(queued_id));
71+
}
72+
73+
let signed = sign_transaction(&unsigned, secret_key_hex)?;
74+
Ok(FlowResult::Ready(signed))
75+
}
76+
77+
/// Raise dispute: build + sign in one call.
78+
pub fn flow_raise_dispute(
79+
contract_id: &str,
80+
trade_id: u64,
81+
sequence: i64,
82+
network_passphrase: &str,
83+
secret_key_hex: &str,
84+
) -> Result<SignedTransaction, MobileSdkError> {
85+
let unsigned = build_raise_dispute(contract_id, trade_id, sequence, network_passphrase)?;
86+
sign_transaction(&unsigned, secret_key_hex)
87+
}
88+
89+
/// Cancel trade: build + sign in one call.
90+
pub fn flow_cancel_trade(
91+
contract_id: &str,
92+
trade_id: u64,
93+
sequence: i64,
94+
network_passphrase: &str,
95+
secret_key_hex: &str,
96+
) -> Result<SignedTransaction, MobileSdkError> {
97+
let unsigned = build_cancel_trade(contract_id, trade_id, sequence, network_passphrase)?;
98+
sign_transaction(&unsigned, secret_key_hex)
99+
}
100+
101+
/// Drain the offline queue and return signed transactions ready for submission.
102+
/// Call this when connectivity is restored.
103+
pub fn flush_offline_queue(
104+
queue: &mut OfflineQueue,
105+
secret_key_hex: &str,
106+
network_passphrase: &str,
107+
) -> Vec<Result<SignedTransaction, MobileSdkError>> {
108+
queue
109+
.drain()
110+
.into_iter()
111+
.map(|queued| {
112+
let unsigned = crate::types::UnsignedTransaction {
113+
xdr: queued.unsigned_xdr,
114+
network_passphrase: network_passphrase.to_string(),
115+
fee: 100,
116+
sequence: 0,
117+
};
118+
sign_transaction(&unsigned, secret_key_hex)
119+
})
120+
.collect()
121+
}

mobile-sdk/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub mod error;
2+
pub mod flows;
23
pub mod mobile_error;
34
pub mod offline_queue;
45
pub mod push_notifications;
56
pub mod signing;
67
pub mod transaction_builder;
78
pub mod types;
9+
pub mod wallet;

0 commit comments

Comments
 (0)