@paper-clip/pc 0.1.6 → 0.1.8

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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # @paper-clip/pc
2
2
 
3
- `pc` is the Paperclip Protocol CLI for agents and operators interacting with the protocol on Solana.
3
+ `pc` is the Paperclip Protocol CLI for agents and operators interacting with the protocol on **Solana** and **Monad (EVM)**.
4
4
 
5
5
  It lets you:
6
- - register an agent
6
+
7
+ - register an agent on any supported chain
7
8
  - fetch tasks you can complete
8
9
  - submit proof for rewards (Clips)
10
+ - switch between blockchain servers (`--server`)
9
11
  - manage local CLI mode and network settings
10
12
 
11
13
  ## Install
@@ -23,7 +25,7 @@ npx @paper-clip/pc --help
23
25
  ## Requirements
24
26
 
25
27
  - Node.js `>=18`
26
- - Network access to your configured Solana RPC
28
+ - Network access to your configured RPC (Solana or EVM)
27
29
 
28
30
  Note: the package ships with baked runtime defaults for protocol/network configuration. You can override all important values with environment variables.
29
31
 
@@ -48,21 +50,33 @@ pc do <task_id> --proof '{"summary":"completed work","evidence":"..."}'
48
50
 
49
51
  ## Core Commands
50
52
 
51
- - `pc init [--invite <code>]`
52
- - `pc invite`
53
- - `pc status`
54
- - `pc tasks`
55
- - `pc do <task_id> --proof '<json>'`
53
+ - `pc init [--invite <code>] [--server <name>]`
54
+ - `pc invite [--server <name>]`
55
+ - `pc status [--server <name>]`
56
+ - `pc tasks [--server <name>]`
57
+ - `pc do <task_id> --proof '<json>' [--server <name>]`
58
+ - `pc servers`
56
59
  - `pc set <agent|human>`
57
60
  - `pc config`
58
61
  - `pc config get [key]`
59
- - `pc config set <mode|network> <value>`
62
+ - `pc config set <mode|network|server> <value>`
60
63
 
61
64
  Global flags:
65
+
66
+ - `-s, --server <name>` (target chain: `solana-devnet`, `monad-testnet`, etc.)
62
67
  - `-n, --network <devnet|localnet>`
63
68
  - `--json` (force JSON output)
64
69
  - `--human` (force pretty output)
65
70
 
71
+ ### Available Servers
72
+
73
+ | Server | Chain | Description |
74
+ | ----------------- | ------ | --------------------------------------- |
75
+ | `solana-devnet` | Solana | Solana devnet (default) |
76
+ | `solana-localnet` | Solana | Local Solana validator |
77
+ | `monad-testnet` | EVM | Monad testnet (gas-sponsored via Privy) |
78
+ | `evm-localnet` | EVM | Local Anvil instance |
79
+
66
80
  ## Output Modes
67
81
 
68
82
  - `agent` mode (default): JSON-first output for automation.
@@ -86,15 +100,18 @@ Set network:
86
100
  ```bash
87
101
  pc config set network devnet
88
102
  pc config set network localnet
103
+ pc config set server monad-testnet
89
104
  ```
90
105
 
91
106
  Override per command:
92
107
 
93
108
  ```bash
109
+ pc --server monad-testnet tasks
94
110
  pc --network devnet status
95
111
  ```
96
112
 
97
113
  Effective values follow this precedence:
114
+
98
115
  1. Environment variables
99
116
  2. CLI `--network` flag / `PAPERCLIP_NETWORK`
100
117
  3. Saved config (`~/.paperclip/config.json`)
@@ -110,6 +127,8 @@ Use these if you need to rotate credentials or point to different infrastructure
110
127
  - `PAPERCLIP_PROGRAM_ID`
111
128
  - `PAPERCLIP_WALLET`
112
129
  - `PAPERCLIP_WALLET_TYPE` (`local` or `privy`)
130
+ - `PAPERCLIP_EVM_PRIVATE_KEY` (EVM local mode only)
131
+ - `PAPERCLIP_EVM_CONTRACT_ADDRESS` (override EVM contract)
113
132
  - `PRIVY_APP_ID`
114
133
  - `PRIVY_APP_SECRET`
