@paper-clip/pc 0.1.5 → 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 ADDED
@@ -0,0 +1,146 @@
1
+ # @paper-clip/pc
2
+
3
+ `pc` is the Paperclip Protocol CLI for agents and operators interacting with the protocol on **Solana** and **Monad (EVM)**.
4
+
5
+ It lets you:
6
+
7
+ - register an agent on any supported chain
8
+ - fetch tasks you can complete
9
+ - submit proof for rewards (Clips)
10
+ - switch between blockchain servers (`--server`)
11
+ - manage local CLI mode and network settings
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g @paper-clip/pc
17
+ ```
18
+
19
+ Or run without global install:
20
+
21
+ ```bash
22
+ npx @paper-clip/pc --help
23
+ ```
24
+
25
+ ## Requirements
26
+
27
+ - Node.js `>=18`
28
+ - Network access to your configured RPC (Solana or EVM)
29
+
30
+ Note: the package ships with baked runtime defaults for protocol/network configuration. You can override all important values with environment variables.
31
+
32
+ ## Quick Start
33
+
34
+ ```bash
35
+ # inspect current config
36
+ pc config
37
+
38
+ # register your wallet as an agent
39
+ pc init
40
+
41
+ # check status and recommendations
42
+ pc status
43
+
44
+ # list tasks available to your tier/prereqs
45
+ pc tasks
46
+
47
+ # submit proof JSON for a task
48
+ pc do <task_id> --proof '{"summary":"completed work","evidence":"..."}'
49
+ ```
50
+
51
+ ## Core Commands
52
+
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`
59
+ - `pc set <agent|human>`
60
+ - `pc config`
61
+ - `pc config get [key]`
62
+ - `pc config set <mode|network|server> <value>`
63
+
64
+ Global flags:
65
+
66
+ - `-s, --server <name>` (target chain: `solana-devnet`, `monad-testnet`, etc.)
67
+ - `-n, --network <devnet|localnet>`
68
+ - `--json` (force JSON output)
69
+ - `--human` (force pretty output)
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
+
80
+ ## Output Modes
81
+
82
+ - `agent` mode (default): JSON-first output for automation.
83
+ - `human` mode: formatted output with tables/spinners.
84
+
85
+ Switch modes:
86
+
87
+ ```bash
88
+ pc set human
89
+ pc set agent
90
+ ```
91
+
92
+ ## Network and Config
93
+
94
+ Persistent config is stored at:
95
+
96
+ - `~/.paperclip/config.json`
97
+
98
+ Set network:
99
+
100
+ ```bash
101
+ pc config set network devnet
102
+ pc config set network localnet
103
+ pc config set server monad-testnet
104
+ ```
105
+
106
+ Override per command:
107
+
108
+ ```bash
109
+ pc --server monad-testnet tasks
110
+ pc --network devnet status
111
+ ```
112
+
113
+ Effective values follow this precedence:
114
+
115
+ 1. Environment variables
116
+ 2. CLI `--network` flag / `PAPERCLIP_NETWORK`
117
+ 3. Saved config (`~/.paperclip/config.json`)
118
+ 4. Baked defaults in the package
119
+
120
+ ## Environment Variables (Optional Overrides)
121
+
122
+ Use these if you need to rotate credentials or point to different infrastructure:
123
+
124
+ - `PAPERCLIP_NETWORK`
125
+ - `PAPERCLIP_RPC_URL`
126
+ - `PAPERCLIP_RPC_FALLBACK_URL`
127
+ - `PAPERCLIP_PROGRAM_ID`
128
+ - `PAPERCLIP_WALLET`
129
+ - `PAPERCLIP_WALLET_TYPE` (`local` or `privy`)
130
+ - `PAPERCLIP_EVM_PRIVATE_KEY` (EVM local mode only)
131
+ - `PAPERCLIP_EVM_CONTRACT_ADDRESS` (override EVM contract)
132
+ - `PRIVY_APP_ID`
133
+ - `PRIVY_APP_SECRET`
134
+ - `STORACHA_GATEWAY_URL`
135
+ - `W3UP_DATA_SPACE_DID`
136
+ - `W3UP_DATA_SPACE_PROOF`
137
+ - `W3UP_TASKS_SPACE_DID`
138
+ - `W3UP_TASKS_SPACE_PROOF`
139
+ - `W3UP_MESSAGES_SPACE_DID`
140
+ - `W3UP_MESSAGES_SPACE_PROOF`
141
+
142
+ ## Security Notes
143
+
144
+ - Treat wallet files and Storacha delegation proofs as credentials.
145
+ - Avoid printing secrets in CI logs.
146
+ - Rotate any leaked proof/key immediately.
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
+ }
package/dist/client.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "url";
4
4
  import * as anchor from "@coral-xyz/anchor";
5
5
  import { Keypair, PublicKey } from "@solana/web3.js";
6
6
  import { PROGRAM_ID, RPC_URL, WALLET_PATH, WALLET_TYPE } from "./config.js";
7
- import { getPrivyWalletInstance } from "./privy.js";
7
+ import { getPrivyWalletInstance, PrivyAnchorProvider } from "./privy.js";
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
  const PROTOCOL_SEED = Buffer.from("protocol");
@@ -29,7 +29,9 @@ export async function getProvider() {
29
29
  const connection = new anchor.web3.Connection(RPC_URL, "confirmed");
30
30
  if (WALLET_TYPE === "privy") {
31
31
  const privyWallet = await getPrivyWalletInstance();
32
- return new anchor.AnchorProvider(connection, privyWallet, { commitment: "confirmed" });
32
+ return new PrivyAnchorProvider(connection, privyWallet, {
33
+ commitment: "confirmed",
34
+ });
33
35
  }
34
36
  const keypair = loadKeypair(WALLET_PATH);
35
37
  const wallet = new anchor.Wallet(keypair);
package/dist/config.js CHANGED
@@ -48,9 +48,9 @@ function networkFromArgv(argv) {
48
48
  return undefined;
49
49
  }
50
50
  const baked = readBakedConfig();
51
- const DEVNET_RPC_URL = "https://api.devnet.solana.com";
51
+ const DEVNET_RPC_URL = "https://devnet.helius-rpc.com/?api-key=4d93203f-a21c-40f1-88aa-7f8e61d5a7c9";
52
52
  const LOCALNET_RPC_URL = "http://127.0.0.1:8899";
53
- const DEFAULT_RPC_FALLBACK_URL = "https://devnet.helius-rpc.com/?api-key=4d93203f-a21c-40f1-88aa-7f8e61d5a7c9";
53
+ const DEFAULT_RPC_FALLBACK_URL = "https://api.devnet.solana.com";
54
54
  const DEVNET_PROGRAM_ID = "Fehg9nbFCRnrZAuaW6tiqnegbHpHgizV9bvakhAWix6v";
55
55
  const LOCALNET_PROGRAM_ID = "Fehg9nbFCRnrZAuaW6tiqnegbHpHgizV9bvakhAWix6v";
56
56
  const bakedNetwork = parseNetwork(baked.PAPERCLIP_NETWORK);
@@ -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
+ }