feat: add database and deploy local script to fill database with information as it creates contracts
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 @@ | ||
| 39 | 39 | "morgan": "^1.10.1", |
| 40 | 40 | "multer": "^2.0.2", |
| 41 | 41 | "paytmchecksum": "^1.5.1", |
| 42 | + "pg": "^8.17.2", | |
| 42 | 43 | "react": "^18.3.1", |
| 43 | 44 | "react-app-rewired": "^2.2.1", |
| 44 | 45 | "react-chartjs-2": "^5.2.0", |
| @@ -24864,6 +24865,95 @@ | ||
| 24864 | 24865 | "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", |
| 24865 | 24866 | "license": "MIT" |
| 24866 | 24867 | }, |
| 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 | + }, | |
| 24867 | 24957 | "node_modules/picocolors": { |
| 24868 | 24958 | "version": "1.1.1", |
| 24869 | 24959 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", |
| @@ -26269,6 +26359,45 @@ | ||
| 26269 | 26359 | "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", |
| 26270 | 26360 | "license": "MIT" |
| 26271 | 26361 | }, |
| 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 | + }, | |
| 26272 | 26401 | "node_modules/potpack": { |
| 26273 | 26402 | "version": "1.0.2", |
| 26274 | 26403 | "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", |
| @@ -29267,6 +29396,15 @@ | ||
| 29267 | 29396 | "node": ">=0.10.0" |
| 29268 | 29397 | } |
| 29269 | 29398 | }, |
| 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 | + }, | |
| 29270 | 29408 | "node_modules/sprintf-js": { |
| 29271 | 29409 | "version": "1.0.3", |
| 29272 | 29410 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", |
+1-0package.json
| @@ -34,6 +34,7 @@ | ||
| 34 | 34 | "morgan": "^1.10.1", |
| 35 | 35 | "multer": "^2.0.2", |
| 36 | 36 | "paytmchecksum": "^1.5.1", |
| 37 | + "pg": "^8.17.2", | |
| 37 | 38 | "react": "^18.3.1", |
| 38 | 39 | "react-app-rewired": "^2.2.1", |
| 39 | 40 | "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 | + | |
| 1 | 3 | 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 | +} | |
| 2 | 253 | |
| 3 | 254 | async function main() { |
| 4 | 255 | const mnemonic = "test test test test test test test test test test test junk"; |
| 5 | 256 | |
| 6 | - // Derive wallets from mnemonic (so we have access to private keys) | |
| 257 | + // Derive wallets | |
| 7 | 258 | const roles = ["Deployer", "Realtor", "Seller", "Buyer"]; |
| 8 | 259 | const wallets = roles.map((_, i) => { |
| 9 | 260 | const path = `m/44'/60'/0'/0/${i}`; |
| 10 | 261 | return hre.ethers.Wallet.fromMnemonic(mnemonic, path).connect(hre.ethers.provider); |
| 11 | 262 | }); |
| 12 | 263 | |
| 13 | - const [deployer, realtor, seller, buyer] = wallets; | |
| 14 | - | |
| 15 | - // Show how keys are derived | |
| 16 | 264 | 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`); | |
| 20 | 266 | |
| 21 | 267 | console.table(wallets.map((w, i) => ({ |
| 22 | 268 | role: roles[i], |
| @@ -25,48 +271,26 @@ async function main() { | ||
| 25 | 271 | privateKey: w.privateKey |
| 26 | 272 | }))); |
| 27 | 273 | |
| 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); | |
| 31 | 277 | |
| 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); | |
| 36 | 280 | |
| 37 | 281 | 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"); | |
| 59 | 286 | console.log(`REACT_APP_FACTORY_ADDRESS=${factory.address}`); |
| 60 | - console.log(`REACT_APP_SAMPLE_PROPERTY=${propertyContract}`); | |
| 61 | 287 | |
| 62 | 288 | 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"); | |
| 70 | 294 | } |
| 71 | 295 | |
| 72 | 296 | main() |
+5-5server/app.js
| @@ -1,12 +1,11 @@ | ||
| 1 | +require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') }); | |
| 2 | + | |
| 1 | 3 | const express = require('express'); |
| 2 | 4 | const path = require('path'); |
| 3 | -const mongoose = require('mongoose'); | |
| 4 | 5 | const morgan = require('morgan'); |
| 5 | 6 | const cors = require('cors'); |
| 6 | 7 | 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'); | |
| 10 | 9 | |
| 11 | 10 | const app = express(); |
| 12 | 11 | const server = http.createServer(app); |
| @@ -18,7 +17,8 @@ const corsOptions = { | ||
| 18 | 17 | }; |
| 19 | 18 | |
| 20 | 19 | app.use(express.static(path.join(__dirname, 'uploads'))); |
| 21 | - | |
| 20 | +app.use(express.json()); | |
| 21 | +app.use(express.urlencoded({ extended: true })); | |
| 22 | 22 | app.use(cors(corsOptions)); |
| 23 | 23 | |
| 24 | 24 | // 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 | +} | |
| 16 | 62 | |
| 17 | 63 | 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' }); | |
| 180 | 246 | } |
| 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 @@ | ||
| 1 | 1 | 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 | - | |
| 8 | 2 | const router = express.Router(); |
| 9 | 3 | const propertyController = require('../controllers/property.controller'); |
| 10 | 4 | |
| 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 | |
| 29 | 6 | router.get('/type', propertyController.propertyTypeList); |
| 30 | 7 | router.post('/type', propertyController.addPropertyType); |
| 31 | 8 | |
| 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); | |
| 34 | 15 | router.get('/list/:userId', propertyController.getUserList); |
| 35 | -router.get('/list/', propertyController.getFullList); | |
| 36 | 16 | 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); | |
| 38 | 18 | router.post('/markAsSold/:propertySlug', propertyController.markAsSold); |
| 39 | - | |
| 40 | -//Properties filter | |
| 41 | 19 | router.get('/filter', propertyController.filterProperties); |
| 42 | 20 | |
| 43 | -module.exports = router; | |
| 21 | +module.exports = router; |
+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'; | ||
| 5 | 5 | import { FaWallet, FaStore, FaMoneyBillWave, FaExchangeAlt, FaChartLine, FaLock, FaUserCog, FaCoins } from 'react-icons/fa'; |
| 6 | 6 | import { SiEthereum } from 'react-icons/si'; |
| 7 | 7 | import { FiChevronDown, FiChevronUp } from 'react-icons/fi'; |
| 8 | +import { useWallet } from '../hooks/useWallet'; | |
| 8 | 9 | |
| 9 | 10 | function Home() { |
| 10 | 11 | const [openSections, setOpenSections] = useState({}); |
| 12 | + const { connect, isConnected, isConnecting, shortAddress } = useWallet(); | |
| 11 | 13 | |
| 12 | 14 | const featuredProperties = [ |
| 13 | 15 | { |
| @@ -426,10 +428,12 @@ function Home() { | ||
| 426 | 428 | Browse Properties |
| 427 | 429 | </Link> |
| 428 | 430 | <button |
| 431 | + onClick={connect} | |
| 432 | + disabled={isConnecting} | |
| 429 | 433 | className="btn bg-gradient-to-r from-primary-700 to-primary-800 hover:from-primary-800 hover:to-primary-900" |
| 430 | 434 | > |
| 431 | 435 | <FaWallet className="mr-2" /> |
| 432 | - Connect Wallet | |
| 436 | + {isConnecting ? 'Connecting...' : isConnected ? shortAddress : 'Connect Wallet'} | |
| 433 | 437 | </button> |
| 434 | 438 | </div> |
| 435 | 439 | </div> |
+188-262src/pages/Properties.jsx
| @@ -1,180 +1,117 @@ | ||
| 1 | -import { useState } from 'react'; | |
| 1 | +import { useState, useEffect } from 'react'; | |
| 2 | 2 | import { Link } from 'react-router-dom'; |
| 3 | 3 | 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'; | |
| 5 | 5 | import { FaEthereum } from 'react-icons/fa'; |
| 6 | 6 | |
| 7 | +const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001'; | |
| 8 | + | |
| 7 | 9 | function Properties() { |
| 10 | + const [properties, setProperties] = useState([]); | |
| 11 | + const [loading, setLoading] = useState(true); | |
| 12 | + const [error, setError] = useState(null); | |
| 8 | 13 | const [showFilters, setShowFilters] = useState(false); |
| 9 | 14 | const [filters, setFilters] = useState({ |
| 10 | 15 | priceRange: 'all', |
| 11 | 16 | propertyType: 'all', |
| 12 | 17 | location: '', |
| 13 | 18 | minROI: '', |
| 14 | - maxROI: '', | |
| 15 | - fundingStatus: 'all', | |
| 16 | 19 | sortBy: 'newest' |
| 17 | 20 | }); |
| 18 | 21 | |
| 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); | |
| 122 | 36 | } |
| 123 | 37 | } |
| 124 | - ]; | |
| 38 | + | |
| 39 | + fetchProperties(); | |
| 40 | + }, []); | |
| 125 | 41 | |
| 126 | 42 | const handleFilterChange = (key, value) => { |
| 127 | 43 | setFilters(prev => ({ ...prev, [key]: value })); |
| 128 | 44 | }; |
| 129 | 45 | |
| 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 | |
| 130 | 61 | 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 | + | |
| 136 | 66 | if (filters.priceRange !== 'all') { |
| 137 | 67 | 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; | |
| 140 | 70 | } |
| 141 | 71 | |
| 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 | - | |
| 159 | 72 | return true; |
| 160 | 73 | }); |
| 161 | 74 | |
| 162 | - // Sort properties based on selected criteria | |
| 75 | + // Sort properties | |
| 163 | 76 | const sortedProperties = [...filteredProperties].sort((a, b) => { |
| 164 | 77 | switch (filters.sortBy) { |
| 165 | 78 | case 'priceAsc': |
| 166 | - return a.price.usd - b.price.usd; | |
| 79 | + return a.price - b.price; | |
| 167 | 80 | case 'priceDesc': |
| 168 | - return b.price.usd - a.price.usd; | |
| 81 | + return b.price - a.price; | |
| 169 | 82 | case 'roiDesc': |
| 170 | - return parseFloat(b.roi) - parseFloat(a.roi); | |
| 83 | + return (b.roi_percentage || 0) - (a.roi_percentage || 0); | |
| 171 | 84 | case 'fundingDesc': |
| 172 | - return parseInt(b.metrics.funded) - parseInt(a.metrics.funded); | |
| 85 | + return getFundingPercent(b) - getFundingPercent(a); | |
| 173 | 86 | default: |
| 174 | - return 0; | |
| 87 | + return new Date(b.created_at) - new Date(a.created_at); | |
| 175 | 88 | } |
| 176 | 89 | }); |
| 177 | 90 | |
| 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 | + | |
| 178 | 115 | return ( |
| 179 | 116 | <div className="min-h-screen bg-gradient-to-br from-sapphire-50 via-white to-platinum-100"> |
| 180 | 117 | {/* Header */} |
| @@ -183,6 +120,7 @@ function Properties() { | ||
| 183 | 120 | <div className="flex justify-between items-center"> |
| 184 | 121 | <h1 className="text-3xl font-bold text-sapphire-800">Investment Properties</h1> |
| 185 | 122 | <div className="flex items-center space-x-4"> |
| 123 | + <span className="text-platinum-600">{properties.length} properties</span> | |
| 186 | 124 | <button |
| 187 | 125 | className={`p-2 rounded-xl transition-all duration-300 ${showFilters ? 'glass-card text-primary-600' : 'hover:bg-glass'}`} |
| 188 | 126 | onClick={() => setShowFilters(!showFilters)} |
| @@ -198,11 +136,9 @@ function Properties() { | ||
| 198 | 136 | {showFilters && ( |
| 199 | 137 | <div className="glass-card border-t border-glass"> |
| 200 | 138 | <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"> | |
| 202 | 140 | <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> | |
| 206 | 142 | <select |
| 207 | 143 | className="input" |
| 208 | 144 | value={filters.priceRange} |
| @@ -216,67 +152,43 @@ function Properties() { | ||
| 216 | 152 | </div> |
| 217 | 153 | |
| 218 | 154 | <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> | |
| 222 | 156 | <select |
| 223 | 157 | className="input" |
| 224 | 158 | value={filters.propertyType} |
| 225 | 159 | onChange={(e) => handleFilterChange('propertyType', e.target.value)} |
| 226 | 160 | > |
| 227 | 161 | <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> | |
| 231 | 165 | </select> |
| 232 | 166 | </div> |
| 233 | 167 | |
| 234 | 168 | <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> | |
| 238 | 170 | <input |
| 239 | 171 | type="text" |
| 240 | 172 | className="input" |
| 241 | - placeholder="Enter location" | |
| 173 | + placeholder="City or state..." | |
| 242 | 174 | value={filters.location} |
| 243 | 175 | onChange={(e) => handleFilterChange('location', e.target.value)} |
| 244 | 176 | /> |
| 245 | 177 | </div> |
| 246 | 178 | |
| 247 | 179 | <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> | |
| 251 | 181 | <input |
| 252 | 182 | type="number" |
| 253 | 183 | className="input" |
| 254 | - placeholder="Min ROI %" | |
| 184 | + placeholder="e.g. 5" | |
| 255 | 185 | value={filters.minROI} |
| 256 | 186 | onChange={(e) => handleFilterChange('minROI', e.target.value)} |
| 257 | 187 | /> |
| 258 | 188 | </div> |
| 259 | 189 | |
| 260 | 190 | <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> | |
| 280 | 192 | <select |
| 281 | 193 | className="input" |
| 282 | 194 | value={filters.sortBy} |
| @@ -296,108 +208,122 @@ function Properties() { | ||
| 296 | 208 | |
| 297 | 209 | {/* Properties Grid */} |
| 298 | 210 | <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 | + )} | |
| 317 | 240 | </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> | |
| 322 | 241 | |
| 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> | |
| 330 | 260 | </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> | |
| 334 | 267 | </div> |
| 335 | 268 | </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> | |
| 341 | 279 | </div> |
| 342 | 280 | </div> |
| 343 | - </div> | |
| 344 | 281 | |
| 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> | |
| 358 | 294 | </div> |
| 359 | - </div> | |
| 360 | 295 | |
| 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 | + )} | |
| 372 | 312 | </div> |
| 373 | - </div> | |
| 374 | 313 | |
| 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> | |
| 387 | 318 | </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 | + )} | |
| 398 | 324 | </div> |
| 399 | 325 | </div> |
| 400 | 326 | ); |
| 401 | 327 | } |
| 402 | 328 | |
| 403 | -export default Properties; | |
| 329 | +export default Properties; |
+352-198src/pages/PropertyDetail.jsx
| @@ -1,79 +1,156 @@ | ||
| 1 | +import { useState, useEffect } from 'react'; | |
| 1 | 2 | import { useParams, Link } from 'react-router-dom'; |
| 2 | 3 | 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'; | |
| 4 | 5 | import { FacebookShareButton, TwitterShareButton, LinkedinShareButton } from 'react-share'; |
| 5 | 6 | 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 | +}; | |
| 6 | 21 | |
| 7 | 22 | 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'); | |
| 73 | 113 | } |
| 74 | 114 | }; |
| 75 | 115 | |
| 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(); | |
| 76 | 151 | 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; | |
| 77 | 154 | |
| 78 | 155 | return ( |
| 79 | 156 | <div className="min-h-screen bg-secondary-50"> |
| @@ -96,31 +173,42 @@ function PropertyDetail() { | ||
| 96 | 173 | {/* Left Column */} |
| 97 | 174 | <div className="lg:col-span-2 space-y-8"> |
| 98 | 175 | {/* 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"> | |
| 104 | 177 | <div className="h-96 rounded-lg overflow-hidden"> |
| 105 | 178 | <img |
| 106 | - src={property.images[0]} | |
| 179 | + src={property.images?.[0] || 'https://images.unsplash.com/photo-1613490493576-7fde63acd811?w=800&q=80'} | |
| 107 | 180 | alt={property.title} |
| 108 | 181 | className="w-full h-full object-cover" |
| 109 | 182 | /> |
| 110 | 183 | </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> | |
| 122 | 184 | </motion.div> |
| 123 | 185 | |
| 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 | + | |
| 124 | 212 | {/* Property Details */} |
| 125 | 213 | <motion.div |
| 126 | 214 | initial={{ opacity: 0, y: 20 }} |
| @@ -130,169 +218,183 @@ function PropertyDetail() { | ||
| 130 | 218 | > |
| 131 | 219 | <h2 className="text-2xl font-bold mb-4">Property Details</h2> |
| 132 | 220 | <p className="text-secondary-600 mb-6">{property.description}</p> |
| 133 | - | |
| 221 | + | |
| 134 | 222 | <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> |
| 135 | 223 | <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> | |
| 138 | 226 | </div> |
| 139 | 227 | <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> | |
| 142 | 230 | </div> |
| 143 | 231 | <div className="flex items-center space-x-2"> |
| 144 | 232 | <FiCalendar className="text-primary-600" /> |
| 145 | - <span>Built {property.yearBuilt}</span> | |
| 233 | + <span>{property.locality}</span> | |
| 146 | 234 | </div> |
| 147 | 235 | <div className="flex items-center space-x-2"> |
| 148 | 236 | <FiUsers className="text-primary-600" /> |
| 149 | - <span>{property.metrics.totalInvestors} Investors</span> | |
| 237 | + <span>{property.investorStats?.total_investors || 0} Investors</span> | |
| 150 | 238 | </div> |
| 151 | 239 | </div> |
| 152 | 240 | |
| 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 | - | |
| 163 | 241 | {/* Token Details */} |
| 164 | 242 | <h3 className="text-xl font-semibold mb-4">Token Information</h3> |
| 165 | 243 | <div className="bg-secondary-50 rounded-lg p-6 mb-6"> |
| 166 | 244 | <div className="grid grid-cols-2 gap-4"> |
| 167 | 245 | <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> | |
| 170 | 248 | </div> |
| 171 | 249 | <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> | |
| 174 | 252 | </div> |
| 175 | 253 | <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> | |
| 178 | 256 | </div> |
| 179 | 257 | <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> | |
| 186 | 260 | </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> | |
| 199 | 265 | </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 | + )} | |
| 216 | 267 | </div> |
| 217 | 268 | </div> |
| 218 | 269 | </motion.div> |
| 219 | 270 | </div> |
| 220 | 271 | |
| 221 | - {/* Right Column */} | |
| 272 | + {/* Right Column - Investment Card */} | |
| 222 | 273 | <motion.div |
| 223 | 274 | initial={{ opacity: 0, x: 20 }} |
| 224 | 275 | animate={{ opacity: 1, x: 0 }} |
| 225 | 276 | transition={{ delay: 0.2 }} |
| 226 | 277 | className="space-y-6" |
| 227 | 278 | > |
| 228 | - {/* Investment Card */} | |
| 229 | 279 | <div className="bg-white rounded-lg shadow-md p-6"> |
| 230 | 280 | <div className="flex justify-between items-center mb-4"> |
| 231 | 281 | <div> |
| 232 | 282 | <p className="text-sm text-secondary-500">Investment Price</p> |
| 233 | 283 | <div className="flex items-center"> |
| 234 | 284 | <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> | |
| 236 | 286 | </div> |
| 237 | 287 | <div className="flex items-center text-primary-600"> |
| 238 | 288 | <FaEthereum className="mr-1" /> |
| 239 | - <span>{property.price.eth} ETH</span> | |
| 289 | + <span>{property.price_eth} ETH</span> | |
| 240 | 290 | </div> |
| 241 | 291 | </div> |
| 242 | 292 | <div className="text-right"> |
| 243 | 293 | <p className="text-sm text-secondary-500">Annual ROI</p> |
| 244 | 294 | <div className="flex items-center justify-end text-green-600"> |
| 245 | 295 | <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> | |
| 247 | 297 | </div> |
| 248 | 298 | </div> |
| 249 | 299 | </div> |
| 250 | 300 | |
| 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 | - | |
| 267 | 301 | {/* Funding Progress */} |
| 268 | 302 | <div className="mb-6"> |
| 269 | 303 | <div className="flex justify-between text-sm mb-1"> |
| 270 | 304 | <span className="text-secondary-600">Funding Progress</span> |
| 271 | - <span className="font-medium">{property.metrics.funded}</span> | |
| 305 | + <span className="font-medium">{getFundingPercent()}%</span> | |
| 272 | 306 | </div> |
| 273 | 307 | <div className="w-full bg-secondary-100 rounded-full h-2"> |
| 274 | 308 | <div |
| 275 | 309 | className="bg-primary-600 h-2 rounded-full" |
| 276 | - style={{ width: property.metrics.funded }} | |
| 310 | + style={{ width: `${getFundingPercent()}%` }} | |
| 277 | 311 | /> |
| 278 | 312 | </div> |
| 279 | - <p className="text-sm text-secondary-500 mt-1"> | |
| 280 | - Min Investment: {property.metrics.minInvestment} | |
| 281 | - </p> | |
| 282 | 313 | </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 | + | |
| 284 | 389 | <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 | + > | |
| 287 | 393 | <FiGrid className="mr-2" /> |
| 288 | 394 | View 3D Model |
| 289 | 395 | </Link> |
| 290 | 396 | |
| 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 */} | |
| 296 | 398 | <div className="flex items-center justify-center space-x-4 pt-4 border-t"> |
| 297 | 399 | <FacebookShareButton url={shareUrl}> |
| 298 | 400 | <FaFacebook className="text-2xl text-blue-600 hover:opacity-80" /> |
| @@ -305,35 +407,87 @@ function PropertyDetail() { | ||
| 305 | 407 | </LinkedinShareButton> |
| 306 | 408 | </div> |
| 307 | 409 | </div> |
| 410 | + </motion.div> | |
| 411 | + </div> | |
| 412 | + </div> | |
| 308 | 413 | |
| 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> | |
| 321 | 427 | </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 | + )} | |
| 329 | 438 | </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 | + )} | |
| 334 | 488 | </motion.div> |
| 335 | 489 | </div> |
| 336 | - </div> | |
| 490 | + )} | |
| 337 | 491 | </div> |
| 338 | 492 | ); |
| 339 | 493 | } |
+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 | +} |