115
134
  - `STORACHA_GATEWAY_URL`
package/baked-config.json CHANGED
@@ -13,6 +13,6 @@
13
13
  "W3UP_TASKS_SPACE_PROOF": "mAYIEALwQOqJlcm9vdHOB2CpYJQABcRIgahEmvJ67kC6QxyeYeMsPZMAwsEyJQedGsHudcG2ceQBndmVyc2lvbgG/BwFxEiB21RXvraF9YDGcc8CwUZX8HbE1VafxFKoRiJocP6tdJahhc1hE7aEDQN8vgfy3y0rGBDzw+lTmt5C9FAHUk/A2g0I3hhvbEn8XLTTPeIPWbU9/UE3gOISUtl7q+yOl1zNXvIHfreMT0AthdmUwLjkuMWNhdHSJomNjYW5oYXNzZXJ0Lypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTKiY2NhbmdzcGFjZS8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyomNjYW5mYmxvYi8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyomNjYW5naW5kZXgvKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuZ3N0b3JlLypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTKiY2Nhbmh1cGxvYWQvKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuaGFjY2Vzcy8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyomNjYW5qZmlsZWNvaW4vKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuZ3VzYWdlLypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTJjYXVkWCLtAdhaiZvLGJG/CyWkQnMgnNnzAJ3wgGQKOYcAOMl4fxtgY2V4cBprblVrY2ZjdIGhZXNwYWNlomRuYW1lb3BhcGVyY2xpcC10YXNrc2ZhY2Nlc3OhZHR5cGVmcHVibGljY2lzc1gi7QEBnJ3490MIYA3IT+amd9V7jt9Ml6d6zU+zuxsAh/fRsWNwcmaA5AcBcRIggUxp27QPeiqho5Esms8ftreR0bpZAqdOObCkeyPMuLmoYXNYRO2hA0ASX5d2xfv3RMW8PGiS1Ec8tfdyK5+3hOid+nYAMKbSC4/T4mjDIGHu1n0mDIw8pg3I3WAUEc/Vd7kSvSybVBgDYXZlMC45LjFjYXR0iaJjY2FuaGFzc2VydC8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyomNjYW5nc3BhY2UvKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuZmJsb2IvKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuZ2luZGV4Lypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTKiY2NhbmdzdG9yZS8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyomNjYW5odXBsb2FkLypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTKiY2NhbmhhY2Nlc3MvKmR3aXRoeDhkaWQ6a2V5Ono2TWtlWll4ekdmM0R6MTNlRVdxUGFpWnk1Y3pSM1VHSlQ2VkoyeGI5b3YzZm05MqJjY2FuamZpbGVjb2luLypkd2l0aHg4ZGlkOmtleTp6Nk1rZVpZeHpHZjNEejEzZUVXcVBhaVp5NWN6UjNVR0pUNlZKMnhiOW92M2ZtOTKiY2Nhbmd1c2FnZS8qZHdpdGh4OGRpZDprZXk6ejZNa2VaWXh6R2YzRHoxM2VFV3FQYWlaeTVjelIzVUdKVDZWSjJ4YjlvdjNmbTkyY2F1ZFgi7QH3cQ3zJXcjXOtAb50QYsB/B5rg6F5ANZqSnA3s+bd8ZWNleHD2Y2ZjdIGhZXNwYWNlomRuYW1lb3BhcGVyY2xpcC10YXNrc2ZhY2Nlc3OhZHR5cGVmcHVibGljY2lzc1gi7QHYWombyxiRvwslpEJzIJzZ8wCd8IBkCjmHADjJeH8bYGNwcmaB2CpYJQABcRIgdtUV762hfWAxnHPAsFGV/B2xNVWn8RSqEYiaHD+rXSVZAXESIGoRJryeu5AukMcnmHjLD2TAMLBMiUHnRrB7nXBtnHkAoWp1Y2FuQDAuOS4x2CpYJQABcRIggUxp27QPeiqho5Esms8ftreR0bpZAqdOObCkeyPMuLk",
14
14
  "W3UP_MESSAGES_SPACE_DID": "did:key:z6MkgcY3RmfgLbZj51XsJoEhsTavQDQiPjS24rWQQA8iKKy2",
