@projectsolo/solo-mission-mcp 0.21.13 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEVELOPER_README.md +1 -1
- package/README.md +96 -12
- package/dist/chunk-NUGGTARW.js +196 -0
- package/dist/{chunk-TKRT2V2W.js → chunk-OP4QYTL4.js} +5 -0
- package/dist/{client-LDWK5HLP.js → client-NS6J6IO2.js} +1 -1
- package/dist/deployment-UCUWVIPJ.js +43 -0
- package/dist/escrowErrors-4XSAYLGT.js +56 -0
- package/dist/index.js +244 -162
- package/dist/verify-7EUVRUAS.js +445 -0
- package/dist/{wallet-V4T4NTCM.js → wallet-LX7SI5RE.js} +28 -14
- package/dist/wire-RLDY4AJR.js +24 -0
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/index.ts +1 -1
- package/src/scripts/check-tools-against-spec.ts +3 -3
- package/src/solana/deployment.test.ts +61 -0
- package/src/solana/deployment.ts +89 -0
- package/src/solana/escrowErrors.ts +78 -0
- package/src/solana/fixtures/config-account.json +6 -0
- package/src/solana/fixtures/funding-transaction-v2-lottery.json +49 -0
- package/src/solana/fixtures/funding-transaction-v2-plain.json +46 -0
- package/src/solana/fixtures/solo_escrow.v2.idl-excerpt.json +683 -0
- package/src/solana/verify.test.ts +605 -78
- package/src/solana/verify.ts +545 -84
- package/src/solana/wallet.test.ts +85 -2
- package/src/solana/wallet.ts +74 -31
- package/src/solana/wire.test.ts +129 -0
- package/src/solana/wire.ts +264 -0
- package/src/tools/missions.ts +56 -119
- package/src/tools/solana.test.ts +356 -0
- package/src/tools/solana.ts +309 -70
- package/src/tools/tracks.ts +1 -1
- package/dist/verify-KAETIGV5.js +0 -136
- /package/src/solana/fixtures/{funding-transaction.json → funding-transaction-v1.json} +0 -0
|
@@ -1,156 +1,683 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The verifier is the one thing standing between an agent and signing bytes it cannot read, so it
|
|
3
|
-
* is tested against
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* is tested against REAL transactions built by the backend's own v2 builders rather than synthetic
|
|
4
|
+
* ones. A hand-built fixture would encode my assumptions about the layout twice and agree with
|
|
5
|
+
* itself.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* fixtures/funding-transaction-v2-lottery.json — co-signed by the devnet Operator
|
|
8
|
+
* fixtures/funding-transaction-v2-plain.json — no co-signer
|
|
9
|
+
* fixtures/config-account.json — the devnet Config those were built against
|
|
10
|
+
* fixtures/solo_escrow.v2.idl-excerpt.json — solo-escrow-solana u1.1.0's IDL, trimmed
|
|
11
|
+
* fixtures/funding-transaction-v1.json — a pre-upgrade transaction, which must be refused
|
|
12
|
+
*
|
|
13
|
+
* Adversarial variants are derived from the real transactions: same instruction, one thing changed.
|
|
8
14
|
*/
|
|
9
|
-
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { describe, it, expect, vi, afterEach } from 'vitest'
|
|
10
16
|
import { readFileSync } from 'fs'
|
|
11
17
|
import { join } from 'path'
|
|
18
|
+
import { Keypair, PublicKey, SystemProgram, Transaction, TransactionInstruction } from '@solana/web3.js'
|
|
12
19
|
import {
|
|
20
|
+
CREATE_TASK_V2_ACCOUNTS,
|
|
21
|
+
CREATE_TASK_V2_DATA_LENGTH,
|
|
22
|
+
DISCRIMINATORS,
|
|
23
|
+
decodeCreateTaskV2,
|
|
24
|
+
decodeEscrowConfig,
|
|
25
|
+
readEscrowConfig,
|
|
26
|
+
readMintDecimals,
|
|
13
27
|
verifyFundingTransaction,
|
|
14
28
|
type VerifyInput,
|
|
15
|
-
type ExpectedFunding,
|
|
16
|
-
type FundingAccounts,
|
|
17
29
|
} from './verify'
|
|
30
|
+
import { ESCROW_ERRORS, escrowErrorFromMessage } from './escrowErrors'
|
|
31
|
+
import { parseWireTransaction } from './wire'
|
|
18
32
|
import { toRawAmount } from '../tools/solana'
|
|
19
33
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const FIX = join(__dirname, 'fixtures')
|
|
35
|
+
const load = (f: string) => JSON.parse(readFileSync(join(FIX, f), 'utf8'))
|
|
36
|
+
const lottery = load('funding-transaction-v2-lottery.json')
|
|
37
|
+
const plain = load('funding-transaction-v2-plain.json')
|
|
38
|
+
const v1 = load('funding-transaction-v1.json')
|
|
39
|
+
const idl = load('solo_escrow.v2.idl-excerpt.json')
|
|
40
|
+
const configFx = load('config-account.json')
|
|
41
|
+
const onchainConfig = decodeEscrowConfig(Buffer.from(configFx.data_base64, 'base64'), configFx.address)
|
|
42
|
+
const PROGRAM_ID: string = lottery.program_id
|
|
43
|
+
const OPERATOR = onchainConfig.operator
|
|
30
44
|
|
|
31
|
-
const
|
|
32
|
-
transaction_base64: fx.transaction_base64,
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const inputFor = (fx: any): VerifyInput => ({
|
|
46
|
+
transaction_base64: fx.response.transaction_base64,
|
|
47
|
+
escrow_interface: fx.response.escrow_interface,
|
|
48
|
+
task_id: fx.response.task_id,
|
|
49
|
+
declared: structuredClone(fx.response.declared),
|
|
50
|
+
accounts: { ...fx.response.accounts },
|
|
51
|
+
message_sha256: fx.response.message_sha256,
|
|
35
52
|
expected: {
|
|
36
|
-
budget_raw:
|
|
37
|
-
base_pool_raw:
|
|
38
|
-
lottery_winner_count:
|
|
39
|
-
lottery_prize_per_winner_raw:
|
|
40
|
-
qualify_deadline:
|
|
41
|
-
settlement_deadline:
|
|
53
|
+
budget_raw: fx.mission.budget_raw,
|
|
54
|
+
base_pool_raw: fx.response.declared.base_pool,
|
|
55
|
+
lottery_winner_count: fx.mission.lottery_winner_count ?? 0,
|
|
56
|
+
lottery_prize_per_winner_raw: fx.mission.lottery_prize_per_winner_raw ?? '0',
|
|
57
|
+
qualify_deadline: fx.mission.qualify_deadline,
|
|
58
|
+
settlement_deadline: fx.mission.settlement_deadline,
|
|
59
|
+
seed_commit: fx.mission.seed_commit,
|
|
60
|
+
max_fee_bps: fx.mission.solana_quoted_fee_bps,
|
|
42
61
|
mint: fx.mint,
|
|
43
62
|
sponsor: fx.sponsor,
|
|
63
|
+
sponsor_token_account: fx.response.accounts.sponsor_token_account,
|
|
44
64
|
},
|
|
45
65
|
expected_program_id: fx.program_id,
|
|
66
|
+
onchain_config: { ...onchainConfig },
|
|
46
67
|
})
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
const raw = (fx: any) => Buffer.from(fx.response.transaction_base64, 'base64')
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Rebuilds a fixture's transaction with one thing changed, the way a backend would have to in
|
|
73
|
+
* order to get it past the verifier: same instruction unless told otherwise.
|
|
74
|
+
*/
|
|
75
|
+
function rebuild(
|
|
76
|
+
fx: any,
|
|
77
|
+
o: {
|
|
78
|
+
operatorSlot?: PublicKey | 'program'
|
|
79
|
+
coSigner?: Keypair
|
|
80
|
+
feePayer?: PublicKey
|
|
81
|
+
data?: Buffer
|
|
82
|
+
keys?: (keys: TransactionInstruction['keys']) => TransactionInstruction['keys']
|
|
83
|
+
extra?: TransactionInstruction
|
|
84
|
+
} = {},
|
|
85
|
+
): string {
|
|
86
|
+
const t0 = Transaction.from(raw(fx))
|
|
87
|
+
const ix0 = t0.instructions[0]
|
|
88
|
+
let keys = ix0.keys.map((k) => ({ ...k }))
|
|
89
|
+
if (o.operatorSlot === 'program') {
|
|
90
|
+
keys[9] = { pubkey: new PublicKey(PROGRAM_ID), isSigner: false, isWritable: false }
|
|
91
|
+
} else if (o.operatorSlot) {
|
|
92
|
+
keys[9] = { pubkey: o.operatorSlot, isSigner: true, isWritable: false }
|
|
93
|
+
}
|
|
94
|
+
if (o.keys) keys = o.keys(keys)
|
|
95
|
+
const t = new Transaction({
|
|
96
|
+
feePayer: o.feePayer ?? new PublicKey(fx.sponsor),
|
|
97
|
+
recentBlockhash: t0.recentBlockhash!,
|
|
98
|
+
}).add(new TransactionInstruction({ programId: ix0.programId, keys, data: o.data ?? ix0.data }))
|
|
99
|
+
if (o.extra) t.add(o.extra)
|
|
100
|
+
if (o.coSigner) t.partialSign(o.coSigner)
|
|
101
|
+
return t.serialize({ requireAllSignatures: false, verifySignatures: false }).toString('base64')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const problemsOf = async (input: VerifyInput) => {
|
|
105
|
+
const r = await verifyFundingTransaction(input)
|
|
106
|
+
return { ...r, text: r.problems.join('\n') }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// A key standing in for the Operator where a test needs a co-signature it controls.
|
|
110
|
+
const testOperator = Keypair.generate()
|
|
111
|
+
const asTestOperator = (input: VerifyInput): VerifyInput => ({
|
|
112
|
+
...input,
|
|
113
|
+
onchain_config: { ...onchainConfig, operator: testOperator.publicKey.toBase58() },
|
|
114
|
+
message_sha256: undefined,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('the v2 interface, byte-checked against the IDL', () => {
|
|
118
|
+
const ixByName = (n: string) => idl.instructions.find((i: any) => i.name === n)
|
|
119
|
+
|
|
120
|
+
it('uses the IDL discriminators', () => {
|
|
121
|
+
for (const name of ['create_task', 'cancel_task', 'emergency_refund', 'claim_refund'] as const) {
|
|
122
|
+
expect(DISCRIMINATORS[name]).toEqual(ixByName(name).discriminator)
|
|
123
|
+
}
|
|
124
|
+
expect(DISCRIMINATORS.Config).toEqual(idl.accounts.find((a: any) => a.name === 'Config').discriminator)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('lists create_task accounts in IDL order, with the optional operator signer after system_program', () => {
|
|
128
|
+
const accounts = ixByName('create_task').accounts
|
|
129
|
+
expect(accounts.map((a: any) => a.name)).toEqual([...CREATE_TASK_V2_ACCOUNTS])
|
|
130
|
+
const op = accounts[CREATE_TASK_V2_ACCOUNTS.indexOf('operator')]
|
|
131
|
+
expect(op).toMatchObject({ name: 'operator', signer: true, optional: true })
|
|
132
|
+
expect(accounts[CREATE_TASK_V2_ACCOUNTS.indexOf('operator') - 1].name).toBe('system_program')
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// Borsh, driven by the IDL's own argument list — an independent encoder for the byte comparison.
|
|
136
|
+
const SIZE: Record<string, number> = { u16: 2, u32: 4, u64: 8, i64: 8 }
|
|
137
|
+
const encodeFromIdl = (values: Record<string, string | number>) => {
|
|
138
|
+
const parts: Buffer[] = [Buffer.from(ixByName('create_task').discriminator)]
|
|
139
|
+
for (const arg of ixByName('create_task').args) {
|
|
140
|
+
const v = values[arg.name]
|
|
141
|
+
if (typeof arg.type === 'object') {
|
|
142
|
+
expect(arg.type).toEqual({ array: ['u8', 32] })
|
|
143
|
+
parts.push(Buffer.from(String(v).replace(/^0x/, ''), 'hex'))
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
const b = Buffer.alloc(SIZE[arg.type])
|
|
147
|
+
if (arg.type === 'u16') b.writeUInt16LE(Number(v))
|
|
148
|
+
else if (arg.type === 'u32') b.writeUInt32LE(Number(v))
|
|
149
|
+
else if (arg.type === 'u64') b.writeBigUInt64LE(BigInt(v))
|
|
150
|
+
else if (arg.type === 'i64') b.writeBigInt64LE(BigInt(v))
|
|
151
|
+
else throw new Error(`unhandled IDL type ${arg.type}`)
|
|
152
|
+
parts.push(b)
|
|
153
|
+
}
|
|
154
|
+
return Buffer.concat(parts)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
it('has max_fee_bps: u16 as the trailing create_task argument, 86 bytes in all', () => {
|
|
158
|
+
const args = ixByName('create_task').args
|
|
159
|
+
expect(args[args.length - 1]).toEqual({ name: 'max_fee_bps', type: 'u16' })
|
|
160
|
+
const len = 8 + args.reduce((n: number, a: any) => n + (typeof a.type === 'object' ? 32 : SIZE[a.type]), 0)
|
|
161
|
+
expect(len).toBe(CREATE_TASK_V2_DATA_LENGTH)
|
|
162
|
+
expect(CREATE_TASK_V2_DATA_LENGTH).toBe(86)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
for (const [label, fx] of [['lottery', lottery], ['non-lottery', plain]] as const) {
|
|
166
|
+
it(`the backend's ${label} instruction data is exactly the IDL encoding of what it declared`, () => {
|
|
167
|
+
const data = parseWireTransaction(raw(fx)).instructions[0].data
|
|
168
|
+
expect(data.equals(encodeFromIdl(fx.response.declared))).toBe(true)
|
|
169
|
+
const decoded = decodeCreateTaskV2(data)
|
|
170
|
+
expect(decoded).toEqual({
|
|
171
|
+
budget: fx.response.declared.budget,
|
|
172
|
+
base_pool: fx.response.declared.base_pool,
|
|
173
|
+
lottery_winner_count: fx.response.declared.lottery_winner_count,
|
|
174
|
+
lottery_prize_per_winner: fx.response.declared.lottery_prize_per_winner,
|
|
175
|
+
qualify_deadline: fx.response.declared.qualify_deadline,
|
|
176
|
+
settlement_deadline: fx.response.declared.settlement_deadline,
|
|
177
|
+
seed_commit: fx.response.declared.seed_commit,
|
|
178
|
+
max_fee_bps: fx.response.declared.max_fee_bps,
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
it('decodes Config at the offsets the IDL field order gives, and the snapshot names the fixture co-signer', () => {
|
|
184
|
+
const fields = idl.types.find((t: any) => t.name === 'Config').type.fields.map((f: any) => f.name)
|
|
185
|
+
expect(fields.slice(0, 8)).toEqual([
|
|
186
|
+
'admin', 'operator', 'guardian', 'treasury', 'fee_bps', 'min_review_window', 'paused', 'task_counter',
|
|
187
|
+
])
|
|
188
|
+
// 8 + 4×32 = 136 (fee_bps), +2 = 138 (min_review_window), +8 = 146 (paused), +1 = 147 (task_counter).
|
|
189
|
+
const data = Buffer.from(configFx.data_base64, 'base64')
|
|
190
|
+
expect(onchainConfig.operator).toBe(new PublicKey(data.subarray(40, 72)).toBase58())
|
|
191
|
+
expect(onchainConfig.fee_bps).toBe(data.readUInt16LE(136))
|
|
192
|
+
expect(onchainConfig.task_counter).toBe(data.readBigUInt64LE(147).toString())
|
|
193
|
+
const tx = parseWireTransaction(raw(lottery))
|
|
194
|
+
expect(tx.accountKeys[tx.instructions[0].accountIndexes[9]]).toBe(OPERATOR)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('refuses to decode a Config without the Config discriminator', () => {
|
|
198
|
+
const data = Buffer.from(configFx.data_base64, 'base64')
|
|
199
|
+
data[0] ^= 0xff
|
|
200
|
+
expect(() => decodeEscrowConfig(data, configFx.address)).toThrow(/discriminator/)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('confirms the refund instructions kept the layout refund_solana_mission verifies', () => {
|
|
204
|
+
// No arguments (so the data is the bare 8-byte discriminator), the sponsor as the only signer,
|
|
205
|
+
// and the sponsor token account as the destination — identical to v1.
|
|
206
|
+
for (const name of ['cancel_task', 'emergency_refund', 'claim_refund']) {
|
|
207
|
+
const i = ixByName(name)
|
|
208
|
+
expect(i.args).toEqual([])
|
|
209
|
+
expect(i.accounts.map((a: any) => a.name)).toEqual([
|
|
210
|
+
'sponsor', 'config', 'task', 'vault', 'sponsor_token_account', 'token_program', 'event_authority', 'program',
|
|
211
|
+
])
|
|
212
|
+
expect(i.accounts.filter((a: any) => a.signer).map((a: any) => a.name)).toEqual(['sponsor'])
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('names program errors by the IDL codes', () => {
|
|
217
|
+
for (const [code, info] of Object.entries(ESCROW_ERRORS)) {
|
|
218
|
+
expect(idl.errors.find((e: any) => e.code === Number(code))?.name).toBe(info.name)
|
|
219
|
+
}
|
|
220
|
+
expect(escrowErrorFromMessage('Error processing Instruction 0: custom program error: 0x17ed')?.name).toBe('TooLateToCancel')
|
|
221
|
+
expect(escrowErrorFromMessage('Program log: AnchorError ... Error Number: 6132.')?.name).toBe('FeeAboveSponsorMax')
|
|
222
|
+
expect(escrowErrorFromMessage('custom program error: 0x17ec')?.name).toBe('LotteryRequiresOperator')
|
|
223
|
+
expect(escrowErrorFromMessage('nothing to see')).toBeNull()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
describe('readEscrowConfig', () => {
|
|
228
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
229
|
+
const ok = (owner = configFx.owner) => ({
|
|
230
|
+
status: 200,
|
|
231
|
+
json: async () => ({ result: { value: { owner, data: [configFx.data_base64, 'base64'] } } }),
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('reads Config at the PDA derived from the pinned program id', async () => {
|
|
235
|
+
const fetchMock = vi.fn(async () => ok())
|
|
236
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
237
|
+
const cfg = await readEscrowConfig('https://rpc.example', PROGRAM_ID)
|
|
238
|
+
expect(cfg.operator).toBe(OPERATOR)
|
|
239
|
+
expect(cfg.address).toBe(configFx.address)
|
|
240
|
+
expect(JSON.parse((fetchMock.mock.calls[0] as any)[1].body).params[0]).toBe(configFx.address)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('retries a rate-limited RPC, then succeeds', async () => {
|
|
244
|
+
const fetchMock = vi
|
|
245
|
+
.fn()
|
|
246
|
+
.mockResolvedValueOnce({ status: 429, json: async () => ({}) })
|
|
247
|
+
.mockResolvedValueOnce({ status: 200, json: async () => ({ error: { message: 'Connection rate limits exceeded' } }) })
|
|
248
|
+
.mockResolvedValueOnce(ok())
|
|
249
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
250
|
+
expect((await readEscrowConfig('https://rpc.example', PROGRAM_ID, { backoffMs: 0 })).operator).toBe(OPERATOR)
|
|
251
|
+
expect(fetchMock).toHaveBeenCalledTimes(3)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('gives up after the last attempt', async () => {
|
|
255
|
+
vi.stubGlobal('fetch', vi.fn(async () => ({ status: 429, json: async () => ({}) })))
|
|
256
|
+
await expect(readEscrowConfig('https://rpc.example', PROGRAM_ID, { backoffMs: 0 })).rejects.toThrow(/HTTP 429/)
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('refuses a Config account not owned by the program', async () => {
|
|
260
|
+
vi.stubGlobal('fetch', vi.fn(async () => ok('11111111111111111111111111111111')))
|
|
261
|
+
await expect(readEscrowConfig('https://rpc.example', PROGRAM_ID)).rejects.toThrow(/not the program/)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('does not retry an RPC error that is not a rate limit', async () => {
|
|
265
|
+
const fetchMock = vi.fn(async () => ({ status: 200, json: async () => ({ error: { message: 'Invalid param' } }) }))
|
|
266
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
267
|
+
await expect(readEscrowConfig('https://rpc.example', PROGRAM_ID, { backoffMs: 0 })).rejects.toThrow(/Invalid param/)
|
|
268
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('readMintDecimals', () => {
|
|
273
|
+
afterEach(() => vi.unstubAllGlobals())
|
|
274
|
+
const mint = (owner: string, decimals: number, initialized = 1, len = 82) => {
|
|
275
|
+
const b = Buffer.alloc(len)
|
|
276
|
+
b[44] = decimals
|
|
277
|
+
b[45] = initialized
|
|
278
|
+
return { status: 200, json: async () => ({ result: { value: { owner, data: [b.toString('base64'), 'base64'] } } }) }
|
|
279
|
+
}
|
|
280
|
+
const TOKEN = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
281
|
+
|
|
282
|
+
it('reads decimals from an initialized SPL Token mint', async () => {
|
|
283
|
+
vi.stubGlobal('fetch', vi.fn(async () => mint(TOKEN, 6)))
|
|
284
|
+
expect(await readMintDecimals('https://rpc.example', lottery.mint)).toBe(6)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('refuses an account that is not an SPL Token mint', async () => {
|
|
288
|
+
vi.stubGlobal('fetch', vi.fn(async () => mint('11111111111111111111111111111111', 6)))
|
|
289
|
+
await expect(readMintDecimals('https://rpc.example', lottery.mint)).rejects.toThrow(/not the SPL Token program/)
|
|
290
|
+
vi.stubGlobal('fetch', vi.fn(async () => mint(TOKEN, 6, 0)))
|
|
291
|
+
await expect(readMintDecimals('https://rpc.example', lottery.mint)).rejects.toThrow(/not an initialized/)
|
|
292
|
+
vi.stubGlobal('fetch', vi.fn(async () => mint(TOKEN, 6, 1, 165)))
|
|
293
|
+
await expect(readMintDecimals('https://rpc.example', lottery.mint)).rejects.toThrow(/not an initialized/)
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
describe('accepts genuine v2 transactions', () => {
|
|
298
|
+
it('accepts the backend-built lottery transaction with a valid Operator co-signature', async () => {
|
|
299
|
+
const r = await problemsOf(inputFor(lottery))
|
|
300
|
+
expect(r.problems).toEqual([])
|
|
301
|
+
expect(r.ok).toBe(true)
|
|
302
|
+
expect(r.summary).toMatchObject({
|
|
303
|
+
lottery: true,
|
|
304
|
+
operator: OPERATOR,
|
|
305
|
+
operator_signature: 'valid',
|
|
306
|
+
max_fee_bps: 0,
|
|
307
|
+
signers: [lottery.sponsor, OPERATOR],
|
|
308
|
+
fee_payer: lottery.sponsor,
|
|
309
|
+
message_sha256: lottery.response.message_sha256,
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('accepts the backend-built non-lottery transaction, with the program id in the operator slot', async () => {
|
|
314
|
+
const r = await problemsOf(inputFor(plain))
|
|
315
|
+
expect(r.problems).toEqual([])
|
|
316
|
+
expect(r.summary).toMatchObject({ lottery: false, operator_signature: 'not_required', signers: [plain.sponsor] })
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('accepts a non-lottery transaction without reading Config', async () => {
|
|
320
|
+
const input = inputFor(plain)
|
|
321
|
+
input.onchain_config = null
|
|
322
|
+
input.onchain_config_error = 'RPC down'
|
|
323
|
+
expect((await problemsOf(input)).problems).toEqual([])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('accepts any valid co-signature by the key Config names as Operator', async () => {
|
|
327
|
+
const input = asTestOperator(inputFor(lottery))
|
|
328
|
+
input.transaction_base64 = rebuild(lottery, { operatorSlot: testOperator.publicKey, coSigner: testOperator })
|
|
329
|
+
const r = await problemsOf(input)
|
|
51
330
|
expect(r.problems).toEqual([])
|
|
331
|
+
expect(r.summary.operator_signature).toBe('valid')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('warns, without refusing, when the live fee is above the ceiling or the program is paused', async () => {
|
|
335
|
+
const input = inputFor(plain)
|
|
336
|
+
input.onchain_config = { ...onchainConfig, fee_bps: 50, paused: true }
|
|
337
|
+
const r = await problemsOf(input)
|
|
52
338
|
expect(r.ok).toBe(true)
|
|
339
|
+
expect(r.warnings.join(' ')).toMatch(/FeeAboveSponsorMax/)
|
|
340
|
+
expect(r.warnings.join(' ')).toMatch(/paused/)
|
|
341
|
+
})
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
describe('lottery co-signature refusals', () => {
|
|
345
|
+
it('refuses a lottery transaction whose Operator signature is missing', async () => {
|
|
346
|
+
const bytes = raw(lottery)
|
|
347
|
+
const tx = parseWireTransaction(bytes)
|
|
348
|
+
const opIndex = tx.accountKeys.indexOf(OPERATOR)
|
|
349
|
+
bytes.fill(0, tx.signatureOffset + 64 * opIndex, tx.signatureOffset + 64 * (opIndex + 1))
|
|
350
|
+
const input = inputFor(lottery)
|
|
351
|
+
input.transaction_base64 = bytes.toString('base64')
|
|
352
|
+
const r = await problemsOf(input)
|
|
353
|
+
expect(r.ok).toBe(false)
|
|
354
|
+
expect(r.summary.operator_signature).toBe('missing')
|
|
355
|
+
expect(r.text).toMatch(/co-signature is missing/)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('refuses a lottery transaction with no co-signer at all (program id in the operator slot)', async () => {
|
|
359
|
+
const input = inputFor(lottery)
|
|
360
|
+
input.transaction_base64 = rebuild(lottery, { operatorSlot: 'program' })
|
|
361
|
+
input.message_sha256 = undefined
|
|
362
|
+
const r = await problemsOf(input)
|
|
363
|
+
expect(r.ok).toBe(false)
|
|
364
|
+
expect(r.text).toMatch(/Config\.operator/)
|
|
365
|
+
expect(r.text).toMatch(/not a signer/)
|
|
53
366
|
})
|
|
54
367
|
|
|
55
|
-
it('
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
368
|
+
it('refuses a co-signature by a key that is not the on-chain Config.operator', async () => {
|
|
369
|
+
const input = inputFor(lottery)
|
|
370
|
+
input.transaction_base64 = rebuild(lottery, { operatorSlot: testOperator.publicKey, coSigner: testOperator })
|
|
371
|
+
input.message_sha256 = undefined
|
|
372
|
+
const r = await problemsOf(input)
|
|
373
|
+
expect(r.ok).toBe(false)
|
|
374
|
+
expect(r.text).toMatch(new RegExp(`on-chain Config\\.operator is ${OPERATOR}`))
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('refuses the real co-signature when Config names a different Operator', async () => {
|
|
378
|
+
const input = inputFor(lottery)
|
|
379
|
+
input.onchain_config = { ...onchainConfig, operator: testOperator.publicKey.toBase58() }
|
|
380
|
+
const r = await problemsOf(input)
|
|
381
|
+
expect(r.ok).toBe(false)
|
|
382
|
+
expect(r.text).toMatch(/Config\.operator/)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it('refuses a lottery when Config cannot be read, rather than trusting the co-signer', async () => {
|
|
386
|
+
const input = inputFor(lottery)
|
|
387
|
+
input.onchain_config = null
|
|
388
|
+
input.onchain_config_error = 'fetch failed'
|
|
389
|
+
const r = await problemsOf(input)
|
|
390
|
+
expect(r.ok).toBe(false)
|
|
391
|
+
expect(r.text).toMatch(/could not be read \(fetch failed\)/)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it('refuses a message altered after co-signing, even when the description was altered to match', async () => {
|
|
395
|
+
// A consistent lie: the budget in the bytes, the declared budget and the agent's expectation
|
|
396
|
+
// all say 20000. Only the Operator's signature over the original bytes catches it.
|
|
397
|
+
const bytes = raw(lottery)
|
|
398
|
+
const tx = parseWireTransaction(bytes)
|
|
399
|
+
const dataAt = bytes.length - tx.instructions[0].data.length
|
|
400
|
+
bytes.writeBigUInt64LE(20_000n, dataAt + 8)
|
|
401
|
+
const input = inputFor(lottery)
|
|
402
|
+
input.transaction_base64 = bytes.toString('base64')
|
|
403
|
+
input.declared.budget = '20000'
|
|
404
|
+
input.expected.budget_raw = '20000'
|
|
405
|
+
input.message_sha256 = undefined
|
|
406
|
+
const r = await problemsOf(input)
|
|
407
|
+
expect(r.ok).toBe(false)
|
|
408
|
+
expect(r.problems).toEqual([expect.stringMatching(/Operator signature does not verify over this message/)])
|
|
409
|
+
expect(r.summary.operator_signature).toBe('invalid')
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('refuses a message whose hash is not the one the backend recorded', async () => {
|
|
413
|
+
const bytes = raw(lottery)
|
|
414
|
+
const tx = parseWireTransaction(bytes)
|
|
415
|
+
// One byte of the recent blockhash: harmless-looking, and it breaks the co-signature too.
|
|
416
|
+
bytes[tx.signatureOffset + 64 * tx.signatures.length + 3 + 1 + 32 * tx.accountKeys.length] ^= 1
|
|
417
|
+
const input = inputFor(lottery)
|
|
418
|
+
input.transaction_base64 = bytes.toString('base64')
|
|
419
|
+
const r = await problemsOf(input)
|
|
420
|
+
expect(r.text).toMatch(/message_sha256/)
|
|
421
|
+
expect(r.text).toMatch(/does not verify/)
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('refuses a lottery where the Operator, not the sponsor, pays the fee', async () => {
|
|
425
|
+
const input = asTestOperator(inputFor(lottery))
|
|
426
|
+
input.transaction_base64 = rebuild(lottery, {
|
|
427
|
+
operatorSlot: testOperator.publicKey,
|
|
428
|
+
coSigner: testOperator,
|
|
429
|
+
feePayer: testOperator.publicKey,
|
|
430
|
+
})
|
|
431
|
+
const r = await problemsOf(input)
|
|
432
|
+
expect(r.ok).toBe(false)
|
|
433
|
+
expect(r.text).toMatch(/fee payer is \w+ \(the Operator\), expected the sponsor/)
|
|
434
|
+
expect(r.summary.operator_signature).toBe('valid') // a valid co-signature does not excuse it
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('refuses a lottery the mission never asked for (non-lottery expected)', async () => {
|
|
438
|
+
const input = inputFor(lottery)
|
|
439
|
+
input.expected.lottery_winner_count = 0
|
|
440
|
+
input.expected.lottery_prize_per_winner_raw = '0'
|
|
441
|
+
const r = await problemsOf(input)
|
|
442
|
+
expect(r.ok).toBe(false)
|
|
443
|
+
expect(r.text).toMatch(/lottery_winner_count/)
|
|
444
|
+
expect(r.text).toMatch(/operator slot holds/)
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe('non-lottery refusals', () => {
|
|
449
|
+
it('refuses a stray Operator in a non-lottery transaction, even unsigned', async () => {
|
|
450
|
+
const input = inputFor(plain)
|
|
451
|
+
input.transaction_base64 = rebuild(plain, { operatorSlot: new PublicKey(OPERATOR) })
|
|
452
|
+
const r = await problemsOf(input)
|
|
453
|
+
expect(r.ok).toBe(false)
|
|
454
|
+
expect(r.text).toMatch(/must pass the program id/)
|
|
455
|
+
expect(r.text).toMatch(/only signer/)
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it('refuses a stray co-signed Operator in a non-lottery transaction', async () => {
|
|
459
|
+
const input = inputFor(plain)
|
|
460
|
+
input.transaction_base64 = rebuild(plain, { operatorSlot: testOperator.publicKey, coSigner: testOperator })
|
|
461
|
+
const r = await problemsOf(input)
|
|
462
|
+
expect(r.ok).toBe(false)
|
|
463
|
+
expect(r.text).toMatch(/must pass the program id/)
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('refuses a lottery the backend quietly turned into a non-lottery', async () => {
|
|
467
|
+
const input = inputFor(plain)
|
|
468
|
+
input.expected.lottery_winner_count = 1
|
|
469
|
+
const r = await problemsOf(input)
|
|
470
|
+
expect(r.ok).toBe(false)
|
|
471
|
+
expect(r.text).toMatch(/lottery_winner_count/)
|
|
472
|
+
})
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
describe('max_fee_bps must equal the quote', () => {
|
|
476
|
+
it('refuses when the quote differs from what is encoded', async () => {
|
|
477
|
+
const input = inputFor(plain)
|
|
478
|
+
input.expected.max_fee_bps = 25
|
|
479
|
+
const r = await problemsOf(input)
|
|
480
|
+
expect(r.ok).toBe(false)
|
|
481
|
+
expect(r.text).toMatch(/max_fee_bps is 0, expected the quoted fee rate 25/)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('refuses a raised ceiling even when the response declares it', async () => {
|
|
485
|
+
const data = Buffer.from(parseWireTransaction(raw(plain)).instructions[0].data)
|
|
486
|
+
data.writeUInt16LE(1000, 84)
|
|
487
|
+
const input = inputFor(plain)
|
|
488
|
+
input.transaction_base64 = rebuild(plain, { data })
|
|
489
|
+
input.declared.max_fee_bps = 1000
|
|
490
|
+
const r = await problemsOf(input)
|
|
491
|
+
expect(r.ok).toBe(false)
|
|
492
|
+
expect(r.text).toMatch(/encodes max_fee_bps=1000, not the quoted fee rate 0/)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('refuses a raised ceiling on a co-signed lottery — a valid co-signature does not make it acceptable', async () => {
|
|
496
|
+
const data = Buffer.from(parseWireTransaction(raw(lottery)).instructions[0].data)
|
|
497
|
+
data.writeUInt16LE(1000, 84)
|
|
498
|
+
const input = asTestOperator(inputFor(lottery))
|
|
499
|
+
input.transaction_base64 = rebuild(lottery, { operatorSlot: testOperator.publicKey, coSigner: testOperator, data })
|
|
500
|
+
input.declared.max_fee_bps = 1000
|
|
501
|
+
const r = await problemsOf(input)
|
|
502
|
+
expect(r.ok).toBe(false)
|
|
503
|
+
expect(r.summary.operator_signature).toBe('valid')
|
|
504
|
+
expect(r.text).toMatch(/max_fee_bps/)
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('refuses a declared ceiling that disagrees with the bytes', async () => {
|
|
508
|
+
const input = inputFor(plain)
|
|
509
|
+
input.declared.max_fee_bps = 7
|
|
510
|
+
input.expected.max_fee_bps = 7
|
|
511
|
+
const r = await problemsOf(input)
|
|
512
|
+
expect(r.ok).toBe(false)
|
|
513
|
+
expect(r.text).toMatch(/described one thing and built another/)
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
it('refuses when there is no quote to compare against', async () => {
|
|
517
|
+
const input = inputFor(plain)
|
|
518
|
+
input.expected.max_fee_bps = undefined
|
|
519
|
+
expect((await problemsOf(input)).text).toMatch(/no quoted fee rate/)
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
it('refuses a response that does not declare max_fee_bps', async () => {
|
|
523
|
+
const input = inputFor(plain)
|
|
524
|
+
delete input.declared.max_fee_bps
|
|
525
|
+
expect((await problemsOf(input)).text).toMatch(/does not declare max_fee_bps/)
|
|
60
526
|
})
|
|
61
527
|
})
|
|
62
528
|
|
|
63
529
|
describe('refuses when the agent expected something else', () => {
|
|
64
530
|
it('catches an altered budget', async () => {
|
|
65
|
-
const input =
|
|
531
|
+
const input = inputFor(plain)
|
|
66
532
|
input.expected.budget_raw = '999999999'
|
|
67
|
-
const r = await
|
|
533
|
+
const r = await problemsOf(input)
|
|
68
534
|
expect(r.ok).toBe(false)
|
|
69
|
-
expect(r.
|
|
535
|
+
expect(r.text).toMatch(/budget/)
|
|
70
536
|
})
|
|
71
537
|
|
|
72
538
|
it('catches a substituted mint', async () => {
|
|
73
|
-
const input =
|
|
539
|
+
const input = inputFor(plain)
|
|
74
540
|
input.expected.mint = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'
|
|
75
|
-
const r = await
|
|
541
|
+
const r = await problemsOf(input)
|
|
76
542
|
expect(r.ok).toBe(false)
|
|
77
|
-
expect(r.
|
|
543
|
+
expect(r.text).toMatch(/mint/)
|
|
78
544
|
})
|
|
79
545
|
|
|
80
546
|
it('catches a different program, even when every parameter matches', async () => {
|
|
81
|
-
const input =
|
|
547
|
+
const input = inputFor(plain)
|
|
82
548
|
input.expected_program_id = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
|
|
83
|
-
const r = await
|
|
549
|
+
const r = await problemsOf(input)
|
|
84
550
|
expect(r.ok).toBe(false)
|
|
85
|
-
expect(r.
|
|
551
|
+
expect(r.text).toMatch(/program/)
|
|
86
552
|
})
|
|
87
553
|
|
|
88
|
-
it('catches a sponsor who is not the signer', async () => {
|
|
89
|
-
const input =
|
|
554
|
+
it('catches a sponsor who is not the signer or fee payer', async () => {
|
|
555
|
+
const input = inputFor(plain)
|
|
90
556
|
input.expected.sponsor = '4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi'
|
|
91
|
-
const r = await
|
|
557
|
+
const r = await problemsOf(input)
|
|
92
558
|
expect(r.ok).toBe(false)
|
|
93
|
-
expect(r.
|
|
559
|
+
expect(r.text).toMatch(/fee payer/)
|
|
560
|
+
expect(r.text).toMatch(/only signer/)
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
it('catches a different seed commitment than the mission recorded', async () => {
|
|
564
|
+
const input = inputFor(lottery)
|
|
565
|
+
input.expected.seed_commit = '0x' + 'ab'.repeat(32)
|
|
566
|
+
expect((await problemsOf(input)).text).toMatch(/seed_commit/)
|
|
94
567
|
})
|
|
95
568
|
|
|
96
569
|
it('reports every problem, not just the first', async () => {
|
|
97
|
-
|
|
98
|
-
const input = baseInput()
|
|
570
|
+
const input = inputFor(plain)
|
|
99
571
|
input.expected.budget_raw = '1'
|
|
100
572
|
input.expected.mint = '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'
|
|
101
|
-
const r = await
|
|
573
|
+
const r = await problemsOf(input)
|
|
102
574
|
expect(r.problems.length).toBeGreaterThan(1)
|
|
103
575
|
})
|
|
104
576
|
})
|
|
105
577
|
|
|
106
578
|
describe('refuses when the backend quoted one thing and built another', () => {
|
|
107
579
|
it('catches a declared budget that disagrees with the encoded bytes', async () => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// the instruction data actually being signed.
|
|
111
|
-
const input = baseInput()
|
|
112
|
-
input.declared = { ...fx.declared, budget: '1' }
|
|
580
|
+
const input = inputFor(plain)
|
|
581
|
+
input.declared.budget = '1'
|
|
113
582
|
input.expected.budget_raw = '1'
|
|
114
|
-
const r = await
|
|
583
|
+
const r = await problemsOf(input)
|
|
115
584
|
expect(r.ok).toBe(false)
|
|
116
|
-
expect(r.
|
|
585
|
+
expect(r.text).toMatch(/described one thing and built another/)
|
|
117
586
|
})
|
|
118
587
|
|
|
119
588
|
it('catches a declared deadline that disagrees with the bytes', async () => {
|
|
120
|
-
const input =
|
|
121
|
-
input.declared =
|
|
589
|
+
const input = inputFor(plain)
|
|
590
|
+
input.declared.settlement_deadline = '1'
|
|
122
591
|
input.expected.settlement_deadline = 1
|
|
123
|
-
const r = await
|
|
592
|
+
const r = await problemsOf(input)
|
|
124
593
|
expect(r.ok).toBe(false)
|
|
125
|
-
expect(r.
|
|
594
|
+
expect(r.text).toMatch(/described one thing and built another/)
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
it('catches operator_cosigned disagreeing with the kind of task', async () => {
|
|
598
|
+
const input = inputFor(plain)
|
|
599
|
+
input.declared.operator_cosigned = true
|
|
600
|
+
expect((await problemsOf(input)).text).toMatch(/operator_cosigned=true/)
|
|
126
601
|
})
|
|
127
602
|
})
|
|
128
603
|
|
|
129
604
|
describe('structural refusals', () => {
|
|
130
|
-
it('
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
input.
|
|
134
|
-
|
|
605
|
+
it('refuses a deployment or response that is not on the v2 interface', async () => {
|
|
606
|
+
const input = inputFor(plain)
|
|
607
|
+
input.escrow_interface = 'v1'
|
|
608
|
+
expect((await problemsOf(input)).text).toMatch(/escrow_interface "v1"/)
|
|
609
|
+
input.escrow_interface = undefined
|
|
610
|
+
expect((await problemsOf(input)).text).toMatch(/escrow_interface undefined/)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
it('refuses a v1 create_task (84 bytes, no max_fee_bps)', async () => {
|
|
614
|
+
const input = inputFor(plain)
|
|
615
|
+
input.transaction_base64 = v1.transaction_base64
|
|
616
|
+
const r = await problemsOf(input)
|
|
617
|
+
expect(r.ok).toBe(false)
|
|
618
|
+
expect(r.text).toMatch(/84 bytes is the v1 layout/)
|
|
619
|
+
})
|
|
620
|
+
|
|
621
|
+
it('refuses an extra instruction', async () => {
|
|
622
|
+
const input = inputFor(plain)
|
|
623
|
+
input.transaction_base64 = rebuild(plain, {
|
|
624
|
+
extra: SystemProgram.transfer({
|
|
625
|
+
fromPubkey: new PublicKey(plain.sponsor),
|
|
626
|
+
toPubkey: new PublicKey(OPERATOR),
|
|
627
|
+
lamports: 1,
|
|
628
|
+
}),
|
|
629
|
+
})
|
|
630
|
+
expect((await problemsOf(input)).text).toMatch(/exactly 1 instruction, found 2/)
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('refuses a vault in the instruction that is not the task vault PDA', async () => {
|
|
634
|
+
const input = inputFor(plain)
|
|
635
|
+
input.transaction_base64 = rebuild(plain, {
|
|
636
|
+
keys: (keys) => keys.map((k, i) => (i === 5 ? { ...keys[6] } : k)),
|
|
637
|
+
})
|
|
638
|
+
expect((await problemsOf(input)).text).toMatch(/account #5 \(vault\)/)
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
it('catches a quoted vault equal to the sponsor token account', async () => {
|
|
642
|
+
const input = inputFor(plain)
|
|
643
|
+
input.accounts.vault = input.accounts.sponsor_token_account
|
|
644
|
+
const r = await problemsOf(input)
|
|
135
645
|
expect(r.ok).toBe(false)
|
|
136
|
-
expect(r.
|
|
646
|
+
expect(r.text).toMatch(/not be escrowed/)
|
|
137
647
|
})
|
|
138
648
|
|
|
139
|
-
it('catches a vault that is not a program-derived address', async () => {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const r = await verifyFundingTransaction(input)
|
|
649
|
+
it('catches a quoted vault that is not a program-derived address', async () => {
|
|
650
|
+
const input = inputFor(plain)
|
|
651
|
+
input.accounts.vault = plain.sponsor
|
|
652
|
+
const r = await problemsOf(input)
|
|
144
653
|
expect(r.ok).toBe(false)
|
|
145
|
-
expect(r.
|
|
654
|
+
expect(r.text).toMatch(/program-derived address/)
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
it('catches a task id that does not match the task account', async () => {
|
|
658
|
+
const input = inputFor(plain)
|
|
659
|
+
input.task_id = String(BigInt(input.task_id) + 1n)
|
|
660
|
+
expect((await problemsOf(input)).text).toMatch(/account #4 \(task\)/)
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
it('catches a sponsor token account other than the one the agent sent', async () => {
|
|
664
|
+
const input = inputFor(plain)
|
|
665
|
+
input.expected.sponsor_token_account = plain.sponsor
|
|
666
|
+
expect((await problemsOf(input)).text).toMatch(/sponsor_token_account/)
|
|
146
667
|
})
|
|
147
668
|
|
|
148
669
|
it('catches a malformed seed commit', async () => {
|
|
149
|
-
const input =
|
|
150
|
-
input.declared =
|
|
151
|
-
const r = await
|
|
670
|
+
const input = inputFor(plain)
|
|
671
|
+
input.declared.seed_commit = 'nope'
|
|
672
|
+
const r = await problemsOf(input)
|
|
152
673
|
expect(r.ok).toBe(false)
|
|
153
|
-
expect(r.
|
|
674
|
+
expect(r.text).toMatch(/seed_commit/)
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it('refuses bytes that do not decode', async () => {
|
|
678
|
+
const input = inputFor(plain)
|
|
679
|
+
input.transaction_base64 = Buffer.from('not a transaction').toString('base64')
|
|
680
|
+
expect((await problemsOf(input)).text).toMatch(/does not decode/)
|
|
154
681
|
})
|
|
155
682
|
})
|
|
156
683
|
|