holosphere 2.0.0-alpha5 → 2.0.0-alpha7

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.
Files changed (34) hide show
  1. package/dist/cjs/holosphere.cjs +1 -1
  2. package/dist/esm/holosphere.js +1 -1
  3. package/dist/index-Bvwyvd0T.cjs +5 -0
  4. package/dist/index-Bvwyvd0T.cjs.map +1 -0
  5. package/dist/{index-BG8FStkt.cjs → index-C-IlLYlk.cjs} +2 -2
  6. package/dist/{index-BG8FStkt.cjs.map → index-C-IlLYlk.cjs.map} +1 -1
  7. package/dist/{index-Bbey4GkP.js → index-d6f4RJBM.js} +222 -116
  8. package/dist/index-d6f4RJBM.js.map +1 -0
  9. package/dist/{index-Cp3xctq8.js → index-jmTHEbR2.js} +2 -2
  10. package/dist/{index-Cp3xctq8.js.map → index-jmTHEbR2.js.map} +1 -1
  11. package/dist/{indexeddb-storage-Bjg84U5R.js → indexeddb-storage-D8kOl0oK.js} +2 -2
  12. package/dist/{indexeddb-storage-Bjg84U5R.js.map → indexeddb-storage-D8kOl0oK.js.map} +1 -1
  13. package/dist/{indexeddb-storage-BD70pN7q.cjs → indexeddb-storage-a8GipaDr.cjs} +2 -2
  14. package/dist/{indexeddb-storage-BD70pN7q.cjs.map → indexeddb-storage-a8GipaDr.cjs.map} +1 -1
  15. package/dist/{memory-storage-CD0XFayE.js → memory-storage-DBQK622V.js} +2 -2
  16. package/dist/{memory-storage-CD0XFayE.js.map → memory-storage-DBQK622V.js.map} +1 -1
  17. package/dist/{memory-storage-DmMyJtOo.cjs → memory-storage-gfRovk2O.cjs} +2 -2
  18. package/dist/{memory-storage-DmMyJtOo.cjs.map → memory-storage-gfRovk2O.cjs.map} +1 -1
  19. package/dist/{secp256k1-TcN6vWGh.cjs → secp256k1-BCAPF45D.cjs} +2 -2
  20. package/dist/{secp256k1-TcN6vWGh.cjs.map → secp256k1-BCAPF45D.cjs.map} +1 -1
  21. package/dist/{secp256k1-69sS9O-P.js → secp256k1-DYm_CMqW.js} +2 -2
  22. package/dist/{secp256k1-69sS9O-P.js.map → secp256k1-DYm_CMqW.js.map} +1 -1
  23. package/package.json +1 -1
  24. package/src/contracts/abis/Bundle.json +1438 -1435
  25. package/src/contracts/deployer.js +32 -3
  26. package/src/federation/handshake.js +13 -5
  27. package/src/index.js +9 -1
  28. package/src/storage/gun-async.js +57 -6
  29. package/src/storage/gun-auth.js +90 -31
  30. package/src/storage/gun-wrapper.js +42 -48
  31. package/src/storage/nostr-client.js +5 -2
  32. package/dist/index-Bbey4GkP.js.map +0 -1
  33. package/dist/index-hfVGRwSr.cjs +0 -5
  34. package/dist/index-hfVGRwSr.cjs.map +0 -1
