@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.cjs
CHANGED
|
@@ -2396,7 +2396,17 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2396
2396
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2397
2397
|
TRACKED_ADDRESSES: "tracked_addresses",
|
|
2398
2398
|
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
2399
|
-
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
2399
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts",
|
|
2400
|
+
/** Group chat: joined groups */
|
|
2401
|
+
GROUP_CHAT_GROUPS: "group_chat_groups",
|
|
2402
|
+
/** Group chat: messages */
|
|
2403
|
+
GROUP_CHAT_MESSAGES: "group_chat_messages",
|
|
2404
|
+
/** Group chat: members */
|
|
2405
|
+
GROUP_CHAT_MEMBERS: "group_chat_members",
|
|
2406
|
+
/** Group chat: processed event IDs for deduplication */
|
|
2407
|
+
GROUP_CHAT_PROCESSED_EVENTS: "group_chat_processed_events",
|
|
2408
|
+
/** Group chat: last used relay URL (stale data detection) */
|
|
2409
|
+
GROUP_CHAT_RELAY_URL: "group_chat_relay_url"
|
|
2400
2410
|
};
|
|
2401
2411
|
var STORAGE_KEYS_ADDRESS = {
|
|
2402
2412
|
/** Pending transfers for this address */
|
|
@@ -2427,13 +2437,95 @@ function getAddressId(directAddress) {
|
|
|
2427
2437
|
const last = hash.slice(-6).toLowerCase();
|
|
2428
2438
|
return `DIRECT_${first}_${last}`;
|
|
2429
2439
|
}
|
|
2440
|
+
var DEFAULT_NOSTR_RELAYS = [
|
|
2441
|
+
"wss://relay.unicity.network",
|
|
2442
|
+
"wss://relay.damus.io",
|
|
2443
|
+
"wss://nos.lol",
|
|
2444
|
+
"wss://relay.nostr.band"
|
|
2445
|
+
];
|
|
2446
|
+
var NIP29_KINDS = {
|
|
2447
|
+
/** Chat message sent to group */
|
|
2448
|
+
CHAT_MESSAGE: 9,
|
|
2449
|
+
/** Thread root message */
|
|
2450
|
+
THREAD_ROOT: 11,
|
|
2451
|
+
/** Thread reply message */
|
|
2452
|
+
THREAD_REPLY: 12,
|
|
2453
|
+
/** User join request */
|
|
2454
|
+
JOIN_REQUEST: 9021,
|
|
2455
|
+
/** User leave request */
|
|
2456
|
+
LEAVE_REQUEST: 9022,
|
|
2457
|
+
/** Admin: add/update user */
|
|
2458
|
+
PUT_USER: 9e3,
|
|
2459
|
+
/** Admin: remove user */
|
|
2460
|
+
REMOVE_USER: 9001,
|
|
2461
|
+
/** Admin: edit group metadata */
|
|
2462
|
+
EDIT_METADATA: 9002,
|
|
2463
|
+
/** Admin: delete event */
|
|
2464
|
+
DELETE_EVENT: 9005,
|
|
2465
|
+
/** Admin: create group */
|
|
2466
|
+
CREATE_GROUP: 9007,
|
|
2467
|
+
/** Admin: delete group */
|
|
2468
|
+
DELETE_GROUP: 9008,
|
|
2469
|
+
/** Admin: create invite code */
|
|
2470
|
+
CREATE_INVITE: 9009,
|
|
2471
|
+
/** Relay-signed group metadata */
|
|
2472
|
+
GROUP_METADATA: 39e3,
|
|
2473
|
+
/** Relay-signed group admins */
|
|
2474
|
+
GROUP_ADMINS: 39001,
|
|
2475
|
+
/** Relay-signed group members */
|
|
2476
|
+
GROUP_MEMBERS: 39002,
|
|
2477
|
+
/** Relay-signed group roles */
|
|
2478
|
+
GROUP_ROLES: 39003
|
|
2479
|
+
};
|
|
2480
|
+
var DEFAULT_AGGREGATOR_URL = "https://aggregator.unicity.network/rpc";
|
|
2481
|
+
var DEV_AGGREGATOR_URL = "https://dev-aggregator.dyndns.org/rpc";
|
|
2482
|
+
var TEST_AGGREGATOR_URL = "https://goggregator-test.unicity.network";
|
|
2483
|
+
var DEFAULT_IPFS_GATEWAYS = [
|
|
2484
|
+
"https://ipfs.unicity.network",
|
|
2485
|
+
"https://dweb.link",
|
|
2486
|
+
"https://ipfs.io"
|
|
2487
|
+
];
|
|
2430
2488
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2431
2489
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2490
|
+
var DEFAULT_ELECTRUM_URL = "wss://fulcrum.alpha.unicity.network:50004";
|
|
2491
|
+
var TEST_ELECTRUM_URL = "wss://fulcrum.alpha.testnet.unicity.network:50004";
|
|
2492
|
+
var TEST_NOSTR_RELAYS = [
|
|
2493
|
+
"wss://nostr-relay.testnet.unicity.network"
|
|
2494
|
+
];
|
|
2495
|
+
var DEFAULT_GROUP_RELAYS = [
|
|
2496
|
+
"wss://sphere-relay.unicity.network"
|
|
2497
|
+
];
|
|
2498
|
+
var NETWORKS = {
|
|
2499
|
+
mainnet: {
|
|
2500
|
+
name: "Mainnet",
|
|
2501
|
+
aggregatorUrl: DEFAULT_AGGREGATOR_URL,
|
|
2502
|
+
nostrRelays: DEFAULT_NOSTR_RELAYS,
|
|
2503
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2504
|
+
electrumUrl: DEFAULT_ELECTRUM_URL,
|
|
2505
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2506
|
+
},
|
|
2507
|
+
testnet: {
|
|
2508
|
+
name: "Testnet",
|
|
2509
|
+
aggregatorUrl: TEST_AGGREGATOR_URL,
|
|
2510
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
2511
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2512
|
+
electrumUrl: TEST_ELECTRUM_URL,
|
|
2513
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2514
|
+
},
|
|
2515
|
+
dev: {
|
|
2516
|
+
name: "Development",
|
|
2517
|
+
aggregatorUrl: DEV_AGGREGATOR_URL,
|
|
2518
|
+
nostrRelays: TEST_NOSTR_RELAYS,
|
|
2519
|
+
ipfsGateways: DEFAULT_IPFS_GATEWAYS,
|
|
2520
|
+
electrumUrl: TEST_ELECTRUM_URL,
|
|
2521
|
+
groupRelays: DEFAULT_GROUP_RELAYS
|
|
2522
|
+
}
|
|
2523
|
+
};
|
|
2432
2524
|
|
|
2433
2525
|
// types/txf.ts
|
|
2434
2526
|
var ARCHIVED_PREFIX = "archived-";
|
|
2435
2527
|
var FORKED_PREFIX = "_forked_";
|
|
2436
|
-
var RESERVED_KEYS = ["_meta", "_nametag", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2528
|
+
var RESERVED_KEYS = ["_meta", "_nametag", "_nametags", "_tombstones", "_invalidatedNametags", "_outbox", "_mintOutbox", "_sent", "_invalid", "_integrity"];
|
|
2437
2529
|
function isTokenKey(key) {
|
|
2438
2530
|
return key.startsWith("_") && !key.startsWith(ARCHIVED_PREFIX) && !key.startsWith(FORKED_PREFIX) && !RESERVED_KEYS.includes(key);
|
|
2439
2531
|
}
|
|
@@ -2469,94 +2561,388 @@ function parseForkedKey(key) {
|
|
|
2469
2561
|
};
|
|
2470
2562
|
}
|
|
2471
2563
|
|
|
2472
|
-
//
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2564
|
+
// registry/token-registry.testnet.json
|
|
2565
|
+
var token_registry_testnet_default = [
|
|
2566
|
+
{
|
|
2567
|
+
network: "unicity:testnet",
|
|
2568
|
+
assetKind: "non-fungible",
|
|
2569
|
+
name: "unicity",
|
|
2570
|
+
description: "Unicity testnet token type",
|
|
2571
|
+
id: "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509"
|
|
2572
|
+
},
|
|
2573
|
+
{
|
|
2574
|
+
network: "unicity:testnet",
|
|
2575
|
+
assetKind: "fungible",
|
|
2576
|
+
name: "unicity",
|
|
2577
|
+
symbol: "UCT",
|
|
2578
|
+
decimals: 18,
|
|
2579
|
+
description: "Unicity testnet native coin",
|
|
2580
|
+
icons: [
|
|
2581
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity_logo_32.png" }
|
|
2582
|
+
],
|
|
2583
|
+
id: "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89"
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
network: "unicity:testnet",
|
|
2587
|
+
assetKind: "fungible",
|
|
2588
|
+
name: "unicity-usd",
|
|
2589
|
+
symbol: "USDU",
|
|
2590
|
+
decimals: 6,
|
|
2591
|
+
description: "Unicity testnet USD stablecoin",
|
|
2592
|
+
icons: [
|
|
2593
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/usdu_logo_32.png" }
|
|
2594
|
+
],
|
|
2595
|
+
id: "8f0f3d7a5e7297be0ee98c63b81bcebb2740f43f616566fc290f9823a54f52d7"
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
network: "unicity:testnet",
|
|
2599
|
+
assetKind: "fungible",
|
|
2600
|
+
name: "unicity-eur",
|
|
2601
|
+
symbol: "EURU",
|
|
2602
|
+
decimals: 6,
|
|
2603
|
+
description: "Unicity testnet EUR stablecoin",
|
|
2604
|
+
icons: [
|
|
2605
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/euru_logo_32.png" }
|
|
2606
|
+
],
|
|
2607
|
+
id: "5e160d5e9fdbb03b553fb9c3f6e6c30efa41fa807be39fb4f18e43776e492925"
|
|
2608
|
+
},
|
|
2609
|
+
{
|
|
2610
|
+
network: "unicity:testnet",
|
|
2611
|
+
assetKind: "fungible",
|
|
2612
|
+
name: "solana",
|
|
2613
|
+
symbol: "SOL",
|
|
2614
|
+
decimals: 9,
|
|
2615
|
+
description: "Solana testnet coin on Unicity",
|
|
2616
|
+
icons: [
|
|
2617
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/sol.svg" },
|
|
2618
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/sol.png" }
|
|
2619
|
+
],
|
|
2620
|
+
id: "dee5f8ce778562eec90e9c38a91296a023210ccc76ff4c29d527ac3eb64ade93"
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
network: "unicity:testnet",
|
|
2624
|
+
assetKind: "fungible",
|
|
2625
|
+
name: "bitcoin",
|
|
2626
|
+
symbol: "BTC",
|
|
2627
|
+
decimals: 8,
|
|
2628
|
+
description: "Bitcoin testnet coin on Unicity",
|
|
2629
|
+
icons: [
|
|
2630
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/btc.svg" },
|
|
2631
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/btc.png" }
|
|
2632
|
+
],
|
|
2633
|
+
id: "86bc190fcf7b2d07c6078de93db803578760148b16d4431aa2f42a3241ff0daa"
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
network: "unicity:testnet",
|
|
2637
|
+
assetKind: "fungible",
|
|
2638
|
+
name: "ethereum",
|
|
2639
|
+
symbol: "ETH",
|
|
2640
|
+
decimals: 18,
|
|
2641
|
+
description: "Ethereum testnet coin on Unicity",
|
|
2642
|
+
icons: [
|
|
2643
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/eth.svg" },
|
|
2644
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/eth.png" }
|
|
2645
|
+
],
|
|
2646
|
+
id: "3c2450f2fd867e7bb60c6a69d7ad0e53ce967078c201a3ecaa6074ed4c0deafb"
|
|
2647
|
+
},
|
|
2648
|
+
{
|
|
2649
|
+
network: "unicity:testnet",
|
|
2650
|
+
assetKind: "fungible",
|
|
2651
|
+
name: "alpha_test",
|
|
2652
|
+
symbol: "ALPHT",
|
|
2653
|
+
decimals: 8,
|
|
2654
|
+
description: "ALPHA testnet coin on Unicity",
|
|
2655
|
+
icons: [
|
|
2656
|
+
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/alpha_coin.png" }
|
|
2657
|
+
],
|
|
2658
|
+
id: "cde78ded16ef65818a51f43138031c4284e519300ab0cb60c30a8f9078080e5f"
|
|
2659
|
+
},
|
|
2660
|
+
{
|
|
2661
|
+
network: "unicity:testnet",
|
|
2662
|
+
assetKind: "fungible",
|
|
2663
|
+
name: "tether",
|
|
2664
|
+
symbol: "USDT",
|
|
2665
|
+
decimals: 6,
|
|
2666
|
+
description: "Tether (Ethereum) testnet coin on Unicity",
|
|
2667
|
+
icons: [
|
|
2668
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdt.svg" },
|
|
2669
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdt.png" }
|
|
2670
|
+
],
|
|
2671
|
+
id: "40d25444648418fe7efd433e147187a3a6adf049ac62bc46038bda5b960bf690"
|
|
2672
|
+
},
|
|
2673
|
+
{
|
|
2674
|
+
network: "unicity:testnet",
|
|
2675
|
+
assetKind: "fungible",
|
|
2676
|
+
name: "usd-coin",
|
|
2677
|
+
symbol: "USDC",
|
|
2678
|
+
decimals: 6,
|
|
2679
|
+
description: "USDC (Ethereum) testnet coin on Unicity",
|
|
2680
|
+
icons: [
|
|
2681
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdc.svg" },
|
|
2682
|
+
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdc.png" }
|
|
2683
|
+
],
|
|
2684
|
+
id: "2265121770fa6f41131dd9a6cc571e28679263d09a53eb2642e145b5b9a5b0a2"
|
|
2480
2685
|
}
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2686
|
+
];
|
|
2687
|
+
|
|
2688
|
+
// registry/TokenRegistry.ts
|
|
2689
|
+
var TokenRegistry = class _TokenRegistry {
|
|
2690
|
+
static instance = null;
|
|
2691
|
+
definitionsById;
|
|
2692
|
+
definitionsBySymbol;
|
|
2693
|
+
definitionsByName;
|
|
2694
|
+
constructor() {
|
|
2695
|
+
this.definitionsById = /* @__PURE__ */ new Map();
|
|
2696
|
+
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
2697
|
+
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
2698
|
+
this.loadRegistry();
|
|
2489
2699
|
}
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
if (data.tokenId !== void 0) {
|
|
2497
|
-
data.tokenId = normalizeToHex(data.tokenId);
|
|
2498
|
-
}
|
|
2499
|
-
if (data.tokenType !== void 0) {
|
|
2500
|
-
data.tokenType = normalizeToHex(data.tokenType);
|
|
2501
|
-
}
|
|
2502
|
-
if (data.salt !== void 0) {
|
|
2503
|
-
data.salt = normalizeToHex(data.salt);
|
|
2700
|
+
/**
|
|
2701
|
+
* Get singleton instance of TokenRegistry
|
|
2702
|
+
*/
|
|
2703
|
+
static getInstance() {
|
|
2704
|
+
if (!_TokenRegistry.instance) {
|
|
2705
|
+
_TokenRegistry.instance = new _TokenRegistry();
|
|
2504
2706
|
}
|
|
2707
|
+
return _TokenRegistry.instance;
|
|
2505
2708
|
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
if (auth.signature !== void 0) {
|
|
2512
|
-
auth.signature = normalizeToHex(auth.signature);
|
|
2513
|
-
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Reset the singleton instance (useful for testing)
|
|
2711
|
+
*/
|
|
2712
|
+
static resetInstance() {
|
|
2713
|
+
_TokenRegistry.instance = null;
|
|
2514
2714
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2715
|
+
/**
|
|
2716
|
+
* Load registry data from bundled JSON
|
|
2717
|
+
*/
|
|
2718
|
+
loadRegistry() {
|
|
2719
|
+
const definitions = token_registry_testnet_default;
|
|
2720
|
+
for (const def of definitions) {
|
|
2721
|
+
const idLower = def.id.toLowerCase();
|
|
2722
|
+
this.definitionsById.set(idLower, def);
|
|
2723
|
+
if (def.symbol) {
|
|
2724
|
+
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
2525
2725
|
}
|
|
2726
|
+
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
2526
2727
|
}
|
|
2527
2728
|
}
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
if (!
|
|
2538
|
-
|
|
2539
|
-
}
|
|
2540
|
-
if (!txfData.version) {
|
|
2541
|
-
txfData.version = "2.0";
|
|
2542
|
-
}
|
|
2543
|
-
if (!txfData.transactions) {
|
|
2544
|
-
txfData.transactions = [];
|
|
2545
|
-
}
|
|
2546
|
-
if (!txfData.nametags) {
|
|
2547
|
-
txfData.nametags = [];
|
|
2548
|
-
}
|
|
2549
|
-
if (!txfData._integrity) {
|
|
2550
|
-
txfData._integrity = {
|
|
2551
|
-
genesisDataJSONHash: "0000" + "0".repeat(60)
|
|
2552
|
-
};
|
|
2553
|
-
}
|
|
2554
|
-
return txfData;
|
|
2555
|
-
} catch {
|
|
2556
|
-
return null;
|
|
2729
|
+
// ===========================================================================
|
|
2730
|
+
// Lookup Methods
|
|
2731
|
+
// ===========================================================================
|
|
2732
|
+
/**
|
|
2733
|
+
* Get token definition by hex coin ID
|
|
2734
|
+
* @param coinId - 64-character hex string
|
|
2735
|
+
* @returns Token definition or undefined if not found
|
|
2736
|
+
*/
|
|
2737
|
+
getDefinition(coinId) {
|
|
2738
|
+
if (!coinId) return void 0;
|
|
2739
|
+
return this.definitionsById.get(coinId.toLowerCase());
|
|
2557
2740
|
}
|
|
2558
|
-
|
|
2559
|
-
|
|
2741
|
+
/**
|
|
2742
|
+
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
2743
|
+
* @param symbol - Token symbol (case-insensitive)
|
|
2744
|
+
* @returns Token definition or undefined if not found
|
|
2745
|
+
*/
|
|
2746
|
+
getDefinitionBySymbol(symbol) {
|
|
2747
|
+
if (!symbol) return void 0;
|
|
2748
|
+
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
2749
|
+
}
|
|
2750
|
+
/**
|
|
2751
|
+
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
2752
|
+
* @param name - Token name (case-insensitive)
|
|
2753
|
+
* @returns Token definition or undefined if not found
|
|
2754
|
+
*/
|
|
2755
|
+
getDefinitionByName(name) {
|
|
2756
|
+
if (!name) return void 0;
|
|
2757
|
+
return this.definitionsByName.get(name.toLowerCase());
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Get token symbol for a coin ID
|
|
2761
|
+
* @param coinId - 64-character hex string
|
|
2762
|
+
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
2763
|
+
*/
|
|
2764
|
+
getSymbol(coinId) {
|
|
2765
|
+
const def = this.getDefinition(coinId);
|
|
2766
|
+
if (def?.symbol) {
|
|
2767
|
+
return def.symbol;
|
|
2768
|
+
}
|
|
2769
|
+
return coinId.slice(0, 6).toUpperCase();
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Get token name for a coin ID
|
|
2773
|
+
* @param coinId - 64-character hex string
|
|
2774
|
+
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
2775
|
+
*/
|
|
2776
|
+
getName(coinId) {
|
|
2777
|
+
const def = this.getDefinition(coinId);
|
|
2778
|
+
if (def?.name) {
|
|
2779
|
+
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
2780
|
+
}
|
|
2781
|
+
return coinId;
|
|
2782
|
+
}
|
|
2783
|
+
/**
|
|
2784
|
+
* Get decimal places for a coin ID
|
|
2785
|
+
* @param coinId - 64-character hex string
|
|
2786
|
+
* @returns Decimals or 0 if not found
|
|
2787
|
+
*/
|
|
2788
|
+
getDecimals(coinId) {
|
|
2789
|
+
const def = this.getDefinition(coinId);
|
|
2790
|
+
return def?.decimals ?? 0;
|
|
2791
|
+
}
|
|
2792
|
+
/**
|
|
2793
|
+
* Get icon URL for a coin ID
|
|
2794
|
+
* @param coinId - 64-character hex string
|
|
2795
|
+
* @param preferPng - Prefer PNG format over SVG
|
|
2796
|
+
* @returns Icon URL or null if not found
|
|
2797
|
+
*/
|
|
2798
|
+
getIconUrl(coinId, preferPng = true) {
|
|
2799
|
+
const def = this.getDefinition(coinId);
|
|
2800
|
+
if (!def?.icons || def.icons.length === 0) {
|
|
2801
|
+
return null;
|
|
2802
|
+
}
|
|
2803
|
+
if (preferPng) {
|
|
2804
|
+
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
2805
|
+
if (pngIcon) return pngIcon.url;
|
|
2806
|
+
}
|
|
2807
|
+
return def.icons[0].url;
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Check if a coin ID is known in the registry
|
|
2811
|
+
* @param coinId - 64-character hex string
|
|
2812
|
+
* @returns true if the coin is in the registry
|
|
2813
|
+
*/
|
|
2814
|
+
isKnown(coinId) {
|
|
2815
|
+
return this.definitionsById.has(coinId.toLowerCase());
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Get all token definitions
|
|
2819
|
+
* @returns Array of all token definitions
|
|
2820
|
+
*/
|
|
2821
|
+
getAllDefinitions() {
|
|
2822
|
+
return Array.from(this.definitionsById.values());
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Get all fungible token definitions
|
|
2826
|
+
* @returns Array of fungible token definitions
|
|
2827
|
+
*/
|
|
2828
|
+
getFungibleTokens() {
|
|
2829
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Get all non-fungible token definitions
|
|
2833
|
+
* @returns Array of non-fungible token definitions
|
|
2834
|
+
*/
|
|
2835
|
+
getNonFungibleTokens() {
|
|
2836
|
+
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Get coin ID by symbol
|
|
2840
|
+
* @param symbol - Token symbol (e.g., "UCT")
|
|
2841
|
+
* @returns Coin ID hex string or undefined if not found
|
|
2842
|
+
*/
|
|
2843
|
+
getCoinIdBySymbol(symbol) {
|
|
2844
|
+
const def = this.getDefinitionBySymbol(symbol);
|
|
2845
|
+
return def?.id;
|
|
2846
|
+
}
|
|
2847
|
+
/**
|
|
2848
|
+
* Get coin ID by name
|
|
2849
|
+
* @param name - Token name (e.g., "bitcoin")
|
|
2850
|
+
* @returns Coin ID hex string or undefined if not found
|
|
2851
|
+
*/
|
|
2852
|
+
getCoinIdByName(name) {
|
|
2853
|
+
const def = this.getDefinitionByName(name);
|
|
2854
|
+
return def?.id;
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
|
|
2858
|
+
// serialization/txf-serializer.ts
|
|
2859
|
+
function bytesToHex3(bytes) {
|
|
2860
|
+
const arr = Array.isArray(bytes) ? bytes : Array.from(bytes);
|
|
2861
|
+
return arr.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2862
|
+
}
|
|
2863
|
+
function normalizeToHex(value) {
|
|
2864
|
+
if (typeof value === "string") {
|
|
2865
|
+
return value;
|
|
2866
|
+
}
|
|
2867
|
+
if (value && typeof value === "object") {
|
|
2868
|
+
const obj = value;
|
|
2869
|
+
if ("bytes" in obj && (Array.isArray(obj.bytes) || obj.bytes instanceof Uint8Array)) {
|
|
2870
|
+
return bytesToHex3(obj.bytes);
|
|
2871
|
+
}
|
|
2872
|
+
if (obj.type === "Buffer" && Array.isArray(obj.data)) {
|
|
2873
|
+
return bytesToHex3(obj.data);
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
return String(value);
|
|
2877
|
+
}
|
|
2878
|
+
function normalizeSdkTokenToStorage(sdkTokenJson) {
|
|
2879
|
+
const txf = JSON.parse(JSON.stringify(sdkTokenJson));
|
|
2880
|
+
if (txf.genesis?.data) {
|
|
2881
|
+
const data = txf.genesis.data;
|
|
2882
|
+
if (data.tokenId !== void 0) {
|
|
2883
|
+
data.tokenId = normalizeToHex(data.tokenId);
|
|
2884
|
+
}
|
|
2885
|
+
if (data.tokenType !== void 0) {
|
|
2886
|
+
data.tokenType = normalizeToHex(data.tokenType);
|
|
2887
|
+
}
|
|
2888
|
+
if (data.salt !== void 0) {
|
|
2889
|
+
data.salt = normalizeToHex(data.salt);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
if (txf.genesis?.inclusionProof?.authenticator) {
|
|
2893
|
+
const auth = txf.genesis.inclusionProof.authenticator;
|
|
2894
|
+
if (auth.publicKey !== void 0) {
|
|
2895
|
+
auth.publicKey = normalizeToHex(auth.publicKey);
|
|
2896
|
+
}
|
|
2897
|
+
if (auth.signature !== void 0) {
|
|
2898
|
+
auth.signature = normalizeToHex(auth.signature);
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
if (Array.isArray(txf.transactions)) {
|
|
2902
|
+
for (const tx of txf.transactions) {
|
|
2903
|
+
if (tx.inclusionProof?.authenticator) {
|
|
2904
|
+
const auth = tx.inclusionProof.authenticator;
|
|
2905
|
+
if (auth.publicKey !== void 0) {
|
|
2906
|
+
auth.publicKey = normalizeToHex(auth.publicKey);
|
|
2907
|
+
}
|
|
2908
|
+
if (auth.signature !== void 0) {
|
|
2909
|
+
auth.signature = normalizeToHex(auth.signature);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
return txf;
|
|
2915
|
+
}
|
|
2916
|
+
function tokenToTxf(token) {
|
|
2917
|
+
const jsonData = token.sdkData;
|
|
2918
|
+
if (!jsonData) {
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
try {
|
|
2922
|
+
const txfData = normalizeSdkTokenToStorage(JSON.parse(jsonData));
|
|
2923
|
+
if (!txfData.genesis || !txfData.state) {
|
|
2924
|
+
return null;
|
|
2925
|
+
}
|
|
2926
|
+
if (!txfData.version) {
|
|
2927
|
+
txfData.version = "2.0";
|
|
2928
|
+
}
|
|
2929
|
+
if (!txfData.transactions) {
|
|
2930
|
+
txfData.transactions = [];
|
|
2931
|
+
}
|
|
2932
|
+
if (!txfData.nametags) {
|
|
2933
|
+
txfData.nametags = [];
|
|
2934
|
+
}
|
|
2935
|
+
if (!txfData._integrity) {
|
|
2936
|
+
txfData._integrity = {
|
|
2937
|
+
genesisDataJSONHash: "0000" + "0".repeat(60)
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
return txfData;
|
|
2941
|
+
} catch {
|
|
2942
|
+
return null;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
function determineTokenStatus(txf) {
|
|
2560
2946
|
if (txf.transactions.length > 0) {
|
|
2561
2947
|
const lastTx = txf.transactions[txf.transactions.length - 1];
|
|
2562
2948
|
if (lastTx.inclusionProof === null) {
|
|
@@ -2580,12 +2966,14 @@ function txfToToken(tokenId, txf) {
|
|
|
2580
2966
|
const tokenType = txf.genesis.data.tokenType;
|
|
2581
2967
|
const isNft = tokenType === "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89";
|
|
2582
2968
|
const now = Date.now();
|
|
2969
|
+
const registry = TokenRegistry.getInstance();
|
|
2970
|
+
const def = registry.getDefinition(coinId);
|
|
2583
2971
|
return {
|
|
2584
2972
|
id: tokenId,
|
|
2585
2973
|
coinId,
|
|
2586
|
-
symbol: isNft ? "NFT" :
|
|
2587
|
-
name: isNft ? "NFT" : "Token",
|
|
2588
|
-
decimals: isNft ? 0 : 8,
|
|
2974
|
+
symbol: isNft ? "NFT" : def?.symbol || coinId.slice(0, 8),
|
|
2975
|
+
name: isNft ? "NFT" : def?.name ? def.name.charAt(0).toUpperCase() + def.name.slice(1) : "Token",
|
|
2976
|
+
decimals: isNft ? 0 : def?.decimals ?? 8,
|
|
2589
2977
|
amount: totalAmount.toString(),
|
|
2590
2978
|
status: determineTokenStatus(txf),
|
|
2591
2979
|
createdAt: now,
|
|
@@ -2600,6 +2988,9 @@ async function buildTxfStorageData(tokens, meta, options) {
|
|
|
2600
2988
|
formatVersion: "2.0"
|
|
2601
2989
|
}
|
|
2602
2990
|
};
|
|
2991
|
+
if (options?.nametags && options.nametags.length > 0) {
|
|
2992
|
+
storageData._nametags = options.nametags;
|
|
2993
|
+
}
|
|
2603
2994
|
if (options?.tombstones && options.tombstones.length > 0) {
|
|
2604
2995
|
storageData._tombstones = options.tombstones;
|
|
2605
2996
|
}
|
|
@@ -2638,7 +3029,7 @@ function parseTxfStorageData(data) {
|
|
|
2638
3029
|
const result = {
|
|
2639
3030
|
tokens: [],
|
|
2640
3031
|
meta: null,
|
|
2641
|
-
|
|
3032
|
+
nametags: [],
|
|
2642
3033
|
tombstones: [],
|
|
2643
3034
|
archivedTokens: /* @__PURE__ */ new Map(),
|
|
2644
3035
|
forkedTokens: /* @__PURE__ */ new Map(),
|
|
@@ -2655,8 +3046,20 @@ function parseTxfStorageData(data) {
|
|
|
2655
3046
|
if (storageData._meta && typeof storageData._meta === "object") {
|
|
2656
3047
|
result.meta = storageData._meta;
|
|
2657
3048
|
}
|
|
3049
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
3050
|
+
if (Array.isArray(storageData._nametags)) {
|
|
3051
|
+
for (const entry of storageData._nametags) {
|
|
3052
|
+
if (entry && typeof entry === "object" && typeof entry.name === "string") {
|
|
3053
|
+
result.nametags.push(entry);
|
|
3054
|
+
seenNames.add(entry.name);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
2658
3058
|
if (storageData._nametag && typeof storageData._nametag === "object") {
|
|
2659
|
-
|
|
3059
|
+
const legacy = storageData._nametag;
|
|
3060
|
+
if (typeof legacy.name === "string" && !seenNames.has(legacy.name)) {
|
|
3061
|
+
result.nametags.push(legacy);
|
|
3062
|
+
}
|
|
2660
3063
|
}
|
|
2661
3064
|
if (storageData._tombstones && Array.isArray(storageData._tombstones)) {
|
|
2662
3065
|
for (const entry of storageData._tombstones) {
|
|
@@ -2756,300 +3159,6 @@ function getCurrentStateHash(txf) {
|
|
|
2756
3159
|
return void 0;
|
|
2757
3160
|
}
|
|
2758
3161
|
|
|
2759
|
-
// registry/token-registry.testnet.json
|
|
2760
|
-
var token_registry_testnet_default = [
|
|
2761
|
-
{
|
|
2762
|
-
network: "unicity:testnet",
|
|
2763
|
-
assetKind: "non-fungible",
|
|
2764
|
-
name: "unicity",
|
|
2765
|
-
description: "Unicity testnet token type",
|
|
2766
|
-
id: "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509"
|
|
2767
|
-
},
|
|
2768
|
-
{
|
|
2769
|
-
network: "unicity:testnet",
|
|
2770
|
-
assetKind: "fungible",
|
|
2771
|
-
name: "unicity",
|
|
2772
|
-
symbol: "UCT",
|
|
2773
|
-
decimals: 18,
|
|
2774
|
-
description: "Unicity testnet native coin",
|
|
2775
|
-
icons: [
|
|
2776
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity_logo_32.png" }
|
|
2777
|
-
],
|
|
2778
|
-
id: "455ad8720656b08e8dbd5bac1f3c73eeea5431565f6c1c3af742b1aa12d41d89"
|
|
2779
|
-
},
|
|
2780
|
-
{
|
|
2781
|
-
network: "unicity:testnet",
|
|
2782
|
-
assetKind: "fungible",
|
|
2783
|
-
name: "unicity-usd",
|
|
2784
|
-
symbol: "USDU",
|
|
2785
|
-
decimals: 6,
|
|
2786
|
-
description: "Unicity testnet USD stablecoin",
|
|
2787
|
-
icons: [
|
|
2788
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/usdu_logo_32.png" }
|
|
2789
|
-
],
|
|
2790
|
-
id: "8f0f3d7a5e7297be0ee98c63b81bcebb2740f43f616566fc290f9823a54f52d7"
|
|
2791
|
-
},
|
|
2792
|
-
{
|
|
2793
|
-
network: "unicity:testnet",
|
|
2794
|
-
assetKind: "fungible",
|
|
2795
|
-
name: "unicity-eur",
|
|
2796
|
-
symbol: "EURU",
|
|
2797
|
-
decimals: 6,
|
|
2798
|
-
description: "Unicity testnet EUR stablecoin",
|
|
2799
|
-
icons: [
|
|
2800
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/euru_logo_32.png" }
|
|
2801
|
-
],
|
|
2802
|
-
id: "5e160d5e9fdbb03b553fb9c3f6e6c30efa41fa807be39fb4f18e43776e492925"
|
|
2803
|
-
},
|
|
2804
|
-
{
|
|
2805
|
-
network: "unicity:testnet",
|
|
2806
|
-
assetKind: "fungible",
|
|
2807
|
-
name: "solana",
|
|
2808
|
-
symbol: "SOL",
|
|
2809
|
-
decimals: 9,
|
|
2810
|
-
description: "Solana testnet coin on Unicity",
|
|
2811
|
-
icons: [
|
|
2812
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/sol.svg" },
|
|
2813
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/sol.png" }
|
|
2814
|
-
],
|
|
2815
|
-
id: "dee5f8ce778562eec90e9c38a91296a023210ccc76ff4c29d527ac3eb64ade93"
|
|
2816
|
-
},
|
|
2817
|
-
{
|
|
2818
|
-
network: "unicity:testnet",
|
|
2819
|
-
assetKind: "fungible",
|
|
2820
|
-
name: "bitcoin",
|
|
2821
|
-
symbol: "BTC",
|
|
2822
|
-
decimals: 8,
|
|
2823
|
-
description: "Bitcoin testnet coin on Unicity",
|
|
2824
|
-
icons: [
|
|
2825
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/btc.svg" },
|
|
2826
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/btc.png" }
|
|
2827
|
-
],
|
|
2828
|
-
id: "86bc190fcf7b2d07c6078de93db803578760148b16d4431aa2f42a3241ff0daa"
|
|
2829
|
-
},
|
|
2830
|
-
{
|
|
2831
|
-
network: "unicity:testnet",
|
|
2832
|
-
assetKind: "fungible",
|
|
2833
|
-
name: "ethereum",
|
|
2834
|
-
symbol: "ETH",
|
|
2835
|
-
decimals: 18,
|
|
2836
|
-
description: "Ethereum testnet coin on Unicity",
|
|
2837
|
-
icons: [
|
|
2838
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/eth.svg" },
|
|
2839
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/eth.png" }
|
|
2840
|
-
],
|
|
2841
|
-
id: "3c2450f2fd867e7bb60c6a69d7ad0e53ce967078c201a3ecaa6074ed4c0deafb"
|
|
2842
|
-
},
|
|
2843
|
-
{
|
|
2844
|
-
network: "unicity:testnet",
|
|
2845
|
-
assetKind: "fungible",
|
|
2846
|
-
name: "alpha_test",
|
|
2847
|
-
symbol: "ALPHT",
|
|
2848
|
-
decimals: 8,
|
|
2849
|
-
description: "ALPHA testnet coin on Unicity",
|
|
2850
|
-
icons: [
|
|
2851
|
-
{ url: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/alpha_coin.png" }
|
|
2852
|
-
],
|
|
2853
|
-
id: "cde78ded16ef65818a51f43138031c4284e519300ab0cb60c30a8f9078080e5f"
|
|
2854
|
-
},
|
|
2855
|
-
{
|
|
2856
|
-
network: "unicity:testnet",
|
|
2857
|
-
assetKind: "fungible",
|
|
2858
|
-
name: "tether",
|
|
2859
|
-
symbol: "USDT",
|
|
2860
|
-
decimals: 6,
|
|
2861
|
-
description: "Tether (Ethereum) testnet coin on Unicity",
|
|
2862
|
-
icons: [
|
|
2863
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdt.svg" },
|
|
2864
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdt.png" }
|
|
2865
|
-
],
|
|
2866
|
-
id: "40d25444648418fe7efd433e147187a3a6adf049ac62bc46038bda5b960bf690"
|
|
2867
|
-
},
|
|
2868
|
-
{
|
|
2869
|
-
network: "unicity:testnet",
|
|
2870
|
-
assetKind: "fungible",
|
|
2871
|
-
name: "usd-coin",
|
|
2872
|
-
symbol: "USDC",
|
|
2873
|
-
decimals: 6,
|
|
2874
|
-
description: "USDC (Ethereum) testnet coin on Unicity",
|
|
2875
|
-
icons: [
|
|
2876
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/svg/icon/usdc.svg" },
|
|
2877
|
-
{ url: "https://raw.githubusercontent.com/spothq/cryptocurrency-icons/master/32/icon/usdc.png" }
|
|
2878
|
-
],
|
|
2879
|
-
id: "2265121770fa6f41131dd9a6cc571e28679263d09a53eb2642e145b5b9a5b0a2"
|
|
2880
|
-
}
|
|
2881
|
-
];
|
|
2882
|
-
|
|
2883
|
-
// registry/TokenRegistry.ts
|
|
2884
|
-
var TokenRegistry = class _TokenRegistry {
|
|
2885
|
-
static instance = null;
|
|
2886
|
-
definitionsById;
|
|
2887
|
-
definitionsBySymbol;
|
|
2888
|
-
definitionsByName;
|
|
2889
|
-
constructor() {
|
|
2890
|
-
this.definitionsById = /* @__PURE__ */ new Map();
|
|
2891
|
-
this.definitionsBySymbol = /* @__PURE__ */ new Map();
|
|
2892
|
-
this.definitionsByName = /* @__PURE__ */ new Map();
|
|
2893
|
-
this.loadRegistry();
|
|
2894
|
-
}
|
|
2895
|
-
/**
|
|
2896
|
-
* Get singleton instance of TokenRegistry
|
|
2897
|
-
*/
|
|
2898
|
-
static getInstance() {
|
|
2899
|
-
if (!_TokenRegistry.instance) {
|
|
2900
|
-
_TokenRegistry.instance = new _TokenRegistry();
|
|
2901
|
-
}
|
|
2902
|
-
return _TokenRegistry.instance;
|
|
2903
|
-
}
|
|
2904
|
-
/**
|
|
2905
|
-
* Reset the singleton instance (useful for testing)
|
|
2906
|
-
*/
|
|
2907
|
-
static resetInstance() {
|
|
2908
|
-
_TokenRegistry.instance = null;
|
|
2909
|
-
}
|
|
2910
|
-
/**
|
|
2911
|
-
* Load registry data from bundled JSON
|
|
2912
|
-
*/
|
|
2913
|
-
loadRegistry() {
|
|
2914
|
-
const definitions = token_registry_testnet_default;
|
|
2915
|
-
for (const def of definitions) {
|
|
2916
|
-
const idLower = def.id.toLowerCase();
|
|
2917
|
-
this.definitionsById.set(idLower, def);
|
|
2918
|
-
if (def.symbol) {
|
|
2919
|
-
this.definitionsBySymbol.set(def.symbol.toUpperCase(), def);
|
|
2920
|
-
}
|
|
2921
|
-
this.definitionsByName.set(def.name.toLowerCase(), def);
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
// ===========================================================================
|
|
2925
|
-
// Lookup Methods
|
|
2926
|
-
// ===========================================================================
|
|
2927
|
-
/**
|
|
2928
|
-
* Get token definition by hex coin ID
|
|
2929
|
-
* @param coinId - 64-character hex string
|
|
2930
|
-
* @returns Token definition or undefined if not found
|
|
2931
|
-
*/
|
|
2932
|
-
getDefinition(coinId) {
|
|
2933
|
-
if (!coinId) return void 0;
|
|
2934
|
-
return this.definitionsById.get(coinId.toLowerCase());
|
|
2935
|
-
}
|
|
2936
|
-
/**
|
|
2937
|
-
* Get token definition by symbol (e.g., "UCT", "BTC")
|
|
2938
|
-
* @param symbol - Token symbol (case-insensitive)
|
|
2939
|
-
* @returns Token definition or undefined if not found
|
|
2940
|
-
*/
|
|
2941
|
-
getDefinitionBySymbol(symbol) {
|
|
2942
|
-
if (!symbol) return void 0;
|
|
2943
|
-
return this.definitionsBySymbol.get(symbol.toUpperCase());
|
|
2944
|
-
}
|
|
2945
|
-
/**
|
|
2946
|
-
* Get token definition by name (e.g., "bitcoin", "ethereum")
|
|
2947
|
-
* @param name - Token name (case-insensitive)
|
|
2948
|
-
* @returns Token definition or undefined if not found
|
|
2949
|
-
*/
|
|
2950
|
-
getDefinitionByName(name) {
|
|
2951
|
-
if (!name) return void 0;
|
|
2952
|
-
return this.definitionsByName.get(name.toLowerCase());
|
|
2953
|
-
}
|
|
2954
|
-
/**
|
|
2955
|
-
* Get token symbol for a coin ID
|
|
2956
|
-
* @param coinId - 64-character hex string
|
|
2957
|
-
* @returns Symbol (e.g., "UCT") or truncated ID if not found
|
|
2958
|
-
*/
|
|
2959
|
-
getSymbol(coinId) {
|
|
2960
|
-
const def = this.getDefinition(coinId);
|
|
2961
|
-
if (def?.symbol) {
|
|
2962
|
-
return def.symbol;
|
|
2963
|
-
}
|
|
2964
|
-
return coinId.slice(0, 6).toUpperCase();
|
|
2965
|
-
}
|
|
2966
|
-
/**
|
|
2967
|
-
* Get token name for a coin ID
|
|
2968
|
-
* @param coinId - 64-character hex string
|
|
2969
|
-
* @returns Name (e.g., "Bitcoin") or coin ID if not found
|
|
2970
|
-
*/
|
|
2971
|
-
getName(coinId) {
|
|
2972
|
-
const def = this.getDefinition(coinId);
|
|
2973
|
-
if (def?.name) {
|
|
2974
|
-
return def.name.charAt(0).toUpperCase() + def.name.slice(1);
|
|
2975
|
-
}
|
|
2976
|
-
return coinId;
|
|
2977
|
-
}
|
|
2978
|
-
/**
|
|
2979
|
-
* Get decimal places for a coin ID
|
|
2980
|
-
* @param coinId - 64-character hex string
|
|
2981
|
-
* @returns Decimals or 0 if not found
|
|
2982
|
-
*/
|
|
2983
|
-
getDecimals(coinId) {
|
|
2984
|
-
const def = this.getDefinition(coinId);
|
|
2985
|
-
return def?.decimals ?? 0;
|
|
2986
|
-
}
|
|
2987
|
-
/**
|
|
2988
|
-
* Get icon URL for a coin ID
|
|
2989
|
-
* @param coinId - 64-character hex string
|
|
2990
|
-
* @param preferPng - Prefer PNG format over SVG
|
|
2991
|
-
* @returns Icon URL or null if not found
|
|
2992
|
-
*/
|
|
2993
|
-
getIconUrl(coinId, preferPng = true) {
|
|
2994
|
-
const def = this.getDefinition(coinId);
|
|
2995
|
-
if (!def?.icons || def.icons.length === 0) {
|
|
2996
|
-
return null;
|
|
2997
|
-
}
|
|
2998
|
-
if (preferPng) {
|
|
2999
|
-
const pngIcon = def.icons.find((i) => i.url.toLowerCase().includes(".png"));
|
|
3000
|
-
if (pngIcon) return pngIcon.url;
|
|
3001
|
-
}
|
|
3002
|
-
return def.icons[0].url;
|
|
3003
|
-
}
|
|
3004
|
-
/**
|
|
3005
|
-
* Check if a coin ID is known in the registry
|
|
3006
|
-
* @param coinId - 64-character hex string
|
|
3007
|
-
* @returns true if the coin is in the registry
|
|
3008
|
-
*/
|
|
3009
|
-
isKnown(coinId) {
|
|
3010
|
-
return this.definitionsById.has(coinId.toLowerCase());
|
|
3011
|
-
}
|
|
3012
|
-
/**
|
|
3013
|
-
* Get all token definitions
|
|
3014
|
-
* @returns Array of all token definitions
|
|
3015
|
-
*/
|
|
3016
|
-
getAllDefinitions() {
|
|
3017
|
-
return Array.from(this.definitionsById.values());
|
|
3018
|
-
}
|
|
3019
|
-
/**
|
|
3020
|
-
* Get all fungible token definitions
|
|
3021
|
-
* @returns Array of fungible token definitions
|
|
3022
|
-
*/
|
|
3023
|
-
getFungibleTokens() {
|
|
3024
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "fungible");
|
|
3025
|
-
}
|
|
3026
|
-
/**
|
|
3027
|
-
* Get all non-fungible token definitions
|
|
3028
|
-
* @returns Array of non-fungible token definitions
|
|
3029
|
-
*/
|
|
3030
|
-
getNonFungibleTokens() {
|
|
3031
|
-
return this.getAllDefinitions().filter((def) => def.assetKind === "non-fungible");
|
|
3032
|
-
}
|
|
3033
|
-
/**
|
|
3034
|
-
* Get coin ID by symbol
|
|
3035
|
-
* @param symbol - Token symbol (e.g., "UCT")
|
|
3036
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3037
|
-
*/
|
|
3038
|
-
getCoinIdBySymbol(symbol) {
|
|
3039
|
-
const def = this.getDefinitionBySymbol(symbol);
|
|
3040
|
-
return def?.id;
|
|
3041
|
-
}
|
|
3042
|
-
/**
|
|
3043
|
-
* Get coin ID by name
|
|
3044
|
-
* @param name - Token name (e.g., "bitcoin")
|
|
3045
|
-
* @returns Coin ID hex string or undefined if not found
|
|
3046
|
-
*/
|
|
3047
|
-
getCoinIdByName(name) {
|
|
3048
|
-
const def = this.getDefinitionByName(name);
|
|
3049
|
-
return def?.id;
|
|
3050
|
-
}
|
|
3051
|
-
};
|
|
3052
|
-
|
|
3053
3162
|
// modules/payments/InstantSplitExecutor.ts
|
|
3054
3163
|
var import_Token4 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
|
|
3055
3164
|
var import_TokenId3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
@@ -4083,7 +4192,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4083
4192
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
4084
4193
|
forkedTokens = /* @__PURE__ */ new Map();
|
|
4085
4194
|
transactionHistory = [];
|
|
4086
|
-
|
|
4195
|
+
nametags = [];
|
|
4087
4196
|
// Payment Requests State (Incoming)
|
|
4088
4197
|
paymentRequests = [];
|
|
4089
4198
|
paymentRequestHandlers = /* @__PURE__ */ new Set();
|
|
@@ -4152,7 +4261,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4152
4261
|
this.archivedTokens.clear();
|
|
4153
4262
|
this.forkedTokens.clear();
|
|
4154
4263
|
this.transactionHistory = [];
|
|
4155
|
-
this.
|
|
4264
|
+
this.nametags = [];
|
|
4156
4265
|
this.deps = deps;
|
|
4157
4266
|
this.priceProvider = deps.price ?? null;
|
|
4158
4267
|
if (this.l1) {
|
|
@@ -4201,8 +4310,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4201
4310
|
}
|
|
4202
4311
|
}
|
|
4203
4312
|
await this.loadPendingV5Tokens();
|
|
4204
|
-
await this.loadTokensFromFileStorage();
|
|
4205
|
-
await this.loadNametagFromFileStorage();
|
|
4206
4313
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
4207
4314
|
if (historyData) {
|
|
4208
4315
|
try {
|
|
@@ -4711,9 +4818,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4711
4818
|
senderPubkey,
|
|
4712
4819
|
{
|
|
4713
4820
|
findNametagToken: async (proxyAddress) => {
|
|
4714
|
-
|
|
4821
|
+
const currentNametag = this.getNametag();
|
|
4822
|
+
if (currentNametag?.token) {
|
|
4715
4823
|
try {
|
|
4716
|
-
const nametagToken = await import_Token6.Token.fromJSON(
|
|
4824
|
+
const nametagToken = await import_Token6.Token.fromJSON(currentNametag.token);
|
|
4717
4825
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
4718
4826
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
4719
4827
|
if (proxy.address === proxyAddress) {
|
|
@@ -5502,7 +5610,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5502
5610
|
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5503
5611
|
};
|
|
5504
5612
|
this.tokens.set(tokenId, confirmedToken);
|
|
5505
|
-
await this.saveTokenToFileStorage(confirmedToken);
|
|
5506
5613
|
await this.addToHistory({
|
|
5507
5614
|
type: "RECEIVED",
|
|
5508
5615
|
amount: confirmedToken.amount,
|
|
@@ -5591,9 +5698,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5591
5698
|
} catch {
|
|
5592
5699
|
}
|
|
5593
5700
|
}
|
|
5594
|
-
|
|
5701
|
+
const localNametag = this.getNametag();
|
|
5702
|
+
if (nametagTokens.length === 0 && localNametag?.token) {
|
|
5595
5703
|
try {
|
|
5596
|
-
const nametagToken = await import_Token6.Token.fromJSON(
|
|
5704
|
+
const nametagToken = await import_Token6.Token.fromJSON(localNametag.token);
|
|
5597
5705
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5598
5706
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5599
5707
|
if (proxy.address === recipientAddressStr) {
|
|
@@ -5754,125 +5862,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5754
5862
|
});
|
|
5755
5863
|
}
|
|
5756
5864
|
await this.save();
|
|
5757
|
-
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5758
|
-
await this.saveTokenToFileStorage(token);
|
|
5759
|
-
}
|
|
5760
5865
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5761
5866
|
return true;
|
|
5762
5867
|
}
|
|
5763
5868
|
/**
|
|
5764
|
-
*
|
|
5765
|
-
*
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5771
|
-
const tokenIdPrefix = sdkTokenId ? sdkTokenId.slice(0, 16) : token.id.slice(0, 16);
|
|
5772
|
-
const filename = `token-${tokenIdPrefix}-${Date.now()}`;
|
|
5773
|
-
const tokenData = {
|
|
5774
|
-
token: token.sdkData ? JSON.parse(token.sdkData) : null,
|
|
5775
|
-
receivedAt: Date.now(),
|
|
5776
|
-
meta: {
|
|
5777
|
-
id: token.id,
|
|
5778
|
-
coinId: token.coinId,
|
|
5779
|
-
symbol: token.symbol,
|
|
5780
|
-
amount: token.amount,
|
|
5781
|
-
status: token.status
|
|
5782
|
-
}
|
|
5783
|
-
};
|
|
5784
|
-
for (const [providerId, provider] of providers) {
|
|
5785
|
-
try {
|
|
5786
|
-
if (provider.saveToken) {
|
|
5787
|
-
await provider.saveToken(filename, tokenData);
|
|
5788
|
-
this.log(`Saved token file ${filename} to ${providerId}`);
|
|
5789
|
-
}
|
|
5790
|
-
} catch (error) {
|
|
5791
|
-
console.warn(`[Payments] Failed to save token to ${providerId}:`, error);
|
|
5792
|
-
}
|
|
5793
|
-
}
|
|
5794
|
-
}
|
|
5795
|
-
/**
|
|
5796
|
-
* Load tokens from file storage providers (lottery compatibility)
|
|
5797
|
-
* This loads tokens from file-based storage that may have been saved
|
|
5798
|
-
* by other applications using the same storage directory.
|
|
5799
|
-
*/
|
|
5800
|
-
async loadTokensFromFileStorage() {
|
|
5801
|
-
const providers = this.getTokenStorageProviders();
|
|
5802
|
-
if (providers.size === 0) return;
|
|
5803
|
-
for (const [providerId, provider] of providers) {
|
|
5804
|
-
if (!provider.listTokenIds || !provider.getToken) continue;
|
|
5805
|
-
try {
|
|
5806
|
-
const allIds = await provider.listTokenIds();
|
|
5807
|
-
const tokenIds = allIds.filter((id) => id.startsWith("token-"));
|
|
5808
|
-
this.log(`Found ${tokenIds.length} token files in ${providerId}`);
|
|
5809
|
-
for (const tokenId of tokenIds) {
|
|
5810
|
-
try {
|
|
5811
|
-
const fileData = await provider.getToken(tokenId);
|
|
5812
|
-
if (!fileData || typeof fileData !== "object") continue;
|
|
5813
|
-
const data = fileData;
|
|
5814
|
-
const tokenJson = data.token;
|
|
5815
|
-
if (!tokenJson) continue;
|
|
5816
|
-
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5817
|
-
continue;
|
|
5818
|
-
}
|
|
5819
|
-
let sdkTokenId;
|
|
5820
|
-
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5821
|
-
const tokenObj = tokenJson;
|
|
5822
|
-
const genesis = tokenObj.genesis;
|
|
5823
|
-
const genesisData = genesis?.data;
|
|
5824
|
-
sdkTokenId = genesisData?.tokenId;
|
|
5825
|
-
}
|
|
5826
|
-
if (sdkTokenId) {
|
|
5827
|
-
let exists = false;
|
|
5828
|
-
for (const existing of this.tokens.values()) {
|
|
5829
|
-
const existingId = extractTokenIdFromSdkData(existing.sdkData);
|
|
5830
|
-
if (existingId === sdkTokenId) {
|
|
5831
|
-
exists = true;
|
|
5832
|
-
break;
|
|
5833
|
-
}
|
|
5834
|
-
}
|
|
5835
|
-
if (exists) continue;
|
|
5836
|
-
}
|
|
5837
|
-
const tokenInfo = await parseTokenInfo(tokenJson);
|
|
5838
|
-
const token = {
|
|
5839
|
-
id: tokenInfo.tokenId ?? tokenId,
|
|
5840
|
-
coinId: tokenInfo.coinId,
|
|
5841
|
-
symbol: tokenInfo.symbol,
|
|
5842
|
-
name: tokenInfo.name,
|
|
5843
|
-
decimals: tokenInfo.decimals,
|
|
5844
|
-
iconUrl: tokenInfo.iconUrl,
|
|
5845
|
-
amount: tokenInfo.amount,
|
|
5846
|
-
status: "confirmed",
|
|
5847
|
-
createdAt: data.receivedAt || Date.now(),
|
|
5848
|
-
updatedAt: Date.now(),
|
|
5849
|
-
sdkData: typeof tokenJson === "string" ? tokenJson : JSON.stringify(tokenJson)
|
|
5850
|
-
};
|
|
5851
|
-
const loadedTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5852
|
-
const loadedStateHash = extractStateHashFromSdkData(token.sdkData);
|
|
5853
|
-
if (loadedTokenId && loadedStateHash && this.isStateTombstoned(loadedTokenId, loadedStateHash)) {
|
|
5854
|
-
this.log(`Skipping tombstoned token file ${tokenId} (${loadedTokenId.slice(0, 8)}...)`);
|
|
5855
|
-
continue;
|
|
5856
|
-
}
|
|
5857
|
-
this.tokens.set(token.id, token);
|
|
5858
|
-
this.log(`Loaded token from file: ${tokenId}`);
|
|
5859
|
-
} catch (tokenError) {
|
|
5860
|
-
console.warn(`[Payments] Failed to load token ${tokenId}:`, tokenError);
|
|
5861
|
-
}
|
|
5862
|
-
}
|
|
5863
|
-
} catch (error) {
|
|
5864
|
-
console.warn(`[Payments] Failed to load tokens from ${providerId}:`, error);
|
|
5865
|
-
}
|
|
5866
|
-
}
|
|
5867
|
-
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5868
|
-
}
|
|
5869
|
-
/**
|
|
5870
|
-
* Update an existing token or add it if not found.
|
|
5871
|
-
*
|
|
5872
|
-
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5873
|
-
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5874
|
-
*
|
|
5875
|
-
* @param token - The token with updated data. Must include a valid `id`.
|
|
5869
|
+
* Update an existing token or add it if not found.
|
|
5870
|
+
*
|
|
5871
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5872
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5873
|
+
*
|
|
5874
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5876
5875
|
*/
|
|
5877
5876
|
async updateToken(token) {
|
|
5878
5877
|
this.ensureInitialized();
|
|
@@ -5924,7 +5923,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5924
5923
|
this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
|
|
5925
5924
|
}
|
|
5926
5925
|
this.tokens.delete(tokenId);
|
|
5927
|
-
await this.deleteTokenFiles(token);
|
|
5928
5926
|
if (!skipHistory && token.coinId && token.amount) {
|
|
5929
5927
|
await this.addToHistory({
|
|
5930
5928
|
type: "SENT",
|
|
@@ -5937,31 +5935,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5937
5935
|
}
|
|
5938
5936
|
await this.save();
|
|
5939
5937
|
}
|
|
5940
|
-
/**
|
|
5941
|
-
* Delete physical token file(s) from all storage providers.
|
|
5942
|
-
* Finds files by matching the SDK token ID prefix in the filename.
|
|
5943
|
-
*/
|
|
5944
|
-
async deleteTokenFiles(token) {
|
|
5945
|
-
const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
|
|
5946
|
-
if (!sdkTokenId) return;
|
|
5947
|
-
const tokenIdPrefix = sdkTokenId.slice(0, 16);
|
|
5948
|
-
const providers = this.getTokenStorageProviders();
|
|
5949
|
-
for (const [providerId, provider] of providers) {
|
|
5950
|
-
if (!provider.listTokenIds || !provider.deleteToken) continue;
|
|
5951
|
-
try {
|
|
5952
|
-
const allIds = await provider.listTokenIds();
|
|
5953
|
-
const matchingFiles = allIds.filter(
|
|
5954
|
-
(id) => id.startsWith(`token-${tokenIdPrefix}`)
|
|
5955
|
-
);
|
|
5956
|
-
for (const fileId of matchingFiles) {
|
|
5957
|
-
await provider.deleteToken(fileId);
|
|
5958
|
-
this.log(`Deleted token file ${fileId} from ${providerId}`);
|
|
5959
|
-
}
|
|
5960
|
-
} catch (error) {
|
|
5961
|
-
console.warn(`[Payments] Failed to delete token files from ${providerId}:`, error);
|
|
5962
|
-
}
|
|
5963
|
-
}
|
|
5964
|
-
}
|
|
5965
5938
|
// ===========================================================================
|
|
5966
5939
|
// Public API - Tombstones
|
|
5967
5940
|
// ===========================================================================
|
|
@@ -6217,18 +6190,30 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6217
6190
|
*/
|
|
6218
6191
|
async setNametag(nametag) {
|
|
6219
6192
|
this.ensureInitialized();
|
|
6220
|
-
this.
|
|
6193
|
+
const idx = this.nametags.findIndex((n) => n.name === nametag.name);
|
|
6194
|
+
if (idx >= 0) {
|
|
6195
|
+
this.nametags[idx] = nametag;
|
|
6196
|
+
} else {
|
|
6197
|
+
this.nametags.push(nametag);
|
|
6198
|
+
}
|
|
6221
6199
|
await this.save();
|
|
6222
|
-
await this.saveNametagToFileStorage(nametag);
|
|
6223
6200
|
this.log(`Nametag set: ${nametag.name}`);
|
|
6224
6201
|
}
|
|
6225
6202
|
/**
|
|
6226
|
-
* Get the current nametag data.
|
|
6203
|
+
* Get the current (first) nametag data.
|
|
6227
6204
|
*
|
|
6228
6205
|
* @returns The nametag data, or `null` if no nametag is set.
|
|
6229
6206
|
*/
|
|
6230
6207
|
getNametag() {
|
|
6231
|
-
return this.
|
|
6208
|
+
return this.nametags[0] ?? null;
|
|
6209
|
+
}
|
|
6210
|
+
/**
|
|
6211
|
+
* Get all nametag data entries.
|
|
6212
|
+
*
|
|
6213
|
+
* @returns A copy of the nametags array.
|
|
6214
|
+
*/
|
|
6215
|
+
getNametags() {
|
|
6216
|
+
return [...this.nametags];
|
|
6232
6217
|
}
|
|
6233
6218
|
/**
|
|
6234
6219
|
* Check whether a nametag is currently set.
|
|
@@ -6236,77 +6221,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6236
6221
|
* @returns `true` if nametag data is present.
|
|
6237
6222
|
*/
|
|
6238
6223
|
hasNametag() {
|
|
6239
|
-
return this.
|
|
6224
|
+
return this.nametags.length > 0;
|
|
6240
6225
|
}
|
|
6241
6226
|
/**
|
|
6242
|
-
* Remove
|
|
6227
|
+
* Remove all nametag data from memory and storage.
|
|
6243
6228
|
*/
|
|
6244
6229
|
async clearNametag() {
|
|
6245
6230
|
this.ensureInitialized();
|
|
6246
|
-
this.
|
|
6231
|
+
this.nametags = [];
|
|
6247
6232
|
await this.save();
|
|
6248
6233
|
}
|
|
6249
|
-
/**
|
|
6250
|
-
* Save nametag to file storage for lottery compatibility
|
|
6251
|
-
* Creates file: nametag-{name}.json
|
|
6252
|
-
*/
|
|
6253
|
-
async saveNametagToFileStorage(nametag) {
|
|
6254
|
-
const providers = this.getTokenStorageProviders();
|
|
6255
|
-
if (providers.size === 0) return;
|
|
6256
|
-
const filename = `nametag-${nametag.name}`;
|
|
6257
|
-
const fileData = {
|
|
6258
|
-
nametag: nametag.name,
|
|
6259
|
-
token: nametag.token,
|
|
6260
|
-
timestamp: nametag.timestamp || Date.now()
|
|
6261
|
-
};
|
|
6262
|
-
for (const [providerId, provider] of providers) {
|
|
6263
|
-
try {
|
|
6264
|
-
if (provider.saveToken) {
|
|
6265
|
-
await provider.saveToken(filename, fileData);
|
|
6266
|
-
this.log(`Saved nametag file ${filename} to ${providerId}`);
|
|
6267
|
-
}
|
|
6268
|
-
} catch (error) {
|
|
6269
|
-
console.warn(`[Payments] Failed to save nametag to ${providerId}:`, error);
|
|
6270
|
-
}
|
|
6271
|
-
}
|
|
6272
|
-
}
|
|
6273
|
-
/**
|
|
6274
|
-
* Load nametag from file storage (lottery compatibility)
|
|
6275
|
-
* Looks for file: nametag-{name}.json
|
|
6276
|
-
*/
|
|
6277
|
-
async loadNametagFromFileStorage() {
|
|
6278
|
-
if (this.nametag) return;
|
|
6279
|
-
const providers = this.getTokenStorageProviders();
|
|
6280
|
-
if (providers.size === 0) return;
|
|
6281
|
-
for (const [providerId, provider] of providers) {
|
|
6282
|
-
if (!provider.listTokenIds || !provider.getToken) continue;
|
|
6283
|
-
try {
|
|
6284
|
-
const tokenIds = await provider.listTokenIds();
|
|
6285
|
-
const nametagFiles = tokenIds.filter((id) => id.startsWith("nametag-"));
|
|
6286
|
-
for (const nametagFile of nametagFiles) {
|
|
6287
|
-
try {
|
|
6288
|
-
const fileData = await provider.getToken(nametagFile);
|
|
6289
|
-
if (!fileData || typeof fileData !== "object") continue;
|
|
6290
|
-
const data = fileData;
|
|
6291
|
-
if (!data.token || !data.nametag) continue;
|
|
6292
|
-
this.nametag = {
|
|
6293
|
-
name: data.nametag,
|
|
6294
|
-
token: data.token,
|
|
6295
|
-
timestamp: data.timestamp || Date.now(),
|
|
6296
|
-
format: "lottery",
|
|
6297
|
-
version: "1.0"
|
|
6298
|
-
};
|
|
6299
|
-
this.log(`Loaded nametag from file: ${nametagFile}`);
|
|
6300
|
-
return;
|
|
6301
|
-
} catch (fileError) {
|
|
6302
|
-
console.warn(`[Payments] Failed to load nametag file ${nametagFile}:`, fileError);
|
|
6303
|
-
}
|
|
6304
|
-
}
|
|
6305
|
-
} catch (error) {
|
|
6306
|
-
console.warn(`[Payments] Failed to search nametag files in ${providerId}:`, error);
|
|
6307
|
-
}
|
|
6308
|
-
}
|
|
6309
|
-
}
|
|
6310
6234
|
/**
|
|
6311
6235
|
* Mint a nametag token on-chain (like Sphere wallet and lottery)
|
|
6312
6236
|
* This creates the nametag token required for receiving tokens via PROXY addresses
|
|
@@ -6794,10 +6718,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6794
6718
|
let nametagTokens = [];
|
|
6795
6719
|
if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
|
|
6796
6720
|
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
6797
|
-
|
|
6721
|
+
const proxyNametag = this.getNametag();
|
|
6722
|
+
if (!proxyNametag?.token) {
|
|
6798
6723
|
throw new Error("Cannot finalize PROXY transfer - no nametag token");
|
|
6799
6724
|
}
|
|
6800
|
-
const nametagToken = await import_Token6.Token.fromJSON(
|
|
6725
|
+
const nametagToken = await import_Token6.Token.fromJSON(proxyNametag.token);
|
|
6801
6726
|
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
6802
6727
|
if (proxy.address !== recipientAddress.address) {
|
|
6803
6728
|
throw new Error(
|
|
@@ -6858,7 +6783,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6858
6783
|
};
|
|
6859
6784
|
this.tokens.set(tokenId, finalizedToken);
|
|
6860
6785
|
await this.save();
|
|
6861
|
-
await this.saveTokenToFileStorage(finalizedToken);
|
|
6862
6786
|
this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
|
|
6863
6787
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6864
6788
|
id: crypto.randomUUID(),
|
|
@@ -6902,9 +6826,6 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6902
6826
|
if (instantBundle) {
|
|
6903
6827
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6904
6828
|
try {
|
|
6905
|
-
if (!this.nametag) {
|
|
6906
|
-
await this.loadNametagFromFileStorage();
|
|
6907
|
-
}
|
|
6908
6829
|
const result = await this.processInstantSplitBundle(
|
|
6909
6830
|
instantBundle,
|
|
6910
6831
|
transfer.senderTransportPubkey
|
|
@@ -7104,6 +7025,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7104
7025
|
ipnsName: this.deps.identity.ipnsName ?? ""
|
|
7105
7026
|
},
|
|
7106
7027
|
{
|
|
7028
|
+
nametags: this.nametags,
|
|
7107
7029
|
tombstones: this.tombstones,
|
|
7108
7030
|
archivedTokens: this.archivedTokens,
|
|
7109
7031
|
forkedTokens: this.forkedTokens
|
|
@@ -7125,9 +7047,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
7125
7047
|
}
|
|
7126
7048
|
this.archivedTokens = parsed.archivedTokens;
|
|
7127
7049
|
this.forkedTokens = parsed.forkedTokens;
|
|
7128
|
-
|
|
7129
|
-
this.nametag = parsed.nametag;
|
|
7130
|
-
}
|
|
7050
|
+
this.nametags = parsed.nametags;
|
|
7131
7051
|
}
|
|
7132
7052
|
// ===========================================================================
|
|
7133
7053
|
// Private: NOSTR-FIRST Proof Polling
|
|
@@ -7345,228 +7265,1605 @@ var CommunicationsModule = class {
|
|
|
7345
7265
|
this.broadcastSubscriptions.clear();
|
|
7346
7266
|
}
|
|
7347
7267
|
// ===========================================================================
|
|
7348
|
-
// Public API - Direct Messages
|
|
7268
|
+
// Public API - Direct Messages
|
|
7269
|
+
// ===========================================================================
|
|
7270
|
+
/**
|
|
7271
|
+
* Send direct message
|
|
7272
|
+
*/
|
|
7273
|
+
async sendDM(recipient, content) {
|
|
7274
|
+
this.ensureInitialized();
|
|
7275
|
+
const recipientPubkey = await this.resolveRecipient(recipient);
|
|
7276
|
+
const eventId = await this.deps.transport.sendMessage(recipientPubkey, content);
|
|
7277
|
+
const message = {
|
|
7278
|
+
id: eventId,
|
|
7279
|
+
senderPubkey: this.deps.identity.chainPubkey,
|
|
7280
|
+
senderNametag: this.deps.identity.nametag,
|
|
7281
|
+
recipientPubkey,
|
|
7282
|
+
content,
|
|
7283
|
+
timestamp: Date.now(),
|
|
7284
|
+
isRead: true
|
|
7285
|
+
};
|
|
7286
|
+
this.messages.set(message.id, message);
|
|
7287
|
+
if (this.config.autoSave) {
|
|
7288
|
+
await this.save();
|
|
7289
|
+
}
|
|
7290
|
+
return message;
|
|
7291
|
+
}
|
|
7292
|
+
/**
|
|
7293
|
+
* Get conversation with peer
|
|
7294
|
+
*/
|
|
7295
|
+
getConversation(peerPubkey) {
|
|
7296
|
+
return Array.from(this.messages.values()).filter(
|
|
7297
|
+
(m) => m.senderPubkey === peerPubkey || m.recipientPubkey === peerPubkey
|
|
7298
|
+
).sort((a, b) => a.timestamp - b.timestamp);
|
|
7299
|
+
}
|
|
7300
|
+
/**
|
|
7301
|
+
* Get all conversations grouped by peer
|
|
7302
|
+
*/
|
|
7303
|
+
getConversations() {
|
|
7304
|
+
const conversations = /* @__PURE__ */ new Map();
|
|
7305
|
+
for (const message of this.messages.values()) {
|
|
7306
|
+
const peer = message.senderPubkey === this.deps?.identity.chainPubkey ? message.recipientPubkey : message.senderPubkey;
|
|
7307
|
+
if (!conversations.has(peer)) {
|
|
7308
|
+
conversations.set(peer, []);
|
|
7309
|
+
}
|
|
7310
|
+
conversations.get(peer).push(message);
|
|
7311
|
+
}
|
|
7312
|
+
for (const msgs of conversations.values()) {
|
|
7313
|
+
msgs.sort((a, b) => a.timestamp - b.timestamp);
|
|
7314
|
+
}
|
|
7315
|
+
return conversations;
|
|
7316
|
+
}
|
|
7317
|
+
/**
|
|
7318
|
+
* Mark messages as read
|
|
7319
|
+
*/
|
|
7320
|
+
async markAsRead(messageIds) {
|
|
7321
|
+
for (const id of messageIds) {
|
|
7322
|
+
const msg = this.messages.get(id);
|
|
7323
|
+
if (msg) {
|
|
7324
|
+
msg.isRead = true;
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
if (this.config.autoSave) {
|
|
7328
|
+
await this.save();
|
|
7329
|
+
}
|
|
7330
|
+
}
|
|
7331
|
+
/**
|
|
7332
|
+
* Get unread count
|
|
7333
|
+
*/
|
|
7334
|
+
getUnreadCount(peerPubkey) {
|
|
7335
|
+
let messages = Array.from(this.messages.values()).filter(
|
|
7336
|
+
(m) => !m.isRead && m.senderPubkey !== this.deps?.identity.chainPubkey
|
|
7337
|
+
);
|
|
7338
|
+
if (peerPubkey) {
|
|
7339
|
+
messages = messages.filter((m) => m.senderPubkey === peerPubkey);
|
|
7340
|
+
}
|
|
7341
|
+
return messages.length;
|
|
7342
|
+
}
|
|
7343
|
+
/**
|
|
7344
|
+
* Subscribe to incoming DMs
|
|
7345
|
+
*/
|
|
7346
|
+
onDirectMessage(handler) {
|
|
7347
|
+
this.dmHandlers.add(handler);
|
|
7348
|
+
return () => this.dmHandlers.delete(handler);
|
|
7349
|
+
}
|
|
7350
|
+
// ===========================================================================
|
|
7351
|
+
// Public API - Broadcasts
|
|
7352
|
+
// ===========================================================================
|
|
7353
|
+
/**
|
|
7354
|
+
* Publish broadcast message
|
|
7355
|
+
*/
|
|
7356
|
+
async broadcast(content, tags) {
|
|
7357
|
+
this.ensureInitialized();
|
|
7358
|
+
const eventId = await this.deps.transport.publishBroadcast?.(content, tags);
|
|
7359
|
+
const message = {
|
|
7360
|
+
id: eventId ?? crypto.randomUUID(),
|
|
7361
|
+
authorPubkey: this.deps.identity.chainPubkey,
|
|
7362
|
+
authorNametag: this.deps.identity.nametag,
|
|
7363
|
+
content,
|
|
7364
|
+
timestamp: Date.now(),
|
|
7365
|
+
tags
|
|
7366
|
+
};
|
|
7367
|
+
this.broadcasts.set(message.id, message);
|
|
7368
|
+
return message;
|
|
7369
|
+
}
|
|
7370
|
+
/**
|
|
7371
|
+
* Subscribe to broadcasts with tags
|
|
7372
|
+
*/
|
|
7373
|
+
subscribeToBroadcasts(tags) {
|
|
7374
|
+
this.ensureInitialized();
|
|
7375
|
+
const key = tags.sort().join(":");
|
|
7376
|
+
if (this.broadcastSubscriptions.has(key)) {
|
|
7377
|
+
return () => {
|
|
7378
|
+
};
|
|
7379
|
+
}
|
|
7380
|
+
const unsub = this.deps.transport.subscribeToBroadcast?.(tags, (broadcast2) => {
|
|
7381
|
+
this.handleIncomingBroadcast(broadcast2);
|
|
7382
|
+
});
|
|
7383
|
+
if (unsub) {
|
|
7384
|
+
this.broadcastSubscriptions.set(key, unsub);
|
|
7385
|
+
}
|
|
7386
|
+
return () => {
|
|
7387
|
+
const sub = this.broadcastSubscriptions.get(key);
|
|
7388
|
+
if (sub) {
|
|
7389
|
+
sub();
|
|
7390
|
+
this.broadcastSubscriptions.delete(key);
|
|
7391
|
+
}
|
|
7392
|
+
};
|
|
7393
|
+
}
|
|
7394
|
+
/**
|
|
7395
|
+
* Get broadcasts
|
|
7396
|
+
*/
|
|
7397
|
+
getBroadcasts(limit) {
|
|
7398
|
+
const messages = Array.from(this.broadcasts.values()).sort((a, b) => b.timestamp - a.timestamp);
|
|
7399
|
+
return limit ? messages.slice(0, limit) : messages;
|
|
7400
|
+
}
|
|
7401
|
+
/**
|
|
7402
|
+
* Subscribe to incoming broadcasts
|
|
7403
|
+
*/
|
|
7404
|
+
onBroadcast(handler) {
|
|
7405
|
+
this.broadcastHandlers.add(handler);
|
|
7406
|
+
return () => this.broadcastHandlers.delete(handler);
|
|
7407
|
+
}
|
|
7408
|
+
// ===========================================================================
|
|
7409
|
+
// Private: Message Handling
|
|
7410
|
+
// ===========================================================================
|
|
7411
|
+
handleIncomingMessage(msg) {
|
|
7412
|
+
if (msg.senderTransportPubkey === this.deps?.identity.chainPubkey) return;
|
|
7413
|
+
const message = {
|
|
7414
|
+
id: msg.id,
|
|
7415
|
+
senderPubkey: msg.senderTransportPubkey,
|
|
7416
|
+
senderNametag: msg.senderNametag,
|
|
7417
|
+
recipientPubkey: this.deps.identity.chainPubkey,
|
|
7418
|
+
content: msg.content,
|
|
7419
|
+
timestamp: msg.timestamp,
|
|
7420
|
+
isRead: false
|
|
7421
|
+
};
|
|
7422
|
+
this.messages.set(message.id, message);
|
|
7423
|
+
this.deps.emitEvent("message:dm", message);
|
|
7424
|
+
for (const handler of this.dmHandlers) {
|
|
7425
|
+
try {
|
|
7426
|
+
handler(message);
|
|
7427
|
+
} catch (error) {
|
|
7428
|
+
console.error("[Communications] Handler error:", error);
|
|
7429
|
+
}
|
|
7430
|
+
}
|
|
7431
|
+
if (this.config.autoSave) {
|
|
7432
|
+
this.save();
|
|
7433
|
+
}
|
|
7434
|
+
this.pruneIfNeeded();
|
|
7435
|
+
}
|
|
7436
|
+
handleIncomingBroadcast(incoming) {
|
|
7437
|
+
const message = {
|
|
7438
|
+
id: incoming.id,
|
|
7439
|
+
authorPubkey: incoming.authorTransportPubkey,
|
|
7440
|
+
content: incoming.content,
|
|
7441
|
+
timestamp: incoming.timestamp,
|
|
7442
|
+
tags: incoming.tags
|
|
7443
|
+
};
|
|
7444
|
+
this.broadcasts.set(message.id, message);
|
|
7445
|
+
this.deps.emitEvent("message:broadcast", message);
|
|
7446
|
+
for (const handler of this.broadcastHandlers) {
|
|
7447
|
+
try {
|
|
7448
|
+
handler(message);
|
|
7449
|
+
} catch (error) {
|
|
7450
|
+
console.error("[Communications] Handler error:", error);
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
}
|
|
7454
|
+
// ===========================================================================
|
|
7455
|
+
// Private: Storage
|
|
7456
|
+
// ===========================================================================
|
|
7457
|
+
async save() {
|
|
7458
|
+
const messages = Array.from(this.messages.values());
|
|
7459
|
+
await this.deps.storage.set("direct_messages", JSON.stringify(messages));
|
|
7460
|
+
}
|
|
7461
|
+
pruneIfNeeded() {
|
|
7462
|
+
if (this.messages.size <= this.config.maxMessages) return;
|
|
7463
|
+
const sorted = Array.from(this.messages.entries()).sort(([, a], [, b]) => a.timestamp - b.timestamp);
|
|
7464
|
+
const toRemove = sorted.slice(0, sorted.length - this.config.maxMessages);
|
|
7465
|
+
for (const [id] of toRemove) {
|
|
7466
|
+
this.messages.delete(id);
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
// ===========================================================================
|
|
7470
|
+
// Private: Helpers
|
|
7471
|
+
// ===========================================================================
|
|
7472
|
+
async resolveRecipient(recipient) {
|
|
7473
|
+
if (recipient.startsWith("@")) {
|
|
7474
|
+
const pubkey = await this.deps.transport.resolveNametag?.(recipient.slice(1));
|
|
7475
|
+
if (!pubkey) {
|
|
7476
|
+
throw new Error(`Nametag not found: ${recipient}`);
|
|
7477
|
+
}
|
|
7478
|
+
return pubkey;
|
|
7479
|
+
}
|
|
7480
|
+
return recipient;
|
|
7481
|
+
}
|
|
7482
|
+
ensureInitialized() {
|
|
7483
|
+
if (!this.deps) {
|
|
7484
|
+
throw new Error("CommunicationsModule not initialized");
|
|
7485
|
+
}
|
|
7486
|
+
}
|
|
7487
|
+
};
|
|
7488
|
+
function createCommunicationsModule(config) {
|
|
7489
|
+
return new CommunicationsModule(config);
|
|
7490
|
+
}
|
|
7491
|
+
|
|
7492
|
+
// modules/groupchat/GroupChatModule.ts
|
|
7493
|
+
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
7494
|
+
|
|
7495
|
+
// modules/groupchat/types.ts
|
|
7496
|
+
var GroupRole = {
|
|
7497
|
+
ADMIN: "ADMIN",
|
|
7498
|
+
MODERATOR: "MODERATOR",
|
|
7499
|
+
MEMBER: "MEMBER"
|
|
7500
|
+
};
|
|
7501
|
+
var GroupVisibility = {
|
|
7502
|
+
PUBLIC: "PUBLIC",
|
|
7503
|
+
PRIVATE: "PRIVATE"
|
|
7504
|
+
};
|
|
7505
|
+
|
|
7506
|
+
// modules/groupchat/GroupChatModule.ts
|
|
7507
|
+
function createNip29Filter(data) {
|
|
7508
|
+
return new import_nostr_js_sdk2.Filter(data);
|
|
7509
|
+
}
|
|
7510
|
+
var GroupChatModule = class {
|
|
7511
|
+
config;
|
|
7512
|
+
deps = null;
|
|
7513
|
+
// Nostr connection (separate from wallet relay)
|
|
7514
|
+
client = null;
|
|
7515
|
+
keyManager = null;
|
|
7516
|
+
connected = false;
|
|
7517
|
+
connecting = false;
|
|
7518
|
+
connectPromise = null;
|
|
7519
|
+
reconnectAttempts = 0;
|
|
7520
|
+
reconnectTimer = null;
|
|
7521
|
+
// Subscription tracking (for cleanup)
|
|
7522
|
+
subscriptionIds = [];
|
|
7523
|
+
// In-memory state
|
|
7524
|
+
groups = /* @__PURE__ */ new Map();
|
|
7525
|
+
messages = /* @__PURE__ */ new Map();
|
|
7526
|
+
// groupId -> messages
|
|
7527
|
+
members = /* @__PURE__ */ new Map();
|
|
7528
|
+
// groupId -> members
|
|
7529
|
+
processedEventIds = /* @__PURE__ */ new Set();
|
|
7530
|
+
pendingLeaves = /* @__PURE__ */ new Set();
|
|
7531
|
+
// Persistence debounce
|
|
7532
|
+
persistTimer = null;
|
|
7533
|
+
persistPromise = null;
|
|
7534
|
+
// Relay admin cache
|
|
7535
|
+
relayAdminPubkeys = null;
|
|
7536
|
+
relayAdminFetchPromise = null;
|
|
7537
|
+
// Listeners
|
|
7538
|
+
messageHandlers = /* @__PURE__ */ new Set();
|
|
7539
|
+
constructor(config) {
|
|
7540
|
+
this.config = {
|
|
7541
|
+
relays: config?.relays ?? [],
|
|
7542
|
+
defaultMessageLimit: config?.defaultMessageLimit ?? 50,
|
|
7543
|
+
maxPreviousTags: config?.maxPreviousTags ?? 3,
|
|
7544
|
+
reconnectDelayMs: config?.reconnectDelayMs ?? 3e3,
|
|
7545
|
+
maxReconnectAttempts: config?.maxReconnectAttempts ?? 5
|
|
7546
|
+
};
|
|
7547
|
+
}
|
|
7548
|
+
// ===========================================================================
|
|
7549
|
+
// Lifecycle
|
|
7550
|
+
// ===========================================================================
|
|
7551
|
+
initialize(deps) {
|
|
7552
|
+
if (this.deps) {
|
|
7553
|
+
this.destroyConnection();
|
|
7554
|
+
}
|
|
7555
|
+
this.deps = deps;
|
|
7556
|
+
const secretKey = Buffer.from(deps.identity.privateKey, "hex");
|
|
7557
|
+
this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
|
|
7558
|
+
}
|
|
7559
|
+
async load() {
|
|
7560
|
+
this.ensureInitialized();
|
|
7561
|
+
const storage = this.deps.storage;
|
|
7562
|
+
const groupsJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_GROUPS);
|
|
7563
|
+
if (groupsJson) {
|
|
7564
|
+
try {
|
|
7565
|
+
const parsed = JSON.parse(groupsJson);
|
|
7566
|
+
this.groups.clear();
|
|
7567
|
+
for (const g of parsed) {
|
|
7568
|
+
this.groups.set(g.id, g);
|
|
7569
|
+
}
|
|
7570
|
+
} catch {
|
|
7571
|
+
}
|
|
7572
|
+
}
|
|
7573
|
+
const messagesJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MESSAGES);
|
|
7574
|
+
if (messagesJson) {
|
|
7575
|
+
try {
|
|
7576
|
+
const parsed = JSON.parse(messagesJson);
|
|
7577
|
+
this.messages.clear();
|
|
7578
|
+
for (const m of parsed) {
|
|
7579
|
+
const groupId = m.groupId;
|
|
7580
|
+
if (!this.messages.has(groupId)) {
|
|
7581
|
+
this.messages.set(groupId, []);
|
|
7582
|
+
}
|
|
7583
|
+
this.messages.get(groupId).push(m);
|
|
7584
|
+
}
|
|
7585
|
+
} catch {
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
const membersJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MEMBERS);
|
|
7589
|
+
if (membersJson) {
|
|
7590
|
+
try {
|
|
7591
|
+
const parsed = JSON.parse(membersJson);
|
|
7592
|
+
this.members.clear();
|
|
7593
|
+
for (const m of parsed) {
|
|
7594
|
+
const groupId = m.groupId;
|
|
7595
|
+
if (!this.members.has(groupId)) {
|
|
7596
|
+
this.members.set(groupId, []);
|
|
7597
|
+
}
|
|
7598
|
+
this.members.get(groupId).push(m);
|
|
7599
|
+
}
|
|
7600
|
+
} catch {
|
|
7601
|
+
}
|
|
7602
|
+
}
|
|
7603
|
+
const processedJson = await storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_PROCESSED_EVENTS);
|
|
7604
|
+
if (processedJson) {
|
|
7605
|
+
try {
|
|
7606
|
+
const parsed = JSON.parse(processedJson);
|
|
7607
|
+
this.processedEventIds = new Set(parsed);
|
|
7608
|
+
} catch {
|
|
7609
|
+
}
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
destroy() {
|
|
7613
|
+
this.destroyConnection();
|
|
7614
|
+
this.groups.clear();
|
|
7615
|
+
this.messages.clear();
|
|
7616
|
+
this.members.clear();
|
|
7617
|
+
this.processedEventIds.clear();
|
|
7618
|
+
this.pendingLeaves.clear();
|
|
7619
|
+
this.messageHandlers.clear();
|
|
7620
|
+
this.relayAdminPubkeys = null;
|
|
7621
|
+
this.relayAdminFetchPromise = null;
|
|
7622
|
+
if (this.persistTimer) {
|
|
7623
|
+
clearTimeout(this.persistTimer);
|
|
7624
|
+
this.persistTimer = null;
|
|
7625
|
+
}
|
|
7626
|
+
this.deps = null;
|
|
7627
|
+
}
|
|
7628
|
+
destroyConnection() {
|
|
7629
|
+
if (this.reconnectTimer) {
|
|
7630
|
+
clearTimeout(this.reconnectTimer);
|
|
7631
|
+
this.reconnectTimer = null;
|
|
7632
|
+
}
|
|
7633
|
+
if (this.client) {
|
|
7634
|
+
for (const subId of this.subscriptionIds) {
|
|
7635
|
+
try {
|
|
7636
|
+
this.client.unsubscribe(subId);
|
|
7637
|
+
} catch {
|
|
7638
|
+
}
|
|
7639
|
+
}
|
|
7640
|
+
this.subscriptionIds = [];
|
|
7641
|
+
try {
|
|
7642
|
+
this.client.disconnect();
|
|
7643
|
+
} catch {
|
|
7644
|
+
}
|
|
7645
|
+
this.client = null;
|
|
7646
|
+
}
|
|
7647
|
+
this.connected = false;
|
|
7648
|
+
this.connecting = false;
|
|
7649
|
+
this.connectPromise = null;
|
|
7650
|
+
this.reconnectAttempts = 0;
|
|
7651
|
+
this.keyManager = null;
|
|
7652
|
+
}
|
|
7653
|
+
// ===========================================================================
|
|
7654
|
+
// Connection
|
|
7655
|
+
// ===========================================================================
|
|
7656
|
+
async connect() {
|
|
7657
|
+
if (this.connected) return;
|
|
7658
|
+
if (this.connectPromise) {
|
|
7659
|
+
return this.connectPromise;
|
|
7660
|
+
}
|
|
7661
|
+
this.connecting = true;
|
|
7662
|
+
this.connectPromise = this.doConnect().finally(() => {
|
|
7663
|
+
this.connecting = false;
|
|
7664
|
+
this.connectPromise = null;
|
|
7665
|
+
});
|
|
7666
|
+
return this.connectPromise;
|
|
7667
|
+
}
|
|
7668
|
+
getConnectionStatus() {
|
|
7669
|
+
return this.connected;
|
|
7670
|
+
}
|
|
7671
|
+
async doConnect() {
|
|
7672
|
+
this.ensureInitialized();
|
|
7673
|
+
if (!this.keyManager) {
|
|
7674
|
+
const secretKey = Buffer.from(this.deps.identity.privateKey, "hex");
|
|
7675
|
+
this.keyManager = import_nostr_js_sdk2.NostrKeyManager.fromPrivateKey(secretKey);
|
|
7676
|
+
}
|
|
7677
|
+
const primaryRelay = this.config.relays[0];
|
|
7678
|
+
if (primaryRelay) {
|
|
7679
|
+
await this.checkAndClearOnRelayChange(primaryRelay);
|
|
7680
|
+
}
|
|
7681
|
+
this.client = new import_nostr_js_sdk2.NostrClient(this.keyManager);
|
|
7682
|
+
try {
|
|
7683
|
+
await this.client.connect(...this.config.relays);
|
|
7684
|
+
this.connected = true;
|
|
7685
|
+
this.reconnectAttempts = 0;
|
|
7686
|
+
this.deps.emitEvent("groupchat:connection", { connected: true });
|
|
7687
|
+
if (this.groups.size === 0) {
|
|
7688
|
+
await this.restoreJoinedGroups();
|
|
7689
|
+
} else {
|
|
7690
|
+
await this.subscribeToJoinedGroups();
|
|
7691
|
+
}
|
|
7692
|
+
} catch (error) {
|
|
7693
|
+
console.error("[GroupChat] Failed to connect to relays", error);
|
|
7694
|
+
this.deps.emitEvent("groupchat:connection", { connected: false });
|
|
7695
|
+
this.scheduleReconnect();
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
scheduleReconnect() {
|
|
7699
|
+
if (this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
7700
|
+
console.error("[GroupChat] Max reconnection attempts reached");
|
|
7701
|
+
return;
|
|
7702
|
+
}
|
|
7703
|
+
this.reconnectAttempts++;
|
|
7704
|
+
this.reconnectTimer = setTimeout(() => {
|
|
7705
|
+
this.reconnectTimer = null;
|
|
7706
|
+
if (this.deps) {
|
|
7707
|
+
this.connect().catch(console.error);
|
|
7708
|
+
}
|
|
7709
|
+
}, this.config.reconnectDelayMs);
|
|
7710
|
+
}
|
|
7711
|
+
// ===========================================================================
|
|
7712
|
+
// Subscription Management
|
|
7713
|
+
// ===========================================================================
|
|
7714
|
+
async subscribeToJoinedGroups() {
|
|
7715
|
+
if (!this.client) return;
|
|
7716
|
+
const groupIds = Array.from(this.groups.keys());
|
|
7717
|
+
if (groupIds.length === 0) return;
|
|
7718
|
+
this.trackSubscription(
|
|
7719
|
+
createNip29Filter({
|
|
7720
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
7721
|
+
"#h": groupIds
|
|
7722
|
+
}),
|
|
7723
|
+
{ onEvent: (event) => this.handleGroupEvent(event) }
|
|
7724
|
+
);
|
|
7725
|
+
this.trackSubscription(
|
|
7726
|
+
createNip29Filter({
|
|
7727
|
+
kinds: [NIP29_KINDS.GROUP_METADATA, NIP29_KINDS.GROUP_MEMBERS, NIP29_KINDS.GROUP_ADMINS],
|
|
7728
|
+
"#d": groupIds
|
|
7729
|
+
}),
|
|
7730
|
+
{ onEvent: (event) => this.handleMetadataEvent(event) }
|
|
7731
|
+
);
|
|
7732
|
+
this.trackSubscription(
|
|
7733
|
+
createNip29Filter({
|
|
7734
|
+
kinds: [NIP29_KINDS.DELETE_EVENT, NIP29_KINDS.REMOVE_USER, NIP29_KINDS.DELETE_GROUP],
|
|
7735
|
+
"#h": groupIds
|
|
7736
|
+
}),
|
|
7737
|
+
{ onEvent: (event) => this.handleModerationEvent(event) }
|
|
7738
|
+
);
|
|
7739
|
+
}
|
|
7740
|
+
subscribeToGroup(groupId) {
|
|
7741
|
+
if (!this.client) return;
|
|
7742
|
+
this.trackSubscription(
|
|
7743
|
+
createNip29Filter({
|
|
7744
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
7745
|
+
"#h": [groupId]
|
|
7746
|
+
}),
|
|
7747
|
+
{ onEvent: (event) => this.handleGroupEvent(event) }
|
|
7748
|
+
);
|
|
7749
|
+
this.trackSubscription(
|
|
7750
|
+
createNip29Filter({
|
|
7751
|
+
kinds: [NIP29_KINDS.DELETE_EVENT, NIP29_KINDS.REMOVE_USER, NIP29_KINDS.DELETE_GROUP],
|
|
7752
|
+
"#h": [groupId]
|
|
7753
|
+
}),
|
|
7754
|
+
{ onEvent: (event) => this.handleModerationEvent(event) }
|
|
7755
|
+
);
|
|
7756
|
+
}
|
|
7757
|
+
// ===========================================================================
|
|
7758
|
+
// Event Handlers
|
|
7759
|
+
// ===========================================================================
|
|
7760
|
+
handleGroupEvent(event) {
|
|
7761
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7762
|
+
const groupId = this.getGroupIdFromEvent(event);
|
|
7763
|
+
if (!groupId) return;
|
|
7764
|
+
const group = this.groups.get(groupId);
|
|
7765
|
+
if (!group) return;
|
|
7766
|
+
const { text: content, senderNametag } = this.unwrapMessageContent(event.content);
|
|
7767
|
+
const message = {
|
|
7768
|
+
id: event.id,
|
|
7769
|
+
groupId,
|
|
7770
|
+
content,
|
|
7771
|
+
timestamp: event.created_at * 1e3,
|
|
7772
|
+
senderPubkey: event.pubkey,
|
|
7773
|
+
senderNametag: senderNametag || void 0,
|
|
7774
|
+
replyToId: this.extractReplyTo(event),
|
|
7775
|
+
previousIds: this.extractPreviousIds(event)
|
|
7776
|
+
};
|
|
7777
|
+
this.saveMessageToMemory(message);
|
|
7778
|
+
this.addProcessedEventId(event.id);
|
|
7779
|
+
if (senderNametag) {
|
|
7780
|
+
this.updateMemberNametag(groupId, event.pubkey, senderNametag, event.created_at * 1e3);
|
|
7781
|
+
}
|
|
7782
|
+
this.updateGroupLastMessage(groupId, content.slice(0, 100), message.timestamp);
|
|
7783
|
+
const myPubkey = this.getMyPublicKey();
|
|
7784
|
+
if (event.pubkey !== myPubkey) {
|
|
7785
|
+
group.unreadCount = (group.unreadCount || 0) + 1;
|
|
7786
|
+
}
|
|
7787
|
+
this.deps.emitEvent("groupchat:message", message);
|
|
7788
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7789
|
+
for (const handler of this.messageHandlers) {
|
|
7790
|
+
try {
|
|
7791
|
+
handler(message);
|
|
7792
|
+
} catch {
|
|
7793
|
+
}
|
|
7794
|
+
}
|
|
7795
|
+
this.schedulePersist();
|
|
7796
|
+
}
|
|
7797
|
+
handleMetadataEvent(event) {
|
|
7798
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7799
|
+
if (!groupId) return;
|
|
7800
|
+
const group = this.groups.get(groupId);
|
|
7801
|
+
if (!group) return;
|
|
7802
|
+
if (event.kind === NIP29_KINDS.GROUP_METADATA) {
|
|
7803
|
+
if (!event.content || event.content.trim() === "") return;
|
|
7804
|
+
try {
|
|
7805
|
+
const metadata = JSON.parse(event.content);
|
|
7806
|
+
group.name = metadata.name || group.name;
|
|
7807
|
+
group.description = metadata.about || group.description;
|
|
7808
|
+
group.picture = metadata.picture || group.picture;
|
|
7809
|
+
group.updatedAt = event.created_at * 1e3;
|
|
7810
|
+
this.groups.set(groupId, group);
|
|
7811
|
+
this.persistGroups();
|
|
7812
|
+
} catch {
|
|
7813
|
+
}
|
|
7814
|
+
} else if (event.kind === NIP29_KINDS.GROUP_MEMBERS) {
|
|
7815
|
+
this.updateMembersFromEvent(groupId, event);
|
|
7816
|
+
} else if (event.kind === NIP29_KINDS.GROUP_ADMINS) {
|
|
7817
|
+
this.updateAdminsFromEvent(groupId, event);
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
handleModerationEvent(event) {
|
|
7821
|
+
const groupId = this.getGroupIdFromEvent(event);
|
|
7822
|
+
if (!groupId) return;
|
|
7823
|
+
const group = this.groups.get(groupId);
|
|
7824
|
+
if (!group) return;
|
|
7825
|
+
if (event.kind === NIP29_KINDS.DELETE_EVENT) {
|
|
7826
|
+
const eTags = event.tags.filter((t) => t[0] === "e");
|
|
7827
|
+
for (const tag of eTags) {
|
|
7828
|
+
const messageId = tag[1];
|
|
7829
|
+
if (messageId) {
|
|
7830
|
+
this.deleteMessageFromMemory(groupId, messageId);
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7834
|
+
this.persistMessages();
|
|
7835
|
+
} else if (event.kind === NIP29_KINDS.REMOVE_USER) {
|
|
7836
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7837
|
+
const eventTimestampMs = event.created_at * 1e3;
|
|
7838
|
+
if (group.localJoinedAt && eventTimestampMs < group.localJoinedAt) {
|
|
7839
|
+
this.addProcessedEventId(event.id);
|
|
7840
|
+
return;
|
|
7841
|
+
}
|
|
7842
|
+
this.addProcessedEventId(event.id);
|
|
7843
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7844
|
+
const myPubkey = this.getMyPublicKey();
|
|
7845
|
+
for (const tag of pTags) {
|
|
7846
|
+
const removedPubkey = tag[1];
|
|
7847
|
+
if (!removedPubkey) continue;
|
|
7848
|
+
if (removedPubkey === myPubkey) {
|
|
7849
|
+
if (this.pendingLeaves.has(groupId)) {
|
|
7850
|
+
this.pendingLeaves.delete(groupId);
|
|
7851
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7852
|
+
} else {
|
|
7853
|
+
const groupName = group.name || groupId;
|
|
7854
|
+
this.removeGroupFromMemory(groupId);
|
|
7855
|
+
this.deps.emitEvent("groupchat:kicked", { groupId, groupName });
|
|
7856
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7857
|
+
}
|
|
7858
|
+
} else {
|
|
7859
|
+
this.removeMemberFromMemory(groupId, removedPubkey);
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
this.schedulePersist();
|
|
7863
|
+
} else if (event.kind === NIP29_KINDS.DELETE_GROUP) {
|
|
7864
|
+
if (this.processedEventIds.has(event.id)) return;
|
|
7865
|
+
const deleteTimestampMs = event.created_at * 1e3;
|
|
7866
|
+
if (deleteTimestampMs < group.createdAt) {
|
|
7867
|
+
this.addProcessedEventId(event.id);
|
|
7868
|
+
return;
|
|
7869
|
+
}
|
|
7870
|
+
this.addProcessedEventId(event.id);
|
|
7871
|
+
const groupName = group.name || groupId;
|
|
7872
|
+
this.removeGroupFromMemory(groupId);
|
|
7873
|
+
this.deps.emitEvent("groupchat:group_deleted", { groupId, groupName });
|
|
7874
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7875
|
+
this.schedulePersist();
|
|
7876
|
+
}
|
|
7877
|
+
}
|
|
7878
|
+
updateMembersFromEvent(groupId, event) {
|
|
7879
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7880
|
+
const existingMembers = this.members.get(groupId) || [];
|
|
7881
|
+
for (const tag of pTags) {
|
|
7882
|
+
const pubkey = tag[1];
|
|
7883
|
+
const roleFromTag = tag[3];
|
|
7884
|
+
const existing = existingMembers.find((m) => m.pubkey === pubkey);
|
|
7885
|
+
const role = roleFromTag || existing?.role || GroupRole.MEMBER;
|
|
7886
|
+
const member = {
|
|
7887
|
+
pubkey,
|
|
7888
|
+
groupId,
|
|
7889
|
+
role,
|
|
7890
|
+
nametag: existing?.nametag,
|
|
7891
|
+
joinedAt: existing?.joinedAt || event.created_at * 1e3
|
|
7892
|
+
};
|
|
7893
|
+
this.saveMemberToMemory(member);
|
|
7894
|
+
}
|
|
7895
|
+
this.persistMembers();
|
|
7896
|
+
}
|
|
7897
|
+
updateAdminsFromEvent(groupId, event) {
|
|
7898
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7899
|
+
const existingMembers = this.members.get(groupId) || [];
|
|
7900
|
+
for (const tag of pTags) {
|
|
7901
|
+
const pubkey = tag[1];
|
|
7902
|
+
const existing = existingMembers.find((m) => m.pubkey === pubkey);
|
|
7903
|
+
if (existing) {
|
|
7904
|
+
existing.role = GroupRole.ADMIN;
|
|
7905
|
+
this.saveMemberToMemory(existing);
|
|
7906
|
+
} else {
|
|
7907
|
+
this.saveMemberToMemory({
|
|
7908
|
+
pubkey,
|
|
7909
|
+
groupId,
|
|
7910
|
+
role: GroupRole.ADMIN,
|
|
7911
|
+
joinedAt: event.created_at * 1e3
|
|
7912
|
+
});
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
this.persistMembers();
|
|
7916
|
+
}
|
|
7917
|
+
// ===========================================================================
|
|
7918
|
+
// Group Membership Restoration
|
|
7919
|
+
// ===========================================================================
|
|
7920
|
+
async restoreJoinedGroups() {
|
|
7921
|
+
if (!this.client) return [];
|
|
7922
|
+
const myPubkey = this.getMyPublicKey();
|
|
7923
|
+
if (!myPubkey) return [];
|
|
7924
|
+
const groupIdsWithMembership = /* @__PURE__ */ new Set();
|
|
7925
|
+
await this.oneshotSubscription(
|
|
7926
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
|
|
7927
|
+
{
|
|
7928
|
+
onEvent: (event) => {
|
|
7929
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7930
|
+
if (!groupId) return;
|
|
7931
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7932
|
+
if (pTags.some((tag) => tag[1] === myPubkey)) {
|
|
7933
|
+
groupIdsWithMembership.add(groupId);
|
|
7934
|
+
}
|
|
7935
|
+
},
|
|
7936
|
+
onComplete: () => {
|
|
7937
|
+
},
|
|
7938
|
+
timeoutMs: 15e3
|
|
7939
|
+
}
|
|
7940
|
+
);
|
|
7941
|
+
if (groupIdsWithMembership.size === 0) return [];
|
|
7942
|
+
const restoredGroups = [];
|
|
7943
|
+
for (const groupId of groupIdsWithMembership) {
|
|
7944
|
+
if (this.groups.has(groupId)) continue;
|
|
7945
|
+
try {
|
|
7946
|
+
const group = await this.fetchGroupMetadataInternal(groupId);
|
|
7947
|
+
if (group) {
|
|
7948
|
+
this.groups.set(groupId, group);
|
|
7949
|
+
restoredGroups.push(group);
|
|
7950
|
+
await Promise.all([
|
|
7951
|
+
this.fetchAndSaveMembers(groupId),
|
|
7952
|
+
this.fetchMessages(groupId)
|
|
7953
|
+
]);
|
|
7954
|
+
}
|
|
7955
|
+
} catch {
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
if (restoredGroups.length > 0) {
|
|
7959
|
+
await this.subscribeToJoinedGroups();
|
|
7960
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
7961
|
+
this.schedulePersist();
|
|
7962
|
+
}
|
|
7963
|
+
return restoredGroups;
|
|
7964
|
+
}
|
|
7965
|
+
// ===========================================================================
|
|
7966
|
+
// Public API — Groups
|
|
7967
|
+
// ===========================================================================
|
|
7968
|
+
async fetchAvailableGroups() {
|
|
7969
|
+
await this.ensureConnected();
|
|
7970
|
+
if (!this.client) return [];
|
|
7971
|
+
const groupsMap = /* @__PURE__ */ new Map();
|
|
7972
|
+
const memberCountsMap = /* @__PURE__ */ new Map();
|
|
7973
|
+
await Promise.all([
|
|
7974
|
+
this.oneshotSubscription(
|
|
7975
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA] }),
|
|
7976
|
+
{
|
|
7977
|
+
onEvent: (event) => {
|
|
7978
|
+
const group = this.parseGroupMetadata(event);
|
|
7979
|
+
if (group && group.visibility === GroupVisibility.PUBLIC) {
|
|
7980
|
+
const existing = groupsMap.get(group.id);
|
|
7981
|
+
if (!existing || group.createdAt > existing.createdAt) {
|
|
7982
|
+
groupsMap.set(group.id, group);
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
},
|
|
7986
|
+
onComplete: () => {
|
|
7987
|
+
},
|
|
7988
|
+
timeoutMs: 1e4
|
|
7989
|
+
}
|
|
7990
|
+
),
|
|
7991
|
+
this.oneshotSubscription(
|
|
7992
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS] }),
|
|
7993
|
+
{
|
|
7994
|
+
onEvent: (event) => {
|
|
7995
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
7996
|
+
if (groupId) {
|
|
7997
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
7998
|
+
memberCountsMap.set(groupId, pTags.length);
|
|
7999
|
+
}
|
|
8000
|
+
},
|
|
8001
|
+
onComplete: () => {
|
|
8002
|
+
},
|
|
8003
|
+
timeoutMs: 1e4
|
|
8004
|
+
}
|
|
8005
|
+
)
|
|
8006
|
+
]);
|
|
8007
|
+
for (const [groupId, count] of memberCountsMap) {
|
|
8008
|
+
const group = groupsMap.get(groupId);
|
|
8009
|
+
if (group) group.memberCount = count;
|
|
8010
|
+
}
|
|
8011
|
+
return Array.from(groupsMap.values());
|
|
8012
|
+
}
|
|
8013
|
+
async joinGroup(groupId, inviteCode) {
|
|
8014
|
+
await this.ensureConnected();
|
|
8015
|
+
if (!this.client) return false;
|
|
8016
|
+
try {
|
|
8017
|
+
let group = await this.fetchGroupMetadataInternal(groupId);
|
|
8018
|
+
if (!group && !inviteCode) return false;
|
|
8019
|
+
const tags = [["h", groupId]];
|
|
8020
|
+
if (inviteCode) tags.push(["code", inviteCode]);
|
|
8021
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8022
|
+
kind: NIP29_KINDS.JOIN_REQUEST,
|
|
8023
|
+
tags,
|
|
8024
|
+
content: ""
|
|
8025
|
+
});
|
|
8026
|
+
if (eventId) {
|
|
8027
|
+
if (!group) {
|
|
8028
|
+
group = await this.fetchGroupMetadataInternal(groupId);
|
|
8029
|
+
if (!group) return false;
|
|
8030
|
+
}
|
|
8031
|
+
group.localJoinedAt = Date.now();
|
|
8032
|
+
this.groups.set(groupId, group);
|
|
8033
|
+
this.subscribeToGroup(groupId);
|
|
8034
|
+
await Promise.all([
|
|
8035
|
+
this.fetchMessages(groupId),
|
|
8036
|
+
this.fetchAndSaveMembers(groupId)
|
|
8037
|
+
]);
|
|
8038
|
+
this.deps.emitEvent("groupchat:joined", { groupId, groupName: group.name });
|
|
8039
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8040
|
+
this.persistAll();
|
|
8041
|
+
return true;
|
|
8042
|
+
}
|
|
8043
|
+
return false;
|
|
8044
|
+
} catch (error) {
|
|
8045
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
8046
|
+
if (msg.includes("already a member")) {
|
|
8047
|
+
const group = await this.fetchGroupMetadataInternal(groupId);
|
|
8048
|
+
if (group) {
|
|
8049
|
+
group.localJoinedAt = Date.now();
|
|
8050
|
+
this.groups.set(groupId, group);
|
|
8051
|
+
this.subscribeToGroup(groupId);
|
|
8052
|
+
await Promise.all([
|
|
8053
|
+
this.fetchMessages(groupId),
|
|
8054
|
+
this.fetchAndSaveMembers(groupId)
|
|
8055
|
+
]);
|
|
8056
|
+
this.deps.emitEvent("groupchat:joined", { groupId, groupName: group.name });
|
|
8057
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8058
|
+
this.persistAll();
|
|
8059
|
+
return true;
|
|
8060
|
+
}
|
|
8061
|
+
}
|
|
8062
|
+
console.error("[GroupChat] Failed to join group", error);
|
|
8063
|
+
return false;
|
|
8064
|
+
}
|
|
8065
|
+
}
|
|
8066
|
+
async leaveGroup(groupId) {
|
|
8067
|
+
await this.ensureConnected();
|
|
8068
|
+
if (!this.client) return false;
|
|
8069
|
+
try {
|
|
8070
|
+
this.pendingLeaves.add(groupId);
|
|
8071
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8072
|
+
kind: NIP29_KINDS.LEAVE_REQUEST,
|
|
8073
|
+
tags: [["h", groupId]],
|
|
8074
|
+
content: ""
|
|
8075
|
+
});
|
|
8076
|
+
if (eventId) {
|
|
8077
|
+
this.removeGroupFromMemory(groupId);
|
|
8078
|
+
this.deps.emitEvent("groupchat:left", { groupId });
|
|
8079
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8080
|
+
this.persistAll();
|
|
8081
|
+
return true;
|
|
8082
|
+
}
|
|
8083
|
+
this.pendingLeaves.delete(groupId);
|
|
8084
|
+
return false;
|
|
8085
|
+
} catch (error) {
|
|
8086
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
8087
|
+
if (msg.includes("group not found") || msg.includes("not a member")) {
|
|
8088
|
+
this.removeGroupFromMemory(groupId);
|
|
8089
|
+
this.persistAll();
|
|
8090
|
+
return true;
|
|
8091
|
+
}
|
|
8092
|
+
console.error("[GroupChat] Failed to leave group", error);
|
|
8093
|
+
return false;
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
async createGroup(options) {
|
|
8097
|
+
await this.ensureConnected();
|
|
8098
|
+
if (!this.client) return null;
|
|
8099
|
+
const creatorPubkey = this.getMyPublicKey();
|
|
8100
|
+
if (!creatorPubkey) return null;
|
|
8101
|
+
const proposedGroupId = options.name.toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 20) || this.randomId();
|
|
8102
|
+
try {
|
|
8103
|
+
const isPrivate = options.visibility === GroupVisibility.PRIVATE;
|
|
8104
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8105
|
+
kind: NIP29_KINDS.CREATE_GROUP,
|
|
8106
|
+
tags: [["h", proposedGroupId]],
|
|
8107
|
+
content: JSON.stringify({
|
|
8108
|
+
name: options.name,
|
|
8109
|
+
about: options.description,
|
|
8110
|
+
picture: options.picture,
|
|
8111
|
+
closed: true,
|
|
8112
|
+
private: isPrivate,
|
|
8113
|
+
hidden: isPrivate
|
|
8114
|
+
})
|
|
8115
|
+
});
|
|
8116
|
+
if (!eventId) return null;
|
|
8117
|
+
let group = await this.fetchGroupMetadataInternal(proposedGroupId);
|
|
8118
|
+
if (!group) {
|
|
8119
|
+
group = {
|
|
8120
|
+
id: proposedGroupId,
|
|
8121
|
+
relayUrl: this.config.relays[0] || "",
|
|
8122
|
+
name: options.name,
|
|
8123
|
+
description: options.description,
|
|
8124
|
+
visibility: options.visibility || GroupVisibility.PUBLIC,
|
|
8125
|
+
createdAt: Date.now(),
|
|
8126
|
+
memberCount: 1
|
|
8127
|
+
};
|
|
8128
|
+
}
|
|
8129
|
+
if (!group.name || group.name === "Unnamed Group") {
|
|
8130
|
+
group.name = options.name;
|
|
8131
|
+
}
|
|
8132
|
+
if (options.description && !group.description) {
|
|
8133
|
+
group.description = options.description;
|
|
8134
|
+
}
|
|
8135
|
+
group.visibility = options.visibility || GroupVisibility.PUBLIC;
|
|
8136
|
+
group.memberCount = 1;
|
|
8137
|
+
this.groups.set(group.id, group);
|
|
8138
|
+
this.subscribeToGroup(group.id);
|
|
8139
|
+
this.client.createAndPublishEvent({
|
|
8140
|
+
kind: NIP29_KINDS.JOIN_REQUEST,
|
|
8141
|
+
tags: [["h", group.id]],
|
|
8142
|
+
content: ""
|
|
8143
|
+
}).catch(() => {
|
|
8144
|
+
});
|
|
8145
|
+
await this.fetchAndSaveMembers(group.id).catch(() => {
|
|
8146
|
+
});
|
|
8147
|
+
this.saveMemberToMemory({
|
|
8148
|
+
pubkey: creatorPubkey,
|
|
8149
|
+
groupId: group.id,
|
|
8150
|
+
role: GroupRole.ADMIN,
|
|
8151
|
+
joinedAt: Date.now()
|
|
8152
|
+
});
|
|
8153
|
+
this.deps.emitEvent("groupchat:joined", { groupId: group.id, groupName: group.name });
|
|
8154
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8155
|
+
this.schedulePersist();
|
|
8156
|
+
return group;
|
|
8157
|
+
} catch (error) {
|
|
8158
|
+
console.error("[GroupChat] Failed to create group", error);
|
|
8159
|
+
return null;
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
async deleteGroup(groupId) {
|
|
8163
|
+
await this.ensureConnected();
|
|
8164
|
+
if (!this.client) return false;
|
|
8165
|
+
const group = this.groups.get(groupId);
|
|
8166
|
+
if (!group) return false;
|
|
8167
|
+
const canDelete = await this.canModerateGroup(groupId);
|
|
8168
|
+
if (!canDelete) return false;
|
|
8169
|
+
try {
|
|
8170
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8171
|
+
kind: NIP29_KINDS.DELETE_GROUP,
|
|
8172
|
+
tags: [["h", groupId]],
|
|
8173
|
+
content: ""
|
|
8174
|
+
});
|
|
8175
|
+
if (eventId) {
|
|
8176
|
+
const groupName = group.name || groupId;
|
|
8177
|
+
this.removeGroupFromMemory(groupId);
|
|
8178
|
+
this.deps.emitEvent("groupchat:group_deleted", { groupId, groupName });
|
|
8179
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8180
|
+
this.persistAll();
|
|
8181
|
+
return true;
|
|
8182
|
+
}
|
|
8183
|
+
return false;
|
|
8184
|
+
} catch (error) {
|
|
8185
|
+
console.error("[GroupChat] Failed to delete group", error);
|
|
8186
|
+
return false;
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
async createInvite(groupId) {
|
|
8190
|
+
await this.ensureConnected();
|
|
8191
|
+
if (!this.client) return null;
|
|
8192
|
+
if (!this.isCurrentUserAdmin(groupId)) return null;
|
|
8193
|
+
try {
|
|
8194
|
+
const inviteCode = this.randomId();
|
|
8195
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8196
|
+
kind: NIP29_KINDS.CREATE_INVITE,
|
|
8197
|
+
tags: [
|
|
8198
|
+
["h", groupId],
|
|
8199
|
+
["code", inviteCode]
|
|
8200
|
+
],
|
|
8201
|
+
content: ""
|
|
8202
|
+
});
|
|
8203
|
+
return eventId ? inviteCode : null;
|
|
8204
|
+
} catch (error) {
|
|
8205
|
+
console.error("[GroupChat] Failed to create invite", error);
|
|
8206
|
+
return null;
|
|
8207
|
+
}
|
|
8208
|
+
}
|
|
8209
|
+
// ===========================================================================
|
|
8210
|
+
// Public API — Messages
|
|
8211
|
+
// ===========================================================================
|
|
8212
|
+
async sendMessage(groupId, content, replyToId) {
|
|
8213
|
+
await this.ensureConnected();
|
|
8214
|
+
if (!this.client) return null;
|
|
8215
|
+
const group = this.groups.get(groupId);
|
|
8216
|
+
if (!group) return null;
|
|
8217
|
+
try {
|
|
8218
|
+
const senderNametag = this.deps.identity.nametag || null;
|
|
8219
|
+
const kind = replyToId ? NIP29_KINDS.THREAD_REPLY : NIP29_KINDS.CHAT_MESSAGE;
|
|
8220
|
+
const tags = [["h", groupId]];
|
|
8221
|
+
const groupMessages = this.messages.get(groupId) || [];
|
|
8222
|
+
const recentIds = groupMessages.slice(-this.config.maxPreviousTags).map((m) => (m.id || "").slice(0, 8)).filter(Boolean);
|
|
8223
|
+
if (recentIds.length > 0) {
|
|
8224
|
+
tags.push(["previous", ...recentIds]);
|
|
8225
|
+
}
|
|
8226
|
+
if (replyToId) {
|
|
8227
|
+
tags.push(["e", replyToId, "", "reply"]);
|
|
8228
|
+
}
|
|
8229
|
+
const wrappedContent = this.wrapMessageContent(content, senderNametag);
|
|
8230
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8231
|
+
kind,
|
|
8232
|
+
tags,
|
|
8233
|
+
content: wrappedContent
|
|
8234
|
+
});
|
|
8235
|
+
if (eventId) {
|
|
8236
|
+
const myPubkey = this.getMyPublicKey();
|
|
8237
|
+
const message = {
|
|
8238
|
+
id: eventId,
|
|
8239
|
+
groupId,
|
|
8240
|
+
content,
|
|
8241
|
+
timestamp: Date.now(),
|
|
8242
|
+
senderPubkey: myPubkey || "",
|
|
8243
|
+
senderNametag: senderNametag || void 0,
|
|
8244
|
+
replyToId,
|
|
8245
|
+
previousIds: recentIds
|
|
8246
|
+
};
|
|
8247
|
+
this.saveMessageToMemory(message);
|
|
8248
|
+
this.addProcessedEventId(eventId);
|
|
8249
|
+
this.updateGroupLastMessage(groupId, content.slice(0, 100), message.timestamp);
|
|
8250
|
+
this.persistAll();
|
|
8251
|
+
return message;
|
|
8252
|
+
}
|
|
8253
|
+
return null;
|
|
8254
|
+
} catch (error) {
|
|
8255
|
+
console.error("[GroupChat] Failed to send message", error);
|
|
8256
|
+
return null;
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
async fetchMessages(groupId, since, limit) {
|
|
8260
|
+
await this.ensureConnected();
|
|
8261
|
+
if (!this.client) return [];
|
|
8262
|
+
const fetchedMessages = [];
|
|
8263
|
+
const filterData = {
|
|
8264
|
+
kinds: [NIP29_KINDS.CHAT_MESSAGE, NIP29_KINDS.THREAD_ROOT, NIP29_KINDS.THREAD_REPLY],
|
|
8265
|
+
"#h": [groupId]
|
|
8266
|
+
};
|
|
8267
|
+
if (since) filterData.since = Math.floor(since / 1e3);
|
|
8268
|
+
if (limit) filterData.limit = limit;
|
|
8269
|
+
if (!limit && !since) filterData.limit = this.config.defaultMessageLimit;
|
|
8270
|
+
return this.oneshotSubscription(createNip29Filter(filterData), {
|
|
8271
|
+
onEvent: (event) => {
|
|
8272
|
+
const { text: content, senderNametag } = this.unwrapMessageContent(event.content);
|
|
8273
|
+
const message = {
|
|
8274
|
+
id: event.id,
|
|
8275
|
+
groupId,
|
|
8276
|
+
content,
|
|
8277
|
+
timestamp: event.created_at * 1e3,
|
|
8278
|
+
senderPubkey: event.pubkey,
|
|
8279
|
+
senderNametag: senderNametag || void 0,
|
|
8280
|
+
replyToId: this.extractReplyTo(event),
|
|
8281
|
+
previousIds: this.extractPreviousIds(event)
|
|
8282
|
+
};
|
|
8283
|
+
fetchedMessages.push(message);
|
|
8284
|
+
this.saveMessageToMemory(message);
|
|
8285
|
+
this.addProcessedEventId(event.id);
|
|
8286
|
+
if (senderNametag) {
|
|
8287
|
+
this.updateMemberNametag(groupId, event.pubkey, senderNametag, event.created_at * 1e3);
|
|
8288
|
+
}
|
|
8289
|
+
},
|
|
8290
|
+
onComplete: () => {
|
|
8291
|
+
this.schedulePersist();
|
|
8292
|
+
return fetchedMessages;
|
|
8293
|
+
},
|
|
8294
|
+
timeoutMs: 1e4
|
|
8295
|
+
});
|
|
8296
|
+
}
|
|
8297
|
+
// ===========================================================================
|
|
8298
|
+
// Public API — Queries (from local state)
|
|
8299
|
+
// ===========================================================================
|
|
8300
|
+
getGroups() {
|
|
8301
|
+
return Array.from(this.groups.values()).sort((a, b) => (b.lastMessageTime || 0) - (a.lastMessageTime || 0));
|
|
8302
|
+
}
|
|
8303
|
+
getGroup(groupId) {
|
|
8304
|
+
return this.groups.get(groupId) || null;
|
|
8305
|
+
}
|
|
8306
|
+
getMessages(groupId) {
|
|
8307
|
+
return (this.messages.get(groupId) || []).sort((a, b) => a.timestamp - b.timestamp);
|
|
8308
|
+
}
|
|
8309
|
+
getMembers(groupId) {
|
|
8310
|
+
return (this.members.get(groupId) || []).sort((a, b) => a.joinedAt - b.joinedAt);
|
|
8311
|
+
}
|
|
8312
|
+
getMember(groupId, pubkey) {
|
|
8313
|
+
const members = this.members.get(groupId) || [];
|
|
8314
|
+
return members.find((m) => m.pubkey === pubkey) || null;
|
|
8315
|
+
}
|
|
8316
|
+
getTotalUnreadCount() {
|
|
8317
|
+
let total = 0;
|
|
8318
|
+
for (const group of this.groups.values()) {
|
|
8319
|
+
total += group.unreadCount || 0;
|
|
8320
|
+
}
|
|
8321
|
+
return total;
|
|
8322
|
+
}
|
|
8323
|
+
markGroupAsRead(groupId) {
|
|
8324
|
+
const group = this.groups.get(groupId);
|
|
8325
|
+
if (group && (group.unreadCount || 0) > 0) {
|
|
8326
|
+
group.unreadCount = 0;
|
|
8327
|
+
this.groups.set(groupId, group);
|
|
8328
|
+
this.persistGroups();
|
|
8329
|
+
}
|
|
8330
|
+
}
|
|
8331
|
+
// ===========================================================================
|
|
8332
|
+
// Public API — Admin
|
|
8333
|
+
// ===========================================================================
|
|
8334
|
+
async kickUser(groupId, userPubkey, reason) {
|
|
8335
|
+
await this.ensureConnected();
|
|
8336
|
+
if (!this.client) return false;
|
|
8337
|
+
const canModerate = await this.canModerateGroup(groupId);
|
|
8338
|
+
if (!canModerate) return false;
|
|
8339
|
+
const myPubkey = this.getMyPublicKey();
|
|
8340
|
+
if (myPubkey === userPubkey) return false;
|
|
8341
|
+
try {
|
|
8342
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8343
|
+
kind: NIP29_KINDS.REMOVE_USER,
|
|
8344
|
+
tags: [["h", groupId], ["p", userPubkey]],
|
|
8345
|
+
content: reason || ""
|
|
8346
|
+
});
|
|
8347
|
+
if (eventId) {
|
|
8348
|
+
this.removeMemberFromMemory(groupId, userPubkey);
|
|
8349
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8350
|
+
this.persistMembers();
|
|
8351
|
+
return true;
|
|
8352
|
+
}
|
|
8353
|
+
return false;
|
|
8354
|
+
} catch (error) {
|
|
8355
|
+
console.error("[GroupChat] Failed to kick user", error);
|
|
8356
|
+
return false;
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
async deleteMessage(groupId, messageId) {
|
|
8360
|
+
await this.ensureConnected();
|
|
8361
|
+
if (!this.client) return false;
|
|
8362
|
+
const canModerate = await this.canModerateGroup(groupId);
|
|
8363
|
+
if (!canModerate) return false;
|
|
8364
|
+
try {
|
|
8365
|
+
const eventId = await this.client.createAndPublishEvent({
|
|
8366
|
+
kind: NIP29_KINDS.DELETE_EVENT,
|
|
8367
|
+
tags: [["h", groupId], ["e", messageId]],
|
|
8368
|
+
content: ""
|
|
8369
|
+
});
|
|
8370
|
+
if (eventId) {
|
|
8371
|
+
this.deleteMessageFromMemory(groupId, messageId);
|
|
8372
|
+
this.deps.emitEvent("groupchat:updated", {});
|
|
8373
|
+
this.persistMessages();
|
|
8374
|
+
return true;
|
|
8375
|
+
}
|
|
8376
|
+
return false;
|
|
8377
|
+
} catch (error) {
|
|
8378
|
+
console.error("[GroupChat] Failed to delete message", error);
|
|
8379
|
+
return false;
|
|
8380
|
+
}
|
|
8381
|
+
}
|
|
8382
|
+
isCurrentUserAdmin(groupId) {
|
|
8383
|
+
const myPubkey = this.getMyPublicKey();
|
|
8384
|
+
if (!myPubkey) return false;
|
|
8385
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8386
|
+
return member?.role === GroupRole.ADMIN;
|
|
8387
|
+
}
|
|
8388
|
+
isCurrentUserModerator(groupId) {
|
|
8389
|
+
const myPubkey = this.getMyPublicKey();
|
|
8390
|
+
if (!myPubkey) return false;
|
|
8391
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8392
|
+
return member?.role === GroupRole.ADMIN || member?.role === GroupRole.MODERATOR;
|
|
8393
|
+
}
|
|
8394
|
+
/**
|
|
8395
|
+
* Check if current user can moderate a group:
|
|
8396
|
+
* - Group admin/moderator can always moderate their group
|
|
8397
|
+
* - Relay admins can moderate public groups
|
|
8398
|
+
*/
|
|
8399
|
+
async canModerateGroup(groupId) {
|
|
8400
|
+
if (this.isCurrentUserAdmin(groupId) || this.isCurrentUserModerator(groupId)) {
|
|
8401
|
+
return true;
|
|
8402
|
+
}
|
|
8403
|
+
const group = this.groups.get(groupId);
|
|
8404
|
+
if (group && group.visibility === GroupVisibility.PUBLIC) {
|
|
8405
|
+
return this.isCurrentUserRelayAdmin();
|
|
8406
|
+
}
|
|
8407
|
+
return false;
|
|
8408
|
+
}
|
|
8409
|
+
async isCurrentUserRelayAdmin() {
|
|
8410
|
+
const myPubkey = this.getMyPublicKey();
|
|
8411
|
+
if (!myPubkey) return false;
|
|
8412
|
+
const admins = await this.fetchRelayAdmins();
|
|
8413
|
+
return admins.has(myPubkey);
|
|
8414
|
+
}
|
|
8415
|
+
getCurrentUserRole(groupId) {
|
|
8416
|
+
const myPubkey = this.getMyPublicKey();
|
|
8417
|
+
if (!myPubkey) return null;
|
|
8418
|
+
const member = this.getMember(groupId, myPubkey);
|
|
8419
|
+
return member?.role || null;
|
|
8420
|
+
}
|
|
8421
|
+
// ===========================================================================
|
|
8422
|
+
// Public API — Listeners
|
|
8423
|
+
// ===========================================================================
|
|
8424
|
+
onMessage(handler) {
|
|
8425
|
+
this.messageHandlers.add(handler);
|
|
8426
|
+
return () => this.messageHandlers.delete(handler);
|
|
8427
|
+
}
|
|
8428
|
+
// ===========================================================================
|
|
8429
|
+
// Public API — Utilities
|
|
8430
|
+
// ===========================================================================
|
|
8431
|
+
getRelayUrls() {
|
|
8432
|
+
return this.config.relays;
|
|
8433
|
+
}
|
|
8434
|
+
getMyPublicKey() {
|
|
8435
|
+
return this.keyManager?.getPublicKeyHex() || null;
|
|
8436
|
+
}
|
|
8437
|
+
// ===========================================================================
|
|
8438
|
+
// Private — Relay Admin
|
|
8439
|
+
// ===========================================================================
|
|
8440
|
+
async fetchRelayAdmins() {
|
|
8441
|
+
if (this.relayAdminPubkeys) return this.relayAdminPubkeys;
|
|
8442
|
+
if (this.relayAdminFetchPromise) return this.relayAdminFetchPromise;
|
|
8443
|
+
this.relayAdminFetchPromise = this.doFetchRelayAdmins();
|
|
8444
|
+
const result = await this.relayAdminFetchPromise;
|
|
8445
|
+
this.relayAdminFetchPromise = null;
|
|
8446
|
+
return result;
|
|
8447
|
+
}
|
|
8448
|
+
async doFetchRelayAdmins() {
|
|
8449
|
+
await this.ensureConnected();
|
|
8450
|
+
if (!this.client) return /* @__PURE__ */ new Set();
|
|
8451
|
+
const adminPubkeys = /* @__PURE__ */ new Set();
|
|
8452
|
+
return this.oneshotSubscription(
|
|
8453
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": ["", "_"] }),
|
|
8454
|
+
{
|
|
8455
|
+
onEvent: (event) => {
|
|
8456
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8457
|
+
for (const tag of pTags) {
|
|
8458
|
+
if (tag[1]) adminPubkeys.add(tag[1]);
|
|
8459
|
+
}
|
|
8460
|
+
},
|
|
8461
|
+
onComplete: () => {
|
|
8462
|
+
this.relayAdminPubkeys = adminPubkeys;
|
|
8463
|
+
return adminPubkeys;
|
|
8464
|
+
}
|
|
8465
|
+
}
|
|
8466
|
+
);
|
|
8467
|
+
}
|
|
8468
|
+
// ===========================================================================
|
|
8469
|
+
// Private — Fetch Helpers
|
|
8470
|
+
// ===========================================================================
|
|
8471
|
+
async fetchGroupMetadataInternal(groupId) {
|
|
8472
|
+
if (!this.client) return null;
|
|
8473
|
+
let result = null;
|
|
8474
|
+
return this.oneshotSubscription(
|
|
8475
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_METADATA], "#d": [groupId] }),
|
|
8476
|
+
{
|
|
8477
|
+
onEvent: (event) => {
|
|
8478
|
+
if (!result) result = this.parseGroupMetadata(event);
|
|
8479
|
+
},
|
|
8480
|
+
onComplete: () => result
|
|
8481
|
+
}
|
|
8482
|
+
);
|
|
8483
|
+
}
|
|
8484
|
+
async fetchAndSaveMembers(groupId) {
|
|
8485
|
+
const [members, adminPubkeys] = await Promise.all([
|
|
8486
|
+
this.fetchGroupMembersInternal(groupId),
|
|
8487
|
+
this.fetchGroupAdminsInternal(groupId)
|
|
8488
|
+
]);
|
|
8489
|
+
for (const member of members) {
|
|
8490
|
+
if (adminPubkeys.includes(member.pubkey)) {
|
|
8491
|
+
member.role = GroupRole.ADMIN;
|
|
8492
|
+
}
|
|
8493
|
+
this.saveMemberToMemory(member);
|
|
8494
|
+
}
|
|
8495
|
+
for (const pubkey of adminPubkeys) {
|
|
8496
|
+
const existing = (this.members.get(groupId) || []).find((m) => m.pubkey === pubkey);
|
|
8497
|
+
if (!existing) {
|
|
8498
|
+
this.saveMemberToMemory({
|
|
8499
|
+
pubkey,
|
|
8500
|
+
groupId,
|
|
8501
|
+
role: GroupRole.ADMIN,
|
|
8502
|
+
joinedAt: Date.now()
|
|
8503
|
+
});
|
|
8504
|
+
}
|
|
8505
|
+
}
|
|
8506
|
+
this.persistMembers();
|
|
8507
|
+
}
|
|
8508
|
+
async fetchGroupMembersInternal(groupId) {
|
|
8509
|
+
if (!this.client) return [];
|
|
8510
|
+
const members = [];
|
|
8511
|
+
return this.oneshotSubscription(
|
|
8512
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_MEMBERS], "#d": [groupId] }),
|
|
8513
|
+
{
|
|
8514
|
+
onEvent: (event) => {
|
|
8515
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8516
|
+
for (const tag of pTags) {
|
|
8517
|
+
members.push({
|
|
8518
|
+
pubkey: tag[1],
|
|
8519
|
+
groupId,
|
|
8520
|
+
role: tag[3] || GroupRole.MEMBER,
|
|
8521
|
+
joinedAt: event.created_at * 1e3
|
|
8522
|
+
});
|
|
8523
|
+
}
|
|
8524
|
+
},
|
|
8525
|
+
onComplete: () => members
|
|
8526
|
+
}
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
async fetchGroupAdminsInternal(groupId) {
|
|
8530
|
+
if (!this.client) return [];
|
|
8531
|
+
const adminPubkeys = [];
|
|
8532
|
+
return this.oneshotSubscription(
|
|
8533
|
+
new import_nostr_js_sdk2.Filter({ kinds: [NIP29_KINDS.GROUP_ADMINS], "#d": [groupId] }),
|
|
8534
|
+
{
|
|
8535
|
+
onEvent: (event) => {
|
|
8536
|
+
const pTags = event.tags.filter((t) => t[0] === "p");
|
|
8537
|
+
for (const tag of pTags) {
|
|
8538
|
+
if (tag[1] && !adminPubkeys.includes(tag[1])) {
|
|
8539
|
+
adminPubkeys.push(tag[1]);
|
|
8540
|
+
}
|
|
8541
|
+
}
|
|
8542
|
+
},
|
|
8543
|
+
onComplete: () => adminPubkeys
|
|
8544
|
+
}
|
|
8545
|
+
);
|
|
8546
|
+
}
|
|
8547
|
+
// ===========================================================================
|
|
8548
|
+
// Private — In-Memory State Helpers
|
|
7349
8549
|
// ===========================================================================
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
const
|
|
7356
|
-
const
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
};
|
|
7366
|
-
this.messages.set(message.id, message);
|
|
7367
|
-
if (this.config.autoSave) {
|
|
7368
|
-
await this.save();
|
|
8550
|
+
saveMessageToMemory(message) {
|
|
8551
|
+
const groupId = message.groupId;
|
|
8552
|
+
if (!this.messages.has(groupId)) {
|
|
8553
|
+
this.messages.set(groupId, []);
|
|
8554
|
+
}
|
|
8555
|
+
const msgs = this.messages.get(groupId);
|
|
8556
|
+
const idx = msgs.findIndex((m) => m.id === message.id);
|
|
8557
|
+
if (idx >= 0) {
|
|
8558
|
+
msgs[idx] = message;
|
|
8559
|
+
} else {
|
|
8560
|
+
msgs.push(message);
|
|
8561
|
+
const maxMessages = this.config.defaultMessageLimit * 2;
|
|
8562
|
+
if (msgs.length > maxMessages) {
|
|
8563
|
+
msgs.splice(0, msgs.length - maxMessages);
|
|
8564
|
+
}
|
|
7369
8565
|
}
|
|
7370
|
-
return message;
|
|
7371
8566
|
}
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
).sort((a, b) => a.timestamp - b.timestamp);
|
|
8567
|
+
deleteMessageFromMemory(groupId, messageId) {
|
|
8568
|
+
const msgs = this.messages.get(groupId);
|
|
8569
|
+
if (msgs) {
|
|
8570
|
+
const idx = msgs.findIndex((m) => m.id === messageId);
|
|
8571
|
+
if (idx >= 0) msgs.splice(idx, 1);
|
|
8572
|
+
}
|
|
7379
8573
|
}
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
const conversations = /* @__PURE__ */ new Map();
|
|
7385
|
-
for (const message of this.messages.values()) {
|
|
7386
|
-
const peer = message.senderPubkey === this.deps?.identity.chainPubkey ? message.recipientPubkey : message.senderPubkey;
|
|
7387
|
-
if (!conversations.has(peer)) {
|
|
7388
|
-
conversations.set(peer, []);
|
|
7389
|
-
}
|
|
7390
|
-
conversations.get(peer).push(message);
|
|
8574
|
+
saveMemberToMemory(member) {
|
|
8575
|
+
const groupId = member.groupId;
|
|
8576
|
+
if (!this.members.has(groupId)) {
|
|
8577
|
+
this.members.set(groupId, []);
|
|
7391
8578
|
}
|
|
7392
|
-
|
|
7393
|
-
|
|
8579
|
+
const mems = this.members.get(groupId);
|
|
8580
|
+
const idx = mems.findIndex((m) => m.pubkey === member.pubkey);
|
|
8581
|
+
if (idx >= 0) {
|
|
8582
|
+
mems[idx] = member;
|
|
8583
|
+
} else {
|
|
8584
|
+
mems.push(member);
|
|
7394
8585
|
}
|
|
7395
|
-
return conversations;
|
|
7396
8586
|
}
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
const msg = this.messages.get(id);
|
|
7403
|
-
if (msg) {
|
|
7404
|
-
msg.isRead = true;
|
|
7405
|
-
}
|
|
8587
|
+
removeMemberFromMemory(groupId, pubkey) {
|
|
8588
|
+
const mems = this.members.get(groupId);
|
|
8589
|
+
if (mems) {
|
|
8590
|
+
const idx = mems.findIndex((m) => m.pubkey === pubkey);
|
|
8591
|
+
if (idx >= 0) mems.splice(idx, 1);
|
|
7406
8592
|
}
|
|
7407
|
-
|
|
7408
|
-
|
|
8593
|
+
const group = this.groups.get(groupId);
|
|
8594
|
+
if (group) {
|
|
8595
|
+
group.memberCount = (this.members.get(groupId) || []).length;
|
|
8596
|
+
this.groups.set(groupId, group);
|
|
7409
8597
|
}
|
|
7410
8598
|
}
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
);
|
|
7418
|
-
if (
|
|
7419
|
-
|
|
8599
|
+
removeGroupFromMemory(groupId) {
|
|
8600
|
+
this.groups.delete(groupId);
|
|
8601
|
+
this.messages.delete(groupId);
|
|
8602
|
+
this.members.delete(groupId);
|
|
8603
|
+
}
|
|
8604
|
+
updateGroupLastMessage(groupId, text, timestamp) {
|
|
8605
|
+
const group = this.groups.get(groupId);
|
|
8606
|
+
if (group && timestamp >= (group.lastMessageTime || 0)) {
|
|
8607
|
+
group.lastMessageText = text;
|
|
8608
|
+
group.lastMessageTime = timestamp;
|
|
8609
|
+
this.groups.set(groupId, group);
|
|
7420
8610
|
}
|
|
7421
|
-
return messages.length;
|
|
7422
8611
|
}
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
8612
|
+
updateMemberNametag(groupId, pubkey, nametag, joinedAt) {
|
|
8613
|
+
const members = this.members.get(groupId) || [];
|
|
8614
|
+
const existing = members.find((m) => m.pubkey === pubkey);
|
|
8615
|
+
if (existing) {
|
|
8616
|
+
if (existing.nametag !== nametag) {
|
|
8617
|
+
existing.nametag = nametag;
|
|
8618
|
+
this.saveMemberToMemory(existing);
|
|
8619
|
+
}
|
|
8620
|
+
} else {
|
|
8621
|
+
this.saveMemberToMemory({
|
|
8622
|
+
pubkey,
|
|
8623
|
+
groupId,
|
|
8624
|
+
role: GroupRole.MEMBER,
|
|
8625
|
+
nametag,
|
|
8626
|
+
joinedAt
|
|
8627
|
+
});
|
|
8628
|
+
}
|
|
8629
|
+
}
|
|
8630
|
+
addProcessedEventId(eventId) {
|
|
8631
|
+
this.processedEventIds.add(eventId);
|
|
8632
|
+
if (this.processedEventIds.size > 1e4) {
|
|
8633
|
+
const arr = Array.from(this.processedEventIds);
|
|
8634
|
+
this.processedEventIds = new Set(arr.slice(arr.length - 1e4));
|
|
8635
|
+
}
|
|
7429
8636
|
}
|
|
7430
8637
|
// ===========================================================================
|
|
7431
|
-
//
|
|
8638
|
+
// Private — Persistence
|
|
7432
8639
|
// ===========================================================================
|
|
7433
|
-
/**
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
timestamp: Date.now(),
|
|
7445
|
-
tags
|
|
7446
|
-
};
|
|
7447
|
-
this.broadcasts.set(message.id, message);
|
|
7448
|
-
return message;
|
|
8640
|
+
/** Schedule a debounced persist (coalesces rapid event bursts). */
|
|
8641
|
+
schedulePersist() {
|
|
8642
|
+
if (this.persistTimer) return;
|
|
8643
|
+
this.persistTimer = setTimeout(() => {
|
|
8644
|
+
this.persistTimer = null;
|
|
8645
|
+
this.persistPromise = this.doPersistAll().catch((err) => {
|
|
8646
|
+
console.error("[GroupChat] Persistence error:", err);
|
|
8647
|
+
}).finally(() => {
|
|
8648
|
+
this.persistPromise = null;
|
|
8649
|
+
});
|
|
8650
|
+
}, 200);
|
|
7449
8651
|
}
|
|
7450
|
-
/**
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
const key = tags.sort().join(":");
|
|
7456
|
-
if (this.broadcastSubscriptions.has(key)) {
|
|
7457
|
-
return () => {
|
|
7458
|
-
};
|
|
8652
|
+
/** Persist immediately (for explicit flush points). */
|
|
8653
|
+
async persistAll() {
|
|
8654
|
+
if (this.persistTimer) {
|
|
8655
|
+
clearTimeout(this.persistTimer);
|
|
8656
|
+
this.persistTimer = null;
|
|
7459
8657
|
}
|
|
7460
|
-
|
|
7461
|
-
this.
|
|
7462
|
-
});
|
|
7463
|
-
if (unsub) {
|
|
7464
|
-
this.broadcastSubscriptions.set(key, unsub);
|
|
8658
|
+
if (this.persistPromise) {
|
|
8659
|
+
await this.persistPromise;
|
|
7465
8660
|
}
|
|
7466
|
-
|
|
7467
|
-
const sub = this.broadcastSubscriptions.get(key);
|
|
7468
|
-
if (sub) {
|
|
7469
|
-
sub();
|
|
7470
|
-
this.broadcastSubscriptions.delete(key);
|
|
7471
|
-
}
|
|
7472
|
-
};
|
|
8661
|
+
await this.doPersistAll();
|
|
7473
8662
|
}
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
8663
|
+
async doPersistAll() {
|
|
8664
|
+
await Promise.all([
|
|
8665
|
+
this.persistGroups(),
|
|
8666
|
+
this.persistMessages(),
|
|
8667
|
+
this.persistMembers(),
|
|
8668
|
+
this.persistProcessedEvents()
|
|
8669
|
+
]);
|
|
7480
8670
|
}
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
8671
|
+
async persistGroups() {
|
|
8672
|
+
if (!this.deps) return;
|
|
8673
|
+
const data = Array.from(this.groups.values());
|
|
8674
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_GROUPS, JSON.stringify(data));
|
|
8675
|
+
}
|
|
8676
|
+
async persistMessages() {
|
|
8677
|
+
if (!this.deps) return;
|
|
8678
|
+
const allMessages = [];
|
|
8679
|
+
for (const msgs of this.messages.values()) {
|
|
8680
|
+
allMessages.push(...msgs);
|
|
8681
|
+
}
|
|
8682
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MESSAGES, JSON.stringify(allMessages));
|
|
8683
|
+
}
|
|
8684
|
+
async persistMembers() {
|
|
8685
|
+
if (!this.deps) return;
|
|
8686
|
+
const allMembers = [];
|
|
8687
|
+
for (const mems of this.members.values()) {
|
|
8688
|
+
allMembers.push(...mems);
|
|
8689
|
+
}
|
|
8690
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_MEMBERS, JSON.stringify(allMembers));
|
|
8691
|
+
}
|
|
8692
|
+
async persistProcessedEvents() {
|
|
8693
|
+
if (!this.deps) return;
|
|
8694
|
+
const arr = Array.from(this.processedEventIds);
|
|
8695
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_PROCESSED_EVENTS, JSON.stringify(arr));
|
|
7487
8696
|
}
|
|
7488
8697
|
// ===========================================================================
|
|
7489
|
-
// Private
|
|
8698
|
+
// Private — Relay URL Change Detection
|
|
7490
8699
|
// ===========================================================================
|
|
7491
|
-
|
|
7492
|
-
if (
|
|
7493
|
-
const
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
|
|
7499
|
-
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
8700
|
+
async checkAndClearOnRelayChange(currentRelayUrl) {
|
|
8701
|
+
if (!this.deps) return;
|
|
8702
|
+
const stored = await this.deps.storage.get(STORAGE_KEYS_GLOBAL.GROUP_CHAT_RELAY_URL);
|
|
8703
|
+
if (stored && stored !== currentRelayUrl) {
|
|
8704
|
+
this.groups.clear();
|
|
8705
|
+
this.messages.clear();
|
|
8706
|
+
this.members.clear();
|
|
8707
|
+
this.processedEventIds.clear();
|
|
8708
|
+
await this.persistAll();
|
|
8709
|
+
}
|
|
8710
|
+
if (!stored) {
|
|
8711
|
+
for (const group of this.groups.values()) {
|
|
8712
|
+
if (group.relayUrl && group.relayUrl !== currentRelayUrl) {
|
|
8713
|
+
this.groups.clear();
|
|
8714
|
+
this.messages.clear();
|
|
8715
|
+
this.members.clear();
|
|
8716
|
+
this.processedEventIds.clear();
|
|
8717
|
+
await this.persistAll();
|
|
8718
|
+
break;
|
|
8719
|
+
}
|
|
7509
8720
|
}
|
|
7510
8721
|
}
|
|
7511
|
-
|
|
7512
|
-
|
|
8722
|
+
await this.deps.storage.set(STORAGE_KEYS_GLOBAL.GROUP_CHAT_RELAY_URL, currentRelayUrl);
|
|
8723
|
+
}
|
|
8724
|
+
// ===========================================================================
|
|
8725
|
+
// Private — Message Content Wrapping
|
|
8726
|
+
// ===========================================================================
|
|
8727
|
+
wrapMessageContent(content, senderNametag) {
|
|
8728
|
+
if (senderNametag) {
|
|
8729
|
+
return JSON.stringify({ senderNametag, text: content });
|
|
7513
8730
|
}
|
|
7514
|
-
|
|
8731
|
+
return content;
|
|
7515
8732
|
}
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
timestamp: incoming.timestamp,
|
|
7522
|
-
tags: incoming.tags
|
|
7523
|
-
};
|
|
7524
|
-
this.broadcasts.set(message.id, message);
|
|
7525
|
-
this.deps.emitEvent("message:broadcast", message);
|
|
7526
|
-
for (const handler of this.broadcastHandlers) {
|
|
7527
|
-
try {
|
|
7528
|
-
handler(message);
|
|
7529
|
-
} catch (error) {
|
|
7530
|
-
console.error("[Communications] Handler error:", error);
|
|
8733
|
+
unwrapMessageContent(content) {
|
|
8734
|
+
try {
|
|
8735
|
+
const parsed = JSON.parse(content);
|
|
8736
|
+
if (typeof parsed === "object" && parsed.text !== void 0) {
|
|
8737
|
+
return { text: parsed.text, senderNametag: parsed.senderNametag || null };
|
|
7531
8738
|
}
|
|
8739
|
+
} catch {
|
|
7532
8740
|
}
|
|
8741
|
+
return { text: content, senderNametag: null };
|
|
7533
8742
|
}
|
|
7534
8743
|
// ===========================================================================
|
|
7535
|
-
// Private
|
|
8744
|
+
// Private — Event Tag Helpers
|
|
7536
8745
|
// ===========================================================================
|
|
7537
|
-
|
|
7538
|
-
const
|
|
7539
|
-
|
|
8746
|
+
getGroupIdFromEvent(event) {
|
|
8747
|
+
const hTag = event.tags.find((t) => t[0] === "h");
|
|
8748
|
+
return hTag ? hTag[1] : null;
|
|
7540
8749
|
}
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
const
|
|
7545
|
-
|
|
7546
|
-
|
|
8750
|
+
getGroupIdFromMetadataEvent(event) {
|
|
8751
|
+
const dTag = event.tags.find((t) => t[0] === "d");
|
|
8752
|
+
if (dTag?.[1]) return dTag[1];
|
|
8753
|
+
const hTag = event.tags.find((t) => t[0] === "h");
|
|
8754
|
+
return hTag?.[1] ?? null;
|
|
8755
|
+
}
|
|
8756
|
+
extractReplyTo(event) {
|
|
8757
|
+
const eTag = event.tags.find((t) => t[0] === "e" && t[3] === "reply");
|
|
8758
|
+
return eTag ? eTag[1] : void 0;
|
|
8759
|
+
}
|
|
8760
|
+
extractPreviousIds(event) {
|
|
8761
|
+
const previousTag = event.tags.find((t) => t[0] === "previous");
|
|
8762
|
+
return previousTag ? previousTag.slice(1) : void 0;
|
|
8763
|
+
}
|
|
8764
|
+
parseGroupMetadata(event) {
|
|
8765
|
+
try {
|
|
8766
|
+
const groupId = this.getGroupIdFromMetadataEvent(event);
|
|
8767
|
+
if (!groupId) return null;
|
|
8768
|
+
let name = "Unnamed Group";
|
|
8769
|
+
let description;
|
|
8770
|
+
let picture;
|
|
8771
|
+
let isPrivate = false;
|
|
8772
|
+
if (event.content && event.content.trim()) {
|
|
8773
|
+
try {
|
|
8774
|
+
const metadata = JSON.parse(event.content);
|
|
8775
|
+
name = metadata.name || name;
|
|
8776
|
+
description = metadata.about || metadata.description;
|
|
8777
|
+
picture = metadata.picture;
|
|
8778
|
+
isPrivate = metadata.private === true;
|
|
8779
|
+
} catch {
|
|
8780
|
+
}
|
|
8781
|
+
}
|
|
8782
|
+
for (const tag of event.tags) {
|
|
8783
|
+
if (tag[0] === "name" && tag[1]) name = tag[1];
|
|
8784
|
+
if (tag[0] === "about" && tag[1]) description = tag[1];
|
|
8785
|
+
if (tag[0] === "picture" && tag[1]) picture = tag[1];
|
|
8786
|
+
if (tag[0] === "private") isPrivate = true;
|
|
8787
|
+
if (tag[0] === "public" && tag[1] === "false") isPrivate = true;
|
|
8788
|
+
}
|
|
8789
|
+
return {
|
|
8790
|
+
id: groupId,
|
|
8791
|
+
relayUrl: this.config.relays[0] || "",
|
|
8792
|
+
name,
|
|
8793
|
+
description,
|
|
8794
|
+
picture,
|
|
8795
|
+
visibility: isPrivate ? GroupVisibility.PRIVATE : GroupVisibility.PUBLIC,
|
|
8796
|
+
createdAt: event.created_at * 1e3
|
|
8797
|
+
};
|
|
8798
|
+
} catch {
|
|
8799
|
+
return null;
|
|
7547
8800
|
}
|
|
7548
8801
|
}
|
|
7549
8802
|
// ===========================================================================
|
|
7550
|
-
// Private
|
|
8803
|
+
// Private — Utility
|
|
7551
8804
|
// ===========================================================================
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
}
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
return
|
|
8805
|
+
/** Subscribe and track the subscription ID for cleanup. */
|
|
8806
|
+
trackSubscription(filter, handlers) {
|
|
8807
|
+
const subId = this.client.subscribe(filter, {
|
|
8808
|
+
onEvent: handlers.onEvent,
|
|
8809
|
+
onEndOfStoredEvents: handlers.onEndOfStoredEvents ?? (() => {
|
|
8810
|
+
})
|
|
8811
|
+
});
|
|
8812
|
+
this.subscriptionIds.push(subId);
|
|
8813
|
+
return subId;
|
|
8814
|
+
}
|
|
8815
|
+
/** Subscribe for a one-shot fetch, auto-unsubscribe on EOSE or timeout. */
|
|
8816
|
+
oneshotSubscription(filter, opts) {
|
|
8817
|
+
return new Promise((resolve) => {
|
|
8818
|
+
let done = false;
|
|
8819
|
+
let subId;
|
|
8820
|
+
const finish = () => {
|
|
8821
|
+
if (done) return;
|
|
8822
|
+
done = true;
|
|
8823
|
+
if (subId) {
|
|
8824
|
+
try {
|
|
8825
|
+
this.client.unsubscribe(subId);
|
|
8826
|
+
} catch {
|
|
8827
|
+
}
|
|
8828
|
+
const idx = this.subscriptionIds.indexOf(subId);
|
|
8829
|
+
if (idx >= 0) this.subscriptionIds.splice(idx, 1);
|
|
8830
|
+
}
|
|
8831
|
+
resolve(opts.onComplete());
|
|
8832
|
+
};
|
|
8833
|
+
subId = this.client.subscribe(filter, {
|
|
8834
|
+
onEvent: (event) => {
|
|
8835
|
+
if (!done) opts.onEvent(event);
|
|
8836
|
+
},
|
|
8837
|
+
onEndOfStoredEvents: finish
|
|
8838
|
+
});
|
|
8839
|
+
this.subscriptionIds.push(subId);
|
|
8840
|
+
setTimeout(finish, opts.timeoutMs ?? 5e3);
|
|
8841
|
+
});
|
|
7561
8842
|
}
|
|
7562
8843
|
ensureInitialized() {
|
|
7563
8844
|
if (!this.deps) {
|
|
7564
|
-
throw new Error("
|
|
8845
|
+
throw new Error("GroupChatModule not initialized");
|
|
8846
|
+
}
|
|
8847
|
+
}
|
|
8848
|
+
async ensureConnected() {
|
|
8849
|
+
if (!this.connected) {
|
|
8850
|
+
await this.connect();
|
|
8851
|
+
}
|
|
8852
|
+
}
|
|
8853
|
+
randomId() {
|
|
8854
|
+
const bytes = new Uint8Array(8);
|
|
8855
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) {
|
|
8856
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
8857
|
+
} else {
|
|
8858
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
8859
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
8860
|
+
}
|
|
7565
8861
|
}
|
|
8862
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
7566
8863
|
}
|
|
7567
8864
|
};
|
|
7568
|
-
function
|
|
7569
|
-
return new
|
|
8865
|
+
function createGroupChatModule(config) {
|
|
8866
|
+
return new GroupChatModule(config);
|
|
7570
8867
|
}
|
|
7571
8868
|
|
|
7572
8869
|
// core/encryption.ts
|
|
@@ -8434,9 +9731,9 @@ var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign
|
|
|
8434
9731
|
var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
8435
9732
|
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
8436
9733
|
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
8437
|
-
var
|
|
9734
|
+
var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
|
|
8438
9735
|
function isValidNametag(nametag) {
|
|
8439
|
-
if ((0,
|
|
9736
|
+
if ((0, import_nostr_js_sdk3.isPhoneNumber)(nametag)) return true;
|
|
8440
9737
|
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8441
9738
|
}
|
|
8442
9739
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
@@ -8482,12 +9779,13 @@ var Sphere = class _Sphere {
|
|
|
8482
9779
|
// Modules
|
|
8483
9780
|
_payments;
|
|
8484
9781
|
_communications;
|
|
9782
|
+
_groupChat = null;
|
|
8485
9783
|
// Events
|
|
8486
9784
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
8487
9785
|
// ===========================================================================
|
|
8488
9786
|
// Constructor (private)
|
|
8489
9787
|
// ===========================================================================
|
|
8490
|
-
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
|
|
9788
|
+
constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider, groupChatConfig) {
|
|
8491
9789
|
this._storage = storage;
|
|
8492
9790
|
this._transport = transport;
|
|
8493
9791
|
this._oracle = oracle;
|
|
@@ -8497,6 +9795,7 @@ var Sphere = class _Sphere {
|
|
|
8497
9795
|
}
|
|
8498
9796
|
this._payments = createPaymentsModule({ l1: l1Config });
|
|
8499
9797
|
this._communications = createCommunicationsModule();
|
|
9798
|
+
this._groupChat = groupChatConfig ? createGroupChatModule(groupChatConfig) : null;
|
|
8500
9799
|
}
|
|
8501
9800
|
// ===========================================================================
|
|
8502
9801
|
// Static Methods - Wallet Management
|
|
@@ -8544,6 +9843,7 @@ var Sphere = class _Sphere {
|
|
|
8544
9843
|
* ```
|
|
8545
9844
|
*/
|
|
8546
9845
|
static async init(options) {
|
|
9846
|
+
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8547
9847
|
const walletExists = await _Sphere.exists(options.storage);
|
|
8548
9848
|
if (walletExists) {
|
|
8549
9849
|
const sphere2 = await _Sphere.load({
|
|
@@ -8552,7 +9852,8 @@ var Sphere = class _Sphere {
|
|
|
8552
9852
|
oracle: options.oracle,
|
|
8553
9853
|
tokenStorage: options.tokenStorage,
|
|
8554
9854
|
l1: options.l1,
|
|
8555
|
-
price: options.price
|
|
9855
|
+
price: options.price,
|
|
9856
|
+
groupChat
|
|
8556
9857
|
});
|
|
8557
9858
|
return { sphere: sphere2, created: false };
|
|
8558
9859
|
}
|
|
@@ -8577,10 +9878,34 @@ var Sphere = class _Sphere {
|
|
|
8577
9878
|
derivationPath: options.derivationPath,
|
|
8578
9879
|
nametag: options.nametag,
|
|
8579
9880
|
l1: options.l1,
|
|
8580
|
-
price: options.price
|
|
9881
|
+
price: options.price,
|
|
9882
|
+
groupChat
|
|
8581
9883
|
});
|
|
8582
9884
|
return { sphere, created: true, generatedMnemonic };
|
|
8583
9885
|
}
|
|
9886
|
+
/**
|
|
9887
|
+
* Resolve groupChat config from init/create/load options.
|
|
9888
|
+
* - `true` → use network-default relays
|
|
9889
|
+
* - `GroupChatModuleConfig` → pass through
|
|
9890
|
+
* - `undefined` → no groupchat
|
|
9891
|
+
*/
|
|
9892
|
+
/**
|
|
9893
|
+
* Resolve GroupChat config from Sphere.init() options.
|
|
9894
|
+
* Note: impl/shared/resolvers.ts has a similar resolver for provider-level config
|
|
9895
|
+
* (different input shape: { enabled?, relays? }). Both fill relay URLs from network defaults.
|
|
9896
|
+
*/
|
|
9897
|
+
static resolveGroupChatConfig(config, network) {
|
|
9898
|
+
if (!config) return void 0;
|
|
9899
|
+
if (config === true) {
|
|
9900
|
+
const netConfig = network ? NETWORKS[network] : NETWORKS.mainnet;
|
|
9901
|
+
return { relays: [...netConfig.groupRelays] };
|
|
9902
|
+
}
|
|
9903
|
+
if (!config.relays || config.relays.length === 0) {
|
|
9904
|
+
const netConfig = network ? NETWORKS[network] : NETWORKS.mainnet;
|
|
9905
|
+
return { ...config, relays: [...netConfig.groupRelays] };
|
|
9906
|
+
}
|
|
9907
|
+
return config;
|
|
9908
|
+
}
|
|
8584
9909
|
/**
|
|
8585
9910
|
* Create new wallet with mnemonic
|
|
8586
9911
|
*/
|
|
@@ -8591,13 +9916,15 @@ var Sphere = class _Sphere {
|
|
|
8591
9916
|
if (await _Sphere.exists(options.storage)) {
|
|
8592
9917
|
throw new Error("Wallet already exists. Use Sphere.load() or Sphere.clear() first.");
|
|
8593
9918
|
}
|
|
9919
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8594
9920
|
const sphere = new _Sphere(
|
|
8595
9921
|
options.storage,
|
|
8596
9922
|
options.transport,
|
|
8597
9923
|
options.oracle,
|
|
8598
9924
|
options.tokenStorage,
|
|
8599
9925
|
options.l1,
|
|
8600
|
-
options.price
|
|
9926
|
+
options.price,
|
|
9927
|
+
groupChatConfig
|
|
8601
9928
|
);
|
|
8602
9929
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
|
|
8603
9930
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
@@ -8622,13 +9949,15 @@ var Sphere = class _Sphere {
|
|
|
8622
9949
|
if (!await _Sphere.exists(options.storage)) {
|
|
8623
9950
|
throw new Error("No wallet found. Use Sphere.create() to create a new wallet.");
|
|
8624
9951
|
}
|
|
9952
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
8625
9953
|
const sphere = new _Sphere(
|
|
8626
9954
|
options.storage,
|
|
8627
9955
|
options.transport,
|
|
8628
9956
|
options.oracle,
|
|
8629
9957
|
options.tokenStorage,
|
|
8630
9958
|
options.l1,
|
|
8631
|
-
options.price
|
|
9959
|
+
options.price,
|
|
9960
|
+
groupChatConfig
|
|
8632
9961
|
);
|
|
8633
9962
|
await sphere.loadIdentityFromStorage();
|
|
8634
9963
|
await sphere.initializeProviders();
|
|
@@ -8667,13 +9996,15 @@ var Sphere = class _Sphere {
|
|
|
8667
9996
|
await options.storage.connect();
|
|
8668
9997
|
console.log("[Sphere.import] Storage reconnected");
|
|
8669
9998
|
}
|
|
9999
|
+
const groupChatConfig = _Sphere.resolveGroupChatConfig(options.groupChat);
|
|
8670
10000
|
const sphere = new _Sphere(
|
|
8671
10001
|
options.storage,
|
|
8672
10002
|
options.transport,
|
|
8673
10003
|
options.oracle,
|
|
8674
10004
|
options.tokenStorage,
|
|
8675
10005
|
options.l1,
|
|
8676
|
-
options.price
|
|
10006
|
+
options.price,
|
|
10007
|
+
groupChatConfig
|
|
8677
10008
|
);
|
|
8678
10009
|
if (options.mnemonic) {
|
|
8679
10010
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
@@ -8833,6 +10164,10 @@ var Sphere = class _Sphere {
|
|
|
8833
10164
|
this.ensureReady();
|
|
8834
10165
|
return this._communications;
|
|
8835
10166
|
}
|
|
10167
|
+
/** Group chat module (NIP-29). Null if not configured. */
|
|
10168
|
+
get groupChat() {
|
|
10169
|
+
return this._groupChat;
|
|
10170
|
+
}
|
|
8836
10171
|
// ===========================================================================
|
|
8837
10172
|
// Public Properties - State
|
|
8838
10173
|
// ===========================================================================
|
|
@@ -9703,8 +11038,14 @@ var Sphere = class _Sphere {
|
|
|
9703
11038
|
transport: this._transport,
|
|
9704
11039
|
emitEvent
|
|
9705
11040
|
});
|
|
11041
|
+
this._groupChat?.initialize({
|
|
11042
|
+
identity: this._identity,
|
|
11043
|
+
storage: this._storage,
|
|
11044
|
+
emitEvent
|
|
11045
|
+
});
|
|
9706
11046
|
await this._payments.load();
|
|
9707
11047
|
await this._communications.load();
|
|
11048
|
+
await this._groupChat?.load();
|
|
9708
11049
|
}
|
|
9709
11050
|
/**
|
|
9710
11051
|
* Derive address at a specific index
|
|
@@ -10299,7 +11640,7 @@ var Sphere = class _Sphere {
|
|
|
10299
11640
|
*/
|
|
10300
11641
|
cleanNametag(raw) {
|
|
10301
11642
|
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10302
|
-
return (0,
|
|
11643
|
+
return (0, import_nostr_js_sdk3.normalizeNametag)(stripped);
|
|
10303
11644
|
}
|
|
10304
11645
|
// ===========================================================================
|
|
10305
11646
|
// Public Methods - Lifecycle
|
|
@@ -10307,6 +11648,7 @@ var Sphere = class _Sphere {
|
|
|
10307
11648
|
async destroy() {
|
|
10308
11649
|
this._payments.destroy();
|
|
10309
11650
|
this._communications.destroy();
|
|
11651
|
+
this._groupChat?.destroy();
|
|
10310
11652
|
await this._transport.disconnect();
|
|
10311
11653
|
await this._storage.disconnect();
|
|
10312
11654
|
await this._oracle.disconnect();
|
|
@@ -10527,8 +11869,14 @@ var Sphere = class _Sphere {
|
|
|
10527
11869
|
transport: this._transport,
|
|
10528
11870
|
emitEvent
|
|
10529
11871
|
});
|
|
11872
|
+
this._groupChat?.initialize({
|
|
11873
|
+
identity: this._identity,
|
|
11874
|
+
storage: this._storage,
|
|
11875
|
+
emitEvent
|
|
11876
|
+
});
|
|
10530
11877
|
await this._payments.load();
|
|
10531
11878
|
await this._communications.load();
|
|
11879
|
+
await this._groupChat?.load();
|
|
10532
11880
|
}
|
|
10533
11881
|
// ===========================================================================
|
|
10534
11882
|
// Private: Helpers
|