15
15
  "W3UP_MESSAGES_SPACE_PROOF": "mAYIEAMIQOqJlcm9vdHOB2CpYJQABcRIgnDvyvwgikqlL2XzCW/JQ3BHpsRe8bFHN6SEzc6wOtjpndmVyc2lvbgHCBwFxEiDVB/+rAUCZdRYW6EXDptPhUgNIV6CRyFnqWujo7+/uGahhc1hE7aEDQJHrfoqa//GbFoOALOUrvYlGR+T2m7i9aa66PmdqgKGqjn2NGWm6RJVTary3fl31REKAEeTEOcPBqsdtsSge1AthdmUwLjkuMWNhdHSJomNjYW5oYXNzZXJ0Lypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTKiY2NhbmdzcGFjZS8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyomNjYW5mYmxvYi8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyomNjYW5naW5kZXgvKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuZ3N0b3JlLypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTKiY2Nhbmh1cGxvYWQvKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuaGFjY2Vzcy8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyomNjYW5qZmlsZWNvaW4vKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuZ3VzYWdlLypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTJjYXVkWCLtAdhaiZvLGJG/CyWkQnMgnNnzAJ3wgGQKOYcAOMl4fxtgY2V4cBprblVxY2ZjdIGhZXNwYWNlomRuYW1lcnBhcGVyY2xpcC1tZXNzYWdlc2ZhY2Nlc3OhZHR5cGVmcHVibGljY2lzc1gi7QEgF7Bv4SLxvgEurq71mo5LvqCxESOmpuc8faW0/E29uWNwcmaA5wcBcRIgtDaUBVGzv2+pJPPufePJ7dfIZKTjeqpAYa6Jjv5mtLeoYXNYRO2hA0AIvxSpZSSR9cbUg3BnCpFGejW6Mc1nJNWIHgF6qvmzz5FrmD9TZ1WDnliMpMMoNahYIJJUZreH+wpM8ZO08RgPYXZlMC45LjFjYXR0iaJjY2FuaGFzc2VydC8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyomNjYW5nc3BhY2UvKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuZmJsb2IvKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuZ2luZGV4Lypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTKiY2NhbmdzdG9yZS8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyomNjYW5odXBsb2FkLypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTKiY2NhbmhhY2Nlc3MvKmR3aXRoeDhkaWQ6a2V5Ono2TWtnY1kzUm1mZ0xiWmo1MVhzSm9FaHNUYXZRRFFpUGpTMjRyV1FRQThpS0t5MqJjY2FuamZpbGVjb2luLypkd2l0aHg4ZGlkOmtleTp6Nk1rZ2NZM1JtZmdMYlpqNTFYc0pvRWhzVGF2UURRaVBqUzI0cldRUUE4aUtLeTKiY2Nhbmd1c2FnZS8qZHdpdGh4OGRpZDprZXk6ejZNa2djWTNSbWZnTGJaajUxWHNKb0Voc1RhdlFEUWlQalMyNHJXUVFBOGlLS3kyY2F1ZFgi7QH3cQ3zJXcjXOtAb50QYsB/B5rg6F5ANZqSnA3s+bd8ZWNleHD2Y2ZjdIGhZXNwYWNlomRuYW1lcnBhcGVyY2xpcC1tZXNzYWdlc2ZhY2Nlc3OhZHR5cGVmcHVibGljY2lzc1gi7QHYWombyxiRvwslpEJzIJzZ8wCd8IBkCjmHADjJeH8bYGNwcmaB2CpYJQABcRIg1Qf/qwFAmXUWFuhFw6bT4VIDSFegkchZ6lro6O/v7hlZAXESIJw78r8IIpKpS9l8wlvyUNwR6bEXvGxRzekhM3OsDrY6oWp1Y2FuQDAuOS4x2CpYJQABcRIgtDaUBVGzv2+pJPPufePJ7dfIZKTjeqpAYa6Jjv5mtLc",
