@paper-clip/pc 0.1.6 → 0.1.7
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 +28 -9
- package/baked-config.json +2 -2
- package/dist/chain-adapter.js +44 -0
- package/dist/evm-adapter.js +298 -0
- package/dist/index.js +269 -282
- package/dist/privy-evm.js +113 -0
- package/dist/privy.js +2 -2
- package/dist/settings.js +8 -0
- package/dist/solana-adapter.js +299 -0
- package/package.json +2 -1
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
|
-
|
|
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
|
|
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": "
|
|
17
|
-
"PRIVY_APP_SECRET": "
|
|
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,298 @@
|
|
|
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
|
+
}
|
|
168
|
+
return tasks;
|
|
169
|
+
}
|
|
170
|
+
async listDoableTasks(agentWallet, agentTier) {
|
|
171
|
+
const allActive = await this.listActiveTasks();
|
|
172
|
+
if (allActive.length === 0)
|
|
173
|
+
return [];
|
|
174
|
+
const tierEligible = allActive.filter((t) => agentTier >= t.minTier);
|
|
175
|
+
if (tierEligible.length === 0)
|
|
176
|
+
return [];
|
|
177
|
+
// Check claims and prerequisites
|
|
178
|
+
const doable = [];
|
|
179
|
+
for (const task of tierEligible) {
|
|
180
|
+
// Check if already claimed
|
|
181
|
+
const claim = await this.getClaim(task.taskId, agentWallet);
|
|
182
|
+
if (claim)
|
|
183
|
+
continue;
|
|
184
|
+
// Check prerequisite
|
|
185
|
+
if (task.requiredTaskId !== null) {
|
|
186
|
+
const prereqClaim = await this.getClaim(task.requiredTaskId, agentWallet);
|
|
187
|
+
if (!prereqClaim)
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
doable.push(task);
|
|
191
|
+
}
|
|
192
|
+
return doable;
|
|
193
|
+
}
|
|
194
|
+
// =========================================================================
|
|
195
|
+
// Mutations — route through Privy or local signer
|
|
196
|
+
// =========================================================================
|
|
197
|
+
async registerAgent() {
|
|
198
|
+
await this.sendMutation("registerAgent", []);
|
|
199
|
+
const wallet = await this.getWalletAddress();
|
|
200
|
+
const agent = await this.getAgent(wallet);
|
|
201
|
+
return {
|
|
202
|
+
wallet,
|
|
203
|
+
clipsBalance: agent.clipsBalance,
|
|
204
|
+
invitedBy: agent.invitedBy,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
async registerAgentWithInvite(inviteCode) {
|
|
208
|
+
// On EVM, the invite code is the inviter's address
|
|
209
|
+
await this.sendMutation("registerAgentWithInvite", [inviteCode]);
|
|
210
|
+
const wallet = await this.getWalletAddress();
|
|
211
|
+
const agent = await this.getAgent(wallet);
|
|
212
|
+
return {
|
|
213
|
+
wallet,
|
|
214
|
+
clipsBalance: agent.clipsBalance,
|
|
215
|
+
invitedBy: agent.invitedBy,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async createInvite() {
|
|
219
|
+
const wallet = await this.getWalletAddress();
|
|
220
|
+
// Check if invite already exists
|
|
221
|
+
const existing = await this.getInvite(wallet);
|
|
222
|
+
if (existing) {
|
|
223
|
+
return {
|
|
224
|
+
inviteCode: existing.inviterWallet,
|
|
225
|
+
invitesRedeemed: existing.invitesRedeemed,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
await this.sendMutation("createInvite", []);
|
|
229
|
+
const invite = await this.getInvite(wallet);
|
|
230
|
+
return {
|
|
231
|
+
inviteCode: invite.inviterWallet,
|
|
232
|
+
invitesRedeemed: invite.invitesRedeemed,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async submitProof(taskId, proofCid) {
|
|
236
|
+
const task = await this.getTask(taskId);
|
|
237
|
+
if (!task)
|
|
238
|
+
throw new Error(`Task ${taskId} not found`);
|
|
239
|
+
await this.sendMutation("submitProof", [taskId, proofCid]);
|
|
240
|
+
return { clipsAwarded: task.rewardClips };
|
|
241
|
+
}
|
|
242
|
+
// =========================================================================
|
|
243
|
+
// Privy wallet provisioning
|
|
244
|
+
// =========================================================================
|
|
245
|
+
async provisionWallet() {
|
|
246
|
+
if (this.isPrivyMode) {
|
|
247
|
+
await provisionPrivyEvmWallet();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// =========================================================================
|
|
251
|
+
// Private helpers
|
|
252
|
+
// =========================================================================
|
|
253
|
+
/**
|
|
254
|
+
* Central mutation dispatcher.
|
|
255
|
+
* - In Privy mode: encode calldata, send via Privy REST API with gas sponsorship
|
|
256
|
+
* - In local mode: send directly via ethers.js signer
|
|
257
|
+
*/
|
|
258
|
+
async sendMutation(functionName, args) {
|
|
259
|
+
if (this.isPrivyMode) {
|
|
260
|
+
return this.sendViaPrivy(functionName, args);
|
|
261
|
+
}
|
|
262
|
+
return this.sendViaLocal(functionName, args);
|
|
263
|
+
}
|
|
264
|
+
async sendViaPrivy(functionName, args) {
|
|
265
|
+
this.ensureContractAddress();
|
|
266
|
+
const wallet = getPersistedEvmWallet();
|
|
267
|
+
const data = this.iface.encodeFunctionData(functionName, args);
|
|
268
|
+
const hash = await sendSponsoredEvmTransaction(wallet.id, this.config.chainId, { to: this.config.contractAddress, data });
|
|
269
|
+
// Wait for confirmation
|
|
270
|
+
const receipt = await this.provider.waitForTransaction(hash, 1, 60_000);
|
|
271
|
+
if (!receipt || receipt.status === 0) {
|
|
272
|
+
throw new Error(`Transaction ${hash} failed on-chain`);
|
|
273
|
+
}
|
|
274
|
+
return hash;
|
|
275
|
+
}
|
|
276
|
+
async sendViaLocal(functionName, args) {
|
|
277
|
+
const contract = this.getReadContract();
|
|
278
|
+
const tx = await contract[functionName](...args);
|
|
279
|
+
const receipt = await tx.wait();
|
|
280
|
+
return receipt.hash;
|
|
281
|
+
}
|
|
282
|
+
mapTask(task) {
|
|
283
|
+
const reqTaskId = Number(task.requiredTaskId);
|
|
284
|
+
return {
|
|
285
|
+
taskId: Number(task.taskId),
|
|
286
|
+
creator: task.creator,
|
|
287
|
+
title: task.title,
|
|
288
|
+
contentCid: task.contentCid,
|
|
289
|
+
rewardClips: Number(task.rewardClips),
|
|
290
|
+
maxClaims: Number(task.maxClaims),
|
|
291
|
+
currentClaims: Number(task.currentClaims),
|
|
292
|
+
isActive: task.isActive,
|
|
293
|
+
createdAt: Number(task.createdAt),
|
|
294
|
+
minTier: Number(task.minTier),
|
|
295
|
+
requiredTaskId: reqTaskId === NO_PREREQ_TASK_ID ? null : reqTaskId,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|