irongit

feat: add database and deploy local script to fill database with information as it creates contracts

huncholanehuncholaneauthored
parent ef45834commit c2f498fd1aca0e2954b65857aaea689f6e8d0e61Browse files

22 files changed, +3217 -730

+145-0db/migrations/20260128202118_init.sql
@@ -0,0 +1,145 @@
1+-- migrate:up
2+
3+-- Users table
4+CREATE TABLE users (
5+ id SERIAL PRIMARY KEY,
6+ fname VARCHAR(100) NOT NULL,
7+ lname VARCHAR(100) NOT NULL,
8+ email VARCHAR(255) UNIQUE NOT NULL,
9+ phone_no VARCHAR(20) UNIQUE NOT NULL,
10+ password VARCHAR(255) NOT NULL,
11+ wallet_address VARCHAR(42),
12+ state_id INTEGER,
13+ city_id INTEGER,
14+ pincode VARCHAR(10),
15+ user_type INTEGER DEFAULT 1,
16+ is_admin BOOLEAN DEFAULT FALSE,
17+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
18+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
19+);
20+
21+-- States table
22+CREATE TABLE states (
23+ id SERIAL PRIMARY KEY,
24+ name VARCHAR(100) UNIQUE NOT NULL,
25+ is_active BOOLEAN DEFAULT TRUE,
26+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
27+);
28+
29+-- Cities table
30+CREATE TABLE cities (
31+ id SERIAL PRIMARY KEY,
32+ name VARCHAR(100) NOT NULL,
33+ state_id INTEGER REFERENCES states(id) ON DELETE CASCADE,
34+ is_active BOOLEAN DEFAULT TRUE,
35+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
36+ UNIQUE(name, state_id)
37+);
38+
39+-- Property types table
40+CREATE TABLE property_types (
41+ id SERIAL PRIMARY KEY,
42+ title VARCHAR(100),
43+ type VARCHAR(20) NOT NULL CHECK (type IN ('residential', 'commercial', 'agricultural')),
44+ is_active BOOLEAN DEFAULT TRUE,
45+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
46+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
47+);
48+
49+-- Properties table
50+CREATE TABLE properties (
51+ id SERIAL PRIMARY KEY,
52+ title VARCHAR(255) NOT NULL,
53+ property_for VARCHAR(10) NOT NULL DEFAULT 'sell' CHECK (property_for IN ('sell', 'rent')),
54+ description TEXT,
55+ type_id INTEGER REFERENCES property_types(id),
56+ state_id INTEGER REFERENCES states(id),
57+ city_id INTEGER REFERENCES cities(id),
58+ locality VARCHAR(255) NOT NULL,
59+ length DECIMAL(10, 2) NOT NULL,
60+ breadth DECIMAL(10, 2) NOT NULL,
61+ corner_plot BOOLEAN DEFAULT FALSE,
62+ is_society BOOLEAN DEFAULT FALSE,
63+ society_name VARCHAR(255),
64+ flat_no VARCHAR(50),
65+ address TEXT NOT NULL,
66+ email VARCHAR(255) NOT NULL,
67+ price DECIMAL(18, 2),
68+ price_eth DECIMAL(18, 8),
69+ phone_no VARCHAR(20) NOT NULL,
70+ pincode VARCHAR(10) NOT NULL,
71+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
72+ status VARCHAR(20) DEFAULT 'available' CHECK (status IN ('available', 'sold', 'rented', 'expired')),
73+ is_active BOOLEAN DEFAULT TRUE,
74+ slug VARCHAR(255) UNIQUE NOT NULL,
75+ images TEXT[], -- Array of image URLs
76+ img_path VARCHAR(500),
77+ -- Blockchain fields
78+ contract_address VARCHAR(42),
79+ token_id VARCHAR(100),
80+ -- Investment metrics
81+ roi_percentage DECIMAL(5, 2),
82+ total_tokens INTEGER,
83+ available_tokens INTEGER,
84+ min_investment DECIMAL(18, 2),
85+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
86+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
87+);
88+
89+-- Blockchain transactions table (links properties to smart contracts)
90+CREATE TABLE blockchain_transactions (
91+ id SERIAL PRIMARY KEY,
92+ property_id INTEGER REFERENCES properties(id) ON DELETE CASCADE,
93+ contract_address VARCHAR(42) NOT NULL,
94+ tx_hash VARCHAR(66),
95+ tx_type VARCHAR(20) NOT NULL CHECK (tx_type IN ('deploy', 'seller_sign', 'buyer_deposit', 'realtor_review', 'finalize', 'withdraw')),
96+ from_address VARCHAR(42),
97+ to_address VARCHAR(42),
98+ value_wei VARCHAR(78),
99+ value_eth DECIMAL(18, 8),
100+ gas_used INTEGER,
101+ block_number INTEGER,
102+ contract_state INTEGER DEFAULT 0,
103+ status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'failed')),
104+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
105+);
106+
107+-- Property investors table (tracks who invested in what)
108+CREATE TABLE property_investors (
109+ id SERIAL PRIMARY KEY,
110+ property_id INTEGER REFERENCES properties(id) ON DELETE CASCADE,
111+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
112+ wallet_address VARCHAR(42) NOT NULL,
113+ tokens_owned INTEGER NOT NULL DEFAULT 0,
114+ investment_amount DECIMAL(18, 2),
115+ investment_eth DECIMAL(18, 8),
116+ tx_hash VARCHAR(66),
117+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
118+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
119+ UNIQUE(property_id, user_id)
120+);
121+
122+-- Add foreign key constraints for users
123+ALTER TABLE users
124+ ADD CONSTRAINT fk_users_state FOREIGN KEY (state_id) REFERENCES states(id),
125+ ADD CONSTRAINT fk_users_city FOREIGN KEY (city_id) REFERENCES cities(id);
126+
127+-- Indexes for performance
128+CREATE INDEX idx_properties_status ON properties(status);
129+CREATE INDEX idx_properties_user_id ON properties(user_id);
130+CREATE INDEX idx_properties_contract_address ON properties(contract_address);
131+CREATE INDEX idx_properties_slug ON properties(slug);
132+CREATE INDEX idx_blockchain_tx_contract ON blockchain_transactions(contract_address);
133+CREATE INDEX idx_blockchain_tx_property ON blockchain_transactions(property_id);
134+CREATE INDEX idx_investors_property ON property_investors(property_id);
135+CREATE INDEX idx_investors_wallet ON property_investors(wallet_address);
136+
137+-- migrate:down
138+
139+DROP TABLE IF EXISTS property_investors;
140+DROP TABLE IF EXISTS blockchain_transactions;
141+DROP TABLE IF EXISTS properties;
142+DROP TABLE IF EXISTS property_types;
143+DROP TABLE IF EXISTS cities;
144+DROP TABLE IF EXISTS users;
145+DROP TABLE IF EXISTS states;
+628-0db/schema.sql
@@ -0,0 +1,628 @@
1+\restrict dbmate
2+
3+-- Dumped from database version 16.9 (Debian 16.9-1.pgdg120+1)
4+-- Dumped by pg_dump version 18.1
5+
6+SET statement_timeout = 0;
7+SET lock_timeout = 0;
8+SET idle_in_transaction_session_timeout = 0;
9+SET transaction_timeout = 0;
10+SET client_encoding = 'UTF8';
11+SET standard_conforming_strings = on;
12+SELECT pg_catalog.set_config('search_path', '', false);
13+SET check_function_bodies = false;
14+SET xmloption = content;
15+SET client_min_messages = warning;
16+SET row_security = off;
17+
18+SET default_tablespace = '';
19+
20+SET default_table_access_method = heap;
21+
22+--
23+-- Name: blockchain_transactions; Type: TABLE; Schema: public; Owner: -
24+--
25+
26+CREATE TABLE public.blockchain_transactions (
27+ id integer NOT NULL,
28+ property_id integer,
29+ contract_address character varying(42) NOT NULL,
30+ tx_hash character varying(66),
31+ tx_type character varying(20) NOT NULL,
32+ from_address character varying(42),
33+ to_address character varying(42),
34+ value_wei character varying(78),
35+ value_eth numeric(18,8),
36+ gas_used integer,
37+ block_number integer,
38+ contract_state integer DEFAULT 0,
39+ status character varying(20) DEFAULT 'pending'::character varying,
40+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
41+ CONSTRAINT blockchain_transactions_status_check CHECK (((status)::text = ANY ((ARRAY['pending'::character varying, 'confirmed'::character varying, 'failed'::character varying])::text[]))),
42+ CONSTRAINT blockchain_transactions_tx_type_check CHECK (((tx_type)::text = ANY ((ARRAY['deploy'::character varying, 'seller_sign'::character varying, 'buyer_deposit'::character varying, 'realtor_review'::character varying, 'finalize'::character varying, 'withdraw'::character varying])::text[])))
43+);
44+
45+
46+--
47+-- Name: blockchain_transactions_id_seq; Type: SEQUENCE; Schema: public; Owner: -
48+--
49+
50+CREATE SEQUENCE public.blockchain_transactions_id_seq
51+ AS integer
52+ START WITH 1
53+ INCREMENT BY 1
54+ NO MINVALUE
55+ NO MAXVALUE
56+ CACHE 1;
57+
58+
59+--
60+-- Name: blockchain_transactions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
61+--
62+
63+ALTER SEQUENCE public.blockchain_transactions_id_seq OWNED BY public.blockchain_transactions.id;
64+
65+
66+--
67+-- Name: cities; Type: TABLE; Schema: public; Owner: -
68+--
69+
70+CREATE TABLE public.cities (
71+ id integer NOT NULL,
72+ name character varying(100) NOT NULL,
73+ state_id integer,
74+ is_active boolean DEFAULT true,
75+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
76+);
77+
78+
79+--
80+-- Name: cities_id_seq; Type: SEQUENCE; Schema: public; Owner: -
81+--
82+
83+CREATE SEQUENCE public.cities_id_seq
84+ AS integer
85+ START WITH 1
86+ INCREMENT BY 1
87+ NO MINVALUE
88+ NO MAXVALUE
89+ CACHE 1;
90+
91+
92+--
93+-- Name: cities_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
94+--
95+
96+ALTER SEQUENCE public.cities_id_seq OWNED BY public.cities.id;
97+
98+
99+--
100+-- Name: properties; Type: TABLE; Schema: public; Owner: -
101+--
102+
103+CREATE TABLE public.properties (
104+ id integer NOT NULL,
105+ title character varying(255) NOT NULL,
106+ property_for character varying(10) DEFAULT 'sell'::character varying NOT NULL,
107+ description text,
108+ type_id integer,
109+ state_id integer,
110+ city_id integer,
111+ locality character varying(255) NOT NULL,
112+ length numeric(10,2) NOT NULL,
113+ breadth numeric(10,2) NOT NULL,
114+ corner_plot boolean DEFAULT false,
115+ is_society boolean DEFAULT false,
116+ society_name character varying(255),
117+ flat_no character varying(50),
118+ address text NOT NULL,
119+ email character varying(255) NOT NULL,
120+ price numeric(18,2),
121+ price_eth numeric(18,8),
122+ phone_no character varying(20) NOT NULL,
123+ pincode character varying(10) NOT NULL,
124+ user_id integer,
125+ status character varying(20) DEFAULT 'available'::character varying,
126+ is_active boolean DEFAULT true,
127+ slug character varying(255) NOT NULL,
128+ images text[],
129+ img_path character varying(500),
130+ contract_address character varying(42),
131+ token_id character varying(100),
132+ roi_percentage numeric(5,2),
133+ total_tokens integer,
134+ available_tokens integer,
135+ min_investment numeric(18,2),
136+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
137+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
138+ CONSTRAINT properties_property_for_check CHECK (((property_for)::text = ANY ((ARRAY['sell'::character varying, 'rent'::character varying])::text[]))),
139+ CONSTRAINT properties_status_check CHECK (((status)::text = ANY ((ARRAY['available'::character varying, 'sold'::character varying, 'rented'::character varying, 'expired'::character varying])::text[])))
140+);
141+
142+
143+--
144+-- Name: properties_id_seq; Type: SEQUENCE; Schema: public; Owner: -
145+--
146+
147+CREATE SEQUENCE public.properties_id_seq
148+ AS integer
149+ START WITH 1
150+ INCREMENT BY 1
151+ NO MINVALUE
152+ NO MAXVALUE
153+ CACHE 1;
154+
155+
156+--
157+-- Name: properties_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
158+--
159+
160+ALTER SEQUENCE public.properties_id_seq OWNED BY public.properties.id;
161+
162+
163+--
164+-- Name: property_investors; Type: TABLE; Schema: public; Owner: -
165+--
166+
167+CREATE TABLE public.property_investors (
168+ id integer NOT NULL,
169+ property_id integer,
170+ user_id integer,
171+ wallet_address character varying(42) NOT NULL,
172+ tokens_owned integer DEFAULT 0 NOT NULL,
173+ investment_amount numeric(18,2),
174+ investment_eth numeric(18,8),
175+ tx_hash character varying(66),
176+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
177+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
178+);
179+
180+
181+--
182+-- Name: property_investors_id_seq; Type: SEQUENCE; Schema: public; Owner: -
183+--
184+
185+CREATE SEQUENCE public.property_investors_id_seq
186+ AS integer
187+ START WITH 1
188+ INCREMENT BY 1
189+ NO MINVALUE
190+ NO MAXVALUE
191+ CACHE 1;
192+
193+
194+--
195+-- Name: property_investors_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
196+--
197+
198+ALTER SEQUENCE public.property_investors_id_seq OWNED BY public.property_investors.id;
199+
200+
201+--
202+-- Name: property_types; Type: TABLE; Schema: public; Owner: -
203+--
204+
205+CREATE TABLE public.property_types (
206+ id integer NOT NULL,
207+ title character varying(100),
208+ type character varying(20) NOT NULL,
209+ is_active boolean DEFAULT true,
210+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
211+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
212+ CONSTRAINT property_types_type_check CHECK (((type)::text = ANY ((ARRAY['residential'::character varying, 'commercial'::character varying, 'agricultural'::character varying])::text[])))
213+);
214+
215+
216+--
217+-- Name: property_types_id_seq; Type: SEQUENCE; Schema: public; Owner: -
218+--
219+
220+CREATE SEQUENCE public.property_types_id_seq
221+ AS integer
222+ START WITH 1
223+ INCREMENT BY 1
224+ NO MINVALUE
225+ NO MAXVALUE
226+ CACHE 1;
227+
228+
229+--
230+-- Name: property_types_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
231+--
232+
233+ALTER SEQUENCE public.property_types_id_seq OWNED BY public.property_types.id;
234+
235+
236+--
237+-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: -
238+--
239+
240+CREATE TABLE public.schema_migrations (
241+ version character varying NOT NULL
242+);
243+
244+
245+--
246+-- Name: states; Type: TABLE; Schema: public; Owner: -
247+--
248+
249+CREATE TABLE public.states (
250+ id integer NOT NULL,
251+ name character varying(100) NOT NULL,
252+ is_active boolean DEFAULT true,
253+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
254+);
255+
256+
257+--
258+-- Name: states_id_seq; Type: SEQUENCE; Schema: public; Owner: -
259+--
260+
261+CREATE SEQUENCE public.states_id_seq
262+ AS integer
263+ START WITH 1
264+ INCREMENT BY 1
265+ NO MINVALUE
266+ NO MAXVALUE
267+ CACHE 1;
268+
269+
270+--
271+-- Name: states_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
272+--
273+
274+ALTER SEQUENCE public.states_id_seq OWNED BY public.states.id;
275+
276+
277+--
278+-- Name: users; Type: TABLE; Schema: public; Owner: -
279+--
280+
281+CREATE TABLE public.users (
282+ id integer NOT NULL,
283+ fname character varying(100) NOT NULL,
284+ lname character varying(100) NOT NULL,
285+ email character varying(255) NOT NULL,
286+ phone_no character varying(20) NOT NULL,
287+ password character varying(255) NOT NULL,
288+ wallet_address character varying(42),
289+ state_id integer,
290+ city_id integer,
291+ pincode character varying(10),
292+ user_type integer DEFAULT 1,
293+ is_admin boolean DEFAULT false,
294+ created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
295+ updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
296+);
297+
298+
299+--
300+-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: -
301+--
302+
303+CREATE SEQUENCE public.users_id_seq
304+ AS integer
305+ START WITH 1
306+ INCREMENT BY 1
307+ NO MINVALUE
308+ NO MAXVALUE
309+ CACHE 1;
310+
311+
312+--
313+-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
314+--
315+
316+ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id;
317+
318+
319+--
320+-- Name: blockchain_transactions id; Type: DEFAULT; Schema: public; Owner: -
321+--
322+
323+ALTER TABLE ONLY public.blockchain_transactions ALTER COLUMN id SET DEFAULT nextval('public.blockchain_transactions_id_seq'::regclass);
324+
325+
326+--
327+-- Name: cities id; Type: DEFAULT; Schema: public; Owner: -
328+--
329+
330+ALTER TABLE ONLY public.cities ALTER COLUMN id SET DEFAULT nextval('public.cities_id_seq'::regclass);
331+
332+
333+--
334+-- Name: properties id; Type: DEFAULT; Schema: public; Owner: -
335+--
336+
337+ALTER TABLE ONLY public.properties ALTER COLUMN id SET DEFAULT nextval('public.properties_id_seq'::regclass);
338+
339+
340+--
341+-- Name: property_investors id; Type: DEFAULT; Schema: public; Owner: -
342+--
343+
344+ALTER TABLE ONLY public.property_investors ALTER COLUMN id SET DEFAULT nextval('public.property_investors_id_seq'::regclass);
345+
346+
347+--
348+-- Name: property_types id; Type: DEFAULT; Schema: public; Owner: -
349+--
350+
351+ALTER TABLE ONLY public.property_types ALTER COLUMN id SET DEFAULT nextval('public.property_types_id_seq'::regclass);
352+
353+
354+--
355+-- Name: states id; Type: DEFAULT; Schema: public; Owner: -
356+--
357+
358+ALTER TABLE ONLY public.states ALTER COLUMN id SET DEFAULT nextval('public.states_id_seq'::regclass);
359+
360+
361+--
362+-- Name: users id; Type: DEFAULT; Schema: public; Owner: -
363+--
364+
365+ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass);
366+
367+
368+--
369+-- Name: blockchain_transactions blockchain_transactions_pkey; Type: CONSTRAINT; Schema: public; Owner: -
370+--
371+
372+ALTER TABLE ONLY public.blockchain_transactions
373+ ADD CONSTRAINT blockchain_transactions_pkey PRIMARY KEY (id);
374+
375+
376+--
377+-- Name: cities cities_name_state_id_key; Type: CONSTRAINT; Schema: public; Owner: -
378+--
379+
380+ALTER TABLE ONLY public.cities
381+ ADD CONSTRAINT cities_name_state_id_key UNIQUE (name, state_id);
382+
383+
384+--
385+-- Name: cities cities_pkey; Type: CONSTRAINT; Schema: public; Owner: -
386+--
387+
388+ALTER TABLE ONLY public.cities
389+ ADD CONSTRAINT cities_pkey PRIMARY KEY (id);
390+
391+
392+--
393+-- Name: properties properties_pkey; Type: CONSTRAINT; Schema: public; Owner: -
394+--
395+
396+ALTER TABLE ONLY public.properties
397+ ADD CONSTRAINT properties_pkey PRIMARY KEY (id);
398+
399+
400+--
401+-- Name: properties properties_slug_key; Type: CONSTRAINT; Schema: public; Owner: -
402+--
403+
404+ALTER TABLE ONLY public.properties
405+ ADD CONSTRAINT properties_slug_key UNIQUE (slug);
406+
407+
408+--
409+-- Name: property_investors property_investors_pkey; Type: CONSTRAINT; Schema: public; Owner: -
410+--
411+
412+ALTER TABLE ONLY public.property_investors
413+ ADD CONSTRAINT property_investors_pkey PRIMARY KEY (id);
414+
415+
416+--
417+-- Name: property_investors property_investors_property_id_user_id_key; Type: CONSTRAINT; Schema: public; Owner: -
418+--
419+
420+ALTER TABLE ONLY public.property_investors
421+ ADD CONSTRAINT property_investors_property_id_user_id_key UNIQUE (property_id, user_id);
422+
423+
424+--
425+-- Name: property_types property_types_pkey; Type: CONSTRAINT; Schema: public; Owner: -
426+--
427+
428+ALTER TABLE ONLY public.property_types
429+ ADD CONSTRAINT property_types_pkey PRIMARY KEY (id);
430+
431+
432+--
433+-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: -
434+--
435+
436+ALTER TABLE ONLY public.schema_migrations
437+ ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version);
438+
439+
440+--
441+-- Name: states states_name_key; Type: CONSTRAINT; Schema: public; Owner: -
442+--
443+
444+ALTER TABLE ONLY public.states
445+ ADD CONSTRAINT states_name_key UNIQUE (name);
446+
447+
448+--
449+-- Name: states states_pkey; Type: CONSTRAINT; Schema: public; Owner: -
450+--
451+
452+ALTER TABLE ONLY public.states
453+ ADD CONSTRAINT states_pkey PRIMARY KEY (id);
454+
455+
456+--
457+-- Name: users users_email_key; Type: CONSTRAINT; Schema: public; Owner: -
458+--
459+
460+ALTER TABLE ONLY public.users
461+ ADD CONSTRAINT users_email_key UNIQUE (email);
462+
463+
464+--
465+-- Name: users users_phone_no_key; Type: CONSTRAINT; Schema: public; Owner: -
466+--
467+
468+ALTER TABLE ONLY public.users
469+ ADD CONSTRAINT users_phone_no_key UNIQUE (phone_no);
470+
471+
472+--
473+-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
474+--
475+
476+ALTER TABLE ONLY public.users
477+ ADD CONSTRAINT users_pkey PRIMARY KEY (id);
478+
479+
480+--
481+-- Name: idx_blockchain_tx_contract; Type: INDEX; Schema: public; Owner: -
482+--
483+
484+CREATE INDEX idx_blockchain_tx_contract ON public.blockchain_transactions USING btree (contract_address);
485+
486+
487+--
488+-- Name: idx_blockchain_tx_property; Type: INDEX; Schema: public; Owner: -
489+--
490+
491+CREATE INDEX idx_blockchain_tx_property ON public.blockchain_transactions USING btree (property_id);
492+
493+
494+--
495+-- Name: idx_investors_property; Type: INDEX; Schema: public; Owner: -
496+--
497+
498+CREATE INDEX idx_investors_property ON public.property_investors USING btree (property_id);
499+
500+
501+--
502+-- Name: idx_investors_wallet; Type: INDEX; Schema: public; Owner: -
503+--
504+
505+CREATE INDEX idx_investors_wallet ON public.property_investors USING btree (wallet_address);
506+
507+
508+--
509+-- Name: idx_properties_contract_address; Type: INDEX; Schema: public; Owner: -
510+--
511+
512+CREATE INDEX idx_properties_contract_address ON public.properties USING btree (contract_address);
513+
514+
515+--
516+-- Name: idx_properties_slug; Type: INDEX; Schema: public; Owner: -
517+--
518+
519+CREATE INDEX idx_properties_slug ON public.properties USING btree (slug);
520+
521+
522+--
523+-- Name: idx_properties_status; Type: INDEX; Schema: public; Owner: -
524+--
525+
526+CREATE INDEX idx_properties_status ON public.properties USING btree (status);
527+
528+
529+--
530+-- Name: idx_properties_user_id; Type: INDEX; Schema: public; Owner: -
531+--
532+
533+CREATE INDEX idx_properties_user_id ON public.properties USING btree (user_id);
534+
535+
536+--
537+-- Name: blockchain_transactions blockchain_transactions_property_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
538+--
539+
540+ALTER TABLE ONLY public.blockchain_transactions
541+ ADD CONSTRAINT blockchain_transactions_property_id_fkey FOREIGN KEY (property_id) REFERENCES public.properties(id) ON DELETE CASCADE;
542+
543+
544+--
545+-- Name: cities cities_state_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
546+--
547+
548+ALTER TABLE ONLY public.cities
549+ ADD CONSTRAINT cities_state_id_fkey FOREIGN KEY (state_id) REFERENCES public.states(id) ON DELETE CASCADE;
550+
551+
552+--
553+-- Name: users fk_users_city; Type: FK CONSTRAINT; Schema: public; Owner: -
554+--
555+
556+ALTER TABLE ONLY public.users
557+ ADD CONSTRAINT fk_users_city FOREIGN KEY (city_id) REFERENCES public.cities(id);
558+
559+
560+--
561+-- Name: users fk_users_state; Type: FK CONSTRAINT; Schema: public; Owner: -
562+--
563+
564+ALTER TABLE ONLY public.users
565+ ADD CONSTRAINT fk_users_state FOREIGN KEY (state_id) REFERENCES public.states(id);
566+
567+
568+--
569+-- Name: properties properties_city_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
570+--
571+
572+ALTER TABLE ONLY public.properties
573+ ADD CONSTRAINT properties_city_id_fkey FOREIGN KEY (city_id) REFERENCES public.cities(id);
574+
575+
576+--
577+-- Name: properties properties_state_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
578+--
579+
580+ALTER TABLE ONLY public.properties
581+ ADD CONSTRAINT properties_state_id_fkey FOREIGN KEY (state_id) REFERENCES public.states(id);
582+
583+
584+--
585+-- Name: properties properties_type_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
586+--
587+
588+ALTER TABLE ONLY public.properties
589+ ADD CONSTRAINT properties_type_id_fkey FOREIGN KEY (type_id) REFERENCES public.property_types(id);
590+
591+
592+--
593+-- Name: properties properties_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
594+--
595+
596+ALTER TABLE ONLY public.properties
597+ ADD CONSTRAINT properties_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
598+
599+
600+--
601+-- Name: property_investors property_investors_property_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
602+--
603+
604+ALTER TABLE ONLY public.property_investors
605+ ADD CONSTRAINT property_investors_property_id_fkey FOREIGN KEY (property_id) REFERENCES public.properties(id) ON DELETE CASCADE;
606+
607+
608+--
609+-- Name: property_investors property_investors_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
610+--
611+
612+ALTER TABLE ONLY public.property_investors
613+ ADD CONSTRAINT property_investors_user_id_fkey FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
614+
615+
616+--
617+-- PostgreSQL database dump complete
618+--
619+
620+\unrestrict dbmate
621+
622+
623+--
624+-- Dbmate schema migrations
625+--
626+
627+INSERT INTO public.schema_migrations (version) VALUES
628+ ('20260128202118');
+138-0package-lock.json
@@ -39,6 +39,7 @@
3939 "morgan": "^1.10.1",
4040 "multer": "^2.0.2",
4141 "paytmchecksum": "^1.5.1",
42+ "pg": "^8.17.2",
4243 "react": "^18.3.1",
4344 "react-app-rewired": "^2.2.1",
4445 "react-chartjs-2": "^5.2.0",
@@ -24864,6 +24865,95 @@
2486424865 "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
2486524866 "license": "MIT"
2486624867 },
24868+ "node_modules/pg": {
24869+ "version": "8.17.2",
24870+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.17.2.tgz",
24871+ "integrity": "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw==",
24872+ "license": "MIT",
24873+ "dependencies": {
24874+ "pg-connection-string": "^2.10.1",
24875+ "pg-pool": "^3.11.0",
24876+ "pg-protocol": "^1.11.0",
24877+ "pg-types": "2.2.0",
24878+ "pgpass": "1.0.5"
24879+ },
24880+ "engines": {
24881+ "node": ">= 16.0.0"
24882+ },
24883+ "optionalDependencies": {
24884+ "pg-cloudflare": "^1.3.0"
24885+ },
24886+ "peerDependencies": {
24887+ "pg-native": ">=3.0.1"
24888+ },
24889+ "peerDependenciesMeta": {
24890+ "pg-native": {
24891+ "optional": true
24892+ }
24893+ }
24894+ },
24895+ "node_modules/pg-cloudflare": {
24896+ "version": "1.3.0",
24897+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
24898+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
24899+ "license": "MIT",
24900+ "optional": true
24901+ },
24902+ "node_modules/pg-connection-string": {
24903+ "version": "2.10.1",
24904+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.10.1.tgz",
24905+ "integrity": "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw==",
24906+ "license": "MIT"
24907+ },
24908+ "node_modules/pg-int8": {
24909+ "version": "1.0.1",
24910+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
24911+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
24912+ "license": "ISC",
24913+ "engines": {
24914+ "node": ">=4.0.0"
24915+ }
24916+ },
24917+ "node_modules/pg-pool": {
24918+ "version": "3.11.0",
24919+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
24920+ "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
24921+ "license": "MIT",
24922+ "peerDependencies": {
24923+ "pg": ">=8.0"
24924+ }
24925+ },
24926+ "node_modules/pg-protocol": {
24927+ "version": "1.11.0",
24928+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
24929+ "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
24930+ "license": "MIT"
24931+ },
24932+ "node_modules/pg-types": {
24933+ "version": "2.2.0",
24934+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
24935+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
24936+ "license": "MIT",
24937+ "dependencies": {
24938+ "pg-int8": "1.0.1",
24939+ "postgres-array": "~2.0.0",
24940+ "postgres-bytea": "~1.0.0",
24941+ "postgres-date": "~1.0.4",
24942+ "postgres-interval": "^1.1.0"
24943+ },
24944+ "engines": {
24945+ "node": ">=4"
24946+ }
24947+ },
24948+ "node_modules/pgpass": {
24949+ "version": "1.0.5",
24950+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
24951+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
24952+ "license": "MIT",
24953+ "dependencies": {
24954+ "split2": "^4.1.0"
24955+ }
24956+ },
2486724957 "node_modules/picocolors": {
2486824958 "version": "1.1.1",
2486924959 "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -26269,6 +26359,45 @@
2626926359 "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
2627026360 "license": "MIT"
2627126361 },
26362+ "node_modules/postgres-array": {
26363+ "version": "2.0.0",
26364+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
26365+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
26366+ "license": "MIT",
26367+ "engines": {
26368+ "node": ">=4"
26369+ }
26370+ },
26371+ "node_modules/postgres-bytea": {
26372+ "version": "1.0.1",
26373+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
26374+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
26375+ "license": "MIT",
26376+ "engines": {
26377+ "node": ">=0.10.0"
26378+ }
26379+ },
26380+ "node_modules/postgres-date": {
26381+ "version": "1.0.7",
26382+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
26383+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
26384+ "license": "MIT",
26385+ "engines": {
26386+ "node": ">=0.10.0"
26387+ }
26388+ },
26389+ "node_modules/postgres-interval": {
26390+ "version": "1.2.0",
26391+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
26392+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
26393+ "license": "MIT",
26394+ "dependencies": {
26395+ "xtend": "^4.0.0"
26396+ },
26397+ "engines": {
26398+ "node": ">=0.10.0"
26399+ }
26400+ },
2627226401 "node_modules/potpack": {
2627326402 "version": "1.0.2",
2627426403 "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
@@ -29267,6 +29396,15 @@
2926729396 "node": ">=0.10.0"
2926829397 }
2926929398 },
29399+ "node_modules/split2": {
29400+ "version": "4.2.0",
29401+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
29402+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
29403+ "license": "ISC",
29404+ "engines": {
29405+ "node": ">= 10.x"
29406+ }
29407+ },
2927029408 "node_modules/sprintf-js": {
2927129409 "version": "1.0.3",
2927229410 "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+1-0package.json
@@ -34,6 +34,7 @@
3434 "morgan": "^1.10.1",
3535 "multer": "^2.0.2",
3636 "paytmchecksum": "^1.5.1",
37+ "pg": "^8.17.2",
3738 "react": "^18.3.1",
3839 "react-app-rewired": "^2.2.1",
3940 "react-chartjs-2": "^5.2.0",
+267-43scripts/deploy.js
@@ -1,22 +1,268 @@
1+require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
2+
13 const hre = require("hardhat");
4+const { execSync } = require("child_process");
5+const { Pool } = require("pg");
6+
7+// Sample data
8+const SAMPLE_PROPERTIES = [
9+ {
10+ title: "Luxury Downtown Apartment",
11+ propertyFor: "sell",
12+ description: "Stunning modern apartment in the heart of downtown with panoramic city views. Features high-end finishes, smart home technology, and access to premium amenities.",
13+ locality: "Financial District",
14+ address: "123 Main Street, Apt 45",
15+ price: 850000,
16+ priceEth: 425,
17+ roiPercentage: 7.2,
18+ totalTokens: 85000,
19+ availableTokens: 9350,
20+ minInvestment: 10,
21+ images: ["https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80"],
22+ },
23+ {
24+ title: "Modern Tech District Complex",
25+ propertyFor: "sell",
26+ description: "Prime commercial space in the booming tech corridor. Perfect for startups and established companies alike. High foot traffic and excellent public transit access.",
27+ locality: "Tech Hub",
28+ address: "456 Innovation Boulevard",
29+ price: 1200000,
30+ priceEth: 600,
31+ roiPercentage: 6.8,
32+ totalTokens: 120000,
33+ availableTokens: 6000,
34+ minInvestment: 10,
35+ images: ["https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80"],
36+ },
37+ {
38+ title: "Waterfront Commercial Space",
39+ propertyFor: "sell",
40+ description: "Exceptional waterfront property with direct water access. Ideal for hospitality or retail ventures. Breathtaking sunset views and ample parking.",
41+ locality: "Harbor District",
42+ address: "789 Waterfront Drive",
43+ price: 2100000,
44+ priceEth: 1050,
45+ roiPercentage: 7.5,
46+ totalTokens: 210000,
47+ availableTokens: 115500,
48+ minInvestment: 10,
49+ images: ["https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800&q=80"],
50+ },
51+ {
52+ title: "Historic Victorian Estate",
53+ propertyFor: "sell",
54+ description: "Beautifully restored Victorian mansion with original architectural details. Features 6 bedrooms, 4 bathrooms, and a sprawling garden. Perfect blend of historic charm and modern comfort.",
55+ locality: "Heritage Hills",
56+ address: "321 Oak Avenue",
57+ price: 1750000,
58+ priceEth: 875,
59+ roiPercentage: 5.9,
60+ totalTokens: 175000,
61+ availableTokens: 52500,
62+ minInvestment: 10,
63+ images: ["https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=800&q=80"],
64+ },
65+ {
66+ title: "Beachfront Vacation Rental",
67+ propertyFor: "rent",
68+ description: "Stunning beachfront property generating consistent rental income. Fully furnished with luxury amenities. Strong booking history and established vacation rental business.",
69+ locality: "Coastal Paradise",
70+ address: "555 Ocean View Lane",
71+ price: 950000,
72+ priceEth: 475,
73+ roiPercentage: 8.4,
74+ totalTokens: 95000,
75+ availableTokens: 28500,
76+ minInvestment: 10,
77+ images: ["https://images.unsplash.com/photo-1499793983690-e29da59ef1c2?w=800&q=80"],
78+ },
79+];
80+
81+async function resetDatabase() {
82+ console.log("=== RESETTING DATABASE ===\n");
83+
84+ try {
85+ console.log("Dropping database...");
86+ execSync("dbmate drop", { stdio: "inherit" });
87+ } catch (e) {
88+ // Database might not exist yet, that's ok
89+ console.log("(Database didn't exist, continuing...)");
90+ }
91+
92+ console.log("Running migrations...");
93+ execSync("dbmate up", { stdio: "inherit" });
94+
95+ console.log("Database reset complete.\n");
96+}
97+
98+async function seedDatabase(wallets) {
99+ console.log("=== SEEDING DATABASE ===\n");
100+
101+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
102+
103+ try {
104+ // Create states
105+ const stateResult = await pool.query(
106+ "INSERT INTO states (name) VALUES ('California'), ('Texas'), ('Washington') RETURNING id, name"
107+ );
108+ const states = stateResult.rows;
109+ console.log(`Created ${states.length} states`);
110+
111+ // Create cities
112+ const cityResult = await pool.query(`
113+ INSERT INTO cities (name, state_id) VALUES
114+ ('Los Angeles', $1), ('San Francisco', $1), ('Beverly Hills', $1),
115+ ('Austin', $2), ('Houston', $2),
116+ ('Seattle', $3)
117+ RETURNING id, name, state_id
118+ `, [states[0].id, states[1].id, states[2].id]);
119+ const cities = cityResult.rows;
120+ console.log(`Created ${cities.length} cities`);
121+
122+ // Create property types
123+ const typeResult = await pool.query(`
124+ INSERT INTO property_types (title, type) VALUES
125+ ('Apartment', 'residential'),
126+ ('House', 'residential'),
127+ ('Commercial Building', 'commercial'),
128+ ('Office Space', 'commercial'),
129+ ('Farm', 'agricultural')
130+ RETURNING id, title
131+ `);
132+ const propertyTypes = typeResult.rows;
133+ console.log(`Created ${propertyTypes.length} property types`);
134+
135+ // Create test users linked to wallets
136+ const [deployer, realtor, seller, buyer] = wallets;
137+
138+ const userResult = await pool.query(`
139+ INSERT INTO users (fname, lname, email, phone_no, password, wallet_address, is_admin) VALUES
140+ ('Admin', 'User', 'admin@goldenprop.com', '555-0001', '$2b$10$test', $1, true),
141+ ('Rachel', 'Realtor', 'realtor@goldenprop.com', '555-0002', '$2b$10$test', $2, false),
142+ ('Sam', 'Seller', 'seller@goldenprop.com', '555-0003', '$2b$10$test', $3, false),
143+ ('Bob', 'Buyer', 'buyer@goldenprop.com', '555-0004', '$2b$10$test', $4, false)
144+ RETURNING id, fname, lname, wallet_address
145+ `, [
146+ deployer.address.toLowerCase(),
147+ realtor.address.toLowerCase(),
148+ seller.address.toLowerCase(),
149+ buyer.address.toLowerCase()
150+ ]);
151+ const users = userResult.rows;
152+ console.log(`Created ${users.length} users`);
153+
154+ console.table(users.map(u => ({
155+ name: `${u.fname} ${u.lname}`,
156+ wallet: u.wallet_address
157+ })));
158+
159+ // Create properties
160+ const propertyIds = [];
161+ for (let i = 0; i < SAMPLE_PROPERTIES.length; i++) {
162+ const p = SAMPLE_PROPERTIES[i];
163+ const cityId = cities[i % cities.length].id;
164+ const stateId = states[i % states.length].id;
165+ const typeId = propertyTypes[i % propertyTypes.length].id;
166+ const slug = p.title.toLowerCase().replace(/\s+/g, '-');
167+
168+ const result = await pool.query(`
169+ INSERT INTO properties (
170+ title, property_for, description, type_id, state_id, city_id,
171+ locality, length, breadth, address, email, price, price_eth,
172+ phone_no, pincode, user_id, slug, images, roi_percentage,
173+ total_tokens, available_tokens, min_investment, status
174+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, 'available')
175+ RETURNING id, title
176+ `, [
177+ p.title, p.propertyFor, p.description, typeId, stateId, cityId,
178+ p.locality, 100, 100, p.address, 'contact@goldenprop.com', p.price, p.priceEth,
179+ '555-0000', '90210', users[1].id, slug, p.images, p.roiPercentage,
180+ p.totalTokens, p.availableTokens, p.minInvestment
181+ ]);
182+
183+ propertyIds.push(result.rows[0].id);
184+ console.log(`Created property: ${result.rows[0].title}`);
185+ }
186+
187+ await pool.end();
188+ return { users, propertyIds };
189+
190+ } catch (error) {
191+ await pool.end();
192+ throw error;
193+ }
194+}
195+
196+async function deployContracts(wallets, propertyIds) {
197+ console.log("\n=== DEPLOYING SMART CONTRACTS ===\n");
198+
199+ const [deployer, realtor, seller, buyer] = wallets;
200+
201+ console.log("Deploying with account:", deployer.address);
202+ console.log("Account balance:", hre.ethers.utils.formatEther(await deployer.getBalance()), "ETH");
203+
204+ // Deploy Factory
205+ const Factory = await hre.ethers.getContractFactory("Factory", deployer);
206+ const factory = await Factory.deploy();
207+ await factory.deployed();
208+ console.log("\nFactory deployed to:", factory.address);
209+
210+ // Create contracts for each property and link to database
211+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
212+ const contractAddresses = [];
213+
214+ for (let i = 0; i < SAMPLE_PROPERTIES.length; i++) {
215+ const p = SAMPLE_PROPERTIES[i];
216+ const price = hre.ethers.utils.parseEther(p.priceEth.toString());
217+ const realtorFee = price.mul(5).div(100); // 5% fee
218+
219+ const tx = await factory.connect(realtor).create(
220+ p.address,
221+ "90210",
222+ p.locality,
223+ realtorFee,
224+ price,
225+ seller.address,
226+ buyer.address
227+ );
228+ await tx.wait();
229+
230+ const contractAddress = await factory.getInstance(i);
231+ contractAddresses.push(contractAddress);
232+
233+ // Link contract to property in database
234+ await pool.query(
235+ "UPDATE properties SET contract_address = $1 WHERE id = $2",
236+ [contractAddress.toLowerCase(), propertyIds[i]]
237+ );
238+
239+ // Record deployment transaction
240+ await pool.query(`
241+ INSERT INTO blockchain_transactions
242+ (property_id, contract_address, tx_type, from_address, status)
243+ VALUES ($1, $2, 'deploy', $3, 'confirmed')
244+ `, [propertyIds[i], contractAddress.toLowerCase(), realtor.address.toLowerCase()]);
245+
246+ console.log(`Property ${i + 1}: ${p.title}`);
247+ console.log(` Contract: ${contractAddress}`);
248+ }
249+
250+ await pool.end();
251+ return { factory, contractAddresses };
252+}
2253
3254 async function main() {
4255 const mnemonic = "test test test test test test test test test test test junk";
5256
6- // Derive wallets from mnemonic (so we have access to private keys)
257+ // Derive wallets
7258 const roles = ["Deployer", "Realtor", "Seller", "Buyer"];
8259 const wallets = roles.map((_, i) => {
9260 const path = `m/44'/60'/0'/0/${i}`;
10261 return hre.ethers.Wallet.fromMnemonic(mnemonic, path).connect(hre.ethers.provider);
11262 });
12263
13- const [deployer, realtor, seller, buyer] = wallets;
14-
15- // Show how keys are derived
16264 console.log("=== KEY DERIVATION ===\n");
17- console.log("Hardhat's hardcoded mnemonic:");
18- console.log(` "${mnemonic}"\n`);
19- console.log("Deriving accounts using BIP-44 path: m/44'/60'/0'/0/{index}\n");
265+ console.log(`Mnemonic: "${mnemonic}"\n`);
20266
21267 console.table(wallets.map((w, i) => ({
22268 role: roles[i],
@@ -25,48 +271,26 @@ async function main() {
25271 privateKey: w.privateKey
26272 })));
27273
28- console.log("\n=== DEPLOYING CONTRACTS ===\n");
29- console.log("Deploying with account:", deployer.address);
30- console.log("Account balance:", hre.ethers.utils.formatEther(await deployer.getBalance()), "ETH");
274+ // Reset and seed database
275+ await resetDatabase();
276+ const { users, propertyIds } = await seedDatabase(wallets);
31277
32- // Deploy Factory
33- const Factory = await hre.ethers.getContractFactory("Factory", deployer);
34- const factory = await Factory.deploy();
35- await factory.deployed();
278+ // Deploy contracts
279+ const { factory, contractAddresses } = await deployContracts(wallets, propertyIds);
36280
37281 console.log("\n=== DEPLOYMENT COMPLETE ===\n");
38- console.log("Factory deployed to:", factory.address);
39-
40- // Create a sample property transaction for testing
41- const price = hre.ethers.utils.parseEther("1"); // 1 ETH
42- const realtorFee = hre.ethers.utils.parseEther("0.05"); // 0.05 ETH
43-
44- const tx = await factory.connect(realtor).create(
45- "123 Test Street",
46- "90210",
47- "Beverly Hills",
48- realtorFee,
49- price,
50- seller.address,
51- buyer.address
52- );
53- await tx.wait();
54-
55- const propertyContract = await factory.getInstance(0);
56- console.log("Sample HomeTransaction deployed to:", propertyContract);
57-
58- console.log("\n=== CONTRACT ADDRESSES ===\n");
282+ console.log("Factory:", factory.address);
283+ console.log("Properties with contracts:", contractAddresses.length);
284+
285+ console.log("\n=== ENVIRONMENT VARIABLES ===\n");
59286 console.log(`REACT_APP_FACTORY_ADDRESS=${factory.address}`);
60- console.log(`REACT_APP_SAMPLE_PROPERTY=${propertyContract}`);
61287
62288 console.log("\n=== METAMASK SETUP ===\n");
63- console.log("1. Open MetaMask → Settings → Networks → Add Network");
64- console.log("2. Enter these details:");
65- console.log(" Network Name: Hardhat Local");
66- console.log(" RPC URL: http://127.0.0.1:8545");
67- console.log(" Chain ID: 31337");
68- console.log(" Currency Symbol: ETH");
69- console.log("\n3. Import accounts using the private keys above");
289+ console.log("Network Name: Hardhat Local");
290+ console.log("RPC URL: http://127.0.0.1:8545");
291+ console.log("Chain ID: 31337");
292+ console.log("Currency: ETH");
293+ console.log("\nImport accounts using private keys from table above");
70294 }
71295
72296 main()
+5-5server/app.js
@@ -1,12 +1,11 @@
1+require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
2+
13 const express = require('express');
24 const path = require('path');
3-const mongoose = require('mongoose');
45 const morgan = require('morgan');
56 const cors = require('cors');
67 const http = require('http');
7-const bodyParser = require('body-parser');
8-const config = require('./config/config');
9-require('dotenv').config();
8+const db = require('./db');
109
1110 const app = express();
1211 const server = http.createServer(app);
@@ -18,7 +17,8 @@ const corsOptions = {
1817 };
1918
2019 app.use(express.static(path.join(__dirname, 'uploads')));
21-
20+app.use(express.json());
21+app.use(express.urlencoded({ extended: true }));
2222 app.use(cors(corsOptions));
2323
2424 // Routing
+245-178server/controllers/property.controller.js
@@ -1,181 +1,248 @@
1-const mongoose = require('mongoose');
2-const moment = require('moment');
3-const fs = require('fs');
4-var Grid = require('gridfs-stream');
5-
6-const helpers = require('../providers/helper');
7-var propertyType = require('../models/propertyTypes');
8-var Property = require('../models/property');
9-
10-var gfs;
11-var conn = mongoose.connection;
12-conn.on('connected', () => {
13- gfs = Grid(conn.db, mongoose.mongo);
14- gfs.collection('imageMeta');
15- });
1+const db = require('../db/queries');
2+const { ethers } = require('ethers');
3+
4+// Contract ABI for reading state
5+const HOME_TRANSACTION_ABI = [
6+ 'function contractState() view returns (uint8)',
7+ 'function price() view returns (uint256)',
8+ 'function deposit() view returns (uint256)',
9+ 'function buyer() view returns (address)',
10+ 'function seller() view returns (address)',
11+ 'function realtor() view returns (address)',
12+];
13+
14+const ContractStateLabels = {
15+ 0: 'Awaiting Seller',
16+ 1: 'Awaiting Buyer',
17+ 2: 'Under Review',
18+ 3: 'Ready to Finalize',
19+ 4: 'Completed',
20+ 5: 'Rejected',
21+};
22+
23+// Get provider for reading contract state
24+function getProvider() {
25+ const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545';
26+ return new ethers.providers.JsonRpcProvider(rpcUrl);
27+}
28+
29+// Fetch contract state for a property
30+async function getContractState(contractAddress) {
31+ if (!contractAddress) return null;
32+
33+ try {
34+ const provider = getProvider();
35+ const contract = new ethers.Contract(contractAddress, HOME_TRANSACTION_ABI, provider);
36+
37+ const [state, price, deposit, buyer, seller, realtor] = await Promise.all([
38+ contract.contractState(),
39+ contract.price(),
40+ contract.deposit(),
41+ contract.buyer(),
42+ contract.seller(),
43+ contract.realtor(),
44+ ]);
45+
46+ return {
47+ state: state,
48+ stateLabel: ContractStateLabels[state] || 'Unknown',
49+ priceWei: price.toString(),
50+ priceEth: ethers.utils.formatEther(price),
51+ depositWei: deposit.toString(),
52+ depositEth: ethers.utils.formatEther(deposit),
53+ buyer,
54+ seller,
55+ realtor,
56+ };
57+ } catch (error) {
58+ console.error('Error fetching contract state:', error.message);
59+ return null;
60+ }
61+}
1662
1763 module.exports = {
18- propertyTypeList: (req, res) => {
19- propertyType.find({ is_active: true }, (err, result) => {
20- if (err)
21- res.status(400).send(err);
22- else
23- res.status(200).json(result);
24- });
25- },
26- addPropertyType: (req, res) => {
27- var proptyp = new propertyType();
28-
29- proptyp.title = req.body.title;
30- proptyp.type = req.body.type;
31- proptyp.createdOn = Date.now();
32-
33- proptyp.save((err, result) => {
34- if (err)
35- res.status(400).send(err);
36- else
37- res.status(200).json({ message: 'Property type added successfully', id: result._id });
38- });
39- },
40- addNewProperty: async (req, res) => {
41- let imgs = [];
42- try{
43- if(req.files && req.files.length)
44- req.files.forEach(ele => imgs.push(ele.filename) )
45- //Creating slug for the listing
46- var slug = await helpers.slugGenerator(req.body.title, 'title', 'property');
47- req.body.slug = slug;
48- req.body.type = req.body.Proptype;
49- req.body.cornrPlot = req.body.cornrPlot ? true : false;
50- req.body.images = imgs;
51- req.body.imgPath = 'properties';
52- if(!req.body.isSociety){
53- req.body.flatNo = '';
54- req.body.societyName = '';
55- }
56- const prop = new Property(req.body);
57- const result = await prop.save();
58-
59- if(result && result._id && result.slug)
60- res.status(200).json({result, message: "Your property has been successfully posted"});
61- else throw new Error('Something Went Wrong');
62- }
63- catch(err){
64- console.log({err});
65- res.status(400).json({message: err.message});
66- }
67- },
68- getUserList: (req, res) => {
69- Property.find({ isActive: true, userId: req.params.userId })
70- .populate('city', 'name')
71- .populate('state', 'name')
72- .populate('type', 'title')
73- .exec((err, result) => {
74- if (err)
75- res.status(400).send(err);
76- else
77- res.status(200).json(result);
78- });
79- },
80- getSingleProperty: async (req, res) => {
81- try{
82- var result = await Property.findOne({ slug: req.params.propertySlug })
83- .populate('city', 'name')
84- .populate('state', 'name')
85- .populate('type', 'title');
86-
87- var files = [];
88- if(result && result.images.length){
89- files = await gfs.files.find({ filename: { $in : result.images } }).toArray();
90- }
91- if(result) res.status(200).json({result, files});
92- else throw new Error('Something Went Wrong');
93- }
94- catch(err){
95- res.status(400).json({message: err.message});
96- }
97-
98- },
99- getFullList: (req, res) => {
100- Property.find({ isActive: true })
101- .populate('city', 'name')
102- .populate('state', 'name')
103- .populate('type', 'title')
104- .populate('userId', 'name')
105- .exec((err, result) => {
106- if (err)
107- res.status(400).send(err);
108- else
109- res.status(200).json(result);
110- });
111- },
112- markAsSold: async (req, res) => {
113- try{
114- const result = await Property.update({ slug: req.params.propertySlug }, { status: req.body.status });
115- console.log({result});
116- if(result && result.nModified == 1) res.status(200).json({ result, message: "Property has been updated Successfully" });
117- else throw new Error('Error in updating property');
118- }
119- catch(err){
120- res.status(400).json({message: err.message});
121- }
122- },
123- filterProperties: (req, res) => {
124- // console.log('propertyFor ', req.query.propertyFor, typeof req.query.propertyFor);
125- // console.log(req.query.propertyFor.split(","));
126- var query = {};
127- // query['isActive'] = true;
128-
129- if (req.query.propertyFor)
130- query['propertyFor'] = { $in: req.query.propertyFor.split(",") }
131- if (req.query.type)
132- query['type'] = { $in: req.query.type.split(",") }
133- if (req.query.city)
134- query['city'] = { $in: req.query.city.split(",") }
135- if (req.query.userId)
136- query['userId'] = req.query.userId
137- if (req.query.notUserId)
138- query['userId'] = { $ne: req.query.notUserId }
139- if (req.query.status)
140- query['status'] = { $in: req.query.status.split(",") }
141- console.log({ query });
142- Property.find(query)
143- .populate('city', 'name')
144- .populate('state', 'name')
145- .populate('type', 'title')
146- .populate('userId', 'name')
147- .exec((err, result) => {
148- if (err)
149- res.status(400).send(err);
150- else
151- res.status(200).json(result);
152- });
153- },
154- testController: async (req, res) => {
155- // console.log({testData});
156- const testData = await Property.find({ updatedOn: { $gte : '2019-04-01' } })
157- console.log({ testData });
158- return res.send(testData);
159- },
160- showGFSImage: (req, res) => {
161- gfs.files.findOne({ filename: req.params.filename }, (err, file) => {
162- // Check if file
163- if (!file || file.length === 0) {
164- return res.status(404).json({
165- err: 'No file exists'
166- });
167- }
168-
169- // Check if image
170- if (file.contentType === 'image/jpeg' || file.contentType === 'image/png') {
171- // Read output to browser
172- const readstream = gfs.createReadStream(file.filename);
173- readstream.pipe(res);
174- } else {
175- res.status(404).json({
176- err: 'Not an image'
177- });
178- }
179- })
64+ // Get all properties with optional filters
65+ getFullList: async (req, res) => {
66+ try {
67+ const { status, typeId, stateId, cityId, limit, offset } = req.query;
68+
69+ const properties = await db.properties.findAll({
70+ status,
71+ typeId: typeId ? parseInt(typeId) : undefined,
72+ stateId: stateId ? parseInt(stateId) : undefined,
73+ cityId: cityId ? parseInt(cityId) : undefined,
74+ limit: limit ? parseInt(limit) : 50,
75+ offset: offset ? parseInt(offset) : 0,
76+ });
77+
78+ // Fetch contract states for properties with contracts
79+ const propertiesWithContracts = await Promise.all(
80+ properties.map(async (property) => {
81+ const contractState = await getContractState(property.contract_address);
82+ return {
83+ ...property,
84+ contractState,
85+ };
86+ })
87+ );
88+
89+ res.status(200).json(propertiesWithContracts);
90+ } catch (error) {
91+ console.error('Error fetching properties:', error);
92+ res.status(500).json({ message: 'Error fetching properties' });
93+ }
94+ },
95+
96+ // Get single property by slug
97+ getSingleProperty: async (req, res) => {
98+ try {
99+ const { propertySlug } = req.params;
100+ const property = await db.properties.findBySlug(propertySlug);
101+
102+ if (!property) {
103+ return res.status(404).json({ message: 'Property not found' });
104+ }
105+
106+ // Get contract state
107+ const contractState = await getContractState(property.contract_address);
108+
109+ // Get investor stats
110+ const investorStats = await db.blockchain.getInvestorStats(property.id);
111+
112+ // Get transaction history
113+ const transactions = await db.blockchain.findTransactionsByProperty(property.id);
114+
115+ res.status(200).json({
116+ ...property,
117+ contractState,
118+ investorStats,
119+ transactions,
120+ });
121+ } catch (error) {
122+ console.error('Error fetching property:', error);
123+ res.status(500).json({ message: 'Error fetching property' });
124+ }
125+ },
126+
127+ // Get single property by ID
128+ getPropertyById: async (req, res) => {
129+ try {
130+ const { id } = req.params;
131+ const property = await db.properties.findById(parseInt(id));
132+
133+ if (!property) {
134+ return res.status(404).json({ message: 'Property not found' });
135+ }
136+
137+ const contractState = await getContractState(property.contract_address);
138+ const investorStats = await db.blockchain.getInvestorStats(property.id);
139+
140+ res.status(200).json({
141+ ...property,
142+ contractState,
143+ investorStats,
144+ });
145+ } catch (error) {
146+ console.error('Error fetching property:', error);
147+ res.status(500).json({ message: 'Error fetching property' });
148+ }
149+ },
150+
151+ // Get properties by user
152+ getUserList: async (req, res) => {
153+ try {
154+ const { userId } = req.params;
155+ const properties = await db.properties.findByUserId(parseInt(userId));
156+ res.status(200).json(properties);
157+ } catch (error) {
158+ console.error('Error fetching user properties:', error);
159+ res.status(500).json({ message: 'Error fetching properties' });
160+ }
161+ },
162+
163+ // Get property types
164+ propertyTypeList: async (req, res) => {
165+ try {
166+ const types = await db.common.getAllPropertyTypes();
167+ res.status(200).json(types);
168+ } catch (error) {
169+ console.error('Error fetching property types:', error);
170+ res.status(500).json({ message: 'Error fetching property types' });
171+ }
172+ },
173+
174+ // Add property type
175+ addPropertyType: async (req, res) => {
176+ try {
177+ const { title, type } = req.body;
178+ const result = await db.common.createPropertyType(title, type);
179+ res.status(200).json({ message: 'Property type added', id: result.id });
180+ } catch (error) {
181+ console.error('Error adding property type:', error);
182+ res.status(500).json({ message: 'Error adding property type' });
183+ }
184+ },
185+
186+ // Get states
187+ getStates: async (req, res) => {
188+ try {
189+ const states = await db.common.getAllStates();
190+ res.status(200).json(states);
191+ } catch (error) {
192+ console.error('Error fetching states:', error);
193+ res.status(500).json({ message: 'Error fetching states' });
194+ }
195+ },
196+
197+ // Get cities by state
198+ getCities: async (req, res) => {
199+ try {
200+ const { stateId } = req.query;
201+ const cities = stateId
202+ ? await db.common.getCitiesByState(parseInt(stateId))
203+ : await db.common.getAllCities();
204+ res.status(200).json(cities);
205+ } catch (error) {
206+ console.error('Error fetching cities:', error);
207+ res.status(500).json({ message: 'Error fetching cities' });
208+ }
209+ },
210+
211+ // Update property status
212+ markAsSold: async (req, res) => {
213+ try {
214+ const { propertySlug } = req.params;
215+ const { status } = req.body;
216+
217+ const property = await db.properties.findBySlug(propertySlug);
218+ if (!property) {
219+ return res.status(404).json({ message: 'Property not found' });
220+ }
221+
222+ const updated = await db.properties.updateStatus(property.id, status);
223+ res.status(200).json({ message: 'Property updated', property: updated });
224+ } catch (error) {
225+ console.error('Error updating property:', error);
226+ res.status(500).json({ message: 'Error updating property' });
227+ }
228+ },
229+
230+ // Filter properties (simplified)
231+ filterProperties: async (req, res) => {
232+ try {
233+ const { status, typeId, stateId, cityId } = req.query;
234+
235+ const properties = await db.properties.findAll({
236+ status,
237+ typeId: typeId ? parseInt(typeId) : undefined,
238+ stateId: stateId ? parseInt(stateId) : undefined,
239+ cityId: cityId ? parseInt(cityId) : undefined,
240+ });
241+
242+ res.status(200).json(properties);
243+ } catch (error) {
244+ console.error('Error filtering properties:', error);
245+ res.status(500).json({ message: 'Error filtering properties' });
180246 }
181-}
247+ },
248+};
+147-0server/db/blockchain.js
@@ -0,0 +1,147 @@
1+const db = require('./index');
2+
3+const blockchain = {
4+ // Transactions
5+ async createTransaction(data) {
6+ const {
7+ propertyId, contractAddress, txHash, txType,
8+ fromAddress, toAddress, valueWei, valueEth,
9+ gasUsed, blockNumber, contractState, status
10+ } = data;
11+
12+ const result = await db.query(
13+ `INSERT INTO blockchain_transactions (
14+ property_id, contract_address, tx_hash, tx_type,
15+ from_address, to_address, value_wei, value_eth,
16+ gas_used, block_number, contract_state, status
17+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
18+ RETURNING *`,
19+ [
20+ propertyId, contractAddress?.toLowerCase(), txHash, txType,
21+ fromAddress?.toLowerCase(), toAddress?.toLowerCase(), valueWei, valueEth,
22+ gasUsed, blockNumber, contractState || 0, status || 'pending'
23+ ]
24+ );
25+ return result.rows[0];
26+ },
27+
28+ async findTransactionsByProperty(propertyId) {
29+ const result = await db.query(
30+ `SELECT * FROM blockchain_transactions
31+ WHERE property_id = $1
32+ ORDER BY created_at DESC`,
33+ [propertyId]
34+ );
35+ return result.rows;
36+ },
37+
38+ async findTransactionsByContract(contractAddress) {
39+ const result = await db.query(
40+ `SELECT bt.*, p.title as property_title
41+ FROM blockchain_transactions bt
42+ LEFT JOIN properties p ON bt.property_id = p.id
43+ WHERE bt.contract_address = $1
44+ ORDER BY bt.created_at DESC`,
45+ [contractAddress.toLowerCase()]
46+ );
47+ return result.rows;
48+ },
49+
50+ async findTransactionByHash(txHash) {
51+ const result = await db.query(
52+ 'SELECT * FROM blockchain_transactions WHERE tx_hash = $1',
53+ [txHash]
54+ );
55+ return result.rows[0];
56+ },
57+
58+ async updateTransactionStatus(txHash, status, { gasUsed, blockNumber, contractState } = {}) {
59+ const result = await db.query(
60+ `UPDATE blockchain_transactions
61+ SET status = $1,
62+ gas_used = COALESCE($2, gas_used),
63+ block_number = COALESCE($3, block_number),
64+ contract_state = COALESCE($4, contract_state)
65+ WHERE tx_hash = $5
66+ RETURNING *`,
67+ [status, gasUsed, blockNumber, contractState, txHash]
68+ );
69+ return result.rows[0];
70+ },
71+
72+ // Investors
73+ async addInvestor(data) {
74+ const {
75+ propertyId, userId, walletAddress,
76+ tokensOwned, investmentAmount, investmentEth, txHash
77+ } = data;
78+
79+ const result = await db.query(
80+ `INSERT INTO property_investors (
81+ property_id, user_id, wallet_address,
82+ tokens_owned, investment_amount, investment_eth, tx_hash
83+ ) VALUES ($1, $2, $3, $4, $5, $6, $7)
84+ ON CONFLICT (property_id, user_id)
85+ DO UPDATE SET
86+ tokens_owned = property_investors.tokens_owned + EXCLUDED.tokens_owned,
87+ investment_amount = property_investors.investment_amount + EXCLUDED.investment_amount,
88+ investment_eth = property_investors.investment_eth + EXCLUDED.investment_eth,
89+ updated_at = CURRENT_TIMESTAMP
90+ RETURNING *`,
91+ [propertyId, userId, walletAddress?.toLowerCase(), tokensOwned, investmentAmount, investmentEth, txHash]
92+ );
93+ return result.rows[0];
94+ },
95+
96+ async findInvestorsByProperty(propertyId) {
97+ const result = await db.query(
98+ `SELECT pi.*, u.fname || ' ' || u.lname as investor_name
99+ FROM property_investors pi
100+ LEFT JOIN users u ON pi.user_id = u.id
101+ WHERE pi.property_id = $1
102+ ORDER BY pi.tokens_owned DESC`,
103+ [propertyId]
104+ );
105+ return result.rows;
106+ },
107+
108+ async findInvestmentsByUser(userId) {
109+ const result = await db.query(
110+ `SELECT pi.*, p.title, p.slug, p.images, p.price, p.status
111+ FROM property_investors pi
112+ JOIN properties p ON pi.property_id = p.id
113+ WHERE pi.user_id = $1
114+ ORDER BY pi.created_at DESC`,
115+ [userId]
116+ );
117+ return result.rows;
118+ },
119+
120+ async findInvestmentsByWallet(walletAddress) {
121+ const result = await db.query(
122+ `SELECT pi.*, p.title, p.slug, p.images, p.price, p.status
123+ FROM property_investors pi
124+ JOIN properties p ON pi.property_id = p.id
125+ WHERE pi.wallet_address = $1
126+ ORDER BY pi.created_at DESC`,
127+ [walletAddress.toLowerCase()]
128+ );
129+ return result.rows;
130+ },
131+
132+ async getInvestorStats(propertyId) {
133+ const result = await db.query(
134+ `SELECT
135+ COUNT(*) as total_investors,
136+ SUM(tokens_owned) as total_tokens_sold,
137+ SUM(investment_amount) as total_invested_usd,
138+ SUM(investment_eth) as total_invested_eth
139+ FROM property_investors
140+ WHERE property_id = $1`,
141+ [propertyId]
142+ );
143+ return result.rows[0];
144+ },
145+};
146+
147+module.exports = blockchain;
+100-0server/db/common.js
@@ -0,0 +1,100 @@
1+const db = require('./index');
2+
3+const common = {
4+ // States
5+ async getAllStates() {
6+ const result = await db.query(
7+ 'SELECT * FROM states WHERE is_active = true ORDER BY name'
8+ );
9+ return result.rows;
10+ },
11+
12+ async getStateById(id) {
13+ const result = await db.query(
14+ 'SELECT * FROM states WHERE id = $1',
15+ [id]
16+ );
17+ return result.rows[0];
18+ },
19+
20+ async createState(name) {
21+ const result = await db.query(
22+ 'INSERT INTO states (name) VALUES ($1) RETURNING *',
23+ [name]
24+ );
25+ return result.rows[0];
26+ },
27+
28+ // Cities
29+ async getAllCities() {
30+ const result = await db.query(
31+ `SELECT c.*, s.name as state_name
32+ FROM cities c
33+ JOIN states s ON c.state_id = s.id
34+ WHERE c.is_active = true
35+ ORDER BY c.name`
36+ );
37+ return result.rows;
38+ },
39+
40+ async getCitiesByState(stateId) {
41+ const result = await db.query(
42+ 'SELECT * FROM cities WHERE state_id = $1 AND is_active = true ORDER BY name',
43+ [stateId]
44+ );
45+ return result.rows;
46+ },
47+
48+ async getCityById(id) {
49+ const result = await db.query(
50+ `SELECT c.*, s.name as state_name
51+ FROM cities c
52+ JOIN states s ON c.state_id = s.id
53+ WHERE c.id = $1`,
54+ [id]
55+ );
56+ return result.rows[0];
57+ },
58+
59+ async createCity(name, stateId) {
60+ const result = await db.query(
61+ 'INSERT INTO cities (name, state_id) VALUES ($1, $2) RETURNING *',
62+ [name, stateId]
63+ );
64+ return result.rows[0];
65+ },
66+
67+ // Property Types
68+ async getAllPropertyTypes() {
69+ const result = await db.query(
70+ 'SELECT * FROM property_types WHERE is_active = true ORDER BY title'
71+ );
72+ return result.rows;
73+ },
74+
75+ async getPropertyTypeById(id) {
76+ const result = await db.query(
77+ 'SELECT * FROM property_types WHERE id = $1',
78+ [id]
79+ );
80+ return result.rows[0];
81+ },
82+
83+ async getPropertyTypesByCategory(type) {
84+ const result = await db.query(
85+ 'SELECT * FROM property_types WHERE type = $1 AND is_active = true ORDER BY title',
86+ [type]
87+ );
88+ return result.rows;
89+ },
90+
91+ async createPropertyType(title, type) {
92+ const result = await db.query(
93+ 'INSERT INTO property_types (title, type) VALUES ($1, $2) RETURNING *',
94+ [title, type]
95+ );
96+ return result.rows[0];
97+ },
98+};
99+
100+module.exports = common;
+26-0server/db/index.js
@@ -0,0 +1,26 @@
1+const { Pool } = require('pg');
2+
3+if (!process.env.DATABASE_URL) {
4+ console.error('DATABASE_URL environment variable is not set');
5+ process.exit(1);
6+}
7+
8+const pool = new Pool({
9+ connectionString: process.env.DATABASE_URL,
10+ max: 20,
11+ idleTimeoutMillis: 30000,
12+ connectionTimeoutMillis: 2000,
13+});
14+
15+pool.on('connect', () => {
16+ console.log('Connected to PostgreSQL');
17+});
18+
19+pool.on('error', (err) => {
20+ console.error('PostgreSQL pool error:', err);
21+});
22+
23+const query = (text, params) => pool.query(text, params);
24+const getClient = () => pool.connect();
25+
26+module.exports = { query, getClient, pool };
+226-0server/db/properties.js
@@ -0,0 +1,226 @@
1+const db = require('./index');
2+
3+const properties = {
4+ async findAll({ status, typeId, stateId, cityId, limit = 50, offset = 0 } = {}) {
5+ let query = `
6+ SELECT p.*,
7+ pt.title as type_title, pt.type as property_type,
8+ s.name as state_name,
9+ c.name as city_name,
10+ u.fname || ' ' || u.lname as owner_name
11+ FROM properties p
12+ LEFT JOIN property_types pt ON p.type_id = pt.id
13+ LEFT JOIN states s ON p.state_id = s.id
14+ LEFT JOIN cities c ON p.city_id = c.id
15+ LEFT JOIN users u ON p.user_id = u.id
16+ WHERE p.is_active = true
17+ `;
18+ const params = [];
19+ let paramCount = 1;
20+
21+ if (status) {
22+ query += ` AND p.status = $${paramCount}`;
23+ params.push(status);
24+ paramCount++;
25+ }
26+ if (typeId) {
27+ query += ` AND p.type_id = $${paramCount}`;
28+ params.push(typeId);
29+ paramCount++;
30+ }
31+ if (stateId) {
32+ query += ` AND p.state_id = $${paramCount}`;
33+ params.push(stateId);
34+ paramCount++;
35+ }
36+ if (cityId) {
37+ query += ` AND p.city_id = $${paramCount}`;
38+ params.push(cityId);
39+ paramCount++;
40+ }
41+
42+ query += ` ORDER BY p.created_at DESC LIMIT $${paramCount} OFFSET $${paramCount + 1}`;
43+ params.push(limit, offset);
44+
45+ const result = await db.query(query, params);
46+ return result.rows;
47+ },
48+
49+ async findById(id) {
50+ const result = await db.query(
51+ `SELECT p.*,
52+ pt.title as type_title, pt.type as property_type,
53+ s.name as state_name,
54+ c.name as city_name,
55+ u.fname || ' ' || u.lname as owner_name,
56+ u.email as owner_email,
57+ u.phone_no as owner_phone
58+ FROM properties p
59+ LEFT JOIN property_types pt ON p.type_id = pt.id
60+ LEFT JOIN states s ON p.state_id = s.id
61+ LEFT JOIN cities c ON p.city_id = c.id
62+ LEFT JOIN users u ON p.user_id = u.id
63+ WHERE p.id = $1`,
64+ [id]
65+ );
66+ return result.rows[0];
67+ },
68+
69+ async findBySlug(slug) {
70+ const result = await db.query(
71+ `SELECT p.*,
72+ pt.title as type_title, pt.type as property_type,
73+ s.name as state_name,
74+ c.name as city_name,
75+ u.fname || ' ' || u.lname as owner_name,
76+ u.email as owner_email,
77+ u.phone_no as owner_phone
78+ FROM properties p
79+ LEFT JOIN property_types pt ON p.type_id = pt.id
80+ LEFT JOIN states s ON p.state_id = s.id
81+ LEFT JOIN cities c ON p.city_id = c.id
82+ LEFT JOIN users u ON p.user_id = u.id
83+ WHERE p.slug = $1`,
84+ [slug]
85+ );
86+ return result.rows[0];
87+ },
88+
89+ async findByUserId(userId) {
90+ const result = await db.query(
91+ `SELECT p.*,
92+ pt.title as type_title, pt.type as property_type,
93+ s.name as state_name,
94+ c.name as city_name
95+ FROM properties p
96+ LEFT JOIN property_types pt ON p.type_id = pt.id
97+ LEFT JOIN states s ON p.state_id = s.id
98+ LEFT JOIN cities c ON p.city_id = c.id
99+ WHERE p.user_id = $1
100+ ORDER BY p.created_at DESC`,
101+ [userId]
102+ );
103+ return result.rows;
104+ },
105+
106+ async findByContractAddress(contractAddress) {
107+ const result = await db.query(
108+ 'SELECT * FROM properties WHERE contract_address = $1',
109+ [contractAddress.toLowerCase()]
110+ );
111+ return result.rows[0];
112+ },
113+
114+ async create(data) {
115+ const {
116+ title, propertyFor, description, typeId, stateId, cityId,
117+ locality, length, breadth, cornerPlot, isSociety, societyName,
118+ flatNo, address, email, price, priceEth, phoneNo, pincode,
119+ userId, slug, images, imgPath, contractAddress, tokenId,
120+ roiPercentage, totalTokens, availableTokens, minInvestment
121+ } = data;
122+
123+ const result = await db.query(
124+ `INSERT INTO properties (
125+ title, property_for, description, type_id, state_id, city_id,
126+ locality, length, breadth, corner_plot, is_society, society_name,
127+ flat_no, address, email, price, price_eth, phone_no, pincode,
128+ user_id, slug, images, img_path, contract_address, token_id,
129+ roi_percentage, total_tokens, available_tokens, min_investment
130+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)
131+ RETURNING *`,
132+ [
133+ title, propertyFor || 'sell', description, typeId, stateId, cityId,
134+ locality, length, breadth, cornerPlot || false, isSociety || false, societyName,
135+ flatNo, address, email, price, priceEth, phoneNo, pincode,
136+ userId, slug, images, imgPath, contractAddress?.toLowerCase(), tokenId,
137+ roiPercentage, totalTokens, availableTokens, minInvestment
138+ ]
139+ );
140+ return result.rows[0];
141+ },
142+
143+ async update(id, fields) {
144+ const setClause = [];
145+ const values = [];
146+ let paramCount = 1;
147+
148+ const fieldMap = {
149+ title: 'title',
150+ propertyFor: 'property_for',
151+ description: 'description',
152+ typeId: 'type_id',
153+ stateId: 'state_id',
154+ cityId: 'city_id',
155+ locality: 'locality',
156+ length: 'length',
157+ breadth: 'breadth',
158+ cornerPlot: 'corner_plot',
159+ isSociety: 'is_society',
160+ societyName: 'society_name',
161+ flatNo: 'flat_no',
162+ address: 'address',
163+ email: 'email',
164+ price: 'price',
165+ priceEth: 'price_eth',
166+ phoneNo: 'phone_no',
167+ pincode: 'pincode',
168+ status: 'status',
169+ isActive: 'is_active',
170+ images: 'images',
171+ imgPath: 'img_path',
172+ contractAddress: 'contract_address',
173+ tokenId: 'token_id',
174+ roiPercentage: 'roi_percentage',
175+ totalTokens: 'total_tokens',
176+ availableTokens: 'available_tokens',
177+ minInvestment: 'min_investment',
178+ };
179+
180+ for (const [key, value] of Object.entries(fields)) {
181+ const dbField = fieldMap[key];
182+ if (dbField) {
183+ setClause.push(`${dbField} = $${paramCount}`);
184+ values.push(key === 'contractAddress' ? value?.toLowerCase() : value);
185+ paramCount++;
186+ }
187+ }
188+
189+ if (setClause.length === 0) return null;
190+
191+ values.push(id);
192+ const result = await db.query(
193+ `UPDATE properties SET ${setClause.join(', ')}, updated_at = CURRENT_TIMESTAMP
194+ WHERE id = $${paramCount}
195+ RETURNING *`,
196+ values
197+ );
198+ return result.rows[0];
199+ },
200+
201+ async updateStatus(id, status) {
202+ const result = await db.query(
203+ 'UPDATE properties SET status = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
204+ [status, id]
205+ );
206+ return result.rows[0];
207+ },
208+
209+ async linkContract(id, contractAddress) {
210+ const result = await db.query(
211+ 'UPDATE properties SET contract_address = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
212+ [contractAddress.toLowerCase(), id]
213+ );
214+ return result.rows[0];
215+ },
216+
217+ async delete(id) {
218+ const result = await db.query(
219+ 'UPDATE properties SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1 RETURNING *',
220+ [id]
221+ );
222+ return result.rows[0];
223+ },
224+};
225+
226+module.exports = properties;
+19-0server/db/queries.js
@@ -0,0 +1,19 @@
1+// Re-export all database query modules
2+const db = require('./index');
3+const users = require('./users');
4+const properties = require('./properties');
5+const blockchain = require('./blockchain');
6+const common = require('./common');
7+
8+module.exports = {
9+ // Raw query access
10+ query: db.query,
11+ getClient: db.getClient,
12+ pool: db.pool,
13+
14+ // Model queries
15+ users,
16+ properties,
17+ blockchain,
18+ common,
19+};
+90-0server/db/users.js
@@ -0,0 +1,90 @@
1+const db = require('./index');
2+
3+const users = {
4+ async findById(id) {
5+ const result = await db.query(
6+ `SELECT u.*, s.name as state_name, c.name as city_name
7+ FROM users u
8+ LEFT JOIN states s ON u.state_id = s.id
9+ LEFT JOIN cities c ON u.city_id = c.id
10+ WHERE u.id = $1`,
11+ [id]
12+ );
13+ return result.rows[0];
14+ },
15+
16+ async findByEmail(email) {
17+ const result = await db.query(
18+ 'SELECT * FROM users WHERE email = $1',
19+ [email]
20+ );
21+ return result.rows[0];
22+ },
23+
24+ async findByPhone(phoneNo) {
25+ const result = await db.query(
26+ 'SELECT * FROM users WHERE phone_no = $1',
27+ [phoneNo]
28+ );
29+ return result.rows[0];
30+ },
31+
32+ async findByWallet(walletAddress) {
33+ const result = await db.query(
34+ 'SELECT * FROM users WHERE wallet_address = $1',
35+ [walletAddress.toLowerCase()]
36+ );
37+ return result.rows[0];
38+ },
39+
40+ async create({ fname, lname, email, phoneNo, password, stateId, cityId, pincode, walletAddress }) {
41+ const result = await db.query(
42+ `INSERT INTO users (fname, lname, email, phone_no, password, state_id, city_id, pincode, wallet_address)
43+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
44+ RETURNING *`,
45+ [fname, lname, email, phoneNo, password, stateId, cityId, pincode, walletAddress?.toLowerCase()]
46+ );
47+ return result.rows[0];
48+ },
49+
50+ async update(id, fields) {
51+ const setClause = [];
52+ const values = [];
53+ let paramCount = 1;
54+
55+ for (const [key, value] of Object.entries(fields)) {
56+ // Convert camelCase to snake_case
57+ const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
58+ setClause.push(`${snakeKey} = $${paramCount}`);
59+ values.push(key === 'walletAddress' ? value?.toLowerCase() : value);
60+ paramCount++;
61+ }
62+
63+ values.push(id);
64+ const result = await db.query(
65+ `UPDATE users SET ${setClause.join(', ')}, updated_at = CURRENT_TIMESTAMP
66+ WHERE id = $${paramCount}
67+ RETURNING *`,
68+ values
69+ );
70+ return result.rows[0];
71+ },
72+
73+ async updatePassword(id, password) {
74+ const result = await db.query(
75+ 'UPDATE users SET password = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
76+ [password, id]
77+ );
78+ return result.rows[0];
79+ },
80+
81+ async linkWallet(id, walletAddress) {
82+ const result = await db.query(
83+ 'UPDATE users SET wallet_address = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 RETURNING *',
84+ [walletAddress.toLowerCase(), id]
85+ );
86+ return result.rows[0];
87+ },
88+};
89+
90+module.exports = users;
+9-31server/routes/property.js
@@ -1,43 +1,21 @@
11 const express = require('express');
2-const multer = require('multer');
3-const crypto = require('crypto');
4-const path = require('path');
5-const mongoose = require('mongoose');
6-const config = require('../config/config');
7-
82 const router = express.Router();
93 const propertyController = require('../controllers/property.controller');
104
11-// Create storage engine
12-const upload = multer({ storage: multer.memoryStorage() });
13-
14-// GridFsStorage setup
15-let gfs;
16-
17-mongoose.connection.once('open', () => {
18- gfs = new mongoose.mongo.GridFsStorage(mongoose.connection.db, {
19- bucketName: 'imageMeta'
20- });
21-});
22-
23-router.use((req, res, next) => {
24- req.gfs = gfs;
25- next();
26-})
27-
28-// Property type dropdown
5+// Property types
296 router.get('/type', propertyController.propertyTypeList);
307 router.post('/type', propertyController.addPropertyType);
318
32-//Property
33-router.post('/new', upload.array("propImages"), propertyController.addNewProperty);
9+// Location data
10+router.get('/states', propertyController.getStates);
11+router.get('/cities', propertyController.getCities);
12+
13+// Properties
14+router.get('/list', propertyController.getFullList);
3415 router.get('/list/:userId', propertyController.getUserList);
35-router.get('/list/', propertyController.getFullList);
3616 router.get('/single/:propertySlug', propertyController.getSingleProperty);
37-router.get('/showGFSImage/:filename', propertyController.showGFSImage); // To view image in front-end
17+router.get('/id/:id', propertyController.getPropertyById);
3818 router.post('/markAsSold/:propertySlug', propertyController.markAsSold);
39-
40-//Properties filter
4119 router.get('/filter', propertyController.filterProperties);
4220
43-module.exports = router;
21+module.exports = router;
+63-12src/components/layout/Navbar.jsx
@@ -1,9 +1,20 @@
11 import { useState } from 'react';
22 import { Link } from 'react-router-dom';
33 import { FiMenu, FiX } from 'react-icons/fi';
4+import { useWallet } from '../../hooks/useWallet';
45
56 function Navbar() {
67 const [isOpen, setIsOpen] = useState(false);
8+ const {
9+ connect,
10+ disconnect,
11+ shortAddress,
12+ isConnected,
13+ isConnecting,
14+ isCorrectNetwork,
15+ switchToHardhat,
16+ hasMetaMask
17+ } = useWallet();
718
819 const navigation = [
920 { name: 'Home', href: '/' },
@@ -13,6 +24,53 @@ function Navbar() {
1324 { name: 'Blog', href: '/blog' },
1425 ];
1526
27+ const handleConnect = async () => {
28+ if (isConnected) {
29+ disconnect();
30+ } else {
31+ await connect();
32+ }
33+ };
34+
35+ const renderConnectButton = (mobile = false) => {
36+ if (!hasMetaMask) {
37+ return (
38+ <a
39+ href="https://metamask.io/download/"
40+ target="_blank"
41+ rel="noopener noreferrer"
42+ className={mobile ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn w-auto" : "btn"}
43+ >
44+ Install MetaMask
45+ </a>
46+ );
47+ }
48+
49+ if (isConnected && !isCorrectNetwork) {
50+ return (
51+ <button
52+ onClick={switchToHardhat}
53+ className={mobile ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn-secondary w-auto" : "btn-secondary"}
54+ >
55+ Wrong Network
56+ </button>
57+ );
58+ }
59+
60+ return (
61+ <button
62+ onClick={handleConnect}
63+ disabled={isConnecting}
64+ className={mobile
65+ ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn w-auto"
66+ : "btn"
67+ }
68+ >
69+ {isConnecting ? 'Connecting...' : isConnected ? shortAddress : 'Connect'}
70+ </button>
71+ );
72+ };
73+
1674 return (
1775 <nav className="glass-nav sticky top-0 z-50">
1876 <div className="container">
@@ -39,11 +97,7 @@ function Navbar() {
3997 <span className="absolute inset-x-0 bottom-0 h-0.5 bg-gradient-to-r from-primary-500 to-primary-600 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300"></span>
4098 </Link>
4199 ))}
42- <button
43- className="btn"
44- >
45- Connect
46- </button>
100+ {renderConnectButton()}
47101 </div>
48102
49103 {/* Mobile menu button */}
@@ -72,12 +126,9 @@ function Navbar() {
72126 {item.name}
73127 </Link>
74128 ))}
75- <button
76- className="block mx-2 mt-2 px-3 py-2 text-base font-medium btn w-auto"
77- onClick={() => setIsOpen(false)}
78- >
79- Connect
80- </button>
129+ <div onClick={() => setIsOpen(false)}>
130+ {renderConnectButton(true)}
131+ </div>
81132 </div>
82133 </div>
83134 )}
@@ -86,4 +137,4 @@ function Navbar() {
86137 );
87138 }
88139
89-export default Navbar;
140+export default Navbar;
+189-0src/hooks/useContracts.js
@@ -0,0 +1,189 @@
1+import { useState, useCallback } from 'react';
2+import { useWallet } from './useWallet';
3+import {
4+ createPropertyTransaction,
5+ getAllTransactions,
6+ getTransactionsByRole,
7+ getTransactionDetails,
8+ sellerSign,
9+ buyerSignAndDeposit,
10+ realtorReview,
11+ buyerFinalize,
12+ withdrawTransaction,
13+} from '../utils/contracts';
14+
15+export function useContracts() {
16+ const { signer, provider, address, isConnected } = useWallet();
17+ const [loading, setLoading] = useState(false);
18+ const [error, setError] = useState(null);
19+
20+ const handleError = (err) => {
21+ console.error('Contract error:', err);
22+ if (err.code === 'ACTION_REJECTED') {
23+ setError('Transaction rejected by user');
24+ } else if (err.reason) {
25+ setError(err.reason);
26+ } else {
27+ setError(err.message || 'Transaction failed');
28+ }
29+ };
30+
31+ // Create a new property transaction (as realtor)
32+ const createTransaction = useCallback(async (propertyData) => {
33+ if (!signer) throw new Error('Wallet not connected');
34+
35+ setLoading(true);
36+ setError(null);
37+
38+ try {
39+ const contractAddress = await createPropertyTransaction(signer, propertyData);
40+ return contractAddress;
41+ } catch (err) {
42+ handleError(err);
43+ throw err;
44+ } finally {
45+ setLoading(false);
46+ }
47+ }, [signer]);
48+
49+ // Get all transactions
50+ const fetchAllTransactions = useCallback(async () => {
51+ if (!provider) return [];
52+
53+ try {
54+ return await getAllTransactions(provider);
55+ } catch (err) {
56+ handleError(err);
57+ return [];
58+ }
59+ }, [provider]);
60+
61+ // Get transactions for connected user by role
62+ const fetchMyTransactions = useCallback(async (role) => {
63+ if (!provider || !address) return [];
64+
65+ try {
66+ return await getTransactionsByRole(provider, role, address);
67+ } catch (err) {
68+ handleError(err);
69+ return [];
70+ }
71+ }, [provider, address]);
72+
73+ // Get details of a specific transaction
74+ const fetchTransactionDetails = useCallback(async (contractAddress) => {
75+ if (!provider) return null;
76+
77+ try {
78+ return await getTransactionDetails(provider, contractAddress);
79+ } catch (err) {
80+ handleError(err);
81+ return null;
82+ }
83+ }, [provider]);
84+
85+ // Seller signs the contract
86+ const signAsSeller = useCallback(async (contractAddress) => {
87+ if (!signer) throw new Error('Wallet not connected');
88+
89+ setLoading(true);
90+ setError(null);
91+
92+ try {
93+ return await sellerSign(signer, contractAddress);
94+ } catch (err) {
95+ handleError(err);
96+ throw err;
97+ } finally {
98+ setLoading(false);
99+ }
100+ }, [signer]);
101+
102+ // Buyer signs and deposits
103+ const signAsBuyerWithDeposit = useCallback(async (contractAddress, depositAmount) => {
104+ if (!signer) throw new Error('Wallet not connected');
105+
106+ setLoading(true);
107+ setError(null);
108+
109+ try {
110+ return await buyerSignAndDeposit(signer, contractAddress, depositAmount);
111+ } catch (err) {
112+ handleError(err);
113+ throw err;
114+ } finally {
115+ setLoading(false);
116+ }
117+ }, [signer]);
118+
119+ // Realtor reviews closing conditions
120+ const reviewAsRealtor = useCallback(async (contractAddress, accepted) => {
121+ if (!signer) throw new Error('Wallet not connected');
122+
123+ setLoading(true);
124+ setError(null);
125+
126+ try {
127+ return await realtorReview(signer, contractAddress, accepted);
128+ } catch (err) {
129+ handleError(err);
130+ throw err;
131+ } finally {
132+ setLoading(false);
133+ }
134+ }, [signer]);
135+
136+ // Buyer finalizes transaction
137+ const finalizeAsBuyer = useCallback(async (contractAddress, remainingAmount) => {
138+ if (!signer) throw new Error('Wallet not connected');
139+
140+ setLoading(true);
141+ setError(null);
142+
143+ try {
144+ return await buyerFinalize(signer, contractAddress, remainingAmount);
145+ } catch (err) {
146+ handleError(err);
147+ throw err;
148+ } finally {
149+ setLoading(false);
150+ }
151+ }, [signer]);
152+
153+ // Withdraw from transaction
154+ const withdraw = useCallback(async (contractAddress) => {
155+ if (!signer) throw new Error('Wallet not connected');
156+
157+ setLoading(true);
158+ setError(null);
159+
160+ try {
161+ return await withdrawTransaction(signer, contractAddress);
162+ } catch (err) {
163+ handleError(err);
164+ throw err;
165+ } finally {
166+ setLoading(false);
167+ }
168+ }, [signer]);
169+
170+ return {
171+ // State
172+ loading,
173+ error,
174+ isConnected,
175+
176+ // Factory functions
177+ createTransaction,
178+ fetchAllTransactions,
179+ fetchMyTransactions,
180+
181+ // Transaction functions
182+ fetchTransactionDetails,
183+ signAsSeller,
184+ signAsBuyerWithDeposit,
185+ reviewAsRealtor,
186+ finalizeAsBuyer,
187+ withdraw,
188+ };
189+}
+162-0src/hooks/useWallet.js
@@ -0,0 +1,162 @@
1+import { useAtom } from 'jotai';
2+import { ethers } from 'ethers';
3+import { useCallback, useEffect } from 'react';
4+import {
5+ addressAtom,
6+ providerAtom,
7+ signerAtom,
8+ chainIdAtom,
9+ isConnectingAtom,
10+ isConnectedAtom,
11+ walletErrorAtom,
12+ shortAddressAtom,
13+ isCorrectNetworkAtom,
14+} from '../store/wallet';
15+
16+export function useWallet() {
17+ const [address, setAddress] = useAtom(addressAtom);
18+ const [provider, setProvider] = useAtom(providerAtom);
19+ const [signer, setSigner] = useAtom(signerAtom);
20+ const [chainId, setChainId] = useAtom(chainIdAtom);
21+ const [isConnecting, setIsConnecting] = useAtom(isConnectingAtom);
22+ const [isConnected] = useAtom(isConnectedAtom);
23+ const [error, setError] = useAtom(walletErrorAtom);
24+ const [shortAddress] = useAtom(shortAddressAtom);
25+ const [isCorrectNetwork] = useAtom(isCorrectNetworkAtom);
26+
27+ // Check if MetaMask is installed
28+ const hasMetaMask = typeof window !== 'undefined' && Boolean(window.ethereum);
29+
30+ // Connect wallet
31+ const connect = useCallback(async () => {
32+ if (!hasMetaMask) {
33+ setError('Please install MetaMask to connect');
34+ return;
35+ }
36+
37+ setIsConnecting(true);
38+ setError(null);
39+
40+ try {
41+ const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
42+
43+ // Request account access
44+ const accounts = await window.ethereum.request({
45+ method: 'eth_requestAccounts',
46+ });
47+
48+ const web3Signer = web3Provider.getSigner();
49+ const network = await web3Provider.getNetwork();
50+
51+ setProvider(web3Provider);
52+ setSigner(web3Signer);
53+ setAddress(accounts[0]);
54+ setChainId(network.chainId);
55+ } catch (err) {
56+ console.error('Failed to connect wallet:', err);
57+ if (err.code === 4001) {
58+ setError('Connection rejected by user');
59+ } else {
60+ setError(err.message || 'Failed to connect wallet');
61+ }
62+ } finally {
63+ setIsConnecting(false);
64+ }
65+ }, [hasMetaMask, setAddress, setChainId, setError, setIsConnecting, setProvider, setSigner]);
66+
67+ // Disconnect wallet
68+ const disconnect = useCallback(() => {
69+ setAddress(null);
70+ setProvider(null);
71+ setSigner(null);
72+ setChainId(null);
73+ setError(null);
74+ }, [setAddress, setChainId, setError, setProvider, setSigner]);
75+
76+ // Switch to Hardhat local network
77+ const switchToHardhat = useCallback(async () => {
78+ if (!hasMetaMask) return;
79+
80+ try {
81+ await window.ethereum.request({
82+ method: 'wallet_switchEthereumChain',
83+ params: [{ chainId: '0x7A69' }], // 31337 in hex
84+ });
85+ } catch (err) {
86+ // Chain not added, try to add it
87+ if (err.code === 4902) {
88+ try {
89+ await window.ethereum.request({
90+ method: 'wallet_addEthereumChain',
91+ params: [{
92+ chainId: '0x7A69',
93+ chainName: 'Hardhat Local',
94+ nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
95+ rpcUrls: ['http://127.0.0.1:8545'],
96+ }],
97+ });
98+ } catch (addErr) {
99+ setError('Failed to add Hardhat network');
100+ }
101+ }
102+ }
103+ }, [hasMetaMask, setError]);
104+
105+ // Listen for account/chain changes
106+ useEffect(() => {
107+ if (!hasMetaMask) return;
108+
109+ const handleAccountsChanged = (accounts) => {
110+ if (accounts.length === 0) {
111+ disconnect();
112+ } else {
113+ setAddress(accounts[0]);
114+ }
115+ };
116+
117+ const handleChainChanged = (chainIdHex) => {
118+ const newChainId = parseInt(chainIdHex, 16);
119+ setChainId(newChainId);
120+ };
121+
122+ window.ethereum.on('accountsChanged', handleAccountsChanged);
123+ window.ethereum.on('chainChanged', handleChainChanged);
124+
125+ return () => {
126+ window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
127+ window.ethereum.removeListener('chainChanged', handleChainChanged);
128+ };
129+ }, [hasMetaMask, disconnect, setAddress, setChainId]);
130+
131+ // Auto-connect if already authorized
132+ useEffect(() => {
133+ if (!hasMetaMask || isConnected) return;
134+
135+ window.ethereum
136+ .request({ method: 'eth_accounts' })
137+ .then((accounts) => {
138+ if (accounts.length > 0) {
139+ connect();
140+ }
141+ })
142+ .catch(console.error);
143+ }, [hasMetaMask, isConnected, connect]);
144+
145+ return {
146+ // State
147+ address,
148+ shortAddress,
149+ provider,
150+ signer,
151+ chainId,
152+ isConnecting,
153+ isConnected,
154+ isCorrectNetwork,
155+ error,
156+ hasMetaMask,
157+ // Actions
158+ connect,
159+ disconnect,
160+ switchToHardhat,
161+ };
162+}
+5-1src/pages/Home.jsx
@@ -5,9 +5,11 @@ import { FiArrowRight, FiUser, FiClock } from 'react-icons/fi';
55 import { FaWallet, FaStore, FaMoneyBillWave, FaExchangeAlt, FaChartLine, FaLock, FaUserCog, FaCoins } from 'react-icons/fa';
66 import { SiEthereum } from 'react-icons/si';
77 import { FiChevronDown, FiChevronUp } from 'react-icons/fi';
8+import { useWallet } from '../hooks/useWallet';
89
910 function Home() {
1011 const [openSections, setOpenSections] = useState({});
12+ const { connect, isConnected, isConnecting, shortAddress } = useWallet();
1113
1214 const featuredProperties = [
1315 {
@@ -426,10 +428,12 @@ function Home() {
426428 Browse Properties
427429 </Link>
428430 <button
431+ onClick={connect}
432+ disabled={isConnecting}
429433 className="btn bg-gradient-to-r from-primary-700 to-primary-800 hover:from-primary-800 hover:to-primary-900"
430434 >
431435 <FaWallet className="mr-2" />
432- Connect Wallet
436+ {isConnecting ? 'Connecting...' : isConnected ? shortAddress : 'Connect Wallet'}
433437 </button>
434438 </div>
435439 </div>
+188-262src/pages/Properties.jsx
@@ -1,180 +1,117 @@
1-import { useState } from 'react';
1+import { useState, useEffect } from 'react';
22 import { Link } from 'react-router-dom';
33 import { motion } from 'framer-motion';
4-import { FiFilter, FiArrowRight, FiTrendingUp, FiDollarSign } from 'react-icons/fi';
4+import { FiFilter, FiArrowRight, FiTrendingUp, FiDollarSign, FiLoader } from 'react-icons/fi';
55 import { FaEthereum } from 'react-icons/fa';
66
7+const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001';
8+
79 function Properties() {
10+ const [properties, setProperties] = useState([]);
11+ const [loading, setLoading] = useState(true);
12+ const [error, setError] = useState(null);
813 const [showFilters, setShowFilters] = useState(false);
914 const [filters, setFilters] = useState({
1015 priceRange: 'all',
1116 propertyType: 'all',
1217 location: '',
1318 minROI: '',
14- maxROI: '',
15- fundingStatus: 'all',
1619 sortBy: 'newest'
1720 });
1821
19- const properties = [
20- {
21- id: 1,
22- title: 'Modern Villa with Pool',
23- price: {
24- usd: 850000,
25- eth: 425
26- },
27- location: 'Beverly Hills, CA',
28- image: 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80',
29- type: 'villa',
30- roi: '7.2%',
31- metrics: {
32- totalInvestors: 142,
33- funded: '89%',
34- minInvestment: '$10',
35- monthlyIncome: '$520',
36- appreciation: '4.5%'
37- },
38- status: 'Active Investment',
39- features: ['Pool', 'Smart Home', 'Solar Panels'],
40- tokenDetails: {
41- totalTokens: 85000,
42- availableTokens: 9350,
43- tokenPrice: '$10'
44- }
45- },
46- {
47- id: 2,
48- title: 'Luxury Penthouse',
49- price: {
50- usd: 1200000,
51- eth: 600
52- },
53- location: 'Manhattan, NY',
54- image: 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80',
55- type: 'apartment',
56- roi: '6.8%',
57- metrics: {
58- totalInvestors: 203,
59- funded: '95%',
60- minInvestment: '$10',
61- monthlyIncome: '$680',
62- appreciation: '5.2%'
63- },
64- status: 'Almost Funded',
65- features: ['Doorman', 'Gym', 'Terrace'],
66- tokenDetails: {
67- totalTokens: 120000,
68- availableTokens: 6000,
69- tokenPrice: '$10'
70- }
71- },
72- {
73- id: 3,
74- title: 'Waterfront Estate',
75- price: {
76- usd: 2100000,
77- eth: 1050
78- },
79- location: 'Miami Beach, FL',
80- image: 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80',
81- type: 'house',
82- roi: '7.5%',
83- metrics: {
84- totalInvestors: 89,
85- funded: '45%',
86- minInvestment: '$10',
87- monthlyIncome: '$1200',
88- appreciation: '6.1%'
89- },
90- status: 'New Listing',
91- features: ['Waterfront', 'Dock', 'Wine Cellar'],
92- tokenDetails: {
93- totalTokens: 210000,
94- availableTokens: 115500,
95- tokenPrice: '$10'
96- }
97- },
98- {
99- id: 4,
100- title: 'Contemporary Smart Home',
101- price: {
102- usd: 950000,
103- eth: 475
104- },
105- location: 'Austin, TX',
106- image: 'https://images.unsplash.com/photo-1600607687939-ce8a6c25118c?w=800&q=80',
107- type: 'house',
108- roi: '8.1%',
109- metrics: {
110- totalInvestors: 67,
111- funded: '32%',
112- minInvestment: '$10',
113- monthlyIncome: '$640',
114- appreciation: '5.8%'
115- },
116- status: 'New Listing',
117- features: ['Smart Home', '3D Virtual Tour', 'Energy Efficient'],
118- tokenDetails: {
119- totalTokens: 95000,
120- availableTokens: 64600,
121- tokenPrice: '$10'
22+ // Fetch properties from API
23+ useEffect(() => {
24+ async function fetchProperties() {
25+ try {
26+ setLoading(true);
27+ const response = await fetch(`${API_URL}/api/property/list`);
28+ if (!response.ok) throw new Error('Failed to fetch properties');
29+ const data = await response.json();
30+ setProperties(data);
31+ } catch (err) {
32+ console.error('Error fetching properties:', err);
33+ setError(err.message);
34+ } finally {
35+ setLoading(false);
12236 }
12337 }
124- ];
38+
39+ fetchProperties();
40+ }, []);
12541
12642 const handleFilterChange = (key, value) => {
12743 setFilters(prev => ({ ...prev, [key]: value }));
12844 };
12945
46+ // Calculate funding percentage
47+ const getFundingPercent = (property) => {
48+ if (!property.total_tokens || !property.available_tokens) return 0;
49+ return Math.round(((property.total_tokens - property.available_tokens) / property.total_tokens) * 100);
50+ };
51+
52+ // Get status label
53+ const getStatusLabel = (property) => {
54+ const fundedPercent = getFundingPercent(property);
55+ if (fundedPercent >= 90) return 'Almost Funded';
56+ if (fundedPercent >= 30) return 'Active Investment';
57+ return 'New Listing';
58+ };
59+
60+ // Filter properties
13061 const filteredProperties = properties.filter(property => {
131- if (filters.propertyType !== 'all' && property.type !== filters.propertyType) return false;
132- if (filters.location && !property.location.toLowerCase().includes(filters.location.toLowerCase())) return false;
133- if (filters.minROI && parseFloat(property.roi) < parseFloat(filters.minROI)) return false;
134- if (filters.maxROI && parseFloat(property.roi) > parseFloat(filters.maxROI)) return false;
135-
62+ if (filters.propertyType !== 'all' && property.property_type !== filters.propertyType) return false;
63+ if (filters.location && !`${property.city_name} ${property.state_name}`.toLowerCase().includes(filters.location.toLowerCase())) return false;
64+ if (filters.minROI && property.roi_percentage < parseFloat(filters.minROI)) return false;
65+
13666 if (filters.priceRange !== 'all') {
13767 const [min, max] = filters.priceRange.split('-').map(Number);
138- if (max && (property.price.usd < min || property.price.usd > max)) return false;
139- if (!max && property.price.usd < min) return false;
68+ if (max && (property.price < min || property.price > max)) return false;
69+ if (!max && property.price < min) return false;
14070 }
14171
142- if (filters.fundingStatus !== 'all') {
143- const fundedPercentage = parseInt(property.metrics.funded);
144- switch (filters.fundingStatus) {
145- case 'new':
146- if (fundedPercentage > 30) return false;
147- break;
148- case 'active':
149- if (fundedPercentage < 30 || fundedPercentage >= 90) return false;
150- break;
151- case 'almostFunded':
152- if (fundedPercentage < 90) return false;
153- break;
154- default:
155- break
156- }
157- }
158-
15972 return true;
16073 });
16174
162- // Sort properties based on selected criteria
75+ // Sort properties
16376 const sortedProperties = [...filteredProperties].sort((a, b) => {
16477 switch (filters.sortBy) {
16578 case 'priceAsc':
166- return a.price.usd - b.price.usd;
79+ return a.price - b.price;
16780 case 'priceDesc':
168- return b.price.usd - a.price.usd;
81+ return b.price - a.price;
16982 case 'roiDesc':
170- return parseFloat(b.roi) - parseFloat(a.roi);
83+ return (b.roi_percentage || 0) - (a.roi_percentage || 0);
17184 case 'fundingDesc':
172- return parseInt(b.metrics.funded) - parseInt(a.metrics.funded);
85+ return getFundingPercent(b) - getFundingPercent(a);
17386 default:
174- return 0;
87+ return new Date(b.created_at) - new Date(a.created_at);
17588 }
17689 });
17790
91+ if (loading) {
92+ return (
93+ <div className="min-h-screen flex items-center justify-center">
94+ <FiLoader className="animate-spin text-4xl text-primary-600" />
95+ </div>
96+ );
97+ }
98+
99+ if (error) {
100+ return (
101+ <div className="min-h-screen flex items-center justify-center">
102+ <div className="text-center">
103+ <p className="text-red-600 mb-4">Error: {error}</p>
104+ <button
105+ onClick={() => window.location.reload()}
106+ className="btn"
107+ >
108+ Retry
109+ </button>
110+ </div>
111+ </div>
112+ );
113+ }
114+
178115 return (
179116 <div className="min-h-screen bg-gradient-to-br from-sapphire-50 via-white to-platinum-100">
180117 {/* Header */}
@@ -183,6 +120,7 @@ function Properties() {
183120 <div className="flex justify-between items-center">
184121 <h1 className="text-3xl font-bold text-sapphire-800">Investment Properties</h1>
185122 <div className="flex items-center space-x-4">
123+ <span className="text-platinum-600">{properties.length} properties</span>
186124 <button
187125 className={`p-2 rounded-xl transition-all duration-300 ${showFilters ? 'glass-card text-primary-600' : 'hover:bg-glass'}`}
188126 onClick={() => setShowFilters(!showFilters)}
@@ -198,11 +136,9 @@ function Properties() {
198136 {showFilters && (
199137 <div className="glass-card border-t border-glass">
200138 <div className="container py-6">
201- <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
139+ <div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4">
202140 <div>
203- <label className="block text-sm font-medium text-sapphire-700 mb-1">
204- Price Range
205- </label>
141+ <label className="block text-sm font-medium text-sapphire-700 mb-1">Price Range</label>
206142 <select
207143 className="input"
208144 value={filters.priceRange}
@@ -216,67 +152,43 @@ function Properties() {
216152 </div>
217153
218154 <div>
219- <label className="block text-sm font-medium text-sapphire-700 mb-1">
220- Property Type
221- </label>
155+ <label className="block text-sm font-medium text-sapphire-700 mb-1">Property Type</label>
222156 <select
223157 className="input"
224158 value={filters.propertyType}
225159 onChange={(e) => handleFilterChange('propertyType', e.target.value)}
226160 >
227161 <option value="all">All Types</option>
228- <option value="house">House</option>
229- <option value="apartment">Apartment</option>
230- <option value="villa">Villa</option>
162+ <option value="residential">Residential</option>
163+ <option value="commercial">Commercial</option>
164+ <option value="agricultural">Agricultural</option>
231165 </select>
232166 </div>
233167
234168 <div>
235- <label className="block text-sm font-medium text-sapphire-700 mb-1">
236- Location
237- </label>
169+ <label className="block text-sm font-medium text-sapphire-700 mb-1">Location</label>
238170 <input
239171 type="text"
240172 className="input"
241- placeholder="Enter location"
173+ placeholder="City or state..."
242174 value={filters.location}
243175 onChange={(e) => handleFilterChange('location', e.target.value)}
244176 />
245177 </div>
246178
247179 <div>
248- <label className="block text-sm font-medium text-sapphire-700 mb-1">
249- Minimum ROI
250- </label>
180+ <label className="block text-sm font-medium text-sapphire-700 mb-1">Min ROI %</label>
251181 <input
252182 type="number"
253183 className="input"
254- placeholder="Min ROI %"
184+ placeholder="e.g. 5"
255185 value={filters.minROI}
256186 onChange={(e) => handleFilterChange('minROI', e.target.value)}
257187 />
258188 </div>
259189
260190 <div>
261- <label className="block text-sm font-medium text-sapphire-700 mb-1">
262- Funding Status
263- </label>
264- <select
265- className="input"
266- value={filters.fundingStatus}
267- onChange={(e) => handleFilterChange('fundingStatus', e.target.value)}
268- >
269- <option value="all">All Statuses</option>
270- <option value="new">New Listings</option>
271- <option value="active">Active Funding</option>
272- <option value="almostFunded">Almost Funded</option>
273- </select>
274- </div>
275-
276- <div>
277- <label className="block text-sm font-medium text-sapphire-700 mb-1">
278- Sort By
279- </label>
191+ <label className="block text-sm font-medium text-sapphire-700 mb-1">Sort By</label>
280192 <select
281193 className="input"
282194 value={filters.sortBy}
@@ -296,108 +208,122 @@ function Properties() {
296208
297209 {/* Properties Grid */}
298210 <div className="container py-8">
299- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
300- {sortedProperties.map((property, index) => (
301- <motion.div
302- key={property.id}
303- className="card group"
304- initial={{ opacity: 0, y: 20 }}
305- animate={{ opacity: 1, y: 0 }}
306- transition={{ delay: index * 0.1 }}
307- >
308- <Link to={`/properties/${property.id}`}>
309- <div className="relative h-48 rounded-t-2xl overflow-hidden">
310- <img
311- src={property.image}
312- alt={property.title}
313- className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
314- />
315- <div className="absolute top-4 right-4 glass-card px-3 py-1 text-primary-600 font-semibold text-sm">
316- {property.status}
211+ {sortedProperties.length === 0 ? (
212+ <div className="text-center py-12">
213+ <p className="text-platinum-600">No properties found matching your criteria.</p>
214+ </div>
215+ ) : (
216+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
217+ {sortedProperties.map((property, index) => (
218+ <motion.div
219+ key={property.id}
220+ className="card group"
221+ initial={{ opacity: 0, y: 20 }}
222+ animate={{ opacity: 1, y: 0 }}
223+ transition={{ delay: index * 0.1 }}
224+ >
225+ <Link to={`/properties/${property.slug}`}>
226+ <div className="relative h-48 rounded-t-2xl overflow-hidden">
227+ <img
228+ src={property.images?.[0] || 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80'}
229+ alt={property.title}
230+ className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
231+ />
232+ <div className="absolute top-4 right-4 glass-card px-3 py-1 text-primary-600 font-semibold text-sm">
233+ {getStatusLabel(property)}
234+ </div>
235+ {property.contractState && (
236+ <div className="absolute top-4 left-4 bg-sapphire-900/80 text-white px-2 py-1 rounded text-xs">
237+ {property.contractState.stateLabel}
238+ </div>
239+ )}
317240 </div>
318- </div>
319- <div className="p-6">
320- <h3 className="text-xl font-semibold mb-2 text-sapphire-800">{property.title}</h3>
321- <p className="text-platinum-600 mb-4">{property.location}</p>
322241
323- {/* Price and ROI */}
324- <div className="flex justify-between items-center mb-4">
325- <div>
326- <p className="text-sm text-platinum-500">Investment Price</p>
327- <div className="flex items-center">
328- <FiDollarSign className="text-primary-600" />
329- <span className="font-semibold text-sapphire-800">${property.price.usd.toLocaleString()}</span>
242+ <div className="p-6">
243+ <h3 className="text-xl font-semibold mb-2 text-sapphire-800">{property.title}</h3>
244+ <p className="text-platinum-600 mb-4">{property.city_name}, {property.state_name}</p>
245+
246+ {/* Price and ROI */}
247+ <div className="flex justify-between items-center mb-4">
248+ <div>
249+ <p className="text-sm text-platinum-500">Investment Price</p>
250+ <div className="flex items-center">
251+ <FiDollarSign className="text-primary-600" />
252+ <span className="font-semibold text-sapphire-800">
253+ {Number(property.price).toLocaleString()}
254+ </span>
255+ </div>
256+ <div className="flex items-center text-sm text-primary-600">
257+ <FaEthereum className="mr-1" />
258+ <span>{property.price_eth} ETH</span>
259+ </div>
330260 </div>
331- <div className="flex items-center text-sm text-primary-600">
332- <FaEthereum className="mr-1" />
333- <span>{property.price.eth} ETH</span>
261+ <div className="text-right">
262+ <p className="text-sm text-platinum-500">Annual ROI</p>
263+ <div className="flex items-center justify-end text-green-600">
264+ <FiTrendingUp className="mr-1" />
265+ <span className="font-semibold">{property.roi_percentage}%</span>
266+ </div>
334267 </div>
335268 </div>
336- <div className="text-right">
337- <p className="text-sm text-platinum-500">Annual ROI</p>
338- <div className="flex items-center justify-end text-green-600">
339- <FiTrendingUp className="mr-1" />
340- <span className="font-semibold">{property.roi}</span>
269+
270+ {/* Investment Metrics */}
271+ <div className="space-y-2 mb-4">
272+ <div className="flex justify-between text-sm">
273+ <span className="text-platinum-600">Property Type</span>
274+ <span className="font-medium text-sapphire-700 capitalize">{property.type_title || property.property_type}</span>
275+ </div>
276+ <div className="flex justify-between text-sm">
277+ <span className="text-platinum-600">Min Investment</span>
278+ <span className="font-medium text-sapphire-700">${property.min_investment}</span>
341279 </div>
342280 </div>
343- </div>
344281
345- {/* Investment Metrics */}
346- <div className="space-y-2 mb-4">
347- <div className="flex justify-between text-sm">
348- <span className="text-platinum-600">Monthly Income</span>
349- <span className="font-medium text-sapphire-700">{property.metrics.monthlyIncome}</span>
350- </div>
351- <div className="flex justify-between text-sm">
352- <span className="text-platinum-600">Appreciation</span>
353- <span className="font-medium text-sapphire-700">{property.metrics.appreciation}</span>
354- </div>
355- <div className="flex justify-between text-sm">
356- <span className="text-platinum-600">Min Investment</span>
357- <span className="font-medium text-sapphire-700">{property.metrics.minInvestment}</span>
282+ {/* Funding Progress */}
283+ <div className="mb-4">
284+ <div className="flex justify-between text-sm mb-1">
285+ <span className="text-platinum-600">Funding Progress</span>
286+ <span className="font-medium text-sapphire-700">{getFundingPercent(property)}%</span>
287+ </div>
288+ <div className="w-full bg-platinum-100 rounded-full h-2">
289+ <div
290+ className="bg-gradient-to-r from-primary-500 to-primary-600 h-2 rounded-full transition-all duration-500"
291+ style={{ width: `${getFundingPercent(property)}%` }}
292+ />
293+ </div>
358294 </div>
359- </div>
360295
361- {/* Funding Progress */}
362- <div className="mb-4">
363- <div className="flex justify-between text-sm mb-1">
364- <span className="text-platinum-600">Funding Progress</span>
365- <span className="font-medium text-sapphire-700">{property.metrics.funded}</span>
366- </div>
367- <div className="w-full bg-platinum-100 rounded-full h-2">
368- <div
369- className="bg-gradient-to-r from-primary-500 to-primary-600 h-2 rounded-full"
370- style={{ width: property.metrics.funded }}
371- />
296+ {/* Token Details */}
297+ <div className="glass-card bg-sapphire-50/50 p-3 mb-4">
298+ <div className="flex justify-between text-sm">
299+ <span className="text-platinum-600">Available Tokens</span>
300+ <span className="font-medium text-sapphire-700">
301+ {Number(property.available_tokens).toLocaleString()} / {Number(property.total_tokens).toLocaleString()}
302+ </span>
303+ </div>
304+ {property.contract_address && (
305+ <div className="flex justify-between text-sm mt-1">
306+ <span className="text-platinum-600">Contract</span>
307+ <span className="font-medium text-sapphire-700 font-mono text-xs">
308+ {property.contract_address.slice(0, 6)}...{property.contract_address.slice(-4)}
309+ </span>
310+ </div>
311+ )}
372312 </div>
373- </div>
374313
375- {/* Token Details */}
376- <div className="glass-card bg-sapphire-50/50 p-3 mb-4">
377- <div className="flex justify-between text-sm">
378- <span className="text-platinum-600">Available Tokens</span>
379- <span className="font-medium text-sapphire-700">
380- {property.tokenDetails.availableTokens.toLocaleString()} / {property.tokenDetails.totalTokens.toLocaleString()}
381- </span>
382- </div>
383- <div className="flex justify-between text-sm mt-1">
384- <span className="text-platinum-600">Token Price</span>
385- <span className="font-medium text-sapphire-700">{property.tokenDetails.tokenPrice}</span>
386- </div>
314+ <button className="btn w-full flex items-center justify-center">
315+ View Details
316+ <FiArrowRight className="ml-2" />
317+ </button>
387318 </div>
388-
389- <button className="btn w-full flex items-center justify-center">
390- Invest Now
391- <FiArrowRight className="ml-2" />
392- </button>
393- </div>
394- </Link>
395- </motion.div>
396- ))}
397- </div>
319+ </Link>
320+ </motion.div>
321+ ))}
322+ </div>
323+ )}
398324 </div>
399325 </div>
400326 );
401327 }
402328
403-export default Properties;
329+export default Properties;
+352-198src/pages/PropertyDetail.jsx
@@ -1,79 +1,156 @@
1+import { useState, useEffect } from 'react';
12 import { useParams, Link } from 'react-router-dom';
23 import { motion } from 'framer-motion';
3-import { FiHome, FiMaximize2, FiCalendar, FiTrendingUp, FiUsers, FiDollarSign, FiGrid } from 'react-icons/fi';
4+import { FiHome, FiMaximize2, FiCalendar, FiTrendingUp, FiUsers, FiDollarSign, FiGrid, FiLoader, FiCheck, FiClock } from 'react-icons/fi';
45 import { FacebookShareButton, TwitterShareButton, LinkedinShareButton } from 'react-share';
56 import { FaFacebook, FaTwitter, FaLinkedin, FaEthereum, FaWallet } from 'react-icons/fa';
7+import { useWallet } from '../hooks/useWallet';
8+import { useContracts } from '../hooks/useContracts';
9+
10+const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001';
11+
12+// Contract state mapping
13+const ContractStates = {
14+ 0: { label: 'Awaiting Seller Signature', color: 'yellow', canInvest: false },
15+ 1: { label: 'Awaiting Buyer Deposit', color: 'blue', canInvest: true },
16+ 2: { label: 'Under Realtor Review', color: 'purple', canInvest: false },
17+ 3: { label: 'Ready to Finalize', color: 'orange', canInvest: false },
18+ 4: { label: 'Transaction Complete', color: 'green', canInvest: false },
19+ 5: { label: 'Transaction Rejected', color: 'red', canInvest: false },
20+};
621
722 function PropertyDetail() {
8- const { id } = useParams();
9-
10- const property = {
11- id: parseInt(id),
12- title: 'Modern Villa with Pool',
13- price: {
14- usd: 850000,
15- eth: 425
16- },
17- location: 'Beverly Hills, CA',
18- type: 'villa',
19- roi: '7.2%',
20- metrics: {
21- totalInvestors: 142,
22- funded: '89%',
23- minInvestment: '$10',
24- monthlyIncome: '$520',
25- appreciation: '4.5%',
26- rentalYield: '5.8%',
27- totalReturn: '10.3%'
28- },
29- status: 'Active Investment',
30- description: 'This stunning modern villa offers luxurious living spaces with high-end finishes throughout. The property has been tokenized for fractional ownership, allowing investors to participate in this premium real estate opportunity with as little as $10.',
31- features: [
32- 'Swimming Pool',
33- 'Smart Home System',
34- 'Gourmet Kitchen',
35- 'Home Theater',
36- 'Wine Cellar',
37- 'Outdoor Kitchen',
38- 'Fire Pit',
39- 'Three-Car Garage'
40- ],
41- tokenDetails: {
42- totalTokens: 85000,
43- availableTokens: 9350,
44- tokenPrice: '$10',
45- tokenSymbol: 'VILLA425',
46- contractAddress: '0x1234...5678',
47- blockchain: 'Ethereum'
48- },
49- financials: {
50- grossRent: '$8,500/month',
51- netRent: '$7,225/month',
52- expenses: {
53- management: '8%',
54- maintenance: '5%',
55- insurance: '2%',
56- property_tax: '1.2%'
57- },
58- projectedAppreciation: '4.5% annually'
59- },
60- yearBuilt: 2020,
61- parkingSpaces: 3,
62- lotSize: '0.5 acres',
63- images: [
64- 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80',
65- 'https://images.unsplash.com/photo-1512917774080-9991f1c4c750?w=800&q=80',
66- 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80'
67- ],
68- agent: {
69- name: 'John Doe',
70- phone: '+1 (555) 123-4567',
71- email: 'john@realestate.com',
72- image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=400&q=80'
23+ const { id } = useParams(); // This is actually the slug from the URL
24+ const { connect, isConnected, isConnecting, shortAddress, address } = useWallet();
25+ const { signAsBuyerWithDeposit, signAsSeller, loading: contractLoading, error: contractError } = useContracts();
26+
27+ const [property, setProperty] = useState(null);
28+ const [loading, setLoading] = useState(true);
29+ const [error, setError] = useState(null);
30+ const [investAmount, setInvestAmount] = useState('');
31+ const [showInvestModal, setShowInvestModal] = useState(false);
32+ const [txStatus, setTxStatus] = useState(null); // 'pending', 'success', 'error'
33+ const [txHash, setTxHash] = useState(null);
34+
35+ // Fetch property from API
36+ useEffect(() => {
37+ async function fetchProperty() {
38+ try {
39+ setLoading(true);
40+ const response = await fetch(`${API_URL}/api/property/single/${id}`);
41+ if (!response.ok) throw new Error('Property not found');
42+ const data = await response.json();
43+ setProperty(data);
44+ } catch (err) {
45+ console.error('Error fetching property:', err);
46+ setError(err.message);
47+ } finally {
48+ setLoading(false);
49+ }
50+ }
51+
52+ fetchProperty();
53+ }, [id]);
54+
55+ // Calculate funding percentage
56+ const getFundingPercent = () => {
57+ if (!property?.total_tokens || !property?.available_tokens) return 0;
58+ return Math.round(((property.total_tokens - property.available_tokens) / property.total_tokens) * 100);
59+ };
60+
61+ // Get contract state info
62+ const getContractStateInfo = () => {
63+ if (!property?.contractState) return null;
64+ return ContractStates[property.contractState.state] || ContractStates[0];
65+ };
66+
67+ // Check if current user is the buyer/seller/realtor
68+ const getUserRole = () => {
69+ if (!address || !property?.contractState) return null;
70+ const lowerAddress = address.toLowerCase();
71+ if (property.contractState.buyer?.toLowerCase() === lowerAddress) return 'buyer';
72+ if (property.contractState.seller?.toLowerCase() === lowerAddress) return 'seller';
73+ if (property.contractState.realtor?.toLowerCase() === lowerAddress) return 'realtor';
74+ return null;
75+ };
76+
77+ // Handle investment
78+ const handleInvest = async () => {
79+ if (!isConnected) {
80+ await connect();
81+ return;
82+ }
83+
84+ if (!investAmount || parseFloat(investAmount) <= 0) {
85+ alert('Please enter a valid investment amount');
86+ return;
87+ }
88+
89+ setShowInvestModal(true);
90+ };
91+
92+ // Execute the investment transaction
93+ const executeInvestment = async () => {
94+ try {
95+ setTxStatus('pending');
96+
97+ const receipt = await signAsBuyerWithDeposit(
98+ property.contract_address,
99+ investAmount
100+ );
101+
102+ setTxHash(receipt.transactionHash);
103+ setTxStatus('success');
104+
105+ // Refresh property data after successful investment
106+ setTimeout(() => {
107+ window.location.reload();
108+ }, 3000);
109+
110+ } catch (err) {
111+ console.error('Investment error:', err);
112+ setTxStatus('error');
73113 }
74114 };
75115
116+ // Handle seller sign
117+ const handleSellerSign = async () => {
118+ try {
119+ setTxStatus('pending');
120+ const receipt = await signAsSeller(property.contract_address);
121+ setTxHash(receipt.transactionHash);
122+ setTxStatus('success');
123+ setTimeout(() => window.location.reload(), 3000);
124+ } catch (err) {
125+ console.error('Seller sign error:', err);
126+ setTxStatus('error');
127+ }
128+ };
129+
130+ if (loading) {
131+ return (
132+ <div className="min-h-screen flex items-center justify-center">
133+ <FiLoader className="animate-spin text-4xl text-primary-600" />
134+ </div>
135+ );
136+ }
137+
138+ if (error || !property) {
139+ return (
140+ <div className="min-h-screen flex items-center justify-center">
141+ <div className="text-center">
142+ <p className="text-red-600 mb-4">Error: {error || 'Property not found'}</p>
143+ <Link to="/properties" className="btn">Back to Properties</Link>
144+ </div>
145+ </div>
146+ );
147+ }
148+
149+ const contractStateInfo = getContractStateInfo();
150+ const userRole = getUserRole();
76151 const shareUrl = window.location.href;
152+ const minDeposit = property.contractState ? parseFloat(property.contractState.priceEth) * 0.1 : 0;
153+ const maxDeposit = property.contractState ? parseFloat(property.contractState.priceEth) : 0;
77154
78155 return (
79156 <div className="min-h-screen bg-secondary-50">
@@ -96,31 +173,42 @@ function PropertyDetail() {
96173 {/* Left Column */}
97174 <div className="lg:col-span-2 space-y-8">
98175 {/* Image Gallery */}
99- <motion.div
100- initial={{ opacity: 0, y: 20 }}
101- animate={{ opacity: 1, y: 0 }}
102- className="space-y-4"
103- >
176+ <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="space-y-4">
104177 <div className="h-96 rounded-lg overflow-hidden">
105178 <img
106- src={property.images[0]}
179+ src={property.images?.[0] || 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80'}
107180 alt={property.title}
108181 className="w-full h-full object-cover"
109182 />
110183 </div>
111- <div className="grid grid-cols-3 gap-4">
112- {property.images.slice(1).map((image, index) => (
113- <div key={index} className="h-32 rounded-lg overflow-hidden">
114- <img
115- src={image}
116- alt={`${property.title} - ${index + 1}`}
117- className="w-full h-full object-cover"
118- />
119- </div>
120- ))}
121- </div>
122184 </motion.div>
123185
186+ {/* Contract Status Banner */}
187+ {contractStateInfo && (
188+ <motion.div
189+ initial={{ opacity: 0, y: 20 }}
190+ animate={{ opacity: 1, y: 0 }}
191+ className={`bg-${contractStateInfo.color}-100 border border-${contractStateInfo.color}-300 rounded-lg p-4`}
192+ >
193+ <div className="flex items-center justify-between">
194+ <div className="flex items-center space-x-3">
195+ <FiClock className={`text-${contractStateInfo.color}-600 text-xl`} />
196+ <div>
197+ <p className="font-semibold">Contract Status: {contractStateInfo.label}</p>
198+ <p className="text-sm text-secondary-600">
199+ Contract: {property.contract_address?.slice(0, 10)}...{property.contract_address?.slice(-8)}
200+ </p>
201+ </div>
202+ </div>
203+ {userRole && (
204+ <span className="px-3 py-1 bg-white rounded-full text-sm font-medium">
205+ You are the {userRole}
206+ </span>
207+ )}
208+ </div>
209+ </motion.div>
210+ )}
211+
124212 {/* Property Details */}
125213 <motion.div
126214 initial={{ opacity: 0, y: 20 }}
@@ -130,169 +218,183 @@ function PropertyDetail() {
130218 >
131219 <h2 className="text-2xl font-bold mb-4">Property Details</h2>
132220 <p className="text-secondary-600 mb-6">{property.description}</p>
133-
221+
134222 <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
135223 <div className="flex items-center space-x-2">
136- {/* <FiBed className="text-primary-600" /> */}
137- <span>{property.parkingSpaces} Parking</span>
224+ <FiMaximize2 className="text-primary-600" />
225+ <span>{property.length}x{property.breadth} sq ft</span>
138226 </div>
139227 <div className="flex items-center space-x-2">
140- <FiMaximize2 className="text-primary-600" />
141- <span>{property.lotSize}</span>
228+ <FiHome className="text-primary-600" />
229+ <span className="capitalize">{property.type_title || property.property_type}</span>
142230 </div>
143231 <div className="flex items-center space-x-2">
144232 <FiCalendar className="text-primary-600" />
145- <span>Built {property.yearBuilt}</span>
233+ <span>{property.locality}</span>
146234 </div>
147235 <div className="flex items-center space-x-2">
148236 <FiUsers className="text-primary-600" />
149- <span>{property.metrics.totalInvestors} Investors</span>
237+ <span>{property.investorStats?.total_investors || 0} Investors</span>
150238 </div>
151239 </div>
152240
153- <h3 className="text-xl font-semibold mb-4">Features</h3>
154- <div className="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
155- {property.features.map((feature, index) => (
156- <div key={index} className="flex items-center space-x-2">
157- <FiHome className="text-primary-600" />
158- <span>{feature}</span>
159- </div>
160- ))}
161- </div>
162-
163241 {/* Token Details */}
164242 <h3 className="text-xl font-semibold mb-4">Token Information</h3>
165243 <div className="bg-secondary-50 rounded-lg p-6 mb-6">
166244 <div className="grid grid-cols-2 gap-4">
167245 <div>
168- <p className="text-sm text-secondary-600">Token Symbol</p>
169- <p className="font-semibold">{property.tokenDetails.tokenSymbol}</p>
246+ <p className="text-sm text-secondary-600">Available Tokens</p>
247+ <p className="font-semibold">{Number(property.available_tokens).toLocaleString()}</p>
170248 </div>
171249 <div>
172- <p className="text-sm text-secondary-600">Token Price</p>
173- <p className="font-semibold">{property.tokenDetails.tokenPrice}</p>
250+ <p className="text-sm text-secondary-600">Total Supply</p>
251+ <p className="font-semibold">{Number(property.total_tokens).toLocaleString()}</p>
174252 </div>
175253 <div>
176- <p className="text-sm text-secondary-600">Available Tokens</p>
177- <p className="font-semibold">{property.tokenDetails.availableTokens.toLocaleString()}</p>
254+ <p className="text-sm text-secondary-600">Min Investment</p>
255+ <p className="font-semibold">${property.min_investment}</p>
178256 </div>
179257 <div>
180- <p className="text-sm text-secondary-600">Total Supply</p>
181- <p className="font-semibold">{property.tokenDetails.totalTokens.toLocaleString()}</p>
182- </div>
183- <div className="col-span-2">
184- <p className="text-sm text-secondary-600">Smart Contract</p>
185- <p className="font-mono text-sm">{property.tokenDetails.contractAddress}</p>
258+ <p className="text-sm text-secondary-600">ROI</p>
259+ <p className="font-semibold text-green-600">{property.roi_percentage}%</p>
186260 </div>
187- </div>
188- </div>
189-
190- {/* Financial Details */}
191- <h3 className="text-xl font-semibold mb-4">Financial Overview</h3>
192- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
193- <div className="bg-secondary-50 rounded-lg p-6">
194- <h4 className="font-semibold mb-4">Rental Income</h4>
195- <div className="space-y-2">
196- <div className="flex justify-between">
197- <span className="text-secondary-600">Gross Rent</span>
198- <span className="font-medium">{property.financials.grossRent}</span>
261+ {property.contract_address && (
262+ <div className="col-span-2">
263+ <p className="text-sm text-secondary-600">Smart Contract</p>
264+ <p className="font-mono text-sm break-all">{property.contract_address}</p>
199265 </div>
200- <div className="flex justify-between">
201- <span className="text-secondary-600">Net Rent</span>
202- <span className="font-medium">{property.financials.netRent}</span>
203- </div>
204- </div>
205- </div>
206- <div className="bg-secondary-50 rounded-lg p-6">
207- <h4 className="font-semibold mb-4">Expenses</h4>
208- <div className="space-y-2">
209- {Object.entries(property.financials.expenses).map(([key, value]) => (
210- <div key={key} className="flex justify-between">
211- <span className="text-secondary-600">{key.replace('_', ' ').charAt(0).toUpperCase() + key.slice(1)}</span>
212- <span className="font-medium">{value}</span>
213- </div>
214- ))}
215- </div>
266+ )}
216267 </div>
217268 </div>
218269 </motion.div>
219270 </div>
220271
221- {/* Right Column */}
272+ {/* Right Column - Investment Card */}
222273 <motion.div
223274 initial={{ opacity: 0, x: 20 }}
224275 animate={{ opacity: 1, x: 0 }}
225276 transition={{ delay: 0.2 }}
226277 className="space-y-6"
227278 >
228- {/* Investment Card */}
229279 <div className="bg-white rounded-lg shadow-md p-6">
230280 <div className="flex justify-between items-center mb-4">
231281 <div>
232282 <p className="text-sm text-secondary-500">Investment Price</p>
233283 <div className="flex items-center">
234284 <FiDollarSign className="text-primary-600" />
235- <span className="text-2xl font-bold">${property.price.usd.toLocaleString()}</span>
285+ <span className="text-2xl font-bold">${Number(property.price).toLocaleString()}</span>
236286 </div>
237287 <div className="flex items-center text-primary-600">
238288 <FaEthereum className="mr-1" />
239- <span>{property.price.eth} ETH</span>
289+ <span>{property.price_eth} ETH</span>
240290 </div>
241291 </div>
242292 <div className="text-right">
243293 <p className="text-sm text-secondary-500">Annual ROI</p>
244294 <div className="flex items-center justify-end text-green-600">
245295 <FiTrendingUp className="mr-1" />
246- <span className="text-2xl font-bold">{property.roi}</span>
296+ <span className="text-2xl font-bold">{property.roi_percentage}%</span>
247297 </div>
248298 </div>
249299 </div>
250300
251- {/* Investment Metrics */}
252- <div className="space-y-3 mb-6">
253- <div className="flex justify-between">
254- <span className="text-secondary-600">Rental Yield</span>
255- <span className="font-medium">{property.metrics.rentalYield}</span>
256- </div>
257- <div className="flex justify-between">
258- <span className="text-secondary-600">Appreciation</span>
259- <span className="font-medium">{property.metrics.appreciation}</span>
260- </div>
261- <div className="flex justify-between">
262- <span className="text-secondary-600">Total Return</span>
263- <span className="font-medium text-green-600">{property.metrics.totalReturn}</span>
264- </div>
265- </div>
266-
267301 {/* Funding Progress */}
268302 <div className="mb-6">
269303 <div className="flex justify-between text-sm mb-1">
270304 <span className="text-secondary-600">Funding Progress</span>
271- <span className="font-medium">{property.metrics.funded}</span>
305+ <span className="font-medium">{getFundingPercent()}%</span>
272306 </div>
273307 <div className="w-full bg-secondary-100 rounded-full h-2">
274308 <div
275309 className="bg-primary-600 h-2 rounded-full"
276- style={{ width: property.metrics.funded }}
310+ style={{ width: `${getFundingPercent()}%` }}
277311 />
278312 </div>
279- <p className="text-sm text-secondary-500 mt-1">
280- Min Investment: {property.metrics.minInvestment}
281- </p>
282313 </div>
283-
314+
315+ {/* Investment Input */}
316+ {contractStateInfo?.canInvest && (
317+ <div className="mb-4">
318+ <label className="block text-sm font-medium text-secondary-700 mb-2">
319+ Investment Amount (ETH)
320+ </label>
321+ <div className="flex space-x-2">
322+ <input
323+ type="number"
324+ step="0.01"
325+ min={minDeposit}
326+ max={maxDeposit}
327+ value={investAmount}
328+ onChange={(e) => setInvestAmount(e.target.value)}
329+ placeholder={`Min: ${minDeposit.toFixed(2)} ETH`}
330+ className="input flex-1"
331+ />
332+ </div>
333+ <p className="text-xs text-secondary-500 mt-1">
334+ Deposit range: {minDeposit.toFixed(2)} - {maxDeposit.toFixed(2)} ETH (10-100% of price)
335+ </p>
336+ </div>
337+ )}
338+
339+ {/* Action Buttons based on state and role */}
340+ {!isConnected ? (
341+ <button
342+ onClick={connect}
343+ disabled={isConnecting}
344+ className="btn w-full mb-4 flex items-center justify-center"
345+ >
346+ <FaWallet className="mr-2" />
347+ {isConnecting ? 'Connecting...' : 'Connect Wallet'}
348+ </button>
349+ ) : (
350+ <>
351+ {/* Seller Sign Button */}
352+ {userRole === 'seller' && property.contractState?.state === 0 && (
353+ <button
354+ onClick={handleSellerSign}
355+ disabled={contractLoading}
356+ className="btn w-full mb-4 flex items-center justify-center bg-green-600 hover:bg-green-700"
357+ >
358+ {contractLoading ? <FiLoader className="animate-spin mr-2" /> : <FiCheck className="mr-2" />}
359+ Sign as Seller
360+ </button>
361+ )}
362+
363+ {/* Buyer Invest Button */}
364+ {contractStateInfo?.canInvest && (
365+ <button
366+ onClick={handleInvest}
367+ disabled={contractLoading || !investAmount}
368+ className="btn w-full mb-4 flex items-center justify-center"
369+ >
370+ <FaWallet className="mr-2" />
371+ {contractLoading ? 'Processing...' : `Invest ${investAmount || '0'} ETH`}
372+ </button>
373+ )}
374+
375+ {/* Show connected address */}
376+ <p className="text-center text-sm text-secondary-500 mb-4">
377+ Connected: {shortAddress}
378+ </p>
379+ </>
380+ )}
381+
382+ {/* Contract State Info */}
383+ {contractStateInfo && (
384+ <div className={`p-3 rounded-lg bg-${contractStateInfo.color}-50 border border-${contractStateInfo.color}-200 mb-4`}>
385+ <p className="text-sm font-medium text-center">{contractStateInfo.label}</p>
386+ </div>
387+ )}
388+
284389 <Link
285- to={`/property-3d/${id}`}
286- className="btn w-full mb-4 flex items-center justify-center">
390+ to={`/property-3d/${property.id}`}
391+ className="btn-secondary w-full mb-4 flex items-center justify-center"
392+ >
287393 <FiGrid className="mr-2" />
288394 View 3D Model
289395 </Link>
290396
291- <button className="btn w-full mb-4 flex items-center justify-center">
292- <FaWallet className="mr-2" />
293- Connect Wallet to Invest
294- </button>
295-
397+ {/* Share buttons */}
296398 <div className="flex items-center justify-center space-x-4 pt-4 border-t">
297399 <FacebookShareButton url={shareUrl}>
298400 <FaFacebook className="text-2xl text-blue-600 hover:opacity-80" />
@@ -305,35 +407,87 @@ function PropertyDetail() {
305407 </LinkedinShareButton>
306408 </div>
307409 </div>
410+ </motion.div>
411+ </div>
412+ </div>
308413
309- {/* Agent Card */}
310- <div className="bg-white rounded-lg shadow-md p-6">
311- <div className="flex items-center space-x-4 mb-4">
312- <img
313- src={property.agent.image}
314- alt={property.agent.name}
315- className="w-16 h-16 rounded-full object-cover"
316- />
317- <div>
318- <h3 className="font-semibold">{property.agent.name}</h3>
319- <p className="text-sm text-secondary-600">Investment Advisor</p>
320- </div>
414+ {/* Investment Confirmation Modal */}
415+ {showInvestModal && (
416+ <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
417+ <motion.div
418+ initial={{ opacity: 0, scale: 0.95 }}
419+ animate={{ opacity: 1, scale: 1 }}
420+ className="bg-white rounded-xl p-6 max-w-md w-full mx-4"
421+ >
422+ {txStatus === 'pending' ? (
423+ <div className="text-center py-8">
424+ <FiLoader className="animate-spin text-4xl text-primary-600 mx-auto mb-4" />
425+ <p className="text-lg font-semibold">Processing Transaction</p>
426+ <p className="text-secondary-600">Please confirm in your wallet...</p>
321427 </div>
322- <div className="space-y-2">
323- <p className="text-sm">
324- <span className="font-medium">Phone:</span> {property.agent.phone}
325- </p>
326- <p className="text-sm">
327- <span className="font-medium">Email:</span> {property.agent.email}
328- </p>
428+ ) : txStatus === 'success' ? (
429+ <div className="text-center py-8">
430+ <FiCheck className="text-4xl text-green-600 mx-auto mb-4" />
431+ <p className="text-lg font-semibold text-green-600">Investment Successful!</p>
432+ <p className="text-secondary-600 mb-4">Your transaction has been confirmed.</p>
433+ {txHash && (
434+ <p className="text-xs font-mono bg-secondary-100 p-2 rounded break-all">
435+ TX: {txHash}
436+ </p>
437+ )}
329438 </div>
330- <button className="btn-secondary w-full mt-4">
331- Schedule Consultation
332- </button>
333- </div>
439+ ) : txStatus === 'error' ? (
440+ <div className="text-center py-8">
441+ <p className="text-lg font-semibold text-red-600 mb-4">Transaction Failed</p>
442+ <p className="text-secondary-600 mb-4">{contractError || 'Please try again'}</p>
443+ <button
444+ onClick={() => { setTxStatus(null); setShowInvestModal(false); }}
445+ className="btn"
446+ >
447+ Close
448+ </button>
449+ </div>
450+ ) : (
451+ <>
452+ <h3 className="text-xl font-bold mb-4">Confirm Investment</h3>
453+ <div className="space-y-3 mb-6">
454+ <div className="flex justify-between">
455+ <span className="text-secondary-600">Property</span>
456+ <span className="font-medium">{property.title}</span>
457+ </div>
458+ <div className="flex justify-between">
459+ <span className="text-secondary-600">Investment Amount</span>
460+ <span className="font-medium">{investAmount} ETH</span>
461+ </div>
462+ <div className="flex justify-between">
463+ <span className="text-secondary-600">Your Address</span>
464+ <span className="font-mono text-sm">{shortAddress}</span>
465+ </div>
466+ </div>
467+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 mb-6">
468+ <p className="text-sm text-yellow-800">
469+ This will send {investAmount} ETH to the property smart contract as your deposit.
470+ </p>
471+ </div>
472+ <div className="flex space-x-3">
473+ <button
474+ onClick={() => setShowInvestModal(false)}
475+ className="btn-secondary flex-1"
476+ >
477+ Cancel
478+ </button>
479+ <button
480+ onClick={executeInvestment}
481+ className="btn flex-1"
482+ >
483+ Confirm Investment
484+ </button>
485+ </div>
486+ </>
487+ )}
334488 </motion.div>
335489 </div>
336- </div>
490+ )}
337491 </div>
338492 );
339493 }
+29-0src/store/wallet.js
@@ -0,0 +1,29 @@
1+import { atom } from 'jotai';
2+
3+// Core wallet state
4+export const addressAtom = atom(null);
5+export const providerAtom = atom(null);
6+export const signerAtom = atom(null);
7+export const chainIdAtom = atom(null);
8+
9+// Connection status
10+export const isConnectingAtom = atom(false);
11+export const isConnectedAtom = atom((get) => get(addressAtom) !== null);
12+
13+// Error state
14+export const walletErrorAtom = atom(null);
15+
16+// Derived atom for shortened address display
17+export const shortAddressAtom = atom((get) => {
18+ const address = get(addressAtom);
19+ if (!address) return null;
20+ return `${address.slice(0, 6)}...${address.slice(-4)}`;
21+});
22+
23+// Check if on correct network (Hardhat local = 31337)
24+export const isCorrectNetworkAtom = atom((get) => {
25+ const chainId = get(chainIdAtom);
26+ // Accept Hardhat (31337) or mainnet (1) or common testnets
27+ const validChains = [31337, 1, 5, 11155111]; // hardhat, mainnet, goerli, sepolia
28+ return chainId ? validChains.includes(chainId) : false;
29+});
+183-0src/utils/contracts.js
@@ -0,0 +1,183 @@
1+import { ethers } from 'ethers';
2+
3+// Contract addresses - update these after deployment
4+export const CONTRACTS = {
5+ // These get set by deploy script - update after running npm run deploy:local
6+ FACTORY: process.env.REACT_APP_FACTORY_ADDRESS || '0x5FbDB2315678afecb367f032d93F642f64180aa3',
7+};
8+
9+// Contract state enums
10+export const ContractState = {
11+ WaitingSellerSignature: 0,
12+ WaitingBuyerSignature: 1,
13+ WaitingRealtorReview: 2,
14+ WaitingFinalization: 3,
15+ Finalized: 4,
16+ Rejected: 5,
17+};
18+
19+export const ContractStateLabels = {
20+ 0: 'Waiting for Seller',
21+ 1: 'Waiting for Buyer',
22+ 2: 'Under Review',
23+ 3: 'Ready to Finalize',
24+ 4: 'Completed',
25+ 5: 'Rejected',
26+};
27+
28+// ABIs (minimal versions with just the functions we need)
29+export const FACTORY_ABI = [
30+ 'event ContractCreated(address indexed contractAddress, address indexed realtor, address indexed seller, address buyer, uint256 price)',
31+ 'function create(string _address, string _zip, string _city, uint256 _realtorFee, uint256 _price, address _seller, address _buyer) returns (address)',
32+ 'function getInstance(uint256 index) view returns (address)',
33+ 'function getInstances() view returns (address[])',
34+ 'function getInstanceCount() view returns (uint256)',
35+ 'function getContractsByRealtor(address realtor) view returns (address[])',
36+ 'function getContractsBySeller(address seller) view returns (address[])',
37+ 'function getContractsByBuyer(address buyer) view returns (address[])',
38+];
39+
40+export const HOME_TRANSACTION_ABI = [
41+ 'event SellerSigned(address indexed seller)',
42+ 'event BuyerSignedAndDeposited(address indexed buyer, uint256 deposit)',
43+ 'event RealtorReviewed(address indexed realtor, bool accepted)',
44+ 'event TransactionFinalized(address indexed buyer, uint256 totalPrice)',
45+ 'event TransactionWithdrawn(address indexed initiator)',
46+ 'function buyer() view returns (address)',
47+ 'function seller() view returns (address)',
48+ 'function realtor() view returns (address)',
49+ 'function price() view returns (uint256)',
50+ 'function realtorFee() view returns (uint256)',
51+ 'function deposit() view returns (uint256)',
52+ 'function contractState() view returns (uint8)',
53+ 'function homeAddress() view returns (string)',
54+ 'function city() view returns (string)',
55+ 'function zip() view returns (string)',
56+ 'function finalizeDeadline() view returns (uint256)',
57+ 'function getContractDetails() view returns (string, string, string, uint256, uint256, uint8)',
58+ 'function getRemainingBalance() view returns (uint256)',
59+ 'function sellerSignContract()',
60+ 'function buyerSignContractAndPayDeposit() payable',
61+ 'function realtorReviewClosingConditions(bool accepted)',
62+ 'function buyerFinalizeTransaction() payable',
63+ 'function withdrawFromTransaction()',
64+];
65+
66+// Get contract instances
67+export function getFactoryContract(signerOrProvider) {
68+ return new ethers.Contract(CONTRACTS.FACTORY, FACTORY_ABI, signerOrProvider);
69+}
70+
71+export function getHomeTransactionContract(address, signerOrProvider) {
72+ return new ethers.Contract(address, HOME_TRANSACTION_ABI, signerOrProvider);
73+}
74+
75+// Factory functions
76+export async function createPropertyTransaction(signer, {
77+ address,
78+ zip,
79+ city,
80+ realtorFee,
81+ price,
82+ sellerAddress,
83+ buyerAddress,
84+}) {
85+ const factory = getFactoryContract(signer);
86+ const tx = await factory.create(
87+ address,
88+ zip,
89+ city,
90+ ethers.utils.parseEther(realtorFee.toString()),
91+ ethers.utils.parseEther(price.toString()),
92+ sellerAddress,
93+ buyerAddress
94+ );
95+ const receipt = await tx.wait();
96+
97+ // Get the contract address from the event
98+ const event = receipt.events?.find(e => e.event === 'ContractCreated');
99+ return event?.args?.contractAddress;
100+}
101+
102+export async function getAllTransactions(provider) {
103+ const factory = getFactoryContract(provider);
104+ return await factory.getInstances();
105+}
106+
107+export async function getTransactionsByRole(provider, role, address) {
108+ const factory = getFactoryContract(provider);
109+
110+ switch (role) {
111+ case 'realtor':
112+ return await factory.getContractsByRealtor(address);
113+ case 'seller':
114+ return await factory.getContractsBySeller(address);
115+ case 'buyer':
116+ return await factory.getContractsByBuyer(address);
117+ default:
118+ return [];
119+ }
120+}
121+
122+// HomeTransaction functions
123+export async function getTransactionDetails(provider, contractAddress) {
124+ const contract = getHomeTransactionContract(contractAddress, provider);
125+
126+ const [details, buyer, seller, realtor, deposit, deadline] = await Promise.all([
127+ contract.getContractDetails(),
128+ contract.buyer(),
129+ contract.seller(),
130+ contract.realtor(),
131+ contract.deposit(),
132+ contract.finalizeDeadline(),
133+ ]);
134+
135+ return {
136+ homeAddress: details[0],
137+ zip: details[1],
138+ city: details[2],
139+ price: ethers.utils.formatEther(details[3]),
140+ realtorFee: ethers.utils.formatEther(details[4]),
141+ state: details[5],
142+ stateLabel: ContractStateLabels[details[5]],
143+ buyer,
144+ seller,
145+ realtor,
146+ deposit: ethers.utils.formatEther(deposit),
147+ finalizeDeadline: deadline.toNumber(),
148+ };
149+}
150+
151+export async function sellerSign(signer, contractAddress) {
152+ const contract = getHomeTransactionContract(contractAddress, signer);
153+ const tx = await contract.sellerSignContract();
154+ return await tx.wait();
155+}
156+
157+export async function buyerSignAndDeposit(signer, contractAddress, depositAmount) {
158+ const contract = getHomeTransactionContract(contractAddress, signer);
159+ const tx = await contract.buyerSignContractAndPayDeposit({
160+ value: ethers.utils.parseEther(depositAmount.toString()),
161+ });
162+ return await tx.wait();
163+}
164+
165+export async function realtorReview(signer, contractAddress, accepted) {
166+ const contract = getHomeTransactionContract(contractAddress, signer);
167+ const tx = await contract.realtorReviewClosingConditions(accepted);
168+ return await tx.wait();
169+}
170+
171+export async function buyerFinalize(signer, contractAddress, remainingAmount) {
172+ const contract = getHomeTransactionContract(contractAddress, signer);
173+ const tx = await contract.buyerFinalizeTransaction({
174+ value: ethers.utils.parseEther(remainingAmount.toString()),
175+ });
176+ return await tx.wait();
177+}
178+
179+export async function withdrawTransaction(signer, contractAddress) {
180+ const contract = getHomeTransactionContract(contractAddress, signer);
181+ const tx = await contract.withdrawFromTransaction();
182+ return await tx.wait();
183+}