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