hyperstack-react 0.4.2 → 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.js CHANGED
@@ -7075,6 +7075,31 @@ class FrameProcessor {
7075
7075
  ? DEFAULT_MAX_ENTRIES_PER_VIEW
7076
7076
  : config.maxEntriesPerView;
7077
7077
  this.flushIntervalMs = config.flushIntervalMs ?? 0;
7078
+ this.schemas = config.schemas;
7079
+ }
7080
+ getSchema(viewPath) {
7081
+ const schemas = this.schemas;
7082
+ if (!schemas)
7083
+ return null;
7084
+ const entityName = viewPath.split('/')[0];
7085
+ if (typeof entityName !== 'string' || entityName.length === 0)
7086
+ return null;
7087
+ const entityKey = entityName;
7088
+ return schemas[entityKey] ?? null;
7089
+ }
7090
+ validateEntity(viewPath, data) {
7091
+ const schema = this.getSchema(viewPath);
7092
+ if (!schema)
7093
+ return true;
7094
+ const result = schema.safeParse(data);
7095
+ if (!result.success) {
7096
+ console.warn('[Hyperstack] Frame validation failed:', {
7097
+ view: viewPath,
7098
+ error: result.error,
7099
+ });
7100
+ return false;
7101
+ }
7102
+ return true;
7078
7103
  }
