@unicitylabs/sphere-sdk 0.2.5 → 0.3.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/core/index.cjs +2143 -795
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +248 -46
- package/dist/core/index.d.ts +248 -46
- package/dist/core/index.js +2144 -792
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +58 -111
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +58 -111
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +33 -85
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +33 -85
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +58 -116
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +23 -20
- package/dist/impl/nodejs/index.d.ts +23 -20
- package/dist/impl/nodejs/index.js +58 -116
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +5647 -4329
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +304 -50
- package/dist/index.d.ts +304 -50
- package/dist/index.js +5634 -4318
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/dist/core/index.js
CHANGED
|
@@ -2303,7 +2303,17 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2303
2303
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2304
2304
|
TRACKED_ADDRESSES: "tracked_addresses",
|
|
2305
2305
|
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
2306
|
-
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
2306
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts",
|
|
2307
|
+
/** Group chat: joined groups */
|
|
2308
|
+
GROUP_CHAT_GROUPS: "group_chat_groups",
|
|
2309
|
+
/** Group chat: messages */
|
|
2310
|
+
GROUP_CHAT_MESSAGES: "group_chat_messages",
|
|
2311
|
+
/** Group chat: members */
|
|
2312
|
+
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
2313
|
+
/** Group chat: processed event IDs for deduplication */
|
|
2314
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
2315
|
+
/** Group chat: last used relay URL (stale data detection) */
|
|
2316
|
+
GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
|
|
2307
2317
|
};
|
|
2308
2318
|
var STORAGE_KEYS_ADDRESS = {
|
|
2309
2319
|
/** Pending transfers for this address */
|
|
@@ -2334,13 +2344,95 @@ function getAddressId(directAddress) {
|
|
|
2334
2344
|
const last = hash.slice(-6).toLowerCase();
|
|
2335
2345
|
return `DIRECT_${first}_${last}`;
|
|
2336
2346
|
}
|
|
2347
|
+
var DEFAULT_NOSTR_RELAYS = [
|
|
2348
|
+
"wss://relay.unicity.network",
|
|
2349
|
+
"wss://relay.damus.io",
|
|
2350
|
+
"wss://nos.lol",
|
|
2351
|
+
"wss://relay.nostr.band"
|
|
2352
|
+
];
|
|
2353
|
+
var NIP29_KINDS = {
|
|
2354
|
+
/** Chat message sent to group */
|
|
2355
|
+
CHAT_MESSAGE: 9,
|
|
2356
|
+
/** Thread root message */
|
|
2357
|
+
THREAD_ROOT: 11,
|
|
2358
|
+
/** Thread reply message */
|
|
2359
|
+
THREAD_REPLY: 12,
|
|
2360
|
+
/** User join request */
|
|
2361
|
+
JOIN_REQUEST: 9021,
|
|
2362
|
+
/** User leave request */
|
|
2363
|
+
LEAVE_REQUEST: 9022,
|
|
2364
|
+
/** Admin: add/update user */
|
|
2365
|
+
PUT_USER: 9e3,
|
|
2366
|
+
/** Admin: remove user */
|
|
2367
|
+
REMOVE_USER: 9001,
|
|
2368
|
+
/** Admin: edit group metadata */
|
|
2369
|
+
EDIT_METADATA: 9002,
|
|
2370
|
+
/** Admin: delete event */
|
|
2371
|
+
DELETE_EVENT: 9005,
|
|
2372
|
+
/** Admin: create group */
|
|
2373
|
+
CREATE_GROUP: 9007,
|
|
2374
|
+
/** Admin: delete group */
|
|
2375
|
+
DELETE_GROUP: 9008,
|
|
2376
|
+
/** Admin: create invite code */
|
|
2377
|
+
CREATE_INVITE: 9009,
|
|
2378
|
+
/** Relay-signed group metadata */
|
|
2379
|
+
GROUP_METADATA: 39e3,
|
|
2380
|
+
/** Relay-signed group admins */
|
|
2381
|
+
GROUP_ADMINS: 39001,
|
|
2382
|
+
/** Relay-signed group members */
|
|
2383
|
+
GROUP_MEMBERS: 39002,
|
|
2384
|
+
/** Relay-signed group roles */
|
|
2385
|
+
GROUP_ROLES: 39003
|
|
2386
|
+
};
|
|
2387
|
+
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
2388
|
+
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
2389
|
+
var TEST_AGGREGATOR_URL = "https://goggregator-test.unicity.network";
|
|
2390
|
+
var DEFAULT_IPFS_GATEWAYS = [
|
|
2391
|
+
"https://ipfs.unicity.network",
|
|
2392
|
+
"https://dweb.link",
|
|
2393
|
+
"https://ipfs.io"
|
|
2394
|
+
];
|
|
2337
2395
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2338
2396
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2397
|
+
var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
|
|
2398
|
+
var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
|
|
2399
|
+
var TEST_NOSTR_RELAYS = [
|
|
2400
|
+
"wss://nostr-relay.testnet.unicity.network"
|
|
2401
|
+
];
|
|
2402
|
+
var DEFAULT_GROUP_RELAYS = [
|
|
2403
|
+
"wss://sphere-relay.unicity.network"
|
|
2404
|
+
];
|
|
2405
|
+
var NETWORKS = {
|
|
2406
|
+
mainnet: {
|
|
2407
|
+
name: "Mainnet",
|
|
2408
|
+
aggregatorUrl: DEFAULT_AGGREGATOR_URL,
|
|
2409
|
+
nostrRelays: DEFAULT_NOSTR_RELAYS,
|
|
2410
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2411
|
+
electrumUrl: DEFAULT_ELECTRUM_URL,
|
|
2412
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2413
|
+
},
|
|
2414
|
+
testnet: {
|
|
2415
|
+
name: "Testnet",
|
|
2416
|
+
aggregatorUrl: TEST_AGGREGATOR_URL,
|
|
2417
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
2418
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2419
|
+
electrumUrl: TEST_ELECTRUM_URL,
|
|
2420
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2421
|
+
},
|
|
2422
|
+
dev: {
|
|
2423
|
+
name: "Development",
|
|
2424
|
+
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
2425
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
2426
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2427
|
+
electrumUrl: TEST_ELECTRUM_URL,
|
|
2428
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2339
2431
|
|
|
2340
2432
|
// types/txf.ts
|
|
2341
2433
|
var ARCHIVED_PREFIX = "archived-";
|
|
2342
2434
|
var FORKED_PREFIX = "_forked_";
|
|
2343
|
-
var RESERVED_KEYS = ["_meta", "_nametag", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2435
|
+
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2344
2436
|
function isTokenKey(key) {
|
|
2345
2437
|
return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
|
|
2346
2438
|
}
|
|
@@ -2376,94 +2468,388 @@ function parseForkedKey(key) {
|
|
|
2376
2468
|
};
|
|
2377
2469
|
}
|
|
2378
2470
|
|
|
2379
|
-
//
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2471
|
+
// registry/token-registry.testnet.json
|
|
2472
|
+
var token_registry_testnet_default = [
|
|
2473
|
+
{
|
|
2474
|
+
network: "unicity:testnet",
|
|
2475
|
+
assetKind: "non-fungible",
|
|
2476
|
+
name: "unicity",
|
|
2477
|
+
description: "Unicity testnet token type",
|
|
2478
|
+
id: "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509"
|
|
2479
|
+
},
|
|
2480
|
+
{
|
|
2481
|
+
network: "unicity:testnet",
|
|
2482
|
+
assetKind: "fungible",
|
|
2483
|
+
name: "unicity",
|
|
2484
|
+
symbol: "UCT",
|
|
2485
|
+
decimals: 18,
|
|
2486
|
+
description: "Unicity testnet native coin",
|
|
2487
|
+
icons: [
|
|
2488
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity_logo_32.png" }
|
|
2489
|
+
],
|
|
2490
|
+
id: "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89"
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
network: "unicity:testnet",
|
|
2494
|
+
assetKind: "fungible",
|
|
2495
|
+
name: "unicity-usd",
|
|
2496
|
+
symbol: "USDU",
|
|
2497
|
+
decimals: 6,
|
|
2498
|
+
description: "Unicity testnet USD stablecoin",
|
|
2499
|
+
icons: [
|
|
2500
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/usdu_logo_32.png" }
|
|
2501
|
+
],
|
|
2502
|
+
id: "8f0f3d7a5e7297be0ee98c63b81bcebb2740f43f616566fc290f9823a54f52d7"
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
network: "unicity:testnet",
|
|
2506
|
+
assetKind: "fungible",
|
|
2507
|
+
name: "unicity-eur",
|
|
2508
|
+
symbol: "EURU",
|
|
2509
|
+
decimals: 6,
|
|
2510
|
+
description: "Unicity testnet EUR stablecoin",
|
|
2511
|
+
icons: [
|
|
2512
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/euru_logo_32.png" }
|
|
2513
|
+
],
|
|
2514
|
+
id: "5e160d5e9fdbb03b553fb9c3f6e6c30efa41fa807be39fb4f18e43776e492925"
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
network: "unicity:testnet",
|
|
2518
|
+
assetKind: "fungible",
|
|
2519
|
+
name: "solana",
|
|
2520
|
+
symbol: "SOL",
|
|
2521
|
+
decimals: 9,
|
|
2522
|
+
description: "Solana testnet coin on Unicity",
|
|
2523
|
+
icons: [
|
|
2524
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/sol.svg" },
|
|
2525
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/sol.png" }
|
|
2526
|
+
],
|
|
2527
|
+
id: "dee5f8ce778562eec90e9c38a91296a023210ccc76ff4c29d527ac3eb64ade93"
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
network: "unicity:testnet",
|
|
2531
|
+
assetKind: "fungible",
|
|
2532
|
+
name: "bitcoin",
|
|
2533
|
+
symbol: "BTC",
|
|
2534
|
+
decimals: 8,
|
|
2535
|
+
description: "Bitcoin testnet coin on Unicity",
|
|
2536
|
+
icons: [
|
|
2537
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/btc.svg" },
|
|
2538
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/btc.png" }
|
|
2539
|
+
],
|
|
2540
|
+
id: "86bc190fcf7b2d07c6078de93db803578760148b16d4431aa2f42a3241ff0daa"
|
|
2541
|
+
},
|
|
2542
|
+
{
|
|
2543
|
+
network: "unicity:testnet",
|
|
2544
|
+
assetKind: "fungible",
|
|
2545
|
+
name: "ethereum",
|
|
2546
|
+
symbol: "ETH",
|
|
2547
|
+
decimals: 18,
|
|
2548
|
+
description: "Ethereum testnet coin on Unicity",
|
|
2549
|
+
icons: [
|
|
2550
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/eth.svg" },
|
|
2551
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/eth.png" }
|
|
2552
|
+
],
|
|
2553
|
+
id: "3c2450f2fd867e7bb60c6a69d7ad0e53ce967078c201a3ecaa6074ed4c0deafb"
|
|
2554
|
+
},
|
|
2555
|
+
{
|
|
2556
|
+
network: "unicity:testnet",
|
|
2557
|
+
assetKind: "fungible",
|
|
2558
|
+
name: "alpha_test",
|
|
2559
|
+
symbol: "ALPHT",
|
|
2560
|
+
decimals: 8,
|
|
2561
|
+
description: "ALPHA testnet coin on Unicity",
|
|
2562
|
+
icons: [
|
|
2563
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/alpha_coin.png" }
|
|
2564
|
+
],
|
|
2565
|
+
id: "cde78ded16ef65818a51f43138031c4284e519300ab0cb60c30a8f9078080e5f"
|
|
2566
|
+
},
|
|
2567
|
+
{
|
|
2568
|
+
network: "unicity:testnet",
|
|
2569
|
+
assetKind: "fungible",
|
|
2570
|
+
name: "tether",
|
|
2571
|
+
symbol: "USDT",
|
|
2572
|
+
decimals: 6,
|
|
2573
|
+
description: "Tether (Ethereum) testnet coin on Unicity",
|
|
2574
|
+
icons: [
|
|
2575
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdt.svg" },
|
|
2576
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdt.png" }
|
|
2577
|
+
],
|
|
2578
|
+
id: "40d25444648418fe7efd433e147187a3a6adf049ac62bc46038bda5b960bf690"
|
|
2579
|
+
},
|
|
2580
|
+
{
|
|
2581
|
+
network: "unicity:testnet",
|
|
2582
|
+
assetKind: "fungible",
|
|
2583
|
+
name: "usd-coin",
|
|
2584
|
+
symbol: "USDC",
|
|
2585
|
+
decimals: 6,
|
|
2586
|
+
description: "USDC (Ethereum) testnet coin on Unicity",
|
|
2587
|
+
icons: [
|
|
2588
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdc.svg" },
|
|
2589
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdc.png" }
|
|
2590
|
+
],
|
|
2591
|
+
id: "2265121770fa6f41131dd9a6cc571e28679263d09a53eb2642e145b5b9a5b0a2"
|
|
2387
2592
|
}
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2593
|
+
];
|
|
2594
|
+
|
|
2595
|
+
// registry/TokenRegistry.ts
|
|
2596
|
+
var TokenRegistry = class _TokenRegistry {
|
|
2597
|
+
static instance = null;
|
|
2598
|
+
definitionsById;
|
|
2599
|
+
definitionsBySymbol;
|
|
2600
|
+
definitionsByName;
|
|
2601
|
+
constructor() {
|
|
2602
|
+
this.definitionsById = /* @__PURE__ */ new Map();
|
|
2603
|
+
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
2604
|
+
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
2605
|
+
this.loadRegistry();
|
|
2396
2606
|
}
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
if (data.tokenId !== void 0) {
|
|
2404
|
-
data.tokenId = normalizeToHex(data.tokenId);
|
|
2405
|
-
}
|
|
2406
|
-
if (data.tokenType !== void 0) {
|
|
2407
|
-
data.tokenType = normalizeToHex(data.tokenType);
|
|
2408
|
-
}
|
|
2409
|
-
if (data.salt !== void 0) {
|
|
2410
|
-
data.salt = normalizeToHex(data.salt);
|
|
2607
|
+
/**
|
|
2608
|
+
* Get singleton instance of TokenRegistry
|
|
2609
|
+
*/
|
|
2610
|
+
static getInstance() {
|
|
2611
|
+
if (!_TokenRegistry.instance) {
|
|
2612
|
+
_TokenRegistry.instance = new _TokenRegistry();
|
|
2411
2613
|
}
|
|
2614
|
+
return _TokenRegistry.instance;
|
|
2412
2615
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
if (auth.signature !== void 0) {
|
|
2419
|
-
auth.signature = normalizeToHex(auth.signature);
|
|
2420
|
-
}
|
|
2616
|
+
/**
|
|
2617
|
+
* Reset the singleton instance (useful for testing)
|
|
2618
|
+
*/
|
|
2619
|
+
static resetInstance() {
|
|
2620
|
+
_TokenRegistry.instance = null;
|
|
2421
2621
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2622
|
+
/**
|
|
2623
|
+
* Load registry data from bundled JSON
|
|
2624
|
+
*/
|
|
2625
|
+
loadRegistry() {
|
|
2626
|
+
const definitions = token_registry_testnet_default;
|
|
2627
|
+
for (const def of definitions) {
|
|
2628
|
+
const idLower = def.id.toLowerCase();
|
|
2629
|
+
this.definitionsById.set(idLower, def);
|
|
2630
|
+
if (def.symbol) {
|
|
2631
|
+
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
2432
2632
|
}
|
|
2633
|
+
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
2433
2634
|
}
|
|
2434
2635
|
}
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
if (!
|
|
2445
|
-
|
|
2446
|
-
}
|
|
2447
|
-
if (!txfData.version) {
|
|
2448
|
-
txfData.version = "2.0";
|
|
2449
|
-
}
|
|
2450
|
-
if (!txfData.transactions) {
|
|
2451
|
-
txfData.transactions = [];
|
|
2452
|
-
}
|
|
2453
|
-
if (!txfData.nametags) {
|
|
2454
|
-
txfData.nametags = [];
|
|
2455
|
-
}
|
|
2456
|
-
if (!txfData._integrity) {
|
|
2457
|
-
txfData._integrity = {
|
|
2458
|
-
genesisDataJSONHash: "0000" + "0".repeat(60)
|
|
2459
|
-
};
|
|
2460
|
-
}
|
|
2461
|
-
return txfData;
|
|
2462
|
-
} catch {
|
|
2463
|
-
return null;
|
|
2636
|
+
// ===========================================================================
|
|
2637
|
+
// Lookup Methods
|
|
2638
|
+
// ===========================================================================
|
|
2639
|
+
/**
|
|
2640
|
+
* Get token definition by hex coin ID
|
|
2641
|
+
* @param coinId - 64-character hex string
|
|
2642
|
+
* @returns Token definition or undefined if not found
|
|
2643
|
+
*/
|
|
2644
|
+
getDefinition(coinId) {
|
|
2645
|
+
if (!coinId) return void 0;
|
|
2646
|
+
return this.definitionsById.get(coinId.toLowerCase());
|
|
2464
2647
|
}
|
|
2465
|
-
|
|
2466
|
-
|
|
2648
|
+
/**
|
|
2649
|
+
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
2650
|
+
* @param symbol - Token symbol (case-insensitive)
|
|
2651
|
+
* @returns Token definition or undefined if not found
|
|
2652
|
+
*/
|
|
2653
|
+
getDefinitionBySymbol(symbol) {
|
|
2654
|
+
if (!symbol) return void 0;
|
|
2655
|
+
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
2659
|
+
* @param name - Token name (case-insensitive)
|
|
2660
|
+
* @returns Token definition or undefined if not found
|
|
2661
|
+
*/
|
|
2662
|
+
getDefinitionByName(name) {
|
|
2663
|
+
if (!name) return void 0;
|
|
2664
|
+
return this.definitionsByName.get(name.toLowerCase());
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Get token symbol for a coin ID
|
|
2668
|
+
* @param coinId - 64-character hex string
|
|
2669
|
+
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
2670
|
+
*/
|
|
2671
|
+
getSymbol(coinId) {
|
|
2672
|
+
const def = this.getDefinition(coinId);
|
|
2673
|
+
if (def?.symbol) {
|
|
2674
|
+
return def.symbol;
|
|
2675
|
+
}
|
|
2676
|
+
return coinId.slice(0, 6).toUpperCase();
|
|
2677
|
+
}
|
|
2678
|
+
/**
|
|
2679
|
+
* Get token name for a coin ID
|
|
2680
|
+
* @param coinId - 64-character hex string
|
|
2681
|
+
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
2682
|
+
*/
|
|
2683
|
+
getName(coinId) {
|
|
2684
|
+
const def = this.getDefinition(coinId);
|
|
2685
|
+
if (def?.name) {
|
|
2686
|
+
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
2687
|
+
}
|
|
2688
|
+
return coinId;
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Get decimal places for a coin ID
|
|
2692
|
+
* @param coinId - 64-character hex string
|
|
2693
|
+
* @returns Decimals or 0 if not found
|
|
2694
|
+
*/
|
|
2695
|
+
getDecimals(coinId) {
|
|
2696
|
+
const def = this.getDefinition(coinId);
|
|
2697
|
+
return def?.decimals ?? 0;
|
|
2698
|
+
}
|
|
2699
|
+
/**
|
|
2700
|
+
* Get icon URL for a coin ID
|
|
2701
|
+
* @param coinId - 64-character hex string
|
|
2702
|
+
* @param preferPng - Prefer PNG format over SVG
|
|
2703
|
+
* @returns Icon URL or null if not found
|
|
2704
|
+
*/
|
|
2705
|
+
getIconUrl(coinId, preferPng = true) {
|
|
2706
|
+
const def = this.getDefinition(coinId);
|
|
2707
|
+
if (!def?.icons || def.icons.length === 0) {
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
if (preferPng) {
|
|
2711
|
+
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
2712
|
+
if (pngIcon) return pngIcon.url;
|
|
2713
|
+
}
|
|
2714
|
+
return def.icons[0].url;
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Check if a coin ID is known in the registry
|
|
2718
|
+
* @param coinId - 64-character hex string
|
|
2719
|
+
* @returns true if the coin is in the registry
|
|
2720
|
+
*/
|
|
2721
|
+
isKnown(coinId) {
|
|
2722
|
+
return this.definitionsById.has(coinId.toLowerCase());
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Get all token definitions
|
|
2726
|
+
* @returns Array of all token definitions
|
|
2727
|
+
*/
|
|
2728
|
+
getAllDefinitions() {
|
|
2729
|
+
return Array.from(this.definitionsById.values());
|
|
2730
|
+
}
|
|
2731
|
+
/**
|
|
2732
|
+
* Get all fungible token definitions
|
|
2733
|
+
* @returns Array of fungible token definitions
|
|
2734
|
+
*/
|
|
2735
|
+
getFungibleTokens() {
|
|
2736
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
2737
|
+
}
|
|
2738
|
+
/**
|
|
2739
|
+
* Get all non-fungible token definitions
|
|
2740
|
+
* @returns Array of non-fungible token definitions
|
|
2741
|
+
*/
|
|
2742
|
+
getNonFungibleTokens() {
|
|
2743
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
2744
|
+
}
|
|
2745
|
+
/**
|
|
2746
|
+
* Get coin ID by symbol
|
|
2747
|
+
* @param symbol - Token symbol (e.g., "UCT")
|
|
2748
|
+
* @returns Coin ID hex string or undefined if not found
|
|
2749
|
+
*/
|
|
2750
|
+
getCoinIdBySymbol(symbol) {
|
|
2751
|
+
const def = this.getDefinitionBySymbol(symbol);
|
|
2752
|
+
return def?.id;
|
|
2753
|
+
}
|
|
2754
|
+
/**
|
|
2755
|
+
* Get coin ID by name
|
|
2756
|
+
* @param name - Token name (e.g., "bitcoin")
|
|
2757
|
+
* @returns Coin ID hex string or undefined if not found
|
|
2758
|
+
*/
|
|
2759
|
+
getCoinIdByName(name) {
|
|
2760
|
+
const def = this.getDefinitionByName(name);
|
|
2761
|
+
return def?.id;
|
|
2762
|
+
}
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
// serialization/txf-serializer.ts
|
|
2766
|
+
function bytesToHex3(bytes) {
|
|
2767
|
+
const arr = Array.isArray(bytes) ? bytes : Array.from(bytes);
|
|
2768
|
+
return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2769
|
+
}
|
|
2770
|
+
function normalizeToHex(value) {
|
|
2771
|
+
if (typeof value === "string") {
|
|
2772
|
+
return value;
|
|
2773
|
+
}
|
|
2774
|
+
if (value && typeof value === "object") {
|
|
2775
|
+
const obj = value;
|
|
2776
|
+
if ("bytes" in obj && (Array.isArray(obj.bytes) || obj.bytes instanceof Uint8Array)) {
|
|
2777
|
+
return bytesToHex3(obj.bytes);
|
|
2778
|
+
}
|
|
2779
|
+
if (obj.type === "Buffer" && Array.isArray(obj.data)) {
|
|
2780
|
+
return bytesToHex3(obj.data);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return String(value);
|
|
2784
|
+
}
|
|
2785
|
+
function normalizeSdkTokenToStorage(sdkTokenJson) {
|
|
2786
|
+
const txf = JSON.parse(JSON.stringify(sdkTokenJson));
|
|
2787
|
+
if (txf.genesis?.data) {
|
|
2788
|
+
const data = txf.genesis.data;
|
|
2789
|
+
if (data.tokenId !== void 0) {
|
|
2790
|
+
data.tokenId = normalizeToHex(data.tokenId);
|
|
2791
|
+
}
|
|
2792
|
+
if (data.tokenType !== void 0) {
|
|
2793
|
+
data.tokenType = normalizeToHex(data.tokenType);
|
|
2794
|
+
}
|
|
2795
|
+
if (data.salt !== void 0) {
|
|
2796
|
+
data.salt = normalizeToHex(data.salt);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (txf.genesis?.inclusionProof?.authenticator) {
|
|
2800
|
+
const auth = txf.genesis.inclusionProof.authenticator;
|
|
2801
|
+
if (auth.publicKey !== void 0) {
|
|
2802
|
+
auth.publicKey = normalizeToHex(auth.publicKey);
|
|
2803
|
+
}
|
|
2804
|
+
if (auth.signature !== void 0) {
|
|
2805
|
+
auth.signature = normalizeToHex(auth.signature);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
if (Array.isArray(txf.transactions)) {
|
|
2809
|
+
for (const tx of txf.transactions) {
|
|
2810
|
+
if (tx.inclusionProof?.authenticator) {
|
|
2811
|
+
const auth = tx.inclusionProof.authenticator;
|
|
2812
|
+
if (auth.publicKey !== void 0) {
|
|
2813
|
+
auth.publicKey = normalizeToHex(auth.publicKey);
|
|
2814
|
+
}
|
|
2815
|
+
if (auth.signature !== void 0) {
|
|
2816
|
+
auth.signature = normalizeToHex(auth.signature);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
return txf;
|
|
2822
|
+
}
|
|
2823
|
+
function tokenToTxf(token) {
|
|
2824
|
+
const jsonData = token.sdkData;
|
|
2825
|
+
if (!jsonData) {
|
|
2826
|
+
return null;
|
|
2827
|
+
}
|
|
2828
|
+
try {
|
|
2829
|
+
const txfData = normalizeSdkTokenToStorage(JSON.parse(jsonData));
|
|
2830
|
+
if (!txfData.genesis || !txfData.state) {
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
if (!txfData.version) {
|
|
2834
|
+
txfData.version = "2.0";
|
|
2835
|
+
}
|
|
2836
|
+
if (!txfData.transactions) {
|
|
2837
|
+
txfData.transactions = [];
|
|
2838
|
+
}
|
|
2839
|
+
if (!txfData.nametags) {
|
|
2840
|
+
txfData.nametags = [];
|
|
2841
|
+
}
|
|
2842
|
+
if (!txfData._integrity) {
|
|
2843
|
+
txfData._integrity = {
|
|
2844
|
+
genesisDataJSONHash: "0000" + "0".repeat(60)
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
return txfData;
|
|
2848
|
+
} catch {
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
function determineTokenStatus(txf) {
|
|
2467
2853
|
if (txf.transactions.length > 0) {
|
|
2468
2854
|
const lastTx = txf.transactions[txf.transactions.length - 1];
|
|
2469
2855
|
if (lastTx.inclusionProof === null) {
|
|
@@ -2487,12 +2873,14 @@ function txfToToken(tokenId, txf) {
|
|
|
2487
2873
|
const tokenType = txf.genesis.data.tokenType;
|
|
2488
2874
|
const isNft = tokenType === "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89";
|
|
2489
2875
|
const now = Date.now();
|
|
2876
|
+
const registry = TokenRegistry.getInstance();
|
|
2877
|
+
const def = registry.getDefinition(coinId);
|
|
2490
2878
|
return {
|
|
2491
2879
|
id: tokenId,
|
|
2492
2880
|
coinId,
|
|
2493
|
-
symbol: isNft ? "NFT" :
|
|
2494
|
-
name: isNft ? "NFT" : "Token",
|
|
2495
|
-
decimals: isNft ? 0 : 8,
|
|
2881
|
+
symbol: isNft ? "NFT" : def?.symbol || coinId.slice(0, 8),
|
|
2882
|
+
name: isNft ? "NFT" : def?.name ? def.name.charAt(0).toUpperCase() + def.name.slice(1) : "Token",
|
|
2883
|
+
decimals: isNft ? 0 : def?.decimals ?? 8,
|
|
2496
2884
|
amount: totalAmount.toString(),
|
|
2497
2885
|
status: determineTokenStatus(txf),
|
|
2498
2886
|
createdAt: now,
|
|
@@ -2507,6 +2895,9 @@ async function buildTxfStorageData(tokens, meta, options) {
|
|
|
2507
2895
|
formatVersion: "2.0"
|
|
2508
2896
|
}
|
|
2509
2897
|
};
|
|
2898
|
+
if (options?.nametags && options.nametags.length > 0) {
|
|
2899
|
+
storageData._nametags = options.nametags;
|
|
2900
|
+
}
|
|
2510
2901
|
if (options?.tombstones && options.tombstones.length > 0) {
|
|
2511
2902
|
storageData._tombstones = options.tombstones;
|
|
2512
2903
|
}
|
|
@@ -2545,7 +2936,7 @@ function parseTxfStorageData(data) {
|
|
|
2545
2936
|
const result = {
|
|
2546
2937
|
tokens: [],
|
|
2547
2938
|
meta: null,
|
|
2548
|
-
|
|
2939
|
+
nametags: [],
|
|
2549
2940
|
tombstones: [],
|
|
2550
2941
|
archivedTokens: /* @__PURE__ */ new Map(),
|
|
2551
2942
|
forkedTokens: /* @__PURE__ */ new Map(),
|
|
@@ -2562,8 +2953,20 @@ function parseTxfStorageData(data) {
|
|
|
2562
2953
|
if (storageData._meta && typeof storageData._meta === "object") {
|
|
2563
2954
|
result.meta = storageData._meta;
|
|
2564
2955
|
}
|
|
2956
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
2957
|
+
if (Array.isArray(storageData._nametags)) {
|
|
2958
|
+
for (const entry of storageData._nametags) {
|
|
2959
|
+
if (entry && typeof entry === "object" && typeof entry.name === "string") {
|
|
2960
|
+
result.nametags.push(entry);
|
|
2961
|
+
seenNames.add(entry.name);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2565
2965
|
if (storageData._nametag && typeof storageData._nametag === "object") {
|
|
2566
|
-
|
|
2966
|
+
const legacy = storageData._nametag;
|
|
2967
|
+
if (typeof legacy.name === "string" && !seenNames.has(legacy.name)) {
|
|
2968
|
+
result.nametags.push(legacy);
|
|
2969
|
+
}
|
|
2567
2970
|
}
|
|
2568
2971
|
if (storageData._tombstones && Array.isArray(storageData._tombstones)) {
|
|
2569
2972
|
for (const entry of storageData._tombstones) {
|
|
@@ -2663,300 +3066,6 @@ function getCurrentStateHash(txf) {
|
|
|
2663
3066
|
return void 0;
|
|
2664
3067
|
}
|
|
2665
3068
|
|
|
2666
|
-
// registry/token-registry.testnet.json
|
|
2667
|
-
var token_registry_testnet_default = [
|
|
2668
|
-
{
|
|
2669
|
-
network: "unicity:testnet",
|
|
2670
|
-
assetKind: "non-fungible",
|
|
2671
|
-
name: "unicity",
|
|
2672
|
-
description: "Unicity testnet token type",
|
|
2673
|
-
id: "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509"
|
|
2674
|
-
},
|
|
2675
|
-
{
|
|
2676
|
-
network: "unicity:testnet",
|
|
2677
|
-
assetKind: "fungible",
|
|
2678
|
-
name: "unicity",
|
|
2679
|
-
symbol: "UCT",
|
|
2680
|
-
decimals: 18,
|
|
2681
|
-
description: "Unicity testnet native coin",
|
|
2682
|
-
icons: [
|
|
2683
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity_logo_32.png" }
|
|
2684
|
-
],
|
|
2685
|
-
id: "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89"
|
|
2686
|
-
},
|
|
2687
|
-
{
|
|
2688
|
-
network: "unicity:testnet",
|
|
2689
|
-
assetKind: "fungible",
|
|
2690
|
-
name: "unicity-usd",
|
|
2691
|
-
symbol: "USDU",
|
|
2692
|
-
decimals: 6,
|
|
2693
|
-
description: "Unicity testnet USD stablecoin",
|
|
2694
|
-
icons: [
|
|
2695
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/usdu_logo_32.png" }
|
|
2696
|
-
],
|
|
2697
|
-
id: "8f0f3d7a5e7297be0ee98c63b81bcebb2740f43f616566fc290f9823a54f52d7"
|
|
2698
|
-
},
|
|
2699
|
-
{
|
|
2700
|
-
network: "unicity:testnet",
|
|
2701
|
-
assetKind: "fungible",
|
|
2702
|
-
name: "unicity-eur",
|
|
2703
|
-
symbol: "EURU",
|
|
2704
|
-
decimals: 6,
|
|
2705
|
-
description: "Unicity testnet EUR stablecoin",
|
|
2706
|
-
icons: [
|
|
2707
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/euru_logo_32.png" }
|
|
2708
|
-
],
|
|
2709
|
-
id: "5e160d5e9fdbb03b553fb9c3f6e6c30efa41fa807be39fb4f18e43776e492925"
|
|
2710
|
-
},
|
|
2711
|
-
{
|
|
2712
|
-
network: "unicity:testnet",
|
|
2713
|
-
assetKind: "fungible",
|
|
2714
|
-
name: "solana",
|
|
2715
|
-
symbol: "SOL",
|
|
2716
|
-
decimals: 9,
|
|
2717
|
-
description: "Solana testnet coin on Unicity",
|
|
2718
|
-
icons: [
|
|
2719
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/sol.svg" },
|
|
2720
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/sol.png" }
|
|
2721
|
-
],
|
|
2722
|
-
id: "dee5f8ce778562eec90e9c38a91296a023210ccc76ff4c29d527ac3eb64ade93"
|
|
2723
|
-
},
|
|
2724
|
-
{
|
|
2725
|
-
network: "unicity:testnet",
|
|
2726
|
-
assetKind: "fungible",
|
|
2727
|
-
name: "bitcoin",
|
|
2728
|
-
symbol: "BTC",
|
|
2729
|
-
decimals: 8,
|
|
2730
|
-
description: "Bitcoin testnet coin on Unicity",
|
|
2731
|
-
icons: [
|
|
2732
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/btc.svg" },
|
|
2733
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/btc.png" }
|
|
2734
|
-
],
|
|
2735
|
-
id: "86bc190fcf7b2d07c6078de93db803578760148b16d4431aa2f42a3241ff0daa"
|
|
2736
|
-
},
|
|
2737
|
-
{
|
|
2738
|
-
network: "unicity:testnet",
|
|
2739
|
-
assetKind: "fungible",
|
|
2740
|
-
name: "ethereum",
|
|
2741
|
-
symbol: "ETH",
|
|
2742
|
-
decimals: 18,
|
|
2743
|
-
description: "Ethereum testnet coin on Unicity",
|
|
2744
|
-
icons: [
|
|
2745
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/eth.svg" },
|
|
2746
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/eth.png" }
|
|
2747
|
-
],
|
|
2748
|
-
id: "3c2450f2fd867e7bb60c6a69d7ad0e53ce967078c201a3ecaa6074ed4c0deafb"
|
|
2749
|
-
},
|
|
2750
|
-
{
|
|
2751
|
-
network: "unicity:testnet",
|
|
2752
|
-
assetKind: "fungible",
|
|
2753
|
-
name: "alpha_test",
|
|
2754
|
-
symbol: "ALPHT",
|
|
2755
|
-
decimals: 8,
|
|
2756
|
-
description: "ALPHA testnet coin on Unicity",
|
|
2757
|
-
icons: [
|
|
2758
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/alpha_coin.png" }
|
|
2759
|
-
],
|
|
2760
|
-
id: "cde78ded16ef65818a51f43138031c4284e519300ab0cb60c30a8f9078080e5f"
|
|
2761
|
-
},
|
|
2762
|
-
{
|
|
2763
|
-
network: "unicity:testnet",
|
|
2764
|
-
assetKind: "fungible",
|
|
2765
|
-
name: "tether",
|
|
2766
|
-
symbol: "USDT",
|
|
2767
|
-
decimals: 6,
|
|
2768
|
-
description: "Tether (Ethereum) testnet coin on Unicity",
|
|
2769
|
-
icons: [
|
|
2770
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdt.svg" },
|
|
2771
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdt.png" }
|
|
2772
|
-
],
|
|
2773
|
-
id: "40d25444648418fe7efd433e147187a3a6adf049ac62bc46038bda5b960bf690"
|
|
2774
|
-
},
|
|
2775
|
-
{
|
|
2776
|
-
network: "unicity:testnet",
|
|
2777
|
-
assetKind: "fungible",
|
|
2778
|
-
name: "usd-coin",
|
|
2779
|
-
symbol: "USDC",
|
|
2780
|
-
decimals: 6,
|
|
2781
|
-
description: "USDC (Ethereum) testnet coin on Unicity",
|
|
2782
|
-
icons: [
|
|
2783
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdc.svg" },
|
|
2784
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdc.png" }
|
|
2785
|
-
],
|
|
2786
|
-
id: "2265121770fa6f41131dd9a6cc571e28679263d09a53eb2642e145b5b9a5b0a2"
|
|
2787
|
-
}
|
|
2788
|
-
];
|
|
2789
|
-
|
|
2790
|
-
// registry/TokenRegistry.ts
|
|
2791
|
-
var TokenRegistry = class _TokenRegistry {
|
|
2792
|
-
static instance = null;
|
|
2793
|
-
definitionsById;
|
|
2794
|
-
definitionsBySymbol;
|
|
2795
|
-
definitionsByName;
|
|
2796
|
-
constructor() {
|
|
2797
|
-
this.definitionsById = /* @__PURE__ */ new Map();
|
|
2798
|
-
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
2799
|
-
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
2800
|
-
this.loadRegistry();
|
|
2801
|
-
}
|
|
2802
|
-
/**
|
|
2803
|
-
* Get singleton instance of TokenRegistry
|
|
2804
|
-
*/
|
|
2805
|
-
static getInstance() {
|
|
2806
|
-
if (!_TokenRegistry.instance) {
|
|
2807
|
-
_TokenRegistry.instance = new _TokenRegistry();
|
|
2808
|
-
}
|
|
2809
|
-
return _TokenRegistry.instance;
|
|
2810
|
-
}
|
|
2811
|
-
/**
|
|
2812
|
-
* Reset the singleton instance (useful for testing)
|
|
2813
|
-
*/
|
|
2814
|
-
static resetInstance() {
|
|
2815
|
-
_TokenRegistry.instance = null;
|
|
2816
|
-
}
|
|
2817
|
-
/**
|
|
2818
|
-
* Load registry data from bundled JSON
|
|
2819
|
-
*/
|
|
2820
|
-
loadRegistry() {
|
|
2821
|
-
const definitions = token_registry_testnet_default;
|
|
2822
|
-
for (const def of definitions) {
|
|
2823
|
-
const idLower = def.id.toLowerCase();
|
|
2824
|
-
this.definitionsById.set(idLower, def);
|
|
2825
|
-
if (def.symbol) {
|
|
2826
|
-
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
2827
|
-
}
|
|
2828
|
-
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
|
-
// ===========================================================================
|
|
2832
|
-
// Lookup Methods
|
|
2833
|
-
// ===========================================================================
|
|
2834
|
-
/**
|
|
2835
|
-
* Get token definition by hex coin ID
|
|
2836
|
-
* @param coinId - 64-character hex string
|
|
2837
|
-
* @returns Token definition or undefined if not found
|
|
2838
|
-
*/
|
|
2839
|
-
getDefinition(coinId) {
|
|
2840
|
-
if (!coinId) return void 0;
|
|
2841
|
-
return this.definitionsById.get(coinId.toLowerCase());
|
|
2842
|
-
}
|
|
2843
|
-
/**
|
|
2844
|
-
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
2845
|
-
* @param symbol - Token symbol (case-insensitive)
|
|
2846
|
-
* @returns Token definition or undefined if not found
|
|
2847
|
-
*/
|
|
2848
|
-
getDefinitionBySymbol(symbol) {
|
|
2849
|
-
if (!symbol) return void 0;
|
|
2850
|
-
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
2851
|
-
}
|
|
2852
|
-
/**
|
|
2853
|
-
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
2854
|
-
* @param name - Token name (case-insensitive)
|
|
2855
|
-
* @returns Token definition or undefined if not found
|
|
2856
|
-
*/
|
|
2857
|
-
getDefinitionByName(name) {
|
|
2858
|
-
if (!name) return void 0;
|
|
2859
|
-
return this.definitionsByName.get(name.toLowerCase());
|
|
2860
|
-
}
|
|
2861
|
-
/**
|
|
2862
|
-
* Get token symbol for a coin ID
|
|
2863
|
-
* @param coinId - 64-character hex string
|
|
2864
|
-
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
2865
|
-
*/
|
|
2866
|
-
getSymbol(coinId) {
|
|
2867
|
-
const def = this.getDefinition(coinId);
|
|
2868
|
-
if (def?.symbol) {
|
|
2869
|
-
return def.symbol;
|
|
2870
|
-
}
|
|
2871
|
-
return coinId.slice(0, 6).toUpperCase();
|
|
2872
|
-
}
|
|
2873
|
-
/**
|
|
2874
|
-
* Get token name for a coin ID
|
|
2875
|
-
* @param coinId - 64-character hex string
|
|
2876
|
-
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
2877
|
-
*/
|
|
2878
|
-
getName(coinId) {
|
|
2879
|
-
const def = this.getDefinition(coinId);
|
|
2880
|
-
if (def?.name) {
|
|
2881
|
-
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
2882
|
-
}
|
|
2883
|
-
return coinId;
|
|
2884
|
-
}
|
|
2885
|
-
/**
|
|
2886
|
-
* Get decimal places for a coin ID
|
|
2887
|
-
* @param coinId - 64-character hex string
|
|
2888
|
-
* @returns Decimals or 0 if not found
|
|
2889
|
-
*/
|
|
2890
|
-
getDecimals(coinId) {
|
|
2891
|
-
const def = this.getDefinition(coinId);
|
|
2892
|
-
return def?.decimals ?? 0;
|
|
2893
|
-
}
|
|
2894
|
-
/**
|
|
2895
|
-
* Get icon URL for a coin ID
|
|
2896
|
-
* @param coinId - 64-character hex string
|
|
2897
|
-
* @param preferPng - Prefer PNG format over SVG
|
|
2898
|
-
* @returns Icon URL or null if not found
|
|
2899
|
-
*/
|
|
2900
|
-
getIconUrl(coinId, preferPng = true) {
|
|
2901
|
-
const def = this.getDefinition(coinId);
|
|
2902
|
-
if (!def?.icons || def.icons.length === 0) {
|
|
2903
|
-
return null;
|
|
2904
|
-
}
|
|
2905
|
-
if (preferPng) {
|
|
2906
|
-
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
2907
|
-
if (pngIcon) return pngIcon.url;
|
|
2908
|
-
}
|
|
2909
|
-
return def.icons[0].url;
|
|
2910
|
-
}
|
|
2911
|
-
/**
|
|
2912
|
-
* Check if a coin ID is known in the registry
|
|
2913
|
-
* @param coinId - 64-character hex string
|
|
2914
|
-
* @returns true if the coin is in the registry
|
|
2915
|
-
*/
|
|
2916
|
-
isKnown(coinId) {
|
|
2917
|
-
return this.definitionsById.has(coinId.toLowerCase());
|
|
2918
|
-
}
|
|
2919
|
-
/**
|
|
2920
|
-
* Get all token definitions
|
|
2921
|
-
* @returns Array of all token definitions
|
|
2922
|
-
*/
|
|
2923
|
-
getAllDefinitions() {
|
|
2924
|
-
return Array.from(this.definitionsById.values());
|
|
2925
|
-
}
|
|
2926
|
-
/**
|
|
2927
|
-
* Get all fungible token definitions
|
|
2928
|
-
* @returns Array of fungible token definitions
|
|
2929
|
-
*/
|
|
2930
|
-
getFungibleTokens() {
|
|
2931
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
2932
|
-
}
|
|
2933
|
-
/**
|
|
2934
|
-
* Get all non-fungible token definitions
|
|
2935
|
-
* @returns Array of non-fungible token definitions
|
|
2936
|
-
*/
|
|
2937
|
-
getNonFungibleTokens() {
|
|
2938
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
2939
|
-
}
|
|
2940
|
-
/**
|
|
2941
|
-
* Get coin ID by symbol
|
|
2942
|
-
* @param symbol - Token symbol (e.g., "UCT")
|
|
2943
|
-
* @returns Coin ID hex string or undefined if not found
|
|
2944
|
-
*/
|
|
2945
|
-
getCoinIdBySymbol(symbol) {
|
|
2946
|
-
const def = this.getDefinitionBySymbol(symbol);
|
|
2947
|
-
return def?.id;
|
|
2948
|
-
}
|
|
2949
|
-
/**
|
|
2950
|
-
* Get coin ID by name
|
|
2951
|
-
* @param name - Token name (e.g., "bitcoin")
|
|
2952
|
-
* @returns Coin ID hex string or undefined if not found
|
|
2953
|
-
*/
|
|
2954
|
-
getCoinIdByName(name) {
|
|
2955
|
-
const def = this.getDefinitionByName(name);
|
|
2956
|
-
return def?.id;
|
|
2957
|
-
}
|
|
2958
|
-
};
|
|
2959
|
-
|
|
2960
3069
|
// modules/payments/InstantSplitExecutor.ts
|
|
2961
3070
|
import { Token as Token3 } from "@unicitylabs/state-transition-sdk/lib/token/Token";
|
|
2962
3071
|
import { TokenId as TokenId3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
@@ -3990,7 +4099,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3990
4099
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
3991
4100
|
forkedTokens = /* @__PURE__ */ new Map();
|
|
3992
4101
|
transactionHistory = [];
|
|
3993
|
-
|
|
4102
|
+
nametags = [];
|
|
3994
4103
|
// Payment Requests State (Incoming)
|
|
3995
4104
|
paymentRequests = [];
|
|
3996
4105
|
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
@@ -4059,7 +4168,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4059
4168
|
this.archivedTokens.clear();
|
|
4060
4169
|
this.forkedTokens.clear();
|
|
4061
4170
|
this.transactionHistory = [];
|
|
4062
|
-
this.
|
|
4171
|
+
this.nametags = [];
|
|
4063
4172
|
this.deps = deps;
|
|
4064
4173
|
this.priceProvider = deps.price ?? null;
|
|
4065
4174
|
if (this.l1) {
|
|
@@ -4108,8 +4217,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4108
4217
|
}
|
|
4109
4218
|
}
|
|
4110
4219
|
await this.loadPendingV5Tokens();
|
|
4111
|
-
await this.loadTokensFromFileStorage();
|
|
4112
|
-
await this.loadNametagFromFileStorage();
|
|
4113
4220
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
4114
4221
|
if (historyData) {
|
|
4115
4222
|
try {
|
|
@@ -4618,9 +4725,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4618
4725
|
senderPubkey,
|
|
4619
4726
|
{
|
|
4620
4727
|
findNametagToken: async (proxyAddress) => {
|
|
4621
|
-
|
|
4728
|
+
const currentNametag = this.getNametag();
|
|
4729
|
+
if (currentNametag?.token) {
|
|
4622
4730
|
try {
|
|
4623
|
-
const nametagToken = await SdkToken2.fromJSON(
|
|
4731
|
+
const nametagToken = await SdkToken2.fromJSON(currentNametag.token);
|
|
4624
4732
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
4625
4733
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
4626
4734
|
if (proxy.address === proxyAddress) {
|
|
@@ -5409,7 +5517,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5409
5517
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5410
5518
|
};
|
|
5411
5519
|
this.tokens.set(tokenId, confirmedToken);
|
|
5412
|
-
await this.saveTokenToFileStorage(confirmedToken);
|
|
5413
5520
|
await this.addToHistory({
|
|
5414
5521
|
type: "RECEIVED",
|
|
5415
5522
|
amount: confirmedToken.amount,
|
|
@@ -5498,9 +5605,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5498
5605
|
} catch {
|
|
5499
5606
|
}
|
|
5500
5607
|
}
|
|
5501
|
-
|
|
5608
|
+
const localNametag = this.getNametag();
|
|
5609
|
+
if (nametagTokens.length === 0 && localNametag?.token) {
|
|
5502
5610
|
try {
|
|
5503
|
-
const nametagToken = await SdkToken2.fromJSON(
|
|
5611
|
+
const nametagToken = await SdkToken2.fromJSON(localNametag.token);
|
|
5504
5612
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5505
5613
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5506
5614
|
if (proxy.address === recipientAddressStr) {
|
|
@@ -5661,125 +5769,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5661
5769
|
});
|
|
5662
5770
|
}
|
|
5663
5771
|
await this.save();
|
|
5664
|
-
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5665
|
-
await this.saveTokenToFileStorage(token);
|
|
5666
|
-
}
|
|
5667
5772
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5668
5773
|
return true;
|
|
5669
5774
|
}
|
|
5670
5775
|
/**
|
|
5671
|
-
*
|
|
5672
|
-
*
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5678
|
-
const tokenIdPrefix = sdkTokenId ? sdkTokenId.slice(0, 16) : token.id.slice(0, 16);
|
|
5679
|
-
const filename = `token-${tokenIdPrefix}-${Date.now()}`;
|
|
5680
|
-
const tokenData = {
|
|
5681
|
-
token: token.sdkData ? JSON.parse(token.sdkData) : null,
|
|
5682
|
-
receivedAt: Date.now(),
|
|
5683
|
-
meta: {
|
|
5684
|
-
id: token.id,
|
|
5685
|
-
coinId: token.coinId,
|
|
5686
|
-
symbol: token.symbol,
|
|
5687
|
-
amount: token.amount,
|
|
5688
|
-
status: token.status
|
|
5689
|
-
}
|
|
5690
|
-
};
|
|
5691
|
-
for (const [providerId, provider] of providers) {
|
|
5692
|
-
try {
|
|
5693
|
-
if (provider.saveToken) {
|
|
5694
|
-
await provider.saveToken(filename, tokenData);
|
|
5695
|
-
this.log(`Saved token file ${filename} to ${providerId}`);
|
|
5696
|
-
}
|
|
5697
|
-
} catch (error) {
|
|
5698
|
-
console.warn(`[Payments] Failed to save token to ${providerId}:`, error);
|
|
5699
|
-
}
|
|
5700
|
-
}
|
|
5701
|
-
}
|
|
5702
|
-
/**
|
|
5703
|
-
* Load tokens from file storage providers (lottery compatibility)
|
|
5704
|
-
* This loads tokens from file-based storage that may have been saved
|
|
5705
|
-
* by other applications using the same storage directory.
|
|
5706
|
-
*/
|
|
5707
|
-
async loadTokensFromFileStorage() {
|
|
5708
|
-
const providers = this.getTokenStorageProviders();
|
|
5709
|
-
if (providers.size === 0) return;
|
|
5710
|
-
for (const [providerId, provider] of providers) {
|
|
5711
|
-
if (!provider.listTokenIds || !provider.getToken) continue;
|
|
5712
|
-
try {
|
|
5713
|
-
const allIds = await provider.listTokenIds();
|
|
5714
|
-
const tokenIds = allIds.filter((id) => id.startsWith("token-"));
|
|
5715
|
-
this.log(`Found ${tokenIds.length} token files in ${providerId}`);
|
|
5716
|
-
for (const tokenId of tokenIds) {
|
|
5717
|
-
try {
|
|
5718
|
-
const fileData = await provider.getToken(tokenId);
|
|
5719
|
-
if (!fileData || typeof fileData !== "object") continue;
|
|
5720
|
-
const data = fileData;
|
|
5721
|
-
const tokenJson = data.token;
|
|
5722
|
-
if (!tokenJson) continue;
|
|
5723
|
-
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5724
|
-
continue;
|
|
5725
|
-
}
|
|
5726
|
-
let sdkTokenId;
|
|
5727
|
-
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5728
|
-
const tokenObj = tokenJson;
|
|
5729
|
-
const genesis = tokenObj.genesis;
|
|
5730
|
-
const genesisData = genesis?.data;
|
|
5731
|
-
sdkTokenId = genesisData?.tokenId;
|
|
5732
|
-
}
|
|
5733
|
-
if (sdkTokenId) {
|
|
5734
|
-
let exists = false;
|
|
5735
|
-
for (const existing of this.tokens.values()) {
|
|
5736
|
-
const existingId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5737
|
-
if (existingId === sdkTokenId) {
|
|
5738
|
-
exists = true;
|
|
5739
|
-
break;
|
|
5740
|
-
}
|
|
5741
|
-
}
|
|
5742
|
-
if (exists) continue;
|
|
5743
|
-
}
|
|
5744
|
-
const tokenInfo = await parseTokenInfo(tokenJson);
|
|
5745
|
-
const token = {
|
|
5746
|
-
id: tokenInfo.tokenId ?? tokenId,
|
|
5747
|
-
coinId: tokenInfo.coinId,
|
|
5748
|
-
symbol: tokenInfo.symbol,
|
|
5749
|
-
name: tokenInfo.name,
|
|
5750
|
-
decimals: tokenInfo.decimals,
|
|
5751
|
-
iconUrl: tokenInfo.iconUrl,
|
|
5752
|
-
amount: tokenInfo.amount,
|
|
5753
|
-
status: "confirmed",
|
|
5754
|
-
createdAt: data.receivedAt || Date.now(),
|
|
5755
|
-
updatedAt: Date.now(),
|
|
5756
|
-
sdkData: typeof tokenJson === "string" ? tokenJson : JSON.stringify(tokenJson)
|
|
5757
|
-
};
|
|
5758
|
-
const loadedTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5759
|
-
const loadedStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5760
|
-
if (loadedTokenId && loadedStateHash && this.isStateTombstoned(loadedTokenId, loadedStateHash)) {
|
|
5761
|
-
this.log(`Skipping tombstoned token file ${tokenId} (${loadedTokenId.slice(0, 8)}...)`);
|
|
5762
|
-
continue;
|
|
5763
|
-
}
|
|
5764
|
-
this.tokens.set(token.id, token);
|
|
5765
|
-
this.log(`Loaded token from file: ${tokenId}`);
|
|
5766
|
-
} catch (tokenError) {
|
|
5767
|
-
console.warn(`[Payments] Failed to load token ${tokenId}:`, tokenError);
|
|
5768
|
-
}
|
|
5769
|
-
}
|
|
5770
|
-
} catch (error) {
|
|
5771
|
-
console.warn(`[Payments] Failed to load tokens from ${providerId}:`, error);
|
|
5772
|
-
}
|
|
5773
|
-
}
|
|
5774
|
-
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5775
|
-
}
|
|
5776
|
-
/**
|
|
5777
|
-
* Update an existing token or add it if not found.
|
|
5778
|
-
*
|
|
5779
|
-
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5780
|
-
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5781
|
-
*
|
|
5782
|
-
* @param token - The token with updated data. Must include a valid `id`.
|
|
5776
|
+
* Update an existing token or add it if not found.
|
|
5777
|
+
*
|
|
5778
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5779
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5780
|
+
*
|
|
5781
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5783
5782
|
*/
|
|
5784
5783
|
async updateToken(token) {
|
|
5785
5784
|
this.ensureInitialized();
|
|
@@ -5831,7 +5830,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5831
5830
|
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
5832
5831
|
}
|
|
5833
5832
|
this.tokens.delete(tokenId);
|
|
5834
|
-
await this.deleteTokenFiles(token);
|
|
5835
5833
|
if (!skipHistory && token.coinId && token.amount) {
|
|
5836
5834
|
await this.addToHistory({
|
|
5837
5835
|
type: "SENT",
|
|
@@ -5844,31 +5842,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5844
5842
|
}
|
|
5845
5843
|
await this.save();
|
|
5846
5844
|
}
|
|
5847
|
-
/**
|
|
5848
|
-
* Delete physical token file(s) from all storage providers.
|
|
5849
|
-
* Finds files by matching the SDK token ID prefix in the filename.
|
|
5850
|
-
*/
|
|
5851
|
-
async deleteTokenFiles(token) {
|
|
5852
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5853
|
-
if (!sdkTokenId) return;
|
|
5854
|
-
const tokenIdPrefix = sdkTokenId.slice(0, 16);
|
|
5855
|
-
const providers = this.getTokenStorageProviders();
|
|
5856
|
-
for (const [providerId, provider] of providers) {
|
|
5857
|
-
if (!provider.listTokenIds || !provider.deleteToken) continue;
|
|
5858
|
-
try {
|
|
5859
|
-
const allIds = await provider.listTokenIds();
|
|
5860
|
-
const matchingFiles = allIds.filter(
|
|
5861
|
-
(id) => id.startsWith(`token-${tokenIdPrefix}`)
|
|
5862
|
-
);
|
|
5863
|
-
for (const fileId of matchingFiles) {
|
|
5864
|
-
await provider.deleteToken(fileId);
|
|
5865
|
-
this.log(`Deleted token file ${fileId} from ${providerId}`);
|
|
5866
|
-
}
|
|
5867
|
-
} catch (error) {
|
|
5868
|
-
console.warn(`[Payments] Failed to delete token files from ${providerId}:`, error);
|
|
5869
|
-
}
|
|
5870
|
-
}
|
|
5871
|
-
}
|
|
5872
5845
|
// ===========================================================================
|
|
5873
5846
|
// Public API - Tombstones
|
|
5874
5847
|
// ===========================================================================
|
|
@@ -6124,18 +6097,30 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6124
6097
|
*/
|
|
6125
6098
|
async setNametag(nametag) {
|
|
6126
6099
|
this.ensureInitialized();
|
|
6127
|
-
this.
|
|
6100
|
+
const idx = this.nametags.findIndex((n) => n.name === nametag.name);
|
|
6101
|
+
if (idx >= 0) {
|
|
6102
|
+
this.nametags[idx] = nametag;
|
|
6103
|
+
} else {
|
|
6104
|
+
this.nametags.push(nametag);
|
|
6105
|
+
}
|
|
6128
6106
|
await this.save();
|
|
6129
|
-
await this.saveNametagToFileStorage(nametag);
|
|
6130
6107
|
this.log(`Nametag set: ${nametag.name}`);
|
|
6131
6108
|
}
|
|
6132
6109
|
/**
|
|
6133
|
-
* Get the current nametag data.
|
|
6110
|
+
* Get the current (first) nametag data.
|
|
6134
6111
|
*
|
|
6135
6112
|
* @returns The nametag data, or `null` if no nametag is set.
|
|
6136
6113
|
*/
|
|
6137
6114
|
getNametag() {
|
|
6138
|
-
return this.
|
|
6115
|
+
return this.nametags[0] ?? null;
|
|
6116
|
+
}
|
|
6117
|
+
/**
|
|
6118
|
+
* Get all nametag data entries.
|
|
6119
|
+
*
|
|
6120
|
+
* @returns A copy of the nametags array.
|
|
6121
|
+
*/
|
|
6122
|
+
getNametags() {
|
|
6123
|
+
return [...this.nametags];
|
|
6139
6124
|
}
|
|
6140
6125
|
/**
|
|
6141
6126
|
* Check whether a nametag is currently set.
|
|
@@ -6143,77 +6128,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6143
6128
|
* @returns `true` if nametag data is present.
|
|
6144
6129
|
*/
|
|
6145
6130
|
hasNametag() {
|
|
6146
|
-
return this.
|
|
6131
|
+
return this.nametags.length > 0;
|
|
6147
6132
|
}
|
|
6148
6133
|
/**
|
|
6149
|
-
* Remove
|
|
6134
|
+
* Remove all nametag data from memory and storage.
|
|
6150
6135
|
*/
|
|
6151
6136
|
async clearNametag() {
|
|
6152
6137
|
this.ensureInitialized();
|
|
6153
|
-
this.
|
|
6138
|
+
this.nametags = [];
|
|
6154
6139
|
await this.save();
|
|
6155
6140
|
}
|
|
6156
|
-
/**
|
|
6157
|
-
* Save nametag to file storage for lottery compatibility
|
|
6158
|
-
* Creates file: nametag-{name}.json
|
|
6159
|
-
*/
|
|
6160
|
-
async saveNametagToFileStorage(nametag) {
|
|
6161
|
-
const providers = this.getTokenStorageProviders();
|
|
6162
|
-
if (providers.size === 0) return;
|
|
6163
|
-
const filename = `nametag-${nametag.name}`;
|
|
6164
|
-
const fileData = {
|
|
6165
|
-
nametag: nametag.name,
|
|
6166
|
-
token: nametag.token,
|
|
6167
|
-
timestamp: nametag.timestamp || Date.now()
|
|
6168
|
-
};
|
|
6169
|
-
for (const [providerId, provider] of providers) {
|
|
6170
|
-
try {
|
|
6171
|
-
if (provider.saveToken) {
|
|
6172
|
-
await provider.saveToken(filename, fileData);
|
|
6173
|
-
this.log(`Saved nametag file ${filename} to ${providerId}`);
|
|
6174
|
-
}
|
|
6175
|
-
} catch (error) {
|
|
6176
|
-
console.warn(`[Payments] Failed to save nametag to ${providerId}:`, error);
|
|
6177
|
-
}
|
|
6178
|
-
}
|
|
6179
|
-
}
|
|
6180
|
-
/**
|
|
6181
|
-
* Load nametag from file storage (lottery compatibility)
|
|
6182
|
-
* Looks for file: nametag-{name}.json
|
|
6183
|
-
*/
|
|
6184
|
-
async loadNametagFromFileStorage() {
|
|
6185
|
-
if (this.nametag) return;
|
|
6186
|
-
const providers = this.getTokenStorageProviders();
|
|
6187
|
-
if (providers.size === 0) return;
|
|
6188
|
-
for (const [providerId, provider] of providers) {
|
|
6189
|
-
if (!provider.listTokenIds || !provider.getToken) continue;
|
|
6190
|
-
try {
|
|
6191
|
-
const tokenIds = await provider.listTokenIds();
|
|
6192
|
-
const nametagFiles = tokenIds.filter((id) => id.startsWith("nametag-"));
|
|
6193
|
-
for (const nametagFile of nametagFiles) {
|
|
6194
|
-
try {
|
|
6195
|
-
const fileData = await provider.getToken(nametagFile);
|
|
6196
|
-
if (!fileData || typeof fileData !== "object") continue;
|
|
6197
|
-
const data = fileData;
|
|
6198
|
-
if (!data.token || !data.nametag) continue;
|
|
6199
|
-
this.nametag = {
|
|
6200
|
-
name: data.nametag,
|
|
6201
|
-
token: data.token,
|
|
6202
|
-
timestamp: data.timestamp || Date.now(),
|
|
6203
|
-
format: "lottery",
|
|
6204
|
-
version: "1.0"
|
|
6205
|
-
};
|
|
6206
|
-
this.log(`Loaded nametag from file: ${nametagFile}`);
|
|
6207
|
-
return;
|
|
6208
|
-
} catch (fileError) {
|
|
6209
|
-
console.warn(`[Payments] Failed to load nametag file ${nametagFile}:`, fileError);
|
|
6210
|
-
}
|
|
6211
|
-
}
|
|
6212
|
-
} catch (error) {
|
|
6213
|
-
console.warn(`[Payments] Failed to search nametag files in ${providerId}:`, error);
|
|
6214
|
-
}
|
|
6215
|
-
}
|
|
6216
|
-
}
|
|
6217
6141
|
/**
|
|
6218
6142
|
* Mint a nametag token on-chain (like Sphere wallet and lottery)
|
|
6219
6143
|
* This creates the nametag token required for receiving tokens via PROXY addresses
|
|
@@ -6701,10 +6625,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6701
6625
|
let nametagTokens = [];
|
|
6702
6626
|
if (addressScheme === AddressScheme.PROXY) {
|
|
6703
6627
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
6704
|
-
|
|
6628
|
+
const proxyNametag = this.getNametag();
|
|
6629
|
+
if (!proxyNametag?.token) {
|
|
6705
6630
|
throw new Error("Cannot finalize PROXY transfer - no nametag token");
|
|
6706
6631
|
}
|
|
6707
|
-
const nametagToken = await SdkToken2.fromJSON(
|
|
6632
|
+
const nametagToken = await SdkToken2.fromJSON(proxyNametag.token);
|
|
6708
6633
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
6709
6634
|
if (proxy.address !== recipientAddress.address) {
|
|
6710
6635
|
throw new Error(
|
|
@@ -6765,7 +6690,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6765
6690
|
};
|
|
6766
6691
|
this.tokens.set(tokenId, finalizedToken);
|
|
6767
6692
|
await this.save();
|
|
6768
|
-
await this.saveTokenToFileStorage(finalizedToken);
|
|
6769
6693
|
this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
|
|
6770
6694
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6771
6695
|
id: crypto.randomUUID(),
|
|
@@ -6809,9 +6733,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6809
6733
|
if (instantBundle) {
|
|
6810
6734
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6811
6735
|
try {
|
|
6812
|
-
if (!this.nametag) {
|
|
6813
|
-
await this.loadNametagFromFileStorage();
|
|
6814
|
-
}
|
|
6815
6736
|
const result = await this.processInstantSplitBundle(
|
|
6816
6737
|
instantBundle,
|
|
6817
6738
|
transfer.senderTransportPubkey
|
|
@@ -7011,6 +6932,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7011
6932
|
ipnsName: this.deps.identity.ipnsName ?? ""
|
|
7012
6933
|
},
|
|
7013
6934
|
{
|
|
6935
|
+
nametags: this.nametags,
|
|
7014
6936
|
tombstones: this.tombstones,
|
|
7015
6937
|
archivedTokens: this.archivedTokens,
|
|
7016
6938
|
forkedTokens: this.forkedTokens
|
|
@@ -7032,9 +6954,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7032
6954
|
}
|
|
7033
6955
|
this.archivedTokens = parsed.archivedTokens;
|
|
7034
6956
|
this.forkedTokens = parsed.forkedTokens;
|
|
7035
|
-
|
|
7036
|
-
this.nametag = parsed.nametag;
|
|
7037
|
-
}
|
|
6957
|
+
this.nametags = parsed.nametags;
|
|
7038
6958
|
}
|
|
7039
6959
|
// ===========================================================================
|
|
7040
6960
|
// Private: NOSTR-FIRST Proof Polling
|
|
@@ -7252,228 +7172,1609 @@ var CommunicationsModule = class {
|
|
|
7252
7172
|
this.broadcastSubscriptions.clear();
|
|
7253
7173
|
}
|
|
7254
7174
|
// ===========================================================================
|
|
7255
|
-
// Public API - Direct Messages
|
|
7175
|
+
// Public API - Direct Messages
|
|
7176
|
+
// ===========================================================================
|
|
7177
|
+
/**
|
|
7178
|
+
* Send direct message
|
|
7179
|
+
*/
|
|
7180
|
+
async sendDM(recipient, content) {
|
|
7181
|
+
this.ensureInitialized();
|
|
7182
|
+
const recipientPubkey = await this.resolveRecipient(recipient);
|
|
7183
|
+
const eventId = await this.deps.transport.sendMessage(recipientPubkey, content);
|
|
7184
|
+
const message = {
|
|
7185
|
+
id: eventId,
|
|
7186
|
+
senderPubkey: this.deps.identity.chainPubkey,
|
|
7187
|
+
senderNametag: this.deps.identity.nametag,
|
|
7188
|
+
recipientPubkey,
|
|
7189
|
+
content,
|
|
7190
|
+
timestamp: Date.now(),
|
|
7191
|
+
isRead: true
|
|
7192
|
+
};
|
|
7193
|
+
this.messages.set(message.id, message);
|
|
7194
|
+
if (this.config.autoSave) {
|
|
7195
|
+
await this.save();
|
|
7196
|
+
}
|
|
7197
|
+
return message;
|
|
7198
|
+
}
|
|
7199
|
+
/**
|
|
7200
|
+
* Get conversation with peer
|
|
7201
|
+
*/
|
|
7202
|
+
getConversation(peerPubkey) {
|
|
7203
|
+
return Array.from(this.messages.values()).filter(
|
|
7204
|
+
(m) => m.senderPubkey === peerPubkey || m.recipientPubkey === peerPubkey
|
|
7205
|
+
).sort((a, b) => a.timestamp - b.timestamp);
|
|
7206
|
+
}
|
|
7207
|
+
/**
|
|
7208
|
+
* Get all conversations grouped by peer
|
|
7209
|
+
*/
|
|
7210
|
+
getConversations() {
|
|
7211
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
7212
|
+
for (const message of this.messages.values()) {
|
|
7213
|
+
const peer = message.senderPubkey === this.deps?.identity.chainPubkey ? message.recipientPubkey : message.senderPubkey;
|
|
7214
|
+
if (!conversations.has(peer)) {
|
|
7215
|
+
conversations.set(peer, []);
|
|
7216
|
+
}
|
|
7217
|
+
conversations.get(peer).push(message);
|
|
7218
|
+
}
|
|
7219
|
+
for (const msgs of conversations.values()) {
|
|
7220
|
+
msgs.sort((a, b) => a.timestamp - b.timestamp);
|
|
7221
|
+
}
|
|
7222
|
+
return conversations;
|
|
7223
|
+
}
|
|
7224
|
+
/**
|
|
7225
|
+
* Mark messages as read
|
|
7226
|
+
*/
|
|
7227
|
+
async markAsRead(messageIds) {
|
|
7228
|
+
for (const id of messageIds) {
|
|
7229
|
+
const msg = this.messages.get(id);
|
|
7230
|
+
if (msg) {
|
|
7231
|
+
msg.isRead = true;
|
|
7232
|
+
}
|
|
7233
|
+
}
|
|
7234
|
+
if (this.config.autoSave) {
|
|
7235
|
+
await this.save();
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
/**
|
|
7239
|
+
* Get unread count
|
|
7240
|
+
*/
|
|
7241
|
+
getUnreadCount(peerPubkey) {
|
|
7242
|
+
let messages = Array.from(this.messages.values()).filter(
|
|
7243
|
+
(m) => !m.isRead && m.senderPubkey !== this.deps?.identity.chainPubkey
|
|
7244
|
+
);
|
|
7245
|
+
if (peerPubkey) {
|
|
7246
|
+
messages = messages.filter((m) => m.senderPubkey === peerPubkey);
|
|
7247
|
+
}
|
|
7248
|
+
return messages.length;
|
|
7249
|
+
}
|
|
7250
|
+
/**
|
|
7251
|
+
* Subscribe to incoming DMs
|
|
7252
|
+
*/
|
|
7253
|
+
onDirectMessage(handler) {
|
|
7254
|
+
this.dmHandlers.add(handler);
|
|
7255
|
+
return () => this.dmHandlers.delete(handler);
|
|
7256
|
+
}
|
|
7257
|
+
// ===========================================================================
|
|
7258
|
+
// Public API - Broadcasts
|
|
7259
|
+
// ===========================================================================
|
|
7260
|
+
/**
|
|
7261
|
+
* Publish broadcast message
|
|
7262
|
+
*/
|
|
7263
|
+
async broadcast(content, tags) {
|
|
7264
|
+
this.ensureInitialized();
|
|
7265
|
+
const eventId = await this.deps.transport.publishBroadcast?.(content, tags);
|
|
7266
|
+
const message = {
|
|
7267
|
+
id: eventId ?? crypto.randomUUID(),
|
|
7268
|
+
authorPubkey: this.deps.identity.chainPubkey,
|
|
7269
|
+
authorNametag: this.deps.identity.nametag,
|
|
7270
|
+
content,
|
|
7271
|
+
timestamp: Date.now(),
|
|
7272
|
+
tags
|
|
7273
|
+
};
|
|
7274
|
+
this.broadcasts.set(message.id, message);
|
|
7275
|
+
return message;
|
|
7276
|
+
}
|
|
7277
|
+
/**
|
|
7278
|
+
* Subscribe to broadcasts with tags
|
|
7279
|
+
*/
|
|
7280
|
+
subscribeToBroadcasts(tags) {
|
|
7281
|
+
this.ensureInitialized();
|
|
7282
|
+
const key = tags.sort().join(":");
|
|
7283
|
+
if (this.broadcastSubscriptions.has(key)) {
|
|
7284
|
+
return () => {
|
|
7285
|
+
};
|
|
7286
|
+
}
|
|
7287
|
+
const unsub = this.deps.transport.subscribeToBroadcast?.(tags, (broadcast2) => {
|
|
7288
|
+
this.handleIncomingBroadcast(broadcast2);
|
|
7289
|
+
});
|
|
7290
|
+
if (unsub) {
|
|
7291
|
+
this.broadcastSubscriptions.set(key, unsub);
|
|
7292
|
+
}
|
|
7293
|
+
return () => {
|
|
7294
|
+
const sub = this.broadcastSubscriptions.get(key);
|
|
7295
|
+
if (sub) {
|
|
7296
|
+
sub();
|
|
7297
|
+
this.broadcastSubscriptions.delete(key);
|
|
7298
|
+
}
|
|
7299
|
+
};
|
|
7300
|
+
}
|
|
7301
|
+
/**
|
|
7302
|
+
* Get broadcasts
|
|
7303
|
+
*/
|
|
7304
|
+
getBroadcasts(limit) {
|
|
7305
|
+
const messages = Array.from(this.broadcasts.values()).sort((a, b) => b.timestamp - a.timestamp);
|
|
7306
|
+
return limit ? messages.slice(0, limit) : messages;
|
|
7307
|
+
}
|
|
7308
|
+
/**
|
|
7309
|
+
* Subscribe to incoming broadcasts
|
|
7310
|
+
*/
|
|
7311
|
+
onBroadcast(handler) {
|
|
7312
|
+
this.broadcastHandlers.add(handler);
|
|
7313
|
+
return () => this.broadcastHandlers.delete(handler);
|
|
7314
|
+
}
|
|
7315
|
+
// ===========================================================================
|
|
7316
|
+
// Private: Message Handling
|
|
7317
|
+
// ===========================================================================
|
|
7318
|
+
handleIncomingMessage(msg) {
|
|
7319
|
+
if (msg.senderTransportPubkey === this.deps?.identity.chainPubkey) return;
|
|
7320
|
+
const message = {
|
|
7321
|
+
id: msg.id,
|
|
7322
|
+
senderPubkey: msg.senderTransportPubkey,
|
|
7323
|
+
senderNametag: msg.senderNametag,
|
|
7324
|
+
recipientPubkey: this.deps.identity.chainPubkey,
|
|
7325
|
+
content: msg.content,
|
|
7326
|
+
timestamp: msg.timestamp,
|
|
7327
|
+
isRead: false
|
|
7328
|
+
};
|
|
7329
|
+
this.messages.set(message.id, message);
|
|
7330
|
+
this.deps.emitEvent("message:dm", message);
|
|
7331
|
+
for (const handler of this.dmHandlers) {
|
|
7332
|
+
try {
|
|
7333
|
+
handler(message);
|
|
7334
|
+
} catch (error) {
|
|
7335
|
+
console.error("[Communications] Handler error:", error);
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
if (this.config.autoSave) {
|
|
7339
|
+
this.save();
|
|
7340
|
+
}
|
|
7341
|
+
this.pruneIfNeeded();
|
|
7342
|
+
}
|
|
7343
|
+
handleIncomingBroadcast(incoming) {
|
|
7344
|
+
const message = {
|
|
7345
|
+
id: incoming.id,
|
|
7346
|
+
authorPubkey: incoming.authorTransportPubkey,
|
|
7347
|
+
content: incoming.content,
|
|
7348
|
+
timestamp: incoming.timestamp,
|
|
7349
|
+
tags: incoming.tags
|
|
7350
|
+
};
|
|
7351
|
+
this.broadcasts.set(message.id, message);
|
|
7352
|
+
this.deps.emitEvent("message:broadcast", message);
|
|
7353
|
+
for (const handler of this.broadcastHandlers) {
|
|
7354
|
+
try {
|
|
7355
|
+
handler(message);
|
|
7356
|
+
} catch (error) {
|
|
7357
|
+
console.error("[Communications] Handler error:", error);
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
}
|
|
7361
|
+
// ===========================================================================
|
|
7362
|
+
// Private: Storage
|
|
7363
|
+
// ===========================================================================
|
|
7364
|
+
async save() {
|
|
7365
|
+
const messages = Array.from(this.messages.values());
|
|
7366
|
+
await this.deps.storage.set("direct_messages", JSON.stringify(messages));
|
|
7367
|
+
}
|
|
7368
|
+
pruneIfNeeded() {
|
|
7369
|
+
if (this.messages.size <= this.config.maxMessages) return;
|
|
7370
|
+
const sorted = Array.from(this.messages.entries()).sort(([, a], [, b]) => a.timestamp - b.timestamp);
|
|
7371
|
+
const toRemove = sorted.slice(0, sorted.length - this.config.maxMessages);
|
|
7372
|
+
for (const [id] of toRemove) {
|
|
7373
|
+
this.messages.delete(id);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
// ===========================================================================
|
|
7377
|
+
// Private: Helpers
|
|
7378
|
+
// ===========================================================================
|
|
7379
|
+
async resolveRecipient(recipient) {
|
|
7380
|
+
if (recipient.startsWith("@")) {
|
|
7381
|
+
const pubkey = await this.deps.transport.resolveNametag?.(recipient.slice(1));
|
|
7382
|
+
if (!pubkey) {
|
|
7383
|
+
throw new Error(`Nametag not found: ${recipient}`);
|
|
7384
|
+
}
|
|
7385
|
+
return pubkey;
|
|
7386
|
+
}
|
|
7387
|
+
return recipient;
|
|
7388
|
+
}
|
|
7389
|
+
ensureInitialized() {
|
|
7390
|
+
if (!this.deps) {
|
|
7391
|
+
throw new Error("CommunicationsModule not initialized");
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
};
|
|
7395
|
+
function createCommunicationsModule(config) {
|
|
7396
|
+
return new CommunicationsModule(config);
|
|
7397
|
+
}
|
|
7398
|
+
|
|
7399
|
+
// modules/groupchat/GroupChatModule.ts
|
|
7400
|
+
import {
|
|
7401
|
+
NostrClient,
|
|
7402
|
+
NostrKeyManager,
|
|
7403
|
+
Filter
|
|
7404
|
+
} from "@unicitylabs/nostr-js-sdk";
|
|
7405
|
+
|
|
7406
|
+
// modules/groupchat/types.ts
|
|
7407
|
+
var GroupRole = {
|
|
7408
|
+
ADMIN: "ADMIN",
|
|
7409
|
+
MODERATOR: "MODERATOR",
|
|
7410
|
+
MEMBER: "MEMBER"
|
|
7411
|
+
};
|
|
7412
|
+
var GroupVisibility = {
|
|
7413
|
+
PUBLIC: "PUBLIC",
|
|
7414
|
+
PRIVATE: "PRIVATE"
|
|
7415
|
+
};
|
|
7416
|
+
|
|
7417
|
+
// modules/groupchat/GroupChatModule.ts
|
|
7418
|
+
function createNip29Filter(data) {
|
|
7419
|
+
return new Filter(data);
|
|
7420
|
+
}
|
|
7421
|
+
var GroupChatModule = class {
|
|
7422
|
+
config;
|
|
7423
|
+
deps = null;
|
|
7424
|
+
// Nostr connection (separate from wallet relay)
|
|
7425
|
+
client = null;
|
|
7426
|
+
keyManager = null;
|
|
7427
|
+
connected = false;
|
|
7428
|
+
connecting = false;
|
|
7429
|
+
connectPromise = null;
|
|
7430
|
+
reconnectAttempts = 0;
|
|
7431
|
+
reconnectTimer = null;
|
|
7432
|
+
// Subscription tracking (for cleanup)
|
|
7433
|
+
subscriptionIds = [];
|
|
7434
|
+
// In-memory state
|
|
7435
|
+
groups = /* @__PURE__ */ new Map();
|
|
7436
|
+
messages = /* @__PURE__ */ new Map();
|
|
7437
|
+
// groupId -> messages
|
|
7438
|
+
members = /* @__PURE__ */ new Map();
|
|
7439
|
+
// groupId -> members
|
|
7440
|
+
processedEventIds = /* @__PURE__ */ new Set();
|
|
7441
|
+
pendingLeaves = /* @__PURE__ */ new Set();
|
|
7442
|
+
// Persistence debounce
|
|
7443
|
+
persistTimer = null;
|
|
7444
|
+
persistPromise = null;
|
|
7445
|
+
// Relay admin cache
|
|
7446
|
+
relayAdminPubkeys = null;
|
|
7447
|
+
relayAdminFetchPromise = null;
|
|
7448
|
+
// Listeners
|
|
7449
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
7450
|
+
constructor(config) {
|
|
7451
|
+
this.config = {
|
|
7452
|
+
relays: config?.relays ?? [],
|
|
7453
|
+
defaultMessageLimit: config?.defaultMessageLimit ?? 50,
|
|
7454
|
+
maxPreviousTags: config?.maxPreviousTags ?? 3,
|
|
7455
|
+
reconnectDelayMs: config?.reconnectDelayMs ?? 3e3,
|
|
7456
|
+
maxReconnectAttempts: config?.maxReconnectAttempts ?? 5
|
|
7457
|
+
};
|
|
7458
|
+
}
|
|
7459
|
+
// ===========================================================================
|
|
7460
|
+
// Lifecycle
|
|
7461
|
+
// ===========================================================================
|
|
7462
|
+
initialize(deps) {
|
|
7463
|
+
if (this.deps) {
|
|
7464
|
+
this.destroyConnection();
|
|
7465
|
+
}
|
|
7466
|
+
this.deps = deps;
|
|
7467
|
+
const secretKey = Buffer.from(deps.identity.privateKey, "hex");
|
|
7468
|
+
this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
|
|
7469
|
+
}
|
|
7470
|
+
async load() {
|
|
7471
|
+
this.ensureInitialized();
|
|
7472
|
+
const storage = this.deps.storage;
|
|
7473
|
+
const groupsJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_GROUPS);
|
|
7474
|
+
if (groupsJson) {
|
|
7475
|
+
try {
|
|
7476
|
+
const parsed = JSON.parse(groupsJson);
|
|
7477
|
+
this.groups.clear();
|
|
7478
|
+
for (const g of parsed) {
|
|
7479
|
+
this.groups.set(g.id, g);
|
|
7480
|
+
}
|
|
7481
|
+
} catch {
|
|
7482
|
+
}
|
|
7483
|
+
}
|
|
7484
|
+
const messagesJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MESSAGES);
|
|
7485
|
+
if (messagesJson) {
|
|
7486
|
+
try {
|
|
7487
|
+
const parsed = JSON.parse(messagesJson);
|
|
7488
|
+
this.messages.clear();
|
|
7489
|
+
for (const m of parsed) {
|
|
7490
|
+
const groupId = m.groupId;
|
|
7491
|
+
if (!this.messages.has(groupId)) {
|
|
7492
|
+
this.messages.set(groupId, []);
|
|
7493
|
+
}
|
|
7494
|
+
this.messages.get(groupId).push(m);
|
|
7495
|
+
}
|
|
7496
|
+
} catch {
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
const membersJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MEMBERS);
|
|
7500
|
+
if (membersJson) {
|
|
7501
|
+
try {
|
|
7502
|
+
const parsed = JSON.parse(membersJson);
|
|
7503
|
+
this.members.clear();
|
|
7504
|
+
for (const m of parsed) {
|
|
7505
|
+
const groupId = m.groupId;
|
|
7506
|
+
if (!this.members.has(groupId)) {
|
|
7507
|
+
this.members.set(groupId, []);
|
|
7508
|
+
}
|
|
7509
|
+
this.members.get(groupId).push(m);
|
|
7510
|
+
}
|
|
7511
|
+
} catch {
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
const processedJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_PROCESSED_EVENTS);
|
|
7515
|
+
if (processedJson) {
|
|
7516
|
+
try {
|
|
7517
|
+
const parsed = JSON.parse(processedJson);
|
|
7518
|
+
this.processedEventIds = new Set(parsed);
|
|
7519
|
+
} catch {
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
}
|
|
7523
|
+
destroy() {
|
|
7524
|
+
this.destroyConnection();
|
|
7525
|
+
this.groups.clear();
|
|
7526
|
+
this.messages.clear();
|
|
7527
|
+
this.members.clear();
|
|
7528
|
+
this.processedEventIds.clear();
|
|
7529
|
+
this.pendingLeaves.clear();
|
|
7530
|
+
this.messageHandlers.clear();
|
|
7531
|
+
this.relayAdminPubkeys = null;
|
|
7532
|
+
this.relayAdminFetchPromise = null;
|
|
7533
|
+
if (this.persistTimer) {
|
|
7534
|
+
clearTimeout(this.persistTimer);
|
|
7535
|
+
this.persistTimer = null;
|
|
7536
|
+
}
|
|
7537
|
+
this.deps = null;
|
|
7538
|
+
}
|
|
7539
|
+
destroyConnection() {
|
|
7540
|
+
if (this.reconnectTimer) {
|
|
7541
|
+
clearTimeout(this.reconnectTimer);
|
|
7542
|
+
this.reconnectTimer = null;
|
|
7543
|
+
}
|
|
7544
|
+
if (this.client) {
|
|
7545
|
+
for (const subId of this.subscriptionIds) {
|
|
7546
|
+
try {
|
|
7547
|
+
this.client.unsubscribe(subId);
|
|
7548
|
+
} catch {
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
this.subscriptionIds = [];
|
|
7552
|
+
try {
|
|
7553
|
+
this.client.disconnect();
|
|
7554
|
+
} catch {
|
|
7555
|
+
}
|
|
7556
|
+
this.client = null;
|
|
7557
|
+
}
|
|
7558
|
+
this.connected = false;
|
|
7559
|
+
this.connecting = false;
|
|
7560
|
+
this.connectPromise = null;
|
|
7561
|
+
this.reconnectAttempts = 0;
|
|
7562
|
+
this.keyManager = null;
|
|
7563
|
+
}
|
|
7564
|
+
// ===========================================================================
|
|
7565
|
+
// Connection
|
|
7566
|
+
// ===========================================================================
|
|
7567
|
+
async connect() {
|
|
7568
|
+
if (this.connected) return;
|
|
7569
|
+
if (this.connectPromise) {
|
|
7570
|
+
return this.connectPromise;
|
|
7571
|
+
}
|
|
7572
|
+
this.connecting = true;
|
|
7573
|
+
this.connectPromise = this.doConnect().finally(() => {
|
|
7574
|
+
this.connecting = false;
|
|
7575
|
+
this.connectPromise = null;
|
|
7576
|
+
});
|
|
7577
|
+
return this.connectPromise;
|
|
7578
|
+
}
|
|
7579
|
+
getConnectionStatus() {
|
|
7580
|
+
return this.connected;
|
|
7581
|
+
}
|
|
7582
|
+
async doConnect() {
|
|
7583
|
+
this.ensureInitialized();
|
|
7584
|
+
if (!this.keyManager) {
|
|
7585
|
+
const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
|
|
7586
|
+
this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
|
|
7587
|
+
}
|
|
7588
|
+
const primaryRelay = this.config.relays[0];
|
|
7589
|
+
if (primaryRelay) {
|
|
7590
|
+
await this.checkAndClearOnRelayChange(primaryRelay);
|
|
7591
|
+
}
|
|
7592
|
+
this.client = new NostrClient(this.keyManager);
|
|
7593
|
+
try {
|
|
7594
|
+
await this.client.connect(...this.config.relays);
|
|
7595
|
+
this.connected = true;
|
|
7596
|
+
this.reconnectAttempts = 0;
|
|
7597
|
+
this.deps.emitEvent("groupchat:connection", { connected: true });
|
|
7598
|
+
if (this.groups.size === 0) {
|
|
7599
|
+
await this.restoreJoinedGroups();
|
|
7600
|
+
} else {
|
|
7601
|
+
await this.subscribeToJoinedGroups();
|
|
7602
|
+
}
|
|
7603
|
+
} catch (error) {
|
|
7604
|
+
console.error("[GroupChat] Failed to connect to relays", error);
|
|
7605
|
+
this.deps.emitEvent("groupchat:connection", { connected: false });
|
|
7606
|
+
this.scheduleReconnect();
|
|
7607
|
+
}
|
|
7608
|
+
}
|
|
7609
|
+
scheduleReconnect() {
|
|
7610
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
7611
|
+
console.error("[GroupChat] Max reconnection attempts reached");
|
|
7612
|
+
return;
|
|
7613
|
+
}
|
|
7614
|
+
this.reconnectAttempts++;
|
|
7615
|
+
this.reconnectTimer = setTimeout(() => {
|
|
7616
|
+
this.reconnectTimer = null;
|
|
7617
|
+
if (this.deps) {
|
|
7618
|
+
this.connect().catch(console.error);
|
|
7619
|
+
}
|
|
7620
|
+
}, this.config.reconnectDelayMs);
|
|
7621
|
+
}
|
|
7622
|
+
// ===========================================================================
|
|
7623
|
+
// Subscription Management
|
|
7624
|
+
// ===========================================================================
|
|
7625
|
+
async subscribeToJoinedGroups() {
|
|
7626
|
+
if (!this.client) return;
|
|
7627
|
+
const groupIds = Array.from(this.groups.keys());
|
|
7628
|
+
if (groupIds.length === 0) return;
|
|
7629
|
+
this.trackSubscription(
|
|
7630
|
+
createNip29Filter({
|
|
7631
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
7632
|
+
"#h": groupIds
|
|
7633
|
+
}),
|
|
7634
|
+
{ onEvent: (event) => this.handleGroupEvent(event) }
|
|
7635
|
+
);
|
|
7636
|
+
this.trackSubscription(
|
|
7637
|
+
createNip29Filter({
|
|
7638
|
+
kinds: [NIP29_KINDS.GROUP_METADATA, NIP29_KINDS.GROUP_MEMBERS, NIP29_KINDS.GROUP_ADMINS],
|
|
7639
|
+
"#d": groupIds
|
|
7640
|
+
}),
|
|
7641
|
+
{ onEvent: (event) => this.handleMetadataEvent(event) }
|
|
7642
|
+
);
|
|
7643
|
+
this.trackSubscription(
|
|
7644
|
+
createNip29Filter({
|
|
7645
|
+
kinds: [NIP29_KINDS.DELETE_EVENT, NIP29_KINDS.REMOVE_USER, NIP29_KINDS.DELETE_GROUP],
|
|
7646
|
+
"#h": groupIds
|
|
7647
|
+
}),
|
|
7648
|
+
{ onEvent: (event) => this.handleModerationEvent(event) }
|
|
7649
|
+
);
|
|
7650
|
+
}
|
|
7651
|
+
subscribeToGroup(groupId) {
|
|
7652
|
+
if (!this.client) return;
|
|
7653
|
+
this.trackSubscription(
|
|
7654
|
+
createNip29Filter({
|
|
7655
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
7656
|
+
"#h": [groupId]
|
|
7657
|
+
}),
|
|
7658
|
+
{ onEvent: (event) => this.handleGroupEvent(event) }
|
|
7659
|
+
);
|
|
7660
|
+
this.trackSubscription(
|
|
7661
|
+
createNip29Filter({
|
|
7662
|
+
kinds: [NIP29_KINDS.DELETE_EVENT, NIP29_KINDS.REMOVE_USER, NIP29_KINDS.DELETE_GROUP],
|
|
7663
|
+
"#h": [groupId]
|
|
7664
|
+
}),
|
|
7665
|
+
{ onEvent: (event) => this.handleModerationEvent(event) }
|
|
7666
|
+
);
|
|
7667
|
+
}
|
|
7668
|
+
// ===========================================================================
|
|
7669
|
+
// Event Handlers
|
|
7670
|
+
// ===========================================================================
|
|
7671
|
+
handleGroupEvent(event) {
|
|
7672
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7673
|
+
const groupId = this.getGroupIdFromEvent(event);
|
|
7674
|
+
if (!groupId) return;
|
|
7675
|
+
const group = this.groups.get(groupId);
|
|
7676
|
+
if (!group) return;
|
|
7677
|
+
const { text: content, senderNametag } = this.unwrapMessageContent(event.content);
|
|
7678
|
+
const message = {
|
|
7679
|
+
id: event.id,
|
|
7680
|
+
groupId,
|
|
7681
|
+
content,
|
|
7682
|
+
timestamp: event.created_at * 1e3,
|
|
7683
|
+
senderPubkey: event.pubkey,
|
|
7684
|
+
senderNametag: senderNametag || void 0,
|
|
7685
|
+
replyToId: this.extractReplyTo(event),
|
|
7686
|
+
previousIds: this.extractPreviousIds(event)
|
|
7687
|
+
};
|
|
7688
|
+
this.saveMessageToMemory(message);
|
|
7689
|
+
this.addProcessedEventId(event.id);
|
|
7690
|
+
if (senderNametag) {
|
|
7691
|
+
this.updateMemberNametag(groupId, event.pubkey, senderNametag, event.created_at * 1e3);
|
|
7692
|
+
}
|
|
7693
|
+
this.updateGroupLastMessage(groupId, content.slice(0, 100), message.timestamp);
|
|
7694
|
+
const myPubkey = this.getMyPublicKey();
|
|
7695
|
+
if (event.pubkey !== myPubkey) {
|
|
7696
|
+
group.unreadCount = (group.unreadCount || 0) + 1;
|
|
7697
|
+
}
|
|
7698
|
+
this.deps.emitEvent("groupchat:message", message);
|
|
7699
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7700
|
+
for (const handler of this.messageHandlers) {
|
|
7701
|
+
try {
|
|
7702
|
+
handler(message);
|
|
7703
|
+
} catch {
|
|
7704
|
+
}
|
|
7705
|
+
}
|
|
7706
|
+
this.schedulePersist();
|
|
7707
|
+
}
|
|
7708
|
+
handleMetadataEvent(event) {
|
|
7709
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7710
|
+
if (!groupId) return;
|
|
7711
|
+
const group = this.groups.get(groupId);
|
|
7712
|
+
if (!group) return;
|
|
7713
|
+
if (event.kind === NIP29_KINDS.GROUP_METADATA) {
|
|
7714
|
+
if (!event.content || event.content.trim() === "") return;
|
|
7715
|
+
try {
|
|
7716
|
+
const metadata = JSON.parse(event.content);
|
|
7717
|
+
group.name = metadata.name || group.name;
|
|
7718
|
+
group.description = metadata.about || group.description;
|
|
7719
|
+
group.picture = metadata.picture || group.picture;
|
|
7720
|
+
group.updatedAt = event.created_at * 1e3;
|
|
7721
|
+
this.groups.set(groupId, group);
|
|
7722
|
+
this.persistGroups();
|
|
7723
|
+
} catch {
|
|
7724
|
+
}
|
|
7725
|
+
} else if (event.kind === NIP29_KINDS.GROUP_MEMBERS) {
|
|
7726
|
+
this.updateMembersFromEvent(groupId, event);
|
|
7727
|
+
} else if (event.kind === NIP29_KINDS.GROUP_ADMINS) {
|
|
7728
|
+
this.updateAdminsFromEvent(groupId, event);
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
handleModerationEvent(event) {
|
|
7732
|
+
const groupId = this.getGroupIdFromEvent(event);
|
|
7733
|
+
if (!groupId) return;
|
|
7734
|
+
const group = this.groups.get(groupId);
|
|
7735
|
+
if (!group) return;
|
|
7736
|
+
if (event.kind === NIP29_KINDS.DELETE_EVENT) {
|
|
7737
|
+
const eTags = event.tags.filter((t) => t[0] === "e");
|
|
7738
|
+
for (const tag of eTags) {
|
|
7739
|
+
const messageId = tag[1];
|
|
7740
|
+
if (messageId) {
|
|
7741
|
+
this.deleteMessageFromMemory(groupId, messageId);
|
|
7742
|
+
}
|
|
7743
|
+
}
|
|
7744
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7745
|
+
this.persistMessages();
|
|
7746
|
+
} else if (event.kind === NIP29_KINDS.REMOVE_USER) {
|
|
7747
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7748
|
+
const eventTimestampMs = event.created_at * 1e3;
|
|
7749
|
+
if (group.localJoinedAt && eventTimestampMs < group.localJoinedAt) {
|
|
7750
|
+
this.addProcessedEventId(event.id);
|
|
7751
|
+
return;
|
|
7752
|
+
}
|
|
7753
|
+
this.addProcessedEventId(event.id);
|
|
7754
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7755
|
+
const myPubkey = this.getMyPublicKey();
|
|
7756
|
+
for (const tag of pTags) {
|
|
7757
|
+
const removedPubkey = tag[1];
|
|
7758
|
+
if (!removedPubkey) continue;
|
|
7759
|
+
if (removedPubkey === myPubkey) {
|
|
7760
|
+
if (this.pendingLeaves.has(groupId)) {
|
|
7761
|
+
this.pendingLeaves.delete(groupId);
|
|
7762
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7763
|
+
} else {
|
|
7764
|
+
const groupName = group.name || groupId;
|
|
7765
|
+
this.removeGroupFromMemory(groupId);
|
|
7766
|
+
this.deps.emitEvent("groupchat:kicked", { groupId, groupName });
|
|
7767
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7768
|
+
}
|
|
7769
|
+
} else {
|
|
7770
|
+
this.removeMemberFromMemory(groupId, removedPubkey);
|
|
7771
|
+
}
|
|
7772
|
+
}
|
|
7773
|
+
this.schedulePersist();
|
|
7774
|
+
} else if (event.kind === NIP29_KINDS.DELETE_GROUP) {
|
|
7775
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7776
|
+
const deleteTimestampMs = event.created_at * 1e3;
|
|
7777
|
+
if (deleteTimestampMs < group.createdAt) {
|
|
7778
|
+
this.addProcessedEventId(event.id);
|
|
7779
|
+
return;
|
|
7780
|
+
}
|
|
7781
|
+
this.addProcessedEventId(event.id);
|
|
7782
|
+
const groupName = group.name || groupId;
|
|
7783
|
+
this.removeGroupFromMemory(groupId);
|
|
7784
|
+
this.deps.emitEvent("groupchat:group_deleted", { groupId, groupName });
|
|
7785
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7786
|
+
this.schedulePersist();
|
|
7787
|
+
}
|
|
7788
|
+
}
|
|
7789
|
+
updateMembersFromEvent(groupId, event) {
|
|
7790
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7791
|
+
const existingMembers = this.members.get(groupId) || [];
|
|
7792
|
+
for (const tag of pTags) {
|
|
7793
|
+
const pubkey = tag[1];
|
|
7794
|
+
const roleFromTag = tag[3];
|
|
7795
|
+
const existing = existingMembers.find((m) => m.pubkey === pubkey);
|
|
7796
|
+
const role = roleFromTag || existing?.role || GroupRole.MEMBER;
|
|
7797
|
+
const member = {
|
|
7798
|
+
pubkey,
|
|
7799
|
+
groupId,
|
|
7800
|
+
role,
|
|
7801
|
+
nametag: existing?.nametag,
|
|
7802
|
+
joinedAt: existing?.joinedAt || event.created_at * 1e3
|
|
7803
|
+
};
|
|
7804
|
+
this.saveMemberToMemory(member);
|
|
7805
|
+
}
|
|
7806
|
+
this.persistMembers();
|
|
7807
|
+
}
|
|
7808
|
+
updateAdminsFromEvent(groupId, event) {
|
|
7809
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7810
|
+
const existingMembers = this.members.get(groupId) || [];
|
|
7811
|
+
for (const tag of pTags) {
|
|
7812
|
+
const pubkey = tag[1];
|
|
7813
|
+
const existing = existingMembers.find((m) => m.pubkey === pubkey);
|
|
7814
|
+
if (existing) {
|
|
7815
|
+
existing.role = GroupRole.ADMIN;
|
|
7816
|
+
this.saveMemberToMemory(existing);
|
|
7817
|
+
} else {
|
|
7818
|
+
this.saveMemberToMemory({
|
|
7819
|
+
pubkey,
|
|
7820
|
+
groupId,
|
|
7821
|
+
role: GroupRole.ADMIN,
|
|
7822
|
+
joinedAt: event.created_at * 1e3
|
|
7823
|
+
});
|
|
7824
|
+
}
|
|
7825
|
+
}
|
|
7826
|
+
this.persistMembers();
|
|
7827
|
+
}
|
|
7828
|
+
// ===========================================================================
|
|
7829
|
+
// Group Membership Restoration
|
|
7830
|
+
// ===========================================================================
|
|
7831
|
+
async restoreJoinedGroups() {
|
|
7832
|
+
if (!this.client) return [];
|
|
7833
|
+
const myPubkey = this.getMyPublicKey();
|
|
7834
|
+
if (!myPubkey) return [];
|
|
7835
|
+
const groupIdsWithMembership = /* @__PURE__ */ new Set();
|
|
7836
|
+
await this.oneshotSubscription(
|
|
7837
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
|
|
7838
|
+
{
|
|
7839
|
+
onEvent: (event) => {
|
|
7840
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7841
|
+
if (!groupId) return;
|
|
7842
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7843
|
+
if (pTags.some((tag) => tag[1] === myPubkey)) {
|
|
7844
|
+
groupIdsWithMembership.add(groupId);
|
|
7845
|
+
}
|
|
7846
|
+
},
|
|
7847
|
+
onComplete: () => {
|
|
7848
|
+
},
|
|
7849
|
+
timeoutMs: 15e3
|
|
7850
|
+
}
|
|
7851
|
+
);
|
|
7852
|
+
if (groupIdsWithMembership.size === 0) return [];
|
|
7853
|
+
const restoredGroups = [];
|
|
7854
|
+
for (const groupId of groupIdsWithMembership) {
|
|
7855
|
+
if (this.groups.has(groupId)) continue;
|
|
7856
|
+
try {
|
|
7857
|
+
const group = await this.fetchGroupMetadataInternal(groupId);
|
|
7858
|
+
if (group) {
|
|
7859
|
+
this.groups.set(groupId, group);
|
|
7860
|
+
restoredGroups.push(group);
|
|
7861
|
+
await Promise.all([
|
|
7862
|
+
this.fetchAndSaveMembers(groupId),
|
|
7863
|
+
this.fetchMessages(groupId)
|
|
7864
|
+
]);
|
|
7865
|
+
}
|
|
7866
|
+
} catch {
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
if (restoredGroups.length > 0) {
|
|
7870
|
+
await this.subscribeToJoinedGroups();
|
|
7871
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7872
|
+
this.schedulePersist();
|
|
7873
|
+
}
|
|
7874
|
+
return restoredGroups;
|
|
7875
|
+
}
|
|
7876
|
+
// ===========================================================================
|
|
7877
|
+
// Public API — Groups
|
|
7878
|
+
// ===========================================================================
|
|
7879
|
+
async fetchAvailableGroups() {
|
|
7880
|
+
await this.ensureConnected();
|
|
7881
|
+
if (!this.client) return [];
|
|
7882
|
+
const groupsMap = /* @__PURE__ */ new Map();
|
|
7883
|
+
const memberCountsMap = /* @__PURE__ */ new Map();
|
|
7884
|
+
await Promise.all([
|
|
7885
|
+
this.oneshotSubscription(
|
|
7886
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
|
|
7887
|
+
{
|
|
7888
|
+
onEvent: (event) => {
|
|
7889
|
+
const group = this.parseGroupMetadata(event);
|
|
7890
|
+
if (group && group.visibility === GroupVisibility.PUBLIC) {
|
|
7891
|
+
const existing = groupsMap.get(group.id);
|
|
7892
|
+
if (!existing || group.createdAt > existing.createdAt) {
|
|
7893
|
+
groupsMap.set(group.id, group);
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
},
|
|
7897
|
+
onComplete: () => {
|
|
7898
|
+
},
|
|
7899
|
+
timeoutMs: 1e4
|
|
7900
|
+
}
|
|
7901
|
+
),
|
|
7902
|
+
this.oneshotSubscription(
|
|
7903
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
|
|
7904
|
+
{
|
|
7905
|
+
onEvent: (event) => {
|
|
7906
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7907
|
+
if (groupId) {
|
|
7908
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7909
|
+
memberCountsMap.set(groupId, pTags.length);
|
|
7910
|
+
}
|
|
7911
|
+
},
|
|
7912
|
+
onComplete: () => {
|
|
7913
|
+
},
|
|
7914
|
+
timeoutMs: 1e4
|
|
7915
|
+
}
|
|
7916
|
+
)
|
|
7917
|
+
]);
|
|
7918
|
+
for (const [groupId, count] of memberCountsMap) {
|
|
7919
|
+
const group = groupsMap.get(groupId);
|
|
7920
|
+
if (group) group.memberCount = count;
|
|
7921
|
+
}
|
|
7922
|
+
return Array.from(groupsMap.values());
|
|
7923
|
+
}
|
|
7924
|
+
async joinGroup(groupId, inviteCode) {
|
|
7925
|
+
await this.ensureConnected();
|
|
7926
|
+
if (!this.client) return false;
|
|
7927
|
+
try {
|
|
7928
|
+
let group = await this.fetchGroupMetadataInternal(groupId);
|
|
7929
|
+
if (!group && !inviteCode) return false;
|
|
7930
|
+
const tags = [["h", groupId]];
|
|
7931
|
+
if (inviteCode) tags.push(["code", inviteCode]);
|
|
7932
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
7933
|
+
kind: NIP29_KINDS.JOIN_REQUEST,
|
|
7934
|
+
tags,
|
|
7935
|
+
content: ""
|
|
7936
|
+
});
|
|
7937
|
+
if (eventId) {
|
|
7938
|
+
if (!group) {
|
|
7939
|
+
group = await this.fetchGroupMetadataInternal(groupId);
|
|
7940
|
+
if (!group) return false;
|
|
7941
|
+
}
|
|
7942
|
+
group.localJoinedAt = Date.now();
|
|
7943
|
+
this.groups.set(groupId, group);
|
|
7944
|
+
this.subscribeToGroup(groupId);
|
|
7945
|
+
await Promise.all([
|
|
7946
|
+
this.fetchMessages(groupId),
|
|
7947
|
+
this.fetchAndSaveMembers(groupId)
|
|
7948
|
+
]);
|
|
7949
|
+
this.deps.emitEvent("groupchat:joined", { groupId, groupName: group.name });
|
|
7950
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7951
|
+
this.persistAll();
|
|
7952
|
+
return true;
|
|
7953
|
+
}
|
|
7954
|
+
return false;
|
|
7955
|
+
} catch (error) {
|
|
7956
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
7957
|
+
if (msg.includes("already a member")) {
|
|
7958
|
+
const group = await this.fetchGroupMetadataInternal(groupId);
|
|
7959
|
+
if (group) {
|
|
7960
|
+
group.localJoinedAt = Date.now();
|
|
7961
|
+
this.groups.set(groupId, group);
|
|
7962
|
+
this.subscribeToGroup(groupId);
|
|
7963
|
+
await Promise.all([
|
|
7964
|
+
this.fetchMessages(groupId),
|
|
7965
|
+
this.fetchAndSaveMembers(groupId)
|
|
7966
|
+
]);
|
|
7967
|
+
this.deps.emitEvent("groupchat:joined", { groupId, groupName: group.name });
|
|
7968
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7969
|
+
this.persistAll();
|
|
7970
|
+
return true;
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
console.error("[GroupChat] Failed to join group", error);
|
|
7974
|
+
return false;
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
async leaveGroup(groupId) {
|
|
7978
|
+
await this.ensureConnected();
|
|
7979
|
+
if (!this.client) return false;
|
|
7980
|
+
try {
|
|
7981
|
+
this.pendingLeaves.add(groupId);
|
|
7982
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
7983
|
+
kind: NIP29_KINDS.LEAVE_REQUEST,
|
|
7984
|
+
tags: [["h", groupId]],
|
|
7985
|
+
content: ""
|
|
7986
|
+
});
|
|
7987
|
+
if (eventId) {
|
|
7988
|
+
this.removeGroupFromMemory(groupId);
|
|
7989
|
+
this.deps.emitEvent("groupchat:left", { groupId });
|
|
7990
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7991
|
+
this.persistAll();
|
|
7992
|
+
return true;
|
|
7993
|
+
}
|
|
7994
|
+
this.pendingLeaves.delete(groupId);
|
|
7995
|
+
return false;
|
|
7996
|
+
} catch (error) {
|
|
7997
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
7998
|
+
if (msg.includes("group not found") || msg.includes("not a member")) {
|
|
7999
|
+
this.removeGroupFromMemory(groupId);
|
|
8000
|
+
this.persistAll();
|
|
8001
|
+
return true;
|
|
8002
|
+
}
|
|
8003
|
+
console.error("[GroupChat] Failed to leave group", error);
|
|
8004
|
+
return false;
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
async createGroup(options) {
|
|
8008
|
+
await this.ensureConnected();
|
|
8009
|
+
if (!this.client) return null;
|
|
8010
|
+
const creatorPubkey = this.getMyPublicKey();
|
|
8011
|
+
if (!creatorPubkey) return null;
|
|
8012
|
+
const proposedGroupId = options.name.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 20) || this.randomId();
|
|
8013
|
+
try {
|
|
8014
|
+
const isPrivate = options.visibility === GroupVisibility.PRIVATE;
|
|
8015
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8016
|
+
kind: NIP29_KINDS.CREATE_GROUP,
|
|
8017
|
+
tags: [["h", proposedGroupId]],
|
|
8018
|
+
content: JSON.stringify({
|
|
8019
|
+
name: options.name,
|
|
8020
|
+
about: options.description,
|
|
8021
|
+
picture: options.picture,
|
|
8022
|
+
closed: true,
|
|
8023
|
+
private: isPrivate,
|
|
8024
|
+
hidden: isPrivate
|
|
8025
|
+
})
|
|
8026
|
+
});
|
|
8027
|
+
if (!eventId) return null;
|
|
8028
|
+
let group = await this.fetchGroupMetadataInternal(proposedGroupId);
|
|
8029
|
+
if (!group) {
|
|
8030
|
+
group = {
|
|
8031
|
+
id: proposedGroupId,
|
|
8032
|
+
relayUrl: this.config.relays[0] || "",
|
|
8033
|
+
name: options.name,
|
|
8034
|
+
description: options.description,
|
|
8035
|
+
visibility: options.visibility || GroupVisibility.PUBLIC,
|
|
8036
|
+
createdAt: Date.now(),
|
|
8037
|
+
memberCount: 1
|
|
8038
|
+
};
|
|
8039
|
+
}
|
|
8040
|
+
if (!group.name || group.name === "Unnamed Group") {
|
|
8041
|
+
group.name = options.name;
|
|
8042
|
+
}
|
|
8043
|
+
if (options.description && !group.description) {
|
|
8044
|
+
group.description = options.description;
|
|
8045
|
+
}
|
|
8046
|
+
group.visibility = options.visibility || GroupVisibility.PUBLIC;
|
|
8047
|
+
group.memberCount = 1;
|
|
8048
|
+
this.groups.set(group.id, group);
|
|
8049
|
+
this.subscribeToGroup(group.id);
|
|
8050
|
+
this.client.createAndPublishEvent({
|
|
8051
|
+
kind: NIP29_KINDS.JOIN_REQUEST,
|
|
8052
|
+
tags: [["h", group.id]],
|
|
8053
|
+
content: ""
|
|
8054
|
+
}).catch(() => {
|
|
8055
|
+
});
|
|
8056
|
+
await this.fetchAndSaveMembers(group.id).catch(() => {
|
|
8057
|
+
});
|
|
8058
|
+
this.saveMemberToMemory({
|
|
8059
|
+
pubkey: creatorPubkey,
|
|
8060
|
+
groupId: group.id,
|
|
8061
|
+
role: GroupRole.ADMIN,
|
|
8062
|
+
joinedAt: Date.now()
|
|
8063
|
+
});
|
|
8064
|
+
this.deps.emitEvent("groupchat:joined", { groupId: group.id, groupName: group.name });
|
|
8065
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8066
|
+
this.schedulePersist();
|
|
8067
|
+
return group;
|
|
8068
|
+
} catch (error) {
|
|
8069
|
+
console.error("[GroupChat] Failed to create group", error);
|
|
8070
|
+
return null;
|
|
8071
|
+
}
|
|
8072
|
+
}
|
|
8073
|
+
async deleteGroup(groupId) {
|
|
8074
|
+
await this.ensureConnected();
|
|
8075
|
+
if (!this.client) return false;
|
|
8076
|
+
const group = this.groups.get(groupId);
|
|
8077
|
+
if (!group) return false;
|
|
8078
|
+
const canDelete = await this.canModerateGroup(groupId);
|
|
8079
|
+
if (!canDelete) return false;
|
|
8080
|
+
try {
|
|
8081
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8082
|
+
kind: NIP29_KINDS.DELETE_GROUP,
|
|
8083
|
+
tags: [["h", groupId]],
|
|
8084
|
+
content: ""
|
|
8085
|
+
});
|
|
8086
|
+
if (eventId) {
|
|
8087
|
+
const groupName = group.name || groupId;
|
|
8088
|
+
this.removeGroupFromMemory(groupId);
|
|
8089
|
+
this.deps.emitEvent("groupchat:group_deleted", { groupId, groupName });
|
|
8090
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8091
|
+
this.persistAll();
|
|
8092
|
+
return true;
|
|
8093
|
+
}
|
|
8094
|
+
return false;
|
|
8095
|
+
} catch (error) {
|
|
8096
|
+
console.error("[GroupChat] Failed to delete group", error);
|
|
8097
|
+
return false;
|
|
8098
|
+
}
|
|
8099
|
+
}
|
|
8100
|
+
async createInvite(groupId) {
|
|
8101
|
+
await this.ensureConnected();
|
|
8102
|
+
if (!this.client) return null;
|
|
8103
|
+
if (!this.isCurrentUserAdmin(groupId)) return null;
|
|
8104
|
+
try {
|
|
8105
|
+
const inviteCode = this.randomId();
|
|
8106
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8107
|
+
kind: NIP29_KINDS.CREATE_INVITE,
|
|
8108
|
+
tags: [
|
|
8109
|
+
["h", groupId],
|
|
8110
|
+
["code", inviteCode]
|
|
8111
|
+
],
|
|
8112
|
+
content: ""
|
|
8113
|
+
});
|
|
8114
|
+
return eventId ? inviteCode : null;
|
|
8115
|
+
} catch (error) {
|
|
8116
|
+
console.error("[GroupChat] Failed to create invite", error);
|
|
8117
|
+
return null;
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
// ===========================================================================
|
|
8121
|
+
// Public API — Messages
|
|
8122
|
+
// ===========================================================================
|
|
8123
|
+
async sendMessage(groupId, content, replyToId) {
|
|
8124
|
+
await this.ensureConnected();
|
|
8125
|
+
if (!this.client) return null;
|
|
8126
|
+
const group = this.groups.get(groupId);
|
|
8127
|
+
if (!group) return null;
|
|
8128
|
+
try {
|
|
8129
|
+
const senderNametag = this.deps.identity.nametag || null;
|
|
8130
|
+
const kind = replyToId ? NIP29_KINDS.THREAD_REPLY : NIP29_KINDS.CHAT_MESSAGE;
|
|
8131
|
+
const tags = [["h", groupId]];
|
|
8132
|
+
const groupMessages = this.messages.get(groupId) || [];
|
|
8133
|
+
const recentIds = groupMessages.slice(-this.config.maxPreviousTags).map((m) => (m.id || "").slice(0, 8)).filter(Boolean);
|
|
8134
|
+
if (recentIds.length > 0) {
|
|
8135
|
+
tags.push(["previous", ...recentIds]);
|
|
8136
|
+
}
|
|
8137
|
+
if (replyToId) {
|
|
8138
|
+
tags.push(["e", replyToId, "", "reply"]);
|
|
8139
|
+
}
|
|
8140
|
+
const wrappedContent = this.wrapMessageContent(content, senderNametag);
|
|
8141
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8142
|
+
kind,
|
|
8143
|
+
tags,
|
|
8144
|
+
content: wrappedContent
|
|
8145
|
+
});
|
|
8146
|
+
if (eventId) {
|
|
8147
|
+
const myPubkey = this.getMyPublicKey();
|
|
8148
|
+
const message = {
|
|
8149
|
+
id: eventId,
|
|
8150
|
+
groupId,
|
|
8151
|
+
content,
|
|
8152
|
+
timestamp: Date.now(),
|
|
8153
|
+
senderPubkey: myPubkey || "",
|
|
8154
|
+
senderNametag: senderNametag || void 0,
|
|
8155
|
+
replyToId,
|
|
8156
|
+
previousIds: recentIds
|
|
8157
|
+
};
|
|
8158
|
+
this.saveMessageToMemory(message);
|
|
8159
|
+
this.addProcessedEventId(eventId);
|
|
8160
|
+
this.updateGroupLastMessage(groupId, content.slice(0, 100), message.timestamp);
|
|
8161
|
+
this.persistAll();
|
|
8162
|
+
return message;
|
|
8163
|
+
}
|
|
8164
|
+
return null;
|
|
8165
|
+
} catch (error) {
|
|
8166
|
+
console.error("[GroupChat] Failed to send message", error);
|
|
8167
|
+
return null;
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
async fetchMessages(groupId, since, limit) {
|
|
8171
|
+
await this.ensureConnected();
|
|
8172
|
+
if (!this.client) return [];
|
|
8173
|
+
const fetchedMessages = [];
|
|
8174
|
+
const filterData = {
|
|
8175
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
8176
|
+
"#h": [groupId]
|
|
8177
|
+
};
|
|
8178
|
+
if (since) filterData.since = Math.floor(since / 1e3);
|
|
8179
|
+
if (limit) filterData.limit = limit;
|
|
8180
|
+
if (!limit && !since) filterData.limit = this.config.defaultMessageLimit;
|
|
8181
|
+
return this.oneshotSubscription(createNip29Filter(filterData), {
|
|
8182
|
+
onEvent: (event) => {
|
|
8183
|
+
const { text: content, senderNametag } = this.unwrapMessageContent(event.content);
|
|
8184
|
+
const message = {
|
|
8185
|
+
id: event.id,
|
|
8186
|
+
groupId,
|
|
8187
|
+
content,
|
|
8188
|
+
timestamp: event.created_at * 1e3,
|
|
8189
|
+
senderPubkey: event.pubkey,
|
|
8190
|
+
senderNametag: senderNametag || void 0,
|
|
8191
|
+
replyToId: this.extractReplyTo(event),
|
|
8192
|
+
previousIds: this.extractPreviousIds(event)
|
|
8193
|
+
};
|
|
8194
|
+
fetchedMessages.push(message);
|
|
8195
|
+
this.saveMessageToMemory(message);
|
|
8196
|
+
this.addProcessedEventId(event.id);
|
|
8197
|
+
if (senderNametag) {
|
|
8198
|
+
this.updateMemberNametag(groupId, event.pubkey, senderNametag, event.created_at * 1e3);
|
|
8199
|
+
}
|
|
8200
|
+
},
|
|
8201
|
+
onComplete: () => {
|
|
8202
|
+
this.schedulePersist();
|
|
8203
|
+
return fetchedMessages;
|
|
8204
|
+
},
|
|
8205
|
+
timeoutMs: 1e4
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
// ===========================================================================
|
|
8209
|
+
// Public API — Queries (from local state)
|
|
8210
|
+
// ===========================================================================
|
|
8211
|
+
getGroups() {
|
|
8212
|
+
return Array.from(this.groups.values()).sort((a, b) => (b.lastMessageTime || 0) - (a.lastMessageTime || 0));
|
|
8213
|
+
}
|
|
8214
|
+
getGroup(groupId) {
|
|
8215
|
+
return this.groups.get(groupId) || null;
|
|
8216
|
+
}
|
|
8217
|
+
getMessages(groupId) {
|
|
8218
|
+
return (this.messages.get(groupId) || []).sort((a, b) => a.timestamp - b.timestamp);
|
|
8219
|
+
}
|
|
8220
|
+
getMembers(groupId) {
|
|
8221
|
+
return (this.members.get(groupId) || []).sort((a, b) => a.joinedAt - b.joinedAt);
|
|
8222
|
+
}
|
|
8223
|
+
getMember(groupId, pubkey) {
|
|
8224
|
+
const members = this.members.get(groupId) || [];
|
|
8225
|
+
return members.find((m) => m.pubkey === pubkey) || null;
|
|
8226
|
+
}
|
|
8227
|
+
getTotalUnreadCount() {
|
|
8228
|
+
let total = 0;
|
|
8229
|
+
for (const group of this.groups.values()) {
|
|
8230
|
+
total += group.unreadCount || 0;
|
|
8231
|
+
}
|
|
8232
|
+
return total;
|
|
8233
|
+
}
|
|
8234
|
+
markGroupAsRead(groupId) {
|
|
8235
|
+
const group = this.groups.get(groupId);
|
|
8236
|
+
if (group && (group.unreadCount || 0) > 0) {
|
|
8237
|
+
group.unreadCount = 0;
|
|
8238
|
+
this.groups.set(groupId, group);
|
|
8239
|
+
this.persistGroups();
|
|
8240
|
+
}
|
|
8241
|
+
}
|
|
8242
|
+
// ===========================================================================
|
|
8243
|
+
// Public API — Admin
|
|
8244
|
+
// ===========================================================================
|
|
8245
|
+
async kickUser(groupId, userPubkey, reason) {
|
|
8246
|
+
await this.ensureConnected();
|
|
8247
|
+
if (!this.client) return false;
|
|
8248
|
+
const canModerate = await this.canModerateGroup(groupId);
|
|
8249
|
+
if (!canModerate) return false;
|
|
8250
|
+
const myPubkey = this.getMyPublicKey();
|
|
8251
|
+
if (myPubkey === userPubkey) return false;
|
|
8252
|
+
try {
|
|
8253
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8254
|
+
kind: NIP29_KINDS.REMOVE_USER,
|
|
8255
|
+
tags: [["h", groupId], ["p", userPubkey]],
|
|
8256
|
+
content: reason || ""
|
|
8257
|
+
});
|
|
8258
|
+
if (eventId) {
|
|
8259
|
+
this.removeMemberFromMemory(groupId, userPubkey);
|
|
8260
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8261
|
+
this.persistMembers();
|
|
8262
|
+
return true;
|
|
8263
|
+
}
|
|
8264
|
+
return false;
|
|
8265
|
+
} catch (error) {
|
|
8266
|
+
console.error("[GroupChat] Failed to kick user", error);
|
|
8267
|
+
return false;
|
|
8268
|
+
}
|
|
8269
|
+
}
|
|
8270
|
+
async deleteMessage(groupId, messageId) {
|
|
8271
|
+
await this.ensureConnected();
|
|
8272
|
+
if (!this.client) return false;
|
|
8273
|
+
const canModerate = await this.canModerateGroup(groupId);
|
|
8274
|
+
if (!canModerate) return false;
|
|
8275
|
+
try {
|
|
8276
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8277
|
+
kind: NIP29_KINDS.DELETE_EVENT,
|
|
8278
|
+
tags: [["h", groupId], ["e", messageId]],
|
|
8279
|
+
content: ""
|
|
8280
|
+
});
|
|
8281
|
+
if (eventId) {
|
|
8282
|
+
this.deleteMessageFromMemory(groupId, messageId);
|
|
8283
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8284
|
+
this.persistMessages();
|
|
8285
|
+
return true;
|
|
8286
|
+
}
|
|
8287
|
+
return false;
|
|
8288
|
+
} catch (error) {
|
|
8289
|
+
console.error("[GroupChat] Failed to delete message", error);
|
|
8290
|
+
return false;
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
isCurrentUserAdmin(groupId) {
|
|
8294
|
+
const myPubkey = this.getMyPublicKey();
|
|
8295
|
+
if (!myPubkey) return false;
|
|
8296
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8297
|
+
return member?.role === GroupRole.ADMIN;
|
|
8298
|
+
}
|
|
8299
|
+
isCurrentUserModerator(groupId) {
|
|
8300
|
+
const myPubkey = this.getMyPublicKey();
|
|
8301
|
+
if (!myPubkey) return false;
|
|
8302
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8303
|
+
return member?.role === GroupRole.ADMIN || member?.role === GroupRole.MODERATOR;
|
|
8304
|
+
}
|
|
8305
|
+
/**
|
|
8306
|
+
* Check if current user can moderate a group:
|
|
8307
|
+
* - Group admin/moderator can always moderate their group
|
|
8308
|
+
* - Relay admins can moderate public groups
|
|
8309
|
+
*/
|
|
8310
|
+
async canModerateGroup(groupId) {
|
|
8311
|
+
if (this.isCurrentUserAdmin(groupId) || this.isCurrentUserModerator(groupId)) {
|
|
8312
|
+
return true;
|
|
8313
|
+
}
|
|
8314
|
+
const group = this.groups.get(groupId);
|
|
8315
|
+
if (group && group.visibility === GroupVisibility.PUBLIC) {
|
|
8316
|
+
return this.isCurrentUserRelayAdmin();
|
|
8317
|
+
}
|
|
8318
|
+
return false;
|
|
8319
|
+
}
|
|
8320
|
+
async isCurrentUserRelayAdmin() {
|
|
8321
|
+
const myPubkey = this.getMyPublicKey();
|
|
8322
|
+
if (!myPubkey) return false;
|
|
8323
|
+
const admins = await this.fetchRelayAdmins();
|
|
8324
|
+
return admins.has(myPubkey);
|
|
8325
|
+
}
|
|
8326
|
+
getCurrentUserRole(groupId) {
|
|
8327
|
+
const myPubkey = this.getMyPublicKey();
|
|
8328
|
+
if (!myPubkey) return null;
|
|
8329
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8330
|
+
return member?.role || null;
|
|
8331
|
+
}
|
|
8332
|
+
// ===========================================================================
|
|
8333
|
+
// Public API — Listeners
|
|
8334
|
+
// ===========================================================================
|
|
8335
|
+
onMessage(handler) {
|
|
8336
|
+
this.messageHandlers.add(handler);
|
|
8337
|
+
return () => this.messageHandlers.delete(handler);
|
|
8338
|
+
}
|
|
8339
|
+
// ===========================================================================
|
|
8340
|
+
// Public API — Utilities
|
|
8341
|
+
// ===========================================================================
|
|
8342
|
+
getRelayUrls() {
|
|
8343
|
+
return this.config.relays;
|
|
8344
|
+
}
|
|
8345
|
+
getMyPublicKey() {
|
|
8346
|
+
return this.keyManager?.getPublicKeyHex() || null;
|
|
8347
|
+
}
|
|
8348
|
+
// ===========================================================================
|
|
8349
|
+
// Private — Relay Admin
|
|
8350
|
+
// ===========================================================================
|
|
8351
|
+
async fetchRelayAdmins() {
|
|
8352
|
+
if (this.relayAdminPubkeys) return this.relayAdminPubkeys;
|
|
8353
|
+
if (this.relayAdminFetchPromise) return this.relayAdminFetchPromise;
|
|
8354
|
+
this.relayAdminFetchPromise = this.doFetchRelayAdmins();
|
|
8355
|
+
const result = await this.relayAdminFetchPromise;
|
|
8356
|
+
this.relayAdminFetchPromise = null;
|
|
8357
|
+
return result;
|
|
8358
|
+
}
|
|
8359
|
+
async doFetchRelayAdmins() {
|
|
8360
|
+
await this.ensureConnected();
|
|
8361
|
+
if (!this.client) return /* @__PURE__ */ new Set();
|
|
8362
|
+
const adminPubkeys = /* @__PURE__ */ new Set();
|
|
8363
|
+
return this.oneshotSubscription(
|
|
8364
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
|
|
8365
|
+
{
|
|
8366
|
+
onEvent: (event) => {
|
|
8367
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8368
|
+
for (const tag of pTags) {
|
|
8369
|
+
if (tag[1]) adminPubkeys.add(tag[1]);
|
|
8370
|
+
}
|
|
8371
|
+
},
|
|
8372
|
+
onComplete: () => {
|
|
8373
|
+
this.relayAdminPubkeys = adminPubkeys;
|
|
8374
|
+
return adminPubkeys;
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8377
|
+
);
|
|
8378
|
+
}
|
|
8379
|
+
// ===========================================================================
|
|
8380
|
+
// Private — Fetch Helpers
|
|
8381
|
+
// ===========================================================================
|
|
8382
|
+
async fetchGroupMetadataInternal(groupId) {
|
|
8383
|
+
if (!this.client) return null;
|
|
8384
|
+
let result = null;
|
|
8385
|
+
return this.oneshotSubscription(
|
|
8386
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
|
|
8387
|
+
{
|
|
8388
|
+
onEvent: (event) => {
|
|
8389
|
+
if (!result) result = this.parseGroupMetadata(event);
|
|
8390
|
+
},
|
|
8391
|
+
onComplete: () => result
|
|
8392
|
+
}
|
|
8393
|
+
);
|
|
8394
|
+
}
|
|
8395
|
+
async fetchAndSaveMembers(groupId) {
|
|
8396
|
+
const [members, adminPubkeys] = await Promise.all([
|
|
8397
|
+
this.fetchGroupMembersInternal(groupId),
|
|
8398
|
+
this.fetchGroupAdminsInternal(groupId)
|
|
8399
|
+
]);
|
|
8400
|
+
for (const member of members) {
|
|
8401
|
+
if (adminPubkeys.includes(member.pubkey)) {
|
|
8402
|
+
member.role = GroupRole.ADMIN;
|
|
8403
|
+
}
|
|
8404
|
+
this.saveMemberToMemory(member);
|
|
8405
|
+
}
|
|
8406
|
+
for (const pubkey of adminPubkeys) {
|
|
8407
|
+
const existing = (this.members.get(groupId) || []).find((m) => m.pubkey === pubkey);
|
|
8408
|
+
if (!existing) {
|
|
8409
|
+
this.saveMemberToMemory({
|
|
8410
|
+
pubkey,
|
|
8411
|
+
groupId,
|
|
8412
|
+
role: GroupRole.ADMIN,
|
|
8413
|
+
joinedAt: Date.now()
|
|
8414
|
+
});
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8417
|
+
this.persistMembers();
|
|
8418
|
+
}
|
|
8419
|
+
async fetchGroupMembersInternal(groupId) {
|
|
8420
|
+
if (!this.client) return [];
|
|
8421
|
+
const members = [];
|
|
8422
|
+
return this.oneshotSubscription(
|
|
8423
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
|
|
8424
|
+
{
|
|
8425
|
+
onEvent: (event) => {
|
|
8426
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8427
|
+
for (const tag of pTags) {
|
|
8428
|
+
members.push({
|
|
8429
|
+
pubkey: tag[1],
|
|
8430
|
+
groupId,
|
|
8431
|
+
role: tag[3] || GroupRole.MEMBER,
|
|
8432
|
+
joinedAt: event.created_at * 1e3
|
|
8433
|
+
});
|
|
8434
|
+
}
|
|
8435
|
+
},
|
|
8436
|
+
onComplete: () => members
|
|
8437
|
+
}
|
|
8438
|
+
);
|
|
8439
|
+
}
|
|
8440
|
+
async fetchGroupAdminsInternal(groupId) {
|
|
8441
|
+
if (!this.client) return [];
|
|
8442
|
+
const adminPubkeys = [];
|
|
8443
|
+
return this.oneshotSubscription(
|
|
8444
|
+
new Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
|
|
8445
|
+
{
|
|
8446
|
+
onEvent: (event) => {
|
|
8447
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8448
|
+
for (const tag of pTags) {
|
|
8449
|
+
if (tag[1] && !adminPubkeys.includes(tag[1])) {
|
|
8450
|
+
adminPubkeys.push(tag[1]);
|
|
8451
|
+
}
|
|
8452
|
+
}
|
|
8453
|
+
},
|
|
8454
|
+
onComplete: () => adminPubkeys
|
|
8455
|
+
}
|
|
8456
|
+
);
|
|
8457
|
+
}
|
|
8458
|
+
// ===========================================================================
|
|
8459
|
+
// Private — In-Memory State Helpers
|
|
7256
8460
|
// ===========================================================================
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
const
|
|
7263
|
-
const
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
};
|
|
7273
|
-
this.messages.set(message.id, message);
|
|
7274
|
-
if (this.config.autoSave) {
|
|
7275
|
-
await this.save();
|
|
8461
|
+
saveMessageToMemory(message) {
|
|
8462
|
+
const groupId = message.groupId;
|
|
8463
|
+
if (!this.messages.has(groupId)) {
|
|
8464
|
+
this.messages.set(groupId, []);
|
|
8465
|
+
}
|
|
8466
|
+
const msgs = this.messages.get(groupId);
|
|
8467
|
+
const idx = msgs.findIndex((m) => m.id === message.id);
|
|
8468
|
+
if (idx >= 0) {
|
|
8469
|
+
msgs[idx] = message;
|
|
8470
|
+
} else {
|
|
8471
|
+
msgs.push(message);
|
|
8472
|
+
const maxMessages = this.config.defaultMessageLimit * 2;
|
|
8473
|
+
if (msgs.length > maxMessages) {
|
|
8474
|
+
msgs.splice(0, msgs.length - maxMessages);
|
|
8475
|
+
}
|
|
7276
8476
|
}
|
|
7277
|
-
return message;
|
|
7278
8477
|
}
|
|
7279
|
-
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
).sort((a, b) => a.timestamp - b.timestamp);
|
|
8478
|
+
deleteMessageFromMemory(groupId, messageId) {
|
|
8479
|
+
const msgs = this.messages.get(groupId);
|
|
8480
|
+
if (msgs) {
|
|
8481
|
+
const idx = msgs.findIndex((m) => m.id === messageId);
|
|
8482
|
+
if (idx >= 0) msgs.splice(idx, 1);
|
|
8483
|
+
}
|
|
7286
8484
|
}
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
const conversations = /* @__PURE__ */ new Map();
|
|
7292
|
-
for (const message of this.messages.values()) {
|
|
7293
|
-
const peer = message.senderPubkey === this.deps?.identity.chainPubkey ? message.recipientPubkey : message.senderPubkey;
|
|
7294
|
-
if (!conversations.has(peer)) {
|
|
7295
|
-
conversations.set(peer, []);
|
|
7296
|
-
}
|
|
7297
|
-
conversations.get(peer).push(message);
|
|
8485
|
+
saveMemberToMemory(member) {
|
|
8486
|
+
const groupId = member.groupId;
|
|
8487
|
+
if (!this.members.has(groupId)) {
|
|
8488
|
+
this.members.set(groupId, []);
|
|
7298
8489
|
}
|
|
7299
|
-
|
|
7300
|
-
|
|
8490
|
+
const mems = this.members.get(groupId);
|
|
8491
|
+
const idx = mems.findIndex((m) => m.pubkey === member.pubkey);
|
|
8492
|
+
if (idx >= 0) {
|
|
8493
|
+
mems[idx] = member;
|
|
8494
|
+
} else {
|
|
8495
|
+
mems.push(member);
|
|
7301
8496
|
}
|
|
7302
|
-
return conversations;
|
|
7303
8497
|
}
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
|
|
7309
|
-
const msg = this.messages.get(id);
|
|
7310
|
-
if (msg) {
|
|
7311
|
-
msg.isRead = true;
|
|
7312
|
-
}
|
|
8498
|
+
removeMemberFromMemory(groupId, pubkey) {
|
|
8499
|
+
const mems = this.members.get(groupId);
|
|
8500
|
+
if (mems) {
|
|
8501
|
+
const idx = mems.findIndex((m) => m.pubkey === pubkey);
|
|
8502
|
+
if (idx >= 0) mems.splice(idx, 1);
|
|
7313
8503
|
}
|
|
7314
|
-
|
|
7315
|
-
|
|
8504
|
+
const group = this.groups.get(groupId);
|
|
8505
|
+
if (group) {
|
|
8506
|
+
group.memberCount = (this.members.get(groupId) || []).length;
|
|
8507
|
+
this.groups.set(groupId, group);
|
|
7316
8508
|
}
|
|
7317
8509
|
}
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7324
|
-
);
|
|
7325
|
-
if (
|
|
7326
|
-
|
|
8510
|
+
removeGroupFromMemory(groupId) {
|
|
8511
|
+
this.groups.delete(groupId);
|
|
8512
|
+
this.messages.delete(groupId);
|
|
8513
|
+
this.members.delete(groupId);
|
|
8514
|
+
}
|
|
8515
|
+
updateGroupLastMessage(groupId, text, timestamp) {
|
|
8516
|
+
const group = this.groups.get(groupId);
|
|
8517
|
+
if (group && timestamp >= (group.lastMessageTime || 0)) {
|
|
8518
|
+
group.lastMessageText = text;
|
|
8519
|
+
group.lastMessageTime = timestamp;
|
|
8520
|
+
this.groups.set(groupId, group);
|
|
7327
8521
|
}
|
|
7328
|
-
return messages.length;
|
|
7329
8522
|
}
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
8523
|
+
updateMemberNametag(groupId, pubkey, nametag, joinedAt) {
|
|
8524
|
+
const members = this.members.get(groupId) || [];
|
|
8525
|
+
const existing = members.find((m) => m.pubkey === pubkey);
|
|
8526
|
+
if (existing) {
|
|
8527
|
+
if (existing.nametag !== nametag) {
|
|
8528
|
+
existing.nametag = nametag;
|
|
8529
|
+
this.saveMemberToMemory(existing);
|
|
8530
|
+
}
|
|
8531
|
+
} else {
|
|
8532
|
+
this.saveMemberToMemory({
|
|
8533
|
+
pubkey,
|
|
8534
|
+
groupId,
|
|
8535
|
+
role: GroupRole.MEMBER,
|
|
8536
|
+
nametag,
|
|
8537
|
+
joinedAt
|
|
8538
|
+
});
|
|
8539
|
+
}
|
|
8540
|
+
}
|
|
8541
|
+
addProcessedEventId(eventId) {
|
|
8542
|
+
this.processedEventIds.add(eventId);
|
|
8543
|
+
if (this.processedEventIds.size > 1e4) {
|
|
8544
|
+
const arr = Array.from(this.processedEventIds);
|
|
8545
|
+
this.processedEventIds = new Set(arr.slice(arr.length - 1e4));
|
|
8546
|
+
}
|
|
7336
8547
|
}
|
|
7337
8548
|
// ===========================================================================
|
|
7338
|
-
//
|
|
8549
|
+
// Private — Persistence
|
|
7339
8550
|
// ===========================================================================
|
|
7340
|
-
/**
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
timestamp: Date.now(),
|
|
7352
|
-
tags
|
|
7353
|
-
};
|
|
7354
|
-
this.broadcasts.set(message.id, message);
|
|
7355
|
-
return message;
|
|
8551
|
+
/** Schedule a debounced persist (coalesces rapid event bursts). */
|
|
8552
|
+
schedulePersist() {
|
|
8553
|
+
if (this.persistTimer) return;
|
|
8554
|
+
this.persistTimer = setTimeout(() => {
|
|
8555
|
+
this.persistTimer = null;
|
|
8556
|
+
this.persistPromise = this.doPersistAll().catch((err) => {
|
|
8557
|
+
console.error("[GroupChat] Persistence error:", err);
|
|
8558
|
+
}).finally(() => {
|
|
8559
|
+
this.persistPromise = null;
|
|
8560
|
+
});
|
|
8561
|
+
}, 200);
|
|
7356
8562
|
}
|
|
7357
|
-
/**
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
const key = tags.sort().join(":");
|
|
7363
|
-
if (this.broadcastSubscriptions.has(key)) {
|
|
7364
|
-
return () => {
|
|
7365
|
-
};
|
|
8563
|
+
/** Persist immediately (for explicit flush points). */
|
|
8564
|
+
async persistAll() {
|
|
8565
|
+
if (this.persistTimer) {
|
|
8566
|
+
clearTimeout(this.persistTimer);
|
|
8567
|
+
this.persistTimer = null;
|
|
7366
8568
|
}
|
|
7367
|
-
|
|
7368
|
-
this.
|
|
7369
|
-
});
|
|
7370
|
-
if (unsub) {
|
|
7371
|
-
this.broadcastSubscriptions.set(key, unsub);
|
|
8569
|
+
if (this.persistPromise) {
|
|
8570
|
+
await this.persistPromise;
|
|
7372
8571
|
}
|
|
7373
|
-
|
|
7374
|
-
const sub = this.broadcastSubscriptions.get(key);
|
|
7375
|
-
if (sub) {
|
|
7376
|
-
sub();
|
|
7377
|
-
this.broadcastSubscriptions.delete(key);
|
|
7378
|
-
}
|
|
7379
|
-
};
|
|
8572
|
+
await this.doPersistAll();
|
|
7380
8573
|
}
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
8574
|
+
async doPersistAll() {
|
|
8575
|
+
await Promise.all([
|
|
8576
|
+
this.persistGroups(),
|
|
8577
|
+
this.persistMessages(),
|
|
8578
|
+
this.persistMembers(),
|
|
8579
|
+
this.persistProcessedEvents()
|
|
8580
|
+
]);
|
|
7387
8581
|
}
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
8582
|
+
async persistGroups() {
|
|
8583
|
+
if (!this.deps) return;
|
|
8584
|
+
const data = Array.from(this.groups.values());
|
|
8585
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_GROUPS, JSON.stringify(data));
|
|
8586
|
+
}
|
|
8587
|
+
async persistMessages() {
|
|
8588
|
+
if (!this.deps) return;
|
|
8589
|
+
const allMessages = [];
|
|
8590
|
+
for (const msgs of this.messages.values()) {
|
|
8591
|
+
allMessages.push(...msgs);
|
|
8592
|
+
}
|
|
8593
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MESSAGES, JSON.stringify(allMessages));
|
|
8594
|
+
}
|
|
8595
|
+
async persistMembers() {
|
|
8596
|
+
if (!this.deps) return;
|
|
8597
|
+
const allMembers = [];
|
|
8598
|
+
for (const mems of this.members.values()) {
|
|
8599
|
+
allMembers.push(...mems);
|
|
8600
|
+
}
|
|
8601
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MEMBERS, JSON.stringify(allMembers));
|
|
8602
|
+
}
|
|
8603
|
+
async persistProcessedEvents() {
|
|
8604
|
+
if (!this.deps) return;
|
|
8605
|
+
const arr = Array.from(this.processedEventIds);
|
|
8606
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_PROCESSED_EVENTS, JSON.stringify(arr));
|
|
7394
8607
|
}
|
|
7395
8608
|
// ===========================================================================
|
|
7396
|
-
// Private
|
|
8609
|
+
// Private — Relay URL Change Detection
|
|
7397
8610
|
// ===========================================================================
|
|
7398
|
-
|
|
7399
|
-
if (
|
|
7400
|
-
const
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
8611
|
+
async checkAndClearOnRelayChange(currentRelayUrl) {
|
|
8612
|
+
if (!this.deps) return;
|
|
8613
|
+
const stored = await this.deps.storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_RELAY_URL);
|
|
8614
|
+
if (stored && stored !== currentRelayUrl) {
|
|
8615
|
+
this.groups.clear();
|
|
8616
|
+
this.messages.clear();
|
|
8617
|
+
this.members.clear();
|
|
8618
|
+
this.processedEventIds.clear();
|
|
8619
|
+
await this.persistAll();
|
|
8620
|
+
}
|
|
8621
|
+
if (!stored) {
|
|
8622
|
+
for (const group of this.groups.values()) {
|
|
8623
|
+
if (group.relayUrl && group.relayUrl !== currentRelayUrl) {
|
|
8624
|
+
this.groups.clear();
|
|
8625
|
+
this.messages.clear();
|
|
8626
|
+
this.members.clear();
|
|
8627
|
+
this.processedEventIds.clear();
|
|
8628
|
+
await this.persistAll();
|
|
8629
|
+
break;
|
|
8630
|
+
}
|
|
7416
8631
|
}
|
|
7417
8632
|
}
|
|
7418
|
-
|
|
7419
|
-
|
|
8633
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_RELAY_URL, currentRelayUrl);
|
|
8634
|
+
}
|
|
8635
|
+
// ===========================================================================
|
|
8636
|
+
// Private — Message Content Wrapping
|
|
8637
|
+
// ===========================================================================
|
|
8638
|
+
wrapMessageContent(content, senderNametag) {
|
|
8639
|
+
if (senderNametag) {
|
|
8640
|
+
return JSON.stringify({ senderNametag, text: content });
|
|
7420
8641
|
}
|
|
7421
|
-
|
|
8642
|
+
return content;
|
|
7422
8643
|
}
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
timestamp: incoming.timestamp,
|
|
7429
|
-
tags: incoming.tags
|
|
7430
|
-
};
|
|
7431
|
-
this.broadcasts.set(message.id, message);
|
|
7432
|
-
this.deps.emitEvent("message:broadcast", message);
|
|
7433
|
-
for (const handler of this.broadcastHandlers) {
|
|
7434
|
-
try {
|
|
7435
|
-
handler(message);
|
|
7436
|
-
} catch (error) {
|
|
7437
|
-
console.error("[Communications] Handler error:", error);
|
|
8644
|
+
unwrapMessageContent(content) {
|
|
8645
|
+
try {
|
|
8646
|
+
const parsed = JSON.parse(content);
|
|
8647
|
+
if (typeof parsed === "object" && parsed.text !== void 0) {
|
|
8648
|
+
return { text: parsed.text, senderNametag: parsed.senderNametag || null };
|
|
7438
8649
|
}
|
|
8650
|
+
} catch {
|
|
7439
8651
|
}
|
|
8652
|
+
return { text: content, senderNametag: null };
|
|
7440
8653
|
}
|
|
7441
8654
|
// ===========================================================================
|
|
7442
|
-
// Private
|
|
8655
|
+
// Private — Event Tag Helpers
|
|
7443
8656
|
// ===========================================================================
|
|
7444
|
-
|
|
7445
|
-
const
|
|
7446
|
-
|
|
8657
|
+
getGroupIdFromEvent(event) {
|
|
8658
|
+
const hTag = event.tags.find((t) => t[0] === "h");
|
|
8659
|
+
return hTag ? hTag[1] : null;
|
|
7447
8660
|
}
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
const
|
|
7452
|
-
|
|
7453
|
-
|
|
8661
|
+
getGroupIdFromMetadataEvent(event) {
|
|
8662
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
8663
|
+
if (dTag?.[1]) return dTag[1];
|
|
8664
|
+
const hTag = event.tags.find((t) => t[0] === "h");
|
|
8665
|
+
return hTag?.[1] ?? null;
|
|
8666
|
+
}
|
|
8667
|
+
extractReplyTo(event) {
|
|
8668
|
+
const eTag = event.tags.find((t) => t[0] === "e" && t[3] === "reply");
|
|
8669
|
+
return eTag ? eTag[1] : void 0;
|
|
8670
|
+
}
|
|
8671
|
+
extractPreviousIds(event) {
|
|
8672
|
+
const previousTag = event.tags.find((t) => t[0] === "previous");
|
|
8673
|
+
return previousTag ? previousTag.slice(1) : void 0;
|
|
8674
|
+
}
|
|
8675
|
+
parseGroupMetadata(event) {
|
|
8676
|
+
try {
|
|
8677
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
8678
|
+
if (!groupId) return null;
|
|
8679
|
+
let name = "Unnamed Group";
|
|
8680
|
+
let description;
|
|
8681
|
+
let picture;
|
|
8682
|
+
let isPrivate = false;
|
|
8683
|
+
if (event.content && event.content.trim()) {
|
|
8684
|
+
try {
|
|
8685
|
+
const metadata = JSON.parse(event.content);
|
|
8686
|
+
name = metadata.name || name;
|
|
8687
|
+
description = metadata.about || metadata.description;
|
|
8688
|
+
picture = metadata.picture;
|
|
8689
|
+
isPrivate = metadata.private === true;
|
|
8690
|
+
} catch {
|
|
8691
|
+
}
|
|
8692
|
+
}
|
|
8693
|
+
for (const tag of event.tags) {
|
|
8694
|
+
if (tag[0] === "name" && tag[1]) name = tag[1];
|
|
8695
|
+
if (tag[0] === "about" && tag[1]) description = tag[1];
|
|
8696
|
+
if (tag[0] === "picture" && tag[1]) picture = tag[1];
|
|
8697
|
+
if (tag[0] === "private") isPrivate = true;
|
|
8698
|
+
if (tag[0] === "public" && tag[1] === "false") isPrivate = true;
|
|
8699
|
+
}
|
|
8700
|
+
return {
|
|
8701
|
+
id: groupId,
|
|
8702
|
+
relayUrl: this.config.relays[0] || "",
|
|
8703
|
+
name,
|
|
8704
|
+
description,
|
|
8705
|
+
picture,
|
|
8706
|
+
visibility: isPrivate ? GroupVisibility.PRIVATE : GroupVisibility.PUBLIC,
|
|
8707
|
+
createdAt: event.created_at * 1e3
|
|
8708
|
+
};
|
|
8709
|
+
} catch {
|
|
8710
|
+
return null;
|
|
7454
8711
|
}
|
|
7455
8712
|
}
|
|
7456
8713
|
// ===========================================================================
|
|
7457
|
-
// Private
|
|
8714
|
+
// Private — Utility
|
|
7458
8715
|
// ===========================================================================
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
}
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
return
|
|
8716
|
+
/** Subscribe and track the subscription ID for cleanup. */
|
|
8717
|
+
trackSubscription(filter, handlers) {
|
|
8718
|
+
const subId = this.client.subscribe(filter, {
|
|
8719
|
+
onEvent: handlers.onEvent,
|
|
8720
|
+
onEndOfStoredEvents: handlers.onEndOfStoredEvents ?? (() => {
|
|
8721
|
+
})
|
|
8722
|
+
});
|
|
8723
|
+
this.subscriptionIds.push(subId);
|
|
8724
|
+
return subId;
|
|
8725
|
+
}
|
|
8726
|
+
/** Subscribe for a one-shot fetch, auto-unsubscribe on EOSE or timeout. */
|
|
8727
|
+
oneshotSubscription(filter, opts) {
|
|
8728
|
+
return new Promise((resolve) => {
|
|
8729
|
+
let done = false;
|
|
8730
|
+
let subId;
|
|
8731
|
+
const finish = () => {
|
|
8732
|
+
if (done) return;
|
|
8733
|
+
done = true;
|
|
8734
|
+
if (subId) {
|
|
8735
|
+
try {
|
|
8736
|
+
this.client.unsubscribe(subId);
|
|
8737
|
+
} catch {
|
|
8738
|
+
}
|
|
8739
|
+
const idx = this.subscriptionIds.indexOf(subId);
|
|
8740
|
+
if (idx >= 0) this.subscriptionIds.splice(idx, 1);
|
|
8741
|
+
}
|
|
8742
|
+
resolve(opts.onComplete());
|
|
8743
|
+
};
|
|
8744
|
+
subId = this.client.subscribe(filter, {
|
|
8745
|
+
onEvent: (event) => {
|
|
8746
|
+
if (!done) opts.onEvent(event);
|
|
8747
|
+
},
|
|
8748
|
+
onEndOfStoredEvents: finish
|
|
8749
|
+
});
|
|
8750
|
+
this.subscriptionIds.push(subId);
|
|
8751
|
+
setTimeout(finish, opts.timeoutMs ?? 5e3);
|
|
8752
|
+
});
|
|
7468
8753
|
}
|
|
7469
8754
|
ensureInitialized() {
|
|
7470
8755
|
if (!this.deps) {
|
|
7471
|
-
throw new Error("
|
|
8756
|
+
throw new Error("GroupChatModule not initialized");
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8759
|
+
async ensureConnected() {
|
|
8760
|
+
if (!this.connected) {
|
|
8761
|
+
await this.connect();
|
|
8762
|
+
}
|
|
8763
|
+
}
|
|
8764
|
+
randomId() {
|
|
8765
|
+
const bytes = new Uint8Array(8);
|
|
8766
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) {
|
|
8767
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
8768
|
+
} else {
|
|
8769
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
8770
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
8771
|
+
}
|
|
7472
8772
|
}
|
|
8773
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7473
8774
|
}
|
|
7474
8775
|
};
|
|
7475
|
-
function
|
|
7476
|
-
return new
|
|
8776
|
+
function createGroupChatModule(config) {
|
|
8777
|
+
return new GroupChatModule(config);
|
|
7477
8778
|
}
|
|
7478
8779
|
|
|
7479
8780
|
// core/encryption.ts
|
|
@@ -8389,12 +9690,13 @@ var Sphere = class _Sphere {
|
|
|
8389
9690
|
// Modules
|
|
8390
9691
|
_payments;
|
|
8391
9692
|
_communications;
|
|
9693
|
+
_groupChat = null;
|
|
8392
9694
|
// Events
|
|
8393
9695
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
8394
9696
|
// ===========================================================================
|
|
8395
9697
|
// Constructor (private)
|
|
8396
9698
|
// ===========================================================================
|
|
8397
|
-
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
|
|
9699
|
+
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider, groupChatConfig) {
|
|
8398
9700
|
this._storage = storage;
|
|
8399
9701
|
this._transport = transport;
|
|
8400
9702
|
this._oracle = oracle;
|
|
@@ -8404,6 +9706,7 @@ var Sphere = class _Sphere {
|
|
|
8404
9706
|
}
|
|
8405
9707
|
this._payments = createPaymentsModule({ l1: l1Config });
|
|
8406
9708
|
this._communications = createCommunicationsModule();
|
|
9709
|
+
this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
|
|
8407
9710
|
}
|
|
8408
9711
|
// ===========================================================================
|
|
8409
9712
|
// Static Methods - Wallet Management
|
|
@@ -8451,6 +9754,7 @@ var Sphere = class _Sphere {
|
|
|
8451
9754
|
* ```
|
|
8452
9755
|
*/
|
|
8453
9756
|
static async init(options) {
|
|
9757
|
+
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8454
9758
|
const walletExists = await _Sphere.exists(options.storage);
|
|
8455
9759
|
if (walletExists) {
|
|
8456
9760
|
const sphere2 = await _Sphere.load({
|
|
@@ -8459,7 +9763,8 @@ var Sphere = class _Sphere {
|
|
|
8459
9763
|
oracle: options.oracle,
|
|
8460
9764
|
tokenStorage: options.tokenStorage,
|
|
8461
9765
|
l1: options.l1,
|
|
8462
|
-
price: options.price
|
|
9766
|
+
price: options.price,
|
|
9767
|
+
groupChat
|
|
8463
9768
|
});
|
|
8464
9769
|
return { sphere: sphere2, created: false };
|
|
8465
9770
|
}
|
|
@@ -8484,10 +9789,34 @@ var Sphere = class _Sphere {
|
|
|
8484
9789
|
derivationPath: options.derivationPath,
|
|
8485
9790
|
nametag: options.nametag,
|
|
8486
9791
|
l1: options.l1,
|
|
8487
|
-
price: options.price
|
|
9792
|
+
price: options.price,
|
|
9793
|
+
groupChat
|
|
8488
9794
|
});
|
|
8489
9795
|
return { sphere, created: true, generatedMnemonic };
|
|
8490
9796
|
}
|
|
9797
|
+
/**
|
|
9798
|
+
* Resolve groupChat config from init/create/load options.
|
|
9799
|
+
* - `true` → use network-default relays
|
|
9800
|
+
* - `GroupChatModuleConfig` → pass through
|
|
9801
|
+
* - `undefined` → no groupchat
|
|
9802
|
+
*/
|
|
9803
|
+
/**
|
|
9804
|
+
* Resolve GroupChat config from Sphere.init() options.
|
|
9805
|
+
* Note: impl/shared/resolvers.ts has a similar resolver for provider-level config
|
|
9806
|
+
* (different input shape: { enabled?, relays? }). Both fill relay URLs from network defaults.
|
|
9807
|
+
*/
|
|
9808
|
+
static resolveGroupChatConfig(config, network) {
|
|
9809
|
+
if (!config) return void 0;
|
|
9810
|
+
if (config === true) {
|
|
9811
|
+
const netConfig = network ? NETWORKS[network] : NETWORKS.mainnet;
|
|
9812
|
+
return { relays: [...netConfig.groupRelays] };
|
|
9813
|
+
}
|
|
9814
|
+
if (!config.relays || config.relays.length === 0) {
|
|
9815
|
+
const netConfig = network ? NETWORKS[network] : NETWORKS.mainnet;
|
|
9816
|
+
return { ...config, relays: [...netConfig.groupRelays] };
|
|
9817
|
+
}
|
|
9818
|
+
return config;
|
|
9819
|
+
}
|
|
8491
9820
|
/**
|
|
8492
9821
|
* Create new wallet with mnemonic
|
|
8493
9822
|
*/
|
|
@@ -8498,13 +9827,15 @@ var Sphere = class _Sphere {
|
|
|
8498
9827
|
if (await _Sphere.exists(options.storage)) {
|
|
8499
9828
|
throw new Error("Wallet already exists. Use Sphere.load() or Sphere.clear() first.");
|
|
8500
9829
|
}
|
|
9830
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8501
9831
|
const sphere = new _Sphere(
|
|
8502
9832
|
options.storage,
|
|
8503
9833
|
options.transport,
|
|
8504
9834
|
options.oracle,
|
|
8505
9835
|
options.tokenStorage,
|
|
8506
9836
|
options.l1,
|
|
8507
|
-
options.price
|
|
9837
|
+
options.price,
|
|
9838
|
+
groupChatConfig
|
|
8508
9839
|
);
|
|
8509
9840
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
8510
9841
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
@@ -8529,13 +9860,15 @@ var Sphere = class _Sphere {
|
|
|
8529
9860
|
if (!await _Sphere.exists(options.storage)) {
|
|
8530
9861
|
throw new Error("No wallet found. Use Sphere.create() to create a new wallet.");
|
|
8531
9862
|
}
|
|
9863
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8532
9864
|
const sphere = new _Sphere(
|
|
8533
9865
|
options.storage,
|
|
8534
9866
|
options.transport,
|
|
8535
9867
|
options.oracle,
|
|
8536
9868
|
options.tokenStorage,
|
|
8537
9869
|
options.l1,
|
|
8538
|
-
options.price
|
|
9870
|
+
options.price,
|
|
9871
|
+
groupChatConfig
|
|
8539
9872
|
);
|
|
8540
9873
|
await sphere.loadIdentityFromStorage();
|
|
8541
9874
|
await sphere.initializeProviders();
|
|
@@ -8574,13 +9907,15 @@ var Sphere = class _Sphere {
|
|
|
8574
9907
|
await options.storage.connect();
|
|
8575
9908
|
console.log("[Sphere.import] Storage reconnected");
|
|
8576
9909
|
}
|
|
9910
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat);
|
|
8577
9911
|
const sphere = new _Sphere(
|
|
8578
9912
|
options.storage,
|
|
8579
9913
|
options.transport,
|
|
8580
9914
|
options.oracle,
|
|
8581
9915
|
options.tokenStorage,
|
|
8582
9916
|
options.l1,
|
|
8583
|
-
options.price
|
|
9917
|
+
options.price,
|
|
9918
|
+
groupChatConfig
|
|
8584
9919
|
);
|
|
8585
9920
|
if (options.mnemonic) {
|
|
8586
9921
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
@@ -8740,6 +10075,10 @@ var Sphere = class _Sphere {
|
|
|
8740
10075
|
this.ensureReady();
|
|
8741
10076
|
return this._communications;
|
|
8742
10077
|
}
|
|
10078
|
+
/** Group chat module (NIP-29). Null if not configured. */
|
|
10079
|
+
get groupChat() {
|
|
10080
|
+
return this._groupChat;
|
|
10081
|
+
}
|
|
8743
10082
|
// ===========================================================================
|
|
8744
10083
|
// Public Properties - State
|
|
8745
10084
|
// ===========================================================================
|
|
@@ -9610,8 +10949,14 @@ var Sphere = class _Sphere {
|
|
|
9610
10949
|
transport: this._transport,
|
|
9611
10950
|
emitEvent
|
|
9612
10951
|
});
|
|
10952
|
+
this._groupChat?.initialize({
|
|
10953
|
+
identity: this._identity,
|
|
10954
|
+
storage: this._storage,
|
|
10955
|
+
emitEvent
|
|
10956
|
+
});
|
|
9613
10957
|
await this._payments.load();
|
|
9614
10958
|
await this._communications.load();
|
|
10959
|
+
await this._groupChat?.load();
|
|
9615
10960
|
}
|
|
9616
10961
|
/**
|
|
9617
10962
|
* Derive address at a specific index
|
|
@@ -10214,6 +11559,7 @@ var Sphere = class _Sphere {
|
|
|
10214
11559
|
async destroy() {
|
|
10215
11560
|
this._payments.destroy();
|
|
10216
11561
|
this._communications.destroy();
|
|
11562
|
+
this._groupChat?.destroy();
|
|
10217
11563
|
await this._transport.disconnect();
|
|
10218
11564
|
await this._storage.disconnect();
|
|
10219
11565
|
await this._oracle.disconnect();
|
|
@@ -10434,8 +11780,14 @@ var Sphere = class _Sphere {
|
|
|
10434
11780
|
transport: this._transport,
|
|
10435
11781
|
emitEvent
|
|
10436
11782
|
});
|
|
11783
|
+
this._groupChat?.initialize({
|
|
11784
|
+
identity: this._identity,
|
|
11785
|
+
storage: this._storage,
|
|
11786
|
+
emitEvent
|
|
11787
|
+
});
|
|
10437
11788
|
await this._payments.load();
|
|
10438
11789
|
await this._communications.load();
|
|
11790
|
+
await this._groupChat?.load();
|
|
10439
11791
|
}
|
|
10440
11792
|
// ===========================================================================
|
|
10441
11793
|
// Private: Helpers
|