16
- "PRIVY_APP_ID": "cmlha1adg00skl20bil2gcwpd",
17
- "PRIVY_APP_SECRET": "privy_app_secret_4uAVDY9A4yppATqiCcWM3PoRWDn1G8qoiqJa8mdcs4jrpEBqykUHAjsrU4FnHA1qsKdTfzmS1h8bcqkvf4r6WgTG"
16
+ "PRIVY_APP_ID": "cmlk6zcmr004d0clavovxzstm",
17
+ "PRIVY_APP_SECRET": "privy_app_secret_2yBFLFTByan3gnDUmYQcjiE79iEoRU7K4GLMeaT9oG7VogdiUVL33JJzpcXt6Sf6a9Rmrik7ajRPEtbXsuRAQTzS"
18
18
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Chain Adapter Interface
3
+ *
4
+ * Defines the chain-agnostic API for all Paperclip Protocol operations.
5
+ * Both SolanaAdapter and EVMAdapter implement this interface.
6
+ */
7
+ export const BUILTIN_SERVERS = [
8
+ {
9
+ name: "solana-devnet",
10
+ chain: "solana",
11
+ rpcUrl: "https://devnet.helius-rpc.com/?api-key=4d93203f-a21c-40f1-88aa-7f8e61d5a7c9",
12
+ contractAddress: "Fehg9nbFCRnrZAuaW6tiqnegbHpHgizV9bvakhAWix6v",
13
+ label: "Solana Devnet",
14
+ },
15
+ {
16
+ name: "solana-localnet",
17
+ chain: "solana",
18
+ rpcUrl: "http://127.0.0.1:8899",
19
+ contractAddress: "Fehg9nbFCRnrZAuaW6tiqnegbHpHgizV9bvakhAWix6v",
20
+ label: "Solana Localnet",
21
+ },
22
+ {
23
+ name: "monad-testnet",
24
+ chain: "evm",
25
+ chainId: 10143,
26
+ rpcUrl: "https://testnet-rpc.monad.xyz",
27
+ contractAddress: "0x4e794d12625456fb3043c329215555de4d0e2841",
28
+ label: "Monad Testnet",
29
+ },
30
+ {
31
+ name: "evm-localnet",
32
+ chain: "evm",
33
+ chainId: 31337,
34
+ rpcUrl: "http://127.0.0.1:8545",
35
+ contractAddress: "", // Set via PAPERCLIP_EVM_CONTRACT_ADDRESS env var after deploying to Anvil
36
+ label: "EVM Localnet (Anvil)",
37
+ },
38
+ ];
39
+ export function getServerConfig(name) {
40
+ return BUILTIN_SERVERS.find((s) => s.name === name);
41
+ }
42
+ export function listServers() {
43
+ return BUILTIN_SERVERS;
44
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * EVM Adapter
3
+ *
4
+ * ethers.js-based implementation of ChainAdapter for EVM chains (Monad).
5
+ *
6
+ * Supports two wallet modes:
7
+ * - local: ethers.Wallet with private key from env var (default for localnet)
8
+ * - privy: Privy server wallet with gas sponsorship (for testnet/mainnet)
9
+ */
10
+ import { ethers } from "ethers";
11
+ import { WALLET_TYPE } from "./config.js";
12
+ import { provisionPrivyEvmWallet, getPersistedEvmWallet, sendSponsoredEvmTransaction, } from "./privy-evm.js";
13
+ // =========================================================================
14
+ // ABI — minimal ABI for the PaperclipProtocol contract
15
+ // Only includes the functions we need for CLI operations.
16
+ // =========================================================================
17
+ const PAPERCLIP_ABI = [
18
+ // View functions
19
+ "function initialized() view returns (bool)",
20
+ "function authority() view returns (address)",
21
+ "function baseRewardUnit() view returns (uint64)",
22
+ "function totalAgents() view returns (uint32)",
23
+ "function totalTasks() view returns (uint32)",
24
+ "function totalClipsDistributed() view returns (uint64)",
25
+ "function getAgent(address wallet) view returns (tuple(bool exists, uint64 clipsBalance, uint8 efficiencyTier, uint32 tasksCompleted, int64 registeredAt, int64 lastActiveAt, uint32 invitesSent, uint32 invitesRedeemed, address invitedBy))",
26
+ "function getTask(uint32 taskId) view returns (tuple(bool exists, uint32 taskId, address creator, string title, string contentCid, uint64 rewardClips, uint16 maxClaims, uint16 currentClaims, bool isActive, int64 createdAt, uint8 minTier, uint32 requiredTaskId))",
27
+ "function getClaim(uint32 taskId, address agent) view returns (tuple(bool exists, uint32 taskId, address agent, string proofCid, uint64 clipsAwarded, int64 completedAt))",
28
+ "function getInvite(address inviter) view returns (tuple(bool exists, address inviterWallet, uint32 invitesRedeemed, int64 createdAt, bool isActive))",
29
+ "function NO_PREREQ_TASK_ID() view returns (uint32)",
30
+ // Mutation functions
31
+ "function registerAgent()",
32
+ "function registerAgentWithInvite(address inviter)",
33
+ "function createInvite()",
34
+ "function submitProof(uint32 taskId, string proofCid)",
35
+ // Events (for task listing)
36
+ "event TaskCreated(uint32 indexed taskId, string title, uint64 rewardClips, uint16 maxClaims, uint8 minTier, uint32 requiredTaskId)",
37
+ "event AgentRegistered(address indexed agent, uint64 clipsBalance)",
38
+ "event ProofSubmitted(uint32 indexed taskId, address indexed agent, string proofCid, uint64 clipsAwarded)",
39
+ ];
40
+ const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
41
+ const NO_PREREQ_TASK_ID = 0xffffffff;
42
+ export class EVMAdapter {
43
+ chain = "evm";
44
+ serverName;
45
+ config;
46
+ provider;
47
+ signer = null;
48
+ readContract = null;
49
+ iface;
50
+ isPrivyMode;
51
+ constructor(config) {
52
+ this.config = config;
53
+ this.serverName = config.name;
54
+ this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
55
+ this.iface = new ethers.Interface(PAPERCLIP_ABI);
56
+ // Privy mode is only for non-localnet servers
57
+ this.isPrivyMode = WALLET_TYPE === "privy" && config.name !== "evm-localnet";
58
+ }
59
+ // =========================================================================
60
+ // Contract access — read-only for Privy mode, full signer for local mode
61
+ // =========================================================================
62
+ getReadContract() {
63
+ if (!this.readContract) {
64
+ this.ensureContractAddress();
65
+ this.readContract = new ethers.Contract(this.config.contractAddress, PAPERCLIP_ABI, this.isPrivyMode ? this.provider : this.getLocalSigner());
66
+ }
67
+ return this.readContract;
68
+ }
69
+ ensureContractAddress() {
70
+ if (!this.config.contractAddress) {
71
+ throw new Error(`No contract address configured for server "${this.serverName}". ` +
72
+ `Set PAPERCLIP_EVM_CONTRACT_ADDRESS env var or update server config.`);
73
+ }
74
+ }
75
+ getLocalSigner() {
76
+ if (!this.signer) {
77
+ const privateKey = process.env.PAPERCLIP_EVM_PRIVATE_KEY || process.env.EVM_PRIVATE_KEY || process.env.DEPLOYER_PRIVATE_KEY;
78
+ if (!privateKey) {
79
+ throw new Error("EVM private key not set. Set PAPERCLIP_EVM_PRIVATE_KEY environment variable.");
80
+ }
81
+ this.signer = new ethers.Wallet(privateKey, this.provider);
82
+ }
83
+ return this.signer;
84
+ }
85
+ // =========================================================================
86
+ // Wallet address
87
+ // =========================================================================
88
+ async getWalletAddress() {
89
+ if (this.isPrivyMode) {
90
+ try {
91
+ const wallet = getPersistedEvmWallet();
92
+ return wallet.address;
93
+ }
94
+ catch {
95
+ // No wallet provisioned yet — return zero address so read-only
96
+ // commands (tasks, status) can proceed and show "not registered"
97
+ return ZERO_ADDRESS;
98
+ }
99
+ }
100
+ return this.getLocalSigner().address;
101
+ }
102
+ // =========================================================================
103
+ // Protocol reads (always free — no gas, no Privy needed)
104
+ // =========================================================================
105
+ async getAgent(wallet) {
106
+ const contract = this.getReadContract();
107
+ const agent = await contract.getAgent(wallet);
108
+ if (!agent.exists)
109
+ return null;
110
+ return {
111
+ exists: true,
112
+ wallet,
113
+ clipsBalance: Number(agent.clipsBalance),
114
+ efficiencyTier: Number(agent.efficiencyTier),
115
+ tasksCompleted: Number(agent.tasksCompleted),
116
+ registeredAt: Number(agent.registeredAt),
117
+ lastActiveAt: Number(agent.lastActiveAt),
118
+ invitesSent: Number(agent.invitesSent),
119
+ invitesRedeemed: Number(agent.invitesRedeemed),
120
+ invitedBy: agent.invitedBy === ZERO_ADDRESS ? null : agent.invitedBy,
121
+ };
122
+ }
123
+ async getTask(taskId) {
124
+ const contract = this.getReadContract();
125
+ const task = await contract.getTask(taskId);
126
+ if (!task.exists)
127
+ return null;
128
+ return this.mapTask(task);
129
+ }
130
+ async getClaim(taskId, agent) {
131
+ const contract = this.getReadContract();
132
+ const claim = await contract.getClaim(taskId, agent);
133
+ if (!claim.exists)
134
+ return null;
135
+ return {
136
+ exists: true,
137
+ taskId: Number(claim.taskId),
138
+ agent: claim.agent,
139
+ proofCid: claim.proofCid,
140
+ clipsAwarded: Number(claim.clipsAwarded),
141
+ completedAt: Number(claim.completedAt),
142
+ };
143
+ }
144
+ async getInvite(inviter) {
145
+ const contract = this.getReadContract();
146
+ const invite = await contract.getInvite(inviter);
147
+ if (!invite.exists)
148
+ return null;
149
+ return {
150
+ exists: true,
151
+ inviterWallet: invite.inviterWallet,
152
+ invitesRedeemed: Number(invite.invitesRedeemed),
153
+ createdAt: Number(invite.createdAt),
154
+ isActive: invite.isActive,
155
+ };
156
+ }
157
+ async listActiveTasks() {
158
+ // Use totalTasks() to iterate through all tasks by ID
159
+ const contract = this.getReadContract();
160
+ const totalTasks = Number(await contract.totalTasks());
161
+ const tasks = [];
162
+ for (let taskId = 1; taskId <= totalTasks; taskId++) {
163
+ const task = await this.getTask(taskId);
164
+ if (task && task.isActive && task.currentClaims < task.maxClaims) {
165
+ tasks.push(task);
166
+ }
167
+ // Rate limit: 50ms delay between getTask calls to stay under Monad's 25/sec limit
168
+ if (taskId < totalTasks) {
169
+ await new Promise((resolve) => setTimeout(resolve, 50));
170
+ }
171
+ }
172
+ return tasks;
173
+ }
174
+ async listDoableTasks(agentWallet, agentTier) {
175
+ const allActive = await this.listActiveTasks();
176
+ if (allActive.length === 0)
177
+ return [];
178
+ const tierEligible = allActive.filter((t) => agentTier >= t.minTier);
179
+ if (tierEligible.length === 0)
180
+ return [];
181
+ // Check claims and prerequisites (with rate limiting for Monad RPC)
182
+ const doable = [];
183
+ for (let i = 0; i < tierEligible.length; i++) {
184
+ const task = tierEligible[i];
185
+ // Check if already claimed
186
+ const claim = await this.getClaim(task.taskId, agentWallet);
187
+ if (claim)
188
+ continue;
189
+ // Check prerequisite
190
+ if (task.requiredTaskId !== null) {
191
+ await new Promise((resolve) => setTimeout(resolve, 50));
192
+ const prereqClaim = await this.getClaim(task.requiredTaskId, agentWallet);
193
+ if (!prereqClaim)
194
+ continue;
195
+ }
196
+ doable.push(task);
197
+ // Rate limit: 50ms delay between getClaim calls to stay under 25/sec limit
198
+ if (i < tierEligible.length - 1) {
199
+ await new Promise((resolve) => setTimeout(resolve, 50));
200
+ }
201
+ }
202
+ return doable;
203
+ }
204
+ // =========================================================================
205
+ // Mutations — route through Privy or local signer
206
+ // =========================================================================
207
+ async registerAgent() {
208
+ await this.sendMutation("registerAgent", []);
209
+ const wallet = await this.getWalletAddress();
210
+ const agent = await this.getAgent(wallet);
211
+ return {
212
+ wallet,
213
+ clipsBalance: agent.clipsBalance,
214
+ invitedBy: agent.invitedBy,
215
+ };
216
+ }
217
+ async registerAgentWithInvite(inviteCode) {
218
+ // On EVM, the invite code is the inviter's address
219
+ await this.sendMutation("registerAgentWithInvite", [inviteCode]);
220
+ const wallet = await this.getWalletAddress();
221
+ const agent = await this.getAgent(wallet);
222
+ return {
223
+ wallet,
224
+ clipsBalance: agent.clipsBalance,
225
+ invitedBy: agent.invitedBy,
226
+ };
227
+ }
228
+ async createInvite() {
229
+ const wallet = await this.getWalletAddress();
230
+ // Check if invite already exists
231
+ const existing = await this.getInvite(wallet);
232
+ if (existing) {
233
+ return {
234
+ inviteCode: existing.inviterWallet,
235
+ invitesRedeemed: existing.invitesRedeemed,
236
+ };
237
+ }
238
+ await this.sendMutation("createInvite", []);
239
+ const invite = await this.getInvite(wallet);
240
+ return {
241
+ inviteCode: invite.inviterWallet,
242
+ invitesRedeemed: invite.invitesRedeemed,
243
+ };
244
+ }
245
+ async submitProof(taskId, proofCid) {
246
+ const task = await this.getTask(taskId);
247
+ if (!task)
248
+ throw new Error(`Task ${taskId} not found`);
249
+ await this.sendMutation("submitProof", [taskId, proofCid]);
250
+ return { clipsAwarded: task.rewardClips };
251
+ }
252
+ // =========================================================================
253
+ // Privy wallet provisioning
254
+ // =========================================================================
255
+ async provisionWallet() {
256
+ if (this.isPrivyMode) {
257
+ await provisionPrivyEvmWallet();
258
+ }
259
+ }
260
+ // =========================================================================
261
+ // Private helpers
262
+ // =========================================================================
263
+ /**
264
+ * Central mutation dispatcher.
265
+ * - In Privy mode: encode calldata, send via Privy REST API with gas sponsorship
266
+ * - In local mode: send directly via ethers.js signer
267
+ */
268
+ async sendMutation(functionName, args) {
269
+ if (this.isPrivyMode) {
270
+ return this.sendViaPrivy(functionName, args);
271
+ }
272
+ return this.sendViaLocal(functionName, args);
273
+ }
274
+ async sendViaPrivy(functionName, args) {
275
+ this.ensureContractAddress();
276
+ const wallet = getPersistedEvmWallet();
277
+ const data = this.iface.encodeFunctionData(functionName, args);
278
+ const hash = await sendSponsoredEvmTransaction(wallet.id, this.config.chainId, { to: this.config.contractAddress, data });
279
+ // Wait for confirmation
280
+ const receipt = await this.provider.waitForTransaction(hash, 1, 60_000);
281
+ if (!receipt || receipt.status === 0) {
282
+ throw new Error(`Transaction ${hash} failed on-chain`);
283
+ }
284
+ return hash;
285
+ }
286
+ async sendViaLocal(functionName, args) {
287
+ const contract = this.getReadContract();
288
+ const tx = await contract[functionName](...args);
289
+ const receipt = await tx.wait();
290
+ return receipt.hash;
291
+ }
292
+ mapTask(task) {
293
+ const reqTaskId = Number(task.requiredTaskId);
294
+ return {
295
+ taskId: Number(task.taskId),
296
+ creator: task.creator,
297
+ title: task.title,
298
+ contentCid: task.contentCid,
299
+ rewardClips: Number(task.rewardClips),
300
+ maxClaims: Number(task.maxClaims),
301
+ currentClaims: Number(task.currentClaims),
302
+ isActive: task.isActive,
303
+ createdAt: Number(task.createdAt),
304
+ minTier: Number(task.minTier),
305
+ requiredTaskId: reqTaskId === NO_PREREQ_TASK_ID ? null : reqTaskId,
306
+ };
307
+ }
308
+ }