7079
7104
  handleFrame(frame) {
7080
7105
  if (this.flushIntervalMs === 0) {
@@ -7170,6 +7195,9 @@ class FrameProcessor {
7170
7195
  handleSnapshotFrameWithoutEnforce(frame) {
7171
7196
  const viewPath = frame.entity;
7172
7197
  for (const entity of frame.data) {
7198
+ if (!this.validateEntity(viewPath, entity.data)) {
7199
+ continue;
7200
+ }
7173
7201
  const previousValue = this.storage.get(viewPath, entity.key);
7174
7202
  this.storage.set(viewPath, entity.key, entity.data);
7175
7203
  this.storage.notifyUpdate(viewPath, entity.key, {
@@ -7190,6 +7218,9 @@ class FrameProcessor {
7190
7218
  switch (frame.op) {
7191
7219
  case 'create':
7192
7220
  case 'upsert':
7221
+ if (!this.validateEntity(viewPath, frame.data)) {
7222
+ break;
7223
+ }
7193
7224
  this.storage.set(viewPath, frame.key, frame.data);
7194
7225
  this.storage.notifyUpdate(viewPath, frame.key, {
7195
7226
  type: 'upsert',
@@ -7204,6 +7235,9 @@ class FrameProcessor {
7204
7235
  const merged = existing
7205
7236
  ? deepMergeWithAppend$1(existing, frame.data, appendPaths)
7206
7237
  : frame.data;
7238
+ if (!this.validateEntity(viewPath, merged)) {
7239
+ break;
7240
+ }
7207
7241
  this.storage.set(viewPath, frame.key, merged);
7208
7242
  this.storage.notifyUpdate(viewPath, frame.key, {
7209
7243
  type: 'patch',
@@ -7692,7 +7726,8 @@ function createUpdateStream(storage, subscriptionRegistry, subscription, keyFilt
7692
7726
  },
7693
7727
  };
7694
7728
  }
7695
- function createEntityStream(storage, subscriptionRegistry, subscription, keyFilter) {
7729
+ function createEntityStream(storage, subscriptionRegistry, subscription, options, keyFilter) {
7730
+ const schema = options?.schema;
7696
7731
  return {
7697
7732
  [Symbol.asyncIterator]() {
7698
7733
  const queue = [];
@@ -7708,16 +7743,27 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
7708
7743
  if (update.type === 'deleted')
7709
7744
  return;
7710
7745
  const entity = (update.type === 'created' ? update.data : update.after);
7746
+ let output;
7747
+ if (schema) {
7748
+ const parsed = schema.safeParse(entity);
7749
+ if (!parsed.success) {
7750
+ return;
7751
+ }
7752
+ output = parsed.data;
7753
+ }
7754
+ else {
7755
+ output = entity;
7756
+ }
7711
7757
  if (waitingResolve) {
7712
7758
  const resolve = waitingResolve;
7713
7759
  waitingResolve = null;
7714
- resolve({ value: entity, done: false });
7760
+ resolve({ value: output, done: false });
7715
7761
  }
7716
7762
  else {
7717
7763
  if (queue.length >= MAX_QUEUE_SIZE) {
7718
7764
  queue.shift();
7719
7765
  }
7720
- queue.push(entity);
7766
+ queue.push(output);
7721
7767
  }
7722
7768
  };
7723
7769
  const start = () => {
@@ -7730,7 +7776,7 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
7730
7776
  unsubscribeRegistry?.();
7731
7777
  };
7732
7778
  start();
7733
- return {
7779
+ const iterator = {
7734
7780
  async next() {
7735
7781
  if (done) {
7736
7782
  return { value: undefined, done: true };
@@ -7752,6 +7798,7 @@ function createEntityStream(storage, subscriptionRegistry, subscription, keyFilt
7752
7798
  throw error;
7753
7799
  },
7754
7800
  };
7801
+ return iterator;
7755
7802
  },
7756
7803
  };
7757
7804
  }
@@ -7823,13 +7870,16 @@ function createRichUpdateStream(storage, subscriptionRegistry, subscription, key
7823
7870
  function createTypedStateView(viewDef, storage, subscriptionRegistry) {
7824
7871
  return {
7825
7872
  use(key, options) {
7826
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...options }, key);
7873
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7874
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, options, key);
7827
7875
  },
7828
7876
  watch(key, options) {
7829
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...options }, key);
7877
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7878
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
7830
7879
  },
7831
7880
  watchRich(key, options) {
7832
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...options }, key);
7881
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7882
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, key, ...subscriptionOptions }, key);
7833
7883
  },
7834
7884
  async get(key) {
7835
7885
  return storage.get(viewDef.view, key);
@@ -7842,13 +7892,16 @@ function createTypedStateView(viewDef, storage, subscriptionRegistry) {
7842
7892
  function createTypedListView(viewDef, storage, subscriptionRegistry) {
7843
7893
  return {
7844
7894
  use(options) {
7845
- return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...options });
7895
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7896
+ return createEntityStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions }, options);
7846
7897
  },
7847
7898
  watch(options) {
7848
- return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...options });
7899
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7900
+ return createUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
7849
7901
  },
7850
7902
  watchRich(options) {
7851
- return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...options });
7903
+ const { schema: _schema, ...subscriptionOptions } = options ?? {};
7904
+ return createRichUpdateStream(storage, subscriptionRegistry, { view: viewDef.view, ...subscriptionOptions });
7852
7905
  },
7853
7906
  async get() {
7854
7907
  return storage.getAll(viewDef.view);
@@ -7876,39 +7929,335 @@ function createTypedViews(stack, storage, subscriptionRegistry) {
7876
7929
  return views;
7877
7930
  }
7878
7931
 
7932
+ /**
7933
+ * PDA (Program Derived Address) derivation utilities.
7934
+ *
7935
+ * Implements Solana's PDA derivation algorithm without depending on @solana/web3.js.
7936
+ */
7937
+ // Base58 alphabet (Bitcoin/Solana style)
7938
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
7939
+ /**
7940
+ * Decode base58 string to Uint8Array.
7941
+ */
7942
+ function decodeBase58(str) {
7943
+ if (str.length === 0) {
7944
+ return new Uint8Array(0);
7945
+ }
7946
+ const bytes = [0];
7947
+ for (const char of str) {
7948
+ const value = BASE58_ALPHABET.indexOf(char);
7949
+ if (value === -1) {
7950
+ throw new Error('Invalid base58 character: ' + char);
7951
+ }
7952
+ let carry = value;
7953
+ for (let i = 0; i < bytes.length; i++) {
7954
+ carry += (bytes[i] ?? 0) * 58;
7955
+ bytes[i] = carry & 0xff;
7956
+ carry >>= 8;
7957
+ }
7958
+ while (carry > 0) {
7959
+ bytes.push(carry & 0xff);
7960
+ carry >>= 8;
7961
+ }
7962
+ }
7963
+ // Add leading zeros for each leading '1' in input
7964
+ for (const char of str) {
7965
+ if (char !== '1')
7966
+ break;
7967
+ bytes.push(0);
7968
+ }
7969
+ return new Uint8Array(bytes.reverse());
7970
+ }
7971
+ /**
7972
+ * Encode Uint8Array to base58 string.
7973
+ */
7974
+ function encodeBase58(bytes) {
7975
+ if (bytes.length === 0) {
7976
+ return '';
7977
+ }
7978
+ const digits = [0];
7979
+ for (const byte of bytes) {
7980
+ let carry = byte;
7981
+ for (let i = 0; i < digits.length; i++) {
7982
+ carry += (digits[i] ?? 0) << 8;
7983
+ digits[i] = carry % 58;
7984
+ carry = (carry / 58) | 0;
7985
+ }
7986
+ while (carry > 0) {
7987
+ digits.push(carry % 58);
7988
+ carry = (carry / 58) | 0;
7989
+ }
7990
+ }
7991
+ // Add leading zeros for each leading 0 byte in input
7992
+ for (const byte of bytes) {
7993
+ if (byte !== 0)
7994
+ break;
7995
+ digits.push(0);
7996
+ }
7997
+ return digits.reverse().map(d => BASE58_ALPHABET[d]).join('');
7998
+ }
7999
+ /**
8000
+ * SHA-256 hash function (synchronous, Node.js).
8001
+ */
8002
+ function sha256Sync(data) {
8003
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
8004
+ const { createHash } = require('crypto');
8005
+ return new Uint8Array(createHash('sha256').update(Buffer.from(data)).digest());
8006
+ }
8007
+ /**
8008
+ * SHA-256 hash function (async, works in browser and Node.js).
8009
+ */
8010
+ async function sha256Async(data) {
8011
+ if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.subtle) {
8012
+ // Create a copy of the data to ensure we have an ArrayBuffer
8013
+ const copy = new Uint8Array(data);
8014
+ const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', copy);
8015
+ return new Uint8Array(hashBuffer);
8016
+ }
8017
+ return sha256Sync(data);
8018
+ }
8019
+ /**
8020
+ * PDA marker bytes appended to seeds before hashing.
8021
+ */
8022
+ const PDA_MARKER = new TextEncoder().encode('ProgramDerivedAddress');
8023
+ /**
8024
+ * Build the hash input buffer for PDA derivation.
8025
+ */
8026
+ function buildPdaBuffer(seeds, programIdBytes, bump) {
8027
+ const totalLength = seeds.reduce((sum, s) => sum + s.length, 0)
8028
+ + 1 // bump
8029
+ + 32 // programId
8030
+ + PDA_MARKER.length;
8031
+ const buffer = new Uint8Array(totalLength);
8032
+ let offset = 0;
8033
+ // Copy seeds
8034
+ for (const seed of seeds) {
8035
+ buffer.set(seed, offset);
8036
+ offset += seed.length;
8037
+ }
8038
+ // Add bump seed
8039
+ buffer[offset++] = bump;
8040
+ // Add program ID
8041
+ buffer.set(programIdBytes, offset);
8042
+ offset += 32;
8043
+ // Add PDA marker
8044
+ buffer.set(PDA_MARKER, offset);
8045
+ return buffer;
8046
+ }
8047
+ /**
8048
+ * Validate seeds before PDA derivation.
8049
+ */
8050
+ function validateSeeds(seeds) {
8051
+ if (seeds.length > 16) {
8052
+ throw new Error('Maximum of 16 seeds allowed');
8053
+ }
8054
+ for (let i = 0; i < seeds.length; i++) {
8055
+ const seed = seeds[i];
8056
+ if (seed && seed.length > 32) {
8057
+ throw new Error('Seed ' + i + ' exceeds maximum length of 32 bytes');
8058
+ }
8059
+ }
8060
+ }
8061
+ /**
8062
+ * Derives a Program-Derived Address (PDA) from seeds and program ID.
8063
+ *
8064
+ * Algorithm:
8065
+ * 1. For bump = 255 down to 0:
8066
+ * a. Concatenate: seeds + [bump] + programId + "ProgramDerivedAddress"
8067
+ * b. SHA-256 hash the concatenation
8068
+ * c. If result is off the ed25519 curve, return it
8069
+ * 2. If no valid PDA found after 256 attempts, throw error
8070
+ *
8071
+ * @param seeds - Array of seed buffers (max 32 bytes each, max 16 seeds)
8072
+ * @param programId - The program ID (base58 string)
8073
+ * @returns Tuple of [derivedAddress (base58), bumpSeed]
8074
+ */
8075
+ async function findProgramAddress(seeds, programId) {
8076
+ validateSeeds(seeds);
8077
+ const programIdBytes = decodeBase58(programId);
8078
+ if (programIdBytes.length !== 32) {
8079
+ throw new Error('Program ID must be 32 bytes');
8080
+ }
8081
+ // Try bump seeds from 255 down to 0
8082
+ for (let bump = 255; bump >= 0; bump--) {
8083
+ const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
8084
+ const hash = await sha256Async(buffer);
8085
+ {
8086
+ return [encodeBase58(hash), bump];
8087
+ }
8088
+ }
8089
+ throw new Error('Unable to find a valid PDA');
8090
+ }
8091
+ /**
8092
+ * Synchronous version of findProgramAddress.
8093
+ * Uses synchronous SHA-256 (Node.js crypto module).
8094
+ */
8095
+ function findProgramAddressSync(seeds, programId) {
8096
+ validateSeeds(seeds);
8097
+ const programIdBytes = decodeBase58(programId);
8098
+ if (programIdBytes.length !== 32) {
8099
+ throw new Error('Program ID must be 32 bytes');
8100
+ }
8101
+ // Try bump seeds from 255 down to 0
8102
+ for (let bump = 255; bump >= 0; bump--) {
8103
+ const buffer = buildPdaBuffer(seeds, programIdBytes, bump);
8104
+ const hash = sha256Sync(buffer);
8105
+ {
8106
+ return [encodeBase58(hash), bump];
8107
+ }
8108
+ }
8109
+ throw new Error('Unable to find a valid PDA');
8110
+ }
8111
+ /**
8112
+ * Creates a seed buffer from various input types.
8113
+ *
8114
+ * @param value - The value to convert to a seed
8115
+ * @returns Uint8Array suitable for PDA derivation
8116
+ */
8117
+ function createSeed(value) {
8118
+ if (value instanceof Uint8Array) {
8119
+ return value;
8120
+ }
8121
+ if (typeof value === 'string') {
8122
+ return new TextEncoder().encode(value);
8123
+ }
8124
+ if (typeof value === 'bigint') {
8125
+ // Convert bigint to 8-byte buffer (u64 little-endian)
8126
+ const buffer = new Uint8Array(8);
8127
+ let n = value;
8128
+ for (let i = 0; i < 8; i++) {
8129
+ buffer[i] = Number(n & BigInt(0xff));
8130
+ n >>= BigInt(8);
8131
+ }
8132
+ return buffer;
8133
+ }
8134
+ if (typeof value === 'number') {
8135
+ // Assume u64
8136
+ return createSeed(BigInt(value));
8137
+ }
8138
+ throw new Error('Cannot create seed from value');
8139
+ }
8140
+ /**
8141
+ * Creates a public key seed from a base58-encoded address.
8142
+ *
8143
+ * @param address - Base58-encoded public key
8144
+ * @returns 32-byte Uint8Array
8145
+ */
8146
+ function createPublicKeySeed(address) {
8147
+ const decoded = decodeBase58(address);
8148
+ if (decoded.length !== 32) {
8149
+ throw new Error('Invalid public key length: expected 32, got ' + decoded.length);
8150
+ }
8151
+ return decoded;
8152
+ }
8153
+
8154
+ /**
8155
+ * Topologically sort accounts so that dependencies (accountRef) are resolved first.
8156
+ * Non-PDA accounts come first, then PDAs in dependency order.
8157
+ */
8158
+ function sortAccountsByDependency(accountMetas) {
8159
+ // Separate non-PDA and PDA accounts
8160
+ const nonPda = [];
8161
+ const pda = [];
8162
+ for (const meta of accountMetas) {
8163
+ if (meta.category === 'pda') {
8164
+ pda.push(meta);
8165
+ }
8166
+ else {
8167
+ nonPda.push(meta);
8168
+ }
8169
+ }
8170
+ // Build dependency graph for PDAs
8171
+ const pdaDeps = new Map();
8172
+ for (const meta of pda) {
8173
+ const deps = new Set();
8174
+ if (meta.pdaConfig) {
8175
+ for (const seed of meta.pdaConfig.seeds) {
8176
+ if (seed.type === 'accountRef') {
8177
+ deps.add(seed.accountName);
8178
+ }
8179
+ }
8180
+ }
8181
+ pdaDeps.set(meta.name, deps);
8182
+ }
8183
+ // Topological sort PDAs
8184
+ const sortedPda = [];
8185
+ const visited = new Set();
8186
+ const visiting = new Set();
8187
+ function visit(name) {
8188
+ if (visited.has(name))
8189
+ return;
8190
+ if (visiting.has(name)) {
8191
+ throw new Error('Circular dependency in PDA accounts: ' + name);
8192
+ }
8193
+ const meta = pda.find(m => m.name === name);
8194
+ if (!meta)
8195
+ return; // Not a PDA, skip
8196
+ visiting.add(name);
8197
+ const deps = pdaDeps.get(name) || new Set();
8198
+ for (const dep of deps) {
8199
+ // Only visit if dep is also a PDA
8200
+ if (pda.some(m => m.name === dep)) {
8201
+ visit(dep);
8202
+ }
8203
+ }
8204
+ visiting.delete(name);
8205
+ visited.add(name);
8206
+ sortedPda.push(meta);
8207
+ }
8208
+ for (const meta of pda) {
8209
+ visit(meta.name);
8210
+ }
8211
+ return [...nonPda, ...sortedPda];
8212
+ }
7879
8213
  /**
7880
8214
  * Resolves instruction accounts by categorizing and deriving addresses.
7881
8215
  *
8216
+ * Resolution order:
8217
+ * 1. Non-PDA accounts (signer, known, userProvided) are resolved first
8218
+ * 2. PDA accounts are resolved in dependency order (accounts they reference come first)
8219
+ *
7882
8220
  * @param accountMetas - Account metadata from the instruction definition
7883
- * @param args - Instruction arguments (used for PDA derivation)
7884
- * @param options - Resolution options including wallet and user-provided accounts
8221
+ * @param args - Instruction arguments (used for PDA derivation with argRef seeds)
8222
+ * @param options - Resolution options including wallet, user-provided accounts, and programId
7885
8223
  * @returns Resolved accounts and any missing required accounts
7886
8224
  */
7887
8225
  function resolveAccounts(accountMetas, args, options) {
7888
- const resolved = [];
8226
+ // Sort accounts by dependency
8227
+ const sorted = sortAccountsByDependency(accountMetas);
8228
+ // Track resolved accounts for PDA accountRef lookups
8229
+ const resolvedMap = {};
7889
8230
  const missing = [];
7890
- for (const meta of accountMetas) {
7891
- const resolvedAccount = resolveSingleAccount(meta, args, options);
8231
+ for (const meta of sorted) {
8232
+ const resolvedAccount = resolveSingleAccount(meta, args, options, resolvedMap);
7892
8233
  if (resolvedAccount) {
7893
- resolved.push(resolvedAccount);
8234
+ resolvedMap[meta.name] = resolvedAccount;
7894
8235
  }
7895
8236
  else if (!meta.isOptional) {
7896
8237
  missing.push(meta.name);
7897
8238
  }
7898
8239
  }
8240
+ // Return accounts in original order (as defined in accountMetas)
8241
+ const orderedAccounts = [];
8242
+ for (const meta of accountMetas) {
8243
+ const resolved = resolvedMap[meta.name];
8244
+ if (resolved) {
8245
+ orderedAccounts.push(resolved);
8246
+ }
8247
+ }
7899
8248
  return {
7900
- accounts: resolved,
8249
+ accounts: orderedAccounts,
7901
8250
  missingUserAccounts: missing,
7902
8251
  };
7903
8252
  }
7904
- function resolveSingleAccount(meta, args, options) {
8253
+ function resolveSingleAccount(meta, args, options, resolvedMap) {
7905
8254
  switch (meta.category) {
7906
8255
  case 'signer':
7907
8256
  return resolveSignerAccount(meta, options.wallet);
7908
8257
  case 'known':
7909
8258
  return resolveKnownAccount(meta);
7910
8259
  case 'pda':
7911
- return resolvePdaAccount(meta);
8260
+ return resolvePdaAccount(meta, args, resolvedMap, options.programId);
7912
8261
  case 'userProvided':
7913
8262
  return resolveUserProvidedAccount(meta, options.accounts);
7914
8263
  default:
@@ -7937,15 +8286,51 @@ function resolveKnownAccount(meta) {
7937
8286
  isWritable: meta.isWritable,
7938
8287
  };
7939
8288
  }
7940
- function resolvePdaAccount(meta, args) {
8289
+ function resolvePdaAccount(meta, args, resolvedMap, programId) {
7941
8290
  if (!meta.pdaConfig) {
7942
8291
  return null;
7943
8292
  }
7944
- // PDA derivation will be implemented in pda.ts
7945
- // For now, return a placeholder that will be resolved later
8293
+ // Determine which program to derive against
8294
+ const pdaProgramId = meta.pdaConfig.programId || programId;
8295
+ if (!pdaProgramId) {
8296
+ throw new Error('Cannot derive PDA for "' + meta.name + '": no programId specified. ' +
8297
+ 'Either set pdaConfig.programId or pass programId in options.');
8298
+ }
8299
+ // Build seeds array
8300
+ const seeds = [];
8301
+ for (const seed of meta.pdaConfig.seeds) {
8302
+ switch (seed.type) {
8303
+ case 'literal':
8304
+ seeds.push(createSeed(seed.value));
8305
+ break;
8306
+ case 'argRef': {
8307
+ const argValue = args[seed.argName];
8308
+ if (argValue === undefined) {
8309
+ throw new Error('PDA seed references missing argument: ' + seed.argName +
8310
+ ' (for account "' + meta.name + '")');
8311
+ }
8312
+ seeds.push(createSeed(argValue));
8313
+ break;
8314
+ }
8315
+ case 'accountRef': {
8316
+ const refAccount = resolvedMap[seed.accountName];
8317
+ if (!refAccount) {
8318
+ throw new Error('PDA seed references unresolved account: ' + seed.accountName +
8319
+ ' (for account "' + meta.name + '")');
8320
+ }
8321
+ // Account addresses are 32 bytes
8322
+ seeds.push(decodeBase58(refAccount.address));
8323
+ break;
8324
+ }
8325
+ default:
8326
+ throw new Error('Unknown seed type');
8327
+ }
8328
+ }
8329
+ // Derive the PDA
8330
+ const [derivedAddress] = findProgramAddressSync(seeds, pdaProgramId);
7946
8331
  return {
7947
8332
  name: meta.name,
7948
- address: '', // Will be derived
8333
+ address: derivedAddress,
7949
8334
  isSigner: meta.isSigner,
7950
8335
  isWritable: meta.isWritable,
7951
8336
  };
@@ -7970,86 +8355,10 @@ function resolveUserProvidedAccount(meta, accounts) {
7970
8355
  */
7971
8356
  function validateAccountResolution(result) {
7972
8357
  if (result.missingUserAccounts.length > 0) {
7973
- throw new Error(`Missing required accounts: ${result.missingUserAccounts.join(', ')}`);
8358
+ throw new Error('Missing required accounts: ' + result.missingUserAccounts.join(', '));
7974
8359
  }
7975
8360
  }
7976
8361
 
7977
- /**
7978
- * Derives a Program-Derived Address (PDA) from seeds and program ID.
7979
- *
7980
- * This function implements PDA derivation using the Solana algorithm:
7981
- * 1. Concatenate all seeds
7982
- * 2. Hash with SHA-256
7983
- * 3. Check if result is off-curve (valid PDA)
7984
- *
7985
- * Note: This is a placeholder implementation. In production, you would use
7986
- * the actual Solana web3.js library's PDA derivation.
7987
- *
7988
- * @param seeds - Array of seed buffers
7989
- * @param programId - The program ID (as base58 string)
7990
- * @returns The derived PDA address (base58 string)
7991
- */
7992
- async function derivePda(seeds, programId) {
7993
- // In production, this would use:
7994
- // PublicKey.findProgramAddressSync(seeds, new PublicKey(programId))
7995
- // For now, return a placeholder that will be replaced with actual implementation
7996
- const combined = Buffer.concat(seeds);
7997
- // Simulate PDA derivation (this is NOT the actual algorithm)
7998
- const hash = await simulateHash(combined);
7999
- // Return base58-encoded address
8000
- return bs58Encode(hash);
8001
- }
8002
- /**
8003
- * Creates a seed buffer from various input types.
8004
- *
8005
- * @param value - The value to convert to a seed
8006
- * @returns Buffer suitable for PDA derivation
8007
- */
8008
- function createSeed(value) {
8009
- if (Buffer.isBuffer(value)) {
8010
- return value;
8011
- }
8012
- if (value instanceof Uint8Array) {
8013
- return Buffer.from(value);
8014
- }
8015
- if (typeof value === 'string') {
8016
- return Buffer.from(value, 'utf-8');
8017
- }
8018
- if (typeof value === 'bigint') {
8019
- // Convert bigint to 8-byte buffer (u64)
8020
- const buffer = Buffer.alloc(8);
8021
- buffer.writeBigUInt64LE(value);
8022
- return buffer;
8023
- }
8024
- throw new Error(`Cannot create seed from type: ${typeof value}`);
8025
- }
8026
- /**
8027
- * Creates a public key seed from a base58-encoded address.
8028
- *
8029
- * @param address - Base58-encoded public key
8030
- * @returns 32-byte buffer
8031
- */
8032
- function createPublicKeySeed(address) {
8033
- // In production, decode base58 to 32-byte buffer
8034
- // For now, return placeholder
8035
- return Buffer.alloc(32);
8036
- }
8037
- async function simulateHash(data) {
8038
- // In production, use actual SHA-256
8039
- // This is a placeholder
8040
- if (typeof crypto !== 'undefined' && crypto.subtle) {
8041
- const hashBuffer = await crypto.subtle.digest('SHA-256', data);
8042
- return Buffer.from(hashBuffer);
8043
- }
8044
- // Fallback for Node.js
8045
- return Buffer.alloc(32, 0);
8046
- }
8047
- function bs58Encode(buffer) {
8048
- // In production, use actual base58 encoding
8049
- // This is a placeholder
8050
- return 'P' + buffer.toString('hex').slice(0, 31);
8051
- }
8052
-
8053
8362
  /**
8054
8363
  * Borsh-compatible instruction data serializer.
8055
8364
  *
@@ -8140,6 +8449,7 @@ function serializePrimitive(value, type) {
8140
8449
  strLen.writeUInt32LE(strBytes.length, 0);
8141
8450
  return Buffer.concat([strLen, strBytes]);
8142
8451
  case 'pubkey':
8452
+ // Public key is 32 bytes
8143
8453
  // In production, decode base58 to 32 bytes
8144
8454
  return Buffer.alloc(32, 0);
8145
8455
  default:
@@ -8192,7 +8502,7 @@ async function waitForConfirmation(signature, level = 'confirmed', timeout = 600
8192
8502
  }
8193
8503
  throw new Error(`Transaction confirmation timeout after ${timeout}ms`);
8194
8504
  }
8195
- async function checkTransactionStatus(signature) {
8505
+ async function checkTransactionStatus(_signature) {
8196
8506
  // In production, query the Solana RPC
8197
8507
  return {
8198
8508
  err: null,
@@ -8308,6 +8618,7 @@ async function executeInstruction(handler, args, options = {}) {
8308
8618
  const resolutionOptions = {
8309
8619
  accounts: options.accounts,
8310
8620
  wallet: options.wallet,
8621
+ programId: handler.programId, // Pass programId for PDA derivation
8311
8622
  };
8312
8623
  const resolution = resolveAccounts(handler.accounts, args, resolutionOptions);
8313
8624
  validateAccountResolution(resolution);
@@ -8373,6 +8684,7 @@ class HyperStack {
8373
8684
  this.processor = new FrameProcessor(this.storage, {
8374
8685
  maxEntriesPerView: options.maxEntriesPerView,
8375
8686
  flushIntervalMs: options.flushIntervalMs,
8687
+ schemas: options.validateFrames ? this.stack.schemas : undefined,
8376
8688
  });
8377
8689
  this.connection = new ConnectionManager({
8378
8690
  websocketUrl: url,
@@ -8410,6 +8722,7 @@ class HyperStack {
8410
8722
  autoReconnect: options?.autoReconnect,
8411
8723
  reconnectIntervals: options?.reconnectIntervals,
8412
8724
  maxReconnectAttempts: options?.maxReconnectAttempts,
8725
+ validateFrames: options?.validateFrames,
8413
8726
  };
8414
8727
  const client = new HyperStack(url, internalOptions);
8415
8728
  if (options?.autoReconnect !== false) {
@@ -8634,6 +8947,16 @@ const HyperstackContext = React.createContext(null);
8634
8947
  function HyperstackProvider({ children, fallback = null, ...config }) {
8635
8948
  const clientsRef = React.useRef(new Map());
8636
8949
  const connectingRef = React.useRef(new Map());
8950
+ const clientChangeListenersRef = React.useRef(new Set());
8951
+ const notifyClientChange = React.useCallback(() => {
8952
+ clientChangeListenersRef.current.forEach(cb => { cb(); });
8953
+ }, []);
8954
+ const subscribeToClientChanges = React.useCallback((callback) => {
8955
+ clientChangeListenersRef.current.add(callback);
8956
+ return () => {
8957
+ clientChangeListenersRef.current.delete(callback);
8958
+ };
8959
+ }, []);
8637
8960
  const getOrCreateClient = React.useCallback(async (stack, urlOverride) => {
8638
8961
  const cacheKey = urlOverride ? `${stack.name}:${urlOverride}` : stack.name;
8639
8962
  const existing = clientsRef.current.get(cacheKey);
@@ -8663,14 +8986,20 @@ function HyperstackProvider({ children, fallback = null, ...config }) {
8663
8986
  disconnect: () => client.disconnect()
8664
8987
  });
8665
8988
  connectingRef.current.delete(cacheKey);
8989
+ notifyClientChange();
8666
8990
  return client;
8667
8991
  });
8668
8992
  connectingRef.current.set(cacheKey, connectionPromise);
8669
8993
  return connectionPromise;
8670
- }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView]);
8994
+ }, [config.autoConnect, config.reconnectIntervals, config.maxReconnectAttempts, config.maxEntriesPerView, notifyClientChange]);
8671
8995
  const getClient = React.useCallback((stack) => {
8672
- if (!stack)
8996
+ if (!stack) {
8997
+ if (clientsRef.current.size === 1) {
8998
+ const firstEntry = clientsRef.current.values().next().value;
8999
+ return firstEntry ? firstEntry.client : null;
9000
+ }
8673
9001
  return null;
9002
+ }
8674
9003
  const entry = clientsRef.current.get(stack.name);
8675
9004
  return entry ? entry.client : null;
8676
9005
  }, []);
@@ -8686,6 +9015,7 @@ function HyperstackProvider({ children, fallback = null, ...config }) {
8686
9015
  const value = {
8687
9016
  getOrCreateClient,
8688
9017
  getClient,
9018
+ subscribeToClientChanges,
8689
9019
  config,
8690
9020
  };
8691
9021
  return (React.createElement(HyperstackContext.Provider, { value: value }, children));
@@ -8698,13 +9028,38 @@ function useHyperstackContext() {
8698
9028
  return context;
8699
9029
  }
8700
9030
  function useConnectionState(stack) {
8701
- const { getClient } = useHyperstackContext();
8702
- const client = stack ? getClient(stack) : null;
8703
- return React.useSyncExternalStore((callback) => {
8704
- if (!client)
8705
- return () => { };
8706
- return client.onConnectionStateChange(callback);
8707
- }, () => client?.connectionState ?? 'disconnected');
9031
+ const { getClient, subscribeToClientChanges } = useHyperstackContext();
9032
+ const [state, setState] = React.useState(() => {
9033
+ const client = getClient(stack);
9034
+ return client?.connectionState ?? 'disconnected';
9035
+ });
9036
+ const unsubscribeRef = React.useRef(undefined);
9037
+ React.useEffect(() => {
9038
+ let mounted = true;
9039
+ const setupClientSubscription = () => {
9040
+ unsubscribeRef.current?.();
9041
+ unsubscribeRef.current = undefined;
9042
+ const client = getClient(stack);
9043
+ if (client && mounted) {
9044
+ setState(client.connectionState);
9045
+ unsubscribeRef.current = client.onConnectionStateChange((newState) => {
9046
+ if (mounted)
9047
+ setState(newState);
9048
+ });
9049
+ }
9050
+ else if (mounted) {
9051
+ setState('disconnected');
9052
+ }
9053
+ };
9054
+ const unsubscribeFromClientChanges = subscribeToClientChanges(setupClientSubscription);
9055
+ setupClientSubscription();
9056
+ return () => {
9057
+ mounted = false;
9058
+ unsubscribeFromClientChanges();
9059
+ unsubscribeRef.current?.();
9060
+ };
9061
+ }, [getClient, subscribeToClientChanges, stack]);
9062
+ return state;
8708
9063
  }
8709
9064
  function useView(stack, viewPath) {
8710
9065
  const { getClient } = useHyperstackContext();
@@ -8756,6 +9111,7 @@ function useStateView(viewDef, client, key, options) {
8756
9111
  const cachedSnapshotRef = React.useRef(undefined);
8757
9112
  const keyString = key ? Object.values(key)[0] : undefined;
8758
9113
  const enabled = options?.enabled !== false;
9114
+ const schema = options?.schema;
8759
9115
  React.useEffect(() => {
8760
9116
  if (!enabled || !clientRef.current)
8761
9117
  return undefined;
@@ -8810,12 +9166,14 @@ function useStateView(viewDef, client, key, options) {
8810
9166
  const entity = keyString
8811
9167
  ? clientRef.current.store.get(viewDef.view, keyString)
8812
9168
  : clientRef.current.store.getAll(viewDef.view)[0];
8813
- // Cache the result to return stable reference for useSyncExternalStore
8814
- if (entity !== cachedSnapshotRef.current) {
8815
- cachedSnapshotRef.current = entity;
9169
+ const validated = entity && schema
9170
+ ? (schema.safeParse(entity).success ? entity : undefined)
9171
+ : entity;
9172
+ if (validated !== cachedSnapshotRef.current) {
9173
+ cachedSnapshotRef.current = validated;
8816
9174
  }
8817
9175
  return cachedSnapshotRef.current;
8818
- }, [viewDef.view, keyString, client]);
9176
+ }, [viewDef.view, keyString, schema, client]);
8819
9177
  const data = React.useSyncExternalStore(subscribe, getSnapshot);
8820
9178
  React.useEffect(() => {
8821
9179
  if (data !== undefined && isLoading) {
@@ -8842,6 +9200,7 @@ function useListView(viewDef, client, params, options) {
8842
9200
  const whereJson = params?.where ? JSON.stringify(params.where) : undefined;
8843
9201
  const filtersJson = params?.filters ? JSON.stringify(params.filters) : undefined;
8844
9202
  const limit = params?.limit;
9203
+ const schema = params?.schema;
8845
9204
  React.useEffect(() => {
8846
9205
  if (!enabled || !clientRef.current)
8847
9206
  return undefined;
@@ -8932,16 +9291,18 @@ function useListView(viewDef, client, params, options) {
8932
9291
  });
8933
9292
  });
8934
9293
  }
9294
+ if (schema) {
9295
+ items = items.filter((item) => schema.safeParse(item).success);
9296
+ }
8935
9297
  if (limit) {
8936
9298
  items = items.slice(0, limit);
8937
9299
  }
8938
9300
  const result = items;
8939
- // Cache the result - only update if data actually changed
8940
9301
  if (!shallowArrayEqual(cachedSnapshotRef.current, result)) {
8941
9302
  cachedSnapshotRef.current = result;
8942
9303
  }
8943
9304
  return cachedSnapshotRef.current;
8944
- }, [viewDef.view, whereJson, limit, client]);
9305
+ }, [viewDef.view, whereJson, limit, schema, client]);
8945
9306
  const data = React.useSyncExternalStore(subscribe, getSnapshot);
8946
9307
  React.useEffect(() => {
8947
9308
  if (data !== undefined && isLoading) {
@@ -9040,6 +9401,7 @@ function useHyperstack(stack, options) {
9040
9401
  const [client, setClient] = React.useState(getClient(stack));
9041
9402
  const [isLoading, setIsLoading] = React.useState(!client);
9042
9403
  const [error, setError] = React.useState(null);
9404
+ const [connectionState, setConnectionState] = React.useState(() => client?.connectionState ?? 'disconnected');
9043
9405
  React.useEffect(() => {
9044
9406
  const existingClient = getClient(stack);
9045
9407
  if (existingClient && !urlOverride) {
@@ -9059,6 +9421,17 @@ function useHyperstack(stack, options) {
9059
9421
  setIsLoading(false);
9060
9422
  });
9061
9423
  }, [stack, getOrCreateClient, getClient, urlOverride]);
9424
+ React.useEffect(() => {
9425
+ if (!client) {
9426
+ setConnectionState('disconnected');
9427
+ return;
9428
+ }
9429
+ setConnectionState(client.connectionState);
9430
+ const unsubscribe = client.onConnectionStateChange((state) => {
9431
+ setConnectionState(state);
9432
+ });
9433
+ return unsubscribe;
9434
+ }, [client]);
9062
9435
  const views = React.useMemo(() => {
9063
9436
  const result = {};
9064
9437
  for (const [viewName, viewGroup] of Object.entries(stack.views)) {
@@ -9095,6 +9468,8 @@ function useHyperstack(stack, options) {
9095
9468
  instructions: instructions,
9096
9469
  zustandStore: client?.store?.store,
9097
9470
  client: client,
9471
+ connectionState,
9472
+ isConnected: connectionState === 'connected',
9098
9473
  isLoading,
9099
9474
  error
9100
9475
  };
@@ -9113,7 +9488,7 @@ exports.ZustandAdapter = ZustandAdapter;
9113
9488
  exports.createInstructionExecutor = createInstructionExecutor;
9114
9489
  exports.createPublicKeySeed = createPublicKeySeed;
9115
9490
  exports.createSeed = createSeed;
9116
- exports.derivePda = derivePda;
9491
+ exports.derivePda = findProgramAddress;
9117
9492
  exports.executeInstruction = executeInstruction;
9118
9493
  exports.formatProgramError = formatProgramError;
9119
9494
  exports.isSnapshotFrame = isSnapshotFrame;