@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/src/solana/verify.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Decodes a funding transaction and checks it against what the agent asked for.
|
|
3
3
|
*
|
|
4
|
-
* WHY THIS IS MANDATORY, NOT DEFENSIVE.
|
|
5
|
-
* parameters
|
|
6
|
-
* signing
|
|
7
|
-
*
|
|
8
|
-
* base64 blob.
|
|
4
|
+
* WHY THIS IS MANDATORY, NOT DEFENSIVE. An agent that built the funding instruction itself from
|
|
5
|
+
* published parameters could check every field before signing. Solana funding instead uses partial
|
|
6
|
+
* signing — the backend builds, the agent signs, the backend submits — which removes a whole class
|
|
7
|
+
* of integration breakage but hands the agent an opaque base64 blob.
|
|
9
8
|
*
|
|
10
9
|
* A human signing in Phantom gets that blob decoded for free by their wallet UI. **An agent has no
|
|
11
10
|
* wallet UI.** So without this, "the agent authorises exactly what it signs" is technically true and
|
|
@@ -13,12 +12,24 @@
|
|
|
13
12
|
* this the least settled decision in the whole plan and makes the verifier a requirement of the
|
|
14
13
|
* flow, not an optional extra.
|
|
15
14
|
*
|
|
16
|
-
* WHAT THIS DOES AND DOES NOT PROVE. It
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* WHAT THIS DOES AND DOES NOT PROVE. It decodes the instruction data and compares every field with
|
|
16
|
+
* the agent's own expectations, and it checks every account the instruction touches, by position,
|
|
17
|
+
* against addresses it derives itself. So it catches a backend that quoted one budget and built
|
|
18
|
+
* another, a substituted mint, a redirected vault, a raised fee ceiling, a co-signer that is not
|
|
19
|
+
* the program's Operator, or a different program entirely. It does NOT prove the backend will
|
|
20
|
+
* submit the transaction it showed you — but it cannot usefully alter one afterwards either,
|
|
21
|
+
* because any change invalidates the signature.
|
|
22
|
+
*
|
|
23
|
+
* ESCROW INTERFACE v2 ONLY (solo-escrow-solana u1.1.0, `main` 4376bac). Relative to v1:
|
|
24
|
+
* - `create_task` data gains a trailing `max_fee_bps: u16` (N-2). It must equal the fee rate
|
|
25
|
+
* the agent was quoted, so the platform cannot raise the fee between quote and execution.
|
|
26
|
+
* - The accounts gain an optional `operator` signer after `system_program` (SE-02). It is the
|
|
27
|
+
* program id — Anchor's `None` — for a non-lottery task, and the Operator, as a signer, for a
|
|
28
|
+
* lottery task. A lottery transaction arrives already co-signed by the Operator; this checks
|
|
29
|
+
* that the co-signer is the on-chain `Config.operator` and that its ed25519 signature is valid
|
|
30
|
+
* over the exact message the agent is about to sign.
|
|
31
|
+
* A deployment reporting any other interface is refused rather than decoded against the wrong
|
|
32
|
+
* layout.
|
|
22
33
|
*
|
|
23
34
|
* This reintroduces a dependency on the instruction layout, which partial signing was meant to
|
|
24
35
|
* remove. The difference is where that dependency lives: inside a package we version and ship,
|
|
@@ -26,8 +37,60 @@
|
|
|
26
37
|
* honest to say the shape dependency is reduced rather than eliminated.
|
|
27
38
|
*/
|
|
28
39
|
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
import { createHash } from 'crypto';
|
|
41
|
+
import {
|
|
42
|
+
base58Decode,
|
|
43
|
+
base58Encode,
|
|
44
|
+
ed25519Verify,
|
|
45
|
+
isEmptySignature,
|
|
46
|
+
isSignerIndex,
|
|
47
|
+
isWritableIndex,
|
|
48
|
+
parseWireTransaction,
|
|
49
|
+
type WireTransaction,
|
|
50
|
+
} from './wire.js';
|
|
51
|
+
|
|
52
|
+
/** The only program interface this verifier decodes. */
|
|
53
|
+
export const SUPPORTED_ESCROW_INTERFACE = 'v2';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Discriminators from solo-escrow-solana u1.1.0 `idl/solo_escrow.json`. Byte-checked against the
|
|
57
|
+
* vendored IDL excerpt in `verify.test.ts`.
|
|
58
|
+
*/
|
|
59
|
+
export const DISCRIMINATORS = {
|
|
60
|
+
create_task: [194, 80, 6, 180, 232, 127, 48, 171],
|
|
61
|
+
cancel_task: [69, 228, 134, 187, 134, 105, 238, 48],
|
|
62
|
+
emergency_refund: [188, 73, 52, 195, 137, 70, 180, 147],
|
|
63
|
+
claim_refund: [15, 16, 30, 161, 255, 228, 97, 60],
|
|
64
|
+
/** The `Config` ACCOUNT discriminator (first 8 bytes of its data). */
|
|
65
|
+
Config: [155, 12, 170, 224, 30, 250, 204, 130],
|
|
66
|
+
} as const;
|
|
67
|
+
|
|
68
|
+
/** `create_task`'s accounts in IDL order (v2). The instruction must list exactly these. */
|
|
69
|
+
export const CREATE_TASK_V2_ACCOUNTS = [
|
|
70
|
+
'sponsor',
|
|
71
|
+
'config',
|
|
72
|
+
'whitelist',
|
|
73
|
+
'mint',
|
|
74
|
+
'task',
|
|
75
|
+
'vault',
|
|
76
|
+
'sponsor_token_account',
|
|
77
|
+
'token_program',
|
|
78
|
+
'system_program',
|
|
79
|
+
'operator',
|
|
80
|
+
'event_authority',
|
|
81
|
+
'program',
|
|
82
|
+
] as const;
|
|
83
|
+
|
|
84
|
+
/** 8 discriminator | budget u64 | base_pool u64 | lottery_winner_count u32
|
|
85
|
+
* | lottery_prize_per_winner u64 | qualify_deadline i64 | settlement_deadline i64
|
|
86
|
+
* | seed_commit [u8; 32] | max_fee_bps u16 */
|
|
87
|
+
export const CREATE_TASK_V2_DATA_LENGTH = 8 + 8 + 8 + 4 + 8 + 8 + 8 + 32 + 2;
|
|
88
|
+
|
|
89
|
+
export const TOKEN_PROGRAM_ID = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
|
|
90
|
+
export const SYSTEM_PROGRAM_ID = '11111111111111111111111111111111';
|
|
91
|
+
|
|
92
|
+
/** What the backend says the transaction encodes (`declared` in the funding response). */
|
|
93
|
+
export interface DeclaredFunding {
|
|
31
94
|
/** Smallest-unit strings, as the API returns them. Compared as bigints so "10" and "10.0" or a
|
|
32
95
|
* leading zero cannot slip past a string equality check. */
|
|
33
96
|
budget: string;
|
|
@@ -38,6 +101,10 @@ export interface ExpectedFunding {
|
|
|
38
101
|
settlement_deadline: string;
|
|
39
102
|
/** 64 hex chars, no 0x. */
|
|
40
103
|
seed_commit: string;
|
|
104
|
+
/** v2: the fee ceiling encoded as `create_task`'s `max_fee_bps`. */
|
|
105
|
+
max_fee_bps?: number;
|
|
106
|
+
/** v2: whether the platform Operator co-signed (lottery tasks only). */
|
|
107
|
+
operator_cosigned?: boolean;
|
|
41
108
|
}
|
|
42
109
|
|
|
43
110
|
export interface FundingAccounts {
|
|
@@ -51,10 +118,29 @@ export interface FundingAccounts {
|
|
|
51
118
|
sponsor_token_account: string;
|
|
52
119
|
}
|
|
53
120
|
|
|
121
|
+
/** The escrow program's `Config` account, as read from chain. */
|
|
122
|
+
export interface EscrowConfigState {
|
|
123
|
+
address: string;
|
|
124
|
+
admin: string;
|
|
125
|
+
operator: string;
|
|
126
|
+
guardian: string;
|
|
127
|
+
treasury: string;
|
|
128
|
+
fee_bps: number;
|
|
129
|
+
min_review_window: number;
|
|
130
|
+
paused: boolean;
|
|
131
|
+
task_counter: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
54
134
|
export interface VerifyInput {
|
|
55
135
|
transaction_base64: string;
|
|
56
|
-
|
|
136
|
+
/** `escrow_interface` from the funding response. Anything but 'v2' is refused. */
|
|
137
|
+
escrow_interface: string | undefined;
|
|
138
|
+
/** `task_id` from the funding response; the task and vault PDAs are derived from it. */
|
|
139
|
+
task_id: string;
|
|
140
|
+
declared: DeclaredFunding;
|
|
57
141
|
accounts: FundingAccounts;
|
|
142
|
+
/** v2 lottery: the hash of the co-signed message, as the backend recorded it. */
|
|
143
|
+
message_sha256?: string;
|
|
58
144
|
/** What the agent asked for, independent of what the backend replied. */
|
|
59
145
|
expected: {
|
|
60
146
|
budget_raw: string;
|
|
@@ -63,29 +149,206 @@ export interface VerifyInput {
|
|
|
63
149
|
lottery_prize_per_winner_raw: string;
|
|
64
150
|
qualify_deadline: number;
|
|
65
151
|
settlement_deadline: number;
|
|
152
|
+
/** Hex, with or without 0x. Optional: compared when known. */
|
|
153
|
+
seed_commit?: string;
|
|
154
|
+
/** The fee rate the agent was quoted (create_mission's `solana_quoted_fee_bps`). */
|
|
155
|
+
max_fee_bps: number | undefined;
|
|
66
156
|
mint: string;
|
|
67
157
|
sponsor: string;
|
|
158
|
+
/** The token account the agent asked the budget to be taken from. */
|
|
159
|
+
sponsor_token_account: string;
|
|
68
160
|
};
|
|
69
161
|
/** The program the agent expects. Pinned so a transaction pointed at a different program is
|
|
70
162
|
* rejected even if every parameter matches. */
|
|
71
163
|
expected_program_id: string;
|
|
164
|
+
/** `Config` read from chain (`readEscrowConfig`), or null when the read failed. Needed to check a
|
|
165
|
+
* lottery co-signer; a non-lottery transaction does not depend on it. */
|
|
166
|
+
onchain_config: EscrowConfigState | null;
|
|
167
|
+
onchain_config_error?: string;
|
|
72
168
|
}
|
|
73
169
|
|
|
74
170
|
export interface VerifyResult {
|
|
75
171
|
ok: boolean;
|
|
76
172
|
/** Every discrepancy found, not just the first — an agent debugging this wants the whole list. */
|
|
77
173
|
problems: string[];
|
|
174
|
+
/** Not a reason to refuse, but likely to make the transaction fail on chain. */
|
|
175
|
+
warnings: string[];
|
|
78
176
|
/** What the transaction actually contains, for logging even on success. */
|
|
79
177
|
summary: {
|
|
178
|
+
escrow_interface: string | undefined;
|
|
80
179
|
program_id: string;
|
|
81
180
|
instruction_count: number;
|
|
82
181
|
signers: string[];
|
|
182
|
+
fee_payer: string | null;
|
|
83
183
|
budget: string;
|
|
84
184
|
mint: string;
|
|
85
185
|
task: string;
|
|
186
|
+
lottery: boolean;
|
|
187
|
+
max_fee_bps: number | null;
|
|
188
|
+
operator: string | null;
|
|
189
|
+
operator_signature: 'valid' | 'invalid' | 'missing' | 'not_required';
|
|
190
|
+
message_sha256: string | null;
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** The fields of a v2 `create_task` instruction, decoded. */
|
|
195
|
+
export interface CreateTaskV2Fields {
|
|
196
|
+
budget: string;
|
|
197
|
+
base_pool: string;
|
|
198
|
+
lottery_winner_count: number;
|
|
199
|
+
lottery_prize_per_winner: string;
|
|
200
|
+
qualify_deadline: string;
|
|
201
|
+
settlement_deadline: string;
|
|
202
|
+
seed_commit: string;
|
|
203
|
+
max_fee_bps: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Decodes v2 `create_task` instruction data. Throws if it is not exactly that instruction. */
|
|
207
|
+
export function decodeCreateTaskV2(data: Buffer): CreateTaskV2Fields {
|
|
208
|
+
if (!data.subarray(0, 8).equals(Buffer.from(DISCRIMINATORS.create_task))) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`instruction discriminator is ${data.subarray(0, 8).toString('hex')}, not create_task's ` +
|
|
211
|
+
Buffer.from(DISCRIMINATORS.create_task).toString('hex'),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (data.length !== CREATE_TASK_V2_DATA_LENGTH) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`create_task data is ${data.length} bytes, expected ${CREATE_TASK_V2_DATA_LENGTH} for the v2 ` +
|
|
217
|
+
'interface' +
|
|
218
|
+
(data.length === CREATE_TASK_V2_DATA_LENGTH - 2 ? ' (84 bytes is the v1 layout, without max_fee_bps)' : ''),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
budget: data.readBigUInt64LE(8).toString(),
|
|
223
|
+
base_pool: data.readBigUInt64LE(16).toString(),
|
|
224
|
+
lottery_winner_count: data.readUInt32LE(24),
|
|
225
|
+
lottery_prize_per_winner: data.readBigUInt64LE(28).toString(),
|
|
226
|
+
qualify_deadline: data.readBigInt64LE(36).toString(),
|
|
227
|
+
settlement_deadline: data.readBigInt64LE(44).toString(),
|
|
228
|
+
seed_commit: data.subarray(52, 84).toString('hex'),
|
|
229
|
+
max_fee_bps: data.readUInt16LE(84),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Decodes the escrow `Config` account:
|
|
235
|
+
* 8 discriminator | admin 32 | operator 32 | guardian 32 | treasury 32 | fee_bps u16 (136)
|
|
236
|
+
* | min_review_window i64 (138) | paused bool (146) | task_counter u64 (147)
|
|
237
|
+
*/
|
|
238
|
+
export function decodeEscrowConfig(data: Buffer, address: string): EscrowConfigState {
|
|
239
|
+
if (data.length < 155) throw new Error(`Config account is ${data.length} bytes, too short`);
|
|
240
|
+
if (!data.subarray(0, 8).equals(Buffer.from(DISCRIMINATORS.Config))) {
|
|
241
|
+
throw new Error('account at the Config address does not carry the Config discriminator');
|
|
242
|
+
}
|
|
243
|
+
const key = (o: number) => base58Encode(data.subarray(o, o + 32));
|
|
244
|
+
return {
|
|
245
|
+
address,
|
|
246
|
+
admin: key(8),
|
|
247
|
+
operator: key(40),
|
|
248
|
+
guardian: key(72),
|
|
249
|
+
treasury: key(104),
|
|
250
|
+
fee_bps: data.readUInt16LE(136),
|
|
251
|
+
min_review_window: Number(data.readBigInt64LE(138)),
|
|
252
|
+
paused: data[146] !== 0,
|
|
253
|
+
task_counter: data.readBigUInt64LE(147).toString(),
|
|
86
254
|
};
|
|
87
255
|
}
|
|
88
256
|
|
|
257
|
+
/** The Config PDA, `[b"config"]` under the program. */
|
|
258
|
+
export async function configAddress(programId: string): Promise<string> {
|
|
259
|
+
const { PublicKey } = await import('@solana/web3.js');
|
|
260
|
+
return PublicKey.findProgramAddressSync([Buffer.from('config')], new PublicKey(programId))[0].toBase58();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
interface RpcAccount {
|
|
264
|
+
owner: string;
|
|
265
|
+
data: [string, string];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* `getAccountInfo` (base64, confirmed). Public RPC endpoints rate-limit hard (devnet answers
|
|
270
|
+
* "Connection rate limits exceeded"), and a transient failure would refuse a legitimate funding, so
|
|
271
|
+
* rate limits, 5xx and network errors are retried — and nothing else.
|
|
272
|
+
*/
|
|
273
|
+
async function getAccountInfo(
|
|
274
|
+
rpcUrl: string,
|
|
275
|
+
address: string,
|
|
276
|
+
{ attempts = 3, backoffMs = 500 }: { attempts?: number; backoffMs?: number } = {},
|
|
277
|
+
): Promise<RpcAccount | null> {
|
|
278
|
+
type RpcBody = { result?: { value: RpcAccount | null }; error?: { message: string } };
|
|
279
|
+
let lastError = '';
|
|
280
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch(rpcUrl, {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
headers: { 'Content-Type': 'application/json' },
|
|
285
|
+
body: JSON.stringify({
|
|
286
|
+
jsonrpc: '2.0',
|
|
287
|
+
id: 1,
|
|
288
|
+
method: 'getAccountInfo',
|
|
289
|
+
params: [address, { encoding: 'base64', commitment: 'confirmed' }],
|
|
290
|
+
}),
|
|
291
|
+
signal: AbortSignal.timeout(15_000),
|
|
292
|
+
});
|
|
293
|
+
if (res.status === 429 || res.status >= 500) {
|
|
294
|
+
lastError = `HTTP ${res.status}`;
|
|
295
|
+
} else {
|
|
296
|
+
const body = (await res.json()) as RpcBody;
|
|
297
|
+
if (!body.error) return body.result?.value ?? null;
|
|
298
|
+
if (!/rate limit|too many/i.test(body.error.message)) {
|
|
299
|
+
throw new Error(`RPC getAccountInfo(${address}) failed: ${body.error.message}`);
|
|
300
|
+
}
|
|
301
|
+
lastError = body.error.message;
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
if ((e as Error).message.startsWith('RPC getAccountInfo')) throw e;
|
|
305
|
+
lastError = (e as Error).message;
|
|
306
|
+
}
|
|
307
|
+
if (attempt < attempts) await new Promise((r) => setTimeout(r, backoffMs * attempt));
|
|
308
|
+
}
|
|
309
|
+
throw new Error(`RPC getAccountInfo(${address}) failed: ${lastError}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Reads the escrow `Config` straight from the cluster. The address is derived here from the pinned
|
|
314
|
+
* program id — never taken from the backend — and the account must be owned by that program.
|
|
315
|
+
*/
|
|
316
|
+
export async function readEscrowConfig(
|
|
317
|
+
rpcUrl: string,
|
|
318
|
+
programId: string,
|
|
319
|
+
opts: { attempts?: number; backoffMs?: number } = {},
|
|
320
|
+
): Promise<EscrowConfigState> {
|
|
321
|
+
const address = await configAddress(programId);
|
|
322
|
+
const value = await getAccountInfo(rpcUrl, address, opts);
|
|
323
|
+
if (!value) throw new Error(`Config account ${address} does not exist on this cluster`);
|
|
324
|
+
if (value.owner !== programId) {
|
|
325
|
+
throw new Error(`Config account ${address} is owned by ${value.owner}, not the program ${programId}`);
|
|
326
|
+
}
|
|
327
|
+
return decodeEscrowConfig(Buffer.from(value.data[0], 'base64'), address);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* A mint's decimals, read from the SPL Token mint account (82 bytes; `decimals` at 44,
|
|
332
|
+
* `is_initialized` at 45). `expected_budget` is scaled by this, so it must not come from the API:
|
|
333
|
+
* a reported 9 for a 6-decimal mint would make an agent that meant 10 approve 10,000.
|
|
334
|
+
*/
|
|
335
|
+
export async function readMintDecimals(
|
|
336
|
+
rpcUrl: string,
|
|
337
|
+
mint: string,
|
|
338
|
+
opts: { attempts?: number; backoffMs?: number } = {},
|
|
339
|
+
): Promise<number> {
|
|
340
|
+
const value = await getAccountInfo(rpcUrl, mint, opts);
|
|
341
|
+
if (!value) throw new Error(`mint ${mint} does not exist on this cluster`);
|
|
342
|
+
if (value.owner !== TOKEN_PROGRAM_ID) {
|
|
343
|
+
throw new Error(`mint ${mint} is owned by ${value.owner}, not the SPL Token program`);
|
|
344
|
+
}
|
|
345
|
+
const data = Buffer.from(value.data[0], 'base64');
|
|
346
|
+
if (data.length !== 82 || data[45] !== 1) {
|
|
347
|
+
throw new Error(`account ${mint} is not an initialized SPL Token mint`);
|
|
348
|
+
}
|
|
349
|
+
return data[44];
|
|
350
|
+
}
|
|
351
|
+
|
|
89
352
|
const eqAmount = (a: string, b: string): boolean => {
|
|
90
353
|
try {
|
|
91
354
|
return BigInt(a) === BigInt(b);
|
|
@@ -94,17 +357,57 @@ const eqAmount = (a: string, b: string): boolean => {
|
|
|
94
357
|
}
|
|
95
358
|
};
|
|
96
359
|
|
|
360
|
+
const hex = (s: string | undefined) => String(s ?? '').replace(/^0x/i, '').toLowerCase();
|
|
361
|
+
|
|
97
362
|
/**
|
|
98
|
-
* Verifies a funding transaction. Returns every problem rather than throwing on the first.
|
|
363
|
+
* Verifies a v2 funding transaction. Returns every problem rather than throwing on the first.
|
|
99
364
|
*
|
|
100
365
|
* REFUSE TO SIGN when `ok` is false. The caller must treat this as a hard stop — a mismatch means
|
|
101
366
|
* the backend built something other than what was quoted, and signing it authorises that difference.
|
|
102
367
|
*/
|
|
103
368
|
export async function verifyFundingTransaction(input: VerifyInput): Promise<VerifyResult> {
|
|
104
|
-
const {
|
|
369
|
+
const { PublicKey } = await import('@solana/web3.js');
|
|
105
370
|
const problems: string[] = [];
|
|
371
|
+
const warnings: string[] = [];
|
|
372
|
+
const e = input.expected;
|
|
373
|
+
const d = input.declared;
|
|
374
|
+
const expectLottery = e.lottery_winner_count > 0;
|
|
106
375
|
|
|
107
|
-
const
|
|
376
|
+
const summary: VerifyResult['summary'] = {
|
|
377
|
+
escrow_interface: input.escrow_interface,
|
|
378
|
+
program_id: '(none)',
|
|
379
|
+
instruction_count: 0,
|
|
380
|
+
signers: [],
|
|
381
|
+
fee_payer: null,
|
|
382
|
+
budget: d.budget,
|
|
383
|
+
mint: input.accounts.mint,
|
|
384
|
+
task: input.accounts.task,
|
|
385
|
+
lottery: expectLottery,
|
|
386
|
+
max_fee_bps: null,
|
|
387
|
+
operator: null,
|
|
388
|
+
operator_signature: expectLottery ? 'missing' : 'not_required',
|
|
389
|
+
message_sha256: null,
|
|
390
|
+
};
|
|
391
|
+
const done = (): VerifyResult => ({ ok: problems.length === 0, problems, warnings, summary });
|
|
392
|
+
|
|
393
|
+
if (input.escrow_interface !== SUPPORTED_ESCROW_INTERFACE) {
|
|
394
|
+
problems.push(
|
|
395
|
+
`the funding response reports escrow_interface ${JSON.stringify(input.escrow_interface)}; this ` +
|
|
396
|
+
`verifier decodes only the ${SUPPORTED_ESCROW_INTERFACE} create_task layout`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let tx: WireTransaction;
|
|
401
|
+
try {
|
|
402
|
+
tx = parseWireTransaction(Buffer.from(input.transaction_base64, 'base64'));
|
|
403
|
+
} catch (err) {
|
|
404
|
+
problems.push(`the transaction does not decode: ${(err as Error).message}`);
|
|
405
|
+
return done();
|
|
406
|
+
}
|
|
407
|
+
summary.instruction_count = tx.instructions.length;
|
|
408
|
+
summary.signers = tx.accountKeys.slice(0, tx.header.numRequiredSignatures);
|
|
409
|
+
summary.fee_payer = tx.accountKeys[0] ?? null;
|
|
410
|
+
summary.message_sha256 = createHash('sha256').update(tx.message).digest('hex');
|
|
108
411
|
|
|
109
412
|
// ---- structure ------------------------------------------------------------------------------
|
|
110
413
|
// Exactly one instruction. An extra one is the cheapest way to hide something — a token transfer
|
|
@@ -115,9 +418,9 @@ export async function verifyFundingTransaction(input: VerifyInput): Promise<Veri
|
|
|
115
418
|
'additional instructions would be authorised by the same signature',
|
|
116
419
|
);
|
|
117
420
|
}
|
|
118
|
-
|
|
119
421
|
const ix = tx.instructions[0];
|
|
120
|
-
const programId = ix
|
|
422
|
+
const programId = ix ? tx.accountKeys[ix.programIdIndex] : '(none)';
|
|
423
|
+
summary.program_id = programId;
|
|
121
424
|
if (programId !== input.expected_program_id) {
|
|
122
425
|
problems.push(`program is ${programId}, expected ${input.expected_program_id}`);
|
|
123
426
|
}
|
|
@@ -128,63 +431,206 @@ export async function verifyFundingTransaction(input: VerifyInput): Promise<Veri
|
|
|
128
431
|
);
|
|
129
432
|
}
|
|
130
433
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
434
|
+
if (
|
|
435
|
+
input.message_sha256 !== undefined &&
|
|
436
|
+
input.message_sha256.toLowerCase() !== summary.message_sha256
|
|
437
|
+
) {
|
|
134
438
|
problems.push(
|
|
135
|
-
`
|
|
439
|
+
`the response's message_sha256 ${input.message_sha256} is not the hash of this transaction's ` +
|
|
440
|
+
`message (${summary.message_sha256}) — confirm-funding would not recognise it`,
|
|
136
441
|
);
|
|
137
442
|
}
|
|
138
|
-
|
|
443
|
+
|
|
444
|
+
// ---- the parameters in the bytes -------------------------------------------------------------
|
|
445
|
+
let encoded: CreateTaskV2Fields | null = null;
|
|
446
|
+
try {
|
|
447
|
+
encoded = decodeCreateTaskV2(ix?.data ?? Buffer.alloc(0));
|
|
448
|
+
summary.max_fee_bps = encoded.max_fee_bps;
|
|
449
|
+
summary.lottery = encoded.lottery_winner_count > 0;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
problems.push(`${(err as Error).message} — this is not the instruction it claims to be`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ---- accounts, by position -------------------------------------------------------------------
|
|
455
|
+
// Derived here from the pinned program id, never taken from the backend's `accounts` block.
|
|
456
|
+
const program = new PublicKey(input.expected_program_id);
|
|
457
|
+
const pda = (...seeds: Buffer[]) => PublicKey.findProgramAddressSync(seeds, program)[0].toBase58();
|
|
458
|
+
let taskId: bigint | null = null;
|
|
459
|
+
try {
|
|
460
|
+
taskId = BigInt(input.task_id);
|
|
461
|
+
if (taskId < 0n || taskId > 0xffffffffffffffffn) throw new Error('out of range');
|
|
462
|
+
} catch {
|
|
463
|
+
problems.push(`task_id ${JSON.stringify(input.task_id)} is not a u64`);
|
|
464
|
+
taskId = null;
|
|
465
|
+
}
|
|
466
|
+
const u64le = (v: bigint) => {
|
|
467
|
+
const b = Buffer.alloc(8);
|
|
468
|
+
b.writeBigUInt64LE(v);
|
|
469
|
+
return b;
|
|
470
|
+
};
|
|
471
|
+
let mintKey: Buffer | null = null;
|
|
472
|
+
try {
|
|
473
|
+
mintKey = base58Decode(e.mint);
|
|
474
|
+
if (mintKey.length !== 32) mintKey = null;
|
|
475
|
+
} catch {
|
|
476
|
+
mintKey = null;
|
|
477
|
+
}
|
|
478
|
+
if (!mintKey) problems.push(`expected mint ${e.mint} is not a valid address`);
|
|
479
|
+
|
|
480
|
+
const operatorSlot = CREATE_TASK_V2_ACCOUNTS.indexOf('operator');
|
|
481
|
+
const want: Record<(typeof CREATE_TASK_V2_ACCOUNTS)[number], string | null> = {
|
|
482
|
+
sponsor: e.sponsor,
|
|
483
|
+
config: pda(Buffer.from('config')),
|
|
484
|
+
whitelist: mintKey ? pda(Buffer.from('whitelist'), mintKey) : null,
|
|
485
|
+
mint: e.mint,
|
|
486
|
+
task: taskId !== null ? pda(Buffer.from('task'), u64le(taskId)) : null,
|
|
487
|
+
vault: taskId !== null ? pda(Buffer.from('vault'), u64le(taskId)) : null,
|
|
488
|
+
sponsor_token_account: e.sponsor_token_account,
|
|
489
|
+
token_program: TOKEN_PROGRAM_ID,
|
|
490
|
+
system_program: SYSTEM_PROGRAM_ID,
|
|
491
|
+
operator: null, // checked below: depends on lottery vs not
|
|
492
|
+
event_authority: pda(Buffer.from('__event_authority')),
|
|
493
|
+
program: input.expected_program_id,
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const ixKeys = (ix?.accountIndexes ?? []).map((i) => tx.accountKeys[i]);
|
|
497
|
+
if (ix && ixKeys.length !== CREATE_TASK_V2_ACCOUNTS.length) {
|
|
139
498
|
problems.push(
|
|
140
|
-
`
|
|
499
|
+
`create_task lists ${ixKeys.length} accounts, expected ${CREATE_TASK_V2_ACCOUNTS.length} ` +
|
|
500
|
+
`(${CREATE_TASK_V2_ACCOUNTS.join(', ')})`,
|
|
141
501
|
);
|
|
502
|
+
} else if (ix) {
|
|
503
|
+
CREATE_TASK_V2_ACCOUNTS.forEach((name, i) => {
|
|
504
|
+
const expectedKey = want[name];
|
|
505
|
+
if (expectedKey !== null && ixKeys[i] !== expectedKey) {
|
|
506
|
+
problems.push(`account #${i} (${name}) is ${ixKeys[i]}, expected ${expectedKey}`);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
if (!isSignerIndex(tx, ix.accountIndexes[0]) || !isWritableIndex(tx, ix.accountIndexes[0])) {
|
|
510
|
+
problems.push('the sponsor account is not a writable signer');
|
|
511
|
+
}
|
|
142
512
|
}
|
|
143
513
|
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
['
|
|
148
|
-
['
|
|
514
|
+
// The backend's own description of the accounts must agree with what the bytes say.
|
|
515
|
+
for (const [label, quoted, derived] of [
|
|
516
|
+
['task', input.accounts.task, want.task],
|
|
517
|
+
['vault', input.accounts.vault, want.vault],
|
|
518
|
+
['config', input.accounts.config, want.config],
|
|
519
|
+
['whitelist', input.accounts.whitelist, want.whitelist],
|
|
149
520
|
] as const) {
|
|
150
|
-
if (
|
|
151
|
-
problems.push(
|
|
521
|
+
if (derived !== null && quoted !== derived) {
|
|
522
|
+
problems.push(
|
|
523
|
+
`quoted ${label} ${quoted} is not the program-derived address ${derived} — ` +
|
|
524
|
+
'an address that is not the PDA may be a wallet somebody holds the key to',
|
|
525
|
+
);
|
|
152
526
|
}
|
|
153
527
|
}
|
|
154
|
-
if (input.accounts.mint !==
|
|
155
|
-
problems.push(`quoted mint ${input.accounts.mint}, expected ${
|
|
528
|
+
if (input.accounts.mint !== e.mint) {
|
|
529
|
+
problems.push(`quoted mint ${input.accounts.mint}, expected ${e.mint}`);
|
|
530
|
+
}
|
|
531
|
+
if (input.accounts.sponsor !== e.sponsor) {
|
|
532
|
+
problems.push(`quoted sponsor ${input.accounts.sponsor}, expected ${e.sponsor}`);
|
|
156
533
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
534
|
+
if (input.accounts.sponsor_token_account !== e.sponsor_token_account) {
|
|
535
|
+
problems.push(
|
|
536
|
+
`quoted sponsor_token_account ${input.accounts.sponsor_token_account}, expected ${e.sponsor_token_account}`,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
// A vault equal to the sponsor's own token account would mean the budget never leaves their
|
|
540
|
+
// control — funded on paper, not escrowed.
|
|
160
541
|
if (input.accounts.vault === input.accounts.sponsor_token_account) {
|
|
161
542
|
problems.push('escrow vault equals the sponsor token account — the budget would not be escrowed');
|
|
162
543
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
544
|
+
|
|
545
|
+
// ---- fee payer and signers -------------------------------------------------------------------
|
|
546
|
+
// The sponsor pays the fee and the rent. The Operator co-signs a lottery but must never be the
|
|
547
|
+
// fee payer: that would make the platform the payer of record for the sponsor's escrow.
|
|
548
|
+
const feePayer = tx.accountKeys[0];
|
|
549
|
+
if (feePayer !== e.sponsor) {
|
|
550
|
+
const who =
|
|
551
|
+
input.onchain_config && feePayer === input.onchain_config.operator ? ' (the Operator)' : '';
|
|
552
|
+
problems.push(`fee payer is ${feePayer ?? '(unset)'}${who}, expected the sponsor ${e.sponsor}`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const operatorKey = ix && ixKeys.length === CREATE_TASK_V2_ACCOUNTS.length ? ixKeys[operatorSlot] : undefined;
|
|
556
|
+
const operatorIndex = ix?.accountIndexes[operatorSlot];
|
|
557
|
+
const signers = summary.signers;
|
|
558
|
+
|
|
559
|
+
if (d.operator_cosigned !== undefined && d.operator_cosigned !== expectLottery) {
|
|
560
|
+
problems.push(
|
|
561
|
+
`the response says operator_cosigned=${d.operator_cosigned}, but this is ` +
|
|
562
|
+
(expectLottery ? 'a lottery task, which must be co-signed' : 'not a lottery task, which must not be'),
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (!expectLottery) {
|
|
567
|
+
// Anchor's None for the optional operator is the program id itself, which cannot sign.
|
|
568
|
+
if (operatorKey !== undefined && operatorKey !== input.expected_program_id) {
|
|
569
|
+
problems.push(
|
|
570
|
+
`the operator slot holds ${operatorKey}; a non-lottery create_task must pass the program id ` +
|
|
571
|
+
`${input.expected_program_id} (no co-signer)`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
if (signers.length !== 1 || signers[0] !== e.sponsor) {
|
|
575
|
+
problems.push(
|
|
576
|
+
`expected the sponsor ${e.sponsor} to be the only signer, found [${signers.join(', ')}]`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
summary.operator = operatorKey ?? null;
|
|
581
|
+
const cfg = input.onchain_config;
|
|
582
|
+
if (!cfg) {
|
|
583
|
+
problems.push(
|
|
584
|
+
'cannot check the lottery co-signer: the on-chain Config.operator could not be read' +
|
|
585
|
+
(input.onchain_config_error ? ` (${input.onchain_config_error})` : ''),
|
|
586
|
+
);
|
|
587
|
+
} else if (operatorKey !== cfg.operator) {
|
|
588
|
+
problems.push(
|
|
589
|
+
`the co-signer in the operator slot is ${operatorKey ?? '(none)'}, but the on-chain ` +
|
|
590
|
+
`Config.operator is ${cfg.operator}`,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (operatorKey !== undefined && operatorKey === e.sponsor) {
|
|
594
|
+
problems.push('the operator slot holds the sponsor itself — the co-signature would be the agent\'s own');
|
|
595
|
+
}
|
|
596
|
+
const expectedSigners = operatorKey ? [e.sponsor, operatorKey] : [e.sponsor];
|
|
597
|
+
if (signers.length !== 2 || signers[0] !== expectedSigners[0] || signers[1] !== expectedSigners[1]) {
|
|
598
|
+
problems.push(
|
|
599
|
+
`expected exactly two signers, the sponsor ${e.sponsor} (fee payer) and the Operator ` +
|
|
600
|
+
`${operatorKey ?? '(none)'}, found [${signers.join(', ')}]`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
if (operatorIndex === undefined || !isSignerIndex(tx, operatorIndex)) {
|
|
604
|
+
problems.push('the operator slot is not a signer — a lottery create_task needs the Operator to sign');
|
|
605
|
+
summary.operator_signature = 'missing';
|
|
606
|
+
} else {
|
|
607
|
+
const sig = tx.signatures[operatorIndex];
|
|
608
|
+
if (isEmptySignature(sig)) {
|
|
609
|
+
summary.operator_signature = 'missing';
|
|
610
|
+
problems.push(
|
|
611
|
+
'the Operator co-signature is missing — a lottery create_task without it fails on chain ' +
|
|
612
|
+
'(LotteryRequiresOperator), and an agent-added signature cannot fix that',
|
|
613
|
+
);
|
|
614
|
+
} else if (!ed25519Verify(sig, tx.message, operatorKey as string)) {
|
|
615
|
+
summary.operator_signature = 'invalid';
|
|
616
|
+
problems.push(
|
|
617
|
+
'the Operator signature does not verify over this message — the transaction was altered ' +
|
|
618
|
+
'after it was co-signed, or the signature is not the Operator\'s',
|
|
619
|
+
);
|
|
620
|
+
} else {
|
|
621
|
+
summary.operator_signature = 'valid';
|
|
172
622
|
}
|
|
173
623
|
}
|
|
174
|
-
} catch {
|
|
175
|
-
problems.push('task or vault is not a valid address');
|
|
176
624
|
}
|
|
177
625
|
|
|
178
626
|
// ---- the parameters --------------------------------------------------------------------------
|
|
179
|
-
const d = input.declared;
|
|
180
|
-
const e = input.expected;
|
|
181
627
|
if (!eqAmount(d.budget, e.budget_raw)) {
|
|
182
628
|
problems.push(`budget is ${d.budget}, expected ${e.budget_raw}`);
|
|
183
629
|
}
|
|
184
630
|
if (!eqAmount(d.base_pool, e.base_pool_raw)) {
|
|
185
631
|
problems.push(`base_pool is ${d.base_pool}, expected ${e.base_pool_raw}`);
|
|
186
632
|
}
|
|
187
|
-
if (d.lottery_winner_count !== e.lottery_winner_count) {
|
|
633
|
+
if (Number(d.lottery_winner_count) !== e.lottery_winner_count) {
|
|
188
634
|
problems.push(
|
|
189
635
|
`lottery_winner_count is ${d.lottery_winner_count}, expected ${e.lottery_winner_count}`,
|
|
190
636
|
);
|
|
@@ -204,35 +650,35 @@ export async function verifyFundingTransaction(input: VerifyInput): Promise<Veri
|
|
|
204
650
|
}
|
|
205
651
|
if (!/^[0-9a-f]{64}$/i.test(d.seed_commit)) {
|
|
206
652
|
problems.push(`seed_commit is not 32 bytes of hex: ${d.seed_commit}`);
|
|
653
|
+
} else if (e.seed_commit !== undefined && hex(d.seed_commit) !== hex(e.seed_commit)) {
|
|
654
|
+
problems.push(`seed_commit is ${d.seed_commit}, expected ${hex(e.seed_commit)}`);
|
|
655
|
+
}
|
|
656
|
+
// N-2: the ceiling must be exactly the quote. A higher one lets the fee rise before the
|
|
657
|
+
// transaction lands; a lower one would fail on chain for no reason the agent caused.
|
|
658
|
+
if (typeof e.max_fee_bps !== 'number' || !Number.isInteger(e.max_fee_bps)) {
|
|
659
|
+
problems.push('no quoted fee rate to check max_fee_bps against — refusing to sign an unchecked fee ceiling');
|
|
660
|
+
} else if (d.max_fee_bps === undefined) {
|
|
661
|
+
problems.push('the funding response does not declare max_fee_bps — not a v2 funding transaction');
|
|
662
|
+
} else if (Number(d.max_fee_bps) !== e.max_fee_bps) {
|
|
663
|
+
problems.push(`max_fee_bps is ${d.max_fee_bps}, expected the quoted fee rate ${e.max_fee_bps}`);
|
|
207
664
|
}
|
|
208
665
|
|
|
209
666
|
// ---- the declared parameters must match the bytes actually being signed ----------------------
|
|
210
667
|
// The strongest check here. Everything above compares the backend's own JSON description against
|
|
211
668
|
// the agent's expectations; this compares that description against the instruction data itself, so
|
|
212
669
|
// a backend cannot quote correct parameters and encode different ones.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
problems.push(
|
|
216
|
-
`instruction data is ${data.length} bytes, expected 84 for create_task — ` +
|
|
217
|
-
'this is not the instruction it claims to be',
|
|
218
|
-
);
|
|
219
|
-
} else {
|
|
220
|
-
const encoded = {
|
|
221
|
-
budget: data.readBigUInt64LE(8).toString(),
|
|
222
|
-
base_pool: data.readBigUInt64LE(16).toString(),
|
|
223
|
-
lottery_winner_count: data.readUInt32LE(24),
|
|
224
|
-
lottery_prize_per_winner: data.readBigUInt64LE(28).toString(),
|
|
225
|
-
qualify_deadline: data.readBigInt64LE(36).toString(),
|
|
226
|
-
settlement_deadline: data.readBigInt64LE(44).toString(),
|
|
227
|
-
seed_commit: data.subarray(52, 84).toString('hex'),
|
|
228
|
-
};
|
|
229
|
-
for (const key of Object.keys(encoded) as Array<keyof typeof encoded>) {
|
|
670
|
+
if (encoded) {
|
|
671
|
+
for (const key of Object.keys(encoded) as Array<keyof CreateTaskV2Fields>) {
|
|
230
672
|
const inBytes = String(encoded[key]);
|
|
231
|
-
const
|
|
673
|
+
const quotedRaw = (d as unknown as Record<string, unknown>)[key];
|
|
674
|
+
if (quotedRaw === undefined) continue; // reported above
|
|
675
|
+
const quoted = String(quotedRaw);
|
|
232
676
|
const same =
|
|
233
|
-
key === 'seed_commit'
|
|
234
|
-
? inBytes
|
|
235
|
-
:
|
|
677
|
+
key === 'seed_commit'
|
|
678
|
+
? inBytes === hex(quoted)
|
|
679
|
+
: key === 'lottery_winner_count' || key === 'max_fee_bps'
|
|
680
|
+
? Number(inBytes) === Number(quoted)
|
|
681
|
+
: eqAmount(inBytes, quoted);
|
|
236
682
|
if (!same) {
|
|
237
683
|
problems.push(
|
|
238
684
|
`the transaction encodes ${key}=${inBytes} but the response quoted ${quoted} — ` +
|
|
@@ -240,18 +686,33 @@ export async function verifyFundingTransaction(input: VerifyInput): Promise<Veri
|
|
|
240
686
|
);
|
|
241
687
|
}
|
|
242
688
|
}
|
|
689
|
+
if (typeof e.max_fee_bps === 'number' && encoded.max_fee_bps !== e.max_fee_bps) {
|
|
690
|
+
problems.push(
|
|
691
|
+
`the transaction encodes max_fee_bps=${encoded.max_fee_bps}, not the quoted fee rate ${e.max_fee_bps}`,
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
if ((encoded.lottery_winner_count > 0) !== expectLottery) {
|
|
695
|
+
problems.push(
|
|
696
|
+
`the transaction encodes lottery_winner_count=${encoded.lottery_winner_count}, ` +
|
|
697
|
+
`expected ${e.lottery_winner_count}`,
|
|
698
|
+
);
|
|
699
|
+
}
|
|
243
700
|
}
|
|
244
701
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
702
|
+
// ---- courtesy: things the program would reject anyway ---------------------------------------
|
|
703
|
+
const cfg = input.onchain_config;
|
|
704
|
+
if (cfg && encoded) {
|
|
705
|
+
if (cfg.fee_bps > encoded.max_fee_bps) {
|
|
706
|
+
warnings.push(
|
|
707
|
+
`the program's fee is now ${cfg.fee_bps} bps, above this transaction's max_fee_bps ` +
|
|
708
|
+
`${encoded.max_fee_bps}: create_task will fail with FeeAboveSponsorMax (6132). Cancel the ` +
|
|
709
|
+
'mission and create a new one to get a fresh quote.',
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (cfg.paused) {
|
|
713
|
+
warnings.push('the escrow program is paused: create_task will fail until it is unpaused.');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return done();
|
|
257
718
|
}
|