@projectsolo/solo-mission-mcp 0.21.14 → 0.22.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.
Files changed (33) hide show
  1. package/README.md +96 -12
  2. package/dist/chunk-NUGGTARW.js +196 -0
  3. package/dist/{chunk-TKRT2V2W.js → chunk-OP4QYTL4.js} +5 -0
  4. package/dist/{client-LDWK5HLP.js → client-NS6J6IO2.js} +1 -1
  5. package/dist/deployment-UCUWVIPJ.js +43 -0
  6. package/dist/escrowErrors-4XSAYLGT.js +56 -0
  7. package/dist/index.js +239 -160
  8. package/dist/verify-7EUVRUAS.js +445 -0
  9. package/dist/{wallet-V4T4NTCM.js → wallet-LX7SI5RE.js} +28 -14
  10. package/dist/wire-RLDY4AJR.js +24 -0
  11. package/package.json +1 -1
  12. package/src/config.ts +11 -0
  13. package/src/index.ts +1 -1
  14. package/src/scripts/check-tools-against-spec.ts +3 -3
  15. package/src/solana/deployment.test.ts +61 -0
  16. package/src/solana/deployment.ts +89 -0
  17. package/src/solana/escrowErrors.ts +78 -0
  18. package/src/solana/fixtures/config-account.json +6 -0
  19. package/src/solana/fixtures/funding-transaction-v2-lottery.json +49 -0
  20. package/src/solana/fixtures/funding-transaction-v2-plain.json +46 -0
  21. package/src/solana/fixtures/solo_escrow.v2.idl-excerpt.json +683 -0
  22. package/src/solana/verify.test.ts +605 -78
  23. package/src/solana/verify.ts +545 -84
  24. package/src/solana/wallet.test.ts +85 -2
  25. package/src/solana/wallet.ts +74 -31
  26. package/src/solana/wire.test.ts +129 -0
  27. package/src/solana/wire.ts +264 -0
  28. package/src/tools/missions.ts +45 -117
  29. package/src/tools/solana.test.ts +356 -0
  30. package/src/tools/solana.ts +309 -70
  31. package/src/tools/tracks.ts +1 -1
  32. package/dist/verify-KAETIGV5.js +0 -136
  33. /package/src/solana/fixtures/{funding-transaction.json → funding-transaction-v1.json} +0 -0
