@bounded-sh/core 0.0.72 → 0.0.74
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/dist/client/solana-operations.d.ts +48 -1
- package/dist/{index-yT8ZfzrL.js → index-C9ezg0Ef.js} +17 -8
- package/dist/{index-yT8ZfzrL.js.map → index-C9ezg0Ef.js.map} +1 -1
- package/dist/{index-P4eUuhOB.mjs → index-DXV0ETfH.mjs} +17 -8
- package/dist/{index-P4eUuhOB.mjs.map → index-DXV0ETfH.mjs.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{realtime-store-Bcnv3f0g.mjs → realtime-store-6rndRLaa.mjs} +2 -2
- package/dist/{realtime-store-Bcnv3f0g.mjs.map → realtime-store-6rndRLaa.mjs.map} +1 -1
- package/dist/{realtime-store-BDpIOFMH.js → realtime-store-JPgBppmw.js} +2 -2
- package/dist/{realtime-store-BDpIOFMH.js.map → realtime-store-JPgBppmw.js.map} +1 -1
- package/dist/{solana-message-Dh9ryO-Y.js → solana-message-Cam1-0wi.js} +2 -2
- package/dist/{solana-message-Dh9ryO-Y.js.map → solana-message-Cam1-0wi.js.map} +1 -1
- package/dist/{solana-message-x8ElgC9P.mjs → solana-message-DHKm-c_x.mjs} +2 -2
- package/dist/{solana-message-x8ElgC9P.mjs.map → solana-message-DHKm-c_x.mjs.map} +1 -1
- package/dist/{solana-operations-DEWIxJS8.mjs → solana-operations-DiM6_8z7.mjs} +347 -49
- package/dist/solana-operations-DiM6_8z7.mjs.map +1 -0
- package/dist/{solana-operations-DGEGqFd9.js → solana-operations-DycSSUls.js} +347 -48
- package/dist/solana-operations-DycSSUls.js.map +1 -0
- package/package.json +1 -1
- package/dist/solana-operations-DEWIxJS8.mjs.map +0 -1
- package/dist/solana-operations-DGEGqFd9.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { g as getConfig, b as bufferExports } from './index-
|
|
1
|
+
import { g as getConfig, b as bufferExports } from './index-DXV0ETfH.mjs';
|
|
2
2
|
import 'axios';
|
|
3
3
|
import 'tweetnacl';
|
|
4
4
|
import 'reconnecting-websocket';
|
|
@@ -149,33 +149,39 @@ class BorshCursor {
|
|
|
149
149
|
return value >>> 0;
|
|
150
150
|
}
|
|
151
151
|
skip(length, field) { this.requireBytes(length, field); this.offset += length; }
|
|
152
|
-
|
|
153
|
-
const length = this.readU32(`${field} length`);
|
|
152
|
+
readBytes(length, field) {
|
|
154
153
|
this.requireBytes(length, field);
|
|
155
154
|
const raw = this.data.slice(this.offset, this.offset + length);
|
|
156
155
|
this.offset += length;
|
|
157
|
-
return
|
|
156
|
+
return raw;
|
|
157
|
+
}
|
|
158
|
+
readString(field) {
|
|
159
|
+
const length = this.readU32(`${field} length`);
|
|
160
|
+
return bufferExports.Buffer.from(this.readBytes(length, field)).toString('utf8');
|
|
161
|
+
}
|
|
162
|
+
readLengthPrefixedBytes(field) {
|
|
163
|
+
return this.readBytes(this.readU32(`${field} length`), field);
|
|
158
164
|
}
|
|
159
|
-
skipBytes(field) { this.skip(this.readU32(`${field} length`), field); }
|
|
160
165
|
isAtEnd() { return this.offset === this.data.length; }
|
|
161
166
|
}
|
|
162
|
-
function
|
|
167
|
+
function readBoundedFieldValue(cursor) {
|
|
163
168
|
const option = cursor.readU8('operation value option');
|
|
164
169
|
if (option === 0)
|
|
165
|
-
return;
|
|
170
|
+
return null;
|
|
166
171
|
if (option !== 1)
|
|
167
172
|
throw new Error('Server transaction has malformed Bounded field value option');
|
|
168
173
|
const variant = cursor.readU8('operation value variant');
|
|
169
|
-
if (variant === 0
|
|
170
|
-
cursor.
|
|
171
|
-
|
|
172
|
-
cursor.
|
|
173
|
-
|
|
174
|
-
cursor.
|
|
175
|
-
|
|
176
|
-
cursor.
|
|
177
|
-
|
|
178
|
-
|
|
174
|
+
if (variant === 0)
|
|
175
|
+
return { variant: 'u64', value: readU64LE(cursor.readBytes(8, 'operation numeric value'), 0) };
|
|
176
|
+
if (variant === 1)
|
|
177
|
+
return { variant: 'i64', value: readI64LE(cursor.readBytes(8, 'operation numeric value'), 0) };
|
|
178
|
+
if (variant === 2)
|
|
179
|
+
return { variant: 'bool', value: cursor.readU8('operation bool value') !== 0 };
|
|
180
|
+
if (variant === 3)
|
|
181
|
+
return { variant: 'string', value: cursor.readString('operation string value') };
|
|
182
|
+
if (variant === 4)
|
|
183
|
+
return { variant: 'address', value: cursor.readBytes(32, 'operation address value') };
|
|
184
|
+
throw new Error(`Server transaction has unsupported Bounded field value variant: ${variant}`);
|
|
179
185
|
}
|
|
180
186
|
function normalizeOnchainPath(path) {
|
|
181
187
|
let normalized = path.startsWith('/') ? path.slice(1) : path;
|
|
@@ -194,43 +200,323 @@ function parseBoundedSetDocumentsInstruction(data, label) {
|
|
|
194
200
|
}
|
|
195
201
|
const cursor = new BorshCursor(data, 8, label);
|
|
196
202
|
const appId = cursor.readString('appId');
|
|
197
|
-
const
|
|
203
|
+
const documents = [];
|
|
198
204
|
const documentCount = cursor.readU32('documents length');
|
|
199
205
|
for (let i = 0; i < documentCount; i++) {
|
|
200
|
-
|
|
206
|
+
const path = normalizeOnchainPath(cursor.readString('document path'));
|
|
207
|
+
const operations = [];
|
|
201
208
|
const operationCount = cursor.readU32('operations length');
|
|
202
209
|
for (let j = 0; j < operationCount; j++) {
|
|
203
|
-
cursor.readString('operation key');
|
|
204
|
-
|
|
205
|
-
cursor.
|
|
210
|
+
const key = cursor.readString('operation key');
|
|
211
|
+
const value = readBoundedFieldValue(cursor);
|
|
212
|
+
const kind = cursor.readU8('operation kind');
|
|
213
|
+
operations.push({ key, value, kind });
|
|
206
214
|
}
|
|
215
|
+
documents.push({ path, operations });
|
|
207
216
|
}
|
|
208
217
|
const deletePaths = [];
|
|
209
218
|
const deleteCount = cursor.readU32('delete paths length');
|
|
210
219
|
for (let i = 0; i < deleteCount; i++)
|
|
211
220
|
deletePaths.push(normalizeOnchainPath(cursor.readString('delete path')));
|
|
221
|
+
const txData = [];
|
|
212
222
|
const txDataCount = cursor.readU32('txData length');
|
|
213
223
|
for (let i = 0; i < txDataCount; i++) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
txData.push({
|
|
225
|
+
pluginFunctionKey: cursor.readString('txData plugin function key'),
|
|
226
|
+
payload: cursor.readLengthPrefixedBytes('txData bytes'),
|
|
227
|
+
raIndices: cursor.readLengthPrefixedBytes('txData raIndices'),
|
|
228
|
+
});
|
|
217
229
|
}
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
230
|
+
const simulateByte = cursor.readU8('simulate');
|
|
231
|
+
if (simulateByte !== 0 && simulateByte !== 1)
|
|
220
232
|
throw new Error(`${label} has malformed Bounded simulate flag`);
|
|
221
233
|
if (!cursor.isAtEnd())
|
|
222
234
|
throw new Error(`${label} has trailing Bounded instruction data`);
|
|
223
|
-
return { appId,
|
|
235
|
+
return { appId, documents, deletePaths, txData, simulate: simulateByte === 1 };
|
|
236
|
+
}
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// OA-0001: signing-intent binding
|
|
239
|
+
//
|
|
240
|
+
// The wallet must sign ONLY the state transition the caller actually requested.
|
|
241
|
+
// The expected intent below is derived from the CLIENT'S OWN write request - the
|
|
242
|
+
// one ground truth a tampered builder response cannot change - and the parsed
|
|
243
|
+
// instruction must match it field for field before any signer is invoked.
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
/** The program's field-operation kinds (programs/tarobase; mirrored by the worker's solana-tx-builder). */
|
|
246
|
+
const FIELD_OPERATION_WRITE = 0;
|
|
247
|
+
const FIELD_OPERATION_DELETE = 1;
|
|
248
|
+
const FIELD_OPERATION_TIMESTAMP = 2;
|
|
249
|
+
const FIELD_OPERATION_INCREMENT = 3;
|
|
250
|
+
// Keys the worker drops from a client write by NAME before encoding
|
|
251
|
+
// (realtime-worker types.ts: SYSTEM_FIELDS without the `_`-prefixed entries the
|
|
252
|
+
// client already strips, plus the prototype-pollution DANGEROUS_KEYS). The
|
|
253
|
+
// expected model must skip exactly the keys the honest builder skips, or a
|
|
254
|
+
// legitimate write would be rejected.
|
|
255
|
+
const SERVER_DROPPED_FIELD_KEYS = new Set([
|
|
256
|
+
'pathId',
|
|
257
|
+
'relativePath',
|
|
258
|
+
'absolutePath',
|
|
259
|
+
'__proto__',
|
|
260
|
+
'constructor',
|
|
261
|
+
'prototype',
|
|
262
|
+
]);
|
|
263
|
+
// The worker resolves {operation:'time',value:'now'} into this sentinel and the
|
|
264
|
+
// onchain encoder maps the sentinel to a TIMESTAMP op - including when a caller
|
|
265
|
+
// writes the literal string, so the expected model must mirror that quirk.
|
|
266
|
+
const TIME_NOW_SENTINEL = '__TIME_NOW__';
|
|
267
|
+
/**
|
|
268
|
+
* Build the expected write intent from the exact request the client POSTed
|
|
269
|
+
* (`[{ destinationPath, document }]`, a `null` document being a delete).
|
|
270
|
+
*
|
|
271
|
+
* Each document is JSON-round-tripped first: the server parsed the SERIALIZED
|
|
272
|
+
* body, so the intent must be derived from the same bytes - this drops
|
|
273
|
+
* `undefined` values, turns `NaN`/`Infinity` into `null`, and normalizes key
|
|
274
|
+
* order exactly as the server's JSON.parse did.
|
|
275
|
+
*
|
|
276
|
+
* Operation order mirrors the worker's transform (normalizeWriteBody ->
|
|
277
|
+
* resolveOperationFields -> documentToOperations): plain fields in request key
|
|
278
|
+
* order, then increment fields appended in the order they appeared. The
|
|
279
|
+
* comparison is therefore ORDERED, not a multiset - the honest builder is
|
|
280
|
+
* deterministic given the request, and ordering additionally catches
|
|
281
|
+
* duplicate-path and reordering tampering that the retired set comparison hid.
|
|
282
|
+
*/
|
|
283
|
+
function buildExpectedWriteIntent(documents) {
|
|
284
|
+
const intentDocuments = [];
|
|
285
|
+
const deletePaths = [];
|
|
286
|
+
for (const entry of documents) {
|
|
287
|
+
if (entry.document == null) {
|
|
288
|
+
deletePaths.push(entry.destinationPath);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (typeof entry.document !== 'object') {
|
|
292
|
+
// The worker 400s a scalar document, so no honest 202 can exist for this
|
|
293
|
+
// request. Throw here rather than let a dishonest 202 be compared against
|
|
294
|
+
// an intent this entry never had.
|
|
295
|
+
throw new Error(`Cannot derive the expected write intent for ${entry.destinationPath}: the request document is not an object`);
|
|
296
|
+
}
|
|
297
|
+
const wireDocument = JSON.parse(JSON.stringify(entry.document));
|
|
298
|
+
const operations = [];
|
|
299
|
+
const increments = [];
|
|
300
|
+
for (const [key, value] of Object.entries(wireDocument)) {
|
|
301
|
+
if (key.startsWith('_') || SERVER_DROPPED_FIELD_KEYS.has(key))
|
|
302
|
+
continue;
|
|
303
|
+
if (value === null) {
|
|
304
|
+
operations.push({ key, kind: 'delete' });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (typeof value === 'object' && !Array.isArray(value) &&
|
|
308
|
+
typeof value.operation === 'string' && value.operation.length > 0) {
|
|
309
|
+
const operation = value.operation;
|
|
310
|
+
if (operation === 'increment' && typeof value.value === 'number') {
|
|
311
|
+
increments.push({ key, delta: value.value });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (operation === 'time' && value.value === 'now') {
|
|
315
|
+
operations.push({ key, kind: 'timestamp' });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// Any other {operation,...} shape is rejected by the worker with a 400,
|
|
319
|
+
// so it has no honest onchain encoding. Falling through to a plain write
|
|
320
|
+
// of the raw object keeps the comparison fail-closed: a 202 can only
|
|
321
|
+
// arrive from a dishonest builder, and no instruction can match this.
|
|
322
|
+
}
|
|
323
|
+
if (value === TIME_NOW_SENTINEL) {
|
|
324
|
+
operations.push({ key, kind: 'timestamp' });
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
operations.push({ key, kind: 'write', value });
|
|
328
|
+
}
|
|
329
|
+
for (const { key, delta } of increments) {
|
|
330
|
+
operations.push({ key, kind: 'increment', delta });
|
|
331
|
+
}
|
|
332
|
+
intentDocuments.push({ path: entry.destinationPath, operations });
|
|
333
|
+
}
|
|
334
|
+
return { documents: intentDocuments, deletePaths };
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Minimal base58 decoder, used ONLY to recognize a declared-Address field: a
|
|
338
|
+
* request string that decodes to exactly the 32 bytes the instruction carries
|
|
339
|
+
* is a faithful AddressVal encoding of that string. Returns null on any
|
|
340
|
+
* non-base58 character.
|
|
341
|
+
*/
|
|
342
|
+
function decodeBase58(text) {
|
|
343
|
+
if (text.length === 0)
|
|
344
|
+
return null;
|
|
345
|
+
let value = BigInt(0);
|
|
346
|
+
for (const ch of text) {
|
|
347
|
+
const digit = BASE58_ALPHABET.indexOf(ch);
|
|
348
|
+
if (digit < 0)
|
|
349
|
+
return null;
|
|
350
|
+
value = value * BigInt(58) + BigInt(digit);
|
|
351
|
+
}
|
|
352
|
+
// Leading '1' characters are leading zero bytes, not part of the number.
|
|
353
|
+
let leadingZeros = 0;
|
|
354
|
+
while (leadingZeros < text.length && text[leadingZeros] === BASE58_ALPHABET[0])
|
|
355
|
+
leadingZeros++;
|
|
356
|
+
const body = [];
|
|
357
|
+
if (value > BigInt(0)) {
|
|
358
|
+
let hex = value.toString(16);
|
|
359
|
+
if (hex.length % 2 === 1)
|
|
360
|
+
hex = `0${hex}`;
|
|
361
|
+
for (let i = 0; i < hex.length; i += 2)
|
|
362
|
+
body.push(parseInt(hex.slice(i, i + 2), 16));
|
|
363
|
+
}
|
|
364
|
+
const out = new Uint8Array(leadingZeros + body.length);
|
|
365
|
+
out.set(body, leadingZeros);
|
|
366
|
+
return out;
|
|
224
367
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Whether `parsed` is an encoding the HONEST builder could have produced for
|
|
370
|
+
* the request field value. The onchain FieldValue variant is chosen by the
|
|
371
|
+
* collection's DECLARED schema (worker-side `jsValueToFieldValue`), which the
|
|
372
|
+
* client does not hold, so every variant any declared type could yield for this
|
|
373
|
+
* JSON value is accepted - and nothing else:
|
|
374
|
+
* - boolean: BoolVal only.
|
|
375
|
+
* - integer number/bigint: U64Val (inference/UInt; UInt takes abs for
|
|
376
|
+
* negatives) or I64Val (Int), of the exact integer.
|
|
377
|
+
* - fractional number: StringVal of its decimal text (inference/Float), or a
|
|
378
|
+
* truncated U64/I64 (declared UInt/Int truncates toward zero).
|
|
379
|
+
* - string: StringVal verbatim, AddressVal when it is the base58 of the exact
|
|
380
|
+
* 32 bytes (declared Address), or U64/I64 of its integer text (declared
|
|
381
|
+
* UInt/Int accept numeric strings, truncating any fraction).
|
|
382
|
+
* - array/object: StringVal of its JSON serialization (identical on both sides
|
|
383
|
+
* of the wire for the round-tripped value).
|
|
384
|
+
*/
|
|
385
|
+
function isFaithfulFieldValueEncoding(requestValue, parsed) {
|
|
386
|
+
if (parsed === null)
|
|
387
|
+
return false;
|
|
388
|
+
if (typeof requestValue === 'boolean') {
|
|
389
|
+
return parsed.variant === 'bool' && parsed.value === requestValue;
|
|
390
|
+
}
|
|
391
|
+
if (typeof requestValue === 'number') {
|
|
392
|
+
if (!Number.isFinite(requestValue))
|
|
393
|
+
return false;
|
|
394
|
+
if (Number.isInteger(requestValue)) {
|
|
395
|
+
const exact = BigInt(requestValue);
|
|
396
|
+
if (parsed.variant === 'i64' && parsed.value === exact)
|
|
397
|
+
return true;
|
|
398
|
+
// The declared-UInt encoder stores abs(value), so a negative request
|
|
399
|
+
// integer can honestly arrive as its positive magnitude.
|
|
400
|
+
return parsed.variant === 'u64' && parsed.value === (exact < BigInt(0) ? -exact : exact);
|
|
401
|
+
}
|
|
402
|
+
if (parsed.variant === 'string' && parsed.value === String(requestValue))
|
|
403
|
+
return true;
|
|
404
|
+
const truncated = BigInt(Math.trunc(requestValue));
|
|
405
|
+
if (parsed.variant === 'i64' && parsed.value === truncated)
|
|
406
|
+
return true;
|
|
407
|
+
return parsed.variant === 'u64' && parsed.value === (truncated < BigInt(0) ? -truncated : truncated);
|
|
408
|
+
}
|
|
409
|
+
if (typeof requestValue === 'string') {
|
|
410
|
+
if (parsed.variant === 'string' && parsed.value === requestValue)
|
|
411
|
+
return true;
|
|
412
|
+
if (parsed.variant === 'address') {
|
|
413
|
+
const decoded = decodeBase58(requestValue);
|
|
414
|
+
const expectedBytes = parsed.value;
|
|
415
|
+
if (decoded !== null && decoded.length === 32 && decoded.every((byte, i) => byte === expectedBytes[i]))
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (parsed.variant === 'u64' || parsed.variant === 'i64') {
|
|
419
|
+
const trimmed = requestValue.trim();
|
|
420
|
+
const intPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed;
|
|
421
|
+
if (/^-?\d+$/.test(intPart)) {
|
|
422
|
+
const numeric = BigInt(intPart);
|
|
423
|
+
if (parsed.variant === 'i64' && parsed.value === numeric)
|
|
424
|
+
return true;
|
|
425
|
+
if (parsed.variant === 'u64' && parsed.value === (numeric < BigInt(0) ? -numeric : numeric))
|
|
426
|
+
return true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return false;
|
|
430
|
+
}
|
|
431
|
+
if (requestValue !== null && typeof requestValue === 'object') {
|
|
432
|
+
return parsed.variant === 'string' && parsed.value === JSON.stringify(requestValue);
|
|
433
|
+
}
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const EXPECTED_KIND_TO_WIRE = {
|
|
437
|
+
write: FIELD_OPERATION_WRITE,
|
|
438
|
+
delete: FIELD_OPERATION_DELETE,
|
|
439
|
+
timestamp: FIELD_OPERATION_TIMESTAMP,
|
|
440
|
+
increment: FIELD_OPERATION_INCREMENT,
|
|
441
|
+
};
|
|
442
|
+
function describeParsedValue(value) {
|
|
443
|
+
if (value === null)
|
|
444
|
+
return 'no value';
|
|
445
|
+
if (value.variant === 'address')
|
|
446
|
+
return `address 0x${bufferExports.Buffer.from(value.value).toString('hex')}`;
|
|
447
|
+
if (value.variant === 'u64' || value.variant === 'i64')
|
|
448
|
+
return `${value.variant} ${value.value.toString()}`;
|
|
449
|
+
return `${value.variant} ${JSON.stringify(value.value)}`;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* OA-0001: the one Bounded set-documents instruction in a server-built
|
|
453
|
+
* transaction must encode EXACTLY the requested write - app id, ordered
|
|
454
|
+
* documents, per-field operation key/kind/value, ordered deletes, and a clear
|
|
455
|
+
* simulate flag - or the transaction is rejected BEFORE the wallet sees it.
|
|
456
|
+
*
|
|
457
|
+
* The comparison is ordered because the honest builder (realtime-worker's
|
|
458
|
+
* solana-tx-builder) preserves the request's document/delete/operation order
|
|
459
|
+
* deterministically; nothing legitimate reorders, and an ordered check catches
|
|
460
|
+
* duplicate-path and reorder tampering the retired path-SET comparison hid.
|
|
461
|
+
*
|
|
462
|
+
* Deliberately NOT bound here: the plugin `txData` entries and the remaining
|
|
463
|
+
* accounts their ra_indices reference. Those are derived by server-side policy
|
|
464
|
+
* hooks (a Jupiter quote, an ATA layout), not by the caller's request, so the
|
|
465
|
+
* client holds no request-derived expectation to compare against; that surface
|
|
466
|
+
* stays bound by the onchain policy, the typed setup-instruction guards, and
|
|
467
|
+
* the attestation co-signer the program demands for any non-empty txData.
|
|
468
|
+
*/
|
|
469
|
+
function assertMatchesExpectedIntent(label, expectedAppId, expected, parsed) {
|
|
470
|
+
var _a;
|
|
471
|
+
if (parsed.appId !== expectedAppId)
|
|
472
|
+
throw new Error(`${label} Bounded instruction appId does not match configured appId`);
|
|
473
|
+
if (parsed.simulate) {
|
|
474
|
+
throw new Error(`${label} Bounded instruction has the simulate flag set; a client-signed write must be a real execution`);
|
|
475
|
+
}
|
|
476
|
+
if (parsed.deletePaths.length !== expected.deletePaths.length ||
|
|
477
|
+
parsed.deletePaths.some((path, i) => path !== normalizeOnchainPath(expected.deletePaths[i]))) {
|
|
478
|
+
throw new Error(`${label} Bounded instruction delete paths do not match the requested deletes`);
|
|
479
|
+
}
|
|
480
|
+
if (parsed.documents.length !== expected.documents.length) {
|
|
481
|
+
throw new Error(`${label} Bounded instruction document count does not match the requested write ` +
|
|
482
|
+
`(expected ${expected.documents.length}, got ${parsed.documents.length})`);
|
|
483
|
+
}
|
|
484
|
+
for (let i = 0; i < expected.documents.length; i++) {
|
|
485
|
+
const expectedDoc = expected.documents[i];
|
|
486
|
+
const actualDoc = parsed.documents[i];
|
|
487
|
+
const expectedPath = normalizeOnchainPath(expectedDoc.path);
|
|
488
|
+
if (actualDoc.path !== expectedPath) {
|
|
489
|
+
throw new Error(`${label} Bounded instruction document path "${actualDoc.path}" does not match requested path "${expectedPath}"`);
|
|
490
|
+
}
|
|
491
|
+
if (actualDoc.operations.length !== expectedDoc.operations.length) {
|
|
492
|
+
throw new Error(`${label} Bounded instruction operations for path "${expectedPath}" do not match the requested write ` +
|
|
493
|
+
`(expected ${expectedDoc.operations.length} operations, got ${actualDoc.operations.length})`);
|
|
494
|
+
}
|
|
495
|
+
for (let j = 0; j < expectedDoc.operations.length; j++) {
|
|
496
|
+
const expectedOp = expectedDoc.operations[j];
|
|
497
|
+
const actualOp = actualDoc.operations[j];
|
|
498
|
+
if (actualOp.key !== expectedOp.key || actualOp.kind !== EXPECTED_KIND_TO_WIRE[expectedOp.kind]) {
|
|
499
|
+
throw new Error(`${label} Bounded instruction operation ${j} on path "${expectedPath}" does not match the requested write ` +
|
|
500
|
+
`(expected key "${expectedOp.key}" kind ${expectedOp.kind}, got key "${actualOp.key}" kind ${actualOp.kind})`);
|
|
501
|
+
}
|
|
502
|
+
if (expectedOp.kind === 'delete' || expectedOp.kind === 'timestamp') {
|
|
503
|
+
if (actualOp.value !== null) {
|
|
504
|
+
throw new Error(`${label} Bounded instruction operation "${expectedOp.key}" on path "${expectedPath}" carries an unexpected value`);
|
|
505
|
+
}
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (expectedOp.kind === 'increment') {
|
|
509
|
+
const expectedDelta = BigInt(Math.trunc(expectedOp.delta));
|
|
510
|
+
if (((_a = actualOp.value) === null || _a === void 0 ? void 0 : _a.variant) !== 'i64' || actualOp.value.value !== expectedDelta) {
|
|
511
|
+
throw new Error(`${label} Bounded instruction increment "${expectedOp.key}" on path "${expectedPath}" does not match the requested delta`);
|
|
512
|
+
}
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (!isFaithfulFieldValueEncoding(expectedOp.value, actualOp.value)) {
|
|
516
|
+
throw new Error(`${label} Bounded instruction operation "${expectedOp.key}" on path "${expectedPath}" encodes a value that does not match the requested field ` +
|
|
517
|
+
`(got ${describeParsedValue(actualOp.value)})`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
234
520
|
}
|
|
235
521
|
}
|
|
236
522
|
/** Read a little-endian u64 as a BigInt (lamport amounts can exceed 2^53). */
|
|
@@ -240,6 +526,11 @@ function readU64LE(bytes, offset) {
|
|
|
240
526
|
value |= BigInt(bytes[offset + i]) << BigInt(8 * i);
|
|
241
527
|
return value;
|
|
242
528
|
}
|
|
529
|
+
/** Read a little-endian i64 as a BigInt (two's complement). */
|
|
530
|
+
function readI64LE(bytes, offset) {
|
|
531
|
+
const unsigned = readU64LE(bytes, offset);
|
|
532
|
+
return unsigned >= (BigInt(1) << BigInt(63)) ? unsigned - (BigInt(1) << BigInt(64)) : unsigned;
|
|
533
|
+
}
|
|
243
534
|
/**
|
|
244
535
|
* Resolve an instruction's referenced account keys against the transaction's
|
|
245
536
|
* STATIC key list. A setup instruction that reaches into an address-lookup
|
|
@@ -318,14 +609,14 @@ function deserializeAndValidateServerTransaction(serializedTransaction, web3, op
|
|
|
318
609
|
var _a;
|
|
319
610
|
const { VersionedTransaction: VersionedTransactionClass, PublicKey: PublicKeyCtor } = web3;
|
|
320
611
|
const transaction = VersionedTransactionClass.deserialize(bufferExports.Buffer.from(serializedTransaction, 'base64'));
|
|
321
|
-
const { label, expectedAppId,
|
|
612
|
+
const { label, expectedAppId, expectedIntent } = options;
|
|
322
613
|
const accountKeys = transaction.message.staticAccountKeys;
|
|
323
614
|
if (accountKeys.length === 0)
|
|
324
615
|
throw new Error(`${label} has no static account keys`);
|
|
325
616
|
const feePayer = accountKeys[0];
|
|
326
617
|
const isAccountSigner = (index) => transaction.message.isAccountSigner(index);
|
|
327
618
|
let boundedInstructionCount = 0;
|
|
328
|
-
|
|
619
|
+
let parsedBoundedInstruction = null;
|
|
329
620
|
for (const ix of transaction.message.compiledInstructions) {
|
|
330
621
|
if (ix.programIdIndex >= accountKeys.length)
|
|
331
622
|
throw new Error(`${label} has program ID in lookup table (not allowed)`);
|
|
@@ -345,31 +636,38 @@ function deserializeAndValidateServerTransaction(serializedTransaction, web3, op
|
|
|
345
636
|
}
|
|
346
637
|
else if (programId === BOUNDED_PROGRAM_MAINNET || programId === BOUNDED_PROGRAM_DEVNET) {
|
|
347
638
|
boundedInstructionCount++;
|
|
348
|
-
|
|
349
|
-
if (parsed.appId !== expectedAppId)
|
|
350
|
-
throw new Error(`${label} Bounded instruction appId does not match configured appId`);
|
|
351
|
-
actualWritePaths.push(...parsed.documentPaths, ...parsed.deletePaths);
|
|
639
|
+
parsedBoundedInstruction = parseBoundedSetDocumentsInstruction(data, label);
|
|
352
640
|
}
|
|
353
641
|
}
|
|
354
642
|
if (boundedInstructionCount !== 1)
|
|
355
643
|
throw new Error(`${label} must contain exactly one Bounded set-documents instruction`);
|
|
356
|
-
|
|
644
|
+
// OA-0001: bind the FULL operation semantics of the one Bounded instruction to
|
|
645
|
+
// the caller's own request before any signer runs - not just the app/path
|
|
646
|
+
// envelope, which a tampered builder response can keep while changing amounts.
|
|
647
|
+
assertMatchesExpectedIntent(label, expectedAppId, expectedIntent, parsedBoundedInstruction);
|
|
357
648
|
return transaction;
|
|
358
649
|
}
|
|
359
|
-
async function handlePreBuiltTransaction(tx, authProvider, options,
|
|
650
|
+
async function handlePreBuiltTransaction(tx, authProvider, options, expectedIntent) {
|
|
360
651
|
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
361
652
|
const { Connection, VersionedTransaction, PublicKey } = await import('@solana/web3.js');
|
|
362
653
|
// Honor a wallet client's immutable config snapshot for the expected app-id
|
|
363
654
|
// binding and RPC endpoint (CTA-05); otherwise read the global init() config.
|
|
364
655
|
const config = (_b = (_a = options === null || options === void 0 ? void 0 : options._overrides) === null || _a === void 0 ? void 0 : _a._config) !== null && _b !== void 0 ? _b : await getConfig();
|
|
365
656
|
const transaction = deserializeAndValidateServerTransaction(tx.serializedTransaction, { VersionedTransaction, PublicKey }, {
|
|
366
|
-
label: 'Pre-built transaction', expectedAppId: config.appId,
|
|
657
|
+
label: 'Pre-built transaction', expectedAppId: config.appId, expectedIntent,
|
|
367
658
|
});
|
|
368
659
|
const shouldSubmit = (options === null || options === void 0 ? void 0 : options.shouldSubmitTx) !== false;
|
|
369
660
|
const rpcUrl = (_c = config.rpcUrl) === null || _c === void 0 ? void 0 : _c.trim();
|
|
370
661
|
const injectedRpc = (_d = options === null || options === void 0 ? void 0 : options._overrides) === null || _d === void 0 ? void 0 : _d._solanaRpc;
|
|
371
662
|
if (shouldSubmit && !rpcUrl && !injectedRpc) {
|
|
372
|
-
|
|
663
|
+
// Checked BEFORE the wallet signs, so a misconfigured app fails its first
|
|
664
|
+
// launch attempt without spending the user's signature. Deliberately no
|
|
665
|
+
// bundled default endpoint (2026-06-28 hardening): the app must supply the
|
|
666
|
+
// RPC it submits through.
|
|
667
|
+
throw new Error(`Pre-built Solana transaction submission requires init({ rpcUrl }) for ${tx.network}. ` +
|
|
668
|
+
`Pass a TOP-LEVEL rpcUrl (with chain) to init(), e.g. ` +
|
|
669
|
+
`init({ appId, chain: '${tx.network}', rpcUrl: '<your ${tx.network} RPC endpoint>' }); ` +
|
|
670
|
+
`a nested walletLogin.rpcUrl only configures wallet login and does not enable submission.`);
|
|
373
671
|
}
|
|
374
672
|
const signedTx = await authProvider.signTransaction(transaction);
|
|
375
673
|
const rawTx = signedTx.serialize();
|
|
@@ -450,5 +748,5 @@ async function handlePreBuiltTransaction(tx, authProvider, options, expectedWrit
|
|
|
450
748
|
};
|
|
451
749
|
}
|
|
452
750
|
|
|
453
|
-
export { deriveTransactionSignature, handlePreBuiltTransaction, isDefinitiveSolanaSendFailure };
|
|
454
|
-
//# sourceMappingURL=solana-operations-
|
|
751
|
+
export { buildExpectedWriteIntent, deriveTransactionSignature, handlePreBuiltTransaction, isDefinitiveSolanaSendFailure };
|
|
752
|
+
//# sourceMappingURL=solana-operations-DiM6_8z7.mjs.map
|