Skip to content

Commit 418c429

Browse files
feat: update docs, add security audit, and implement persistent form wrapper
1 parent 5fdbd78 commit 418c429

19 files changed

Lines changed: 824 additions & 1 deletion

File tree

README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,18 @@ We welcome contributions from the community! Please see our [Contributing Guidel
181181

182182
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details.
183183

184+
## Resources and Documentation
185+
186+
- [Architecture Overview](./docs/architecture.md)
187+
- [Architectural Decision Records (ADRs)](./docs/adr/README.md)
188+
- [Security Audit Report](./docs/security/AUDIT_REPORT.md)
189+
- [Contract Documentation](./contract/README.md)
190+
184191
## Community
185192

186193
- [Discord](https://discord.gg/gathera)
187194
- [Twitter](https://twitter.com/gathera)
188-
- [Documentation](https://docs.gathera.io)
195+
- [Official Documentation](https://docs.gathera.io)
189196

190197
## Acknowledgments
191198

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
'use client';
2+
3+
import React, { useEffect, useRef, useCallback } from 'react';
4+
5+
/**
6+
* Persistence storage type
7+
*/
8+
type StorageType = 'local' | 'session';
9+
10+
/**
11+
* Props for the PersistentFormWrapper component
12+
*/
13+
interface PersistentFormWrapperProps {
14+
/** The form elements to wrap */
15+
children: React.ReactNode;
16+
/** Unique key for storage */
17+
persistenceKey: string;
18+
/** Storage medium (local for persistent across sessions, session for current session) */
19+
storageType?: StorageType;
20+
/** Optional form ID to target if multiple forms exist in children */
21+
formId?: string;
22+
/** Enable automatic restoration of values on mount */
23+
autoRestore?: boolean;
24+
/** Callback triggered when state is restored, useful for react-hook-form reset() */
25+
onRestore?: (data: Record<string, string | boolean | string[]>) => void;
26+
/** Debounce time in ms to avoid excessive storage writes */
27+
debounceTime?: number;
28+
}
29+
30+
/**
31+
* PersistentFormWrapper
32+
*
33+
* A wrapper component that automatically saves form input state to browser storage
34+
* and restores it across page refreshes or navigations.
35+
*
36+
* Works with native HTML forms and provides callbacks for React-based form libraries.
37+
*/
38+
export const PersistentFormWrapper: React.FC<PersistentFormWrapperProps> = ({
39+
children,
40+
persistenceKey,
41+
storageType = 'local',
42+
formId,
43+
autoRestore = true,
44+
onRestore,
45+
debounceTime = 500,
46+
}) => {
47+
const containerRef = useRef<HTMLDivElement>(null);
48+
const debounceTimer = useRef<NodeJS.Timeout | null>(null);
49+
50+
// Helper to get storage engine safely (SSR check)
51+
const getStorage = useCallback((): Storage | null => {
52+
if (typeof window === 'undefined') return null;
53+
return storageType === 'local' ? window.localStorage : window.sessionStorage;
54+
}, [storageType]);
55+
56+
/**
57+
* Captures the current state of the form
58+
*/
59+
const captureFormState = useCallback(() => {
60+
const container = containerRef.current;
61+
if (!container) return null;
62+
63+
const form = formId
64+
? (container.querySelector(`#${formId}`) as HTMLFormElement)
65+
: container.querySelector('form');
66+
67+
if (!form) return null;
68+
69+
const formData = new FormData(form);
70+
const data: Record<string, any> = {};
71+
72+
formData.forEach((value, key) => {
73+
// Handle multi-value fields (like checkboxes or multiple selects)
74+
if (data[key]) {
75+
if (Array.isArray(data[key])) {
76+
data[key].push(value);
77+
} else {
78+
data[key] = [data[key], value];
79+
}
80+
} else {
81+
data[key] = value;
82+
}
83+
});
84+
85+
// Special handling for checkboxes that are NOT checked (they don't show up in FormData)
86+
const checkboxes = form.querySelectorAll('input[type="checkbox"]');
87+
checkboxes.forEach((cb: any) => {
88+
if (!cb.checked && !data[cb.name]) {
89+
data[cb.name] = false;
90+
} else if (cb.checked && !Array.isArray(data[cb.name])) {
91+
// Ensure singular checkboxes are booleans if they don't have a value set
92+
if (!cb.getAttribute('value') || cb.getAttribute('value') === 'on') {
93+
data[cb.name] = true;
94+
}
95+
}
96+
});
97+
98+
return data;
99+
}, [formId]);
100+
101+
/**
102+
* Restores state to the DOM elements
103+
*/
104+
const restoreDOMState = useCallback((data: Record<string, any>) => {
105+
const container = containerRef.current;
106+
if (!container) return;
107+
108+
const form = formId
109+
? (container.querySelector(`#${formId}`) as HTMLFormElement)
110+
: container.querySelector('form');
111+
112+
if (!form) return;
113+
114+
Object.entries(data).forEach(([name, value]) => {
115+
const elements = form.elements.namedItem(name);
116+
if (!elements) return;
117+
118+
if (elements instanceof HTMLInputElement) {
119+
if (elements.type === 'checkbox') {
120+
elements.checked = Boolean(value);
121+
} else if (elements.type === 'radio') {
122+
if (elements.value === String(value)) elements.checked = true;
123+
} else {
124+
elements.value = String(value);
125+
}
126+
} else if (elements instanceof RadioNodeList) {
127+
// Handle radio groups or multiple checkboxes with same name
128+
const nodeList = elements as unknown as NodeListOf<HTMLInputElement>;
129+
nodeList.forEach((el) => {
130+
if (el.type === 'radio') {
131+
el.checked = el.value === String(value);
132+
} else if (el.type === 'checkbox') {
133+
if (Array.isArray(value)) {
134+
el.checked = value.includes(el.value);
135+
} else {
136+
el.checked = Boolean(value);
137+
}
138+
}
139+
});
140+
} else if (elements instanceof HTMLTextAreaElement || elements instanceof HTMLSelectElement) {
141+
elements.value = String(value);
142+
}
143+
});
144+
}, [formId]);
145+
146+
/**
147+
* Persist current state to storage
148+
*/
149+
const persistState = useCallback(() => {
150+
const data = captureFormState();
151+
if (!data) return;
152+
153+
const storage = getStorage();
154+
if (storage) {
155+
storage.setItem(persistenceKey, JSON.stringify(data));
156+
}
157+
}, [captureFormState, getStorage, persistenceKey]);
158+
159+
// Handle restoration on mount
160+
useEffect(() => {
161+
if (!autoRestore) return;
162+
163+
const storage = getStorage();
164+
if (!storage) return;
165+
166+
const saved = storage.getItem(persistenceKey);
167+
if (saved) {
168+
try {
169+
const parsedData = JSON.parse(saved);
170+
171+
// Use callback if provided
172+
if (onRestore) {
173+
onRestore(parsedData);
174+
} else {
175+
// Fallback to direct DOM manipulation
176+
// Wait a tick for children to be fully rendered
177+
const timer = setTimeout(() => {
178+
restoreDOMState(parsedData);
179+
}, 0);
180+
return () => clearTimeout(timer);
181+
}
182+
} catch (err) {
183+
console.error(`Error restoring form state for key "${persistenceKey}":`, err);
184+
}
185+
}
186+
}, [autoRestore, getStorage, persistenceKey, onRestore, restoreDOMState]);
187+
188+
// Handle captures on input
189+
useEffect(() => {
190+
const container = containerRef.current;
191+
if (!container) return;
192+
193+
const handleInput = () => {
194+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
195+
196+
debounceTimer.current = setTimeout(() => {
197+
persistState();
198+
}, debounceTime);
199+
};
200+
201+
container.addEventListener('input', handleInput);
202+
container.addEventListener('change', handleInput);
203+
204+
return () => {
205+
container.removeEventListener('input', handleInput);
206+
container.removeEventListener('change', handleInput);
207+
if (debounceTimer.current) clearTimeout(debounceTimer.current);
208+
};
209+
}, [debounceTime, persistState]);
210+
211+
return (
212+
<div
213+
ref={containerRef}
214+
className="persistent-form-wrapper"
215+
data-persistence-key={persistenceKey}
216+
>
217+
{children}
218+
</div>
219+
);
220+
};

app/frontend/components/forms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ export * from './exampleSchemas';
1919
export { default as ErrorSummary } from './ErrorSummary';
2020

2121
export { default as CreateEventForm } from './CreateEventForm';
22+
export { PersistentFormWrapper } from './PersistentFormWrapper';

contract/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ contract/
1212
├── ticket_contract/ # Soulbound ticket system
1313
├── escrow_contract/ # Secure escrow services
1414
├── multisig_wallet_contract/ # Multi-signature wallet
15+
├── dutch_auction_contract/ # Dutch auction for tickets
16+
├── zk_ticket_contract/ # Zero-knowledge ticketing
17+
├── cross_contract_contract/ # Cross-contract operations
1518
├── contracts/ # Integration layer
1619
└── test/ # Testing utilities
1720
```
@@ -66,6 +69,40 @@ contract/
6669

6770
**Dependencies**: `gathera-common`
6871

72+
### 💰 `dutch_auction_contract`
73+
74+
**Purpose**: Implements a Dutch auction mechanism for ticket sales, ensuring fair price discovery.
75+
76+
**Key Features**:
77+
- Linear price decay
78+
- Real-time bidding
79+
- Automatic fulfillment
80+
- Integration with `ticket_contract`
81+
82+
**Dependencies**: `gathera-common`, `ticket_contract`
83+
84+
### 🕶️ `zk_ticket_contract`
85+
86+
**Purpose**: Enables privacy-preserving ticket verification using Zero-Knowledge Proofs.
87+
88+
**Key Features**:
89+
- On-chain ZK verification
90+
- Private attendance logs
91+
- Proof of eligibility without revealing identity
92+
93+
**Dependencies**: `gathera-common`
94+
95+
### 🔀 `cross_contract_contract`
96+
97+
**Purpose**: Orchestrates complex operations involving multiple Gathera contracts.
98+
99+
**Key Features**:
100+
- Atomic multi-contract execution
101+
- Shared state synchronization
102+
- Rollback mechanisms for failed multi-step operations
103+
104+
**Dependencies**: All other contracts
105+
69106
### 🔗 `contracts` (Integration Layer)
70107

71108
**Purpose**: Provides orchestration and unified interfaces for cross-contract operations.
@@ -195,6 +232,30 @@ When contributing to the Gathera contract suite:
195232
4. **Check Gas Usage**: Verify gas efficiency of changes
196233
5. **Run CI**: Ensure all tests pass before submitting
197234

235+
## Troubleshooting
236+
237+
### Common Build Issues
238+
239+
- **WASM Target Missing**: Ensure the `wasm32-unknown-unknown` target is installed:
240+
```bash
241+
rustup target add wasm32-unknown-unknown
242+
```
243+
- **Outdated CLI**: If contract deployment fails, update the Soroban CLI:
244+
```bash
245+
cargo install --locked soroban-cli
246+
```
247+
248+
### Testing Issues
249+
250+
- **Time-based Proofs Fail**: Some tests depend on network time. Ensure your environment matches the expected time parameters in `test/src/utils.rs`.
251+
- **Insufficient Gas**: For complex transactions, increase the gas limit in the `soroban contract invoke` flags.
252+
198253
## License
199254

200255
This project is licensed under the MIT License - see the LICENSE file for details.
256+
257+
---
258+
259+
**Gathera Smart Contract Team** 🛡️
260+
261+
For support, please open an issue in the main repository or contact us on Discord.

contract/common/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Gathera Common Utility Library
2+
3+
The `gathera-common` module provides shared data structures, constants, and utilities used throughout the Gathera smart contract ecosystem.
4+
5+
## Features
6+
7+
- **Standard Types**: Consistent definitions for addresses, ticket IDs, and statuses.
8+
- **Gas Measurement**: Utility traits and macros for benchmarking contract efficiency.
9+
- **Errors**: Centralized error mapping for clear cross-contract debugging.
10+
- **Math Ops**: Safe arithmetic helpers with platform-optimized checks.
11+
12+
## Getting Started
13+
14+
### Prerequisites
15+
16+
- Rust 1.74+
17+
18+
### Building
19+
20+
```bash
21+
cargo build --release
22+
```
23+
24+
This is a library crate and should be referenced in other contracts' `Cargo.toml`.
25+
26+
## Dependencies
27+
28+
- `soroban-sdk`: Core Soroban development kit.
29+
30+
## Troubleshooting
31+
32+
- **Symbol Collision**: Ensure all macros are imported with the `gathera_common::` prefix.
33+
- **Workspace Link Errors**: Verify that the `Cargo.toml` workspace configuration matches the project layout.

0 commit comments

Comments
 (0)