ape-claw 0.1.0
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/.cursor/skills/ape-claw/SKILL.md +322 -0
- package/LICENSE +21 -0
- package/README.md +826 -0
- package/allowlists/opensea-slug-overrides.json +13 -0
- package/allowlists/recommended.apechain.json +322 -0
- package/config/clawbots.example.json +3 -0
- package/config/policy.example.json +27 -0
- package/data/starter-pack-bundle.json +1 -0
- package/data/starter-pack.json +495 -0
- package/docs/ACP_BOUNTIES.md +108 -0
- package/docs/APECLAW_V2_ALPHA.md +206 -0
- package/docs/AUTONOMY_AND_SUBSTRATE.md +69 -0
- package/docs/CLAWBOTS_AND_INVITES.md +102 -0
- package/docs/CLI_GUIDE.md +124 -0
- package/docs/CONTRIBUTING.md +130 -0
- package/docs/DASHBOARD_GUIDE.md +108 -0
- package/docs/GLOBAL_BACKEND.md +145 -0
- package/docs/ONCHAIN_V2_GUIDE.md +140 -0
- package/docs/PRODUCT_OVERVIEW.md +127 -0
- package/docs/README.md +40 -0
- package/docs/SKILLCARDS_AND_IMPORTER.md +147 -0
- package/docs/STARTER_PACK.md +297 -0
- package/docs/SUPPORTED_NETWORKS.md +58 -0
- package/docs/TELEMETRY_AND_EVENTS.md +103 -0
- package/docs/THE_POD_RUNNER.md +198 -0
- package/docs/V1_WORKFLOWS.md +108 -0
- package/docs/V2_ONCHAIN_SKILLS.md +157 -0
- package/docs/WEB4_PLAN_STATUS.md +95 -0
- package/docs/WEB4_SWARM_MODEL.md +104 -0
- package/docs/archive/AUTONOMY_AND_SUBSTRATE.md +66 -0
- package/docs/archive/WEB4_PLAN_STATUS.md +93 -0
- package/docs/archive/WEB4_SWARM_MODEL.md +98 -0
- package/docs/developer/01-architecture.md +345 -0
- package/docs/developer/02-contracts.md +1034 -0
- package/docs/developer/03-writing-modules.md +513 -0
- package/docs/developer/04-skillcard-spec.md +336 -0
- package/docs/developer/05-backend-api.md +1079 -0
- package/docs/developer/06-telemetry.md +798 -0
- package/docs/developer/07-testing.md +546 -0
- package/docs/developer/08-contributing.md +211 -0
- package/docs/operator/01-quickstart.md +49 -0
- package/docs/operator/02-dashboard.md +174 -0
- package/docs/operator/03-cli-reference.md +818 -0
- package/docs/operator/04-skills-library.md +169 -0
- package/docs/operator/05-pod-operations.md +314 -0
- package/docs/operator/06-deployment.md +299 -0
- package/docs/operator/07-safety-and-policy.md +311 -0
- package/docs/operator/08-troubleshooting.md +457 -0
- package/docs/operator/09-env-reference.md +238 -0
- package/docs/social/STARTER_PACK_THREAD.md +209 -0
- package/package.json +77 -0
- package/skillcards/import-sources.json +93 -0
- package/skillcards/seed/acp-bounty-poll.v1.json +38 -0
- package/skillcards/seed/acp-bounty-post.v1.json +55 -0
- package/skillcards/seed/acp-browse.v1.json +41 -0
- package/skillcards/seed/acp-fulfill-and-route.v1.json +56 -0
- package/skillcards/seed/apeclaw-bridge-relay.v1.json +46 -0
- package/skillcards/seed/apeclaw-nft-autobuy.v1.json +60 -0
- package/skillcards/seed/apeclaw-receipt-recorder.v1.json +64 -0
- package/skillcards/seed/humanizer.v1.json +74 -0
- package/skillcards/seed/otherside-navigator.v1.json +116 -0
- package/skillcards/seed/stonkbrokers-launcher.v1.json +280 -0
- package/skillcards/seed/walkie-p2p.v1.json +66 -0
- package/src/cli/index.mjs +8 -0
- package/src/cli.mjs +1929 -0
- package/src/lib/bridge-relay.mjs +294 -0
- package/src/lib/clawbots.mjs +94 -0
- package/src/lib/io.mjs +36 -0
- package/src/lib/market.mjs +233 -0
- package/src/lib/nft-opensea.mjs +159 -0
- package/src/lib/paths.mjs +17 -0
- package/src/lib/pod-init.mjs +40 -0
- package/src/lib/policy.mjs +112 -0
- package/src/lib/rpc.mjs +49 -0
- package/src/lib/telemetry.mjs +92 -0
- package/src/lib/v2-onchain-abi.mjs +294 -0
- package/src/lib/v2-skillcard.mjs +27 -0
- package/src/server/index.mjs +169 -0
- package/src/server/logger.mjs +21 -0
- package/src/server/middleware/auth.mjs +90 -0
- package/src/server/middleware/body-limit.mjs +35 -0
- package/src/server/middleware/cors.mjs +33 -0
- package/src/server/middleware/rate-limit.mjs +44 -0
- package/src/server/routes/chat.mjs +178 -0
- package/src/server/routes/clawbots.mjs +182 -0
- package/src/server/routes/events.mjs +95 -0
- package/src/server/routes/health.mjs +72 -0
- package/src/server/routes/pod.mjs +64 -0
- package/src/server/routes/quotes.mjs +161 -0
- package/src/server/routes/skills.mjs +239 -0
- package/src/server/routes/static.mjs +161 -0
- package/src/server/routes/v2.mjs +48 -0
- package/src/server/sse.mjs +73 -0
- package/src/server/storage/file-backend.mjs +295 -0
- package/src/server/storage/index.mjs +37 -0
- package/src/server/storage/sqlite-backend.mjs +380 -0
- package/src/telemetry-server.mjs +1604 -0
- package/ui/css/dashboard.css +792 -0
- package/ui/css/skills.css +689 -0
- package/ui/docs.html +840 -0
- package/ui/favicon-180.png +0 -0
- package/ui/favicon-192.png +0 -0
- package/ui/favicon-32.png +0 -0
- package/ui/favicon-lobster.png +0 -0
- package/ui/favicon.svg +10 -0
- package/ui/index.html +2957 -0
- package/ui/js/dashboard.js +1766 -0
- package/ui/js/skills.js +1621 -0
- package/ui/pod.html +909 -0
- package/ui/shared/motion.css +286 -0
- package/ui/shared/motion.js +170 -0
- package/ui/shared/sidebar-nav.css +379 -0
- package/ui/shared/sidebar-nav.js +137 -0
- package/ui/skills.html +2879 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# Writing Modules
|
|
2
|
+
|
|
3
|
+
How to create a new `ISkillModule` for ApeClaw.
|
|
4
|
+
|
|
5
|
+
## Interface
|
|
6
|
+
|
|
7
|
+
Every module must implement the `ISkillModule` interface:
|
|
8
|
+
|
|
9
|
+
```solidity
|
|
10
|
+
// From contracts/ISkillModule.sol
|
|
11
|
+
interface ISkillModule {
|
|
12
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output);
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The interface is intentionally minimal:
|
|
17
|
+
- **`agentAccount`**: The `AgentAccount` contract address calling the module
|
|
18
|
+
- **`input`**: Opaque bytes payload (typically `abi.encode(target, calldata)`)
|
|
19
|
+
- **`output`**: Opaque bytes return value
|
|
20
|
+
- **`payable`**: Allows the module to receive ETH
|
|
21
|
+
|
|
22
|
+
## Step-by-Step Guide
|
|
23
|
+
|
|
24
|
+
### 1. Create the Contract
|
|
25
|
+
|
|
26
|
+
Create a new Solidity file in `contracts/`:
|
|
27
|
+
|
|
28
|
+
```solidity
|
|
29
|
+
// SPDX-License-Identifier: MIT
|
|
30
|
+
pragma solidity ^0.8.24;
|
|
31
|
+
|
|
32
|
+
import { ISkillModule } from "./ISkillModule.sol";
|
|
33
|
+
|
|
34
|
+
contract StakeModule is ISkillModule {
|
|
35
|
+
event StakeExecuted(address indexed agent, address indexed target, bytes4 indexed selector, uint256 value);
|
|
36
|
+
|
|
37
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
38
|
+
(address target, bytes memory data) = abi.decode(input, (address, bytes));
|
|
39
|
+
require(target != address(0), "target required");
|
|
40
|
+
|
|
41
|
+
bytes4 selector;
|
|
42
|
+
assembly {
|
|
43
|
+
selector := mload(add(data, 32))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
(bool ok, bytes memory ret) = target.call{ value: msg.value }(data);
|
|
47
|
+
require(ok, "call failed");
|
|
48
|
+
|
|
49
|
+
emit StakeExecuted(agentAccount, target, selector, msg.value);
|
|
50
|
+
return ret;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Implement execute()
|
|
56
|
+
|
|
57
|
+
The standard pattern decodes `input` as `(address target, bytes calldata data)` and forwards the call:
|
|
58
|
+
|
|
59
|
+
```solidity
|
|
60
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
61
|
+
// Decode input: (target address, calldata bytes)
|
|
62
|
+
(address target, bytes memory data) = abi.decode(input, (address, bytes));
|
|
63
|
+
require(target != address(0), "target required");
|
|
64
|
+
|
|
65
|
+
// Extract function selector (first 4 bytes of calldata)
|
|
66
|
+
bytes4 selector;
|
|
67
|
+
assembly {
|
|
68
|
+
selector := mload(add(data, 32))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Forward call to target
|
|
72
|
+
(bool ok, bytes memory ret) = target.call{ value: msg.value }(data);
|
|
73
|
+
require(ok, "call failed");
|
|
74
|
+
|
|
75
|
+
// Emit event for indexing
|
|
76
|
+
emit StakeExecuted(agentAccount, target, selector, msg.value);
|
|
77
|
+
return ret;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Note**: The `AgentAccount` contract enforces policy checks **before** calling your module, so you can trust that:
|
|
82
|
+
- The module is allowlisted
|
|
83
|
+
- The target is allowlisted
|
|
84
|
+
- The selector is allowlisted
|
|
85
|
+
- The value is within `maxValuePerTx`
|
|
86
|
+
|
|
87
|
+
### 3. Deploy the Module
|
|
88
|
+
|
|
89
|
+
Add deployment to `contracts-scripts/deploy-and-seed-v2-alpha.js`:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
console.log("[v2] Deploying StakeModule...");
|
|
93
|
+
const stakeModule = await viem.deployContract("StakeModule");
|
|
94
|
+
console.log("[v2] StakeModule:", stakeModule.address);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Or deploy manually:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx hardhat run scripts/deploy-stake-module.js --network apechain
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Register with PolicyEngine
|
|
104
|
+
|
|
105
|
+
The module must be allowlisted before it can be executed:
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
// From deploy-and-seed-v2-alpha.js
|
|
109
|
+
await policy.write.setModuleAllowed([stakeModule.address, true]);
|
|
110
|
+
console.log("[v2] Allowlisted StakeModule");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 5. Register Target and Selector
|
|
114
|
+
|
|
115
|
+
If your module calls a specific staking contract, allowlist it:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
const stakingContract = "0x..."; // Your staking contract address
|
|
119
|
+
const stakeSelector = "0x..."; // Function selector (e.g., stake(uint256))
|
|
120
|
+
|
|
121
|
+
await policy.write.setTargetAllowed([stakingContract, true]);
|
|
122
|
+
await policy.write.setSelectorAllowed([stakingContract, stakeSelector, true]);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 6. Test with AgentAccount
|
|
126
|
+
|
|
127
|
+
Execute via `AgentAccount.executeSkill()`:
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
// From src/cli.mjs (example)
|
|
131
|
+
const input = abi.encode(
|
|
132
|
+
stakingContract, // target
|
|
133
|
+
encodeFunctionData({ // calldata
|
|
134
|
+
abi: stakingAbi,
|
|
135
|
+
functionName: "stake",
|
|
136
|
+
args: [amount]
|
|
137
|
+
})
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
await agentAccount.write.executeSkill([
|
|
141
|
+
stakeModule.address, // module
|
|
142
|
+
input, // input
|
|
143
|
+
parseEther("0.1"), // value
|
|
144
|
+
traceIdHash, // traceIdHash
|
|
145
|
+
subjectHash, // subjectHash
|
|
146
|
+
"ipfs://..." // uri
|
|
147
|
+
], { value: parseEther("0.1") });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Example: Creating a StakeModule
|
|
151
|
+
|
|
152
|
+
Let's walk through creating a complete `StakeModule` that stakes tokens:
|
|
153
|
+
|
|
154
|
+
### Contract Implementation
|
|
155
|
+
|
|
156
|
+
```solidity
|
|
157
|
+
// SPDX-License-Identifier: MIT
|
|
158
|
+
pragma solidity ^0.8.24;
|
|
159
|
+
|
|
160
|
+
import { ISkillModule } from "./ISkillModule.sol";
|
|
161
|
+
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
162
|
+
|
|
163
|
+
/// @notice Staking module for token staking contracts.
|
|
164
|
+
contract StakeModule is ISkillModule {
|
|
165
|
+
event StakeExecuted(
|
|
166
|
+
address indexed agent,
|
|
167
|
+
address indexed target,
|
|
168
|
+
address indexed token,
|
|
169
|
+
uint256 amount,
|
|
170
|
+
bytes4 selector
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
174
|
+
// Decode: (target staking contract, token address, amount, calldata)
|
|
175
|
+
(address target, address token, uint256 amount, bytes memory data) = abi.decode(
|
|
176
|
+
input,
|
|
177
|
+
(address, address, uint256, bytes)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
require(target != address(0), "target required");
|
|
181
|
+
require(token != address(0), "token required");
|
|
182
|
+
require(amount > 0, "amount required");
|
|
183
|
+
|
|
184
|
+
bytes4 selector;
|
|
185
|
+
assembly {
|
|
186
|
+
selector := mload(add(data, 32))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Approve token spending (if needed)
|
|
190
|
+
IERC20(token).approve(target, amount);
|
|
191
|
+
|
|
192
|
+
// Forward call to staking contract
|
|
193
|
+
(bool ok, bytes memory ret) = target.call(data);
|
|
194
|
+
require(ok, "stake call failed");
|
|
195
|
+
|
|
196
|
+
emit StakeExecuted(agentAccount, target, token, amount, selector);
|
|
197
|
+
return ret;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Deployment Script
|
|
203
|
+
|
|
204
|
+
```javascript
|
|
205
|
+
// scripts/deploy-stake-module.js
|
|
206
|
+
import hre from "hardhat";
|
|
207
|
+
|
|
208
|
+
async function main() {
|
|
209
|
+
const { viem } = await hre.network.connect();
|
|
210
|
+
|
|
211
|
+
console.log("Deploying StakeModule...");
|
|
212
|
+
const stakeModule = await viem.deployContract("StakeModule");
|
|
213
|
+
console.log("StakeModule deployed to:", stakeModule.address);
|
|
214
|
+
|
|
215
|
+
// Get PolicyEngine address from deployment record
|
|
216
|
+
const deployment = JSON.parse(
|
|
217
|
+
fs.readFileSync("state/v2-deployments/apechain.json", "utf8")
|
|
218
|
+
);
|
|
219
|
+
const policy = await viem.getContractAt("PolicyEngine", deployment.policy);
|
|
220
|
+
|
|
221
|
+
// Allowlist module
|
|
222
|
+
await policy.write.setModuleAllowed([stakeModule.address, true]);
|
|
223
|
+
console.log("StakeModule allowlisted");
|
|
224
|
+
|
|
225
|
+
// Allowlist staking contract and selector
|
|
226
|
+
const stakingContract = "0x..."; // Your staking contract
|
|
227
|
+
const stakeSelector = "0xa694fc3a"; // stake(uint256)
|
|
228
|
+
|
|
229
|
+
await policy.write.setTargetAllowed([stakingContract, true]);
|
|
230
|
+
await policy.write.setSelectorAllowed([stakingContract, stakeSelector, true]);
|
|
231
|
+
console.log("Staking contract allowlisted");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
main().catch(console.error);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### CLI Integration
|
|
238
|
+
|
|
239
|
+
Add a CLI command to execute staking:
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
// In src/cli.mjs
|
|
243
|
+
if (group === "stake" && sub === "execute") {
|
|
244
|
+
const stakingContract = args.target;
|
|
245
|
+
const token = args.token;
|
|
246
|
+
const amount = parseUnits(args.amount, 18);
|
|
247
|
+
|
|
248
|
+
if (!stakingContract || !token || !amount) {
|
|
249
|
+
fail("Required: --target --token --amount", command, args);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const input = encodeAbiParameters(
|
|
253
|
+
[{ type: "address" }, { type: "address" }, { type: "uint256" }, { type: "bytes" }],
|
|
254
|
+
[
|
|
255
|
+
stakingContract,
|
|
256
|
+
token,
|
|
257
|
+
amount,
|
|
258
|
+
encodeFunctionData({
|
|
259
|
+
abi: stakingAbi,
|
|
260
|
+
functionName: "stake",
|
|
261
|
+
args: [amount]
|
|
262
|
+
})
|
|
263
|
+
]
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const txHash = await agentAccount.write.executeSkill([
|
|
267
|
+
stakeModuleAddress,
|
|
268
|
+
input,
|
|
269
|
+
0n, // no ETH value
|
|
270
|
+
traceIdHash,
|
|
271
|
+
subjectHash,
|
|
272
|
+
uri
|
|
273
|
+
]);
|
|
274
|
+
|
|
275
|
+
return print({ ok: true, txHash }, asJson);
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Registration
|
|
280
|
+
|
|
281
|
+
### PolicyEngine Allowlists
|
|
282
|
+
|
|
283
|
+
The `PolicyEngine` maintains three allowlists:
|
|
284
|
+
|
|
285
|
+
1. **Module Allowlist**: Which modules can be executed
|
|
286
|
+
2. **Target Allowlist**: Which contracts modules can call
|
|
287
|
+
3. **Selector Allowlist**: Which functions can be called on targets
|
|
288
|
+
|
|
289
|
+
```solidity
|
|
290
|
+
// From PolicyEngine.sol
|
|
291
|
+
mapping(address => bool) public allowedModules;
|
|
292
|
+
mapping(address => bool) public allowedTargets;
|
|
293
|
+
mapping(address => mapping(bytes4 => bool)) public allowedSelectors;
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Registration Functions
|
|
297
|
+
|
|
298
|
+
```solidity
|
|
299
|
+
// Allowlist a module
|
|
300
|
+
function setModuleAllowed(address module, bool allowed) external onlyOwner;
|
|
301
|
+
|
|
302
|
+
// Allowlist a target contract
|
|
303
|
+
function setTargetAllowed(address target, bool allowed) external onlyOwner;
|
|
304
|
+
|
|
305
|
+
// Allowlist a function selector on a target
|
|
306
|
+
function setSelectorAllowed(address target, bytes4 selector, bool allowed) external onlyOwner;
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Example Registration
|
|
310
|
+
|
|
311
|
+
From `deploy-and-seed-v2-alpha.js`:
|
|
312
|
+
|
|
313
|
+
```javascript
|
|
314
|
+
// Set max value per transaction
|
|
315
|
+
await policy.write.setMaxValuePerTx([parseEther("1")]);
|
|
316
|
+
|
|
317
|
+
// Allowlist modules
|
|
318
|
+
await policy.write.setModuleAllowed([swapModule.address, true]);
|
|
319
|
+
await policy.write.setModuleAllowed([bridgeModule.address, true]);
|
|
320
|
+
await policy.write.setModuleAllowed([nftBuyModule.address, true]);
|
|
321
|
+
|
|
322
|
+
// Allowlist targets (example: mock target for testing)
|
|
323
|
+
const mockTarget = await viem.deployContract("MockTarget");
|
|
324
|
+
await policy.write.setTargetAllowed([mockTarget.address, true]);
|
|
325
|
+
|
|
326
|
+
// Allowlist selectors
|
|
327
|
+
const mockSelector = "0x12345678";
|
|
328
|
+
await policy.write.setSelectorAllowed([mockTarget.address, mockSelector, true]);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Receipts
|
|
332
|
+
|
|
333
|
+
Receipts are automatically recorded by `AgentAccount` after module execution:
|
|
334
|
+
|
|
335
|
+
```solidity
|
|
336
|
+
// From AgentAccount.sol
|
|
337
|
+
function executeSkill(...) external payable returns (bytes memory output) {
|
|
338
|
+
// ... policy check ...
|
|
339
|
+
|
|
340
|
+
// Execute module
|
|
341
|
+
output = ISkillModule(module).execute{ value: value }(address(this), input);
|
|
342
|
+
|
|
343
|
+
// Compute content hash
|
|
344
|
+
bytes32 outputHash = keccak256(output);
|
|
345
|
+
bytes32 contentHash = keccak256(abi.encodePacked(
|
|
346
|
+
module,
|
|
347
|
+
keccak256(input),
|
|
348
|
+
value,
|
|
349
|
+
outputHash
|
|
350
|
+
));
|
|
351
|
+
|
|
352
|
+
// Record receipt (best-effort, doesn't fail execution)
|
|
353
|
+
bool receiptOk = false;
|
|
354
|
+
if (address(receipts) != address(0)) {
|
|
355
|
+
try receipts.recordReceipt(traceIdHash, contentHash, subjectHash, uri) {
|
|
356
|
+
receiptOk = true;
|
|
357
|
+
} catch {
|
|
358
|
+
receiptOk = false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
emit SkillExecuted(traceIdHash, contentHash, module, target, selector, value, receiptOk);
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Receipt Structure
|
|
367
|
+
|
|
368
|
+
```solidity
|
|
369
|
+
// From ReceiptRegistry.sol
|
|
370
|
+
struct Receipt {
|
|
371
|
+
bytes32 traceIdHash; // Execution trace identifier
|
|
372
|
+
bytes32 contentHash; // Hash of (module, input, value, output)
|
|
373
|
+
bytes32 subject; // Agent/skill identifier
|
|
374
|
+
string uri; // Metadata URI
|
|
375
|
+
uint64 recordedAt; // Timestamp
|
|
376
|
+
address recorder; // AgentAccount address
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
### Querying Receipts
|
|
381
|
+
|
|
382
|
+
```javascript
|
|
383
|
+
// Check if receipt exists
|
|
384
|
+
const isRecorded = await receipts.read.isRecorded([traceIdHash]);
|
|
385
|
+
|
|
386
|
+
// Get receipt details
|
|
387
|
+
const receipt = await receipts.read.getReceipt([traceIdHash]);
|
|
388
|
+
console.log({
|
|
389
|
+
traceIdHash: receipt.traceIdHash,
|
|
390
|
+
contentHash: receipt.contentHash,
|
|
391
|
+
subject: receipt.subject,
|
|
392
|
+
uri: receipt.uri,
|
|
393
|
+
recordedAt: receipt.recordedAt,
|
|
394
|
+
recorder: receipt.recorder
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Module Patterns
|
|
399
|
+
|
|
400
|
+
### Pattern 1: Simple Forwarder
|
|
401
|
+
|
|
402
|
+
Most modules are simple forwarders (like `SwapModule`, `BridgeModule`, `NftBuyModule`):
|
|
403
|
+
|
|
404
|
+
```solidity
|
|
405
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
406
|
+
(address target, bytes memory data) = abi.decode(input, (address, bytes));
|
|
407
|
+
require(target != address(0), "target required");
|
|
408
|
+
|
|
409
|
+
bytes4 selector;
|
|
410
|
+
assembly { selector := mload(add(data, 32)) }
|
|
411
|
+
|
|
412
|
+
(bool ok, bytes memory ret) = target.call{ value: msg.value }(data);
|
|
413
|
+
require(ok, "call failed");
|
|
414
|
+
|
|
415
|
+
emit ModuleExecuted(agentAccount, target, selector, msg.value);
|
|
416
|
+
return ret;
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Pattern 2: Token Approver
|
|
421
|
+
|
|
422
|
+
Modules that need to approve tokens before calling:
|
|
423
|
+
|
|
424
|
+
```solidity
|
|
425
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
426
|
+
(address target, address token, uint256 amount, bytes memory data) = abi.decode(
|
|
427
|
+
input,
|
|
428
|
+
(address, address, uint256, bytes)
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Approve token spending
|
|
432
|
+
IERC20(token).approve(target, amount);
|
|
433
|
+
|
|
434
|
+
// Forward call
|
|
435
|
+
(bool ok, bytes memory ret) = target.call(data);
|
|
436
|
+
require(ok, "call failed");
|
|
437
|
+
|
|
438
|
+
return ret;
|
|
439
|
+
}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
### Pattern 3: Multi-Step Operations
|
|
443
|
+
|
|
444
|
+
Modules that perform multiple operations:
|
|
445
|
+
|
|
446
|
+
```solidity
|
|
447
|
+
function execute(address agentAccount, bytes calldata input) external payable returns (bytes memory output) {
|
|
448
|
+
(address[] memory targets, bytes[] memory calldatas) = abi.decode(
|
|
449
|
+
input,
|
|
450
|
+
(address[], bytes[])
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
require(targets.length == calldatas.length, "length mismatch");
|
|
454
|
+
|
|
455
|
+
bytes[] memory outputs = new bytes[](targets.length);
|
|
456
|
+
|
|
457
|
+
for (uint256 i = 0; i < targets.length; i++) {
|
|
458
|
+
(bool ok, bytes memory ret) = targets[i].call(calldatas[i]);
|
|
459
|
+
require(ok, "step failed");
|
|
460
|
+
outputs[i] = ret;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return abi.encode(outputs);
|
|
464
|
+
}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
## Best Practices
|
|
468
|
+
|
|
469
|
+
1. **Always validate inputs**: Check for zero addresses and invalid values
|
|
470
|
+
2. **Emit events**: Include `agentAccount`, `target`, `selector`, and `value` for indexing
|
|
471
|
+
3. **Handle failures gracefully**: Use `require()` with clear error messages
|
|
472
|
+
4. **Keep it simple**: Modules should be thin wrappers; complex logic belongs in target contracts
|
|
473
|
+
5. **Document the input format**: Specify the `abi.decode` structure in comments
|
|
474
|
+
6. **Test thoroughly**: Write tests for both success and failure cases
|
|
475
|
+
|
|
476
|
+
## Testing
|
|
477
|
+
|
|
478
|
+
Example Hardhat test:
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
import { expect } from "chai";
|
|
482
|
+
import hre from "hardhat";
|
|
483
|
+
|
|
484
|
+
describe("StakeModule", function() {
|
|
485
|
+
let stakeModule, agentAccount, stakingContract, token;
|
|
486
|
+
|
|
487
|
+
beforeEach(async function() {
|
|
488
|
+
const { viem } = await hre.network.connect();
|
|
489
|
+
|
|
490
|
+
stakeModule = await viem.deployContract("StakeModule");
|
|
491
|
+
// ... deploy other contracts ...
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
it("should stake tokens", async function() {
|
|
495
|
+
const amount = parseEther("100");
|
|
496
|
+
const input = encodeAbiParameters(
|
|
497
|
+
[{ type: "address" }, { type: "address" }, { type: "uint256" }, { type: "bytes" }],
|
|
498
|
+
[stakingContract.address, token.address, amount, calldata]
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
await expect(
|
|
502
|
+
agentAccount.write.executeSkill([
|
|
503
|
+
stakeModule.address,
|
|
504
|
+
input,
|
|
505
|
+
0n,
|
|
506
|
+
traceIdHash,
|
|
507
|
+
subjectHash,
|
|
508
|
+
uri
|
|
509
|
+
])
|
|
510
|
+
).to.emit(stakeModule, "StakeExecuted");
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
```
|