Skip to content

Qarty26/RentChain

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

Partea 1: Implementarea smart-contractelor


Cerinte obligatorii:

  • utilizarea tipurilor de date specifice Solidity (mappings, address).

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.

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.

modifier onlyPropertyRental() {
require(msg.sender == propertyRentalContract, "Only PropertyRental can call this function");
_;
}

  • exemple pentru toate tipurile de funcții (external, pure, view etc.)

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.

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.

Owner public ownerContract;
Client public clientContract;
NFT public nftContract;

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

Cerinte optionale:

  • utilizare librării

contract NFT is ERC721 {

_safeMint(owner, newTokenId);

  • implementarea de teste (cu tool-uri la alegerea echipelor).
npx hardhat test

RentChain/test/Test.js

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

contract NFT is ERC721 {

_safeMint(owner, newTokenId);

  • utilizarea de Oracles

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
AggregatorV3Interface internal priceFeed;
function getLatestPrice() public view returns (int256) {
(, int256 price, , , ) = priceFeed.latestRoundData();
return price; // Price in 8 decimal places (e.g., $2000.00000000)
}

Partea 2: Interacțiunea cu blockchain printr-o aplicație web3.


Cerinte obligatorii:

  • 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).

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.

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;
}
},

Cerinte optionale:

  • Control al stării tranzacțiilor (tratare excepții)

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).

const ownerDeployGas = await ownerContract.deployTransaction.gasLimit;
console.log("Estimated Gas used for Owner contract deployment:", ownerDeployGas.toString());
console.log();

const nftDeployGas = await nftContract.deployTransaction.gasLimit;
console.log("Estimated Gas used for NFT contract deployment:", nftDeployGas.toString());
console.log();

const clientDeployGas = await clientContract.deployTransaction.gasLimit;
console.log("Estimated Gas used for Client contract deployment:", clientDeployGas.toString());
console.log();

const addOwnerGasWithBuffer = addOwnerGas.add(ethers.BigNumber.from('50000')); // Adding buffer of 50k gas units

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors