-
setTokenUrI(e.target.value)} />
-
setRoyalty(Number(e.target.value))} />
-
Mint NFT
+
+
Dashboard
+
+
+
+
Total NFTs minted
+
{totalSupply?.toString() ?? '—'}
+
+
+
NFTs you own
+
{ownedCount?.toString() ?? '—'}
+
+
+
+
+
+
+
+
- )
-}
+ );
+};
-export default Dashboard
\ No newline at end of file
+export default Dashboard;
\ No newline at end of file
diff --git a/frontend-lectures/lesson3/openriver/src/pages/list/index.tsx b/frontend-lectures/lesson3/openriver/src/pages/list/index.tsx
index d0896b1..a3d2f63 100644
--- a/frontend-lectures/lesson3/openriver/src/pages/list/index.tsx
+++ b/frontend-lectures/lesson3/openriver/src/pages/list/index.tsx
@@ -1,35 +1,44 @@
import { useState } from 'react'
import { useWriteContract } from 'wagmi';
import { openriverAbi, openriverAddress } from '../../contracts';
+import Button from '../../components/Button';
- const index = () => {
- const { writeContract: mintNFT } = useWriteContract();
+const index = () => {
+ const { writeContract: listNFT } = useWriteContract();
const [tokenId, setTokenId] = useState(0);
const [price, setPrice] = useState(0);
- const handleMintNFT = async () => {
- mintNFT(
- {
- abi: openriverAbi,
- address: openriverAddress,
- functionName: "listOnMarketplace",
- args: [
- tokenId,
- price
- ]
- }
- )
- }
+ const handleListNFT = () => {
+ listNFT({
+ abi: openriverAbi,
+ address: openriverAddress,
+ functionName: "listOnMarketplace",
+ args: [
+ BigInt(tokenId), // contract expects uint256
+ BigInt(price), // contract expects uint256
+ ],
+ });
+ };
+
return (
- )
-}
-
+ );
+};
-export default index
\ No newline at end of file
+export default index;
diff --git a/frontend-lectures/lesson3/openriver/src/pages/marketplace/index.tsx b/frontend-lectures/lesson3/openriver/src/pages/marketplace/index.tsx
new file mode 100644
index 0000000..8965626
--- /dev/null
+++ b/frontend-lectures/lesson3/openriver/src/pages/marketplace/index.tsx
@@ -0,0 +1,36 @@
+import { NextPage } from "next";
+import { useReadContract } from "wagmi";
+import { openriverAbi, openriverAddress } from "../../contracts";
+import Card from "../../components/Card";
+
+const Marketplace: NextPage = () => {
+ const { data: totalSupplyRaw } = useReadContract({
+ abi: openriverAbi,
+ address: openriverAddress,
+ functionName: "tokenIds",
+ });
+
+ const totalSupply = totalSupplyRaw as bigint | undefined;
+
+ return (
+
+
Marketplace
+
+
+ {Array.from({ length: Number(totalSupply ?? 0) }, (_, i) => i + 1).map((index) => (
+
+
+
+ ))}
+
+
+ {totalSupply === BigInt(0) && (
+
+ )}
+
+ );
+};
+
+export default Marketplace;
diff --git a/frontend-lectures/lesson3/openriver/src/pages/mint/index.tsx b/frontend-lectures/lesson3/openriver/src/pages/mint/index.tsx
index ebd6ea6..a782d48 100644
--- a/frontend-lectures/lesson3/openriver/src/pages/mint/index.tsx
+++ b/frontend-lectures/lesson3/openriver/src/pages/mint/index.tsx
@@ -1,35 +1,44 @@
import React, { useState } from 'react'
import { useWriteContract } from 'wagmi';
import { openriverAbi, openriverAddress } from '../../contracts';
+import Button from '../../components/Button';
- const index = () => {
- const { writeContract: mintNFT } = useWriteContract();
+const index = () => {
+ const { writeContract: mintNFT } = useWriteContract();
const [tokenUrI, setTokenUrI] = useState("");
const [royalty, setRoyalty] = useState(0);
- const handleMintNFT = async () => {
- mintNFT(
- {
- abi: openriverAbi,
- address: openriverAddress,
- functionName: "newItem",
- args: [
- tokenUrI,
- royalty
- ]
- }
- )
- }
+ const handleMintNFT = () => {
+ mintNFT({
+ abi: openriverAbi,
+ address: openriverAddress,
+ functionName: "newItem",
+ args: [
+ tokenUrI,
+ BigInt(royalty), // contract expects uint256
+ ],
+ });
+ };
+
return (
- )
-}
-
+ );
+};
-export default index
\ No newline at end of file
+export default index;
diff --git a/frontend-lectures/lesson3/openriver/src/pages/myNFT/index.tsx b/frontend-lectures/lesson3/openriver/src/pages/myNFT/index.tsx
index 3c1e0b9..c52abf5 100644
--- a/frontend-lectures/lesson3/openriver/src/pages/myNFT/index.tsx
+++ b/frontend-lectures/lesson3/openriver/src/pages/myNFT/index.tsx
@@ -2,36 +2,25 @@ import { NextPage } from "next";
import { Cards } from "../../components/Cards";
import { useReadContract } from "wagmi";
import { openriverAbi, openriverAddress } from "../../contracts";
-import { useEffect, useState } from "react";
const MyNFT: NextPage = () => {
- const [nftsNum, setNftsNum] = useState
()
-
- const { data: nftmaxNum } = useReadContract({
+ const { data } = useReadContract({
abi: openriverAbi,
address: openriverAddress,
functionName: "tokenIds",
- }) as any
-
- useEffect(() => {
- setNftsNum(nftmaxNum)
- }, [nftmaxNum])
-
- useEffect(() => {
- setNftsNum(nftmaxNum)
- }, [nftmaxNum])
+ });
+ const nftmaxNum = data as bigint | undefined;
+ // nftmaxNum is bigint | undefined — pass directly to Cards
return (
-
-
-
{nftsNum?.toString()}
-
-
+
+
{nftmaxNum?.toString()}
+
+
-
);
};
-export default MyNFT;
\ No newline at end of file
+export default MyNFT;
diff --git a/frontend-lectures/lesson3/openriver/tsconfig.json b/frontend-lectures/lesson3/openriver/tsconfig.json
index 748d124..ce48752 100644
--- a/frontend-lectures/lesson3/openriver/tsconfig.json
+++ b/frontend-lectures/lesson3/openriver/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "target": "es5",
"lib": [
"dom",
"dom.iterable",
@@ -17,7 +16,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
- "incremental": true
+ "incremental": true,
+ "target": "ES2017"
},
"include": [
"next-env.d.ts",
diff --git a/wk-6 Testing/hardhat_test/contracts/Adashe.sol b/wk-6 Testing/hardhat_test/contracts/Adashe.sol
index fe83797..701209b 100644
--- a/wk-6 Testing/hardhat_test/contracts/Adashe.sol
+++ b/wk-6 Testing/hardhat_test/contracts/Adashe.sol
@@ -1,9 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
-/// @title Adashe
-/// @notice A rotating savings circle (ROSCA): members register, contribute each round,
-/// and the pooled amount goes to the member whose turn matches the current round.
contract Adashe {
uint8 public noOfPeople;
uint8 public maxPeople;
@@ -62,17 +59,11 @@ contract Adashe {
}
modifier adasheIsNotFull() {
- // if (noOfPeople >= maxPeople) revert AdasheFull();
require(noOfPeople < maxPeople, "AdasheFull");
_;
}
- /// @notice Register for the savings circle. Turn order follows registration order.
function registerForAdashe(string calldata _name) public adasheIsNotFull returns (uint256 turn_) {
- // if (isRegistered[msg.sender]) revert AlreadyRegistered();
- // if (bytes(_name).length == 0) revert EmptyName();
-
-
require(!isRegistered[msg.sender], "AlreadyRegistered");
require(bytes(_name).length > 0, "EmptyName");
noOfPeople++;
@@ -99,7 +90,6 @@ contract Adashe {
return turn_;
}
- /// @notice Pay your contribution for the current round.
function contribute() external payable {
if (status != Packed.PACKED) revert AdasheNotPacked();
if (!isRegistered[msg.sender]) revert NotRegistered();
@@ -120,7 +110,6 @@ contract Adashe {
}
}
- /// @notice Read a member by 1-based person id.
function getAdasheMember(uint8 _personId) public view returns (Person memory person_) {
if (_personId == 0 || _personId > noOfPeople) revert InvalidPersonId();
person_ = adashePeople[_personId];
diff --git a/wk-6 Testing/hardhat_test/contracts/escrow.sol b/wk-6 Testing/hardhat_test/contracts/escrow.sol
new file mode 100644
index 0000000..4cc2af1
--- /dev/null
+++ b/wk-6 Testing/hardhat_test/contracts/escrow.sol
@@ -0,0 +1,204 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.28;
+
+contract Escrow {
+
+ uint256 public escrowCount;
+
+ enum State {
+ OPEN,
+ FUNDED,
+ COMPLETE,
+ CANCELLED
+ }
+
+ struct EscrowDeal {
+ uint256 id;
+ address buyer;
+ address seller;
+ uint256 amount;
+ State state;
+ uint256 createdAt;
+ }
+
+ mapping(uint256 => EscrowDeal) public escrows;
+
+
+
+ event EscrowCreated(
+ uint256 indexed escrowId,
+ address indexed buyer,
+ address indexed seller
+ );
+
+ event Deposited(
+ uint256 indexed escrowId,
+ address indexed buyer,
+ uint256 amount
+ );
+
+ event DeliveryConfirmed(
+ uint256 indexed escrowId,
+ address indexed seller,
+ uint256 amount
+ );
+
+ event EscrowCancelled(
+ uint256 indexed escrowId
+ );
+
+
+
+ error NotBuyer();
+ error NotSeller();
+ error InvalidState();
+ error ZeroAmount();
+ error TransferFailed();
+ error InvalidAddress();
+
+
+
+ modifier onlyBuyer(uint256 _id) {
+ if (msg.sender != escrows[_id].buyer)
+ revert NotBuyer();
+ _;
+ }
+
+ modifier onlySeller(uint256 _id) {
+ if (msg.sender != escrows[_id].seller)
+ revert NotSeller();
+ _;
+ }
+
+ modifier inState(uint256 _id, State expectedState) {
+ if (escrows[_id].state != expectedState)
+ revert InvalidState();
+ _;
+ }
+
+
+
+ function createEscrow(
+ address _buyer,
+ address _seller
+ ) external returns (uint256 escrowId) {
+
+ if (_buyer == address(0) || _seller == address(0))
+ revert InvalidAddress();
+
+ escrowCount++;
+
+ escrowId = escrowCount;
+
+ escrows[escrowId] = EscrowDeal({
+ id: escrowId,
+ buyer: _buyer,
+ seller: _seller,
+ amount: 0,
+ state: State.OPEN,
+ createdAt: block.timestamp
+ });
+
+ emit EscrowCreated(
+ escrowId,
+ _buyer,
+ _seller
+ );
+ }
+
+
+ function deposit(uint256 _id)
+ external
+ payable
+ onlyBuyer(_id)
+ inState(_id, State.OPEN)
+ {
+ if (msg.value == 0)
+ revert ZeroAmount();
+
+ EscrowDeal storage escrow = escrows[_id];
+
+ escrow.amount = msg.value;
+
+ escrow.state = State.FUNDED;
+
+ emit Deposited(
+ _id,
+ msg.sender,
+ msg.value
+ );
+ }
+
+
+
+ function confirmDelivery(uint256 _id)
+ external
+ onlyBuyer(_id)
+ inState(_id, State.FUNDED)
+ {
+ EscrowDeal storage escrow = escrows[_id];
+
+ uint256 payment = escrow.amount;
+
+ escrow.amount = 0;
+
+ escrow.state = State.COMPLETE;
+
+ (bool success,) = payable(
+ escrow.seller
+ ).call{value: payment}("");
+
+ if (!success)
+ revert TransferFailed();
+
+ emit DeliveryConfirmed(
+ _id,
+ escrow.seller,
+ payment
+ );
+ }
+
+
+ function cancelEscrow(uint256 _id)
+ external
+ onlyBuyer(_id)
+ inState(_id, State.OPEN)
+ {
+ escrows[_id].state = State.CANCELLED;
+
+ emit EscrowCancelled(_id);
+ }
+
+
+
+ function refund(uint256 _id)
+ external
+ onlySeller(_id)
+ inState(_id, State.FUNDED)
+ {
+ EscrowDeal storage escrow = escrows[_id];
+
+ uint256 payment = escrow.amount;
+
+ escrow.amount = 0;
+
+ escrow.state = State.CANCELLED;
+
+ (bool success,) = payable(
+ escrow.buyer
+ ).call{value: payment}("");
+
+ if (!success)
+ revert TransferFailed();
+ }
+
+
+
+ function getEscrow(uint256 _id)
+ external
+ view
+ returns (EscrowDeal memory)
+ {
+ return escrows[_id];
+ }
+}
\ No newline at end of file
diff --git a/wk-6 Testing/hardhat_test/test/Adashe.ts b/wk-6 Testing/hardhat_test/test/Adashe.ts
index 285e82d..096e204 100644
--- a/wk-6 Testing/hardhat_test/test/Adashe.ts
+++ b/wk-6 Testing/hardhat_test/test/Adashe.ts
@@ -43,7 +43,7 @@ describe("Deployment Successful", function () {
});
});
-describe.only("Registering a new member", function () {
+describe("Registering a new member", function () {
it("Should register a new member", async function () {
await adashe.connect(addr1).registerForAdashe("John Doe");
expect(await adashe.noOfPeople()).to.equal(1);
@@ -75,5 +75,248 @@ describe.only("Registering a new member", function () {
await expect(adashe.connect(addr4).registerForAdashe("Jack Doe")).to.be.revertedWith("AdasheFull");
});
});
+
+describe("Contribution", function () {
+
+ beforeEach(async function () {
+
+ adashe = await ethers.deployContract(
+ "Adashe",
+ [maxPeople, amountPerHead, duration]
+ );
+
+ [owner, addr1, addr2, addr3, addr4] =
+ await ethers.getSigners();
+
+ });
+
+ it("Should revert if Adashe is not packed", async function () {
+
+ await expect(
+
+ adashe.connect(addr1).contribute({
+
+ value: amountPerHead
+
+ })
+
+ )
+
+ .to.be.revertedWithCustomError(
+
+ adashe,
+
+ "AdasheNotPacked"
+
+ );
+
+});
+
+it("Should revert if sender is not registered", async function () {
+
+ await adashe.connect(addr1).registerForAdashe("John");
+
+ await adashe.connect(addr2).registerForAdashe("Jane");
+
+ await adashe.connect(addr3).registerForAdashe("Peter");
+
+ await expect(
+
+ adashe.connect(owner).contribute({
+
+ value: amountPerHead
+
+ })
+
+ )
+
+ .to.be.revertedWithCustomError(
+
+ adashe,
+
+ "NotRegistered"
+
+ );
+
+});
+it("Should revert if amount is incorrect", async function () {
+
+ await adashe.connect(addr1).registerForAdashe("John");
+
+ await adashe.connect(addr2).registerForAdashe("Jane");
+
+ await adashe.connect(addr3).registerForAdashe("Peter");
+
+ await expect(
+
+ adashe.connect(addr1).contribute({
+
+ value: ethers.parseEther("0.5")
+
+ })
+
+ )
+
+ .to.be.revertedWithCustomError(
+
+ adashe,
+
+ "InvalidAmount"
+
+ );
+
+});
+it("Should revert if member already paid", async function () {
+
+ await adashe.connect(addr1).registerForAdashe("John");
+
+ await adashe.connect(addr2).registerForAdashe("Jane");
+
+ await adashe.connect(addr3).registerForAdashe("Peter");
+
+ await adashe.connect(addr1).contribute({
+
+ value: amountPerHead
+
+ });
+
+ await expect(
+
+ adashe.connect(addr1).contribute({
+
+ value: amountPerHead
+
+ })
+
+ )
+
+ .to.be.revertedWithCustomError(
+
+ adashe,
+
+ "AlreadyPaid"
+
+ );
+
+});
+// it("Should revert if round has expired", async function () {
+
+// await adashe.connect(addr1).registerForAdashe("John");
+
+// await adashe.connect(addr2).registerForAdashe("Jane");
+
+// await adashe.connect(addr3).registerForAdashe("Peter");
+
+// await network.provider.send(
+
+// "evm_increaseTime",
+
+// [Number(duration) + 10]
+
+// );
+
+// await network.provider.send(
+
+// "evm_mine"
+
+// );
+
+// await expect(
+
+// adashe.connect(addr1).contribute({
+
+// value: amountPerHead
+
+// })
+
+// )
+
+// .to.be.revertedWithCustomError(
+
+// adashe,
+
+// "RoundExpired"
+
+// );
+
+// });
+it("Should allow a member to contribute", async function () {
+
+ await adashe.connect(addr1).registerForAdashe("John");
+
+ await adashe.connect(addr2).registerForAdashe("Jane");
+
+ await adashe.connect(addr3).registerForAdashe("Peter");
+
+ await expect(
+
+ adashe.connect(addr1).contribute({
+
+ value: amountPerHead
+
+ })
+
+ )
+
+ .to.emit(
+
+ adashe,
+
+ "ContributionReceived"
+
+ )
+
+ .withArgs(
+
+ addr1.address,
+
+ 1,
+
+ amountPerHead
+
+ );
+
+ const member = await adashe.getAdasheMember(1);
+
+ expect(member.hasPaid).to.equal(true);
+
+});
+it("Should distribute payout after everyone contributes", async function () {
+
+ await adashe.connect(addr1).registerForAdashe("John");
+
+ await adashe.connect(addr2).registerForAdashe("Jane");
+
+ await adashe.connect(addr3).registerForAdashe("Peter");
+
+ await expect(
+
+ async () => {
+
+ await adashe.connect(addr1).contribute({
+
+ value: amountPerHead
+
+ });
+
+ await adashe.connect(addr2).contribute({
+
+ value: amountPerHead
+
+ });
+
+ await adashe.connect(addr3).contribute({
+
+ value: amountPerHead
+
+ });
+
+ }
+
+ )
+
+});
+
+});
});
diff --git a/wk-6 Testing/hardhat_test/test/escrow.ts b/wk-6 Testing/hardhat_test/test/escrow.ts
new file mode 100644
index 0000000..5f299c8
--- /dev/null
+++ b/wk-6 Testing/hardhat_test/test/escrow.ts
@@ -0,0 +1,334 @@
+import { expect } from "chai";
+import { network } from "hardhat";
+
+const { ethers } = await network.create();
+
+describe("Escrow", function () {
+
+ let escrow: any;
+
+ let buyer: any;
+ let seller: any;
+ let holder: any;
+
+ beforeEach(async function () {
+
+ [buyer, seller, holder] =
+ await ethers.getSigners();
+
+ escrow =
+ await ethers.deployContract("Escrow");
+
+ });
+
+ // ─── createEscrow ─────────────────────────────────────────────────────────
+
+ describe("createEscrow", function () {
+
+ it("should create an escrow and emit EscrowCreated", async function () {
+
+ await expect(
+ escrow.createEscrow(buyer.address, seller.address)
+ )
+ .to.emit(escrow, "EscrowCreated")
+ .withArgs(1, buyer.address, seller.address);
+
+ const deal = await escrow.getEscrow(1);
+ expect(deal.id).to.equal(1);
+ expect(deal.buyer).to.equal(buyer.address);
+ expect(deal.seller).to.equal(seller.address);
+ expect(deal.amount).to.equal(0);
+ expect(deal.state).to.equal(0); // State.OPEN
+
+ });
+
+ it("should increment escrowCount on each creation", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ expect(await escrow.escrowCount()).to.equal(2);
+
+ });
+
+ it("should revert if buyer address is zero", async function () {
+
+ await expect(
+ escrow.createEscrow(ethers.ZeroAddress, seller.address)
+ ).to.be.revertedWithCustomError(escrow, "InvalidAddress");
+
+ });
+
+ it("should revert if seller address is zero", async function () {
+
+ await expect(
+ escrow.createEscrow(buyer.address, ethers.ZeroAddress)
+ ).to.be.revertedWithCustomError(escrow, "InvalidAddress");
+
+ });
+
+ });
+
+ // ─── deposit ──────────────────────────────────────────────────────────────
+
+ describe("deposit", function () {
+
+ it("should revert for zero amount", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(buyer).deposit(1, { value: ethers.parseEther("0") })
+ ).to.be.revertedWithCustomError(escrow, "ZeroAmount");
+
+ });
+
+ it("should allow buyer to deposit and emit Deposited", async function () {
+
+ const amount = ethers.parseEther("1");
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(buyer).deposit(1, { value: amount })
+ )
+ .to.emit(escrow, "Deposited")
+ .withArgs(1, buyer.address, amount);
+
+ });
+
+ it("should revert if caller is not buyer", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(holder).deposit(1, { value: ethers.parseEther("1") })
+ ).to.be.revertedWithCustomError(escrow, "NotBuyer");
+
+ });
+
+ it("should set state to FUNDED after deposit", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: ethers.parseEther("1") });
+
+ const deal = await escrow.getEscrow(1);
+ expect(deal.state).to.equal(1); // State.FUNDED
+
+ });
+
+ it("should revert if escrow is not in OPEN state", async function () {
+
+ const amount = ethers.parseEther("1");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+
+ // Already FUNDED — second deposit should fail
+ await expect(
+ escrow.connect(buyer).deposit(1, { value: amount })
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ });
+
+ // ─── confirmDelivery ──────────────────────────────────────────────────────
+
+ describe("confirmDelivery", function () {
+
+ it("should confirm delivery, pay seller, and emit DeliveryConfirmed", async function () {
+
+ const amount = ethers.parseEther("1");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+
+ const sellerBefore = await ethers.provider.getBalance(seller.address);
+
+ await expect(
+ escrow.connect(buyer).confirmDelivery(1)
+ )
+ .to.emit(escrow, "DeliveryConfirmed")
+ .withArgs(1, seller.address, amount);
+
+ const sellerAfter = await ethers.provider.getBalance(seller.address);
+ expect(sellerAfter - sellerBefore).to.equal(amount);
+
+ const deal = await escrow.getEscrow(1);
+ expect(deal.state).to.equal(2); // State.COMPLETE
+ expect(deal.amount).to.equal(0);
+
+ });
+
+ it("should revert if caller is not buyer", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: ethers.parseEther("1") });
+
+ await expect(
+ escrow.connect(holder).confirmDelivery(1)
+ ).to.be.revertedWithCustomError(escrow, "NotBuyer");
+
+ });
+
+ it("should revert if state is not FUNDED", async function () {
+
+ // Still OPEN — never deposited
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(buyer).confirmDelivery(1)
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ it("should revert if already COMPLETE", async function () {
+
+ const amount = ethers.parseEther("1");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+ await escrow.connect(buyer).confirmDelivery(1);
+
+ await expect(
+ escrow.connect(buyer).confirmDelivery(1)
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ });
+
+
+
+ describe("cancelEscrow", function () {
+
+ it("should cancel an OPEN escrow and emit EscrowCancelled", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(buyer).cancelEscrow(1)
+ )
+ .to.emit(escrow, "EscrowCancelled")
+ .withArgs(1);
+
+ const deal = await escrow.getEscrow(1);
+ expect(deal.state).to.equal(3); // State.CANCELLED
+
+ });
+
+ it("should revert if caller is not buyer", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(holder).cancelEscrow(1)
+ ).to.be.revertedWithCustomError(escrow, "NotBuyer");
+
+ });
+
+ it("should revert if escrow is not OPEN (e.g. FUNDED)", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: ethers.parseEther("1") });
+
+ await expect(
+ escrow.connect(buyer).cancelEscrow(1)
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ });
+
+ // ─── refund ───────────────────────────────────────────────────────────────
+
+ describe("refund", function () {
+
+ it("should refund buyer and set state to CANCELLED", async function () {
+
+ const amount = ethers.parseEther("1");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+
+ const buyerBefore = await ethers.provider.getBalance(buyer.address);
+
+ await escrow.connect(seller).refund(1);
+
+ const buyerAfter = await ethers.provider.getBalance(buyer.address);
+ expect(buyerAfter - buyerBefore).to.equal(amount);
+
+ const deal = await escrow.getEscrow(1);
+ expect(deal.state).to.equal(3); // State.CANCELLED
+ expect(deal.amount).to.equal(0);
+
+ });
+
+ it("should revert if caller is not seller", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: ethers.parseEther("1") });
+
+ await expect(
+ escrow.connect(holder).refund(1)
+ ).to.be.revertedWithCustomError(escrow, "NotSeller");
+
+ });
+
+ it("should revert if state is not FUNDED", async function () {
+
+ // OPEN state — no deposit yet
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ await expect(
+ escrow.connect(seller).refund(1)
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ it("should revert if already CANCELLED", async function () {
+
+ const amount = ethers.parseEther("1");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+ await escrow.connect(seller).refund(1);
+
+ await expect(
+ escrow.connect(seller).refund(1)
+ ).to.be.revertedWithCustomError(escrow, "InvalidState");
+
+ });
+
+ });
+
+
+
+ describe("getEscrow", function () {
+
+ it("should return correct escrow data", async function () {
+
+ await escrow.createEscrow(buyer.address, seller.address);
+
+ const deal = await escrow.getEscrow(1);
+
+ expect(deal.id).to.equal(1);
+ expect(deal.buyer).to.equal(buyer.address);
+ expect(deal.seller).to.equal(seller.address);
+ expect(deal.amount).to.equal(0);
+ expect(deal.state).to.equal(0); // State.OPEN
+
+ });
+
+ it("should reflect updated state after deposit", async function () {
+
+ const amount = ethers.parseEther("2");
+ await escrow.createEscrow(buyer.address, seller.address);
+ await escrow.connect(buyer).deposit(1, { value: amount });
+
+ const deal = await escrow.getEscrow(1);
+
+ expect(deal.amount).to.equal(amount);
+ expect(deal.state).to.equal(1); // State.FUNDED
+
+ });
+
+ });
+
+});