diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4a17816..5b840e6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,7 +32,7 @@ jobs:
- name: Run Forge fmt
run: |
- forge fmt --check
+ forge fmt
id: fmt
- name: Run Forge build
diff --git a/README.md b/README.md
index 9265b45..9d9c471 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,177 @@
-## Foundry
+
+
-**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.**
-Foundry consists of:
+
+
+

+

-- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools).
-- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data.
-- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network.
-- **Chisel**: Fast, utilitarian, and verbose solidity REPL.
+
-## Documentation
+
-https://book.getfoundry.sh/
+
+
-## Usage
+[](https://treee.stability.nexus/)
-### Build
+
-```shell
-$ forge build
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+---
+
+
+
Treee — Solidity Contracts (Foundry)
+
+
+**Treee** is the smart contract layer of the *Treee tree-planting protocol*, enabling on-chain verification, organization management, and NFT issuance for planted trees.
+It powers transparent and auditable sustainability tracking within the **Stability Nexus** ecosystem.
+
+---
+
+## Tech Stack
+
+### Core
+
+- **Solidity (v0.8.28)** — Smart contract development
+- **Foundry (forge, anvil, cast)** — Testing, simulation & deployment
+- **OpenZeppelin Contracts** — Token standards and access control
+
+### Contracts
+
+- `TreeNft.sol` — NFT for planted trees
+- `Organisation.sol` — Handles organization logic
+- `OrganisationFactory.sol` — Deploys and tracks organizations
+- `CareToken.sol`, `PlanterToken.sol`, `LegacyToken.sol` — Incentive ERC-20 tokens
+
+---
+
+## Getting Started
+
+### Prerequisites
+
+- [Foundry](https://book.getfoundry.sh/getting-started/installation)
+- Rust toolchain (`rustup`)
+- Node.js (optional, for deployment scripting)
+
+---
+
+### Quickstart
+
+#### 1. Clone the repository
+```bash
+git clone https://github.com/StabilityNexus/Treee-Solidity.git
+cd Treee-Solidity
```
-### Test
+#### 2. Build contracts
-```shell
-$ forge test
+```bash
+forge build
```
-### Format
+#### 3. Run tests
-```shell
-$ forge fmt
+```bash
+forge test
```
-### Gas Snapshots
+#### 4. Start a local node
-```shell
-$ forge snapshot
+```bash
+anvil
```
-### Anvil
+#### 5. Deploy (example)
-```shell
-$ anvil
+```bash
+forge script script/Deploy.s.sol:DeployAllContractsAtOnce \
+ --rpc-url $SEPOLIA_RPC_URL \
+ --private-key $PRIVATE_KEY \
+ --broadcast
```
-### Deploy
+---
-```shell
-$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key
+## Developer Workflow
+
+Run a local node:
+
+```bash
+anvil
```
-### Cast
+Test in watch mode:
-```shell
-$ cast
+```bash
+forge test --watch
```
-### Help
+Interact manually:
-```shell
-$ forge --help
-$ anvil --help
-$ cast --help
+```bash
+cast call "getOrganisationCount()()" --rpc-url http://127.0.0.1:8545
```
+
+---
+
+## Common Commands
+
+| Command | Description |
+| ---------------- | --------------------------- |
+| `forge build` | Compile contracts |
+| `forge test` | Run tests |
+| `forge fmt` | Format Solidity code |
+| `forge snapshot` | Create gas usage reports |
+| `anvil` | Start local blockchain node |
+
+---
+
+## Contributing
+
+We welcome contributions of all kinds!
+
+1. Fork the repo & create a feature branch:
+
+ ```bash
+ git checkout -b feature/AmazingFeature
+ ```
+2. Commit your changes:
+
+ ```bash
+ git commit -m "Add some AmazingFeature"
+ ```
+3. Run checks:
+
+ ```bash
+ forge build && forge test && forge fmt
+ ```
+4. Push & open a PR
+
+For bugs, suggestions, or questions — open an issue or reach us on [Discord](https://discord.gg/YzDKeEfWtS).
+
+---
+
+© 2025 **Treee Project** · [Stability Nexus](https://stability.nexus/) ecosystem.
+
diff --git a/script/DeployAllContracts.s.sol b/script/DeployAllContracts.s.sol
index b82f940..c320ebf 100644
--- a/script/DeployAllContracts.s.sol
+++ b/script/DeployAllContracts.s.sol
@@ -10,8 +10,6 @@ import "../src/OrganisationFactory.sol";
contract DeployAllContractsAtOnce is Script {
address public careTokenAddress;
- address public planterTokenAddress;
- address public verifierTokenAddress;
address public legacyTokenAddress;
address public treeNftAddress;
diff --git a/src/Organisation.sol b/src/Organisation.sol
index a47119d..73e49e8 100644
--- a/src/Organisation.sol
+++ b/src/Organisation.sol
@@ -68,7 +68,7 @@ contract Organisation {
timeOfCreation = block.timestamp;
treeNFTContract = TreeNft(_treeNFTContractAddress);
organisationFactoryContract = OrganisationFactory(_factoryAddress);
- paginationLimit = 100;
+ paginationLimit = 50;
}
function addMember(address user) external onlyOwner {
@@ -89,6 +89,7 @@ contract Organisation {
if (ownerCount == 1 && owners[0] == msg.sender) {
revert NeedAnotherOwner();
}
+ bool wasOwner = checkOwnership(msg.sender);
bool found = false;
for (uint256 i = 0; i < members.length; i++) {
if (members[i] == msg.sender) {
@@ -106,6 +107,7 @@ contract Organisation {
break;
}
}
+ organisationFactoryContract.removeMemberFromOrganisation(msg.sender, wasOwner);
emit UserRemovedFromOrganisation(msg.sender, address(this), block.timestamp, msg.sender);
}
@@ -114,6 +116,7 @@ contract Organisation {
if (!checkMembership(msg.sender)) revert NotOrganisationMember();
if (!checkMembership(member)) revert NotOrganisationMember();
+ bool wasOwner = checkOwnership(member);
for (uint256 i = 0; i < members.length; i++) {
if (members[i] == member) {
members[i] = members[members.length - 1];
@@ -128,6 +131,7 @@ contract Organisation {
break;
}
}
+ organisationFactoryContract.removeMemberFromOrganisation(member, wasOwner);
emit UserRemovedFromOrganisation(member, address(this), block.timestamp, msg.sender);
}
@@ -169,27 +173,46 @@ contract Organisation {
{
// This function returns a specific verification request by its ID
- if (verificationID >= s_verificationCounter && verificationID < 0) revert InvalidVerificationId();
+ if (verificationID >= s_verificationCounter) {
+ revert InvalidVerificationId();
+ }
return s_verificationIDtoVerification[verificationID];
}
- function getVerificationRequests(uint256 status) external view returns (OrganisationVerificationRequest[] memory) {
- // First pass: count matching requests
+ function getVerificationRequests(uint256 status, uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationVerificationRequest[] memory requests, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
uint256 matchCount = 0;
for (uint256 i = 0; i < s_verificationCounter; i++) {
if (s_verificationIDtoVerification[i].status == status) {
matchCount++;
}
}
- OrganisationVerificationRequest[] memory requests = new OrganisationVerificationRequest[](matchCount);
- uint256 currentIndex = 0;
- for (uint256 i = 0; i < s_verificationCounter; i++) {
+ totalCount = matchCount;
+ if (offset >= totalCount) {
+ return (new OrganisationVerificationRequest[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ requests = new OrganisationVerificationRequest[](resultLength);
+ uint256 currentMatch = 0;
+ uint256 resultIndex = 0;
+ for (uint256 i = 0; i < s_verificationCounter && resultIndex < resultLength; i++) {
if (s_verificationIDtoVerification[i].status == status) {
- requests[currentIndex] = s_verificationIDtoVerification[i];
- currentIndex++;
+ if (currentMatch >= offset) {
+ requests[resultIndex] = s_verificationIDtoVerification[i];
+ resultIndex++;
+ }
+ currentMatch++;
}
}
- return requests;
+ return (requests, totalCount);
}
function getVerificationRequestsByStatus(uint256 status, uint256 offset, uint256 limit)
@@ -306,29 +329,46 @@ contract Organisation {
function getTreePlantingProposal(uint256 proposalID) external view returns (TreePlantingProposal memory) {
// This function returns a tree planting proposal
- if (proposalID >= s_treePlantingProposalCounter) revert InvalidProposalId();
+ if (proposalID >= s_treePlantingProposalCounter) {
+ revert InvalidProposalId();
+ }
return s_treePlantingProposalIDtoTreePlantingProposal[proposalID];
}
- function getTreePlantingProposals(uint256 status) external view returns (TreePlantingProposal[] memory) {
- // This function returns all tree planting proposals
-
+ function getTreePlantingProposals(uint256 status, uint256 offset, uint256 limit)
+ external
+ view
+ returns (TreePlantingProposal[] memory proposals, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
uint256 matchCount = 0;
for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) {
if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) {
matchCount++;
}
}
- TreePlantingProposal[] memory proposals = new TreePlantingProposal[](matchCount);
- uint256 currentIndex = 0;
- for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) {
+ totalCount = matchCount;
+ if (offset >= totalCount) {
+ return (new TreePlantingProposal[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ proposals = new TreePlantingProposal[](resultLength);
+ uint256 currentMatch = 0;
+ uint256 resultIndex = 0;
+ for (uint256 i = 0; i < s_treePlantingProposalCounter && resultIndex < resultLength; i++) {
if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) {
- proposals[currentIndex] = s_treePlantingProposalIDtoTreePlantingProposal[i];
- currentIndex++;
+ if (currentMatch >= offset) {
+ proposals[resultIndex] = s_treePlantingProposalIDtoTreePlantingProposal[i];
+ resultIndex++;
+ }
+ currentMatch++;
}
}
-
- return proposals;
+ return (proposals, totalCount);
}
function getTreePlantingProposalsByStatus(uint256 status, uint256 offset, uint256 limit)
@@ -339,7 +379,7 @@ contract Organisation {
// This function returns tree planting proposals by status
if (limit <= 0) revert InvalidInput();
- if (limit > paginationLimit) revert PaginationLimitExceeded();
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
uint256 matchCount = 0;
for (uint256 i = 0; i < s_treePlantingProposalCounter; i++) {
if (s_treePlantingProposalIDtoTreePlantingProposal[i].status == status) {
@@ -376,7 +416,9 @@ contract Organisation {
function voteOnTreePlantingProposal(uint256 proposalID, uint256 vote) external onlyOwner {
// This function allows users to vote on a tree planting proposal
- if (proposalID >= s_treePlantingProposalCounter) revert InvalidProposalId();
+ if (proposalID >= s_treePlantingProposalCounter) {
+ revert InvalidProposalId();
+ }
TreePlantingProposal storage proposal = s_treePlantingProposalIDtoTreePlantingProposal[proposalID];
if (proposal.status != 0) revert AlreadyProcessed();
@@ -422,6 +464,7 @@ contract Organisation {
if (!checkMembership(newOwner)) revert NotOrganisationMember();
if (checkOwnership(newOwner)) revert AlreadyOwner();
owners.push(newOwner);
+ organisationFactoryContract.promoteToOwner(newOwner);
}
function checkOwnership(address user) public view returns (bool) {
@@ -446,16 +489,49 @@ contract Organisation {
return false;
}
- function getMembers() external view returns (address[] memory) {
- // This function returns the list of members in the organisation
-
- return members;
+ function getMembers(uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory memberList, uint256 totalCount)
+ {
+ totalCount = members.length;
+ if (offset >= totalCount) {
+ return (new address[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ memberList = new address[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ memberList[i] = members[offset + i];
+ }
+ return (memberList, totalCount);
}
- function getOwners() external view returns (address[] memory) {
- // This function returns the list of owners in the organisation
+ function getOwners(uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory ownerList, uint256 totalCount)
+ {
+ // This function returns the list of owners of the organisation
- return owners;
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
+ totalCount = owners.length;
+ if (offset >= totalCount) {
+ return (new address[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ ownerList = new address[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ ownerList[i] = owners[offset + i];
+ }
+ return (ownerList, totalCount);
}
function getMemberCount() external view returns (uint256) {
diff --git a/src/OrganisationFactory.sol b/src/OrganisationFactory.sol
index e002b6b..4d1e9bf 100644
--- a/src/OrganisationFactory.sol
+++ b/src/OrganisationFactory.sol
@@ -11,9 +11,12 @@ contract OrganisationFactory is Ownable {
mapping(address => Organisation) public s_organisationAddressToOrganisation;
mapping(address => address[]) public s_userToOrganisations;
+ mapping(address => address[]) private s_userToOrganisationsAsOwner;
+ mapping(address => address[]) private s_userToOrganisationsAsMember;
mapping(address => bool) private s_isOrganisation;
address[] private s_allOrganisations;
+ uint256 public paginationLimit = 50;
constructor(address _treeNFTContract) Ownable(msg.sender) {
treeNFTContract = _treeNFTContract;
@@ -33,28 +36,65 @@ contract OrganisationFactory is Ownable {
);
organisationAddress = address(newOrganisation);
s_userToOrganisations[msg.sender].push(organisationAddress);
+ s_userToOrganisationsAsOwner[msg.sender].push(organisationAddress);
s_isOrganisation[organisationAddress] = true;
s_allOrganisations.push(organisationAddress);
s_organisationAddressToOrganisation[organisationAddress] = newOrganisation;
return (organisationId, organisationAddress);
}
- function getUserOrganisations(address _user) external view returns (address[] memory) {
- // This function retrieves the list of organization IDs associated with a user.
-
- return s_userToOrganisations[_user];
+ function getUserOrganisations(address _user, uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory orgs, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert PaginationLimitExceeded();
+ address[] memory allUserOrgs = s_userToOrganisations[_user];
+ totalCount = allUserOrgs.length;
+ if (offset >= totalCount) {
+ return (new address[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ orgs = new address[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ orgs[i] = allUserOrgs[offset + i];
+ }
+ return (orgs, totalCount);
}
- function getMyOrganisations() external view returns (address[] memory) {
- // This function retrieves the list of organization IDs associated with the caller.
-
- return s_userToOrganisations[msg.sender];
+ function getMyOrganisations(uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory orgs, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert PaginationLimitExceeded();
+ return this.getUserOrganisations(msg.sender, offset, limit);
}
- function getAllOrganisations() external view returns (address[] memory) {
- // This function retrieves the list of all organization addresses.
-
- return s_allOrganisations;
+ function getAllOrganisations(uint256 offset, uint256 limit)
+ external
+ view
+ returns (address[] memory orgs, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert PaginationLimitExceeded();
+ totalCount = s_allOrganisations.length;
+ if (offset >= totalCount) {
+ return (new address[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ orgs = new address[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ orgs[i] = s_allOrganisations[offset + i];
+ }
+ return (orgs, totalCount);
}
function getOrganisationCount() external view returns (uint256) {
@@ -64,10 +104,83 @@ contract OrganisationFactory is Ownable {
}
function addMemberToOrganisation(address _member) external {
- // This function adds a member to an organization.
+ // This function adds a member to an organization (called by Organisation contract)
if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation();
if (msg.sender == address(0)) revert OrganisationDoesNotExist();
s_userToOrganisations[_member].push(msg.sender);
+
+ Organisation org = Organisation(msg.sender);
+ if (org.checkOwnership(_member)) {
+ s_userToOrganisationsAsOwner[_member].push(msg.sender);
+ } else {
+ s_userToOrganisationsAsMember[_member].push(msg.sender);
+ }
+ }
+
+ function promoteToOwner(address _member) external {
+ // This function updates the role mapping when a member becomes an owner
+ if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation();
+
+ // Remove from member-only array
+ address[] storage memberOrgs = s_userToOrganisationsAsMember[_member];
+ for (uint256 i; i < memberOrgs.length;) {
+ if (memberOrgs[i] == msg.sender) {
+ memberOrgs[i] = memberOrgs[memberOrgs.length - 1];
+ memberOrgs.pop();
+ break;
+ }
+ unchecked {
+ ++i;
+ }
+ }
+
+ // Add to owner array
+ s_userToOrganisationsAsOwner[_member].push(msg.sender);
+ }
+
+ function removeMemberFromOrganisation(address _member, bool _wasOwner) external {
+ // This function removes a member from an organization (called by Organisation contract)
+ if (!s_isOrganisation[msg.sender]) revert InvalidOrganisation();
+
+ // Remove from s_userToOrganisations
+ address[] storage userOrgs = s_userToOrganisations[_member];
+ for (uint256 i; i < userOrgs.length;) {
+ if (userOrgs[i] == msg.sender) {
+ userOrgs[i] = userOrgs[userOrgs.length - 1];
+ userOrgs.pop();
+ break;
+ }
+ unchecked {
+ ++i;
+ }
+ }
+
+ // Remove from role-specific mapping
+ if (_wasOwner) {
+ address[] storage ownerOrgs = s_userToOrganisationsAsOwner[_member];
+ for (uint256 i; i < ownerOrgs.length;) {
+ if (ownerOrgs[i] == msg.sender) {
+ ownerOrgs[i] = ownerOrgs[ownerOrgs.length - 1];
+ ownerOrgs.pop();
+ break;
+ }
+ unchecked {
+ ++i;
+ }
+ }
+ } else {
+ address[] storage memberOrgs = s_userToOrganisationsAsMember[_member];
+ for (uint256 i; i < memberOrgs.length;) {
+ if (memberOrgs[i] == msg.sender) {
+ memberOrgs[i] = memberOrgs[memberOrgs.length - 1];
+ memberOrgs.pop();
+ break;
+ }
+ unchecked {
+ ++i;
+ }
+ }
+ }
}
function getOrganisationInfo(address _organisationAddress)
@@ -84,18 +197,31 @@ contract OrganisationFactory is Ownable {
)
{
// This function retrieves detailed information about an organization based on its ID.
- if (!s_isOrganisation[_organisationAddress]) revert OrganisationDoesNotExist();
+ if (!s_isOrganisation[_organisationAddress]) {
+ revert OrganisationDoesNotExist();
+ }
Organisation org = Organisation(_organisationAddress);
return org.getOrganisationInfo();
}
- function getAllOrganisationDetails() external view returns (OrganisationDetails[] memory organizationDetails) {
- // This function retrieves detailed information about all organizations.
-
- uint256 totalOrgs = s_allOrganisations.length;
- organizationDetails = new OrganisationDetails[](totalOrgs);
- for (uint256 i = 0; i < totalOrgs; i++) {
- address organisationAddress = s_allOrganisations[i];
+ function getAllOrganisationDetails(uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationDetails[] memory organizationDetails, uint256 totalCount)
+ {
+ if (limit > paginationLimit) revert PaginationLimitExceeded();
+ totalCount = s_allOrganisations.length;
+ if (offset >= totalCount) {
+ return (new OrganisationDetails[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ organizationDetails = new OrganisationDetails[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ address organisationAddress = s_allOrganisations[offset + i];
Organisation org = Organisation(organisationAddress);
try org.getOrganisationInfo() returns (
address orgAddress,
@@ -134,20 +260,16 @@ contract OrganisationFactory is Ownable {
}
}
- return organizationDetails;
- }
-
- function updateTreeNFTContract(address _newTreeNFTContract) external onlyOwner {
- // This function updates the address of the Tree NFT contract.
-
- if (_newTreeNFTContract == address(0)) revert InvalidInput();
- treeNFTContract = _newTreeNFTContract;
+ return (organizationDetails, totalCount);
}
function removeOrganisation(address _organisationAddress) external onlyOwner {
// This function allows the owner to remove an organization from the factory.
- if (s_isOrganisation[_organisationAddress] == false) revert OrganisationDoesNotExist();
+ if (s_isOrganisation[_organisationAddress] == false) {
+ revert OrganisationDoesNotExist();
+ }
+ s_isOrganisation[_organisationAddress] = false;
for (uint256 i = 0; i < s_allOrganisations.length; i++) {
if (s_allOrganisations[i] == _organisationAddress) {
s_allOrganisations[i] = s_allOrganisations[s_allOrganisations.length - 1];
@@ -161,4 +283,165 @@ contract OrganisationFactory is Ownable {
// This function retrieves the address of the Tree NFT contract.
return treeNFTContract;
}
+
+ function getUserOrganisationsAsOwner(address _user, uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationDetails[] memory orgs, uint256 totalCount)
+ {
+ // This function retrieves paginated organizations where the user is an owner/admin
+
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
+ address[] memory ownerOrgs = s_userToOrganisationsAsOwner[_user];
+ totalCount = ownerOrgs.length;
+
+ if (offset >= totalCount || limit == 0) {
+ return (new OrganisationDetails[](0), totalCount);
+ }
+
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+
+ orgs = new OrganisationDetails[](resultLength);
+
+ for (uint256 i; i < resultLength;) {
+ address orgAddr = ownerOrgs[offset + i];
+ Organisation org = Organisation(orgAddr);
+
+ try org.getOrganisationInfo() returns (
+ address orgAddress,
+ string memory name,
+ string memory description,
+ string memory photoIpfsHash,
+ address[] memory owners,
+ address[] memory members,
+ uint256 timeOfCreation
+ ) {
+ orgs[i] = OrganisationDetails({
+ contractAddress: orgAddress,
+ name: name,
+ description: description,
+ organisationPhoto: photoIpfsHash,
+ owners: owners,
+ members: members,
+ ownerCount: owners.length,
+ memberCount: members.length,
+ isActive: s_isOrganisation[orgAddress],
+ timeOfCreation: timeOfCreation
+ });
+ } catch {
+ orgs[i] = OrganisationDetails({
+ contractAddress: orgAddr,
+ name: "ERROR: Unable to fetch",
+ description: "ERROR: Contract call failed",
+ organisationPhoto: "",
+ owners: new address[](0),
+ members: new address[](0),
+ ownerCount: 0,
+ memberCount: 0,
+ isActive: false,
+ timeOfCreation: 0
+ });
+ }
+ unchecked {
+ ++i;
+ }
+ }
+
+ return (orgs, totalCount);
+ }
+
+ function getUserOrganisationsAsMember(address _user, uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationDetails[] memory orgs, uint256 totalCount)
+ {
+ // This function retrieves paginated organizations where the user is a member (but not an owner)
+
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
+ address[] memory memberOrgs = s_userToOrganisationsAsMember[_user];
+ totalCount = memberOrgs.length;
+
+ if (offset >= totalCount || limit == 0) {
+ return (new OrganisationDetails[](0), totalCount);
+ }
+
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+
+ orgs = new OrganisationDetails[](resultLength);
+
+ for (uint256 i; i < resultLength;) {
+ address orgAddr = memberOrgs[offset + i];
+ Organisation org = Organisation(orgAddr);
+
+ try org.getOrganisationInfo() returns (
+ address orgAddress,
+ string memory name,
+ string memory description,
+ string memory photoIpfsHash,
+ address[] memory owners,
+ address[] memory members,
+ uint256 timeOfCreation
+ ) {
+ orgs[i] = OrganisationDetails({
+ contractAddress: orgAddress,
+ name: name,
+ description: description,
+ organisationPhoto: photoIpfsHash,
+ owners: owners,
+ members: members,
+ ownerCount: owners.length,
+ memberCount: members.length,
+ isActive: s_isOrganisation[orgAddress],
+ timeOfCreation: timeOfCreation
+ });
+ } catch {
+ orgs[i] = OrganisationDetails({
+ contractAddress: orgAddr,
+ name: "ERROR: Unable to fetch",
+ description: "ERROR: Contract call failed",
+ organisationPhoto: "",
+ owners: new address[](0),
+ members: new address[](0),
+ ownerCount: 0,
+ memberCount: 0,
+ isActive: false,
+ timeOfCreation: 0
+ });
+ }
+ unchecked {
+ ++i;
+ }
+ }
+
+ return (orgs, totalCount);
+ }
+
+ function getMyOrganisationsAsOwner(uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationDetails[] memory orgs, uint256 totalCount)
+ {
+ // This function retrieves paginated organizations where the caller is an owner/admin
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
+ return this.getUserOrganisationsAsOwner(msg.sender, offset, limit);
+ }
+
+ function getMyOrganisationsAsMember(uint256 offset, uint256 limit)
+ external
+ view
+ returns (OrganisationDetails[] memory orgs, uint256 totalCount)
+ {
+ // This function retrieves paginated organizations where the caller is a member (but not an owner)
+
+ if (limit > paginationLimit) revert MaximumLimitRequestExceeded();
+ return this.getUserOrganisationsAsMember(msg.sender, offset, limit);
+ }
}
diff --git a/src/TreeNft.sol b/src/TreeNft.sol
index 8708207..4056108 100644
--- a/src/TreeNft.sol
+++ b/src/TreeNft.sol
@@ -22,6 +22,7 @@ contract TreeNft is ERC721, Ownable {
uint256 private s_deathCounter;
uint256 private s_treeNftVerificationCounter;
uint256 private s_userCounter;
+ uint256 public constant maxLimitForPagination = 50;
uint256 public minimumTimeToMarkTreeDead = 365 days;
CareToken public careTokenContract;
@@ -127,14 +128,23 @@ contract TreeNft is ERC721, Ownable {
return string(abi.encodePacked("data:application/json;base64,", Base64.encode(bytes(json))));
}
- function getAllNFTs() public view returns (Tree[] memory) {
- // This function retrieves all NFTs in the contract
+ function getAllNFTs(uint256 offset, uint256 limit) public view returns (Tree[] memory trees, uint256 totalCount) {
+ totalCount = s_treeTokenCounter;
+ if (limit > maxLimitForPagination) revert MaximumLimitRequestExceeded();
- Tree[] memory allTrees = new Tree[](s_treeTokenCounter);
- for (uint256 i = 0; i < allTrees.length; i++) {
- allTrees[i] = s_tokenIDtoTree[i];
+ if (offset >= totalCount) {
+ return (new Tree[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
}
- return allTrees;
+ uint256 resultLength = end - offset;
+ trees = new Tree[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ trees[i] = s_tokenIDtoTree[offset + i];
+ }
+ return (trees, totalCount);
}
function getRecentTreesPaginated(uint256 offset, uint256 limit)
@@ -158,18 +168,6 @@ contract TreeNft is ERC721, Ownable {
return (result, totalTrees, hasMoreTrees);
}
- function getNFTsByUser(address user) public view returns (Tree[] memory) {
- // This function retrieves all NFTs owned by a specific user
-
- uint256[] memory userNFTs = s_userToNFTs[user];
- Tree[] memory userTrees = new Tree[](userNFTs.length);
- for (uint256 i = 0; i < userNFTs.length; i++) {
- uint256 tokenId = userNFTs[i];
- userTrees[i] = s_tokenIDtoTree[tokenId];
- }
- return userTrees;
- }
-
function getNFTsByUserPaginated(address user, uint256 offset, uint256 limit)
public
view
@@ -226,15 +224,18 @@ contract TreeNft is ERC721, Ownable {
TreeNftVerification memory treeVerification = TreeNftVerification(
msg.sender, block.timestamp, _proofHashes, _description, false, _tokenId, planterTokenContract
);
- s_tokenIDtoUserVerification[_tokenId][msg.sender] = true;
- s_tokenIDtoVerifiers[_tokenId].push(msg.sender);
- s_verifierToTreeTokenIDs[msg.sender].push(_tokenId);
- s_tokenIDtoTreeNftVerfication[s_treeNftVerificationCounter] = treeVerification;
+ s_tokenIDtoUserVerification[_tokenId][msg.sender] = true; // mark as verified by the verifier for the tree
+ s_tokenIDtoVerifiers[_tokenId].push(msg.sender); // tree to verifiers
+ s_verifierToTreeTokenIDs[msg.sender].push(_tokenId); // verifier to verified trees
+
+ s_tokenIDtoTreeNftVerfication[s_treeNftVerificationCounter] = treeVerification; // store the verification
s_treeTokenIdToVerifications[_tokenId].push(s_treeNftVerificationCounter);
s_treeNftVerificationCounter++;
+
planterToken.mint(ownerOf(_tokenId), tree.numberOfTrees * 1e18);
s_userToVerifierTokenAddresses[ownerOf(_tokenId)].push(planterTokenContract);
- s_userToVerifications[msg.sender].push(treeVerification);
+
+ s_userToVerifications[ownerOf(_tokenId)].push(treeVerification);
}
}
@@ -245,10 +246,24 @@ contract TreeNft is ERC721, Ownable {
if (!s_tokenIDtoUserVerification[_tokenId][verifier]) {
revert VerificationNotFound();
}
+
+ // Check if verification exists and is not already hidden
+ bool verificationFound = false;
+ uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId];
+ for (uint256 i = 0; i < verificationIds.length; i++) {
+ TreeNftVerification storage treeNftVerification = s_tokenIDtoTreeNftVerfication[verificationIds[i]];
+ if (treeNftVerification.verifier == verifier && !treeNftVerification.isHidden) {
+ verificationFound = true;
+ break;
+ }
+ }
+ if (!verificationFound) {
+ revert VerificationNotFound();
+ }
+
Tree memory tree = s_tokenIDtoTree[_tokenId];
address treeOwner = ownerOf(_tokenId);
- s_tokenIDtoUserVerification[_tokenId][verifier] = false;
address[] storage verifiers = s_tokenIDtoVerifiers[_tokenId];
for (uint256 i = 0; i < verifiers.length; i++) {
if (verifiers[i] == verifier) {
@@ -265,12 +280,24 @@ contract TreeNft is ERC721, Ownable {
break;
}
}
- uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId];
+
for (uint256 i = 0; i < verificationIds.length; i++) {
TreeNftVerification storage treeNftVerification = s_tokenIDtoTreeNftVerfication[verificationIds[i]];
if (treeNftVerification.verifier == verifier && !treeNftVerification.isHidden) {
treeNftVerification.isHidden = true;
+ // Also update the verification in the user's array
+ TreeNftVerification[] storage userVerifications = s_userToVerifications[treeOwner];
+ for (uint256 j = 0; j < userVerifications.length; j++) {
+ if (
+ userVerifications[j].treeNftId == _tokenId && userVerifications[j].verifier == verifier
+ && !userVerifications[j].isHidden
+ ) {
+ userVerifications[j].isHidden = true;
+ break;
+ }
+ }
+
User storage user = s_addressToUser[verifier];
user.verificationsRevoked++;
address planterTokenAddr = s_userToPlanterTokenAddress[verifier];
@@ -296,16 +323,78 @@ contract TreeNft is ERC721, Ownable {
}
}
- function getVerifiedTreesByUser(address verifier) public view returns (Tree[] memory) {
- // This function retrieves all trees verified by a specific verifier
+ function removeVerificationOptimized(
+ uint256 _verificationId,
+ uint256 _verifierArrayIndex,
+ uint256 _verifiedTreesArrayIndex,
+ uint256 _userVerificationIndex,
+ uint256 _verifierTokenAddrIndex
+ ) public {
+ TreeNftVerification storage verification = s_tokenIDtoTreeNftVerfication[_verificationId];
+
+ if (verification.verifier == address(0)) revert VerificationNotFound();
+ if (verification.isHidden) revert VerificationNotFound();
- uint256[] memory verifiedTokens = s_verifierToTreeTokenIDs[verifier];
- Tree[] memory verifiedTrees = new Tree[](verifiedTokens.length);
- for (uint256 i = 0; i < verifiedTokens.length; i++) {
- uint256 tokenId = verifiedTokens[i];
- verifiedTrees[i] = s_tokenIDtoTree[tokenId];
+ uint256 tokenId = verification.treeNftId;
+ address verifier = verification.verifier;
+ address treeOwner = ownerOf(tokenId);
+
+ if (msg.sender != treeOwner) revert NotTreeOwner();
+
+ address[] storage verifiers = s_tokenIDtoVerifiers[tokenId];
+ if (_verifierArrayIndex >= verifiers.length || verifiers[_verifierArrayIndex] != verifier) {
+ revert VerificationNotFound();
}
- return verifiedTrees;
+
+ uint256[] storage verifiedTrees = s_verifierToTreeTokenIDs[verifier];
+ if (_verifiedTreesArrayIndex >= verifiedTrees.length || verifiedTrees[_verifiedTreesArrayIndex] != tokenId) {
+ revert VerificationNotFound();
+ }
+
+ TreeNftVerification[] storage userVerifications = s_userToVerifications[treeOwner];
+ if (
+ _userVerificationIndex >= userVerifications.length
+ || userVerifications[_userVerificationIndex].treeNftId != tokenId
+ || userVerifications[_userVerificationIndex].verifier != verifier
+ || userVerifications[_userVerificationIndex].isHidden
+ ) {
+ revert VerificationNotFound();
+ }
+
+ verifiers[_verifierArrayIndex] = verifiers[verifiers.length - 1];
+ verifiers.pop();
+
+ verifiedTrees[_verifiedTreesArrayIndex] = verifiedTrees[verifiedTrees.length - 1];
+ verifiedTrees.pop();
+
+ verification.isHidden = true;
+ userVerifications[_userVerificationIndex].isHidden = true;
+
+ User storage user = s_addressToUser[verifier];
+ user.verificationsRevoked++;
+
+ address planterTokenAddr = verification.verifierPlanterTokenAddress;
+ if (planterTokenAddr != address(0)) {
+ PlanterToken planterToken = PlanterToken(planterTokenAddr);
+ Tree memory tree = s_tokenIDtoTree[tokenId];
+ uint256 tokensToReturn = tree.numberOfTrees * 1e18;
+
+ if (planterToken.balanceOf(treeOwner) >= tokensToReturn) {
+ planterToken.burnFrom(treeOwner, tokensToReturn);
+
+ address[] storage verifierTokenAddrs = s_userToVerifierTokenAddresses[treeOwner];
+ if (
+ _verifierTokenAddrIndex >= verifierTokenAddrs.length
+ || verifierTokenAddrs[_verifierTokenAddrIndex] != planterTokenAddr
+ ) {
+ revert VerificationNotFound();
+ }
+ verifierTokenAddrs[_verifierTokenAddrIndex] = verifierTokenAddrs[verifierTokenAddrs.length - 1];
+ verifierTokenAddrs.pop();
+ }
+ }
+
+ emit VerificationRemoved(_verificationId, tokenId, verifier);
}
function getVerifiedTreesByUserPaginated(address verifier, uint256 offset, uint256 limit)
@@ -315,6 +404,7 @@ contract TreeNft is ERC721, Ownable {
{
// Get the total number of trees verified by this verifier
+ if (limit > maxLimitForPagination) revert MaximumLimitRequestExceeded();
uint256[] memory verifiedTokens = s_verifierToTreeTokenIDs[verifier];
totalCount = verifiedTokens.length;
if (offset >= totalCount) {
@@ -334,28 +424,6 @@ contract TreeNft is ERC721, Ownable {
return (trees, totalCount);
}
- function getTreeNftVerifiers(uint256 _tokenId) public view returns (TreeNftVerification[] memory) {
- // This function retrieves all verifiers for a specific tree
-
- uint256[] storage verificationIds = s_treeTokenIdToVerifications[_tokenId];
- uint256 visibleCount;
- for (uint256 i = 0; i < verificationIds.length; i++) {
- if (!s_tokenIDtoTreeNftVerfication[verificationIds[i]].isHidden) {
- visibleCount++;
- }
- }
- TreeNftVerification[] memory treeNftVerifications = new TreeNftVerification[](visibleCount);
- uint256 currentIndex;
- for (uint256 i = 0; i < verificationIds.length; i++) {
- TreeNftVerification memory verification = s_tokenIDtoTreeNftVerfication[verificationIds[i]];
- if (!verification.isHidden) {
- treeNftVerifications[currentIndex] = verification;
- currentIndex++;
- }
- }
- return treeNftVerifications;
- }
-
function getTreeNftVerifiersPaginated(uint256 _tokenId, uint256 offset, uint256 limit)
public
view
@@ -442,26 +510,38 @@ contract TreeNft is ERC721, Ownable {
return userDetails;
}
- function getUserVerifierTokenDetails(address userAddress)
+ function getUserVerifierTokenDetails(address userAddress, uint256 offset, uint256 limit)
public
view
- returns (VerificationDetails[] memory verifierTokenDetails)
+ returns (VerificationDetails[] memory verifierTokenDetails, uint256 totalCount)
{
- // This function returns the verifier token address of the user
+ // This function returns the verifier token details of the user with pagination
TreeNftVerification[] memory userVerifications = s_userToVerifications[userAddress];
- for (uint256 i = 0; i < userVerifications.length; i++) {
- PlanterToken planterToken = PlanterToken(userVerifications[i].verifierPlanterTokenAddress);
+ totalCount = userVerifications.length;
+ if (offset >= totalCount) {
+ return (new VerificationDetails[](0), totalCount);
+ }
+ uint256 end = offset + limit;
+ if (end > totalCount) {
+ end = totalCount;
+ }
+ uint256 resultLength = end - offset;
+ verifierTokenDetails = new VerificationDetails[](resultLength);
+ for (uint256 i = 0; i < resultLength; i++) {
+ uint256 verificationIndex = offset + i;
+ PlanterToken planterToken = PlanterToken(userVerifications[verificationIndex].verifierPlanterTokenAddress);
verifierTokenDetails[i] = VerificationDetails({
- verifier: userVerifications[i].verifier,
- timestamp: userVerifications[i].timestamp,
- proofHashes: userVerifications[i].proofHashes,
- description: userVerifications[i].description,
- isHidden: userVerifications[i].isHidden,
+ verifier: userVerifications[verificationIndex].verifier,
+ timestamp: userVerifications[verificationIndex].timestamp,
+ proofHashes: userVerifications[verificationIndex].proofHashes,
+ description: userVerifications[verificationIndex].description,
+ isHidden: userVerifications[verificationIndex].isHidden,
numberOfTrees: planterToken.balanceOf(userAddress),
- verifierPlanterTokenAddress: userVerifications[i].verifierPlanterTokenAddress
+ verifierPlanterTokenAddress: userVerifications[verificationIndex].verifierPlanterTokenAddress
});
}
+ return (verifierTokenDetails, totalCount);
}
function updateUserDetails(string memory _name, string memory _profilePhoto) public {
diff --git a/src/utils/errors.sol b/src/utils/errors.sol
index d392f5e..63e1095 100644
--- a/src/utils/errors.sol
+++ b/src/utils/errors.sol
@@ -45,6 +45,7 @@ error MinimumMarkDeadTimeNotReached();
error InvalidCoordinates();
error CannotVerifyOwnTree();
error VerificationNotFound();
+error MaximumLimitRequestExceeded();
/// User
error UserAlreadyRegistered();
diff --git a/test/OrganisationFactory.t.sol b/test/OrganisationFactory.t.sol
index d247a42..68c5b6a 100644
--- a/test/OrganisationFactory.t.sol
+++ b/test/OrganisationFactory.t.sol
@@ -101,14 +101,16 @@ contract OrganisationFactoryTest is Test {
vm.stopPrank();
vm.startPrank(user1);
- address[] memory user1Orgs = factory.getMyOrganisations();
+ (address[] memory user1Orgs, uint256 count1) = factory.getMyOrganisations(0, 10);
vm.stopPrank();
vm.startPrank(user2);
- address[] memory user2Orgs = factory.getMyOrganisations();
+ (address[] memory user2Orgs, uint256 count2) = factory.getMyOrganisations(0, 10);
vm.stopPrank();
assertEq(user1Orgs.length, 1);
+ assertEq(count1, 1);
assertEq(user2Orgs.length, 1);
+ assertEq(count2, 1);
}
function test_getAllOrganisations() public {
@@ -121,8 +123,9 @@ contract OrganisationFactoryTest is Test {
factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2);
vm.stopPrank();
- OrganisationDetails[] memory orgs = factory.getAllOrganisationDetails();
+ (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getAllOrganisationDetails(0, 10);
assertEq(orgs.length, 2);
+ assertEq(totalCount, 2);
assertEq(orgs[0].name, NAME1);
assertEq(orgs[0].description, DESCRIPTION1);
assertEq(orgs[0].organisationPhoto, PHOTO_HASH1);
@@ -145,7 +148,262 @@ contract OrganisationFactoryTest is Test {
factory.createOrganisation(NAME2, DESCRIPTION2, PHOTO_HASH2);
vm.stopPrank();
- address[] memory orgAddresses = factory.getAllOrganisations();
+ (address[] memory orgAddresses, uint256 totalCount) = factory.getAllOrganisations(0, 10);
assertEq(orgAddresses.length, 2);
+ assertEq(totalCount, 2);
+ }
+
+ function test_getUserOrganisationsAsOwner() public {
+ // This test checks if getUserOrganisationsAsOwner returns correct organizations where user is owner
+
+ // User1 creates 2 organizations (will be owner of both)
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ vm.prank(user1);
+ (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2");
+ vm.stopPrank();
+
+ // User2 creates 1 organization
+ vm.prank(user2);
+ factory.createOrganisation("Org3", "Description3", "Photo3");
+ vm.stopPrank();
+
+ // Get user1's organizations as owner
+ (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getUserOrganisationsAsOwner(user1, 0, 10);
+
+ assertEq(totalCount, 2);
+ assertEq(orgs.length, 2);
+ assertEq(orgs[0].contractAddress, org1);
+ assertEq(orgs[1].contractAddress, org2);
+ assertEq(orgs[0].ownerCount, 1);
+ assertEq(orgs[1].ownerCount, 1);
+ }
+
+ function test_getUserOrganisationsAsOwnerPagination() public {
+ // This test checks pagination works correctly for owner organizations
+
+ // User1 creates 5 organizations
+ address[] memory orgAddresses = new address[](5);
+ for (uint256 i = 0; i < 5; i++) {
+ vm.prank(user1);
+ (, address orgAddr) = factory.createOrganisation(
+ string(abi.encodePacked("Org", vm.toString(i))),
+ string(abi.encodePacked("Desc", vm.toString(i))),
+ string(abi.encodePacked("Photo", vm.toString(i)))
+ );
+ orgAddresses[i] = orgAddr;
+ vm.stopPrank();
+ }
+
+ // Test pagination: offset 0, limit 2
+ (OrganisationDetails[] memory orgs1, uint256 totalCount1) = factory.getUserOrganisationsAsOwner(user1, 0, 2);
+ assertEq(totalCount1, 5);
+ assertEq(orgs1.length, 2);
+ assertEq(orgs1[0].contractAddress, orgAddresses[0]);
+ assertEq(orgs1[1].contractAddress, orgAddresses[1]);
+
+ // Test pagination: offset 2, limit 2
+ (OrganisationDetails[] memory orgs2, uint256 totalCount2) = factory.getUserOrganisationsAsOwner(user1, 2, 2);
+ assertEq(totalCount2, 5);
+ assertEq(orgs2.length, 2);
+ assertEq(orgs2[0].contractAddress, orgAddresses[2]);
+ assertEq(orgs2[1].contractAddress, orgAddresses[3]);
+
+ // Test pagination: offset 4, limit 2 (should return only 1)
+ (OrganisationDetails[] memory orgs3, uint256 totalCount3) = factory.getUserOrganisationsAsOwner(user1, 4, 2);
+ assertEq(totalCount3, 5);
+ assertEq(orgs3.length, 1);
+ assertEq(orgs3[0].contractAddress, orgAddresses[4]);
+
+ // Test pagination: offset beyond total (should return empty array)
+ (OrganisationDetails[] memory orgs4, uint256 totalCount4) = factory.getUserOrganisationsAsOwner(user1, 10, 2);
+ assertEq(totalCount4, 5);
+ assertEq(orgs4.length, 0);
+ }
+
+ function test_getUserOrganisationsAsMember() public {
+ // This test checks if getUserOrganisationsAsMember returns correct organizations where user is only a member
+
+ // User1 creates organization
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ // User1 adds user2 as member (not owner)
+ vm.prank(user1);
+ Organisation(org1).addMember(user2);
+ vm.stopPrank();
+
+ // User2 should have 0 owner orgs and 1 member org
+ (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user2, 0, 10);
+ assertEq(ownerCount, 0);
+ assertEq(ownerOrgs.length, 0);
+
+ (OrganisationDetails[] memory memberOrgs, uint256 memberCount) =
+ factory.getUserOrganisationsAsMember(user2, 0, 10);
+ assertEq(memberCount, 1);
+ assertEq(memberOrgs.length, 1);
+ assertEq(memberOrgs[0].contractAddress, org1);
+ }
+
+ function test_getUserOrganisationsAsMemberPagination() public {
+ // This test checks pagination works correctly for member organizations
+
+ // User1 creates 3 organizations and adds user2 as member to all
+ address[] memory orgAddresses = new address[](3);
+ for (uint256 i = 0; i < 3; i++) {
+ vm.prank(user1);
+ (, address orgAddr) = factory.createOrganisation(
+ string(abi.encodePacked("Org", vm.toString(i))),
+ string(abi.encodePacked("Desc", vm.toString(i))),
+ string(abi.encodePacked("Photo", vm.toString(i)))
+ );
+ orgAddresses[i] = orgAddr;
+ vm.stopPrank();
+
+ vm.prank(user1);
+ Organisation(orgAddr).addMember(user2);
+ vm.stopPrank();
+ }
+
+ // Test pagination: offset 0, limit 2
+ (OrganisationDetails[] memory orgs1, uint256 totalCount1) = factory.getUserOrganisationsAsMember(user2, 0, 2);
+ assertEq(totalCount1, 3);
+ assertEq(orgs1.length, 2);
+
+ // Test pagination: offset 2, limit 2 (should return only 1)
+ (OrganisationDetails[] memory orgs2, uint256 totalCount2) = factory.getUserOrganisationsAsMember(user2, 2, 2);
+ assertEq(totalCount2, 3);
+ assertEq(orgs2.length, 1);
+ }
+
+ function test_promoteToOwner() public {
+ // This test checks if promoting a member to owner updates the role mappings correctly
+
+ // User1 creates organization
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ // User1 adds user2 as member
+ vm.prank(user1);
+ Organisation(org1).addMember(user2);
+ vm.stopPrank();
+
+ // Initially, user2 should be only a member
+ (, uint256 ownerCount1) = factory.getUserOrganisationsAsOwner(user2, 0, 10);
+ (, uint256 memberCount1) = factory.getUserOrganisationsAsMember(user2, 0, 10);
+ assertEq(ownerCount1, 0);
+ assertEq(memberCount1, 1);
+
+ // Promote user2 to owner
+ vm.prank(user1);
+ Organisation(org1).makeOwner(user2);
+ vm.stopPrank();
+
+ // After promotion, user2 should be owner and not in member-only list
+ (OrganisationDetails[] memory ownerOrgs2, uint256 ownerCount2) =
+ factory.getUserOrganisationsAsOwner(user2, 0, 10);
+ (, uint256 memberCount2) = factory.getUserOrganisationsAsMember(user2, 0, 10);
+ assertEq(ownerCount2, 1);
+ assertEq(memberCount2, 0);
+ assertEq(ownerOrgs2[0].contractAddress, org1);
+ }
+
+ function test_getMyOrganisationsAsOwner() public {
+ // This test checks if getMyOrganisationsAsOwner returns correct organizations for the caller
+
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ vm.prank(user1);
+ (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2");
+ vm.stopPrank();
+
+ vm.prank(user1);
+ (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getMyOrganisationsAsOwner(0, 10);
+ vm.stopPrank();
+
+ assertEq(totalCount, 2);
+ assertEq(orgs.length, 2);
+ assertEq(orgs[0].contractAddress, org1);
+ assertEq(orgs[1].contractAddress, org2);
+ }
+
+ function test_getMyOrganisationsAsMember() public {
+ // This test checks if getMyOrganisationsAsMember returns correct organizations for the caller
+
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ vm.prank(user1);
+ Organisation(org1).addMember(user2);
+ vm.stopPrank();
+
+ vm.prank(user2);
+ (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getMyOrganisationsAsMember(0, 10);
+ vm.stopPrank();
+
+ assertEq(totalCount, 1);
+ assertEq(orgs.length, 1);
+ assertEq(orgs[0].contractAddress, org1);
+ }
+
+ function test_mixedRolesAcrossOrganizations() public {
+ // This test checks if a user can be owner in some orgs and member in others
+
+ // User1 creates org1, user2 creates org2
+ vm.prank(user1);
+ (, address org1) = factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ vm.prank(user2);
+ (, address org2) = factory.createOrganisation("Org2", "Description2", "Photo2");
+ vm.stopPrank();
+
+ // User1 adds user2 as member to org1
+ vm.prank(user1);
+ Organisation(org1).addMember(user2);
+ vm.stopPrank();
+
+ // User2 should be owner of 1 org (org2) and member of 1 org (org1)
+ (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user2, 0, 10);
+ (OrganisationDetails[] memory memberOrgs, uint256 memberCount) =
+ factory.getUserOrganisationsAsMember(user2, 0, 10);
+
+ assertEq(ownerCount, 1);
+ assertEq(memberCount, 1);
+ assertEq(ownerOrgs[0].contractAddress, org2);
+ assertEq(memberOrgs[0].contractAddress, org1);
+ }
+
+ function test_emptyResultsForUserWithNoOrganizations() public view {
+ // This test checks if empty arrays are returned for users with no organizations
+
+ (OrganisationDetails[] memory ownerOrgs, uint256 ownerCount) = factory.getUserOrganisationsAsOwner(user4, 0, 10);
+ (OrganisationDetails[] memory memberOrgs, uint256 memberCount) =
+ factory.getUserOrganisationsAsMember(user4, 0, 10);
+
+ assertEq(ownerCount, 0);
+ assertEq(ownerOrgs.length, 0);
+ assertEq(memberCount, 0);
+ assertEq(memberOrgs.length, 0);
+ }
+
+ function test_zeroLimitReturnsEmptyArray() public {
+ // This test checks if limit of 0 returns empty array
+
+ vm.prank(user1);
+ factory.createOrganisation("Org1", "Description1", "Photo1");
+ vm.stopPrank();
+
+ (OrganisationDetails[] memory orgs, uint256 totalCount) = factory.getUserOrganisationsAsOwner(user1, 0, 0);
+
+ assertEq(totalCount, 1);
+ assertEq(orgs.length, 0);
}
}
diff --git a/test/TreeNft.t.sol b/test/TreeNft.t.sol
index 9783616..e0e89b1 100644
--- a/test/TreeNft.t.sol
+++ b/test/TreeNft.t.sol
@@ -131,10 +131,10 @@ contract TreeNftVerificationTest is Test {
vm.prank(planter);
treeNft.removeVerification(0, verifier1);
- assertFalse(treeNft.isVerified(0, verifier1));
- TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0);
+ assertTrue(treeNft.isVerified(0, verifier1));
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 50);
assertEq(verifications.length, 0);
- Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(verifiedTrees.length, 0);
assertEq(planterToken.balanceOf(planter), 0);
}
@@ -159,14 +159,14 @@ contract TreeNftVerificationTest is Test {
treeNft.verify(0, proofs2, "verified by v2");
assertTrue(treeNft.isVerified(0, verifier1));
assertTrue(treeNft.isVerified(0, verifier2));
- TreeNftVerification[] memory verificationsBeforeRemoval = treeNft.getTreeNftVerifiers(0);
+ (TreeNftVerification[] memory verificationsBeforeRemoval,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(verificationsBeforeRemoval.length, 2);
vm.prank(planter);
treeNft.removeVerification(0, verifier1);
- assertFalse(treeNft.isVerified(0, verifier1));
+ assertTrue(treeNft.isVerified(0, verifier1));
assertTrue(treeNft.isVerified(0, verifier2));
- TreeNftVerification[] memory verificationsAfterRemoval = treeNft.getTreeNftVerifiers(0);
+ (TreeNftVerification[] memory verificationsAfterRemoval,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(verificationsAfterRemoval.length, 1);
assertEq(verificationsAfterRemoval[0].verifier, verifier2);
UserDetails memory verifier1Details = treeNft.getUserProfile(verifier1);
@@ -212,11 +212,11 @@ contract TreeNftVerificationTest is Test {
treeNft.verify(1, proofs, "verified tree 1");
vm.stopPrank();
- Tree[] memory verifiedTreesBefore = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory verifiedTreesBefore,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(verifiedTreesBefore.length, 2);
vm.prank(planter);
treeNft.removeVerification(0, verifier1);
- Tree[] memory verifiedTreesAfter = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory verifiedTreesAfter,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(verifiedTreesAfter.length, 1);
assertEq(verifiedTreesAfter[0].id, 1);
}
@@ -262,14 +262,14 @@ contract TreeNftVerificationTest is Test {
address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1);
PlanterToken planterToken = PlanterToken(planterTokenAddr);
vm.prank(planter);
- planterToken.transfer(address(0x999), NUM_TREES * 1e18 / 2);
+ planterToken.transfer(address(0x999), (NUM_TREES * 1e18) / 2);
uint256 balanceBeforeRemoval = planterToken.balanceOf(planter);
assertLt(balanceBeforeRemoval, NUM_TREES * 1e18);
vm.prank(planter);
treeNft.removeVerification(0, verifier1);
- assertFalse(treeNft.isVerified(0, verifier1));
- TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0);
+ assertTrue(treeNft.isVerified(0, verifier1));
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(verifications.length, 0);
}
@@ -305,7 +305,7 @@ contract TreeNftVerificationTest is Test {
proofs2[0] = "proof2";
treeNft.verify(0, proofs2, "verified by v2");
- TreeNftVerification[] memory verifications = treeNft.getTreeNftVerifiers(0);
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(verifications.length, 2);
assertEq(verifications[0].verifier, verifier1);
assertEq(verifications[1].verifier, verifier2);
@@ -329,7 +329,7 @@ contract TreeNftVerificationTest is Test {
vm.prank(verifier1);
treeNft.verify(1, proofs, "verified tree 1");
- Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(verifiedTrees.length, 2);
assertEq(verifiedTrees[0].id, 0);
@@ -426,12 +426,12 @@ contract TreeNftVerificationTest is Test {
treeNft.verify(2, proofs, "verified tree 2");
vm.stopPrank();
- Tree[] memory verifiedTrees = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory verifiedTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(verifiedTrees.length, 3);
vm.prank(planter);
treeNft.removeVerification(1, verifier1);
- Tree[] memory remainingTrees = treeNft.getVerifiedTreesByUser(verifier1);
+ (Tree[] memory remainingTrees,) = treeNft.getVerifiedTreesByUserPaginated(verifier1, 0, 50);
assertEq(remainingTrees.length, 2);
bool hasTree0 = false;
@@ -442,7 +442,7 @@ contract TreeNftVerificationTest is Test {
}
assertTrue(hasTree0, "Tree 0 should still be verified");
assertTrue(hasTree2, "Tree 2 should still be verified");
- assertFalse(treeNft.isVerified(1, verifier1));
+ assertTrue(treeNft.isVerified(1, verifier1));
}
function test_removeVerificationPreservesOtherVerifiers() public {
@@ -471,17 +471,412 @@ contract TreeNftVerificationTest is Test {
assertTrue(treeNft.isVerified(0, verifier2));
assertTrue(treeNft.isVerified(0, thirdVerifier));
- TreeNftVerification[] memory allVerifications = treeNft.getTreeNftVerifiers(0);
+ (TreeNftVerification[] memory allVerifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(allVerifications.length, 3);
vm.prank(planter);
treeNft.removeVerification(0, verifier2);
assertTrue(treeNft.isVerified(0, verifier1));
- assertFalse(treeNft.isVerified(0, verifier2));
+ assertTrue(treeNft.isVerified(0, verifier2));
assertTrue(treeNft.isVerified(0, thirdVerifier));
- TreeNftVerification[] memory remainingVerifications = treeNft.getTreeNftVerifiers(0);
+ (TreeNftVerification[] memory remainingVerifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
assertEq(remainingVerifications.length, 2);
}
+
+ function test_getUserVerifierTokenDetailsPaginated() public {
+ // Register user and mint trees
+
+ vm.prank(verifier1);
+ treeNft.registerUserProfile("Verifier1", "ipfs://profile1");
+
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+
+ vm.prank(planter);
+ treeNft.mintNft(LATITUDE, LONGITUDE, "Tree1", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(planter);
+ treeNft.mintNft(
+ LATITUDE + 1000, LONGITUDE + 1000, "Tree2", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES
+ );
+
+ vm.prank(planter);
+ treeNft.mintNft(
+ LATITUDE + 2000, LONGITUDE + 2000, "Tree3", IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES
+ );
+
+ // Verifier1 verifies all trees
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified tree 0");
+
+ vm.prank(verifier1);
+ treeNft.verify(1, proofs, "verified tree 1");
+
+ vm.prank(verifier1);
+ treeNft.verify(2, proofs, "verified tree 2");
+
+ (VerificationDetails[] memory details1, uint256 totalCount1) =
+ treeNft.getUserVerifierTokenDetails(planter, 0, 2);
+ assertEq(totalCount1, 3, "Total count should be 3");
+ assertEq(details1.length, 2, "Should return 2 items with limit=2");
+ assertEq(details1[0].verifier, verifier1);
+ assertEq(details1[1].verifier, verifier1);
+ assertEq(details1[0].numberOfTrees, 3 * NUM_TREES * 1e18);
+ assertEq(details1[1].numberOfTrees, 3 * NUM_TREES * 1e18);
+
+ (VerificationDetails[] memory details2, uint256 totalCount2) =
+ treeNft.getUserVerifierTokenDetails(planter, 2, 2);
+ assertEq(details2.length, 1);
+ assertEq(totalCount2, 3);
+ assertEq(details2[0].verifier, verifier1);
+ assertEq(details2[0].numberOfTrees, 3 * NUM_TREES * 1e18);
+
+ (VerificationDetails[] memory details3, uint256 totalCount3) =
+ treeNft.getUserVerifierTokenDetails(planter, 5, 2);
+ assertEq(details3.length, 0);
+ assertEq(totalCount3, 3);
+ }
+
+ function test_getUserVerifierTokenDetailsEmptyUser() public view {
+ // Test with user who has no verifications
+ (VerificationDetails[] memory details, uint256 totalCount) =
+ treeNft.getUserVerifierTokenDetails(verifier1, 0, 10);
+ assertEq(details.length, 0);
+ assertEq(totalCount, 0);
+ }
+
+ function test_getUserVerifierTokenDetailsWithTokenBalances() public {
+ // Register user and mint tree
+ vm.prank(verifier1);
+ treeNft.registerUserProfile("Verifier1", "ipfs://profile1");
+
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ (VerificationDetails[] memory details, uint256 totalCount) = treeNft.getUserVerifierTokenDetails(planter, 0, 10);
+ assertEq(details.length, 1);
+ assertEq(totalCount, 1);
+
+ address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1);
+ PlanterToken planterToken = PlanterToken(planterTokenAddr);
+ assertEq(details[0].numberOfTrees, planterToken.balanceOf(planter));
+ assertEq(details[0].numberOfTrees, NUM_TREES * 1e18);
+ assertEq(details[0].verifierPlanterTokenAddress, planterTokenAddr);
+ assertEq(details[0].description, "verified");
+ assertFalse(details[0].isHidden);
+ }
+
+ function test_getUserVerifierTokenDetailsWithHiddenVerifications() public {
+ // Register users and mint tree
+ vm.prank(verifier1);
+ treeNft.registerUserProfile("Verifier1", "ipfs://profile1");
+
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ treeNft.removeVerification(0, verifier1);
+
+ (VerificationDetails[] memory details, uint256 totalCount) = treeNft.getUserVerifierTokenDetails(planter, 0, 10);
+ assertEq(details.length, 1);
+ assertEq(totalCount, 1);
+ assertTrue(details[0].isHidden); // Should be marked as hidden
+ assertEq(details[0].verifier, verifier1);
+ }
+
+ function test_getUserVerifierTokenDetailsPaginationBoundaries() public {
+ // Register user and mint tree
+
+ vm.prank(verifier1);
+ treeNft.registerUserProfile("Verifier1", "ipfs://profile1");
+
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ // Verifier1 verifies the tree
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ (VerificationDetails[] memory details1, uint256 totalCount1) =
+ treeNft.getUserVerifierTokenDetails(planter, 0, 100);
+ assertEq(details1.length, 1);
+ assertEq(totalCount1, 1);
+
+ (VerificationDetails[] memory details2, uint256 totalCount2) =
+ treeNft.getUserVerifierTokenDetails(planter, 1, 10);
+ assertEq(details2.length, 0);
+ assertEq(totalCount2, 1);
+
+ (VerificationDetails[] memory details3, uint256 totalCount3) =
+ treeNft.getUserVerifierTokenDetails(planter, 0, 0);
+ assertEq(details3.length, 0);
+ assertEq(totalCount3, 1);
+ }
+
+ function test_cannotReverifyAfterRemoval() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+ assertTrue(treeNft.isVerified(0, verifier1));
+
+ vm.prank(planter);
+ treeNft.removeVerification(0, verifier1);
+ assertTrue(treeNft.isVerified(0, verifier1));
+
+ vm.prank(verifier1);
+ vm.expectRevert(AlreadyVerified.selector);
+ treeNft.verify(0, proofs, "try to verify again");
+ }
+
+ function test_removeVerificationOptimized() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1);
+ PlanterToken planterToken = PlanterToken(planterTokenAddr);
+ assertEq(planterToken.balanceOf(planter), NUM_TREES * 1e18);
+
+ uint256 verificationId = 0;
+ uint256 verifierArrayIndex = 0;
+ uint256 verifiedTreesArrayIndex = 0;
+ uint256 userVerificationIndex = 0;
+ uint256 verifierTokenAddrIndex = 0;
+
+ vm.prank(planter);
+ treeNft.removeVerificationOptimized(
+ verificationId, verifierArrayIndex, verifiedTreesArrayIndex, userVerificationIndex, verifierTokenAddrIndex
+ );
+
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
+ assertEq(verifications.length, 0);
+ assertEq(planterToken.balanceOf(planter), 0);
+ }
+
+ function test_removeVerificationOptimizedWithMultipleVerifiers() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs1 = new string[](1);
+ proofs1[0] = "proof1";
+ treeNft.verify(0, proofs1, "verified by v1");
+
+ vm.prank(verifier2);
+ string[] memory proofs2 = new string[](1);
+ proofs2[0] = "proof2";
+ treeNft.verify(0, proofs2, "verified by v2");
+
+ uint256 verificationId = 1;
+ uint256 verifierArrayIndex = 1;
+ uint256 verifiedTreesArrayIndex = 0;
+ uint256 userVerificationIndex = 1;
+ uint256 verifierTokenAddrIndex = 1;
+
+ vm.prank(planter);
+ treeNft.removeVerificationOptimized(
+ verificationId, verifierArrayIndex, verifiedTreesArrayIndex, userVerificationIndex, verifierTokenAddrIndex
+ );
+
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
+ assertEq(verifications.length, 1);
+ assertEq(verifications[0].verifier, verifier1);
+ }
+
+ function test_removeVerificationOptimizedInvalidVerificationId() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ vm.expectRevert(VerificationNotFound.selector);
+ treeNft.removeVerificationOptimized(999, 0, 0, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedInvalidVerifierIndex() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ vm.expectRevert(VerificationNotFound.selector);
+ treeNft.removeVerificationOptimized(0, 999, 0, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedInvalidTreeIndex() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ vm.expectRevert(VerificationNotFound.selector);
+ treeNft.removeVerificationOptimized(0, 0, 999, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedInvalidUserVerificationIndex() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ vm.expectRevert(VerificationNotFound.selector);
+ treeNft.removeVerificationOptimized(0, 0, 0, 999, 0);
+ }
+
+ function test_removeVerificationOptimizedOnlyOwner() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(verifier2);
+ vm.expectRevert(NotTreeOwner.selector);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedAlreadyHidden() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+
+ vm.prank(planter);
+ vm.expectRevert(VerificationNotFound.selector);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedEmitsEvent() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ vm.expectEmit(true, true, true, false);
+ emit VerificationRemoved(0, 0, verifier1);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+ }
+
+ function test_removeVerificationOptimizedWithInsufficientTokens() public {
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ address planterTokenAddr = treeNft.s_userToPlanterTokenAddress(verifier1);
+ PlanterToken planterToken = PlanterToken(planterTokenAddr);
+
+ vm.prank(planter);
+ planterToken.transfer(address(0x999), (NUM_TREES * 1e18) / 2);
+
+ vm.prank(planter);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+
+ (TreeNftVerification[] memory verifications,,) = treeNft.getTreeNftVerifiersPaginated(0, 0, 100);
+ assertEq(verifications.length, 0);
+ }
+
+ function test_removeVerificationOptimizedRevocationCounter() public {
+ vm.prank(verifier1);
+ treeNft.registerUserProfile("Verifier1", "ipfs://profile");
+
+ vm.prank(planter);
+ string[] memory photos = new string[](1);
+ photos[0] = "photo1";
+ treeNft.mintNft(LATITUDE, LONGITUDE, SPECIES, IMAGE_URI, QR_HASH, METADATA, GEOHASH, photos, NUM_TREES);
+
+ vm.prank(verifier1);
+ string[] memory proofs = new string[](1);
+ proofs[0] = "proof1";
+ treeNft.verify(0, proofs, "verified");
+
+ vm.prank(planter);
+ treeNft.removeVerificationOptimized(0, 0, 0, 0, 0);
+
+ UserDetails memory userDetails = treeNft.getUserProfile(verifier1);
+ assertEq(userDetails.verificationsRevoked, 1);
+ }
}