To run:
make sure you have npm: npm -v
npm install
npx hardhat node
npx hardhat run scripts/deploy.js --network localhost (in another terminal, so that it connects to the local blockchain network)
open another terminal and then:
cd frontend
npm run dev
- utilizarea tipurilor de date specifice Solidity (mappings, address).
RentChain/contracts/Property.sol
Lines 10 to 22 in 9c32606
| contract PropertyRental { | |
| struct Property { | |
| uint256 id; | |
| string name; | |
| string location; | |
| uint256 pricePerDay; | |
| address payable owner; | |
| } | |
| uint256 public propertyCount; | |
| mapping(uint256 => Property) public properties; | |
| mapping(uint256 => mapping(uint256 => bool)) public isBooked; // propertyId -> day -> isBooked | |
| mapping(address => uint256) public ownerRevenue; |
- înregistrarea de events.
RentChain/contracts/Property.sol
Lines 28 to 30 in 9c32606
| event PropertyAdded(uint256 id, string name, string location, address owner); | |
| event PropertyBooked(uint256 id, address client, uint256 startDate, uint256 endDate, uint256 totalCost); | |
| event Withdraw(address owner, uint256 amount); |
- utilizarea de modifiers.
Lines 10 to 13 in 9c32606
| modifier onlyPropertyRental() { | |
| require(msg.sender == propertyRentalContract, "Only PropertyRental can call this function"); | |
| _; | |
| } |
- exemple pentru toate tipurile de funcții (external, pure, view etc.)
RentChain/contracts/Property.sol
Lines 39 to 121 in adde26e
| function addProperty( | |
| string memory _name, | |
| string memory _location, | |
| uint256 _pricePerDay | |
| ) external { | |
| require(ownerContract.isOwner(msg.sender), "Only owners can add properties"); | |
| require(_pricePerDay > 0, "Price per day must be greater than zero"); | |
| propertyCount++; | |
| properties[propertyCount] = Property({ | |
| id: propertyCount, | |
| name: _name, | |
| location: _location, | |
| pricePerDay: _pricePerDay, | |
| owner: payable(msg.sender) | |
| }); | |
| nftContract.createToken(msg.sender, _name, _location, _location); | |
| ownerContract.addProperty(msg.sender, propertyCount); | |
| emit PropertyAdded(propertyCount, _name, _location, msg.sender); | |
| } | |
| function getPropertyCount() public view returns (uint256) { | |
| return propertyCount; | |
| } | |
| // Book property by a client | |
| function bookProperty( | |
| uint256 _propertyId, | |
| uint256 _startDate, | |
| uint256 _endDate | |
| ) external payable { | |
| require(clientContract.isClient(msg.sender), "Only clients can book properties"); | |
| require(_startDate < _endDate, "Invalid booking dates"); | |
| require(properties[_propertyId].id != 0, "Property does not exist"); | |
| require(_endDate - _startDate >= 1 days); | |
| uint256 daysToBook = (_endDate - _startDate) / 1 days; | |
| uint256 totalCost = properties[_propertyId].pricePerDay * daysToBook; | |
| require(msg.value == totalCost, "Incorrect Ether value sent"); | |
| ownerRevenue[properties[_propertyId].owner] += msg.value; | |
| for (uint256 day = _startDate / 1 days; day < _endDate / 1 days; day++) { | |
| require(!isBooked[_propertyId][day], "Property already booked for one or more days"); | |
| isBooked[_propertyId][day] = true; | |
| } | |
| clientContract.addBooking(msg.sender, _propertyId); | |
| emit PropertyBooked(_propertyId, msg.sender, _startDate, _endDate, totalCost); | |
| } | |
| function calculateBookingCost(uint256 pricePerDay, uint256 numberOfDays) public pure returns (uint256) { | |
| return pricePerDay * numberOfDays; | |
| } | |
| function withdraw(uint256 amount) external { | |
| require(ownerRevenue[msg.sender] >= amount, "Insufficient revenue to withdraw"); | |
| ownerRevenue[msg.sender] -= amount; | |
| (bool success, ) = payable(msg.sender).call{value: amount}(""); | |
| require(success, "Transfer failed"); | |
| emit Withdraw(msg.sender, amount); | |
| } | |
| function getPropertiesByOwner(address _owner) external view returns (uint256[] memory) { | |
| return ownerContract.getOwnerProperties(_owner); | |
| } | |
| function getUserBookings(address _client) external view returns (uint256[] memory) { | |
| return clientContract.getClientBookings(_client); | |
| } |
- exemple de transfer de eth.
RentChain/contracts/Property.sol
Lines 97 to 107 in 9c32606
| function withdraw(uint256 amount) external { | |
| require(ownerRevenue[msg.sender] >= amount, "Insufficient revenue to withdraw"); | |
| ownerRevenue[msg.sender] -= amount; | |
| (bool success, ) = payable(msg.sender).call{value: amount}(""); | |
| require(success, "Transfer failed"); | |
| emit Withdraw(msg.sender, amount); | |
| } |
- ilustrarea interacțiunii dintre smart contracte.
RentChain/contracts/Property.sol
Lines 24 to 26 in 9c32606
| Owner public ownerContract; | |
| Client public clientContract; | |
| NFT public nftContract; |
RentChain/contracts/Property.sol
Lines 56 to 58 in 9c32606
| nftContract.createToken(msg.sender, _name, _location, _location); | |
| ownerContract.addProperty(msg.sender, propertyCount); |
- deploy pe o rețea locală sau pe o rețea de test Ethereum.
npx hardhat node
npx hardhat run ./scripts/deploy.js --localhost
or
npx hardhat run ./scripts/deploy3.js --network sepolia
- utilizare librării
Line 7 in 9c32606
| contract NFT is ERC721 { |
Line 32 in 9c32606
| _safeMint(owner, newTokenId); |
- implementarea de teste (cu tool-uri la alegerea echipelor).
npx hardhat test
Lines 1 to 236 in 9c32606
| const { ethers } = require("hardhat"); | |
| const assert = require("assert"); | |
| describe("PropertyRental Tests", function () { | |
| let deployer, owner1, owner2, owner3 ,client1, client2, nonOwner, nonClient; | |
| let ownerContract, clientContract, nftContract, propertyRental; | |
| before(async () => { | |
| [deployer, owner1, owner2, owner3, client1, client2, nonOwner, nonClient] = await ethers.getSigners(); | |
| // Deploy contracts | |
| const OwnerFactory = await ethers.getContractFactory("Owner"); | |
| ownerContract = await OwnerFactory.deploy(deployer.address); | |
| await ownerContract.deployed(); | |
| const NFTFactory = await ethers.getContractFactory("NFT"); | |
| nftContract = await NFTFactory.deploy(); | |
| await nftContract.deployed(); | |
| const ClientFactory = await ethers.getContractFactory("Client"); | |
| clientContract = await ClientFactory.deploy(deployer.address); | |
| await clientContract.deployed(); | |
| const PropertyRentalFactory = await ethers.getContractFactory("PropertyRental"); | |
| propertyRental = await PropertyRentalFactory.deploy( | |
| ownerContract.address, | |
| clientContract.address, | |
| nftContract.address | |
| ); | |
| await propertyRental.deployed(); | |
| // Add owners and clients | |
| await ownerContract.connect(deployer).addOwner(owner1.address); | |
| await ownerContract.connect(deployer).addOwner(owner2.address); | |
| await ownerContract.connect(deployer).addOwner(owner3.address); | |
| await clientContract.connect(deployer).addClient(client1.address); | |
| await clientContract.connect(deployer).addClient(client2.address); | |
| const prop1 = await propertyRental | |
| .connect(owner1) | |
| .addProperty("Cozy Apartment", "Paris", ethers.utils.parseEther("0.2")); | |
| await prop1.wait(); | |
| const prop2 = await propertyRental | |
| .connect(owner3) | |
| .addProperty("Modern Loft", "Berlin", ethers.utils.parseEther("0.3")); | |
| await prop2.wait(); | |
| const prop3 = await propertyRental | |
| .connect(owner2) | |
| .addProperty("Beach House", "Malibu", ethers.utils.parseEther("0.5")); | |
| await prop3.wait(); | |
| console.log("finished before"); | |
| }); | |
| it("Should fail when getting properties of a non-owner", async function () { | |
| try { | |
| await propertyRental.getPropertiesByOwner(nonOwner.address); | |
| assert.fail("Non-owner was able to get properties"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Not a registered owner"), | |
| "Unexpected error message for non-owner access" | |
| ); | |
| } | |
| }); | |
| it("Should fail when getting bookings of a non-client", async function () { | |
| try { | |
| await propertyRental.getUserBookings(nonClient.address); | |
| assert.fail("Non-client was able to get bookings"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Not a registered client"), | |
| "Unexpected error message for non-client access" | |
| ); | |
| } | |
| }); | |
| it("Should fail when a non-owner tries to add a property", async function () { | |
| try { | |
| await propertyRental.connect(nonOwner).addProperty("Test Property", "Nowhere", ethers.utils.parseEther("1")); | |
| assert.fail("Non-owner was able to add a property"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Only owners can add properties"), | |
| "Unexpected error message for non-owner adding property" | |
| ); | |
| } | |
| }); | |
| it("Should fail when a non-client tries to book a property", async function () { | |
| const startDate = Math.floor(Date.now() / 1000); | |
| const endDate = startDate + 86400 * 3; // 3 days | |
| try { | |
| await propertyRental.connect(nonClient).bookProperty(1, startDate, endDate, { | |
| value: ethers.utils.parseEther("0.6"), | |
| }); | |
| assert.fail("Non-client was able to book a property"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Only clients can book properties"), | |
| "Unexpected error message for non-client booking property" | |
| ); | |
| } | |
| }); | |
| it("Should fail when booking with insufficient Ether", async function () { | |
| const startDate = Math.floor(Date.now() / 1000); | |
| const endDate = startDate + 86400 * 2; // 2 days | |
| const insufficientPayment = ethers.utils.parseEther("0.1"); // Less than required | |
| try { | |
| await propertyRental.connect(client2).bookProperty(1, startDate, endDate, { | |
| value: insufficientPayment, | |
| }); | |
| assert.fail("Booking succeeded with insufficient Ether"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Incorrect Ether value sent"), | |
| "Unexpected error message for insufficient Ether" | |
| ); | |
| } | |
| }); | |
| it("Should fail when booking a non-existent property", async function () { | |
| const startDate = Math.floor(Date.now() / 1000); | |
| const endDate = startDate + 86400 * 2; // 2 days | |
| try { | |
| await propertyRental.connect(client1).bookProperty(999, startDate, endDate, { | |
| value: ethers.utils.parseEther("0.4"), | |
| }); | |
| assert.fail("Booking succeeded for a non-existent property"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Property does not exist"), | |
| "Unexpected error message for non-existent property booking" | |
| ); | |
| } | |
| }); | |
| it("Should fail when booking an already booked property", async function () { | |
| const startDate = Math.floor(Date.now() / 1000); | |
| const endDate = startDate + 86400 * 3; // 3 days | |
| // Client1 books property | |
| await propertyRental.connect(client1).bookProperty(2, startDate, endDate, { | |
| value: ethers.utils.parseEther("0.9"), | |
| }); | |
| // Client2 tries to book the same property for overlapping dates | |
| try { | |
| await propertyRental.connect(client2).bookProperty(2, startDate + 86400, endDate + 86400, { | |
| value: ethers.utils.parseEther("0.9"), | |
| }); | |
| assert.fail("Double booking succeeded"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Property already booked for one or more days"), | |
| "Unexpected error message for double booking" | |
| ); | |
| } | |
| }); | |
| it("Should allow adding properties", async function () { | |
| const propertyCount = await propertyRental.getPropertyCount(); | |
| assert.strictEqual(propertyCount.toNumber(), 3, "Property count mismatch"); | |
| }); | |
| it("Should allow booking a property", async function () { | |
| const startDate = Math.floor(Date.now() / 1000); | |
| const endDate = startDate + 86400 * 3; // 3 days | |
| const bookingTx = await propertyRental.connect(client1).bookProperty(1, startDate, endDate, { | |
| value: ethers.utils.parseEther("0.6"), | |
| }); | |
| await bookingTx.wait(); | |
| // Check owner revenue | |
| const ownerRevenue = await propertyRental.ownerRevenue(owner1.address); | |
| assert.strictEqual( | |
| ownerRevenue.toString(), | |
| ethers.utils.parseEther("0.6").toString(), | |
| "Revenue mismatch after booking" | |
| ); | |
| }); | |
| it("Should allow withdrawing revenue", async function () { | |
| const initialBalance = await owner1.getBalance(); | |
| const revenue = await propertyRental.ownerRevenue(owner1.address); | |
| const withdrawTx = await propertyRental.connect(owner1).withdraw(revenue); | |
| await withdrawTx.wait(); | |
| const finalBalance = await owner1.getBalance(); | |
| assert(finalBalance.gt(initialBalance), "Balance not increased after withdrawal"); | |
| }); | |
| it("Should track NFTs correctly", async function () { | |
| const tokenIds = await nftContract.connect(owner1).getTokenIds(); | |
| assert(tokenIds.length > 0, "Owner does not own any NFTs"); | |
| for (const tokenId of tokenIds) { | |
| const name = await nftContract.getName(tokenId); | |
| const description = await nftContract.getDescription(tokenId); | |
| assert(name, "Token name is missing"); | |
| assert(description, "Token description is missing"); | |
| } | |
| }); | |
| it("Should fail for unauthorized actions", async function () { | |
| try { | |
| await ownerContract.connect(client1).addOwner(client2.address); | |
| assert.fail("Non-deployer added an owner"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Ownable: caller is not the owner"), | |
| "Unexpected error message for unauthorized action" | |
| ); | |
| } | |
| try { | |
| await propertyRental.connect(client1).addProperty("Test", "Test", ethers.utils.parseEther("1")); | |
| assert.fail("Non-owner added a property"); | |
| } catch (error) { | |
| assert( | |
| error.message.includes("Caller is not an owner"), | |
| "Unexpected error message for unauthorized action" | |
| ); | |
| } | |
| }); | |
| }); |
- implementarea de standarde ERC
Line 7 in 9c32606
| contract NFT is ERC721 { |
Line 32 in 9c32606
| _safeMint(owner, newTokenId); |
- utilizarea de Oracles
| import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; |
RentChain/contracts/PropertyOracles.sol
Line 28 in 9c32606
| AggregatorV3Interface internal priceFeed; |
RentChain/contracts/PropertyOracles.sol
Lines 41 to 44 in 9c32606
| function getLatestPrice() public view returns (int256) { | |
| (, int256 price, , , ) = priceFeed.latestRoundData(); | |
| return price; // Price in 8 decimal places (e.g., $2000.00000000) | |
| } |
- Utilizarea unei librării web3 (exemple web3 sau ethersjs) și conectarea cu un Web3 Provider pentru accesarea unor informații generale despre conturi (adresa, balance).
RentChain/scripts/ethersUtils.js
Lines 1 to 4 in b540600
| import { ethers } from "ethers"; | |
| import addresses from "../artifacts/addresses.json"; | |
| const provider = new ethers.providers.JsonRpcProvider("http://localhost:8545"); |
- Inițierea tranzacțiilor de transfer sau de apel de funcții, utilizând clase din librăriile web3.
RentChain/frontend/src/App.vue
Lines 54 to 84 in b540600
| import { getContractAddresses, getOwner1Info, getClient1Info, transferEther, getOwner1Tokens } from "../../scripts/ethersUtils"; | |
| export default { | |
| data() { | |
| return { | |
| accounts: [], | |
| balance: null, | |
| contractAddresses: {}, | |
| owner1Info: {}, | |
| client1Info: {}, | |
| rentCounter: 0, // Tracks how many months of rent have been paid | |
| rentButtonLabel: "Pay Rent", | |
| owner1Tokens: [], | |
| showTokens: false, }; | |
| }, | |
| async mounted() { | |
| this.contractAddresses = getContractAddresses(); | |
| this.owner1Info = await getOwner1Info(); | |
| this.client1Info = await getClient1Info(); | |
| this.owner1Tokens = await getOwner1Tokens(); | |
| // Load rentCounter and rentButtonLabel from localStorage | |
| const savedRentCounter = localStorage.getItem('rentCounter'); | |
| const savedRentButtonLabel = localStorage.getItem('rentButtonLabel'); | |
| if (savedRentCounter !== null) { | |
| this.rentCounter = parseInt(savedRentCounter, 10); | |
| } | |
| if (savedRentButtonLabel !== null) { | |
| this.rentButtonLabel = savedRentButtonLabel; | |
| } | |
| }, |
- Control al stării tranzacțiilor (tratare excepții)
RentChain/frontend/src/App.vue
Lines 109 to 139 in 7d90535
| async handlePayRent() { | |
| if (this.rentCounter >= 2) { | |
| alert("Maximum number of months already paid in advance!"); | |
| return; | |
| } | |
| try { | |
| const clientBalance = parseFloat(this.client1Info.balance); | |
| const requiredAmount = 0.1; | |
| if (clientBalance < requiredAmount) { | |
| alert("INSUFFICIENT FUNDS"); | |
| return; | |
| } | |
| const from = this.client1Info.address; // Sender address (client1) | |
| const to = this.owner1Info.address; // Recipient address (owner1) | |
| const amount = "0.1"; // Amount in ETH to transfer | |
| await transferEther(from, to, amount); | |
| this.rentCounter++; | |
| this.updateRentButtonLabel(); | |
| this.client1Info = await getClient1Info(); | |
| this.owner1Info = await getOwner1Info(); | |
| alert("Rent payment successful!"); | |
| } catch (error) { | |
| console.error("Error during rent payment:", error); | |
| alert("Rent payment failed!"); | |
| } | |
| }, |
- Analiza gas-cost (estimare cost și fixare limită de cost).
Lines 14 to 16 in 7d90535
| const ownerDeployGas = await ownerContract.deployTransaction.gasLimit; | |
| console.log("Estimated Gas used for Owner contract deployment:", ownerDeployGas.toString()); | |
| console.log(); |
Lines 24 to 26 in 7d90535
| const nftDeployGas = await nftContract.deployTransaction.gasLimit; | |
| console.log("Estimated Gas used for NFT contract deployment:", nftDeployGas.toString()); | |
| console.log(); |
Lines 34 to 36 in 7d90535
| const clientDeployGas = await clientContract.deployTransaction.gasLimit; | |
| console.log("Estimated Gas used for Client contract deployment:", clientDeployGas.toString()); | |
| console.log(); |
Line 54 in 7d90535
| const addOwnerGasWithBuffer = addOwnerGas.add(ethers.BigNumber.from('50000')); // Adding buffer of 50k gas units |