@projectsolo/solo-mission-mcp 0.21.13 → 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.
- package/DEVELOPER_README.md +1 -1
- package/README.md +96 -12
- package/dist/chunk-NUGGTARW.js +196 -0
- package/dist/{chunk-TKRT2V2W.js → chunk-OP4QYTL4.js} +5 -0
- package/dist/{client-LDWK5HLP.js → client-NS6J6IO2.js} +1 -1
- package/dist/deployment-UCUWVIPJ.js +43 -0
- package/dist/escrowErrors-4XSAYLGT.js +56 -0
- package/dist/index.js +244 -162
- package/dist/verify-7EUVRUAS.js +445 -0
- package/dist/{wallet-V4T4NTCM.js → wallet-LX7SI5RE.js} +28 -14
- package/dist/wire-RLDY4AJR.js +24 -0
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/index.ts +1 -1
- package/src/scripts/check-tools-against-spec.ts +3 -3
- package/src/solana/deployment.test.ts +61 -0
- package/src/solana/deployment.ts +89 -0
- package/src/solana/escrowErrors.ts +78 -0
- package/src/solana/fixtures/config-account.json +6 -0
- package/src/solana/fixtures/funding-transaction-v2-lottery.json +49 -0
- package/src/solana/fixtures/funding-transaction-v2-plain.json +46 -0
- package/src/solana/fixtures/solo_escrow.v2.idl-excerpt.json +683 -0
- package/src/solana/verify.test.ts +605 -78
- package/src/solana/verify.ts +545 -84
- package/src/solana/wallet.test.ts +85 -2
- package/src/solana/wallet.ts +74 -31
- package/src/solana/wire.test.ts +129 -0
- package/src/solana/wire.ts +264 -0
- package/src/tools/missions.ts +56 -119
- package/src/tools/solana.test.ts +356 -0
- package/src/tools/solana.ts +309 -70
- package/src/tools/tracks.ts +1 -1
- package/dist/verify-KAETIGV5.js +0 -136
- /package/src/solana/fixtures/{funding-transaction.json → funding-transaction-v1.json} +0 -0
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
apiPatch,
|
|
6
6
|
apiPost,
|
|
7
7
|
publicApiPost
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OP4QYTL4.js";
|
|
9
9
|
|
|
10
10
|
// src/index.ts
|
|
11
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
var RESPONSE_SCHEMA_PROPERTY = {
|
|
20
20
|
type: "array",
|
|
21
21
|
maxItems: 20,
|
|
22
|
-
description: 'Structured completion questions a human answers to complete the mission. Question ids must be unique within one schema \u2014 a duplicate id is rejected at creation/update, not merged or silently overwritten. When set, finalize_qualification auto-derives qualified UIDs from completion instead of requiring an explicit qualified_human_uids list \u2014 same mechanism media_review has always used for track ratings, generalized to any mission type. media_review defaults to a canonical two-question schema when this is omitted at creation \u2014 literally { id: "rating", kind: "stars", required: true } and { id: "comment", kind: "long", required:
|
|
22
|
+
description: 'Structured completion questions a human answers to complete the mission. Question ids must be unique within one schema \u2014 a duplicate id is rejected at creation/update, not merged or silently overwritten. When set, finalize_qualification auto-derives qualified UIDs from completion instead of requiring an explicit qualified_human_uids list \u2014 same mechanism media_review has always used for track ratings, generalized to any mission type. media_review defaults to a canonical two-question schema when this is omitted at creation \u2014 literally { id: "rating", kind: "stars", required: true } and { id: "comment", kind: "long", required: true } \u2014 so read/append against those exact ids if you rely on the default rather than supplying your own schema. Every other type has no default (stays fully manual/chat-based unless you set this).',
|
|
23
23
|
items: {
|
|
24
24
|
type: "object",
|
|
25
25
|
properties: {
|
|
@@ -43,7 +43,10 @@ var RESPONSE_SCHEMA_PROPERTY = {
|
|
|
43
43
|
},
|
|
44
44
|
label: { type: "string", description: "Question text shown to the human (max 200 chars). For the checkbox kind this is the checkbox's own clickable text, not a separate heading." },
|
|
45
45
|
help: { type: "string", description: "Optional helper text shown under the label (max 500 chars)." },
|
|
46
|
-
required: {
|
|
46
|
+
required: {
|
|
47
|
+
type: "boolean",
|
|
48
|
+
description: "Whether this question must be answered to complete the mission. A required: false question does NOT block mission completion or finalize_qualification's auto-qualification \u2014 both derive purely from the required questions, so an optional question left blank (or still being typed into) will not stop either from firing the instant the required ones are answered. If every question in the schema should actually be answered, mark them all required: true; reserve required: false for a genuinely optional/skippable field."
|
|
49
|
+
},
|
|
47
50
|
options: {
|
|
48
51
|
type: "array",
|
|
49
52
|
items: { type: "string" },
|
|
@@ -58,7 +61,7 @@ var RESPONSE_SCHEMA_PROPERTY = {
|
|
|
58
61
|
var missionTools = [
|
|
59
62
|
{
|
|
60
63
|
name: "create_mission",
|
|
61
|
-
description: "Create a new mission. Off-chain missions (no budget field) are always free \u2014 no payment, no escrow.
|
|
64
|
+
description: "Create a new mission. Off-chain missions (no budget field) are always free \u2014 no payment, no escrow. Include the budget field for a paid mission escrowed on Solana: it is created in pending_funding, and you then fund it with fund_solana_mission (pass the returned mission id). Solana is the only chain (the backend also defaults a missing chain to solana, and this tool always sends it). A paid mission's response includes funding_params: chain ('solana'), cluster, program_id, rpc_url, mint, token_address (same as mint), token_decimals, amount_raw, base_pool, lottery_reward_per_winner_raw, lottery_winner_count, qualify_deadline, settlement_deadline, seed_commit and expires_at. There is no escrow address or task id yet: the program assigns those when the mission is funded. expires_at is creation + 24 hours \u2014 a mission still unfunded then is cancelled automatically. You do not need to build anything from funding_params; fund_solana_mission does the whole funding step. The response's mission.solana_quoted_fee_bps is the platform fee rate you were quoted: the escrow transaction carries it as max_fee_bps, the most the program may charge, and fund_solana_mission refuses to sign any other value. A paid lottery (lottery_winner_count > 0) is funded with a transaction the platform Operator has already co-signed; fund_solana_mission verifies that co-signature before adding yours. Paid-mission deadlines follow the escrow program's rules \u2014 see hiring_duration_hours and work_duration_hours.",
|
|
62
65
|
inputSchema: {
|
|
63
66
|
type: "object",
|
|
64
67
|
properties: {
|
|
@@ -69,8 +72,8 @@ var missionTools = [
|
|
|
69
72
|
},
|
|
70
73
|
chain: {
|
|
71
74
|
type: "string",
|
|
72
|
-
enum: ["
|
|
73
|
-
description: "
|
|
75
|
+
enum: ["solana"],
|
|
76
|
+
description: "Optional. Solana is the only chain: a paid (budget-bearing) mission is always sent with chain 'solana' whether or not you set this. Ignored for off-chain missions (no budget field)."
|
|
74
77
|
},
|
|
75
78
|
title: { type: "string", description: "Mission title (max 100 chars)" },
|
|
76
79
|
description: { type: "string", description: "Detailed mission description (max 2000 chars). Supports Markdown \u2014 use ## headings, - bullet lists, and **bold** to structure your content. The platform renders it as formatted text." },
|
|
@@ -87,14 +90,14 @@ var missionTools = [
|
|
|
87
90
|
reward_usdt: { type: "number", minimum: 0, description: "Deprecated \u2014 ignored for off-chain missions (which are always free). Has no effect. Omit this field." },
|
|
88
91
|
max_participants: { type: "number", minimum: 1, description: "Maximum number of participants" },
|
|
89
92
|
expires_in_hours: { type: "number", minimum: 1, description: "Hours until mission expires" },
|
|
90
|
-
budget: { type: "number", description: "Total mission budget in USDC. Enables on-chain escrow
|
|
93
|
+
budget: { type: "number", description: "Total mission budget in USDC. Enables on-chain escrow on Solana (fund it afterwards with fund_solana_mission). Must also cover the platform fee, which is reserved out of the budget: base_reward * max_humans + lottery_prize_per_winner * lottery_winner_count + floor(budget * fee_bps / 10000) <= budget (equality passes). fee_bps is read from the Solana escrow program's on-chain config, not a fixed rate (currently 0 on devnet). A 400 for this rule states the rate in use; a 503 means the rate could not be read, nothing was created, and you should retry shortly. Both the backend and the Solana escrow program enforce this." },
|
|
91
94
|
max_humans: { type: "number", minimum: 1, description: "On-chain: maximum number of participants (both base-reward and lottery entrants)." },
|
|
92
95
|
base_reward: { type: "number", minimum: 0, description: "Per-participant base reward in USDC paid to every qualified human. Defaults to 0. Set to 0 for pure-lottery missions." },
|
|
93
96
|
reward_per_human: { type: "number", minimum: 0, description: "Deprecated \u2014 use base_reward instead." },
|
|
94
|
-
lottery_winner_count: { type: "integer", minimum: 1, description: "Number of winners randomly selected from all qualified participants. Must be <= max_humans. Must be paired with lottery_prize_per_winner.
|
|
97
|
+
lottery_winner_count: { type: "integer", minimum: 1, description: "Number of winners randomly selected from all qualified participants. Must be <= max_humans. Must be paired with lottery_prize_per_winner. For a paid mission the draw happens on chain at settlement: final_entropy = keccak256(seed_reveal \u2016 entropy_hash), where seed_reveal opens the seed_commit fixed at funding and entropy_hash is the SlotHashes sysvar bank hash of the first produced slot after finalize_qualification, stored on the Task by record_entropy (settle captures it if nobody recorded it). It is NOT the blockhash getBlock returns for that slot, so recompute a draw from the Task account's entropy_hash, not from getBlock. The platform Operator co-signs a lottery's funding transaction (the escrow program requires it)." },
|
|
95
98
|
lottery_prize_per_winner: { type: "number", minimum: 0, description: "Additional prize in USDC paid to each lottery winner on top of base_reward. Must be paired with lottery_winner_count." },
|
|
96
|
-
hiring_duration_hours: { type: "number", minimum: 0.0166, description: "How long (hours) the mission accepts applications and the agent hires/rejects. The hiring window closes at now + hiring_duration_hours. Finalize-qualification cannot be called before this. Backend floor is 60s (0.0166h)
|
|
97
|
-
work_duration_hours: { type: "number", minimum: 0.0166, description: "How long (hours) hired participants have to complete the work. Agent must call settle_mission before this period ends. Backend floor is 60s (0.0166h)
|
|
99
|
+
hiring_duration_hours: { type: "number", minimum: 0.0166, description: "How long (hours) the mission accepts applications and the agent hires/rejects. The hiring window closes at now + hiring_duration_hours. Finalize-qualification cannot be called before this. Closing hiring does NOT end the work: hired humans keep voting and submitting answers after it (see work_duration_hours). Backend floor is 60s (0.0166h) \u2014 see work_duration_hours for what that floor does and does not guarantee. For a paid mission the end of the hiring window is the escrow's qualify_deadline: it may be at most 180 days ahead, and it is the cancel cut-off \u2014 refund_solana_mission's cancel is refused on chain from qualify_deadline on (TooLateToCancel)." },
|
|
100
|
+
work_duration_hours: { type: "number", minimum: 0.0166, description: "How long (hours) hired participants have to complete the work: settlement_deadline = end of the hiring window + work_duration_hours. Agent must call settle_mission before this period ends. Humans can vote and submit answers while the mission is active: on-chain until max(qualify_deadline, settlement_deadline \u2212 1260s), i.e. about 21 minutes before settlement_deadline; off-chain until finalize. Either way, finalize_qualification closes submissions. Backend floor is 60s (0.0166h) \u2014 a sanity check against a near-zero window, not a guarantee the escrow accepts it. For a paid mission the Solana escrow program separately requires settlement_deadline >= qualify_deadline + min_review_window + finalize grace, and settlement_deadline <= qualify_deadline + 90 days. min_review_window is the program's configured review window and the finalize grace equals its floor (10 seconds each on the current devnet build; the floor is 1 hour on a production build). create_mission checks both against the live program config and returns 400 on a violation; the program enforces them again when the escrow is funded, and finalize_qualification must still leave min_review_window before settlement_deadline." },
|
|
98
101
|
auto_accept_applicants: { type: "boolean", description: "Defaults to true \u2014 applicants are automatically hired when they apply, no manual hire_participant call needed, first-come first-served up to max_humans, face verification still required. Pass false to opt into manual review instead (see hire_participant for how unreviewed applicants are still handled once the hiring window closes)." },
|
|
99
102
|
response_schema: RESPONSE_SCHEMA_PROPERTY
|
|
100
103
|
},
|
|
@@ -140,21 +143,9 @@ var missionTools = [
|
|
|
140
143
|
required: ["mission_id"]
|
|
141
144
|
}
|
|
142
145
|
},
|
|
143
|
-
{
|
|
144
|
-
name: "confirm_funding",
|
|
145
|
-
description: `Base (EscrowVault) only \u2014 Solana missions use fund_solana_mission instead, which confirms as part of the same call. Base is currently closed to new mission creation, so this only applies to a Base mission created before that change. After calling createTask() on the EscrowVault contract, confirm the funding via the SOLO API. The backend verifies the transaction on-chain. Mission transitions from pending_funding \u2192 active. tx_hash is optional \u2014 backend reconciles from the contract if omitted. For media_review missions: returns 409 if no tracks are confirmed yet \u2014 call add_mission_track first. Also returns 409 if the on-chain task's budget, lottery, deadline, or seed_commit values do not match what create_mission originally quoted (e.g. createTask() was called with hand-typed or re-derived values instead of the exact funding_params fields) \u2014 unlike the "not yet FUNDED" 409, this one is NOT retryable: the task_id can never be confirmed. Call cancelTask() on-chain to reclaim the full escrow, then call create_mission again.`,
|
|
146
|
-
inputSchema: {
|
|
147
|
-
type: "object",
|
|
148
|
-
properties: {
|
|
149
|
-
mission_id: { type: "string" },
|
|
150
|
-
tx_hash: { type: "string", description: "Transaction hash of createTask() call (optional)" }
|
|
151
|
-
},
|
|
152
|
-
required: ["mission_id"]
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
146
|
{
|
|
156
147
|
name: "hire_participant",
|
|
157
|
-
description: "Accept a human applicant for a mission. Only applied humans can be hired. Hired humans can start work via conversations. Only valid while mission is active. IMPORTANT: silence is not neutral \u2014 an applicant you never call this (or reject_participant) on is automatically hired
|
|
148
|
+
description: "Accept a human applicant for a mission. Only applied humans can be hired. Hired humans can start work via conversations. Only valid while mission is active. IMPORTANT: silence is not neutral \u2014 an applicant you never call this (or reject_participant) on is automatically hired (up to max_humans, oldest applied_at first). That does NOT happen when the hiring window closes. For an on-chain mission it happens in the window just before settlement_deadline (roughly 21 to 1 minutes before it), in the same pass that auto-finalizes and settles. For an off-chain mission it happens at expires_at. If you do not want someone, call reject_participant before then.",
|
|
158
149
|
inputSchema: {
|
|
159
150
|
type: "object",
|
|
160
151
|
properties: {
|
|
@@ -166,7 +157,7 @@ var missionTools = [
|
|
|
166
157
|
},
|
|
167
158
|
{
|
|
168
159
|
name: "reject_participant",
|
|
169
|
-
description: "Reject a human applicant or hired participant. Valid for applied or hired status, before finalize_qualification is called. This is the ONLY way to exclude someone \u2014 the platform auto-hires
|
|
160
|
+
description: "Reject a human applicant or hired participant. Valid for applied or hired status, before finalize_qualification is called. This is the ONLY way to exclude someone \u2014 the platform auto-hires remaining applicants and auto-qualifies hired participants by default (silence = go), so anyone you do not explicitly reject ends up hired, qualified and paid. That happens in the window just before settlement_deadline for an on-chain mission (roughly 21 to 1 minutes before it) and at expires_at for an off-chain mission, not when the hiring window closes.",
|
|
170
161
|
inputSchema: {
|
|
171
162
|
type: "object",
|
|
172
163
|
properties: {
|
|
@@ -178,7 +169,7 @@ var missionTools = [
|
|
|
178
169
|
},
|
|
179
170
|
{
|
|
180
171
|
name: "finalize_qualification",
|
|
181
|
-
description: "Lock in the qualified participants after reviewing their work. If the mission has a response_schema (media_review always does, by default or explicitly; any other type only if you set one via create_mission/update_mission_questions), pass an empty body \u2014 qualified_human_uids is ignored and the backend auto-qualifies anyone who completed it (every ready track for media_review, a full submission for other types). Otherwise provide an explicit list of UIDs whose work was accepted. For on-chain missions, the backend
|
|
172
|
+
description: "Lock in the qualified participants after reviewing their work. If the mission has a response_schema (media_review always does, by default or explicitly; any other type only if you set one via create_mission/update_mission_questions), pass an empty body \u2014 qualified_human_uids is ignored and the backend auto-qualifies anyone who completed it (every ready track for media_review, a full submission for other types). Otherwise provide an explicit list of UIDs whose work was accepted. For on-chain missions, the backend records the qualified set on the Solana escrow program. Mission transitions to qualifying. If you never call this, the platform does it for you (silence = go), qualifying every hired participant you did not explicitly reject_participant. It does NOT happen when the hiring window closes. For an on-chain mission it happens in a short window just before settlement_deadline (roughly 21 to 1 minutes before it), where the platform also auto-hires any remaining applicants, finalizes and settles in one pass. For an off-chain mission it happens at expires_at. Call reject_participant first for anyone whose work should NOT be paid.",
|
|
182
173
|
inputSchema: {
|
|
183
174
|
type: "object",
|
|
184
175
|
properties: {
|
|
@@ -194,7 +185,7 @@ var missionTools = [
|
|
|
194
185
|
},
|
|
195
186
|
{
|
|
196
187
|
name: "settle_mission",
|
|
197
|
-
description: "Settle the mission after finalize_qualification. For on-chain missions,
|
|
188
|
+
description: "Settle the mission after finalize_qualification. For on-chain missions, the backend settles the Solana escrow with aggregate payout numbers only (no per-wallet computation); mission transitions to completed or refundable. A 'refundable' mission is holding leftover budget for you: claim it with refund_solana_mission, action: 'claim_refund' (the mission then moves to refunded). IMPORTANT: status alone cannot tell you whether anyone was paid \u2014 a mission with 8/10 slots filled (2 slots' budget refunded) and one with 0/10 filled (the whole budget refunded) both land on 'refundable'/'refunded'. Check the settlement_outcome field on the response instead: 'completed' (fully spent, no refund), 'completed_refundable' (real participants were paid, only the unfilled slots refund), or 'no_payout_refunded' (nobody qualified, the entire budget bounces). Rewards are NOT immediately claimable \u2014 a separate batched process publishes the Merkle root that makes them claimable, gated by a review window currently defaulting to about 10 seconds (minimized pre-launch; will lengthen to an hour or more once real funds are at stake). Check GET /human/solana/rewards for actual claimable status rather than assuming a fixed delay. For free (off-chain) missions, marks qualified participants as completed \u2014 no payment is involved; settlement_outcome is always 'completed' there. You may settle at any time after finalize_qualification and before settlement_deadline; there is no need to wait. If you never call this, the platform settles on your behalf, but NOT right after finalize: for an on-chain mission it happens in the window just before settlement_deadline (roughly 21 to 1 minutes before it), and for an off-chain mission at expires_at. Who is qualified is fixed at finalize, so reject anyone unwanted before finalize, not after.",
|
|
198
189
|
inputSchema: {
|
|
199
190
|
type: "object",
|
|
200
191
|
properties: {
|
|
@@ -205,7 +196,7 @@ var missionTools = [
|
|
|
205
196
|
},
|
|
206
197
|
{
|
|
207
198
|
name: "cancel_mission",
|
|
208
|
-
description: "Cancel an off-chain mission
|
|
199
|
+
description: "Cancel a mission that holds no escrowed funds: an off-chain mission (status active or qualifying), or an unfunded Solana mission still in pending_funding. For a Solana mission the backend first checks the chain: if it finds an escrow that was created but never confirmed, it returns 409 with that task_id and does not cancel \u2014 use refund_solana_mission with action: 'cancel' then. For a funded mission also use refund_solana_mission with action: 'cancel': it returns the escrowed budget to you and moves the mission to cancelled, but only strictly before qualify_deadline (the end of the hiring window) \u2014 the escrow program refuses a cancel from then on (TooLateToCancel), and the remaining exit is emergency_refund once settlement_deadline passes without a settlement. An unfunded Solana mission you leave alone is cancelled automatically at its funding_params.expires_at (creation + 24 hours).",
|
|
209
200
|
inputSchema: {
|
|
210
201
|
type: "object",
|
|
211
202
|
properties: {
|
|
@@ -214,78 +205,9 @@ var missionTools = [
|
|
|
214
205
|
required: ["mission_id"]
|
|
215
206
|
}
|
|
216
207
|
},
|
|
217
|
-
{
|
|
218
|
-
name: "get_cancel_params",
|
|
219
|
-
description: "Get on-chain transaction parameters to cancel a funded mission via cancelTask() on EscrowVault. Only valid before qualify_deadline. After qualify_deadline, use get_emergency_refund_params instead.",
|
|
220
|
-
inputSchema: {
|
|
221
|
-
type: "object",
|
|
222
|
-
properties: {
|
|
223
|
-
mission_id: { type: "string" }
|
|
224
|
-
},
|
|
225
|
-
required: ["mission_id"]
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
name: "confirm_cancel",
|
|
230
|
-
description: "After executing cancelTask() on EscrowVault, confirm the cancellation on the SOLO platform. Mission transitions to cancelled.",
|
|
231
|
-
inputSchema: {
|
|
232
|
-
type: "object",
|
|
233
|
-
properties: {
|
|
234
|
-
mission_id: { type: "string" },
|
|
235
|
-
tx_hash: { type: "string", description: "Transaction hash of cancelTask() (optional)" }
|
|
236
|
-
},
|
|
237
|
-
required: ["mission_id"]
|
|
238
|
-
}
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
name: "get_emergency_refund_params",
|
|
242
|
-
description: "Get on-chain transaction parameters to force-refund a mission after the settlement_deadline has passed without settlement. Returns eligible: true with params if eligible, or eligible: false with retry_after if not yet past the deadline.",
|
|
243
|
-
inputSchema: {
|
|
244
|
-
type: "object",
|
|
245
|
-
properties: {
|
|
246
|
-
mission_id: { type: "string" }
|
|
247
|
-
},
|
|
248
|
-
required: ["mission_id"]
|
|
249
|
-
}
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: "confirm_emergency_refund",
|
|
253
|
-
description: "After executing emergencyRefund() on EscrowVault, confirm on the SOLO platform. Mission transitions to cancelled.",
|
|
254
|
-
inputSchema: {
|
|
255
|
-
type: "object",
|
|
256
|
-
properties: {
|
|
257
|
-
mission_id: { type: "string" },
|
|
258
|
-
tx_hash: { type: "string", description: "Transaction hash of emergencyRefund() (optional)" }
|
|
259
|
-
},
|
|
260
|
-
required: ["mission_id"]
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
name: "get_refund_params",
|
|
265
|
-
description: "Get on-chain transaction parameters to claim unused budget via claimRefund() on EscrowVault. Only valid when mission is in refundable state (settled with leftover budget).",
|
|
266
|
-
inputSchema: {
|
|
267
|
-
type: "object",
|
|
268
|
-
properties: {
|
|
269
|
-
mission_id: { type: "string" }
|
|
270
|
-
},
|
|
271
|
-
required: ["mission_id"]
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
name: "confirm_refund",
|
|
276
|
-
description: "After executing claimRefund() on EscrowVault, confirm the refund on the SOLO platform. Mission transitions to refunded.",
|
|
277
|
-
inputSchema: {
|
|
278
|
-
type: "object",
|
|
279
|
-
properties: {
|
|
280
|
-
mission_id: { type: "string" },
|
|
281
|
-
tx_hash: { type: "string", description: "Transaction hash of claimRefund() (optional)" }
|
|
282
|
-
},
|
|
283
|
-
required: ["mission_id"]
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
208
|
{
|
|
287
209
|
name: "rate_participant",
|
|
288
|
-
description: "Leave a rating and optional comment for a mission participant.
|
|
210
|
+
description: "Leave a rating and optional comment for a mission participant. Allowed for every settled outcome (mission status completed, refundable or refunded), within 7 days of settlement. Limited to one rating per participant per mission; calling again overwrites the previous rating/comment.",
|
|
289
211
|
inputSchema: {
|
|
290
212
|
type: "object",
|
|
291
213
|
properties: {
|
|
@@ -300,8 +222,10 @@ var missionTools = [
|
|
|
300
222
|
];
|
|
301
223
|
async function handleMissionTool(name, args) {
|
|
302
224
|
switch (name) {
|
|
303
|
-
case "create_mission":
|
|
304
|
-
|
|
225
|
+
case "create_mission": {
|
|
226
|
+
const { chain: _chain, ...body } = args;
|
|
227
|
+
return apiPost("/agent/missions", body.budget !== void 0 ? { ...body, chain: "solana" } : body);
|
|
228
|
+
}
|
|
305
229
|
case "list_missions": {
|
|
306
230
|
const params = {};
|
|
307
231
|
if (args.status) params.status = args.status;
|
|
@@ -313,8 +237,6 @@ async function handleMissionTool(name, args) {
|
|
|
313
237
|
return apiGet(`/agent/missions/${args.mission_id}`);
|
|
314
238
|
case "update_mission_questions":
|
|
315
239
|
return apiPatch(`/agent/missions/${args.mission_id}/questions`, { response_schema: args.response_schema });
|
|
316
|
-
case "confirm_funding":
|
|
317
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-funding`, { tx_hash: args.tx_hash });
|
|
318
240
|
case "hire_participant":
|
|
319
241
|
return apiPost(`/agent/missions/${args.mission_id}/participants/${args.uid}/hire`);
|
|
320
242
|
case "reject_participant":
|
|
@@ -327,18 +249,6 @@ async function handleMissionTool(name, args) {
|
|
|
327
249
|
return apiPost(`/agent/missions/${args.mission_id}/settle`);
|
|
328
250
|
case "cancel_mission":
|
|
329
251
|
return apiPost(`/agent/missions/${args.mission_id}/cancel`);
|
|
330
|
-
case "get_cancel_params":
|
|
331
|
-
return apiGet(`/agent/missions/${args.mission_id}/cancel-params`);
|
|
332
|
-
case "confirm_cancel":
|
|
333
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-cancel`, { tx_hash: args.tx_hash });
|
|
334
|
-
case "get_emergency_refund_params":
|
|
335
|
-
return apiGet(`/agent/missions/${args.mission_id}/emergency-refund-params`);
|
|
336
|
-
case "confirm_emergency_refund":
|
|
337
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-emergency-refund`, { tx_hash: args.tx_hash });
|
|
338
|
-
case "get_refund_params":
|
|
339
|
-
return apiGet(`/agent/missions/${args.mission_id}/refund-params`);
|
|
340
|
-
case "confirm_refund":
|
|
341
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-refund`, { tx_hash: args.tx_hash });
|
|
342
252
|
case "rate_participant":
|
|
343
253
|
return apiPost(`/agent/missions/${args.mission_id}/participants/${args.uid}/comment`, {
|
|
344
254
|
rating: args.rating,
|
|
@@ -879,7 +789,7 @@ import { readFile } from "fs/promises";
|
|
|
879
789
|
var trackTools = [
|
|
880
790
|
{
|
|
881
791
|
name: "add_mission_track",
|
|
882
|
-
description: "Upload a media item (audio, image, or video) to a media_review mission. Provide the file EITHER via file_path (the MCP server reads it from local disk and uploads it directly \u2014 use this for audio/video, since inlining a multi-MB file as base64 in the tool call can exceed the calling agent's own tool-call or context limits, causing a silent client-side failure before any request reaches the API) OR inline as file_base64 (fine for small images). Exactly one of the two is required. For on-chain missions, call this BEFORE
|
|
792
|
+
description: "Upload a media item (audio, image, or video) to a media_review mission. Provide the file EITHER via file_path (the MCP server reads it from local disk and uploads it directly \u2014 use this for audio/video, since inlining a multi-MB file as base64 in the tool call can exceed the calling agent's own tool-call or context limits, causing a silent client-side failure before any request reaches the API) OR inline as file_base64 (fine for small images). Exactly one of the two is required. For on-chain missions, call this BEFORE fund_solana_mission \u2014 uploads are blocked once the mission is active. For off-chain missions, call while the mission is active and before any participant is hired. The item becomes visible to hired participants once confirmed.",
|
|
883
793
|
inputSchema: {
|
|
884
794
|
type: "object",
|
|
885
795
|
properties: {
|
|
@@ -1002,12 +912,12 @@ async function handleTrackTool(name, args) {
|
|
|
1002
912
|
var solanaTools = [
|
|
1003
913
|
{
|
|
1004
914
|
name: "get_solana_config",
|
|
1005
|
-
description: "Read the Solana escrow deployment: program id, cluster, RPC endpoint, accepted mints and their decimals,
|
|
915
|
+
description: "Read the Solana escrow deployment: program id, cluster, RPC endpoint, accepted mints and their decimals, the minimum first payout, and escrow_interface \u2014 the program interface funding transactions use. This server verifies only escrow_interface 'v2' and refuses to fund against anything else. Call this before funding so you use a whitelisted mint \u2014 a mint that is not whitelisted is rejected on chain, not by the API. Requires no wallet.",
|
|
1006
916
|
inputSchema: { type: "object", properties: {} }
|
|
1007
917
|
},
|
|
1008
918
|
{
|
|
1009
919
|
name: "get_solana_wallet",
|
|
1010
|
-
description: "Show your Solana wallet address and its balances. Reports SOL (needed for transaction fees and for account rent) and the token balance for a given mint (the mission budget). Use this before funding: an agent needs BOTH, and the rent line
|
|
920
|
+
description: "Show your Solana wallet address and its balances. Reports SOL (needed for transaction fees and for account rent) and the token balance for a given mint (the mission budget). Use this before funding: an agent needs BOTH, and the rent line is easy to miss. Rent is a refundable deposit, not a fee \u2014 most of it returns when the task is closed. If no wallet is configured this explains how to set one up.",
|
|
1011
921
|
inputSchema: {
|
|
1012
922
|
type: "object",
|
|
1013
923
|
properties: {
|
|
@@ -1020,18 +930,23 @@ var solanaTools = [
|
|
|
1020
930
|
},
|
|
1021
931
|
{
|
|
1022
932
|
name: "fund_solana_mission",
|
|
1023
|
-
description: "Fund a Solana mission end to end: the backend builds the escrow transaction, this tool DECODES AND VERIFIES it against the parameters you expect, signs it locally with your wallet, and submits it. Your key never leaves this process.\n\nVerification is not optional and cannot be skipped. On Solana the backend builds the transaction rather than publishing a parameter set for you to rebuild, so without a decode you would be signing bytes you cannot read. This tool refuses to sign if anything differs from what you expect \u2014 a substituted mint, an altered budget, an extra instruction,
|
|
933
|
+
description: "Fund a Solana mission end to end: the backend builds the escrow transaction, this tool DECODES AND VERIFIES it against the parameters you expect, signs it locally with your wallet, and submits it. Your key never leaves this process.\n\nVerification is not optional and cannot be skipped. On Solana the backend builds the transaction rather than publishing a parameter set for you to rebuild, so without a decode you would be signing bytes you cannot read. This tool refuses to sign if anything differs from what you expect \u2014 a substituted mint, an altered budget or deadline, an extra instruction, an account that is not the program-derived address, a raised fee ceiling, a lottery co-signer that is not the program's Operator \u2014 and returns the discrepancies instead.\n\nThe escrow program id is pinned in this package per cluster (SOLO_SOLANA_PROGRAM_ID can pin another) and the API's program_id must match it; the mint's decimals, which scale expected_budget, are read from chain.\n\nEscrow interface v2 (the only one this server verifies; get_solana_config reports it):\n- max_fee_bps: create_task carries the highest platform fee the escrow may take. It must equal the fee rate create_mission quoted you (mission.solana_quoted_fee_bps). If the program's fee has risen above it by the time the transaction lands, create_task fails with FeeAboveSponsorMax instead of charging you more; cancel the mission and create a new one for a fresh quote.\n- Lottery co-signature: a mission with lottery_winner_count > 0 must be co-signed by the platform Operator, so the transaction arrives already signed by it. This tool checks that the co-signer is the Operator in the program's on-chain Config (read directly from the cluster, not from the API), that its ed25519 signature is valid over the exact message, and that you, not the Operator, pay the fee. It then adds ONLY your signature and leaves the Operator's untouched; any change to the bytes would invalidate it. A non-lottery transaction must carry no co-signer.\n\nCall create_mission with a budget first and pass that mission's id here. Fund it within 24 hours: an unfunded mission is cancelled automatically at funding_params.expires_at. A media_review mission needs at least one ready track (add_mission_track) before funding, or this returns 409 before anything is signed.",
|
|
1024
934
|
inputSchema: {
|
|
1025
935
|
type: "object",
|
|
1026
936
|
properties: {
|
|
1027
|
-
mission_id: { type: "string", description:
|
|
937
|
+
mission_id: { type: "string", description: "A paid (budget-bearing) mission from create_mission, still in pending_funding." },
|
|
1028
938
|
expected_budget: {
|
|
1029
939
|
type: "number",
|
|
1030
940
|
description: "The total budget in whole tokens (e.g. 10 for 10 USDC) you expect to escrow. Verified against the transaction before signing. Pass what you intended, NOT what the API told you \u2014 comparing the API to itself proves nothing."
|
|
1031
941
|
},
|
|
1032
942
|
expected_mint: {
|
|
1033
943
|
type: "string",
|
|
1034
|
-
description: "The mint you expect the budget to be taken in. Verified before signing. Defaults to the
|
|
944
|
+
description: "The mint you expect the budget to be taken in. Verified before signing. Defaults to the mint create_mission recorded for this mission (its funding_params.mint)."
|
|
945
|
+
},
|
|
946
|
+
expected_max_fee_bps: {
|
|
947
|
+
type: "integer",
|
|
948
|
+
minimum: 0,
|
|
949
|
+
description: "The platform fee rate, in basis points, that create_mission quoted you: its response's mission.solana_quoted_fee_bps. Verified against the transaction's max_fee_bps before signing. Defaults to the quote recorded on the mission."
|
|
1035
950
|
},
|
|
1036
951
|
dry_run: {
|
|
1037
952
|
type: "boolean",
|
|
@@ -1043,7 +958,7 @@ var solanaTools = [
|
|
|
1043
958
|
},
|
|
1044
959
|
{
|
|
1045
960
|
name: "refund_solana_mission",
|
|
1046
|
-
description: "Get a Solana mission's escrowed funds back to the sponsor. Covers all three routes
|
|
961
|
+
description: "Get a Solana mission's escrowed funds back to the sponsor. Covers all three routes:\n- cancel: while the task is funded and strictly before qualify_deadline (the end of the hiring window). The escrow program refuses a cancel from qualify_deadline on (TooLateToCancel), even one built earlier, because by then the participants' work is done.\n- emergency_refund: once settlement_deadline has passed and the platform has not settled. It stays available after the cancel cut-off, and a pause does not block it.\n- claim_refund: leftover budget after a settle (mission status refundable).\n\nSame flow as funding: the backend builds the transaction, this tool VERIFIES it, signs locally, and submits. Your key never leaves this process.\n\nCall with no action to ask what is currently available \u2014 the response lists the legal actions for the mission's state rather than guessing. If the backend or the chain rejects an action, the response says which one did, names the program error, and lists the actions that remain.",
|
|
1047
962
|
inputSchema: {
|
|
1048
963
|
type: "object",
|
|
1049
964
|
properties: {
|
|
@@ -1076,19 +991,25 @@ function toRawAmount(amount, decimals) {
|
|
|
1076
991
|
return (whole + frac.padEnd(decimals, "0")).replace(/^0+(?=\d)/, "");
|
|
1077
992
|
}
|
|
1078
993
|
var SOLANA_TOOL_NAMES = new Set(solanaTools.map((t) => t.name));
|
|
994
|
+
function apiErrorBody(e) {
|
|
995
|
+
return e?.data ?? e?.response?.data ?? e?.body ?? {};
|
|
996
|
+
}
|
|
997
|
+
var CANCEL_CUTOFF_RULE = "cancel is refused on chain from qualify_deadline on (TooLateToCancel, error 6125); emergency_refund opens once settlement_deadline has passed without a settlement.";
|
|
1079
998
|
async function handleSolanaTool(name, args) {
|
|
1080
|
-
const { apiGet: apiGet2, apiPost: apiPost2 } = await import("./client-
|
|
999
|
+
const { apiGet: apiGet2, apiPost: apiPost2 } = await import("./client-NS6J6IO2.js");
|
|
1000
|
+
const { resolvePinnedProgram, resolveTrustedRpc, solanaRpcUrl } = await import("./deployment-UCUWVIPJ.js");
|
|
1081
1001
|
switch (name) {
|
|
1082
1002
|
case "get_solana_config":
|
|
1083
1003
|
return apiGet2("/agent/solana/config");
|
|
1084
1004
|
case "get_solana_wallet": {
|
|
1085
|
-
const { hasSolanaWallet, loadSolanaWallet, associatedTokenAddress } = await import("./wallet-
|
|
1005
|
+
const { hasSolanaWallet, loadSolanaWallet, associatedTokenAddress, KEY_STORAGE_GUIDANCE } = await import("./wallet-LX7SI5RE.js");
|
|
1086
1006
|
if (!hasSolanaWallet()) {
|
|
1087
1007
|
return {
|
|
1088
1008
|
configured: false,
|
|
1089
1009
|
how_to_configure: {
|
|
1090
|
-
|
|
1091
|
-
|
|
1010
|
+
recommended: "SOLO_SOLANA_KEYPAIR_ENCRYPTED_PATH + SOLO_SOLANA_KEYPAIR_PASSWORD_FILE \u2014 a keyfile encrypted with `openssl enc -aes-256-cbc -pbkdf2 -iter 10000 -md sha256`, plus a separate passphrase file (chmod 600 both)",
|
|
1011
|
+
alternative: "SOLO_SOLANA_KEYPAIR_PATH \u2014 path to a plaintext `solana-keygen` keyfile, e.g. ~/.config/solana/id.json (chmod 600)",
|
|
1012
|
+
key_storage: KEY_STORAGE_GUIDANCE
|
|
1092
1013
|
},
|
|
1093
1014
|
what_you_need: "SOL for transaction fees and account rent, plus the payout token for the budget. Rent is a refundable deposit, not a fee \u2014 most of it returns on close_task. Roughly $0.58 is locked per mission and about $0.24 is permanent, which buys the on-chain record that makes the escrow verifiable by anyone."
|
|
1094
1015
|
};
|
|
@@ -1099,7 +1020,7 @@ async function handleSolanaTool(name, args) {
|
|
|
1099
1020
|
const mint = args.mint ?? cfg.mints[symbol];
|
|
1100
1021
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1101
1022
|
const rpc = async (method, params) => {
|
|
1102
|
-
const res = await fetch(cfg
|
|
1023
|
+
const res = await fetch(solanaRpcUrl(cfg), {
|
|
1103
1024
|
method: "POST",
|
|
1104
1025
|
headers: { "Content-Type": "application/json" },
|
|
1105
1026
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
@@ -1128,23 +1049,87 @@ async function handleSolanaTool(name, args) {
|
|
|
1128
1049
|
};
|
|
1129
1050
|
}
|
|
1130
1051
|
case "fund_solana_mission": {
|
|
1131
|
-
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-
|
|
1132
|
-
const { verifyFundingTransaction } = await import("./verify-
|
|
1052
|
+
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-LX7SI5RE.js");
|
|
1053
|
+
const { verifyFundingTransaction, readEscrowConfig, readMintDecimals, SUPPORTED_ESCROW_INTERFACE } = await import("./verify-7EUVRUAS.js");
|
|
1133
1054
|
const wallet = await loadSolanaWallet();
|
|
1134
1055
|
const cfg = await apiGet2("/agent/solana/config");
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1056
|
+
if (cfg.escrow_interface !== SUPPORTED_ESCROW_INTERFACE) {
|
|
1057
|
+
return {
|
|
1058
|
+
funded: false,
|
|
1059
|
+
refused_to_sign: true,
|
|
1060
|
+
problems: [
|
|
1061
|
+
`the deployment reports escrow_interface ${JSON.stringify(cfg.escrow_interface)}; this version of @projectsolo/solo-mission-mcp verifies only '${SUPPORTED_ESCROW_INTERFACE}' funding transactions`
|
|
1062
|
+
],
|
|
1063
|
+
what_this_means: "Nothing was built, signed or escrowed. This server cannot decode the funding transaction the deployment would build, so it will not sign one. Use a version of this package that supports the reported escrow interface."
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
const pin = resolvePinnedProgram(cfg);
|
|
1067
|
+
const rpc = resolveTrustedRpc(cfg);
|
|
1068
|
+
if ("problem" in pin || "problem" in rpc) {
|
|
1069
|
+
return {
|
|
1070
|
+
funded: false,
|
|
1071
|
+
refused_to_sign: true,
|
|
1072
|
+
problems: [
|
|
1073
|
+
..."problem" in pin ? [pin.problem] : [],
|
|
1074
|
+
..."problem" in rpc ? [rpc.problem] : []
|
|
1075
|
+
],
|
|
1076
|
+
what_this_means: "Nothing was built, signed or escrowed."
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const rpcUrl = rpc.rpc_url;
|
|
1080
|
+
const { mission } = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1081
|
+
if (!mission) {
|
|
1082
|
+
return {
|
|
1083
|
+
funded: false,
|
|
1084
|
+
error: `Could not read mission ${args.mission_id} \u2014 call get_mission to check it exists.`
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const mint = args.expected_mint ?? mission.token_address ?? cfg.mints[Object.keys(cfg.mints)[0]];
|
|
1088
|
+
let decimals;
|
|
1089
|
+
try {
|
|
1090
|
+
decimals = await readMintDecimals(rpcUrl, mint);
|
|
1091
|
+
} catch (e) {
|
|
1092
|
+
return {
|
|
1093
|
+
funded: false,
|
|
1094
|
+
refused_to_sign: true,
|
|
1095
|
+
problems: [
|
|
1096
|
+
`cannot read mint ${mint}'s decimals from chain, which expected_budget is scaled by: ` + e.message
|
|
1097
|
+
],
|
|
1098
|
+
what_this_means: "Nothing was built, signed or escrowed."
|
|
1099
|
+
};
|
|
1100
|
+
}
|
|
1138
1101
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1139
1102
|
const built = await apiPost2(
|
|
1140
1103
|
`/agent/solana/missions/${args.mission_id}/funding-transaction`,
|
|
1141
1104
|
{ sponsor_wallet: wallet.publicKey, sponsor_token_account: tokenAccount }
|
|
1142
1105
|
);
|
|
1106
|
+
let onchainConfig = null;
|
|
1107
|
+
let onchainConfigError;
|
|
1108
|
+
try {
|
|
1109
|
+
onchainConfig = await readEscrowConfig(rpcUrl, pin.program_id);
|
|
1110
|
+
} catch (e) {
|
|
1111
|
+
onchainConfigError = e.message;
|
|
1112
|
+
}
|
|
1113
|
+
let quotedFeeBps;
|
|
1114
|
+
let feeQuoteSource;
|
|
1115
|
+
if (args.expected_max_fee_bps !== void 0) {
|
|
1116
|
+
quotedFeeBps = Number(args.expected_max_fee_bps);
|
|
1117
|
+
feeQuoteSource = "expected_max_fee_bps argument";
|
|
1118
|
+
} else if (typeof mission.solana_quoted_fee_bps === "number") {
|
|
1119
|
+
quotedFeeBps = mission.solana_quoted_fee_bps;
|
|
1120
|
+
feeQuoteSource = "mission.solana_quoted_fee_bps (the create_mission quote)";
|
|
1121
|
+
} else {
|
|
1122
|
+
quotedFeeBps = onchainConfig?.fee_bps;
|
|
1123
|
+
feeQuoteSource = "on-chain Config.fee_bps (the mission has no recorded quote)";
|
|
1124
|
+
}
|
|
1143
1125
|
const expectedBudgetRaw = toRawAmount(args.expected_budget, decimals);
|
|
1144
1126
|
const verdict = await verifyFundingTransaction({
|
|
1145
1127
|
transaction_base64: built.transaction_base64,
|
|
1128
|
+
escrow_interface: built.escrow_interface,
|
|
1129
|
+
task_id: String(built.task_id),
|
|
1146
1130
|
declared: built.declared,
|
|
1147
1131
|
accounts: built.accounts,
|
|
1132
|
+
message_sha256: built.message_sha256,
|
|
1148
1133
|
expected: {
|
|
1149
1134
|
budget_raw: expectedBudgetRaw,
|
|
1150
1135
|
// base_pool is derived by the backend from reward × max_humans. The agent's check on it
|
|
@@ -1152,21 +1137,30 @@ async function handleSolanaTool(name, args) {
|
|
|
1152
1137
|
// asserting a locally recomputed figure would require duplicating that arithmetic here
|
|
1153
1138
|
// and would fail on a legitimately rounded reward.
|
|
1154
1139
|
base_pool_raw: String(built.declared.base_pool),
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1140
|
+
// The rest is what create_mission recorded, not what the funding response says: whether
|
|
1141
|
+
// this is a lottery decides whether a co-signer is required at all.
|
|
1142
|
+
lottery_winner_count: Number(mission.lottery_winner_count ?? 0),
|
|
1143
|
+
lottery_prize_per_winner_raw: String(mission.lottery_prize_per_winner_raw ?? "0"),
|
|
1144
|
+
qualify_deadline: Number(mission.qualify_deadline),
|
|
1145
|
+
settlement_deadline: Number(mission.settlement_deadline),
|
|
1146
|
+
seed_commit: mission.seed_commit,
|
|
1147
|
+
max_fee_bps: quotedFeeBps,
|
|
1159
1148
|
mint,
|
|
1160
|
-
sponsor: wallet.publicKey
|
|
1149
|
+
sponsor: wallet.publicKey,
|
|
1150
|
+
sponsor_token_account: tokenAccount
|
|
1161
1151
|
},
|
|
1162
|
-
expected_program_id:
|
|
1152
|
+
expected_program_id: pin.program_id,
|
|
1153
|
+
onchain_config: onchainConfig,
|
|
1154
|
+
onchain_config_error: onchainConfigError
|
|
1163
1155
|
});
|
|
1164
1156
|
if (!verdict.ok) {
|
|
1165
1157
|
return {
|
|
1166
1158
|
funded: false,
|
|
1167
1159
|
refused_to_sign: true,
|
|
1168
1160
|
problems: verdict.problems,
|
|
1161
|
+
warnings: verdict.warnings,
|
|
1169
1162
|
summary: verdict.summary,
|
|
1163
|
+
fee_quote_source: feeQuoteSource,
|
|
1170
1164
|
what_this_means: "The transaction does not match what you asked for, so it was NOT signed and nothing was escrowed. This is the verifier doing its job. Do not retry blindly \u2014 the discrepancy above is either a bug or an attempt to have you authorise something else."
|
|
1171
1165
|
};
|
|
1172
1166
|
}
|
|
@@ -1177,22 +1171,49 @@ async function handleSolanaTool(name, args) {
|
|
|
1177
1171
|
verified: true,
|
|
1178
1172
|
task_id: built.task_id,
|
|
1179
1173
|
summary: verdict.summary,
|
|
1174
|
+
warnings: verdict.warnings,
|
|
1175
|
+
fee_quote_source: feeQuoteSource,
|
|
1180
1176
|
would_escrow: `${args.expected_budget} (${expectedBudgetRaw} raw) of ${mint}`
|
|
1181
1177
|
};
|
|
1182
1178
|
}
|
|
1183
1179
|
const signed = await signTransaction(built.transaction_base64, wallet);
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1180
|
+
let confirmed;
|
|
1181
|
+
try {
|
|
1182
|
+
confirmed = await apiPost2(`/agent/solana/missions/${args.mission_id}/confirm-funding`, {
|
|
1183
|
+
signed_transaction: signed,
|
|
1184
|
+
task_id: built.task_id
|
|
1185
|
+
});
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
const { escrowErrorFromMessage } = await import("./escrowErrors-4XSAYLGT.js");
|
|
1188
|
+
const body = apiErrorBody(e);
|
|
1189
|
+
const message = body.message ?? e?.message ?? String(e);
|
|
1190
|
+
return {
|
|
1191
|
+
funded: false,
|
|
1192
|
+
verified: true,
|
|
1193
|
+
signed: true,
|
|
1194
|
+
status: e?.status,
|
|
1195
|
+
message,
|
|
1196
|
+
program_error: escrowErrorFromMessage(message) ?? void 0,
|
|
1197
|
+
...body.funding_mismatch ? { funding_mismatch: body.funding_mismatch } : {},
|
|
1198
|
+
...body.task_id ? { task_id: body.task_id } : {}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
return {
|
|
1202
|
+
funded: true,
|
|
1203
|
+
verified: true,
|
|
1204
|
+
lottery: verdict.summary.lottery,
|
|
1205
|
+
operator_cosigned: verdict.summary.operator_signature === "valid",
|
|
1206
|
+
max_fee_bps: verdict.summary.max_fee_bps,
|
|
1207
|
+
...confirmed
|
|
1208
|
+
};
|
|
1189
1209
|
}
|
|
1190
1210
|
case "refund_solana_mission": {
|
|
1191
|
-
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-
|
|
1211
|
+
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-LX7SI5RE.js");
|
|
1212
|
+
const { escrowErrorFromMessage } = await import("./escrowErrors-4XSAYLGT.js");
|
|
1192
1213
|
const wallet = await loadSolanaWallet();
|
|
1193
1214
|
const cfg = await apiGet2("/agent/solana/config");
|
|
1194
|
-
const mission = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1195
|
-
const mint = mission
|
|
1215
|
+
const { mission } = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1216
|
+
const mint = mission?.token_address;
|
|
1196
1217
|
if (!mint) {
|
|
1197
1218
|
return {
|
|
1198
1219
|
refunded: false,
|
|
@@ -1200,6 +1221,11 @@ async function handleSolanaTool(name, args) {
|
|
|
1200
1221
|
};
|
|
1201
1222
|
}
|
|
1202
1223
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1224
|
+
const deadlines = {
|
|
1225
|
+
qualify_deadline: mission?.qualify_deadline,
|
|
1226
|
+
settlement_deadline: mission?.settlement_deadline,
|
|
1227
|
+
...mission?.qualify_deadline !== void 0 ? { cancel_cutoff: new Date(mission.qualify_deadline * 1e3).toISOString() } : {}
|
|
1228
|
+
};
|
|
1203
1229
|
if (!args.action) {
|
|
1204
1230
|
try {
|
|
1205
1231
|
await apiPost2(`/agent/solana/missions/${args.mission_id}/refund-transaction`, {
|
|
@@ -1209,31 +1235,69 @@ async function handleSolanaTool(name, args) {
|
|
|
1209
1235
|
});
|
|
1210
1236
|
return { available_actions: ["claim_refund"], note: "claim_refund is available now" };
|
|
1211
1237
|
} catch (e) {
|
|
1212
|
-
const body = e
|
|
1238
|
+
const body = apiErrorBody(e);
|
|
1213
1239
|
return {
|
|
1214
1240
|
available_actions: body.available_actions ?? [],
|
|
1215
1241
|
why_not_claim_refund: body.message,
|
|
1216
|
-
|
|
1242
|
+
...deadlines,
|
|
1243
|
+
note: (body.available_actions?.length ?? 0) === 0 ? `Nothing is refundable right now. ${CANCEL_CUTOFF_RULE}` : `Re-run with one of available_actions. ${CANCEL_CUTOFF_RULE}`
|
|
1217
1244
|
};
|
|
1218
1245
|
}
|
|
1219
1246
|
}
|
|
1220
|
-
const
|
|
1221
|
-
|
|
1222
|
-
{
|
|
1247
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1248
|
+
if (args.action === "cancel" && typeof mission?.qualify_deadline === "number" && nowSec >= mission.qualify_deadline) {
|
|
1249
|
+
return {
|
|
1250
|
+
refunded: false,
|
|
1251
|
+
rejected_by: "client",
|
|
1252
|
+
action: "cancel",
|
|
1253
|
+
message: `qualify_deadline (${deadlines.cancel_cutoff}) has passed by this machine's clock, so the escrow program would refuse this cancel (TooLateToCancel). Nothing was built or signed.`,
|
|
1254
|
+
...deadlines,
|
|
1255
|
+
what_to_do: CANCEL_CUTOFF_RULE
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
const pin = resolvePinnedProgram(cfg);
|
|
1259
|
+
if ("problem" in pin) {
|
|
1260
|
+
return {
|
|
1261
|
+
refunded: false,
|
|
1262
|
+
refused_to_sign: true,
|
|
1263
|
+
problems: [pin.problem],
|
|
1264
|
+
what_this_means: "Nothing was built, signed or moved."
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
let built;
|
|
1268
|
+
try {
|
|
1269
|
+
built = await apiPost2(
|
|
1270
|
+
`/agent/solana/missions/${args.mission_id}/refund-transaction`,
|
|
1271
|
+
{
|
|
1272
|
+
action: args.action,
|
|
1273
|
+
sponsor_wallet: wallet.publicKey,
|
|
1274
|
+
sponsor_token_account: tokenAccount
|
|
1275
|
+
}
|
|
1276
|
+
);
|
|
1277
|
+
} catch (e) {
|
|
1278
|
+
const body = apiErrorBody(e);
|
|
1279
|
+
const message = body.message ?? e?.message ?? String(e);
|
|
1280
|
+
return {
|
|
1281
|
+
refunded: false,
|
|
1282
|
+
rejected_by: "backend",
|
|
1223
1283
|
action: args.action,
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1284
|
+
status: e?.status,
|
|
1285
|
+
message,
|
|
1286
|
+
available_actions: body.available_actions ?? [],
|
|
1287
|
+
program_error: escrowErrorFromMessage(message) ?? void 0,
|
|
1288
|
+
...deadlines,
|
|
1289
|
+
what_to_do: CANCEL_CUTOFF_RULE
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
const { Transaction } = await import("@solana/web3.js");
|
|
1229
1293
|
const tx = Transaction.from(Buffer.from(built.transaction_base64, "base64"));
|
|
1230
1294
|
const problems = [];
|
|
1231
1295
|
if (tx.instructions.length !== 1) {
|
|
1232
1296
|
problems.push(`expected 1 instruction, found ${tx.instructions.length}`);
|
|
1233
1297
|
}
|
|
1234
1298
|
const ix = tx.instructions[0];
|
|
1235
|
-
if (ix?.programId?.toBase58() !==
|
|
1236
|
-
problems.push(`program is ${ix?.programId?.toBase58()}, expected ${
|
|
1299
|
+
if (ix?.programId?.toBase58() !== pin.program_id) {
|
|
1300
|
+
problems.push(`program is ${ix?.programId?.toBase58()}, expected ${pin.program_id}`);
|
|
1237
1301
|
}
|
|
1238
1302
|
const signers = (ix?.keys ?? []).filter((k) => k.isSigner).map((k) => k.pubkey.toBase58());
|
|
1239
1303
|
if (signers.length !== 1 || signers[0] !== wallet.publicKey) {
|
|
@@ -1248,7 +1312,6 @@ async function handleSolanaTool(name, args) {
|
|
|
1248
1312
|
if ((ix?.data?.length ?? 0) !== 8) {
|
|
1249
1313
|
problems.push(`instruction data is ${ix?.data?.length} bytes, expected 8`);
|
|
1250
1314
|
}
|
|
1251
|
-
void PublicKey;
|
|
1252
1315
|
if (problems.length > 0) {
|
|
1253
1316
|
return {
|
|
1254
1317
|
refunded: false,
|
|
@@ -1264,14 +1327,33 @@ async function handleSolanaTool(name, args) {
|
|
|
1264
1327
|
verified: true,
|
|
1265
1328
|
action: args.action,
|
|
1266
1329
|
task_id: built.task_id,
|
|
1267
|
-
destination: tokenAccount
|
|
1330
|
+
destination: tokenAccount,
|
|
1331
|
+
...built.cancel_deadline ? { cancel_deadline: built.cancel_deadline } : {}
|
|
1268
1332
|
};
|
|
1269
1333
|
}
|
|
1270
1334
|
const signed = await signTransaction(built.transaction_base64, wallet);
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1335
|
+
let confirmed;
|
|
1336
|
+
try {
|
|
1337
|
+
confirmed = await apiPost2(`/agent/solana/missions/${args.mission_id}/confirm-refund`, {
|
|
1338
|
+
signed_transaction: signed,
|
|
1339
|
+
action: args.action
|
|
1340
|
+
});
|
|
1341
|
+
} catch (e) {
|
|
1342
|
+
const body = apiErrorBody(e);
|
|
1343
|
+
const message = body.message ?? e?.message ?? String(e);
|
|
1344
|
+
const programError = escrowErrorFromMessage(message);
|
|
1345
|
+
return {
|
|
1346
|
+
refunded: false,
|
|
1347
|
+
rejected_by: programError || /transaction failed/i.test(message) ? "chain" : "backend",
|
|
1348
|
+
action: args.action,
|
|
1349
|
+
status: e?.status,
|
|
1350
|
+
message,
|
|
1351
|
+
program_error: programError ?? void 0,
|
|
1352
|
+
available_actions: body.available_actions,
|
|
1353
|
+
...deadlines,
|
|
1354
|
+
what_to_do: programError?.what_to_do ?? CANCEL_CUTOFF_RULE
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1275
1357
|
return { refunded: true, verified: true, ...confirmed };
|
|
1276
1358
|
}
|
|
1277
1359
|
default:
|