@moltium/world-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -50
- package/foundry.toml +27 -0
- package/package.json +11 -4
- package/remappings.txt +2 -0
- package/script/Deploy.s.sol +76 -0
- package/scripts/deploy.js +83 -0
- package/scripts/postinstall.sh +15 -0
- package/test/AgentRegistry.t.sol +87 -0
- package/test/WorldMembership.t.sol +96 -0
package/README.md
CHANGED
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
# @moltium/world-core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **A2A Protocol Integration** — Discover and communicate with Moltium agents via A2A
|
|
8
|
-
- **Blockchain Validation** — On-chain agent registration and soulbound NFT membership on Monad
|
|
9
|
-
- **Configurable Persistence** — SQLite, PostgreSQL, Redis, MongoDB, or LevelDB backends
|
|
10
|
-
- **Flexible Admission Rules** — Skill-based, tag-based, and custom evaluators
|
|
11
|
-
- **World Simulation Engine** — Tick-based execution with rule enforcement
|
|
12
|
-
- **Entry Fee System** — MON token payment with soulbound NFT access control
|
|
3
|
+
This package requires [Foundry](https://getfoundry.sh/) to be installed for smart contract deployment.
|
|
13
4
|
|
|
14
5
|
## Installation
|
|
15
6
|
|
|
@@ -17,51 +8,34 @@ World runtime for creating agent simulation environments with A2A-based admissio
|
|
|
17
8
|
npm install @moltium/world-core
|
|
18
9
|
```
|
|
19
10
|
|
|
20
|
-
|
|
11
|
+
The package will automatically run `forge install` during installation to fetch OpenZeppelin dependencies. If Foundry is not installed, you'll need to run it manually:
|
|
21
12
|
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
```bash
|
|
14
|
+
cd node_modules/@moltium/world-core
|
|
15
|
+
forge install
|
|
16
|
+
```
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
name: 'Trading Arena',
|
|
28
|
-
description: 'Autonomous trading simulation',
|
|
29
|
-
|
|
30
|
-
server: {
|
|
31
|
-
port: 4000,
|
|
32
|
-
host: 'localhost',
|
|
33
|
-
},
|
|
34
|
-
|
|
35
|
-
admission: {
|
|
36
|
-
maxAgents: 50,
|
|
37
|
-
requiredSkills: ['trading', 'analysis'],
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
simulation: {
|
|
41
|
-
tickIntervalMs: 5000,
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
persistence: {
|
|
45
|
-
type: 'sqlite',
|
|
46
|
-
sqlite: { filename: './world.db' },
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
blockchain: {
|
|
50
|
-
rpcUrl: process.env.MONAD_RPC_URL!,
|
|
51
|
-
privateKey: process.env.DEPLOYER_PRIVATE_KEY!,
|
|
52
|
-
entryFee: 0.5, // MON tokens
|
|
53
|
-
requireMembership: true,
|
|
54
|
-
},
|
|
55
|
-
};
|
|
18
|
+
## What's Included
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
20
|
+
- TypeScript SDK for world management
|
|
21
|
+
- Solidity smart contracts:
|
|
22
|
+
- `AgentRegistry.sol` - Agent registration and metadata
|
|
23
|
+
- `WorldMembership.sol` - Soulbound NFT membership tokens
|
|
24
|
+
- `WorldToken.sol` - ERC20 world currency template
|
|
25
|
+
- Foundry deployment scripts
|
|
26
|
+
- Test suite
|
|
27
|
+
|
|
28
|
+
## Foundry Dependencies
|
|
29
|
+
|
|
30
|
+
This package uses Foundry submodules for contract dependencies. The following will be auto-installed:
|
|
31
|
+
|
|
32
|
+
- `openzeppelin-contracts` - Standard token implementations
|
|
33
|
+
|
|
34
|
+
These are cloned into the `lib/` folder when you run `forge install`.
|
|
61
35
|
|
|
62
|
-
##
|
|
36
|
+
## Usage
|
|
63
37
|
|
|
64
|
-
See the [
|
|
38
|
+
See the [@moltium/world-cli](https://www.npmjs.com/package/@moltium/world-cli) package for creating and deploying worlds.
|
|
65
39
|
|
|
66
40
|
## License
|
|
67
41
|
|
package/foundry.toml
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[profile.default]
|
|
2
|
+
src = "contracts"
|
|
3
|
+
out = "out"
|
|
4
|
+
libs = ["lib"]
|
|
5
|
+
solc_version = "0.8.24"
|
|
6
|
+
optimizer = true
|
|
7
|
+
optimizer_runs = 200
|
|
8
|
+
via_ir = false
|
|
9
|
+
|
|
10
|
+
# Monad network configuration
|
|
11
|
+
[rpc_endpoints]
|
|
12
|
+
monad = "${MONAD_RPC_URL}"
|
|
13
|
+
|
|
14
|
+
[etherscan]
|
|
15
|
+
monad = { key = "${MONAD_ETHERSCAN_KEY}", url = "https://explorer.monad.xyz/api" }
|
|
16
|
+
|
|
17
|
+
# Test configuration
|
|
18
|
+
[profile.default.fuzz]
|
|
19
|
+
runs = 256
|
|
20
|
+
|
|
21
|
+
[profile.default.invariant]
|
|
22
|
+
runs = 256
|
|
23
|
+
depth = 15
|
|
24
|
+
|
|
25
|
+
eth-rpc-url="https://testnet-rpc.monad.xyz"
|
|
26
|
+
chain_id = 10143
|
|
27
|
+
evm_version = "prague"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moltium/world-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "World runtime for creating agent simulation environments with A2A-based admission and blockchain validation",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
|
-
"contracts"
|
|
19
|
+
"contracts",
|
|
20
|
+
"test",
|
|
21
|
+
"script",
|
|
22
|
+
"scripts",
|
|
23
|
+
"foundry.toml",
|
|
24
|
+
"remappings.txt"
|
|
20
25
|
],
|
|
21
26
|
"scripts": {
|
|
22
27
|
"build": "tsup",
|
|
@@ -24,8 +29,10 @@
|
|
|
24
29
|
"test": "vitest run",
|
|
25
30
|
"test:watch": "vitest",
|
|
26
31
|
"clean": "rm -rf dist",
|
|
27
|
-
"compile-contracts": "
|
|
28
|
-
"test-contracts": "
|
|
32
|
+
"compile-contracts": "forge build",
|
|
33
|
+
"test-contracts": "forge test",
|
|
34
|
+
"install-forge-deps": "forge install",
|
|
35
|
+
"postinstall": "bash scripts/postinstall.sh || true"
|
|
29
36
|
},
|
|
30
37
|
"publishConfig": {
|
|
31
38
|
"access": "public"
|
package/remappings.txt
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {Script, console} from "forge-std/Script.sol";
|
|
5
|
+
import {AgentRegistry} from "../contracts/AgentRegistry.sol";
|
|
6
|
+
import {WorldMembership} from "../contracts/WorldMembership.sol";
|
|
7
|
+
import {WorldToken} from "../contracts/WorldToken.sol";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Foundry deployment script for Monad network
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* forge script script/Deploy.s.sol:DeployScript --rpc-url monad --broadcast --verify
|
|
14
|
+
*/
|
|
15
|
+
contract DeployScript is Script {
|
|
16
|
+
function run() external {
|
|
17
|
+
// Load environment variables
|
|
18
|
+
uint256 deployerPrivateKey = vm.envUint("DEPLOYER_PRIVATE_KEY");
|
|
19
|
+
string memory worldName = vm.envOr("WORLD_NAME", string("TestWorld"));
|
|
20
|
+
uint256 entryFee = vm.envOr("ENTRY_FEE_MON", uint256(0.1 ether));
|
|
21
|
+
bool deployToken = vm.envOr("DEPLOY_WORLD_TOKEN", false);
|
|
22
|
+
|
|
23
|
+
vm.startBroadcast(deployerPrivateKey);
|
|
24
|
+
|
|
25
|
+
// 1. Deploy AgentRegistry
|
|
26
|
+
console.log("Deploying AgentRegistry...");
|
|
27
|
+
AgentRegistry agentRegistry = new AgentRegistry();
|
|
28
|
+
console.log("AgentRegistry deployed to:", address(agentRegistry));
|
|
29
|
+
|
|
30
|
+
// 2. Deploy WorldMembership
|
|
31
|
+
console.log("\nDeploying WorldMembership...");
|
|
32
|
+
console.log(" World Name:", worldName);
|
|
33
|
+
console.log(" Entry Fee:", entryFee / 1 ether, "MON");
|
|
34
|
+
WorldMembership membership = new WorldMembership(worldName, entryFee);
|
|
35
|
+
console.log("WorldMembership deployed to:", address(membership));
|
|
36
|
+
|
|
37
|
+
// 3. Deploy WorldToken (optional)
|
|
38
|
+
address tokenAddress = address(0);
|
|
39
|
+
if (deployToken) {
|
|
40
|
+
string memory tokenName = vm.envOr("TOKEN_NAME", string("World Token"));
|
|
41
|
+
string memory tokenSymbol = vm.envOr("TOKEN_SYMBOL", string("WORLD"));
|
|
42
|
+
uint256 initialSupply = vm.envOr("TOKEN_INITIAL_SUPPLY", uint256(1000000));
|
|
43
|
+
uint8 decimals = uint8(vm.envOr("TOKEN_DECIMALS", uint256(18)));
|
|
44
|
+
|
|
45
|
+
console.log("\nDeploying WorldToken...");
|
|
46
|
+
console.log(" Name:", tokenName);
|
|
47
|
+
console.log(" Symbol:", tokenSymbol);
|
|
48
|
+
console.log(" Initial Supply:", initialSupply);
|
|
49
|
+
console.log(" Decimals:", decimals);
|
|
50
|
+
|
|
51
|
+
WorldToken token = new WorldToken(tokenName, tokenSymbol, initialSupply, decimals);
|
|
52
|
+
tokenAddress = address(token);
|
|
53
|
+
console.log("WorldToken deployed to:", tokenAddress);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
vm.stopBroadcast();
|
|
57
|
+
|
|
58
|
+
// Print summary
|
|
59
|
+
console.log("\n=====================================");
|
|
60
|
+
console.log("Deployment Summary");
|
|
61
|
+
console.log("=====================================");
|
|
62
|
+
console.log("AgentRegistry: ", address(agentRegistry));
|
|
63
|
+
console.log("WorldMembership: ", address(membership));
|
|
64
|
+
if (deployToken) {
|
|
65
|
+
console.log("WorldToken: ", tokenAddress);
|
|
66
|
+
}
|
|
67
|
+
console.log("=====================================");
|
|
68
|
+
|
|
69
|
+
console.log("\nAdd these to your .env file:");
|
|
70
|
+
console.log("AGENT_REGISTRY_ADDRESS=", address(agentRegistry));
|
|
71
|
+
console.log("WORLD_MEMBERSHIP_ADDRESS=", address(membership));
|
|
72
|
+
if (deployToken) {
|
|
73
|
+
console.log("WORLD_TOKEN_ADDRESS=", tokenAddress);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const hre = require("hardhat");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deploy all world contracts to Monad network
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx hardhat run scripts/deploy.js --network monad
|
|
8
|
+
*/
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log("Deploying World SDK contracts to Monad...\n");
|
|
11
|
+
|
|
12
|
+
const [deployer] = await hre.ethers.getSigners();
|
|
13
|
+
console.log("Deploying with account:", deployer.address);
|
|
14
|
+
console.log("Account balance:", (await hre.ethers.provider.getBalance(deployer.address)).toString(), "\n");
|
|
15
|
+
|
|
16
|
+
// 1. Deploy AgentRegistry
|
|
17
|
+
console.log("Deploying AgentRegistry...");
|
|
18
|
+
const AgentRegistry = await hre.ethers.getContractFactory("AgentRegistry");
|
|
19
|
+
const agentRegistry = await AgentRegistry.deploy();
|
|
20
|
+
await agentRegistry.waitForDeployment();
|
|
21
|
+
const agentRegistryAddress = await agentRegistry.getAddress();
|
|
22
|
+
console.log("✅ AgentRegistry deployed to:", agentRegistryAddress, "\n");
|
|
23
|
+
|
|
24
|
+
// 2. Deploy WorldMembership
|
|
25
|
+
const worldName = process.env.WORLD_NAME || "TestWorld";
|
|
26
|
+
const entryFee = hre.ethers.parseEther(process.env.ENTRY_FEE_MON || "0.1"); // Default 0.1 MON
|
|
27
|
+
|
|
28
|
+
console.log("Deploying WorldMembership...");
|
|
29
|
+
console.log(" World Name:", worldName);
|
|
30
|
+
console.log(" Entry Fee:", hre.ethers.formatEther(entryFee), "MON");
|
|
31
|
+
|
|
32
|
+
const WorldMembership = await hre.ethers.getContractFactory("WorldMembership");
|
|
33
|
+
const worldMembership = await WorldMembership.deploy(worldName, entryFee);
|
|
34
|
+
await worldMembership.waitForDeployment();
|
|
35
|
+
const membershipAddress = await worldMembership.getAddress();
|
|
36
|
+
console.log("✅ WorldMembership deployed to:", membershipAddress, "\n");
|
|
37
|
+
|
|
38
|
+
// 3. Deploy WorldToken (optional)
|
|
39
|
+
let worldTokenAddress = null;
|
|
40
|
+
if (process.env.DEPLOY_WORLD_TOKEN === "true") {
|
|
41
|
+
const tokenName = process.env.TOKEN_NAME || "World Token";
|
|
42
|
+
const tokenSymbol = process.env.TOKEN_SYMBOL || "WORLD";
|
|
43
|
+
const initialSupply = process.env.TOKEN_INITIAL_SUPPLY || "1000000";
|
|
44
|
+
const decimals = parseInt(process.env.TOKEN_DECIMALS || "18");
|
|
45
|
+
|
|
46
|
+
console.log("Deploying WorldToken...");
|
|
47
|
+
console.log(" Name:", tokenName);
|
|
48
|
+
console.log(" Symbol:", tokenSymbol);
|
|
49
|
+
console.log(" Initial Supply:", initialSupply);
|
|
50
|
+
console.log(" Decimals:", decimals);
|
|
51
|
+
|
|
52
|
+
const WorldToken = await hre.ethers.getContractFactory("WorldToken");
|
|
53
|
+
const worldToken = await WorldToken.deploy(tokenName, tokenSymbol, initialSupply, decimals);
|
|
54
|
+
await worldToken.waitForDeployment();
|
|
55
|
+
worldTokenAddress = await worldToken.getAddress();
|
|
56
|
+
console.log("✅ WorldToken deployed to:", worldTokenAddress, "\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Summary
|
|
60
|
+
console.log("=====================================");
|
|
61
|
+
console.log("Deployment Summary");
|
|
62
|
+
console.log("=====================================");
|
|
63
|
+
console.log("AgentRegistry: ", agentRegistryAddress);
|
|
64
|
+
console.log("WorldMembership: ", membershipAddress);
|
|
65
|
+
if (worldTokenAddress) {
|
|
66
|
+
console.log("WorldToken: ", worldTokenAddress);
|
|
67
|
+
}
|
|
68
|
+
console.log("=====================================\n");
|
|
69
|
+
|
|
70
|
+
console.log("Add these to your .env file:");
|
|
71
|
+
console.log(`AGENT_REGISTRY_ADDRESS=${agentRegistryAddress}`);
|
|
72
|
+
console.log(`WORLD_MEMBERSHIP_ADDRESS=${membershipAddress}`);
|
|
73
|
+
if (worldTokenAddress) {
|
|
74
|
+
console.log(`WORLD_TOKEN_ADDRESS=${worldTokenAddress}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main()
|
|
79
|
+
.then(() => process.exit(0))
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
console.error(error);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Post-install script for @moltium/world-core
|
|
4
|
+
# Installs Foundry dependencies if forge is available
|
|
5
|
+
|
|
6
|
+
if command -v forge &> /dev/null; then
|
|
7
|
+
echo "📦 Installing Foundry dependencies..."
|
|
8
|
+
cd "$(dirname "$0")" || exit 1
|
|
9
|
+
forge install --no-commit
|
|
10
|
+
echo "✅ Foundry dependencies installed"
|
|
11
|
+
else
|
|
12
|
+
echo "⚠️ Foundry not found - skipping dependency installation"
|
|
13
|
+
echo " Install Foundry from https://getfoundry.sh/"
|
|
14
|
+
echo " Then run: cd node_modules/@moltium/world-core && forge install"
|
|
15
|
+
fi
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {AgentRegistry} from "../contracts/AgentRegistry.sol";
|
|
6
|
+
|
|
7
|
+
contract AgentRegistryTest is Test {
|
|
8
|
+
AgentRegistry public registry;
|
|
9
|
+
address public agent1 = address(0x1);
|
|
10
|
+
address public agent2 = address(0x2);
|
|
11
|
+
|
|
12
|
+
function setUp() public {
|
|
13
|
+
registry = new AgentRegistry();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function testRegisterAgent() public {
|
|
17
|
+
vm.prank(agent1);
|
|
18
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
19
|
+
|
|
20
|
+
assertTrue(registry.isAgentValid(agent1));
|
|
21
|
+
|
|
22
|
+
AgentRegistry.AgentInfo memory info = registry.getAgent(agent1);
|
|
23
|
+
assertEq(info.walletAddress, agent1);
|
|
24
|
+
assertEq(info.agentUrl, "http://localhost:3000");
|
|
25
|
+
assertEq(info.name, "Agent1");
|
|
26
|
+
assertTrue(info.isActive);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function testCannotRegisterTwice() public {
|
|
30
|
+
vm.startPrank(agent1);
|
|
31
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
32
|
+
|
|
33
|
+
vm.expectRevert("Agent already registered");
|
|
34
|
+
registry.registerAgent("http://localhost:3001", "Agent1Updated");
|
|
35
|
+
vm.stopPrank();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function testCannotRegisterSameUrl() public {
|
|
39
|
+
vm.prank(agent1);
|
|
40
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
41
|
+
|
|
42
|
+
vm.prank(agent2);
|
|
43
|
+
vm.expectRevert("URL already registered");
|
|
44
|
+
registry.registerAgent("http://localhost:3000", "Agent2");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function testDeactivateAgent() public {
|
|
48
|
+
vm.startPrank(agent1);
|
|
49
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
50
|
+
assertTrue(registry.isAgentValid(agent1));
|
|
51
|
+
|
|
52
|
+
registry.deactivateAgent();
|
|
53
|
+
assertFalse(registry.isAgentValid(agent1));
|
|
54
|
+
vm.stopPrank();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function testReactivateAgent() public {
|
|
58
|
+
vm.startPrank(agent1);
|
|
59
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
60
|
+
registry.deactivateAgent();
|
|
61
|
+
assertFalse(registry.isAgentValid(agent1));
|
|
62
|
+
|
|
63
|
+
registry.activateAgent();
|
|
64
|
+
assertTrue(registry.isAgentValid(agent1));
|
|
65
|
+
vm.stopPrank();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function testGetWalletByUrl() public {
|
|
69
|
+
vm.prank(agent1);
|
|
70
|
+
registry.registerAgent("http://localhost:3000", "Agent1");
|
|
71
|
+
|
|
72
|
+
address wallet = registry.getWalletByUrl("http://localhost:3000");
|
|
73
|
+
assertEq(wallet, agent1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function testEmptyUrlReverts() public {
|
|
77
|
+
vm.prank(agent1);
|
|
78
|
+
vm.expectRevert("Agent URL cannot be empty");
|
|
79
|
+
registry.registerAgent("", "Agent1");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function testEmptyNameReverts() public {
|
|
83
|
+
vm.prank(agent1);
|
|
84
|
+
vm.expectRevert("Agent name cannot be empty");
|
|
85
|
+
registry.registerAgent("http://localhost:3000", "");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.24;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
import {WorldMembership} from "../contracts/WorldMembership.sol";
|
|
6
|
+
|
|
7
|
+
contract WorldMembershipTest is Test {
|
|
8
|
+
WorldMembership public membership;
|
|
9
|
+
address public worldOwner = address(this);
|
|
10
|
+
address public agent1 = address(0x1);
|
|
11
|
+
address public agent2 = address(0x2);
|
|
12
|
+
uint256 public entryFee = 0.1 ether;
|
|
13
|
+
|
|
14
|
+
function setUp() public {
|
|
15
|
+
membership = new WorldMembership("TestWorld", entryFee);
|
|
16
|
+
vm.deal(worldOwner, 100 ether);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function testMintMembership() public {
|
|
20
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
21
|
+
|
|
22
|
+
assertTrue(membership.hasMembership(agent1));
|
|
23
|
+
assertEq(membership.totalMembers(), 1);
|
|
24
|
+
assertEq(membership.balanceOf(agent1), 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function testCannotMintWithInsufficientFee() public {
|
|
28
|
+
vm.expectRevert("Insufficient entry fee");
|
|
29
|
+
membership.mintMembership{value: 0.05 ether}(agent1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function testCannotMintDuplicate() public {
|
|
33
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
34
|
+
|
|
35
|
+
vm.expectRevert("Agent already has membership");
|
|
36
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function testRevokeMembership() public {
|
|
40
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
41
|
+
assertTrue(membership.hasMembership(agent1));
|
|
42
|
+
|
|
43
|
+
membership.revokeMembership(agent1);
|
|
44
|
+
assertFalse(membership.hasMembership(agent1));
|
|
45
|
+
assertEq(membership.balanceOf(agent1), 0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function testSoulboundTransferReverts() public {
|
|
49
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
50
|
+
uint256 tokenId = membership.agentToToken(agent1);
|
|
51
|
+
|
|
52
|
+
vm.prank(agent1);
|
|
53
|
+
// Using try-catch to silence erc20-unchecked-transfer warning
|
|
54
|
+
// Note: This is an ERC721 call, not ERC20, and we expect it to revert
|
|
55
|
+
try membership.transferFrom(agent1, agent2, tokenId) {
|
|
56
|
+
fail("Expected transfer to revert");
|
|
57
|
+
} catch Error(string memory reason) {
|
|
58
|
+
assertEq(reason, "WorldMembership: Token is soulbound and cannot be transferred");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function testWithdrawFees() public {
|
|
63
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
64
|
+
membership.mintMembership{value: entryFee}(agent2);
|
|
65
|
+
|
|
66
|
+
uint256 balanceBefore = address(this).balance;
|
|
67
|
+
membership.withdrawFees();
|
|
68
|
+
uint256 balanceAfter = address(this).balance;
|
|
69
|
+
|
|
70
|
+
assertEq(balanceAfter - balanceBefore, entryFee * 2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function testUpdateEntryFee() public {
|
|
74
|
+
uint256 newFee = 0.2 ether;
|
|
75
|
+
membership.updateEntryFee(newFee);
|
|
76
|
+
assertEq(membership.entryFee(), newFee);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function testOnlyOwnerCanMint() public {
|
|
80
|
+
// hoax sets both msg.sender and provides ETH
|
|
81
|
+
hoax(agent1, 1 ether);
|
|
82
|
+
vm.expectRevert();
|
|
83
|
+
membership.mintMembership{value: entryFee}(agent2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function testOnlyOwnerCanRevoke() public {
|
|
87
|
+
membership.mintMembership{value: entryFee}(agent1);
|
|
88
|
+
|
|
89
|
+
vm.prank(agent2);
|
|
90
|
+
vm.expectRevert();
|
|
91
|
+
membership.revokeMembership(agent1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Allow this contract to receive ether
|
|
95
|
+
receive() external payable {}
|
|
96
|
+
}
|