@projectsolo/solo-mission-mcp 0.21.8 → 0.21.10
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.
|
@@ -51,6 +51,16 @@ async function apiPost(path, body) {
|
|
|
51
51
|
if (!response.ok) return parseErrorResponse(response);
|
|
52
52
|
return response.json();
|
|
53
53
|
}
|
|
54
|
+
async function apiPatch(path, body) {
|
|
55
|
+
const response = await fetch(`${config.apiUrl}${path}`, {
|
|
56
|
+
method: "PATCH",
|
|
57
|
+
headers: DEFAULT_HEADERS,
|
|
58
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
59
|
+
signal: AbortSignal.timeout(3e4)
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) return parseErrorResponse(response);
|
|
62
|
+
return response.json();
|
|
63
|
+
}
|
|
54
64
|
async function apiDelete(path) {
|
|
55
65
|
const response = await fetch(`${config.apiUrl}${path}`, {
|
|
56
66
|
method: "DELETE",
|
|
@@ -74,6 +84,7 @@ async function publicApiPost(path, body) {
|
|
|
74
84
|
export {
|
|
75
85
|
apiGet,
|
|
76
86
|
apiPost,
|
|
87
|
+
apiPatch,
|
|
77
88
|
apiDelete,
|
|
78
89
|
publicApiPost
|
|
79
90
|
};
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
apiDelete,
|
|
4
4
|
apiGet,
|
|
5
|
+
apiPatch,
|
|
5
6
|
apiPost,
|
|
6
7
|
publicApiPost
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-TKRT2V2W.js";
|
|
8
9
|
|
|
9
10
|
// src/index.ts
|
|
10
11
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -15,6 +16,32 @@ import {
|
|
|
15
16
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
16
17
|
|
|
17
18
|
// src/tools/missions.ts
|
|
19
|
+
var RESPONSE_SCHEMA_PROPERTY = {
|
|
20
|
+
type: "array",
|
|
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: false } \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
|
+
items: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
id: { type: "string", description: "Unique id within this schema \u2014 the key answers are stored under. Duplicates are rejected, not merged." },
|
|
27
|
+
kind: {
|
|
28
|
+
type: "string",
|
|
29
|
+
enum: ["single", "multi", "likert", "stars", "short", "long", "dropdown", "consent"],
|
|
30
|
+
description: 'single=one choice, multi=several choices, likert=a scale rendered as a row of buttons (NOT fixed at 5 points \u2014 the number of options you supply IS the number of points, so give exactly as many option labels as the scale should have; a common convention is 5 labels like "1 Strongly disagree".."5 Strongly agree" but nothing enforces that), stars=1-5 rating (fixed, no options), short=one line of text, long=paragraph text (see min_length), dropdown=one choice from a long list, consent=a checkbox that must be explicitly true \u2014 the checkbox\'s own clickable text is `label` itself (there is no separate heading rendered above it, unlike every other kind); use `help` for any additional wording you want shown near it.'
|
|
31
|
+
},
|
|
32
|
+
label: { type: "string", description: "Question text shown to the human (max 200 chars). For the consent kind this is the checkbox's own clickable text, not a separate heading." },
|
|
33
|
+
help: { type: "string", description: "Optional helper text shown under the label (max 500 chars)." },
|
|
34
|
+
required: { type: "boolean", description: "Whether this question must be answered to complete the mission." },
|
|
35
|
+
options: {
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
description: "Required (non-empty) for single/multi/dropdown/likert (max 20 options, 50 chars each). Not accepted by other kinds."
|
|
39
|
+
},
|
|
40
|
+
min_length: { type: "number", description: "Minimum answer length. Only read for the 'long' kind \u2014 silently ignored (not rejected) if set on any other kind." }
|
|
41
|
+
},
|
|
42
|
+
required: ["id", "kind", "label", "required"]
|
|
43
|
+
}
|
|
44
|
+
};
|
|
18
45
|
var missionTools = [
|
|
19
46
|
{
|
|
20
47
|
name: "create_mission",
|
|
@@ -55,11 +82,24 @@ var missionTools = [
|
|
|
55
82
|
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." },
|
|
56
83
|
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), same for every chain \u2014 see work_duration_hours for the reasoning." },
|
|
57
84
|
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) for every chain \u2014 this is a flat sanity check against a near-zero window, not a guarantee of a legal settlement window on every chain: Base's own EscrowVault contract separately enforces its own fixed 1-hour minimum regardless of what this floor allows through, so a too-short Base mission still fails downstream (at finalize_qualification, or on-chain) instead of being caught at creation." },
|
|
58
|
-
auto_accept_applicants: { type: "boolean", description: "When true, applicants are automatically hired when they apply \u2014 no manual hire_participant call needed. First-come first-served up to max_humans. Face verification is still required. Ideal for open media_review missions." }
|
|
85
|
+
auto_accept_applicants: { type: "boolean", description: "When true, applicants are automatically hired when they apply \u2014 no manual hire_participant call needed. First-come first-served up to max_humans. Face verification is still required. Ideal for open media_review missions." },
|
|
86
|
+
response_schema: RESPONSE_SCHEMA_PROPERTY
|
|
59
87
|
},
|
|
60
88
|
required: ["type", "title", "description"]
|
|
61
89
|
}
|
|
62
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: "update_mission_questions",
|
|
93
|
+
description: "Set or replace a mission's response_schema after creation. Locked once hiring starts \u2014 same window as add_mission_track (on-chain: only before funding; off-chain: only while active with zero hired participants yet). This window is what makes a schema replacement safe: a human can only submit answers once hired, and the schema can no longer change once anyone has been hired \u2014 there is no state where a human has answered under one schema and the mission later presents a different one. Replaces the entire schema, not a merge.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
mission_id: { type: "string" },
|
|
98
|
+
response_schema: RESPONSE_SCHEMA_PROPERTY
|
|
99
|
+
},
|
|
100
|
+
required: ["mission_id", "response_schema"]
|
|
101
|
+
}
|
|
102
|
+
},
|
|
63
103
|
{
|
|
64
104
|
name: "list_missions",
|
|
65
105
|
description: "List missions created by this agent. Filter by status to manage your mission portfolio.",
|
|
@@ -125,7 +165,7 @@ var missionTools = [
|
|
|
125
165
|
},
|
|
126
166
|
{
|
|
127
167
|
name: "finalize_qualification",
|
|
128
|
-
description: "Lock in the qualified participants after reviewing their work.
|
|
168
|
+
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 calls finalizeQualification() on EscrowVault. Mission transitions to qualifying. If you never call this, the platform does it for you shortly after the hiring window closes, qualifying every hired participant you did not explicitly reject_participant (silence = go) \u2014 call reject_participant first for anyone whose work should NOT be paid.",
|
|
129
169
|
inputSchema: {
|
|
130
170
|
type: "object",
|
|
131
171
|
properties: {
|
|
@@ -133,7 +173,7 @@ var missionTools = [
|
|
|
133
173
|
qualified_human_uids: {
|
|
134
174
|
type: "array",
|
|
135
175
|
items: { type: "string" },
|
|
136
|
-
description: "Firebase UIDs of humans whose work was accepted. Ignored for
|
|
176
|
+
description: "Firebase UIDs of humans whose work was accepted. Ignored for any mission with a response_schema (auto-derived from completion instead)."
|
|
137
177
|
}
|
|
138
178
|
},
|
|
139
179
|
required: ["mission_id"]
|
|
@@ -258,6 +298,8 @@ async function handleMissionTool(name, args) {
|
|
|
258
298
|
}
|
|
259
299
|
case "get_mission":
|
|
260
300
|
return apiGet(`/agent/missions/${args.mission_id}`);
|
|
301
|
+
case "update_mission_questions":
|
|
302
|
+
return apiPatch(`/agent/missions/${args.mission_id}/questions`, { response_schema: args.response_schema });
|
|
261
303
|
case "confirm_funding":
|
|
262
304
|
return apiPost(`/agent/missions/${args.mission_id}/confirm-funding`, { tx_hash: args.tx_hash });
|
|
263
305
|
case "hire_participant":
|
|
@@ -999,7 +1041,7 @@ function toRawAmount(amount, decimals) {
|
|
|
999
1041
|
}
|
|
1000
1042
|
var SOLANA_TOOL_NAMES = new Set(solanaTools.map((t) => t.name));
|
|
1001
1043
|
async function handleSolanaTool(name, args) {
|
|
1002
|
-
const { apiGet: apiGet2, apiPost: apiPost2 } = await import("./client-
|
|
1044
|
+
const { apiGet: apiGet2, apiPost: apiPost2 } = await import("./client-LDWK5HLP.js");
|
|
1003
1045
|
switch (name) {
|
|
1004
1046
|
case "get_solana_config":
|
|
1005
1047
|
return apiGet2("/agent/solana/config");
|
|
@@ -1113,7 +1155,14 @@ async function handleSolanaTool(name, args) {
|
|
|
1113
1155
|
const { loadSolanaWallet, associatedTokenAddress, signTransaction } = await import("./wallet-IUQWBW6F.js");
|
|
1114
1156
|
const wallet = await loadSolanaWallet();
|
|
1115
1157
|
const cfg = await apiGet2("/agent/solana/config");
|
|
1116
|
-
const
|
|
1158
|
+
const mission = await apiGet2(`/agent/missions/${args.mission_id}`);
|
|
1159
|
+
const mint = mission.mission?.token_address;
|
|
1160
|
+
if (!mint) {
|
|
1161
|
+
return {
|
|
1162
|
+
refunded: false,
|
|
1163
|
+
error: `Could not resolve mission ${args.mission_id}'s token_address \u2014 call get_mission to check it exists and is funded.`
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1117
1166
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
1118
1167
|
if (!args.action) {
|
|
1119
1168
|
try {
|
package/package.json
CHANGED
package/src/api/client.ts
CHANGED
|
@@ -48,6 +48,17 @@ export async function apiPost<T>(path: string, body?: unknown): Promise<T> {
|
|
|
48
48
|
return response.json() as Promise<T>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
export async function apiPatch<T>(path: string, body?: unknown): Promise<T> {
|
|
52
|
+
const response = await fetch(`${config.apiUrl}${path}`, {
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
headers: DEFAULT_HEADERS,
|
|
55
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
56
|
+
signal: AbortSignal.timeout(30_000),
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) return parseErrorResponse(response);
|
|
59
|
+
return response.json() as Promise<T>;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
export async function apiDelete<T>(path: string): Promise<T> {
|
|
52
63
|
const response = await fetch(`${config.apiUrl}${path}`, {
|
|
53
64
|
method: 'DELETE',
|
package/src/tools/missions.ts
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { apiGet, apiPost } from '../api/client.js';
|
|
2
|
+
import { apiGet, apiPatch, apiPost } from '../api/client.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared JSON-schema for Mission.response_schema, used by both create_mission (set at creation)
|
|
6
|
+
* and update_mission_questions (set/replace afterward, pre-hiring). v1 kinds only — text/choice/
|
|
7
|
+
* rating input; kinds needing video-stimulus/gating infra (ab/slider/rank/tags/timestamps/upload)
|
|
8
|
+
* aren't supported yet.
|
|
9
|
+
*/
|
|
10
|
+
const RESPONSE_SCHEMA_PROPERTY = {
|
|
11
|
+
type: 'array' as const,
|
|
12
|
+
maxItems: 20,
|
|
13
|
+
description:
|
|
14
|
+
'Structured completion questions a human answers to complete the mission. Question ids must ' +
|
|
15
|
+
'be unique within one schema — a duplicate id is rejected at creation/update, not merged or ' +
|
|
16
|
+
'silently overwritten. When set, finalize_qualification auto-derives qualified UIDs from ' +
|
|
17
|
+
'completion instead of requiring an explicit qualified_human_uids list — same mechanism ' +
|
|
18
|
+
'media_review has always used for track ratings, generalized to any mission type. ' +
|
|
19
|
+
'media_review defaults to a canonical two-question schema when this is omitted at creation — ' +
|
|
20
|
+
'literally { id: "rating", kind: "stars", required: true } and { id: "comment", kind: "long", ' +
|
|
21
|
+
'required: false } — so read/append against those exact ids if you rely on the default rather ' +
|
|
22
|
+
'than supplying your own schema. Every other type has no default (stays fully manual/chat-based ' +
|
|
23
|
+
'unless you set this).',
|
|
24
|
+
items: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
properties: {
|
|
27
|
+
id: { type: 'string', description: 'Unique id within this schema — the key answers are stored under. Duplicates are rejected, not merged.' },
|
|
28
|
+
kind: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
enum: ['single', 'multi', 'likert', 'stars', 'short', 'long', 'dropdown', 'consent'],
|
|
31
|
+
description:
|
|
32
|
+
'single=one choice, multi=several choices, likert=a scale rendered as a row of buttons (NOT ' +
|
|
33
|
+
'fixed at 5 points — the number of options you supply IS the number of points, so give exactly ' +
|
|
34
|
+
'as many option labels as the scale should have; a common convention is 5 labels like "1 ' +
|
|
35
|
+
'Strongly disagree".."5 Strongly agree" but nothing enforces that), stars=1-5 rating (fixed, ' +
|
|
36
|
+
'no options), short=one line of text, long=paragraph text (see min_length), dropdown=one ' +
|
|
37
|
+
'choice from a long list, consent=a checkbox that must be explicitly true — the checkbox\'s ' +
|
|
38
|
+
'own clickable text is `label` itself (there is no separate heading rendered above it, unlike ' +
|
|
39
|
+
'every other kind); use `help` for any additional wording you want shown near it.',
|
|
40
|
+
},
|
|
41
|
+
label: { type: 'string', description: 'Question text shown to the human (max 200 chars). For the consent kind this is the checkbox\'s own clickable text, not a separate heading.' },
|
|
42
|
+
help: { type: 'string', description: 'Optional helper text shown under the label (max 500 chars).' },
|
|
43
|
+
required: { type: 'boolean', description: 'Whether this question must be answered to complete the mission.' },
|
|
44
|
+
options: {
|
|
45
|
+
type: 'array',
|
|
46
|
+
items: { type: 'string' },
|
|
47
|
+
description: 'Required (non-empty) for single/multi/dropdown/likert (max 20 options, 50 chars each). Not accepted by other kinds.',
|
|
48
|
+
},
|
|
49
|
+
min_length: { type: 'number', description: "Minimum answer length. Only read for the 'long' kind — silently ignored (not rejected) if set on any other kind." },
|
|
50
|
+
},
|
|
51
|
+
required: ['id', 'kind', 'label', 'required'],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
3
54
|
|
|
4
55
|
export const missionTools: Tool[] = [
|
|
5
56
|
{
|
|
@@ -42,10 +93,29 @@ export const missionTools: Tool[] = [
|
|
|
42
93
|
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), same for every chain — see work_duration_hours for the reasoning.' },
|
|
43
94
|
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) for every chain — this is a flat sanity check against a near-zero window, not a guarantee of a legal settlement window on every chain: Base\'s own EscrowVault contract separately enforces its own fixed 1-hour minimum regardless of what this floor allows through, so a too-short Base mission still fails downstream (at finalize_qualification, or on-chain) instead of being caught at creation.' },
|
|
44
95
|
auto_accept_applicants: { type: 'boolean', description: 'When true, applicants are automatically hired when they apply — no manual hire_participant call needed. First-come first-served up to max_humans. Face verification is still required. Ideal for open media_review missions.' },
|
|
96
|
+
response_schema: RESPONSE_SCHEMA_PROPERTY,
|
|
45
97
|
},
|
|
46
98
|
required: ['type', 'title', 'description'],
|
|
47
99
|
},
|
|
48
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: 'update_mission_questions',
|
|
103
|
+
description:
|
|
104
|
+
"Set or replace a mission's response_schema after creation. Locked once hiring starts — " +
|
|
105
|
+
'same window as add_mission_track (on-chain: only before funding; off-chain: only while ' +
|
|
106
|
+
'active with zero hired participants yet). This window is what makes a schema replacement ' +
|
|
107
|
+
'safe: a human can only submit answers once hired, and the schema can no longer change once ' +
|
|
108
|
+
'anyone has been hired — there is no state where a human has answered under one schema and ' +
|
|
109
|
+
'the mission later presents a different one. Replaces the entire schema, not a merge.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
mission_id: { type: 'string' },
|
|
114
|
+
response_schema: RESPONSE_SCHEMA_PROPERTY,
|
|
115
|
+
},
|
|
116
|
+
required: ['mission_id', 'response_schema'],
|
|
117
|
+
},
|
|
118
|
+
},
|
|
49
119
|
{
|
|
50
120
|
name: 'list_missions',
|
|
51
121
|
description: 'List missions created by this agent. Filter by status to manage your mission portfolio.',
|
|
@@ -111,7 +181,7 @@ export const missionTools: Tool[] = [
|
|
|
111
181
|
},
|
|
112
182
|
{
|
|
113
183
|
name: 'finalize_qualification',
|
|
114
|
-
description: 'Lock in the qualified participants after reviewing their work.
|
|
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 — 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 calls finalizeQualification() on EscrowVault. Mission transitions to qualifying. If you never call this, the platform does it for you shortly after the hiring window closes, qualifying every hired participant you did not explicitly reject_participant (silence = go) — call reject_participant first for anyone whose work should NOT be paid.',
|
|
115
185
|
inputSchema: {
|
|
116
186
|
type: 'object',
|
|
117
187
|
properties: {
|
|
@@ -119,7 +189,7 @@ export const missionTools: Tool[] = [
|
|
|
119
189
|
qualified_human_uids: {
|
|
120
190
|
type: 'array',
|
|
121
191
|
items: { type: 'string' },
|
|
122
|
-
description: 'Firebase UIDs of humans whose work was accepted. Ignored for
|
|
192
|
+
description: 'Firebase UIDs of humans whose work was accepted. Ignored for any mission with a response_schema (auto-derived from completion instead).',
|
|
123
193
|
},
|
|
124
194
|
},
|
|
125
195
|
required: ['mission_id'],
|
|
@@ -248,6 +318,9 @@ export async function handleMissionTool(name: string, args: Record<string, any>)
|
|
|
248
318
|
case 'get_mission':
|
|
249
319
|
return apiGet(`/agent/missions/${args.mission_id}`);
|
|
250
320
|
|
|
321
|
+
case 'update_mission_questions':
|
|
322
|
+
return apiPatch(`/agent/missions/${args.mission_id}/questions`, { response_schema: args.response_schema });
|
|
323
|
+
|
|
251
324
|
case 'confirm_funding':
|
|
252
325
|
return apiPost(`/agent/missions/${args.mission_id}/confirm-funding`, { tx_hash: args.tx_hash });
|
|
253
326
|
|
package/src/tools/solana.ts
CHANGED
|
@@ -282,7 +282,22 @@ export async function handleSolanaTool(
|
|
|
282
282
|
program_id: string;
|
|
283
283
|
mints: Record<string, string>;
|
|
284
284
|
};
|
|
285
|
-
|
|
285
|
+
// The mission's ACTUAL mint, not a guessed default — get_solana_config's mints always
|
|
286
|
+
// includes both the production payout mint (USDC) and TEST_USDC, and a mission is funded
|
|
287
|
+
// in exactly one of them. Guessing (the previous code preferred TEST_USDC whenever present)
|
|
288
|
+
// computes the wrong associated-token-account for any mission funded in the other one, and
|
|
289
|
+
// the refund/cancel/emergency-refund transaction then fails on-chain with
|
|
290
|
+
// AccountNotInitialized — confirmed live against a real USDC-funded mission.
|
|
291
|
+
const mission = (await apiGet(`/agent/missions/${args.mission_id}`)) as {
|
|
292
|
+
mission?: { token_address?: string };
|
|
293
|
+
};
|
|
294
|
+
const mint = mission.mission?.token_address;
|
|
295
|
+
if (!mint) {
|
|
296
|
+
return {
|
|
297
|
+
refunded: false,
|
|
298
|
+
error: `Could not resolve mission ${args.mission_id}'s token_address — call get_mission to check it exists and is funded.`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
286
301
|
const tokenAccount = await associatedTokenAddress(mint, wallet.publicKey);
|
|
287
302
|
|
|
288
303
|
// No action: ask what is legal rather than guessing and getting a 409. Uses a deliberately
|