hyperstack-typescript 0.4.3 → 0.5.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/dist/index.d.ts +101 -24
- package/dist/index.esm.js +519 -102
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +529 -102
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -316,6 +316,31 @@ class FrameProcessor {
|
|
|
316
316
|
? DEFAULT_MAX_ENTRIES_PER_VIEW
|
|
317
317
|
: config.maxEntriesPerView;
|
|
318
318
|
this.flushIntervalMs = config.flushIntervalMs ?? 0;
|
|
319
|
+
this.schemas = config.schemas;
|
|
320
|
+
}
|
|
321
|
+
getSchema(viewPath) {
|
|
322
|
+
const schemas = this.schemas;
|
|
323
|
+
if (!schemas)
|
|
324
|
+
return null;
|
|
325
|
+
const entityName = viewPath.split('/')[0];
|
|
326
|
+
if (typeof entityName !== 'string' || entityName.length === 0)
|
|
327
|
+
return null;
|
|
328
|
+
const entityKey = entityName;
|
|
329
|
+
return schemas[entityKey] ?? null;
|
|
330
|
+
}
|
|
331
|
+
validateEntity(viewPath, data) {
|
|
332
|
+
const schema = this.getSchema(viewPath);
|
|
333
|
+
if (!schema)
|
|
334
|
+
return true;
|
|
335
|
+
const result = schema.safeParse(data);
|
|
336
|
+
if (!result.success) {
|
|
337
|
+
console.warn('[Hyperstack] Frame validation failed:', {
|
|
338
|
+
view: viewPath,
|
|
339
|
+
error: result.error,
|
|
340
|
+
});
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
319
344
|
}
|
|
320
345
|
handleFrame(frame) {
|
|
321
346
|
if (this.flushIntervalMs === 0) {
|
|
@@ -411,6 +436,9 @@ class FrameProcessor {
|
|
|
411
436
|
handleSnapshotFrameWithoutEnforce(frame) {
|
|
412
437
|
const viewPath = frame.entity;
|
|
413
438
|
for (const entity of frame.data) {
|
|
439
|
+
if (!this.validateEntity(viewPath, entity.data)) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
414
442
|
const previousValue = this.storage.get(viewPath, entity.key);
|
|
415
443
|
this.storage.set(viewPath, entity.key, entity.data);
|
|
416
444
|
this.storage.notifyUpdate(viewPath, entity.key, {
|
|
@@ -431,6 +459,9 @@ class FrameProcessor {
|
|
|
431
459
|
switch (frame.op) {
|
|
432
460
|
case 'create':
|
|
433
461
|
case 'upsert':
|
|
462
|
+
if (!this.validateEntity(viewPath, frame.data)) {
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
434
465
|
this.storage.set(viewPath, frame.key, frame.data);
|
|
435
466
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
436
467
|
type: 'upsert',
|
|
@@ -445,6 +476,9 @@ class FrameProcessor {
|
|
|
445
476
|
const merged = existing
|
|
446
477
|
? deepMergeWithAppend$1(existing, frame.data, appendPaths)
|
|
447
478
|
: frame.data;
|
|
479
|
+
if (!this.validateEntity(viewPath, merged)) {
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
448
482
|
this.storage.set(viewPath, frame.key, merged);
|
|
449
483
|
this.storage.notifyUpdate(viewPath, frame.key, {
|
|
450
484
|
type: 'patch',
|
|
@@ -933,7 +967,8 @@ function createUpdateStream(storage, subscriptionRegistry, subscription, keyFilt
|
|
|
933
967
|
},
|
|
934
968
|
};
|
|
935
969
|
}
|
|
936
|
-
function createEntityStream(storage, subscriptionRegistry, subscription, keyFilter) {
|
|
970
|
+
function createEntityStream(storage, subscriptionRegistry, subscription, options, keyFilter) {
|
|
971
|
+
const schema = options?.schema;
|
|
937
972
|
return {
|
|
938
973
|
[Symbol.asyncIterator]() {
|
|
939
974
|
const queue = [];
|
|
@@ -949,16 +984,27 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
|
|
|
949
984
|
if (update.type === 'deleted')
|
|
950
985
|
return;
|
|
951
986
|
const entity = (update.type === 'created' ? update.data : update.after);
|
|
987
|
+
let output;
|
|
988
|
+
if (schema) {
|
|
989
|
+
const parsed = schema.safeParse(entity);
|
|
990
|
+
if (!parsed.success) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
output = parsed.data;
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
output = entity;
|
|
997
|
+
}
|
|
952
998
|
if (waitingResolve) {
|
|
953
999
|
const resolve = waitingResolve;
|
|
954
1000
|
waitingResolve = null;
|
|
955
|
-
resolve({ value:
|
|
1001
|
+
resolve({ value: output, done: false });
|
|
956
1002
|
}
|
|
957
1003
|
else {
|
|
958
1004
|
if (queue.length >= MAX_QUEUE_SIZE) {
|
|
959
1005
|
queue.shift();
|
|
960
1006
|
}
|
|
961
|
-
queue.push(
|
|
1007
|
+
queue.push(output);
|
|
962
1008
|
}
|
|
963
1009
|
};
|
|
964
1010
|
const start = () => {
|
|
@@ -971,7 +1017,7 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
|
|
|
971
1017
|
unsubscribeRegistry?.();
|
|
972
1018
|
};
|
|
973
1019
|
start();
|
|
974
|
-
|
|
1020
|
+
const iterator = {
|
|
975
1021
|
async next() {
|
|
976
1022
|
if (done) {
|
|
977
1023
|
return { value: undefined, done: true };
|
|
@@ -993,6 +1039,7 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
|
|
|
993
1039
|
throw error;
|
|
994
1040
|
},
|
|
995
1041
|
};
|
|
1042
|
+
return iterator;
|
|
996
1043
|
},
|
|
997
1044
|
};
|
|
998
1045
|
}
|
|
@@ -1064,13 +1111,16 @@ function createRichUpdateStream(storage, subscriptionRegistry, subscription, key
|
|
|
1064
1111
|
function createTypedStateView(viewDef, storage, subscriptionRegistry) {
|
|
1065
1112
|
return {
|
|
1066
1113
|
use(key, options) {
|
|
1067
|
-
|
|
1114
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1115
|
+
return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, options, key);
|
|
1068
1116
|
},
|
|
1069
1117
|
watch(key, options) {
|
|
1070
|
-
|
|
1118
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1119
|
+
return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
|
|
1071
1120
|
},
|
|
1072
1121
|
watchRich(key, options) {
|
|
1073
|
-
|
|
1122
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1123
|
+
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
|
|
1074
1124
|
},
|
|
1075
1125
|
async get(key) {
|
|
1076
1126
|
return storage.get(viewDef.view, key);
|
|
@@ -1083,13 +1133,16 @@ function createTypedStateView(viewDef, storage, subscriptionRegistry) {
|
|
|
1083
1133
|
function createTypedListView(viewDef, storage, subscriptionRegistry) {
|
|
1084
1134
|
return {
|
|
1085
1135
|
use(options) {
|
|
1086
|
-
|
|
1136
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1137
|
+
return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions }, options);
|
|
1087
1138
|
},
|
|
1088
1139
|
watch(options) {
|
|
1089
|
-
|
|
1140
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1141
|
+
return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
|
|
1090
1142
|
},
|
|
1091
1143
|
watchRich(options) {
|
|
1092
|
-
|
|
1144
|
+
const { schema: _schema, ...subscriptionOptions } = options ?? {};
|
|
1145
|
+
return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
|
|
1093
1146
|
},
|
|
1094
1147
|
async get() {
|
|
1095
1148
|
return storage.getAll(viewDef.view);
|
|
@@ -1117,39 +1170,335 @@ function createTypedViews(stack, storage, subscriptionRegistry) {
|
|
|
1117
1170
|
return views;
|
|
1118
1171
|
}
|
|
1119
1172
|
|
|
1173
|
+
/**
|
|
1174
|
+
* PDA (Program Derived Address) derivation utilities.
|
|
1175
|
+
*
|
|
1176
|
+
* Implements Solana's PDA derivation algorithm without depending on @solana/web3.js.
|
|
1177
|
+
*/
|
|
1178
|
+
// Base58 alphabet (Bitcoin/Solana style)
|
|
1179
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
1180
|
+
/**
|
|
1181
|
+
* Decode base58 string to Uint8Array.
|
|
1182
|
+
*/
|
|
1183
|
+
function decodeBase58(str) {
|
|
1184
|
+
if (str.length === 0) {
|
|
1185
|
+
return new Uint8Array(0);
|
|
1186
|
+
}
|
|
1187
|
+
const bytes = [0];
|
|
1188
|
+
for (const char of str) {
|
|
1189
|
+
const value = BASE58_ALPHABET.indexOf(char);
|
|
1190
|
+
if (value === -1) {
|
|
1191
|
+
throw new Error('Invalid base58 character: ' + char);
|
|
1192
|
+
}
|
|
1193
|
+
let carry = value;
|
|
1194
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1195
|
+
carry += (bytes[i] ?? 0) * 58;
|
|
1196
|
+
bytes[i] = carry & 0xff;
|
|
1197
|
+
carry >>= 8;
|
|
1198
|
+
}
|
|
1199
|
+
while (carry > 0) {
|
|
1200
|
+
bytes.push(carry & 0xff);
|
|
1201
|
+
carry >>= 8;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
// Add leading zeros for each leading '1' in input
|
|
1205
|
+
for (const char of str) {
|
|
1206
|
+
if (char !== '1')
|
|
1207
|
+
break;
|
|
1208
|
+
bytes.push(0);
|
|
1209
|
+
}
|
|
1210
|
+
return new Uint8Array(bytes.reverse());
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Encode Uint8Array to base58 string.
|
|
1214
|
+
*/
|
|
1215
|
+
function encodeBase58(bytes) {
|
|
1216
|
+
if (bytes.length === 0) {
|
|
1217
|
+
return '';
|
|
1218
|
+
}
|
|
1219
|
+
const digits = [0];
|
|
1220
|
+
for (const byte of bytes) {
|
|
1221
|
+
let carry = byte;
|
|
1222
|
+
for (let i = 0; i < digits.length; i++) {
|
|
1223
|
+
carry += (digits[i] ?? 0) << 8;
|
|
1224
|
+
digits[i] = carry % 58;
|
|
1225
|
+
carry = (carry / 58) | 0;
|
|
1226
|
+
}
|
|
1227
|
+
while (carry > 0) {
|
|
1228
|
+
digits.push(carry % 58);
|
|
1229
|
+
carry = (carry / 58) | 0;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// Add leading zeros for each leading 0 byte in input
|
|
1233
|
+
for (const byte of bytes) {
|
|
1234
|
+
if (byte !== 0)
|
|
1235
|
+
break;
|
|
1236
|
+
digits.push(0);
|
|
1237
|
+
}
|
|
1238
|
+
return digits.reverse().map(d => BASE58_ALPHABET[d]).join('');
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* SHA-256 hash function (synchronous, Node.js).
|
|
1242
|
+
*/
|
|
1243
|
+
function sha256Sync(data) {
|
|
1244
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
1245
|
+
const { createHash } = require('crypto');
|
|
1246
|
+
return new Uint8Array(createHash('sha256').update(Buffer.from(data)).digest());
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* SHA-256 hash function (async, works in browser and Node.js).
|
|
1250
|
+
*/
|
|
1251
|
+
async function sha256Async(data) {
|
|
1252
|
+
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
|
|
1253
|
+
// Create a copy of the data to ensure we have an ArrayBuffer
|
|
1254
|
+
const copy = new Uint8Array(data);
|
|
1255
|
+
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', copy);
|
|
1256
|
+
return new Uint8Array(hashBuffer);
|
|
1257
|
+
}
|
|
1258
|
+
return sha256Sync(data);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* PDA marker bytes appended to seeds before hashing.
|
|
1262
|
+
*/
|
|
1263
|
+
const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
|
|
1264
|
+
/**
|
|
1265
|
+
* Build the hash input buffer for PDA derivation.
|
|
1266
|
+
*/
|
|
1267
|
+
function buildPdaBuffer(seeds, programIdBytes, bump) {
|
|
1268
|
+
const totalLength = seeds.reduce((sum, s) => sum + s.length, 0)
|
|
1269
|
+
+ 1 // bump
|
|
1270
|
+
+ 32 // programId
|
|
1271
|
+
+ PDA_MARKER.length;
|
|
1272
|
+
const buffer = new Uint8Array(totalLength);
|
|
1273
|
+
let offset = 0;
|
|
1274
|
+
// Copy seeds
|
|
1275
|
+
for (const seed of seeds) {
|
|
1276
|
+
buffer.set(seed, offset);
|
|
1277
|
+
offset += seed.length;
|
|
1278
|
+
}
|
|
1279
|
+
// Add bump seed
|
|
1280
|
+
buffer[offset++] = bump;
|
|
1281
|
+
// Add program ID
|
|
1282
|
+
buffer.set(programIdBytes, offset);
|
|
1283
|
+
offset += 32;
|
|
1284
|
+
// Add PDA marker
|
|
1285
|
+
buffer.set(PDA_MARKER, offset);
|
|
1286
|
+
return buffer;
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Validate seeds before PDA derivation.
|
|
1290
|
+
*/
|
|
1291
|
+
function validateSeeds(seeds) {
|
|
1292
|
+
if (seeds.length > 16) {
|
|
1293
|
+
throw new Error('Maximum of 16 seeds allowed');
|
|
1294
|
+
}
|
|
1295
|
+
for (let i = 0; i < seeds.length; i++) {
|
|
1296
|
+
const seed = seeds[i];
|
|
1297
|
+
if (seed && seed.length > 32) {
|
|
1298
|
+
throw new Error('Seed ' + i + ' exceeds maximum length of 32 bytes');
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Derives a Program-Derived Address (PDA) from seeds and program ID.
|
|
1304
|
+
*
|
|
1305
|
+
* Algorithm:
|
|
1306
|
+
* 1. For bump = 255 down to 0:
|
|
1307
|
+
* a. Concatenate: seeds + [bump] + programId + "ProgramDerivedAddress"
|
|
1308
|
+
* b. SHA-256 hash the concatenation
|
|
1309
|
+
* c. If result is off the ed25519 curve, return it
|
|
1310
|
+
* 2. If no valid PDA found after 256 attempts, throw error
|
|
1311
|
+
*
|
|
1312
|
+
* @param seeds - Array of seed buffers (max 32 bytes each, max 16 seeds)
|
|
1313
|
+
* @param programId - The program ID (base58 string)
|
|
1314
|
+
* @returns Tuple of [derivedAddress (base58), bumpSeed]
|
|
1315
|
+
*/
|
|
1316
|
+
async function findProgramAddress(seeds, programId) {
|
|
1317
|
+
validateSeeds(seeds);
|
|
1318
|
+
const programIdBytes = decodeBase58(programId);
|
|
1319
|
+
if (programIdBytes.length !== 32) {
|
|
1320
|
+
throw new Error('Program ID must be 32 bytes');
|
|
1321
|
+
}
|
|
1322
|
+
// Try bump seeds from 255 down to 0
|
|
1323
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
1324
|
+
const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
|
|
1325
|
+
const hash = await sha256Async(buffer);
|
|
1326
|
+
{
|
|
1327
|
+
return [encodeBase58(hash), bump];
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
throw new Error('Unable to find a valid PDA');
|
|
1331
|
+
}
|
|
1332
|
+
/**
|
|
1333
|
+
* Synchronous version of findProgramAddress.
|
|
1334
|
+
* Uses synchronous SHA-256 (Node.js crypto module).
|
|
1335
|
+
*/
|
|
1336
|
+
function findProgramAddressSync(seeds, programId) {
|
|
1337
|
+
validateSeeds(seeds);
|
|
1338
|
+
const programIdBytes = decodeBase58(programId);
|
|
1339
|
+
if (programIdBytes.length !== 32) {
|
|
1340
|
+
throw new Error('Program ID must be 32 bytes');
|
|
1341
|
+
}
|
|
1342
|
+
// Try bump seeds from 255 down to 0
|
|
1343
|
+
for (let bump = 255; bump >= 0; bump--) {
|
|
1344
|
+
const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
|
|
1345
|
+
const hash = sha256Sync(buffer);
|
|
1346
|
+
{
|
|
1347
|
+
return [encodeBase58(hash), bump];
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
throw new Error('Unable to find a valid PDA');
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Creates a seed buffer from various input types.
|
|
1354
|
+
*
|
|
1355
|
+
* @param value - The value to convert to a seed
|
|
1356
|
+
* @returns Uint8Array suitable for PDA derivation
|
|
1357
|
+
*/
|
|
1358
|
+
function createSeed(value) {
|
|
1359
|
+
if (value instanceof Uint8Array) {
|
|
1360
|
+
return value;
|
|
1361
|
+
}
|
|
1362
|
+
if (typeof value === 'string') {
|
|
1363
|
+
return new TextEncoder().encode(value);
|
|
1364
|
+
}
|
|
1365
|
+
if (typeof value === 'bigint') {
|
|
1366
|
+
// Convert bigint to 8-byte buffer (u64 little-endian)
|
|
1367
|
+
const buffer = new Uint8Array(8);
|
|
1368
|
+
let n = value;
|
|
1369
|
+
for (let i = 0; i < 8; i++) {
|
|
1370
|
+
buffer[i] = Number(n & BigInt(0xff));
|
|
1371
|
+
n >>= BigInt(8);
|
|
1372
|
+
}
|
|
1373
|
+
return buffer;
|
|
1374
|
+
}
|
|
1375
|
+
if (typeof value === 'number') {
|
|
1376
|
+
// Assume u64
|
|
1377
|
+
return createSeed(BigInt(value));
|
|
1378
|
+
}
|
|
1379
|
+
throw new Error('Cannot create seed from value');
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Creates a public key seed from a base58-encoded address.
|
|
1383
|
+
*
|
|
1384
|
+
* @param address - Base58-encoded public key
|
|
1385
|
+
* @returns 32-byte Uint8Array
|
|
1386
|
+
*/
|
|
1387
|
+
function createPublicKeySeed(address) {
|
|
1388
|
+
const decoded = decodeBase58(address);
|
|
1389
|
+
if (decoded.length !== 32) {
|
|
1390
|
+
throw new Error('Invalid public key length: expected 32, got ' + decoded.length);
|
|
1391
|
+
}
|
|
1392
|
+
return decoded;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* Topologically sort accounts so that dependencies (accountRef) are resolved first.
|
|
1397
|
+
* Non-PDA accounts come first, then PDAs in dependency order.
|
|
1398
|
+
*/
|
|
1399
|
+
function sortAccountsByDependency(accountMetas) {
|
|
1400
|
+
// Separate non-PDA and PDA accounts
|
|
1401
|
+
const nonPda = [];
|
|
1402
|
+
const pda = [];
|
|
1403
|
+
for (const meta of accountMetas) {
|
|
1404
|
+
if (meta.category === 'pda') {
|
|
1405
|
+
pda.push(meta);
|
|
1406
|
+
}
|
|
1407
|
+
else {
|
|
1408
|
+
nonPda.push(meta);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
// Build dependency graph for PDAs
|
|
1412
|
+
const pdaDeps = new Map();
|
|
1413
|
+
for (const meta of pda) {
|
|
1414
|
+
const deps = new Set();
|
|
1415
|
+
if (meta.pdaConfig) {
|
|
1416
|
+
for (const seed of meta.pdaConfig.seeds) {
|
|
1417
|
+
if (seed.type === 'accountRef') {
|
|
1418
|
+
deps.add(seed.accountName);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
pdaDeps.set(meta.name, deps);
|
|
1423
|
+
}
|
|
1424
|
+
// Topological sort PDAs
|
|
1425
|
+
const sortedPda = [];
|
|
1426
|
+
const visited = new Set();
|
|
1427
|
+
const visiting = new Set();
|
|
1428
|
+
function visit(name) {
|
|
1429
|
+
if (visited.has(name))
|
|
1430
|
+
return;
|
|
1431
|
+
if (visiting.has(name)) {
|
|
1432
|
+
throw new Error('Circular dependency in PDA accounts: ' + name);
|
|
1433
|
+
}
|
|
1434
|
+
const meta = pda.find(m => m.name === name);
|
|
1435
|
+
if (!meta)
|
|
1436
|
+
return; // Not a PDA, skip
|
|
1437
|
+
visiting.add(name);
|
|
1438
|
+
const deps = pdaDeps.get(name) || new Set();
|
|
1439
|
+
for (const dep of deps) {
|
|
1440
|
+
// Only visit if dep is also a PDA
|
|
1441
|
+
if (pda.some(m => m.name === dep)) {
|
|
1442
|
+
visit(dep);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
visiting.delete(name);
|
|
1446
|
+
visited.add(name);
|
|
1447
|
+
sortedPda.push(meta);
|
|
1448
|
+
}
|
|
1449
|
+
for (const meta of pda) {
|
|
1450
|
+
visit(meta.name);
|
|
1451
|
+
}
|
|
1452
|
+
return [...nonPda, ...sortedPda];
|
|
1453
|
+
}
|
|
1120
1454
|
/**
|
|
1121
1455
|
* Resolves instruction accounts by categorizing and deriving addresses.
|
|
1122
1456
|
*
|
|
1457
|
+
* Resolution order:
|
|
1458
|
+
* 1. Non-PDA accounts (signer, known, userProvided) are resolved first
|
|
1459
|
+
* 2. PDA accounts are resolved in dependency order (accounts they reference come first)
|
|
1460
|
+
*
|
|
1123
1461
|
* @param accountMetas - Account metadata from the instruction definition
|
|
1124
|
-
* @param args - Instruction arguments (used for PDA derivation)
|
|
1125
|
-
* @param options - Resolution options including wallet
|
|
1462
|
+
* @param args - Instruction arguments (used for PDA derivation with argRef seeds)
|
|
1463
|
+
* @param options - Resolution options including wallet, user-provided accounts, and programId
|
|
1126
1464
|
* @returns Resolved accounts and any missing required accounts
|
|
1127
1465
|
*/
|
|
1128
1466
|
function resolveAccounts(accountMetas, args, options) {
|
|
1129
|
-
|
|
1467
|
+
// Sort accounts by dependency
|
|
1468
|
+
const sorted = sortAccountsByDependency(accountMetas);
|
|
1469
|
+
// Track resolved accounts for PDA accountRef lookups
|
|
1470
|
+
const resolvedMap = {};
|
|
1130
1471
|
const missing = [];
|
|
1131
|
-
for (const meta of
|
|
1132
|
-
const resolvedAccount = resolveSingleAccount(meta, args, options);
|
|
1472
|
+
for (const meta of sorted) {
|
|
1473
|
+
const resolvedAccount = resolveSingleAccount(meta, args, options, resolvedMap);
|
|
1133
1474
|
if (resolvedAccount) {
|
|
1134
|
-
|
|
1475
|
+
resolvedMap[meta.name] = resolvedAccount;
|
|
1135
1476
|
}
|
|
1136
1477
|
else if (!meta.isOptional) {
|
|
1137
1478
|
missing.push(meta.name);
|
|
1138
1479
|
}
|
|
1139
1480
|
}
|
|
1481
|
+
// Return accounts in original order (as defined in accountMetas)
|
|
1482
|
+
const orderedAccounts = [];
|
|
1483
|
+
for (const meta of accountMetas) {
|
|
1484
|
+
const resolved = resolvedMap[meta.name];
|
|
1485
|
+
if (resolved) {
|
|
1486
|
+
orderedAccounts.push(resolved);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1140
1489
|
return {
|
|
1141
|
-
accounts:
|
|
1490
|
+
accounts: orderedAccounts,
|
|
1142
1491
|
missingUserAccounts: missing,
|
|
1143
1492
|
};
|
|
1144
1493
|
}
|
|
1145
|
-
function resolveSingleAccount(meta, args, options) {
|
|
1494
|
+
function resolveSingleAccount(meta, args, options, resolvedMap) {
|
|
1146
1495
|
switch (meta.category) {
|
|
1147
1496
|
case 'signer':
|
|
1148
1497
|
return resolveSignerAccount(meta, options.wallet);
|
|
1149
1498
|
case 'known':
|
|
1150
1499
|
return resolveKnownAccount(meta);
|
|
1151
1500
|
case 'pda':
|
|
1152
|
-
return resolvePdaAccount(meta);
|
|
1501
|
+
return resolvePdaAccount(meta, args, resolvedMap, options.programId);
|
|
1153
1502
|
case 'userProvided':
|
|
1154
1503
|
return resolveUserProvidedAccount(meta, options.accounts);
|
|
1155
1504
|
default:
|
|
@@ -1178,15 +1527,51 @@ function resolveKnownAccount(meta) {
|
|
|
1178
1527
|
isWritable: meta.isWritable,
|
|
1179
1528
|
};
|
|
1180
1529
|
}
|
|
1181
|
-
function resolvePdaAccount(meta, args) {
|
|
1530
|
+
function resolvePdaAccount(meta, args, resolvedMap, programId) {
|
|
1182
1531
|
if (!meta.pdaConfig) {
|
|
1183
1532
|
return null;
|
|
1184
1533
|
}
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1534
|
+
// Determine which program to derive against
|
|
1535
|
+
const pdaProgramId = meta.pdaConfig.programId || programId;
|
|
1536
|
+
if (!pdaProgramId) {
|
|
1537
|
+
throw new Error('Cannot derive PDA for "' + meta.name + '": no programId specified. ' +
|
|
1538
|
+
'Either set pdaConfig.programId or pass programId in options.');
|
|
1539
|
+
}
|
|
1540
|
+
// Build seeds array
|
|
1541
|
+
const seeds = [];
|
|
1542
|
+
for (const seed of meta.pdaConfig.seeds) {
|
|
1543
|
+
switch (seed.type) {
|
|
1544
|
+
case 'literal':
|
|
1545
|
+
seeds.push(createSeed(seed.value));
|
|
1546
|
+
break;
|
|
1547
|
+
case 'argRef': {
|
|
1548
|
+
const argValue = args[seed.argName];
|
|
1549
|
+
if (argValue === undefined) {
|
|
1550
|
+
throw new Error('PDA seed references missing argument: ' + seed.argName +
|
|
1551
|
+
' (for account "' + meta.name + '")');
|
|
1552
|
+
}
|
|
1553
|
+
seeds.push(createSeed(argValue));
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
case 'accountRef': {
|
|
1557
|
+
const refAccount = resolvedMap[seed.accountName];
|
|
1558
|
+
if (!refAccount) {
|
|
1559
|
+
throw new Error('PDA seed references unresolved account: ' + seed.accountName +
|
|
1560
|
+
' (for account "' + meta.name + '")');
|
|
1561
|
+
}
|
|
1562
|
+
// Account addresses are 32 bytes
|
|
1563
|
+
seeds.push(decodeBase58(refAccount.address));
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
default:
|
|
1567
|
+
throw new Error('Unknown seed type');
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
// Derive the PDA
|
|
1571
|
+
const [derivedAddress] = findProgramAddressSync(seeds, pdaProgramId);
|
|
1187
1572
|
return {
|
|
1188
1573
|
name: meta.name,
|
|
1189
|
-
address:
|
|
1574
|
+
address: derivedAddress,
|
|
1190
1575
|
isSigner: meta.isSigner,
|
|
1191
1576
|
isWritable: meta.isWritable,
|
|
1192
1577
|
};
|
|
@@ -1211,86 +1596,10 @@ function resolveUserProvidedAccount(meta, accounts) {
|
|
|
1211
1596
|
*/
|
|
1212
1597
|
function validateAccountResolution(result) {
|
|
1213
1598
|
if (result.missingUserAccounts.length > 0) {
|
|
1214
|
-
throw new Error(
|
|
1599
|
+
throw new Error('Missing required accounts: ' + result.missingUserAccounts.join(', '));
|
|
1215
1600
|
}
|
|
1216
1601
|
}
|
|
1217
1602
|
|
|
1218
|
-
/**
|
|
1219
|
-
* Derives a Program-Derived Address (PDA) from seeds and program ID.
|
|
1220
|
-
*
|
|
1221
|
-
* This function implements PDA derivation using the Solana algorithm:
|
|
1222
|
-
* 1. Concatenate all seeds
|
|
1223
|
-
* 2. Hash with SHA-256
|
|
1224
|
-
* 3. Check if result is off-curve (valid PDA)
|
|
1225
|
-
*
|
|
1226
|
-
* Note: This is a placeholder implementation. In production, you would use
|
|
1227
|
-
* the actual Solana web3.js library's PDA derivation.
|
|
1228
|
-
*
|
|
1229
|
-
* @param seeds - Array of seed buffers
|
|
1230
|
-
* @param programId - The program ID (as base58 string)
|
|
1231
|
-
* @returns The derived PDA address (base58 string)
|
|
1232
|
-
*/
|
|
1233
|
-
async function derivePda(seeds, programId) {
|
|
1234
|
-
// In production, this would use:
|
|
1235
|
-
// PublicKey.findProgramAddressSync(seeds, new PublicKey(programId))
|
|
1236
|
-
// For now, return a placeholder that will be replaced with actual implementation
|
|
1237
|
-
const combined = Buffer.concat(seeds);
|
|
1238
|
-
// Simulate PDA derivation (this is NOT the actual algorithm)
|
|
1239
|
-
const hash = await simulateHash(combined);
|
|
1240
|
-
// Return base58-encoded address
|
|
1241
|
-
return bs58Encode(hash);
|
|
1242
|
-
}
|
|
1243
|
-
/**
|
|
1244
|
-
* Creates a seed buffer from various input types.
|
|
1245
|
-
*
|
|
1246
|
-
* @param value - The value to convert to a seed
|
|
1247
|
-
* @returns Buffer suitable for PDA derivation
|
|
1248
|
-
*/
|
|
1249
|
-
function createSeed(value) {
|
|
1250
|
-
if (Buffer.isBuffer(value)) {
|
|
1251
|
-
return value;
|
|
1252
|
-
}
|
|
1253
|
-
if (value instanceof Uint8Array) {
|
|
1254
|
-
return Buffer.from(value);
|
|
1255
|
-
}
|
|
1256
|
-
if (typeof value === 'string') {
|
|
1257
|
-
return Buffer.from(value, 'utf-8');
|
|
1258
|
-
}
|
|
1259
|
-
if (typeof value === 'bigint') {
|
|
1260
|
-
// Convert bigint to 8-byte buffer (u64)
|
|
1261
|
-
const buffer = Buffer.alloc(8);
|
|
1262
|
-
buffer.writeBigUInt64LE(value);
|
|
1263
|
-
return buffer;
|
|
1264
|
-
}
|
|
1265
|
-
throw new Error(`Cannot create seed from type: ${typeof value}`);
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* Creates a public key seed from a base58-encoded address.
|
|
1269
|
-
*
|
|
1270
|
-
* @param address - Base58-encoded public key
|
|
1271
|
-
* @returns 32-byte buffer
|
|
1272
|
-
*/
|
|
1273
|
-
function createPublicKeySeed(address) {
|
|
1274
|
-
// In production, decode base58 to 32-byte buffer
|
|
1275
|
-
// For now, return placeholder
|
|
1276
|
-
return Buffer.alloc(32);
|
|
1277
|
-
}
|
|
1278
|
-
async function simulateHash(data) {
|
|
1279
|
-
// In production, use actual SHA-256
|
|
1280
|
-
// This is a placeholder
|
|
1281
|
-
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
1282
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
1283
|
-
return Buffer.from(hashBuffer);
|
|
1284
|
-
}
|
|
1285
|
-
// Fallback for Node.js
|
|
1286
|
-
return Buffer.alloc(32, 0);
|
|
1287
|
-
}
|
|
1288
|
-
function bs58Encode(buffer) {
|
|
1289
|
-
// In production, use actual base58 encoding
|
|
1290
|
-
// This is a placeholder
|
|
1291
|
-
return 'P' + buffer.toString('hex').slice(0, 31);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
1603
|
/**
|
|
1295
1604
|
* Borsh-compatible instruction data serializer.
|
|
1296
1605
|
*
|
|
@@ -1381,6 +1690,7 @@ function serializePrimitive(value, type) {
|
|
|
1381
1690
|
strLen.writeUInt32LE(strBytes.length, 0);
|
|
1382
1691
|
return Buffer.concat([strLen, strBytes]);
|
|
1383
1692
|
case 'pubkey':
|
|
1693
|
+
// Public key is 32 bytes
|
|
1384
1694
|
// In production, decode base58 to 32 bytes
|
|
1385
1695
|
return Buffer.alloc(32, 0);
|
|
1386
1696
|
default:
|
|
@@ -1433,7 +1743,7 @@ async function waitForConfirmation(signature, level = 'confirmed', timeout = 600
|
|
|
1433
1743
|
}
|
|
1434
1744
|
throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
|
|
1435
1745
|
}
|
|
1436
|
-
async function checkTransactionStatus(
|
|
1746
|
+
async function checkTransactionStatus(_signature) {
|
|
1437
1747
|
// In production, query the Solana RPC
|
|
1438
1748
|
return {
|
|
1439
1749
|
err: null,
|
|
@@ -1549,6 +1859,7 @@ async function executeInstruction(handler, args, options = {}) {
|
|
|
1549
1859
|
const resolutionOptions = {
|
|
1550
1860
|
accounts: options.accounts,
|
|
1551
1861
|
wallet: options.wallet,
|
|
1862
|
+
programId: handler.programId, // Pass programId for PDA derivation
|
|
1552
1863
|
};
|
|
1553
1864
|
const resolution = resolveAccounts(handler.accounts, args, resolutionOptions);
|
|
1554
1865
|
validateAccountResolution(resolution);
|
|
@@ -1607,6 +1918,108 @@ function createInstructionExecutor(wallet) {
|
|
|
1607
1918
|
};
|
|
1608
1919
|
}
|
|
1609
1920
|
|
|
1921
|
+
function literal(value) {
|
|
1922
|
+
return { type: 'literal', value };
|
|
1923
|
+
}
|
|
1924
|
+
function account(name) {
|
|
1925
|
+
return { type: 'accountRef', accountName: name };
|
|
1926
|
+
}
|
|
1927
|
+
function arg(name, type) {
|
|
1928
|
+
return { type: 'argRef', argName: name, argType: type };
|
|
1929
|
+
}
|
|
1930
|
+
function bytes(value) {
|
|
1931
|
+
return { type: 'bytes', value };
|
|
1932
|
+
}
|
|
1933
|
+
function resolveSeeds(seeds, context) {
|
|
1934
|
+
return seeds.map((seed) => {
|
|
1935
|
+
switch (seed.type) {
|
|
1936
|
+
case 'literal':
|
|
1937
|
+
return new TextEncoder().encode(seed.value);
|
|
1938
|
+
case 'bytes':
|
|
1939
|
+
return seed.value;
|
|
1940
|
+
case 'argRef': {
|
|
1941
|
+
const value = context.args?.[seed.argName];
|
|
1942
|
+
if (value === undefined) {
|
|
1943
|
+
throw new Error(`Missing arg for PDA seed: ${seed.argName}`);
|
|
1944
|
+
}
|
|
1945
|
+
return serializeArgForSeed(value, seed.argType);
|
|
1946
|
+
}
|
|
1947
|
+
case 'accountRef': {
|
|
1948
|
+
const address = context.accounts?.[seed.accountName];
|
|
1949
|
+
if (!address) {
|
|
1950
|
+
throw new Error(`Missing account for PDA seed: ${seed.accountName}`);
|
|
1951
|
+
}
|
|
1952
|
+
return decodeBase58(address);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
function serializeArgForSeed(value, argType) {
|
|
1958
|
+
if (value instanceof Uint8Array) {
|
|
1959
|
+
return value;
|
|
1960
|
+
}
|
|
1961
|
+
if (typeof value === 'string') {
|
|
1962
|
+
if (value.length === 43 || value.length === 44) {
|
|
1963
|
+
try {
|
|
1964
|
+
return decodeBase58(value);
|
|
1965
|
+
}
|
|
1966
|
+
catch {
|
|
1967
|
+
return new TextEncoder().encode(value);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return new TextEncoder().encode(value);
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof value === 'bigint' || typeof value === 'number') {
|
|
1973
|
+
const size = getArgSize(argType);
|
|
1974
|
+
return serializeNumber(value, size);
|
|
1975
|
+
}
|
|
1976
|
+
throw new Error(`Cannot serialize value for PDA seed: ${typeof value}`);
|
|
1977
|
+
}
|
|
1978
|
+
function getArgSize(argType) {
|
|
1979
|
+
if (!argType)
|
|
1980
|
+
return 8;
|
|
1981
|
+
const match = argType.match(/^[ui](\d+)$/);
|
|
1982
|
+
if (match && match[1]) {
|
|
1983
|
+
return parseInt(match[1], 10) / 8;
|
|
1984
|
+
}
|
|
1985
|
+
if (argType === 'pubkey')
|
|
1986
|
+
return 32;
|
|
1987
|
+
return 8;
|
|
1988
|
+
}
|
|
1989
|
+
function serializeNumber(value, size) {
|
|
1990
|
+
const buffer = new Uint8Array(size);
|
|
1991
|
+
let n = typeof value === 'bigint' ? value : BigInt(value);
|
|
1992
|
+
for (let i = 0; i < size; i++) {
|
|
1993
|
+
buffer[i] = Number(n & BigInt(0xff));
|
|
1994
|
+
n >>= BigInt(8);
|
|
1995
|
+
}
|
|
1996
|
+
return buffer;
|
|
1997
|
+
}
|
|
1998
|
+
function pda(programId, ...seeds) {
|
|
1999
|
+
return {
|
|
2000
|
+
seeds,
|
|
2001
|
+
programId,
|
|
2002
|
+
program(newProgramId) {
|
|
2003
|
+
return pda(newProgramId, ...seeds);
|
|
2004
|
+
},
|
|
2005
|
+
async derive(context) {
|
|
2006
|
+
const resolvedSeeds = resolveSeeds(this.seeds, context);
|
|
2007
|
+
const pid = context.programId ?? this.programId;
|
|
2008
|
+
const [address] = await findProgramAddress(resolvedSeeds, pid);
|
|
2009
|
+
return address;
|
|
2010
|
+
},
|
|
2011
|
+
deriveSync(context) {
|
|
2012
|
+
const resolvedSeeds = resolveSeeds(this.seeds, context);
|
|
2013
|
+
const pid = context.programId ?? this.programId;
|
|
2014
|
+
const [address] = findProgramAddressSync(resolvedSeeds, pid);
|
|
2015
|
+
return address;
|
|
2016
|
+
},
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function createProgramPdas(pdas) {
|
|
2020
|
+
return pdas;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
1610
2023
|
class HyperStack {
|
|
1611
2024
|
constructor(url, options) {
|
|
1612
2025
|
this.stack = options.stack;
|
|
@@ -1614,6 +2027,7 @@ class HyperStack {
|
|
|
1614
2027
|
this.processor = new FrameProcessor(this.storage, {
|
|
1615
2028
|
maxEntriesPerView: options.maxEntriesPerView,
|
|
1616
2029
|
flushIntervalMs: options.flushIntervalMs,
|
|
2030
|
+
schemas: options.validateFrames ? this.stack.schemas : undefined,
|
|
1617
2031
|
});
|
|
1618
2032
|
this.connection = new ConnectionManager({
|
|
1619
2033
|
websocketUrl: url,
|
|
@@ -1651,6 +2065,7 @@ class HyperStack {
|
|
|
1651
2065
|
autoReconnect: options?.autoReconnect,
|
|
1652
2066
|
reconnectIntervals: options?.reconnectIntervals,
|
|
1653
2067
|
maxReconnectAttempts: options?.maxReconnectAttempts,
|
|
2068
|
+
validateFrames: options?.validateFrames,
|
|
1654
2069
|
};
|
|
1655
2070
|
const client = new HyperStack(url, internalOptions);
|
|
1656
2071
|
if (options?.autoReconnect !== false) {
|
|
@@ -1775,6 +2190,8 @@ class ViewData {
|
|
|
1775
2190
|
while (low < high) {
|
|
1776
2191
|
const mid = Math.floor((low + high) / 2);
|
|
1777
2192
|
const midKey = this.sortedKeys[mid];
|
|
2193
|
+
if (midKey === undefined)
|
|
2194
|
+
break;
|
|
1778
2195
|
const midEntity = this.entities.get(midKey);
|
|
1779
2196
|
const midValue = getNestedValue(midEntity, this.sortConfig.field);
|
|
1780
2197
|
let cmp = compareSortValues(sortValue, midValue);
|
|
@@ -2132,8 +2549,12 @@ exports.HyperStack = HyperStack;
|
|
|
2132
2549
|
exports.HyperStackError = HyperStackError;
|
|
2133
2550
|
exports.MemoryAdapter = MemoryAdapter;
|
|
2134
2551
|
exports.SubscriptionRegistry = SubscriptionRegistry;
|
|
2552
|
+
exports.account = account;
|
|
2553
|
+
exports.arg = arg;
|
|
2554
|
+
exports.bytes = bytes;
|
|
2135
2555
|
exports.createEntityStream = createEntityStream;
|
|
2136
2556
|
exports.createInstructionExecutor = createInstructionExecutor;
|
|
2557
|
+
exports.createProgramPdas = createProgramPdas;
|
|
2137
2558
|
exports.createPublicKeySeed = createPublicKeySeed;
|
|
2138
2559
|
exports.createRichUpdateStream = createRichUpdateStream;
|
|
2139
2560
|
exports.createSeed = createSeed;
|
|
@@ -2141,16 +2562,22 @@ exports.createTypedListView = createTypedListView;
|
|
|
2141
2562
|
exports.createTypedStateView = createTypedStateView;
|
|
2142
2563
|
exports.createTypedViews = createTypedViews;
|
|
2143
2564
|
exports.createUpdateStream = createUpdateStream;
|
|
2144
|
-
exports.
|
|
2565
|
+
exports.decodeBase58 = decodeBase58;
|
|
2566
|
+
exports.derivePda = findProgramAddress;
|
|
2567
|
+
exports.encodeBase58 = encodeBase58;
|
|
2145
2568
|
exports.executeInstruction = executeInstruction;
|
|
2569
|
+
exports.findProgramAddress = findProgramAddress;
|
|
2570
|
+
exports.findProgramAddressSync = findProgramAddressSync;
|
|
2146
2571
|
exports.formatProgramError = formatProgramError;
|
|
2147
2572
|
exports.isEntityFrame = isEntityFrame;
|
|
2148
2573
|
exports.isSnapshotFrame = isSnapshotFrame;
|
|
2149
2574
|
exports.isSubscribedFrame = isSubscribedFrame;
|
|
2150
2575
|
exports.isValidFrame = isValidFrame;
|
|
2576
|
+
exports.literal = literal;
|
|
2151
2577
|
exports.parseFrame = parseFrame;
|
|
2152
2578
|
exports.parseFrameFromBlob = parseFrameFromBlob;
|
|
2153
2579
|
exports.parseInstructionError = parseInstructionError;
|
|
2580
|
+
exports.pda = pda;
|
|
2154
2581
|
exports.resolveAccounts = resolveAccounts;
|
|
2155
2582
|
exports.serializeInstructionData = serializeInstructionData;
|
|
2156
2583
|
exports.validateAccountResolution = validateAccountResolution;
|