package/README.md CHANGED
@@ -11,7 +11,6 @@ MCP server that lets AI agents interact with the [Solo Mission Platform](https:/
11
11
  - [Getting an Agent Key](#getting-an-agent-key)
12
12
  - [Setup (Claude Desktop / Cursor / Windsurf)](#setup-claude-desktop--cursor--windsurf)
13
13
  - [Tools Reference](#tools-reference)
14
- - [Conversations (6 tools)](#conversations-6-tools)
15
14
  - [Rate Limits](#rate-limits)
16
15
 
17
16
  ---
@@ -142,20 +141,47 @@ Restart the client. No build step or clone required — npx downloads and runs t
142
141
  | `SOLO_AGENT_KEY` | *(required)* | Your agent API key |
143
142
  | `SOLO_MISSION_API_URL` | `https://api.mission.projectsolo.ai` | Override for local/staging |
144
143
 
144
+ ### Solana wallet (paid missions only)
145
+
146
+ Free off-chain missions need no wallet. To fund or refund a paid mission, point the server at a keypair **file**. The key signs locally and is never sent to the Solo API.
147
+
148
+ | Variable | Description |
149
+ |---|---|
150
+ | `SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH` + `SOLO_SOLANA_KEYPAIR_PASSWORD_FILE` | **Recommended.** A keyfile encrypted with `openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -md sha256`, plus a separate passphrase file. `chmod 600` both. |
151
+ | `SOLO_SOLANA_KEYPAIR_PATH` | A plaintext `solana-keygen` keyfile, e.g. `~/.config/solana/id.json` (`chmod 600`). |
152
+ | `SOLO_SOLANA_RPC_URL` | Optional. The RPC endpoint for the reads the server checks itself: mint decimals, the escrow `Config` a lottery co-signer is checked against, and balances. Defaults to the pinned cluster's public endpoint (`https://api.devnet.solana.com` on devnet). The API's `rpc_url` is never used for reads that decide what to sign, so a cluster with no pinned endpoint needs this. |
153
+ | `SOLO_SOLANA_PROGRAM_ID` | Optional. The escrow program id to trust. The server pins the devnet program (`2CPC5V63FDs7SdWu89iSYYsTEpqBwuQeYuA9ASzuSo8a`) itself. Set this only for a deployment it doesn't know yet, together with `SOLO_SOLANA_RPC_URL`; without them, funding and refunds are refused there. |
154
+
155
+ `SOLO_SOLANA_KEYPAIR` (the raw key in an env var) is **no longer supported**. If it is set, the Solana tools refuse to run and tell you to switch to one of the file forms above.
156
+
157
+ **Where to keep the key.** Store the keyfile and the passphrase in your secret manager and have it write the files when the MCP server starts. For example: 1Password (`op inject`, `op read --out-file`), a HashiCorp Vault Agent template, GCP Secret Manager (`gcloud secrets versions access`), or the macOS Keychain (`security find-generic-password -w`). Never paste the key or the passphrase into chat, a command line or shell history. Remote signing (KMS/HSM) is not supported yet.
158
+
145
159
  ---
146
160
 
147
161
  ## Tools Reference
148
162
 
149
- ### Missions (6 tools)
163
+ 34 tools in total.
164
+
165
+ ### Agent (1 tool)
166
+
167
+ | Tool | Description |
168
+ |---|---|
169
+ | `register_agent` | Self-register a new agent; no existing key needed. Returns `agent_id` and `api_key` (shown only once) |
170
+
171
+ ### Missions (10 tools)
150
172
 
151
173
  | Tool | Description |
152
174
  |---|---|
153
- | `create_mission` | Create a new mission (title ≤100 chars, description ≤2000 chars, **Markdown supported**; off-chain missions are always free — use `budget` + `chain: 'solana'` for paid on-chain escrow. Base is closed to new creation during the Solana private beta migration — a budget-bearing request without `chain: 'solana'` now returns 503) |
175
+ | `create_mission` | Create a new mission (title ≤100 chars, description ≤2000 chars, **Markdown supported**; off-chain missions are always free — set `budget` for a paid mission escrowed on Solana, then fund it with `fund_solana_mission`) |
176
+ | `update_mission_questions` | Set or replace a mission's `response_schema` before hiring starts |
154
177
  | `list_missions` | List your agent's missions — supports `status`, `page`, `limit` |
155
- | `get_mission` | Get details of a specific mission by ID |
156
- | `complete_mission` | Mark a mission as completed |
157
- | `cancel_mission` | Cancel an active mission |
158
- | `rate_participant` | Rate a participant (1–5 stars) with optional feedback |
178
+ | `get_mission` | Get details of a specific mission by ID, including participants and `settlement_outcome` |
179
+ | `hire_participant` | Hire an applicant (unreviewed applicants are auto-hired later unless rejected) |
180
+ | `reject_participant` | Reject an applicant or hired participant — the only way to exclude someone |
181
+ | `finalize_qualification` | Lock in the qualified participants (auto-derived when the mission has a `response_schema`) |
182
+ | `settle_mission` | Settle after finalize; pays out on-chain missions via the Solana escrow |
183
+ | `cancel_mission` | Cancel a mission with nothing escrowed: an active off-chain mission, or an unfunded Solana mission in `pending_funding` (for a funded mission, or when this returns 409 with a `task_id`, use `refund_solana_mission` with `action: 'cancel'`) |
184
+ | `rate_participant` | Rate a participant (1–5 stars) with optional feedback, within 7 days of settlement |
159
185
 
160
186
  ### Humans (2 tools)
161
187
 
@@ -164,7 +190,7 @@ Restart the client. No build step or clone required — npx downloads and runs t
164
190
  | `browse_humans` | Browse face-verified humans — filter by `skills`, `location`, `languages`, `min_rating`, `max_hourly_rate`; supports `page`. Each result includes `uid` (stable Firebase identifier, use this for `start_conversation`) and `user_id` (mutable display name, for `get_human_profile` only) |
165
191
  | `get_human_profile` | Get a human's full profile by `user_id` |
166
192
 
167
- ### Conversations (6 tools)
193
+ ### Conversations (7 tools)
168
194
 
169
195
  | Tool | Description |
170
196
  |---|---|
@@ -174,20 +200,78 @@ Restart the client. No build step or clone required — npx downloads and runs t
174
200
  | `upload_conversation_image` | Upload an image from base64 data; returns `storage_path` for use in `send_message` `attachment_paths`. Use this so the agent can send images in one step. |
175
201
  | `send_message` | Send a message (text and/or images). Use `attachment_paths` for images after `upload_conversation_image` (or `get_conversation_upload_url` + PUT). At least one of `content` or `attachment_paths` required; max 4 attachments per message. |
176
202
  | `get_messages` | Fetch messages; use `since` (ISO 8601) to poll for new ones. Messages may include `attachment_urls` (signed read URLs for images). |
203
+ | `close_conversation` | `archive` (reopenable), `close` (permanent) or `reopen` a conversation |
177
204
 
178
205
  **Attachments (agents):** To send an image, call `upload_conversation_image` with `conversation_id` and `image_base64` (and optional `content_type`). Use the returned `storage_path` in `send_message` with `attachment_paths`. For custom clients that do their own PUT, use `get_conversation_upload_url` instead. The API stores 7-day signed read URLs in the message (GCS max).
179
206
 
180
- ### Real-time (3 tools)
207
+ ### Real-time (6 tools)
181
208
 
182
209
  | Tool | Description |
183
210
  |---|---|
184
- | `watch_conversation` | Start background polling (every 5s) for new messages |
211
+ | `watch_conversation` | Start background polling for new messages (backs off from 1s to 600s, resets when the human replies) |
185
212
  | `get_pending_messages` | Drain the message buffer for a watched conversation |
186
213
  | `unwatch_conversation` | Stop polling and discard the buffer |
214
+ | `watch_mission` | Start polling a mission for new participants (`interval_minutes`, default 10) |
215
+ | `get_pending_mission_updates` | Drain buffered new-participant updates, each with its `conversation_id` once the human has started one |
216
+ | `unwatch_mission` | Stop polling a mission |
217
+
218
+ **How it works:** MCP servers run as a persistent process for the duration of the client session (stdio transport). `watch_conversation` and `watch_mission` start in-process timers that poll the API and buffer what they find in memory. The agent calls `get_pending_messages` / `get_pending_mission_updates` whenever it wants to drain the queue.
219
+
220
+ **Limitation:** The buffers are in-memory only. If the session ends (client closes, process crashes), unread buffered messages are lost and watches stop. To recover, call `get_messages` with a manual `since` timestamp and re-watch.
221
+
222
+ ### Media tracks (4 tools, `media_review` missions)
223
+
224
+ | Tool | Description |
225
+ |---|---|
226
+ | `add_mission_track` | Upload an audio, image or video track, via `file_path` (use this for audio/video) or `file_base64` |
227
+ | `list_mission_tracks` | List a mission's tracks with `vote_counts` and `total_listen_seconds` |
228
+ | `get_track_ratings` | Per-participant star ratings and comments for one track |
229
+ | `delete_mission_track` | Delete a track that has no ratings yet |
230
+
231
+ ### Solana (4 tools)
232
+
233
+ | Tool | Description |
234
+ |---|---|
235
+ | `get_solana_config` | Program id, cluster, RPC, accepted mints and decimals, and `escrow_interface` (this server verifies only `v2`). No wallet needed |
236
+ | `get_solana_wallet` | Your wallet address, SOL balance and token balance; explains setup if no wallet is configured |
237
+ | `fund_solana_mission` | Build, verify, sign locally and submit the escrow funding transaction. Checks `max_fee_bps` against your quote and, for a lottery, the Operator's co-signature; refuses to sign on any mismatch |
238
+ | `refund_solana_mission` | `cancel` (before `qualify_deadline` only), `emergency_refund` (after `settlement_deadline`) or `claim_refund`, with the same verify-then-sign flow; omit `action` to list what is available. Says whether the backend or the chain refused, and why |
239
+
240
+ ### Solana escrow rules (escrow interface v2)
241
+
242
+ The Solana escrow program on devnet runs [solo-escrow-solana](https://github.com/ProjectSoloAI/solo-escrow-solana) `u1.1.0`, and `get_solana_config` reports `escrow_interface: "v2"`. This server verifies only v2 funding transactions and refuses to fund against any other interface.
243
+
244
+ **What `fund_solana_mission` checks before it signs.** The backend builds the funding transaction and this server decodes it. It refuses to sign if any of the following doesn't hold:
245
+
246
+ - The transaction has exactly one instruction, `create_task`, on the pinned program id. The escrow program id is pinned in this package per cluster, or by `SOLO_SOLANA_PROGRAM_ID`, and the `program_id` the API reports must equal it: the API is the party being checked, so it can't also choose the program.
247
+ - `expected_budget` is scaled by the mint's decimals, which are read from the mint account on chain rather than from the API.
248
+ - Every argument matches what the mission recorded and you expect: budget, base pool, lottery winners and prize, both deadlines, and the seed commitment.
249
+ - Every account is in its IDL position. The config, whitelist, task, vault and event-authority accounts are addresses the server derives itself; your token account must be the one you sent.
250
+ - `max_fee_bps` is the fee rate `create_mission` quoted you (`mission.solana_quoted_fee_bps`, or `expected_max_fee_bps` if you pass it). It is the most the escrow may charge: if the program's fee has risen above it when the transaction lands, `create_task` fails with `FeeAboveSponsorMax` instead of charging you more. Cancel the mission and create a new one to get a fresh quote.
251
+ - **Lotteries are co-signed by the platform Operator.** The program requires the Operator to sign every lottery `create_task`, so a lottery's funding transaction arrives already signed by it. The server checks three things:
252
+ - the co-signer is `Config.operator`, read straight from the cluster rather than from the API;
253
+ - its ed25519 signature is valid over the exact message;
254
+ - you, not the Operator, are the fee payer.
255
+
256
+ It then writes only your own signature slot and leaves the Operator's signature untouched. Any change to the bytes would invalidate that signature.
257
+ - A non-lottery transaction passes the program id in the Operator slot (Anchor's "none") and has no other signer.
258
+
259
+ **Deadlines.** The end of the hiring window is the escrow's `qualify_deadline`, and the end of the work window is `settlement_deadline`. For a paid mission:
260
+
261
+ - `settlement_deadline ≥ qualify_deadline + min_review_window + finalize grace`. `min_review_window` is the program's configured review window. The finalize grace equals the review-window floor: 10 s each on the current devnet build, while the floor is 1 hour on a production build.
262
+ - `qualify_deadline` is at most 180 days ahead.
263
+ - `settlement_deadline` is at most 90 days after `qualify_deadline`.
264
+
265
+ `create_mission` returns 400 for a violation, and the program enforces the same rules again when the escrow is funded.
266
+
267
+ **Cancel cut-off.** `refund_solana_mission` with `action: 'cancel'` returns the whole budget, but only strictly before `qualify_deadline`. From then on, the program refuses a cancel with `TooLateToCancel` (error 6125), including one built before the deadline that lands after it. The mission then settles normally, or you can use `emergency_refund` once `settlement_deadline` has passed without a settlement. A pause doesn't block `emergency_refund`.
268
+
269
+ **The lottery draw.** At settlement the program computes `final_entropy = keccak256(seed_reveal ‖ entropy_hash)`:
187
270
 
188
- **How it works:** MCP servers run as a persistent process for the duration of the client session (stdio transport). `watch_conversation` starts an in-process `setInterval` that polls the API every 5 seconds and buffers new messages in memory. The agent calls `get_pending_messages` whenever it wants to drain the queue.
271
+ - `seed_reveal` opens the `seed_commit` fixed at funding.
272
+ - `entropy_hash` is the SlotHashes sysvar's bank hash for the first produced slot after `finalize_qualification`. `record_entropy` stores it on the Task, or `settle_task` captures it if nobody recorded it in time.
189
273
 
190
- **Limitation:** The message buffer is in-memory only. If the session ends (client closes, process crashes), unread buffered messages are lost. To recover, call `get_messages` with a manual `since` timestamp.
274
+ `entropy_hash` is **not** the blockhash that `getBlock` returns for that slot. To check a draw, recompute it from the Task account's `entropy_slot`, `entropy_hash` and `final_entropy`, not from `getBlock`.
191
275
 
192
276
  ---
193
277
 
@@ -0,0 +1,196 @@
1
+ // src/solana/wire.ts
2
+ import { createPrivateKey, createPublicKey, sign, verify } from "crypto";
3
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
4
+ function base58Encode(bytes) {
5
+ let zeros = 0;
6
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
7
+ const digits = [];
8
+ for (let i = zeros; i < bytes.length; i++) {
9
+ let carry = bytes[i];
10
+ for (let j = 0; j < digits.length; j++) {
11
+ carry += digits[j] << 8;
12
+ digits[j] = carry % 58;
13
+ carry = carry / 58 | 0;
14
+ }
15
+ while (carry > 0) {
16
+ digits.push(carry % 58);
17
+ carry = carry / 58 | 0;
18
+ }
19
+ }
20
+ let out = "1".repeat(zeros);
21
+ for (let i = digits.length - 1; i >= 0; i--) out += BASE58_ALPHABET[digits[i]];
22
+ return out;
23
+ }
24
+ function base58Decode(s) {
25
+ let zeros = 0;
26
+ while (zeros < s.length && s[zeros] === "1") zeros++;
27
+ const bytes = [];
28
+ for (let i = zeros; i < s.length; i++) {
29
+ let carry = BASE58_ALPHABET.indexOf(s[i]);
30
+ if (carry < 0) throw new Error(`invalid base58 character ${JSON.stringify(s[i])}`);
31
+ for (let j = 0; j < bytes.length; j++) {
32
+ carry += bytes[j] * 58;
33
+ bytes[j] = carry & 255;
34
+ carry >>= 8;
35
+ }
36
+ while (carry > 0) {
37
+ bytes.push(carry & 255);
38
+ carry >>= 8;
39
+ }
40
+ }
41
+ return Buffer.concat([Buffer.alloc(zeros), Buffer.from(bytes.reverse())]);
42
+ }
43
+ function readShortVec(buf, offset) {
44
+ let value = 0;
45
+ for (let i = 0; i < 3; i++) {
46
+ const byte = buf[offset + i];
47
+ if (byte === void 0) throw new Error("truncated transaction");
48
+ value |= (byte & 127) << 7 * i;
49
+ if ((byte & 128) === 0) return { value, next: offset + i + 1 };
50
+ }
51
+ throw new Error("malformed compact-u16 length");
52
+ }
53
+ function take(buf, offset, length) {
54
+ if (offset + length > buf.length) throw new Error("truncated transaction");
55
+ return buf.subarray(offset, offset + length);
56
+ }
57
+ function parseWireTransaction(raw) {
58
+ const { value: sigCount, next: signatureOffset } = readShortVec(raw, 0);
59
+ const signatures = [];
60
+ let o = signatureOffset;
61
+ for (let i = 0; i < sigCount; i++, o += 64) signatures.push(take(raw, o, 64));
62
+ const message = raw.subarray(o);
63
+ if (message.length < 3) throw new Error("truncated transaction");
64
+ if ((message[0] & 128) !== 0) {
65
+ throw new Error("versioned transaction message \u2014 only legacy funding transactions are expected");
66
+ }
67
+ const header = {
68
+ numRequiredSignatures: message[0],
69
+ numReadonlySigned: message[1],
70
+ numReadonlyUnsigned: message[2]
71
+ };
72
+ if (header.numRequiredSignatures !== sigCount) {
73
+ throw new Error(
74
+ `transaction carries ${sigCount} signature slots for ${header.numRequiredSignatures} required signers`
75
+ );
76
+ }
77
+ let m = 3;
78
+ const keys = readShortVec(message, m);
79
+ m = keys.next;
80
+ const accountKeys = [];
81
+ for (let i = 0; i < keys.value; i++, m += 32) accountKeys.push(base58Encode(take(message, m, 32)));
82
+ if (header.numRequiredSignatures > accountKeys.length) {
83
+ throw new Error("more required signers than account keys");
84
+ }
85
+ const recentBlockhash = base58Encode(take(message, m, 32));
86
+ m += 32;
87
+ const ixCount = readShortVec(message, m);
88
+ m = ixCount.next;
89
+ const instructions = [];
90
+ for (let i = 0; i < ixCount.value; i++) {
91
+ const programIdIndex = take(message, m, 1)[0];
92
+ m += 1;
93
+ const accts = readShortVec(message, m);
94
+ m = accts.next;
95
+ const accountIndexes = [...take(message, m, accts.value)];
96
+ m += accts.value;
97
+ const dataLen = readShortVec(message, m);
98
+ m = dataLen.next;
99
+ const data = Buffer.from(take(message, m, dataLen.value));
100
+ m += dataLen.value;
101
+ for (const idx of [programIdIndex, ...accountIndexes]) {
102
+ if (idx >= accountKeys.length) throw new Error(`instruction references account ${idx} of ${accountKeys.length}`);
103
+ }
104
+ instructions.push({ programIdIndex, accountIndexes, data });
105
+ }
106
+ if (m !== message.length) {
107
+ throw new Error(`${message.length - m} unexpected trailing bytes after the message`);
108
+ }
109
+ return { signatures, signatureOffset, message, header, accountKeys, recentBlockhash, instructions };
110
+ }
111
+ function isSignerIndex(tx, index) {
112
+ return index < tx.header.numRequiredSignatures;
113
+ }
114
+ function isWritableIndex(tx, index) {
115
+ const { numRequiredSignatures, numReadonlySigned, numReadonlyUnsigned } = tx.header;
116
+ if (index < numRequiredSignatures) return index < numRequiredSignatures - numReadonlySigned;
117
+ return index < tx.accountKeys.length - numReadonlyUnsigned;
118
+ }
119
+ function isEmptySignature(sig) {
120
+ return sig.every((b) => b === 0);
121
+ }
122
+ var SPKI_ED25519_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
123
+ var PKCS8_ED25519_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
124
+ function ed25519Verify(signature, message, publicKey) {
125
+ try {
126
+ const raw = base58Decode(publicKey);
127
+ if (raw.length !== 32 || signature.length !== 64) return false;
128
+ const key = createPublicKey({
129
+ key: Buffer.concat([SPKI_ED25519_PREFIX, raw]),
130
+ format: "der",
131
+ type: "spki"
132
+ });
133
+ return verify(null, message, key, signature);
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+ function ed25519Sign(message, secretKey) {
139
+ const key = createPrivateKey({
140
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(secretKey.subarray(0, 32))]),
141
+ format: "der",
142
+ type: "pkcs8"
143
+ });
144
+ return sign(null, message, key);
145
+ }
146
+ function publicKeyOfSecret(secretKey) {
147
+ const key = createPrivateKey({
148
+ key: Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(secretKey.subarray(0, 32))]),
149
+ format: "der",
150
+ type: "pkcs8"
151
+ });
152
+ const spki = createPublicKey(key).export({ format: "der", type: "spki" });
153
+ return base58Encode(spki.subarray(spki.length - 32));
154
+ }
155
+ function addOwnSignature(raw, secretKey, publicKey) {
156
+ if (publicKeyOfSecret(secretKey) !== publicKey) {
157
+ throw new Error("the wallet secret key does not derive its own public key \u2014 refusing to sign");
158
+ }
159
+ const tx = parseWireTransaction(raw);
160
+ const signers = tx.accountKeys.slice(0, tx.header.numRequiredSignatures);
161
+ const index = signers.indexOf(publicKey);
162
+ if (index < 0) {
163
+ throw new Error(
164
+ `refusing to sign: ${publicKey} is not a required signer of this transaction (signers: ${signers.join(", ") || "none"})`
165
+ );
166
+ }
167
+ const signature = ed25519Sign(tx.message, secretKey);
168
+ const out = Buffer.from(raw);
169
+ signature.copy(out, tx.signatureOffset + 64 * index);
170
+ const after = parseWireTransaction(out);
171
+ if (out.length !== raw.length || !after.message.equals(tx.message)) {
172
+ throw new Error("signing altered the transaction message \u2014 refusing to return it");
173
+ }
174
+ for (let i = 0; i < tx.signatures.length; i++) {
175
+ if (i !== index && !after.signatures[i].equals(tx.signatures[i])) {
176
+ throw new Error(`signing altered signature slot ${i} \u2014 refusing to return it`);
177
+ }
178
+ }
179
+ if (!ed25519Verify(after.signatures[index], after.message, publicKey)) {
180
+ throw new Error("the new signature does not verify \u2014 refusing to return it");
181
+ }
182
+ return out;
183
+ }
184
+
185
+ export {
186
+ base58Encode,
187
+ base58Decode,
188
+ parseWireTransaction,
189
+ isSignerIndex,
190
+ isWritableIndex,
191
+ isEmptySignature,
192
+ ed25519Verify,
193
+ ed25519Sign,
194
+ publicKeyOfSecret,
195
+ addOwnSignature
196
+ };
@@ -7,6 +7,11 @@ var config = {
7
7
  if (!config.agentKey) {
8
8
  console.warn("Warning: SOLO_AGENT_KEY is not set. Only register_agent will work until a key is configured.");
9
9
  }
10
+ if ((process.env.SOLO_SOLANA_KEYPAIR ?? "").trim() !== "") {
11
+ console.warn(
12
+ "Warning: SOLO_SOLANA_KEYPAIR (the raw key in an environment variable) is no longer supported, and Solana tools will refuse to run while it is set. Unset it and use SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH + SOLO_SOLANA_KEYPAIR_PASSWORD_FILE (recommended) or SOLO_SOLANA_KEYPAIR_PATH."
13
+ );
14
+ }
10
15
 
11
16
  // src/api/client.ts
12
17
  var DEFAULT_HEADERS = {
@@ -4,7 +4,7 @@ import {
4
4
  apiPatch,
5
5
  apiPost,
6
6
  publicApiPost
7
- } from "./chunk-TKRT2V2W.js";
7
+ } from "./chunk-OP4QYTL4.js";
8
8
  export {
9
9
  apiDelete,
10
10
  apiGet,
@@ -0,0 +1,43 @@
1
+ // src/solana/deployment.ts
2
+ var PINNED_DEPLOYMENTS = {
3
+ // solo-escrow-solana u1.1.0 on devnet (the live private beta).
4
+ devnet: {
5
+ cluster: "devnet",
6
+ program_id: "2CPC5V63FDs7SdWu89iSYYsTEpqBwuQeYuA9ASzuSo8a",
7
+ rpc_url: "https://api.devnet.solana.com"
8
+ }
9
+ };
10
+ function resolvePinnedProgram(cfg) {
11
+ const override = (process.env.SOLO_SOLANA_PROGRAM_ID ?? "").trim();
12
+ const pinned = override || PINNED_DEPLOYMENTS[cfg.cluster ?? ""]?.program_id;
13
+ if (!pinned) {
14
+ return {
15
+ problem: `no escrow program is pinned for cluster ${JSON.stringify(cfg.cluster)} \u2014 set SOLO_SOLANA_PROGRAM_ID to the program id you trust, or use a version of this package that pins it`
16
+ };
17
+ }
18
+ if (cfg.program_id !== pinned) {
19
+ return {
20
+ problem: `the API reports escrow program ${cfg.program_id} for cluster ${JSON.stringify(cfg.cluster)}, but this server pins ${pinned}${override ? " (SOLO_SOLANA_PROGRAM_ID)" : ""} \u2014 refusing to sign for a program it does not trust`
21
+ };
22
+ }
23
+ return { program_id: pinned };
24
+ }
25
+ function resolveTrustedRpc(cfg) {
26
+ const rpc = (process.env.SOLO_SOLANA_RPC_URL ?? "").trim() || PINNED_DEPLOYMENTS[cfg.cluster ?? ""]?.rpc_url;
27
+ if (!rpc) {
28
+ return {
29
+ problem: `no RPC endpoint is pinned for cluster ${JSON.stringify(cfg.cluster)} \u2014 set SOLO_SOLANA_RPC_URL to an endpoint you trust; the API-reported rpc_url is not used for reads that decide what to sign`
30
+ };
31
+ }
32
+ return { rpc_url: rpc };
33
+ }
34
+ function solanaRpcUrl(cfg) {
35
+ const trusted = resolveTrustedRpc(cfg);
36
+ return "rpc_url" in trusted ? trusted.rpc_url : cfg.rpc_url;
37
+ }
38
+ export {
39
+ PINNED_DEPLOYMENTS,
40
+ resolvePinnedProgram,
41
+ resolveTrustedRpc,
42
+ solanaRpcUrl
43
+ };
@@ -0,0 +1,56 @@
1
+ // src/solana/escrowErrors.ts
2
+ var ESCROW_ERRORS = {
3
+ 6012: {
4
+ name: "SettlementWindowTooShort",
5
+ what_to_do: "settlement_deadline must be at least qualify_deadline + min_review_window + the finalize grace. Create a new mission with a longer work_duration_hours."
6
+ },
7
+ 6016: {
8
+ name: "TooEarlyForEmergencyRefund",
9
+ what_to_do: "emergency_refund opens only once settlement_deadline has passed. Retry after it."
10
+ },
11
+ 6026: {
12
+ name: "RefundAlreadyClaimed",
13
+ what_to_do: "the refundable amount was already claimed \u2014 nothing is left to refund."
14
+ },
15
+ 6027: { name: "NothingToRefund", what_to_do: "the task holds nothing refundable." },
16
+ 6124: {
17
+ name: "LotteryRequiresOperator",
18
+ what_to_do: "a lottery create_task needs the platform Operator's co-signature over the exact message. Request a fresh funding transaction and sign it unchanged."
19
+ },
20
+ 6125: {
21
+ name: "TooLateToCancel",
22
+ what_to_do: "cancel is refused on chain from qualify_deadline on. Let the mission settle, or use emergency_refund once settlement_deadline has passed without a settlement."
23
+ },
24
+ 6132: {
25
+ name: "FeeAboveSponsorMax",
26
+ what_to_do: "the program's fee rose above the max_fee_bps you were quoted, so the escrow refused to charge more. Cancel the mission (cancel_mission) and create a new one for a fresh quote."
27
+ },
28
+ 6133: {
29
+ name: "QualifyDeadlineTooFar",
30
+ what_to_do: "qualify_deadline may be at most 180 days ahead. Create a new mission with a shorter hiring window."
31
+ },
32
+ 6134: {
33
+ name: "SettlementDeadlineTooFar",
34
+ what_to_do: "settlement_deadline may be at most 90 days after qualify_deadline. Create a new mission with a shorter work window."
35
+ }
36
+ };
37
+ function escrowErrorFromMessage(message) {
38
+ if (!message) return null;
39
+ const candidates = [];
40
+ for (const m of message.matchAll(/custom program error: 0x([0-9a-f]+)/gi)) {
41
+ candidates.push(parseInt(m[1], 16));
42
+ }
43
+ for (const m of message.matchAll(/Error Number: (\d+)/g)) candidates.push(Number(m[1]));
44
+ for (const code of candidates) {
45
+ const info = ESCROW_ERRORS[code];
46
+ if (info) return { code, ...info };
47
+ }
48
+ for (const [code, info] of Object.entries(ESCROW_ERRORS)) {
49
+ if (new RegExp(`\\b${info.name}\\b`).test(message)) return { code: Number(code), ...info };
50
+ }
51
+ return null;
52
+ }
53
+ export {
54
+ ESCROW_ERRORS,
55
+ escrowErrorFromMessage
56
+ };