@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.
- 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 +239 -160
- 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 +45 -117
- 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";
|
|
@@ -61,7 +61,7 @@ var RESPONSE_SCHEMA_PROPERTY = {
|
|
|
61
61
|
var missionTools = [
|
|
62
62
|
{
|
|
63
63
|
name: "create_mission",
|
|
64
|
-
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.",
|
|
65
65
|
inputSchema: {
|
|
66
66
|
type: "object",
|
|
67
67
|
properties: {
|
|
@@ -72,8 +72,8 @@ var missionTools = [
|
|
|
72
72
|
},
|
|
73
73
|
chain: {
|
|
74
74
|
type: "string",
|
|
75
|
-
enum: ["
|
|
76
|
-
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)."
|
|
77
77
|
},
|
|
78
78
|
title: { type: "string", description: "Mission title (max 100 chars)" },
|
|
79
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." },
|
|
@@ -90,14 +90,14 @@ var missionTools = [
|
|
|
90
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." },
|
|
91
91
|
max_participants: { type: "number", minimum: 1, description: "Maximum number of participants" },
|
|
92
92
|
expires_in_hours: { type: "number", minimum: 1, description: "Hours until mission expires" },
|
|
93
|
-
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." },
|
|
94
94
|
max_humans: { type: "number", minimum: 1, description: "On-chain: maximum number of participants (both base-reward and lottery entrants)." },
|
|
95
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." },
|
|
96
96
|
reward_per_human: { type: "number", minimum: 0, description: "Deprecated \u2014 use base_reward instead." },
|
|
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.
|
|
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)." },
|
|
98
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." },
|
|
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. Backend floor is 60s (0.0166h)
|
|
100
|
-
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." },
|
|
101
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)." },
|
|
102
102
|
response_schema: RESPONSE_SCHEMA_PROPERTY
|
|
103
103
|
},
|
|
@@ -143,21 +143,9 @@ var missionTools = [
|
|
|
143
143
|
required: ["mission_id"]
|
|
144
144
|
}
|
|
145
145
|
},
|
|
146
|
-
{
|
|
147
|
-
name: "confirm_funding",
|
|
148
|
-
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.`,
|
|
149
|
-
inputSchema: {
|
|
150
|
-
type: "object",
|
|
151
|
-
properties: {
|
|
152
|
-
mission_id: { type: "string" },
|
|
153
|
-
tx_hash: { type: "string", description: "Transaction hash of createTask() call (optional)" }
|
|
154
|
-
},
|
|
155
|
-
required: ["mission_id"]
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
146
|
{
|
|
159
147
|
name: "hire_participant",
|
|
160
|
-
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.",
|
|
161
149
|
inputSchema: {
|
|
162
150
|
type: "object",
|
|
163
151
|
properties: {
|
|
@@ -169,7 +157,7 @@ var missionTools = [
|
|
|
169
157
|
},
|
|
170
158
|
{
|
|
171
159
|
name: "reject_participant",
|
|
172
|
-
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.",
|
|
173
161
|
inputSchema: {
|
|
174
162
|
type: "object",
|
|
175
163
|
properties: {
|
|
@@ -181,7 +169,7 @@ var missionTools = [
|
|
|
181
169
|
},
|
|
182
170
|
{
|
|
183
171
|
name: "finalize_qualification",
|
|
184
|
-
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.",
|
|
185
173
|
inputSchema: {
|
|
186
174
|
type: "object",
|
|
187
175
|
properties: {
|
|
@@ -197,7 +185,7 @@ var missionTools = [
|
|
|
197
185
|
},
|
|
198
186
|
{
|
|
199
187
|
name: "settle_mission",
|
|
200
|
-
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.",
|
|
201
189
|
inputSchema: {
|
|
202
190
|
type: "object",
|
|
203
191
|
properties: {
|
|
@@ -208,7 +196,7 @@ var missionTools = [
|
|
|
208
196
|
},
|
|
209
197
|
{
|
|
210
198
|
name: "cancel_mission",
|
|
211
|
-
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).",
|
|
212
200
|
inputSchema: {
|
|
213
201
|
type: "object",
|
|
214
202
|
properties: {
|
|
@@ -217,78 +205,9 @@ var missionTools = [
|
|
|
217
205
|
required: ["mission_id"]
|
|
218
206
|
}
|
|
219
207
|
},
|
|
220
|
-
{
|
|
221
|
-
name: "get_cancel_params",
|
|
222
|
-
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.",
|
|
223
|
-
inputSchema: {
|
|
224
|
-
type: "object",
|
|
225
|
-
properties: {
|
|
226
|
-
mission_id: { type: "string" }
|
|
227
|
-
},
|
|
228
|
-
required: ["mission_id"]
|
|
229
|
-
}
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: "confirm_cancel",
|
|
233
|
-
description: "After executing cancelTask() on EscrowVault, confirm the cancellation on the SOLO platform. Mission transitions to cancelled.",
|
|
234
|
-
inputSchema: {
|
|
235
|
-
type: "object",
|
|
236
|
-
properties: {
|
|
237
|
-
mission_id: { type: "string" },
|
|
238
|
-
tx_hash: { type: "string", description: "Transaction hash of cancelTask() (optional)" }
|
|
239
|
-
},
|
|
240
|
-
required: ["mission_id"]
|
|
241
|
-
}
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
name: "get_emergency_refund_params",
|
|
245
|
-
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.",
|
|
246
|
-
inputSchema: {
|
|
247
|
-
type: "object",
|
|
248
|
-
properties: {
|
|
249
|
-
mission_id: { type: "string" }
|
|
250
|
-
},
|
|
251
|
-
required: ["mission_id"]
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: "confirm_emergency_refund",
|
|
256
|
-
description: "After executing emergencyRefund() on EscrowVault, confirm on the SOLO platform. Mission transitions to cancelled.",
|
|
257
|
-
inputSchema: {
|
|
258
|
-
type: "object",
|
|
259
|
-
properties: {
|
|
260
|
-
mission_id: { type: "string" },
|
|
261
|
-
tx_hash: { type: "string", description: "Transaction hash of emergencyRefund() (optional)" }
|
|
262
|
-
},
|
|
263
|
-
required: ["mission_id"]
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: "get_refund_params",
|
|
268
|
-
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).",
|
|
269
|
-
inputSchema: {
|
|
270
|
-
type: "object",
|
|
271
|
-
properties: {
|
|
272
|
-
mission_id: { type: "string" }
|
|
273
|
-
},
|
|
274
|
-
required: ["mission_id"]
|
|
275
|
-
}
|
|
276
|
-
},
|
|
277
|
-
{
|
|
278
|
-
name: "confirm_refund",
|
|
279
|
-
description: "After executing claimRefund() on EscrowVault, confirm the refund on the SOLO platform. Mission transitions to refunded.",
|
|
280
|
-
inputSchema: {
|
|
281
|
-
type: "object",
|
|
282
|
-
properties: {
|
|
283
|
-
mission_id: { type: "string" },
|
|
284
|
-
tx_hash: { type: "string", description: "Transaction hash of claimRefund() (optional)" }
|
|
285
|
-
},
|
|
286
|
-
required: ["mission_id"]
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
208
|
{
|
|
290
209
|
name: "rate_participant",
|
|
291
|
-
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.",
|
|
292
211
|
inputSchema: {
|
|
293
212
|
type: "object",
|
|
294
213
|
properties: {
|
|
@@ -303,8 +222,10 @@ var missionTools = [
|
|
|
303
222
|
];
|
|
304
223
|
async function handleMissionTool(name, args) {
|
|
305
224
|
switch (name) {
|
|
306
|
-
case "create_mission":
|
|
307
|
-
|
|
225
|
+
case "create_mission": {
|
|
226
|
+
const { chain: _chain, ...body } = args;
|
|
227
|
+
return apiPost("/agent/missions", body.budget !== void 0 ? { ...body, chain: "solana" } : body);
|
|
228
|
+
}
|
|
308
229
|
case "list_missions": {
|
|
309
230
|
const params = {};
|
|
310
231
|
if (args.status) params.status = args.status;
|
|
@@ -316,8 +237,6 @@ async function handleMissionTool(name, args) {
|
|
|
316
237
|
return apiGet(`/agent/missions/${args.mission_id}`);
|
|
317
238
|
case "update_mission_questions":
|
|
318
239
|
return apiPatch(`/agent/missions/${args.mission_id}/questions`, { response_schema: args.response_schema });
|
|
319
|
-
case "confirm_funding":
|
|
320
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-funding`, { tx_hash: args.tx_hash });
|
|
321
240
|
case "hire_participant":
|
|
322
241
|
return apiPost(`/agent/missions/${args.mission_id}/participants/${args.uid}/hire`);
|
|
323
242
|
case "reject_participant":
|
|
@@ -330,18 +249,6 @@ async function handleMissionTool(name, args) {
|
|
|
330
249
|
return apiPost(`/agent/missions/${args.mission_id}/settle`);
|
|
331
250
|
case "cancel_mission":
|
|
332
251
|
return apiPost(`/agent/missions/${args.mission_id}/cancel`);
|
|
333
|
-
case "get_cancel_params":
|
|
334
|
-
return apiGet(`/agent/missions/${args.mission_id}/cancel-params`);
|
|
335
|
-
case "confirm_cancel":
|
|
336
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-cancel`, { tx_hash: args.tx_hash });
|
|
337
|
-
case "get_emergency_refund_params":
|
|
338
|
-
return apiGet(`/agent/missions/${args.mission_id}/emergency-refund-params`);
|
|
339
|
-
case "confirm_emergency_refund":
|
|
340
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-emergency-refund`, { tx_hash: args.tx_hash });
|
|
341
|
-
case "get_refund_params":
|
|
342
|
-
return apiGet(`/agent/missions/${args.mission_id}/refund-params`);
|
|
343
|
-
case "confirm_refund":
|
|
344
|
-
return apiPost(`/agent/missions/${args.mission_id}/confirm-refund`, { tx_hash: args.tx_hash });
|
|
345
252
|
case "rate_participant":
|
|
346
253
|
return apiPost(`/agent/missions/${args.mission_id}/participants/${args.uid}/comment`, {
|
|
347
254
|
rating: args.rating,
|
|
@@ -882,7 +789,7 @@ import { readFile } from "fs/promises";
|
|
|
882
789
|
var trackTools = [
|
|
883
790
|
{
|
|
884
791
|
name: "add_mission_track",
|
|
885
|
-
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.",
|
|
886
793
|
inputSchema: {
|
|
887
794
|
type: "object",
|
|
888
795
|
properties: {
|
|
@@ -1005,12 +912,12 @@ async function handleTrackTool(name, args) {
|
|
|
1005
912
|
var solanaTools = [
|
|
1006
913
|
{
|
|
1007
914
|
name: "get_solana_config",
|
|
1008
|
-
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.",
|
|
1009
916
|
inputSchema: { type: "object", properties: {} }
|
|
1010
917
|
},
|
|
1011
918
|
{
|
|
1012
919
|
name: "get_solana_wallet",
|
|
1013
|
-
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.",
|
|
1014
921
|
inputSchema: {
|
|
1015
922
|
type: "object",
|
|
1016
923
|
properties: {
|
|
@@ -1023,18 +930,23 @@ var solanaTools = [
|
|
|
1023
930
|
},
|
|
1024
931
|
{
|
|
1025
932
|
name: "fund_solana_mission",
|
|
1026
|
-
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.",
|
|
1027
934
|
inputSchema: {
|
|
1028
935
|
type: "object",
|
|
1029
936
|
properties: {
|
|
1030
|
-
mission_id: { type: "string", description:
|
|
937
|
+
mission_id: { type: "string", description: "A paid (budget-bearing) mission from create_mission, still in pending_funding." },
|
|
1031
938
|
expected_budget: {
|
|
1032
939
|
type: "number",
|
|
1033
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."
|
|
1034
941
|
},
|
|
1035
942
|
expected_mint: {
|
|
1036
943
|
type: "string",
|
|
1037
|
-
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."
|
|
1038
950
|
},
|
|
1039
951
|
dry_run: {
|
|
1040
952
|
type: "boolean",
|
|
@@ -1046,7 +958,7 @@ var solanaTools = [
|
|
|
1046
958
|
},
|
|
1047
959
|
{
|
|
1048
960
|
name: "refund_solana_mission",
|
|
1049
|
-
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.",
|
|
1050
962
|
inputSchema: {
|
|
1051
963
|
type: "object",
|
|
1052
964
|
properties: {
|
|
@@ -1079,19 +991,25 @@ function toRawAmount(amount, decimals) {
|
|
|
1079
991
|
return (whole + frac.padEnd(decimals, "0")).replace(/^0+(?=\d)/, "");
|
|
1080
992
|
}
|
|
1081
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.";
|
|
1082
998
|
async function handleSolanaTool(name, args) {
|
|
1083
|
-
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");
|
|
1084
1001
|
switch (name) {
|
|
1085
1002
|
case "get_solana_config":
|
|
1086
1003
|
return apiGet2("/agent/solana/config");
|
|
1087
1004
|
case "get_solana_wallet": {
|
|
1088
|
-
const { hasSolanaWallet, loadSolanaWallet, associatedTokenAddress } = await import("./wallet-
|
|
1005
|
+
const { hasSolanaWallet, loadSolanaWallet, associatedTokenAddress, KEY_STORAGE_GUIDANCE } = await import("./wallet-LX7SI5RE.js");
|
|
1089
1006
|
if (!hasSolanaWallet()) {
|
|
1090
1007
|
return {
|
|
1091
1008
|
configured: false,
|
|
1092
1009
|
how_to_configure: {
|
|
1093
|
-
|
|
1094
|
-
|
|
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
|
|
1095
1013
|
},
|
|
1096
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."
|
|
1097
1015
|
};
|
|
@@ -1102,7 +1020,7 @@ async function handleSolanaTool(name, args) {
|
|
|
1102
1020
|
const mint = args.mint ?? cfg.mints[symbol];
|
|
1103
1021
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1104
1022
|
const rpc = async (method, params) => {
|
|
1105
|
-
const res = await fetch(cfg
|
|
1023
|
+
const res = await fetch(solanaRpcUrl(cfg), {
|
|
1106
1024
|
method: "POST",
|
|
1107
1025
|
headers: { "Content-Type": "application/json" },
|
|
1108
1026
|
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
|
|
@@ -1131,23 +1049,87 @@ async function handleSolanaTool(name, args) {
|
|
|
1131
1049
|
};
|
|
1132
1050
|
}
|
|
1133
1051
|
case "fund_solana_mission": {
|
|
1134
|
-
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-
|
|
1135
|
-
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");
|
|
1136
1054
|
const wallet = await loadSolanaWallet();
|
|
1137
1055
|
const cfg = await apiGet2("/agent/solana/config");
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
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
|
+
}
|
|
1141
1101
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1142
1102
|
const built = await apiPost2(
|
|
1143
1103
|
`/agent/solana/missions/${args.mission_id}/funding-transaction`,
|
|
1144
1104
|
{ sponsor_wallet: wallet.publicKey, sponsor_token_account: tokenAccount }
|
|
1145
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
|
+
}
|
|
1146
1125
|
const expectedBudgetRaw = toRawAmount(args.expected_budget, decimals);
|
|
1147
1126
|
const verdict = await verifyFundingTransaction({
|
|
1148
1127
|
transaction_base64: built.transaction_base64,
|
|
1128
|
+
escrow_interface: built.escrow_interface,
|
|
1129
|
+
task_id: String(built.task_id),
|
|
1149
1130
|
declared: built.declared,
|
|
1150
1131
|
accounts: built.accounts,
|
|
1132
|
+
message_sha256: built.message_sha256,
|
|
1151
1133
|
expected: {
|
|
1152
1134
|
budget_raw: expectedBudgetRaw,
|
|
1153
1135
|
// base_pool is derived by the backend from reward × max_humans. The agent's check on it
|
|
@@ -1155,21 +1137,30 @@ async function handleSolanaTool(name, args) {
|
|
|
1155
1137
|
// asserting a locally recomputed figure would require duplicating that arithmetic here
|
|
1156
1138
|
// and would fail on a legitimately rounded reward.
|
|
1157
1139
|
base_pool_raw: String(built.declared.base_pool),
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
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,
|
|
1162
1148
|
mint,
|
|
1163
|
-
sponsor: wallet.publicKey
|
|
1149
|
+
sponsor: wallet.publicKey,
|
|
1150
|
+
sponsor_token_account: tokenAccount
|
|
1164
1151
|
},
|
|
1165
|
-
expected_program_id:
|
|
1152
|
+
expected_program_id: pin.program_id,
|
|
1153
|
+
onchain_config: onchainConfig,
|
|
1154
|
+
onchain_config_error: onchainConfigError
|
|
1166
1155
|
});
|
|
1167
1156
|
if (!verdict.ok) {
|
|
1168
1157
|
return {
|
|
1169
1158
|
funded: false,
|
|
1170
1159
|
refused_to_sign: true,
|
|
1171
1160
|
problems: verdict.problems,
|
|
1161
|
+
warnings: verdict.warnings,
|
|
1172
1162
|
summary: verdict.summary,
|
|
1163
|
+
fee_quote_source: feeQuoteSource,
|
|
1173
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."
|
|
1174
1165
|
};
|
|
1175
1166
|
}
|
|
@@ -1180,22 +1171,49 @@ async function handleSolanaTool(name, args) {
|
|
|
1180
1171
|
verified: true,
|
|
1181
1172
|
task_id: built.task_id,
|
|
1182
1173
|
summary: verdict.summary,
|
|
1174
|
+
warnings: verdict.warnings,
|
|
1175
|
+
fee_quote_source: feeQuoteSource,
|
|
1183
1176
|
would_escrow: `${args.expected_budget} (${expectedBudgetRaw} raw) of ${mint}`
|
|
1184
1177
|
};
|
|
1185
1178
|
}
|
|
1186
1179
|
const signed = await signTransaction(built.transaction_base64, wallet);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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
|
+
};
|
|
1192
1209
|
}
|
|
1193
1210
|
case "refund_solana_mission": {
|
|
1194
|
-
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");
|
|
1195
1213
|
const wallet = await loadSolanaWallet();
|
|
1196
1214
|
const cfg = await apiGet2("/agent/solana/config");
|
|
1197
|
-
const mission = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1198
|
-
const mint = mission
|
|
1215
|
+
const { mission } = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1216
|
+
const mint = mission?.token_address;
|
|
1199
1217
|
if (!mint) {
|
|
1200
1218
|
return {
|
|
1201
1219
|
refunded: false,
|
|
@@ -1203,6 +1221,11 @@ async function handleSolanaTool(name, args) {
|
|
|
1203
1221
|
};
|
|
1204
1222
|
}
|
|
1205
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
|
+
};
|
|
1206
1229
|
if (!args.action) {
|
|
1207
1230
|
try {
|
|
1208
1231
|
await apiPost2(`/agent/solana/missions/${args.mission_id}/refund-transaction`, {
|
|
@@ -1212,31 +1235,69 @@ async function handleSolanaTool(name, args) {
|
|
|
1212
1235
|
});
|
|
1213
1236
|
return { available_actions: ["claim_refund"], note: "claim_refund is available now" };
|
|
1214
1237
|
} catch (e) {
|
|
1215
|
-
const body = e
|
|
1238
|
+
const body = apiErrorBody(e);
|
|
1216
1239
|
return {
|
|
1217
1240
|
available_actions: body.available_actions ?? [],
|
|
1218
1241
|
why_not_claim_refund: body.message,
|
|
1219
|
-
|
|
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}`
|
|
1220
1244
|
};
|
|
1221
1245
|
}
|
|
1222
1246
|
}
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
{
|
|
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",
|
|
1226
1283
|
action: args.action,
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
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");
|
|
1232
1293
|
const tx = Transaction.from(Buffer.from(built.transaction_base64, "base64"));
|
|
1233
1294
|
const problems = [];
|
|
1234
1295
|
if (tx.instructions.length !== 1) {
|
|
1235
1296
|
problems.push(`expected 1 instruction, found ${tx.instructions.length}`);
|
|
1236
1297
|
}
|
|
1237
1298
|
const ix = tx.instructions[0];
|
|
1238
|
-
if (ix?.programId?.toBase58() !==
|
|
1239
|
-
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}`);
|
|
1240
1301
|
}
|
|
1241
1302
|
const signers = (ix?.keys ?? []).filter((k) => k.isSigner).map((k) => k.pubkey.toBase58());
|
|
1242
1303
|
if (signers.length !== 1 || signers[0] !== wallet.publicKey) {
|
|
@@ -1251,7 +1312,6 @@ async function handleSolanaTool(name, args) {
|
|
|
1251
1312
|
if ((ix?.data?.length ?? 0) !== 8) {
|
|
1252
1313
|
problems.push(`instruction data is ${ix?.data?.length} bytes, expected 8`);
|
|
1253
1314
|
}
|
|
1254
|
-
void PublicKey;
|
|
1255
1315
|
if (problems.length > 0) {
|
|
1256
1316
|
return {
|
|
1257
1317
|
refunded: false,
|
|
@@ -1267,14 +1327,33 @@ async function handleSolanaTool(name, args) {
|
|
|
1267
1327
|
verified: true,
|
|
1268
1328
|
action: args.action,
|
|
1269
1329
|
task_id: built.task_id,
|
|
1270
|
-
destination: tokenAccount
|
|
1330
|
+
destination: tokenAccount,
|
|
1331
|
+
...built.cancel_deadline ? { cancel_deadline: built.cancel_deadline } : {}
|
|
1271
1332
|
};
|
|
1272
1333
|
}
|
|
1273
1334
|
const signed = await signTransaction(built.transaction_base64, wallet);
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
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
|
+
}
|
|
1278
1357
|
return { refunded: true, verified: true, ...confirmed };
|
|
1279
1358
|
}
|
|
1280
1359
|
default:
|