@@ -304,6 +304,7 @@ export class ContractDeployer {
304
304
 
305
305
  /**
306
306
  * Deploy a Bundle contract (2-way split with steepness)
307
+ * This is the simplest way to deploy a holon - no registry needed!
307
308
  * @param {string} name - Contract name
308
309
  * @param {number} [steepness=500000000000000000n] - Steepness factor (0.5e18 = 50% decay)
309
310
  * @param {number} [nZones=6] - Number of zones
@@ -319,9 +320,17 @@ export class ContractDeployer {
319
320
  // Default steepness: 0.5e18 (50% decay per zone)
320
321
  const steepnessValue = steepness || ethers.parseEther('0.5');
321
322
 
323
+ // BundleABI now has { abi, bytecode } structure
324
+ const abi = BundleABI.abi || BundleABI;
325
+ const bytecode = BundleABI.bytecode;
326
+
327
+ if (!bytecode) {
328
+ throw new Error('Bundle bytecode not available. Please rebuild contracts.');
329
+ }
330
+
322
331
  const result = await this._deployContract(
323
- BundleABI.abi,
324
- BundleABI.bytecode,
332
+ abi,
333
+ bytecode,
325
334
  [deployerAddress, creatorUserId, name, steepnessValue, nZones]
326
335
  );
327
336
 
@@ -329,10 +338,30 @@ export class ContractDeployer {
329
338
  ...result,
330
339
  type: 'Bundle',
331
340
  steepness: steepnessValue,
332
- nZones
341
+ nZones,
342
+ creatorUserId,
343
+ name
333
344
  };
334
345
  }
335
346
 
347
+ /**
348
+ * Deploy a Bundle contract directly (simplified 1-click deployment)
349
+ * No registry needed - just deploys and returns the address
350
+ * @param {string} holonId - Unique identifier for the holon
351
+ * @param {string} name - Display name for the holon
352
+ * @param {Object} [options] - Options { steepness, nZones }
353
+ * @returns {Promise<{address, txHash, contract}>}
354
+ */
355
+ async deployBundleDirect(holonId, name, options = {}) {
356
+ const steepness = options.steepness;
357
+ const nZones = options.nZones || 6;
358
+
359
+ return this.deployBundle(name, steepness, nZones, {
360
+ creatorUserId: holonId,
361
+ ...options
362
+ });
363
+ }
364
+
336
365
  /**
337
366
  * Deploy a TestToken (ERC20 for testing)
338
367
  * @param {string} [initialSupply='1000000'] - Initial supply in whole tokens
@@ -301,8 +301,8 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
301
301
 
302
302
  try {
303
303
  // Get sender's public key
304
- const { getPublicKeyFromPrivate } = await import('../crypto/nostr-utils.js');
305
- const senderPubKey = getPublicKeyFromPrivate(privateKey);
304
+ const { getPublicKey } = await import('../crypto/nostr-utils.js');
305
+ const senderPubKey = getPublicKey(privateKey);
306
306
 
307
307
  // Create federation request
308
308
  const request = createFederationRequest({
@@ -361,10 +361,10 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
361
361
 
362
362
  try {
363
363
  // Get responder's public key
364
- const { getPublicKeyFromPrivate } = await import('../crypto/nostr-utils.js');
365
- const responderPubKey = getPublicKeyFromPrivate(privateKey);
364
+ const { getPublicKey } = await import('../crypto/nostr-utils.js');
365
+ const responderPubKey = getPublicKey(privateKey);
366
366
 
367
- // Add sender as federated partner
367
+ // Add sender as federated partner in Nostr registry
368
368
  if (holosphere.client) {
369
369
  await addFederatedPartner(holosphere.client, holosphere.config.appName, senderPubKey, {
370
370
  alias: request.senderHolonName,
@@ -379,6 +379,14 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
379
379
  }
380
380
  }
381
381
 
382
+ // Create the actual federation record in GunDB storage
383
+ // This is what makes the partner appear in getFederation() / loadFederationData()
384
+ // Store the sender's name so we can display it without reading their settings
385
+ await holosphere.federateHolon(holonId, senderPubKey, {
386
+ lensConfig,
387
+ partnerName: request.senderHolonName
388
+ });
389
+
382
390
  // Create and send response
383
391
  const response = createFederationResponse({
384
392
  requestId: request.requestId,
package/src/index.js CHANGED
@@ -798,6 +798,7 @@ class HoloSphereBase extends HoloSphereCore {
798
798
  if (!Array.isArray(data.inbound)) data.inbound = [];
799
799
  if (!Array.isArray(data.outbound)) data.outbound = [];
800
800
  if (!data.lensConfig || typeof data.lensConfig !== 'object') data.lensConfig = {};
801
+ if (!data.partnerNames || typeof data.partnerNames !== 'object') data.partnerNames = {};
801
802
 
802
803
  if (!Array.isArray(data.federated)) {
803
804
  const allFederated = new Set([...data.inbound, ...data.outbound]);
@@ -808,7 +809,7 @@ class HoloSphereBase extends HoloSphereCore {
808
809
  }
809
810
 
810
811
  async federateHolon(sourceHolon, targetHolon, options = {}) {
811
- const { lensConfig = { inbound: [], outbound: [] } } = options;
812
+ const { lensConfig = { inbound: [], outbound: [] }, partnerName = null } = options;
812
813
 
813
814
  if (sourceHolon === targetHolon) {
814
815
  throw new Error('Cannot federate a holon with itself');
@@ -821,6 +822,7 @@ class HoloSphereBase extends HoloSphereCore {
821
822
  inbound: [],
822
823
  outbound: [],
823
824
  lensConfig: {},
825
+ partnerNames: {},
824
826
  timestamp: Date.now()
825
827
  };
826
828
 
@@ -828,11 +830,17 @@ class HoloSphereBase extends HoloSphereCore {
828
830
  if (!Array.isArray(federationData.inbound)) federationData.inbound = [];
829
831
  if (!Array.isArray(federationData.outbound)) federationData.outbound = [];
830
832
  if (!federationData.lensConfig || typeof federationData.lensConfig !== 'object') federationData.lensConfig = {};
833
+ if (!federationData.partnerNames || typeof federationData.partnerNames !== 'object') federationData.partnerNames = {};
831
834
 
832
835
  if (!federationData.federated.includes(targetHolon)) {
833
836
  federationData.federated.push(targetHolon);
834
837
  }
835
838
 
839
+ // Store the partner's name if provided
840
+ if (partnerName) {
841
+ federationData.partnerNames[targetHolon] = partnerName;
842
+ }
843
+
836
844
  if (lensConfig.outbound && lensConfig.outbound.length > 0) {
837
845
  if (!federationData.outbound.includes(targetHolon)) {
838
846
  federationData.outbound.push(targetHolon);
@@ -64,22 +64,73 @@ export function gunPut(gunChain, data, timeout = 1000) {
64
64
 
65
65
  /**
66
66
  * Get all items from a Gun map
67
+ * First gets count of items, then collects until count is reached
67
68
  * @param {Object} gunChain - Gun chain reference
68
- * @param {number} timeout - Timeout in ms (default 300ms)
69
+ * @param {number} timeout - Timeout in ms (fallback, default 5000ms)
69
70
  * @returns {Promise<Object>} Promise resolving to map of items
70
71
  */
71
- export async function gunMap(gunChain, timeout = 300) {
72
+ export async function gunMap(gunChain, timeout = 5000) {
72
73
  return new Promise((resolve) => {
73
74
  const items = {};
75
+ let settled = false;
76
+ let expectedCount = 0;
77
+ let receivedCount = 0;
74
78
 
75
- gunChain.map().once((data, key) => {
76
- if (data && !key.startsWith('_')) {
77
- items[key] = data;
79
+ const tryResolve = () => {
80
+ if (settled) return;
81
+ if (expectedCount > 0 && receivedCount >= expectedCount) {
82
+ settled = true;
83
+ resolve(items);
84
+ }
85
+ };
86
+
87
+ // Step 1: Get parent to count expected items
88
+ gunChain.once((parentData) => {
89
+ if (settled) return;
90
+
91
+ if (!parentData) {
92
+ settled = true;
93
+ resolve({});
94
+ return;
95
+ }
96
+
97
+ const keys = Object.keys(parentData).filter(k => k !== '_');
98
+ if (keys.length === 0) {
99
+ settled = true;
100
+ resolve({});
101
+ return;
78
102
  }
103
+
104
+ // Pre-collect inline items (not Gun references)
105
+ for (const key of keys) {
106
+ const rawItem = parentData[key];
107
+ if (!rawItem) continue;
108
+
109
+ // Skip Gun references - will be fetched via map().once()
110
+ if (typeof rawItem === 'object' && rawItem['#']) continue;
111
+
112
+ items[key] = rawItem;
113
+ }
114
+
115
+ // Expected count is ALL keys (map().once() fires for all)
116
+ expectedCount = keys.length;
117
+
118
+ // Step 2: Collect items via map().once(), counting as we go
119
+ gunChain.map().once((data, key) => {
120
+ if (settled || !data || key.startsWith('_')) return;
121
+ // Add/update the item (inline items may be updated by map().once())
122
+ items[key] = data;
123
+ receivedCount++;
124
+ tryResolve();
125
+ });
79
126
  });
80
127
 
128
+ // Fallback timeout
81
129
  setTimeout(() => {
82
- resolve(items);
130
+ if (!settled) {
131
+ settled = true;
132
+ resolve(items);
133
+ }
83
134
  }, timeout);
84
135
  });
85
136
  }
@@ -208,11 +208,12 @@ export class GunAuth {
208
208
 
209
209
  /**
210
210
  * Read all data from a private lens
211
+ * First gets count of items, then collects until count is reached
211
212
  * @param {string} lens - Lens name
212
- * @param {number} timeout - Timeout in ms
213
+ * @param {number} timeout - Timeout in ms (fallback)
213
214
  * @returns {Promise<Object[]>} Array of data objects
214
215
  */
215
- async readAllPrivate(lens, timeout = 2000) {
216
+ async readAllPrivate(lens, timeout = 5000) {
216
217
  if (!this.isAuthenticated()) {
217
218
  throw new Error('Not authenticated');
218
219
  }
@@ -221,42 +222,100 @@ export class GunAuth {
221
222
  const results = [];
222
223
  const seen = new Set();
223
224
  const ref = this.getPrivatePath(lens);
225
+ let settled = false;
226
+ let expectedCount = 0;
227
+ let receivedCount = 0;
228
+
229
+ const tryResolve = () => {
230
+ if (settled) return;
231
+ if (expectedCount > 0 && receivedCount >= expectedCount) {
232
+ settled = true;
233
+ resolve(results);
234
+ }
235
+ };
224
236
 
225
- const timer = setTimeout(() => {
226
- try {
227
- ref.off();
228
- } catch (e) {}
229
- resolve(results);
230
- }, timeout);
231
-
232
- ref.map().once((data, key) => {
233
- if (data && !key.startsWith('_') && !seen.has(key)) {
234
- seen.add(key);
235
-
236
- let parsed = null;
237
- // Primary: JSON string format (like old holosphere)
238
- if (typeof data === 'string') {
239
- try {
240
- parsed = JSON.parse(data);
241
- } catch (e) {
242
- parsed = data;
243
- }
244
- // Legacy: _json wrapper format
245
- } else if (data._json && typeof data._json === 'string') {
246
- try {
247
- parsed = JSON.parse(data._json);
248
- } catch (e) {}
249
- // Plain object
250
- } else if (typeof data === 'object') {
251
- parsed = { ...data };
252
- delete parsed['_'];
237
+ const parseItem = (data) => {
238
+ if (!data) return null;
239
+ let parsed = null;
240
+ // Primary: JSON string format (like old holosphere)
241
+ if (typeof data === 'string') {
242
+ try {
243
+ parsed = JSON.parse(data);
244
+ } catch (e) {
245
+ parsed = data;
253
246
  }
247
+ // Legacy: _json wrapper format
248
+ } else if (data._json && typeof data._json === 'string') {
249
+ try {
250
+ parsed = JSON.parse(data._json);
251
+ } catch (e) {}
252
+ // Plain object
253
+ } else if (typeof data === 'object') {
254
+ parsed = { ...data };
255
+ delete parsed['_'];
256
+ }
257
+ return parsed;
258
+ };
259
+
260
+ // Step 1: Get parent to count expected items
261
+ ref.once((parentData) => {
262
+ if (settled) return;
254
263
 
255
- if (parsed && !parsed._deleted) {
264
+ if (!parentData) {
265
+ settled = true;
266
+ resolve([]);
267
+ return;
268
+ }
269
+
270
+ const keys = Object.keys(parentData).filter(k => k !== '_');
271
+ if (keys.length === 0) {
272
+ settled = true;
273
+ resolve([]);
274
+ return;
275
+ }
276
+
277
+ // Pre-parse inline items (not Gun references)
278
+ for (const key of keys) {
279
+ const rawItem = parentData[key];
280
+ if (!rawItem) continue;
281
+
282
+ // Skip Gun references - will be fetched via map().once()
283
+ if (typeof rawItem === 'object' && rawItem['#']) continue;
284
+
285
+ const parsed = parseItem(rawItem);
286
+ if (parsed && !parsed._deleted && !seen.has(key)) {
287
+ seen.add(key);
256
288
  results.push(parsed);
257
289
  }
258
290
  }
291
+
292
+ // Expected count is ALL keys (map().once() fires for all)
293
+ expectedCount = keys.length;
294
+
295
+ // Step 2: Collect items via map().once(), counting as we go
296
+ ref.map().once((data, key) => {
297
+ if (settled || !data || key.startsWith('_')) return;
298
+
299
+ // Count every item, but only add if not already seen
300
+ if (!seen.has(key)) {
301
+ seen.add(key);
302
+ const parsed = parseItem(data);
303
+ if (parsed && !parsed._deleted) {
304
+ results.push(parsed);
305
+ }
306
+ }
307
+ receivedCount++;
308
+ tryResolve();
309
+ });
259
310
  });
311
+
312
+ // Fallback timeout
313
+ setTimeout(() => {
314
+ if (!settled) {
315
+ settled = true;
316
+ resolve(results);
317
+ }
318
+ }, timeout);
260
319
  });
261
320
  }
262
321
 
@@ -186,20 +186,34 @@ export async function read(gun, path) {
186
186
 
187
187
  /**
188
188
  * Read all data under a path (lens query)
189
- * Uses Gun's .open() which waits for nested data to load
190
- * Falls back to .once() with iteration
189
+ * First gets the count of expected items, then collects until count is reached
191
190
  * @param {Object} gun - Gun instance
192
191
  * @param {string} path - Gun path
192
+ * @param {number} timeout - Maximum timeout in ms (default 5000)
193
193
  * @returns {Promise<Object[]>} Array of data objects
194
194
  */
195
- export async function readAll(gun, path) {
195
+ export async function readAll(gun, path, timeout = 5000) {
196
196
  return new Promise((resolve) => {
197
197
  const output = new Map();
198
198
  let settled = false;
199
+ let expectedCount = 0;
200
+ let receivedCount = 0;
199
201
 
200
202
  const ref = gun.get(path);
201
203
 
202
- // Use .once() on parent to get all keys, then process
204
+ const tryResolve = () => {
205
+ if (settled) return;
206
+ if (expectedCount > 0 && receivedCount >= expectedCount) {
207
+ settled = true;
208
+ resolve(Array.from(output.values()));
209
+ }
210
+ };
211
+
212
+ const parseItem = (data) => {
213
+ return deserializeFromGun(data);
214
+ };
215
+
216
+ // Step 1: Get the parent data to count expected items
203
217
  ref.once((parentData) => {
204
218
  if (settled) return;
205
219
 
@@ -218,81 +232,61 @@ export async function readAll(gun, path) {
218
232
  return;
219
233
  }
220
234
 
221
- // Process inline data directly from parentData
222
- // Gun often includes the data inline as references or direct values
235
+ // Count expected items and check for inline data
236
+ const referenceKeys = [];
237
+
223
238
  for (const key of keys) {
224
239
  const rawItem = parentData[key];
225
240
  if (!rawItem) continue;
226
241
 
227
- let item = null;
228
-
229
- // Check if it's a Gun reference (soul)
242
+ // Check if it's a Gun reference (soul) - need to fetch separately
230
243
  if (typeof rawItem === 'object' && rawItem['#']) {
231
- // Skip references for now, map().once will handle them
244
+ referenceKeys.push(key);
232
245
  continue;
233
246
  }
234
247
 
235
- // Try to parse as _json wrapper
236
- if (rawItem._json && typeof rawItem._json === 'string') {
237
- try {
238
- item = JSON.parse(rawItem._json);
239
- } catch (e) {}
240
- }
241
- // Try as direct JSON string
242
- else if (typeof rawItem === 'string') {
243
- try {
244
- item = JSON.parse(rawItem);
245
- } catch (e) {}
246
- }
247
-
248
+ // Try to parse inline data (don't count yet - will count in map().once() phase)
249
+ const item = parseItem(rawItem);
248
250
  if (item && item.id && !item._deleted) {
249
251
  output.set(item.id, item);
250
252
  }
251
253
  }
252
254
 
253
- // If we got all data inline, resolve now
254
- if (output.size > 0) {
255
+ // Set expected count: ALL keys that could have data (references + inline)
256
+ // We use total keys because map().once() will fire for all of them
257
+ expectedCount = keys.length;
258
+
259
+ // If no keys, we're done (shouldn't happen but be safe)
260
+ if (expectedCount === 0) {
255
261
  settled = true;
256
262
  resolve(Array.from(output.values()));
257
263
  return;
258
264
  }
259
265
 
260
- // Otherwise use map().once() to resolve references
266
+ // Step 2: Use map().once() to resolve all items, counting as we go
261
267
  ref.map().once((data, key) => {
262
268
  if (settled || !data || key === '_') return;
263
269
 
264
- let item = null;
265
- if (data._json && typeof data._json === 'string') {
266
- try {
267
- item = JSON.parse(data._json);
268
- } catch (e) {}
269
- } else if (typeof data === 'string') {
270
- try {
271
- item = JSON.parse(data);
272
- } catch (e) {}
273
- }
274
-
270
+ const item = parseItem(data);
275
271
  if (item && item.id && !item._deleted) {
276
- output.set(item.id, item);
272
+ // Add to output if not already there (inline items already added)
273
+ if (!output.has(item.id)) {
274
+ output.set(item.id, item);
275
+ }
277
276
  }
277
+ // Count every item received (inline or reference)
278
+ receivedCount++;
279
+ tryResolve();
278
280
  });
279
-
280
- // Give map().once() time to complete
281
- setTimeout(() => {
282
- if (!settled) {
283
- settled = true;
284
- resolve(Array.from(output.values()));
285
- }
286
- }, 500);
287
281
  });
288
282
 
289
- // Overall timeout
283
+ // Fallback timeout in case count-based resolution fails
290
284
  setTimeout(() => {
291
285
  if (!settled) {
292
286
  settled = true;
293
287
  resolve(Array.from(output.values()));
294
288
  }
295
- }, 2000);
289
+ }, timeout);
296
290
  });
297
291
  }
298
292
 
@@ -496,8 +496,11 @@ export class NostrClient {
496
496
  async _doPublish(event, options = {}) {
497
497
  const waitForRelays = options.waitForRelays || false;
498
498
 
499
- // Sign the event
500
- const signedEvent = finalizeEvent(event, this.privateKey);
499
+ // Check if event is already signed (has id and sig)
500
+ // If so, use it as-is; otherwise sign it
501
+ const signedEvent = (event.id && event.sig)
502
+ ? event
503
+ : finalizeEvent(event, this.privateKey);
501
504
 
502
505
  // 1. Cache the event locally first (this makes reads instant)
503
506
  await this._cacheEvent(signedEvent);