diff --git a/CairoContracts/Scarb.lock b/CairoContracts/Scarb.lock index 202b491..ff2bb16 100644 --- a/CairoContracts/Scarb.lock +++ b/CairoContracts/Scarb.lock @@ -4,3 +4,21 @@ version = 1 [[package]] name = "devotecontract" version = "0.1.0" +dependencies = [ + "snforge_std", +] + +[[package]] +name = "snforge_scarb_plugin" +version = "0.44.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:ec8c7637b33392a53153c1e5b87a4617ddcb1981951b233ea043cad5136697e2" + +[[package]] +name = "snforge_std" +version = "0.44.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:d4affedfb90715b1ac417b915c0a63377ae6dd69432040e5d933130d65114702" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/CairoContracts/Scarb.toml b/CairoContracts/Scarb.toml index 49c3691..4c12222 100644 --- a/CairoContracts/Scarb.toml +++ b/CairoContracts/Scarb.toml @@ -6,3 +6,17 @@ edition = "2024_07" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] +starknet = "2.11.4" + +[dev-dependencies] +snforge_std = "0.44.0" +assert_macros = "2.11.4" + +[[target.starknet-contract]] +sierra = true + +[scripts] +test = "snforge test" + +[tool.scarb] +allow-prebuilt-plugins = ["snforge_std"] diff --git a/CairoContracts/src/devote.cairo b/CairoContracts/src/devote.cairo index 420e83a..afed177 100644 --- a/CairoContracts/src/devote.cairo +++ b/CairoContracts/src/devote.cairo @@ -1,5 +1,5 @@ -use core::starknet::{ContractAddress}; -use starknet::storage::{Vec, Map}; +use starknet::ContractAddress; +use starknet::storage::{Map, Vec}; #[derive(Drop, Copy, Serde, starknet::Store)] pub struct PersonProposalStruct { pub proposal_id: felt252, @@ -58,8 +58,11 @@ pub struct ProposalPublic { } #[starknet::interface] trait IDeVote { - fn create_new_person(ref self: ContractState, person_id: felt252); - fn change_person_rol(ref self: ContractState, new_rol: felt252); + fn create_admin(ref self: ContractState, person_id: felt252, voter_wallet: ContractAddress); + fn create_new_person( + ref self: ContractState, person_id: felt252, voter_wallet: ContractAddress, + ); + fn change_person_rol(ref self: ContractState, voter_wallet: ContractAddress, new_rol: felt252); fn get_person(self: @ContractState, connected_walled_id: ContractAddress) -> PersonPublic; fn get_person_rol(self: @ContractState, connected_walled_id: ContractAddress) -> felt252; fn get_person_proposals( @@ -78,7 +81,8 @@ trait IDeVote { self: @ContractState, proposal_id: felt252, connected_walled_id: ContractAddress, ) -> Array; fn start_votation(ref self: ContractState, proposal_id: felt252); - fn vote(ref self: ContractState, proposal_id: felt252, vote_type: felt252); + fn add_white_list(ref self: ContractState, proposal_id: felt252, secret: felt252); + fn vote(ref self: ContractState, proposal_id: felt252, vote_type: felt252, secret: felt252); fn end_votation(ref self: ContractState, proposal_id: felt252); fn view_votation( self: @ContractState, proposal_id: felt252, connected_walled_id: ContractAddress, @@ -92,20 +96,15 @@ trait IDeVote { } #[starknet::contract] mod DeVote { - use super::IDeVote; - use super::PersonProposalStruct; - use super::PersonPublic; - use super::Person; - use super::ProposalVoterStruct; - use super::ProposalVoteTypeStruct; - use super::Proposal; - use super::ProposalPublic; - use super::PersonIdStruct; use starknet::storage::{ - StoragePointerReadAccess, StoragePointerWriteAccess, MutableVecTrait, Map, StoragePathEntry, - VecTrait, Vec, + Map, MutableVecTrait, StoragePathEntry, StoragePointerReadAccess, StoragePointerWriteAccess, + Vec, VecTrait, + }; + use starknet::{ContractAddress, get_caller_address}; + use super::{ + IDeVote, Person, PersonIdStruct, PersonProposalStruct, PersonPublic, Proposal, + ProposalPublic, ProposalVoteTypeStruct, ProposalVoterStruct, }; - use core::starknet::{ContractAddress, get_caller_address}; #[storage] struct Storage { persons_ids: Vec, @@ -162,31 +161,45 @@ mod DeVote { fn constructor(ref self: ContractState) {} #[abi(embed_v0)] impl DeVoteImpl of IDeVote { - fn create_new_person(ref self: ContractState, person_id: felt252) { - let wallet_id = get_caller_address(); - let mut person = self.persons.entry(wallet_id); + fn create_admin( + ref self: ContractState, person_id: felt252, voter_wallet: ContractAddress, + ) { + let mut person = self.persons.entry(voter_wallet); person.id_number.write(person_id); - person.wallet_id.write(wallet_id); - person.role.write(0); - self - .emit( - PersonAdded { - wallet_id: person.wallet_id.read(), - id_number: person.id_number.read(), - role: person.role.read(), - }, - ); + person.wallet_id.write(voter_wallet); + person.role.write('admin'); + + let person_id_struct = PersonIdStruct { wallet_id: voter_wallet, id_number: person_id }; + self.persons_ids.append().write(person_id_struct); + + self.emit(PersonAdded { wallet_id: voter_wallet, id_number: person_id, role: 'admin' }); } - fn change_person_rol(ref self: ContractState, new_rol: felt252) { - let person = self.persons.entry(get_caller_address()); + fn create_new_person( + ref self: ContractState, person_id: felt252, voter_wallet: ContractAddress, + ) { + let caller = get_caller_address(); + let mut person = self.persons.entry(voter_wallet); + person.id_number.write(person_id); + person.wallet_id.write(voter_wallet); + person.role.write('user'); + + let person_id_struct = PersonIdStruct { wallet_id: voter_wallet, id_number: person_id }; + self.persons_ids.append().write(person_id_struct); + + self.emit(PersonAdded { wallet_id: voter_wallet, id_number: person_id, role: 'user' }); + } + + fn change_person_rol( + ref self: ContractState, voter_wallet: ContractAddress, new_rol: felt252, + ) { + let caller = get_caller_address(); + let person = self.persons.entry(voter_wallet); person.role.write(new_rol); self .emit( PersonUpdated { - wallet_id_signer: get_caller_address(), - wallet_id: person.wallet_id.read(), - role: person.role.read(), + wallet_id_signer: caller, wallet_id: voter_wallet, role: new_rol, }, ); } @@ -199,7 +212,7 @@ mod DeVote { while idx < person.proposals.len() { proposals.append(person.proposals.at(idx).read()); idx += 1; - }; + } return PersonPublic { wallet_id: person.wallet_id.read(), id_number: person.id_number.read(), @@ -221,7 +234,7 @@ mod DeVote { while idx < person.proposals.len() { proposals.append(person.proposals.at(idx).read()); idx += 1; - }; + } return proposals; } fn create_proposal(ref self: ContractState, proposal_id: felt252, name: felt252) { @@ -273,7 +286,7 @@ mod DeVote { votation.append(vote); } idx += 1; - }; + } let person = self.persons.entry(connected_walled_id); return ProposalPublic { @@ -417,7 +430,7 @@ mod DeVote { while idx < proposal.type_votes.len() { proposals.append(proposal.type_votes.at(idx).read()); idx += 1; - }; + } return proposals; } fn start_votation(ref self: ContractState, proposal_id: felt252) { @@ -435,7 +448,32 @@ mod DeVote { ); } } - fn vote(ref self: ContractState, proposal_id: felt252, vote_type: felt252) { + fn add_white_list(ref self: ContractState, proposal_id: felt252, secret: felt252) { + if can_modify_proposal(@self, proposal_id, 0) { + self + .emit( + GeneralEvent { + function_name: 'add_white_list', + type_message: 'whitelist added', + wallet_id: get_caller_address(), + data: secret, + }, + ); + } else { + self + .emit( + UnauthorizeEvent { + function_name: 'add_white_list', + type_error: 'unauthorize', + wallet_id: get_caller_address(), + }, + ); + } + } + + fn vote( + ref self: ContractState, proposal_id: felt252, vote_type: felt252, secret: felt252, + ) { let person = self.persons.entry(get_caller_address()); let mut proposal = self.proposals.entry(proposal_id); if proposal.state.read() != 1 { @@ -492,6 +530,15 @@ mod DeVote { } idx += 1; } + self + .emit( + GeneralEvent { + function_name: 'vote', + type_message: 'vote cast', + wallet_id: get_caller_address(), + data: secret, + }, + ); } } fn end_votation(ref self: ContractState, proposal_id: felt252) { @@ -521,7 +568,7 @@ mod DeVote { votation.append(vote); } idx += 1; - }; + } return votation; } @@ -531,7 +578,7 @@ mod DeVote { let mut person_ids = ArrayTrait::::new(); for i in 0..self.persons_ids.len() { person_ids.append(self.persons_ids.at(i).read()); - }; + } person_ids } @@ -549,9 +596,9 @@ mod DeVote { id_number: person.id_number.read(), }, ); - }; + } idx += 1; - }; + } return person_list; } } diff --git a/DevoteApp/components/CreateUserModal.tsx b/DevoteApp/components/CreateUserModal.tsx index 19bda42..d032549 100644 --- a/DevoteApp/components/CreateUserModal.tsx +++ b/DevoteApp/components/CreateUserModal.tsx @@ -6,7 +6,10 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { useToast } from "@/hooks/use-toast" - +import { useContractCustom } from "@/hooks/use-contract" +import { useWallet } from "@/hooks/use-wallet" +import { loginStatus } from "@/interfaces/Login" +import { shortString } from "starknet" interface CreateUserModalProps { isOpen: boolean @@ -17,12 +20,18 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp const [email, setEmail] = useState("") const [userId, setUserId] = useState("") const [userName, setUserName] = useState("") + const [walletAddress, setWalletAddress] = useState("") const [isCreating, setIsCreating] = useState(false) const [searchLoading, setSearchLoading] = useState(false) const [searchError, setSearchError] = useState("") + const [isAdmin, setIsAdmin] = useState(false) const { toast } = useToast() + const { createPersonOnChain, createAdminOnChain } = useContractCustom() + const { connectionStatus, address } = useWallet() + + const isConnected = connectionStatus === loginStatus.CONNECTED - // Función que se ejecuta al presionar el botón "Search" para buscar el citizen + // Function to search for citizen information const handleSearchCitizen = async () => { if (!userId) { setSearchError("Please enter a User ID.") @@ -38,7 +47,6 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp return } const data = await res.json() - // Se asume que la respuesta tiene los campos firstName y lastName if (data && data.firstName && data.lastName) { const fullName = `${data.firstName} ${data.lastName}` setUserName(fullName) @@ -55,42 +63,80 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp } } - // Función que se ejecuta al presionar "Create User" + // Function to create user on blockchain const handleCreateUser = async () => { + if (!isConnected) { + toast({ + title: "Wallet Not Connected", + description: "Please connect your wallet to create a user on the blockchain", + variant: "destructive", + }) + return + } + + if (!userId || !walletAddress) { + toast({ + title: "Missing Information", + description: "Please fill in all required fields", + variant: "destructive", + }) + return + } + setIsCreating(true) try { - const res = await fetch('/api/users', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - // Enviamos "ine" usando el valor de userId (que es el INE del citizen) - body: JSON.stringify({ email, ine: userId }) - }) - if (!res.ok) { - const errorData = await res.json() - toast({ - title: "Error", - description: errorData.message || "Failed to create user", - variant: "destructive", + // Convert userId to felt252 format + const userIdFelt = shortString.encodeShortString(userId) + + // Create user on blockchain + let result + if (isAdmin) { + result = await createAdminOnChain(userIdFelt) + } else { + result = await createPersonOnChain(userIdFelt, walletAddress) + } + + if (result) { + // Also create user in the database + const res = await fetch('/api/users', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email, + ine: userId, + walletAddress, + isAdmin + }) }) - setIsCreating(false) - return + + if (res.ok) { + const data = await res.json() + toast({ + title: "Success!", + description: `User created successfully on blockchain and database. Transaction hash: ${result.transaction_hash}`, + variant: "default", + }) + } else { + toast({ + title: "Partial Success", + description: `User created on blockchain but database creation failed. Transaction hash: ${result.transaction_hash}`, + variant: "default", + }) + } + + // Reset form + setEmail("") + setUserId("") + setUserName("") + setWalletAddress("") + setIsAdmin(false) + onClose() } - const data = await res.json() - toast({ - title: "User Created", - description: `New user account created for ${data.user.name}`, - duration: 3000, - variant: "success", - }) - setEmail("") - setUserId("") - setUserName("") - onClose() } catch (error) { - console.error("Error creating user:", error) + console.error("Error creating user on blockchain:", error) toast({ title: "Error", - description: "Failed to create user", + description: `Failed to create user on blockchain: ${error instanceof Error ? error.message : 'Unknown error'}`, variant: "destructive", }) } finally { @@ -100,11 +146,22 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp return ( - + Create New User
+ {/* Wallet Connection Status */} +
+
+
+ + {isConnected ? `Wallet Connected: ${address?.slice(0, 6)}...${address?.slice(-4)}` : 'Wallet Not Connected'} + +
+
+ + {/* Email Field */}
setEmail(e.target.value)} className="bg-gray-800 border-gray-700 text-white" + placeholder="user@example.com" />
+ + {/* User ID Field with Search */}
@@ -123,6 +183,7 @@ export default function CreateUserModal({ isOpen, onClose }: CreateUserModalProp value={userId} onChange={(e) => setUserId(e.target.value)} className="bg-gray-800 border-gray-700 text-white" + placeholder="Enter INE number" />
+ + {/* Search Error */} {searchError &&

{searchError}

} + + {/* User Name Field */}
+ + {/* Wallet Address Field */} +
+ + setWalletAddress(e.target.value)} + className="bg-gray-800 border-gray-700 text-white" + placeholder="0x..." + /> +
+ + {/* Admin Checkbox */} +
+ setIsAdmin(e.target.checked)} + className="w-4 h-4 rounded border-gray-700 bg-gray-800 text-[#f7cf1d]" + /> + +
+ + {/* Create User Button */} + + {/* Help Text */} +
+

• This will create a user directly on the Starknet blockchain

+

• Make sure your wallet is connected and has sufficient funds

+

• The transaction will be recorded permanently on the blockchain

+
diff --git a/DevoteApp/hooks/use-contract.tsx b/DevoteApp/hooks/use-contract.tsx index 2a6c2bf..d920f35 100644 --- a/DevoteApp/hooks/use-contract.tsx +++ b/DevoteApp/hooks/use-contract.tsx @@ -831,7 +831,7 @@ export function useContractCustom() { ]); const res = await newContract.create_new_person(createUserCall.calldata); const result = await provider.waitForTransaction(res.transaction_hash); - return result; + return { ...result, transaction_hash: res.transaction_hash }; }; const createAdminOnChain = async (person_id: string) => { @@ -847,7 +847,7 @@ export function useContractCustom() { ]); const res = await newContract.create_admin(createUserCall.calldata); const result = await provider.waitForTransaction(res.transaction_hash); - return result; + return { ...result, transaction_hash: res.transaction_hash }; } catch (error) { console.error("Error creating admin on chain:", error); throw new Error("Failed to create admin on chain: " + (error instanceof Error ? error.message : String(error)));