@unicitylabs/sphere-sdk 0.1.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/README.md ADDED
@@ -0,0 +1,1112 @@
1
+ # Sphere SDK
2
+
3
+ A modular TypeScript SDK for Unicity wallet operations supporting both Layer 1 (ALPHA blockchain) and Layer 3 (Unicity state transition network).
4
+
5
+ ## Features
6
+
7
+ - **Wallet Management** - BIP39/BIP32 key derivation, AES-256 encryption
8
+ - **L1 Payments** - ALPHA blockchain transactions via Fulcrum WebSocket
9
+ - **L3 Payments** - Token transfers with state transition proofs
10
+ - **Payment Requests** - Request payments with async response tracking
11
+ - **Nostr Transport** - P2P messaging with NIP-04 encryption
12
+ - **IPFS Storage** - Decentralized token backup with Helia
13
+ - **Token Splitting** - Partial transfer amount calculations
14
+ - **Multi-Address** - HD address derivation (BIP32/BIP44)
15
+ - **TXF Serialization** - Token eXchange Format for storage and transfer
16
+ - **Token Validation** - Aggregator-based token verification
17
+ - **Core Utilities** - Crypto, currency, bech32, base58 functions
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @unicitylabs/sphere-sdk
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```typescript
28
+ import { Sphere } from '@unicitylabs/sphere-sdk';
29
+ import { createBrowserProviders } from '@unicitylabs/sphere-sdk/impl/browser';
30
+
31
+ // Create providers (browser) - defaults to mainnet
32
+ const providers = createBrowserProviders();
33
+
34
+ // Or use testnet for development
35
+ const testnetProviders = createBrowserProviders({ network: 'testnet' });
36
+
37
+ // Initialize (auto-creates wallet if needed)
38
+ const { sphere, created, generatedMnemonic } = await Sphere.init({
39
+ ...providers,
40
+ autoGenerate: true, // Generate mnemonic if wallet doesn't exist
41
+ });
42
+
43
+ if (created && generatedMnemonic) {
44
+ console.log('Save this mnemonic:', generatedMnemonic);
45
+ }
46
+
47
+ // Get identity
48
+ console.log('Address:', sphere.identity?.address);
49
+
50
+ // Check balance
51
+ const balance = await sphere.payments.getBalance();
52
+ console.log('L3 Balance:', balance);
53
+
54
+ // Send tokens
55
+ const result = await sphere.payments.send({
56
+ recipient: '@alice',
57
+ amount: '1000000',
58
+ coinId: 'UCT',
59
+ });
60
+
61
+ // Derive additional addresses
62
+ const addr1 = sphere.deriveAddress(1);
63
+ console.log('Address 1:', addr1.address);
64
+ ```
65
+
66
+ ## Network Configuration
67
+
68
+ The SDK supports three network presets that configure all services automatically:
69
+
70
+ | Network | Aggregator | Nostr Relay | Electrum (L1) |
71
+ |---------|------------|-------------|---------------|
72
+ | `mainnet` | aggregator.unicity.network | relay.unicity.network | fulcrum.alpha.unicity.network |
73
+ | `testnet` | goggregator-test.unicity.network | nostr-relay.testnet.unicity.network | fulcrum.alpha.testnet.unicity.network |
74
+ | `dev` | dev-aggregator.dyndns.org | nostr-relay.testnet.unicity.network | fulcrum.alpha.testnet.unicity.network |
75
+
76
+ ```typescript
77
+ // Use testnet for all services
78
+ const providers = createBrowserProviders({ network: 'testnet' });
79
+
80
+ // Override specific services while using network preset
81
+ const providers = createBrowserProviders({
82
+ network: 'testnet',
83
+ oracle: { url: 'https://custom-aggregator.example.com' }, // custom oracle
84
+ });
85
+
86
+ // Enable L1 with network defaults
87
+ const providers = createBrowserProviders({
88
+ network: 'testnet',
89
+ l1: { enableVesting: true }, // uses testnet electrum URL automatically
90
+ });
91
+ ```
92
+
93
+ ## Multi-Address Support
94
+
95
+ The SDK supports HD (Hierarchical Deterministic) wallets with multiple addresses:
96
+
97
+ ```typescript
98
+ // Get current address index
99
+ const currentIndex = sphere.getCurrentAddressIndex(); // 0
100
+
101
+ // Switch to a different address
102
+ await sphere.switchToAddress(1);
103
+ console.log(sphere.identity?.address); // alpha1... (address at index 1)
104
+
105
+ // Register nametag for this address (independent per address)
106
+ await sphere.registerNametag('bob');
107
+
108
+ // Switch back to first address
109
+ await sphere.switchToAddress(0);
110
+
111
+ // Get nametag for specific address
112
+ const bobNametag = sphere.getNametagForAddress(1); // 'bob'
113
+
114
+ // Get all address nametags
115
+ const allNametags = sphere.getAllAddressNametags();
116
+ // Map { 0 => 'alice', 1 => 'bob' }
117
+
118
+ // Derive address without switching (for display/receiving)
119
+ const addr2 = sphere.deriveAddress(2);
120
+ console.log(addr2.address, addr2.publicKey);
121
+ ```
122
+
123
+ ### Identity Properties
124
+
125
+ ```typescript
126
+ interface Identity {
127
+ publicKey: string; // secp256k1 public key (hex)
128
+ address: string; // L1 address (alpha1...)
129
+ predicateAddress?: string; // L3 address (DIRECT://...)
130
+ ipnsName?: string; // IPNS name for token sync
131
+ nametag?: string; // Registered nametag (@username)
132
+ }
133
+
134
+ // Access identity
135
+ console.log(sphere.identity?.address); // alpha1qw3e...
136
+ console.log(sphere.identity?.predicateAddress); // DIRECT://0000be36...
137
+ console.log(sphere.identity?.nametag); // alice
138
+ ```
139
+
140
+ ### Address Change Event
141
+
142
+ ```typescript
143
+ // Listen for address switches
144
+ sphere.on('identity:changed', (event) => {
145
+ console.log('Switched to address index:', event.data.addressIndex);
146
+ console.log('L1 address:', event.data.address);
147
+ console.log('L3 address:', event.data.predicateAddress);
148
+ console.log('Public key:', event.data.publicKey);
149
+ console.log('Nametag:', event.data.nametag);
150
+ });
151
+ ```
152
+
153
+ ## Payment Requests
154
+
155
+ Request payments from others with response tracking:
156
+
157
+ ```typescript
158
+ // Send payment request
159
+ const result = await sphere.payments.sendPaymentRequest('@bob', {
160
+ amount: '1000000',
161
+ coinId: 'UCT',
162
+ message: 'Payment for order #1234',
163
+ });
164
+
165
+ // Wait for response (with 2 minute timeout)
166
+ if (result.success) {
167
+ const response = await sphere.payments.waitForPaymentResponse(result.requestId!, 120000);
168
+ if (response.responseType === 'paid') {
169
+ console.log('Payment received! Transfer:', response.transferId);
170
+ }
171
+ }
172
+
173
+ // Or subscribe to responses
174
+ sphere.payments.onPaymentRequestResponse((response) => {
175
+ console.log(`Response: ${response.responseType}`);
176
+ });
177
+
178
+ // Handle incoming payment requests
179
+ sphere.payments.onPaymentRequest((request) => {
180
+ console.log(`${request.senderNametag} requests ${request.amount} ${request.symbol}`);
181
+
182
+ // Accept and pay
183
+ await sphere.payments.payPaymentRequest(request.id);
184
+
185
+ // Or reject
186
+ await sphere.payments.rejectPaymentRequest(request.id);
187
+ });
188
+ ```
189
+
190
+ ## L1 (ALPHA Blockchain) Operations
191
+
192
+ Access L1 payments through `sphere.payments.l1`:
193
+
194
+ ```typescript
195
+ // L1 configuration is optional (has defaults)
196
+ const { sphere } = await Sphere.init({
197
+ ...providers,
198
+ autoGenerate: true,
199
+ l1: {
200
+ electrumUrl: 'wss://fulcrum.alpha.unicity.network:50004', // default
201
+ defaultFeeRate: 10, // sat/byte, default
202
+ enableVesting: true, // default
203
+ },
204
+ });
205
+
206
+ // Get L1 balance
207
+ const balance = await sphere.payments.l1.getBalance();
208
+ console.log('L1 Balance:', balance.total);
209
+ console.log('Vested:', balance.vested);
210
+ console.log('Unvested:', balance.unvested);
211
+
212
+ // Get UTXOs
213
+ const utxos = await sphere.payments.l1.getUtxos();
214
+ console.log('UTXOs:', utxos.length);
215
+
216
+ // Send L1 transaction
217
+ const result = await sphere.payments.l1.send({
218
+ to: 'alpha1qxyz...',
219
+ amount: '100000', // in satoshis
220
+ feeRate: 5, // optional, sat/byte
221
+ });
222
+
223
+ if (result.success) {
224
+ console.log('TX Hash:', result.txHash);
225
+ }
226
+
227
+ // Get transaction history
228
+ const history = await sphere.payments.l1.getHistory(10);
229
+
230
+ // Estimate fee
231
+ const { fee, feeRate } = await sphere.payments.l1.estimateFee('alpha1...', '50000');
232
+ ```
233
+
234
+ ## Alternative: Manual Create/Load
235
+
236
+ ```typescript
237
+ import { Sphere } from '@unicitylabs/sphere-sdk';
238
+ import {
239
+ createLocalStorageProvider,
240
+ createNostrTransportProvider,
241
+ createUnicityAggregatorProvider,
242
+ } from '@unicitylabs/sphere-sdk/impl/browser';
243
+
244
+ const storage = createLocalStorageProvider();
245
+ const transport = createNostrTransportProvider();
246
+ const oracle = createUnicityAggregatorProvider({ url: '/rpc' });
247
+
248
+ // Check if wallet exists
249
+ if (await Sphere.exists(storage)) {
250
+ // Load existing wallet
251
+ const sphere = await Sphere.load({ storage, transport, oracle });
252
+ } else {
253
+ // Create new wallet with mnemonic
254
+ const mnemonic = Sphere.generateMnemonic();
255
+ const sphere = await Sphere.create({
256
+ mnemonic,
257
+ storage,
258
+ transport,
259
+ oracle,
260
+ });
261
+ console.log('Save this mnemonic:', mnemonic);
262
+ }
263
+ ```
264
+
265
+ ## Import from Master Key (Legacy Wallets)
266
+
267
+ For compatibility with legacy wallet files (.dat, .txt):
268
+
269
+ ```typescript
270
+ // Import from master key + chain code (BIP32 mode)
271
+ const sphere = await Sphere.import({
272
+ masterKey: '64-hex-chars-master-private-key',
273
+ chainCode: '64-hex-chars-chain-code',
274
+ basePath: "m/84'/1'/0'", // from wallet.dat descriptor
275
+ derivationMode: 'bip32',
276
+ storage, transport, oracle,
277
+ });
278
+
279
+ // Import from master key only (WIF HMAC mode)
280
+ const sphere = await Sphere.import({
281
+ masterKey: '64-hex-chars-master-private-key',
282
+ derivationMode: 'wif_hmac',
283
+ storage, transport, oracle,
284
+ });
285
+ ```
286
+
287
+ ## Wallet Export/Import (JSON)
288
+
289
+ ```typescript
290
+ // Export to JSON (for backup)
291
+ const json = sphere.exportToJSON();
292
+ console.log(JSON.stringify(json));
293
+
294
+ // Export with encryption
295
+ const encryptedJson = sphere.exportToJSON({ password: 'user-password' });
296
+
297
+ // Export with multiple addresses
298
+ const multiJson = sphere.exportToJSON({ addressCount: 5 });
299
+
300
+ // Import from JSON
301
+ const { success, mnemonic, error } = await Sphere.importFromJSON({
302
+ jsonContent: JSON.stringify(json),
303
+ password: 'user-password', // if encrypted
304
+ storage, transport, oracle,
305
+ });
306
+
307
+ if (success && mnemonic) {
308
+ console.log('Recovered mnemonic:', mnemonic);
309
+ }
310
+ ```
311
+
312
+ ## Wallet Info & Backup
313
+
314
+ ```typescript
315
+ // Get wallet info
316
+ const info = sphere.getWalletInfo();
317
+ console.log('Source:', info.source); // 'mnemonic' | 'file'
318
+ console.log('Has mnemonic:', info.hasMnemonic);
319
+ console.log('Derivation mode:', info.derivationMode);
320
+ console.log('Base path:', info.basePath);
321
+
322
+ // Get mnemonic for backup (if available)
323
+ const mnemonic = sphere.getMnemonic();
324
+ if (mnemonic) {
325
+ console.log('Backup this:', mnemonic);
326
+ }
327
+ ```
328
+
329
+ ## Import from Legacy Files (.dat, .txt)
330
+
331
+ ```typescript
332
+ // Import from wallet.dat file
333
+ const fileBuffer = await file.arrayBuffer();
334
+ const result = await Sphere.importFromLegacyFile({
335
+ fileContent: new Uint8Array(fileBuffer),
336
+ fileName: 'wallet.dat',
337
+ password: 'wallet-password', // if encrypted
338
+ onDecryptProgress: (i, total) => console.log(`Decrypting: ${i}/${total}`),
339
+ storage, transport, oracle,
340
+ });
341
+
342
+ if (result.needsPassword) {
343
+ // Re-prompt user for password
344
+ }
345
+
346
+ if (result.success) {
347
+ const sphere = result.sphere;
348
+ console.log('Imported wallet:', sphere.identity?.address);
349
+ }
350
+
351
+ // Import from text backup file
352
+ const textContent = await file.text();
353
+ const result = await Sphere.importFromLegacyFile({
354
+ fileContent: textContent,
355
+ fileName: 'backup.txt',
356
+ storage, transport, oracle,
357
+ });
358
+
359
+ // Detect file type and encryption status
360
+ const fileType = Sphere.detectLegacyFileType(fileName, content);
361
+ // Returns: 'dat' | 'txt' | 'json' | 'mnemonic' | 'unknown'
362
+
363
+ const isEncrypted = Sphere.isLegacyFileEncrypted(fileName, content);
364
+ ```
365
+
366
+ ## Core Utilities
367
+
368
+ The SDK exports commonly needed utility functions:
369
+
370
+ ```typescript
371
+ import {
372
+ // Crypto
373
+ bytesToHex, hexToBytes,
374
+ generateMnemonic, validateMnemonic,
375
+ sha256, ripemd160, hash160,
376
+ getPublicKey, createKeyPair,
377
+ deriveAddressInfo,
378
+
379
+ // Currency conversion
380
+ toSmallestUnit, // "1.5" → 1500000000000000000n
381
+ toHumanReadable, // 1500000000000000000n → "1.5"
382
+ formatAmount, // Format with decimals and symbol
383
+
384
+ // Address encoding
385
+ encodeBech32, decodeBech32,
386
+ createAddress, isValidBech32,
387
+
388
+ // Base58 (Bitcoin-style)
389
+ base58Encode, base58Decode,
390
+ isValidPrivateKey,
391
+
392
+ // General utilities
393
+ sleep, randomHex, randomUUID,
394
+ findPattern, extractFromText,
395
+ } from '@unicitylabs/sphere-sdk';
396
+ ```
397
+
398
+ ## TXF Serialization
399
+
400
+ Token eXchange Format for storage and transfer:
401
+
402
+ ```typescript
403
+ import {
404
+ tokenToTxf, // Token → TXF format
405
+ txfToToken, // TXF → Token
406
+ buildTxfStorageData, // Build IPFS storage data
407
+ parseTxfStorageData, // Parse storage data
408
+ getCurrentStateHash, // Get token's current state hash
409
+ hasUncommittedTransactions,
410
+ } from '@unicitylabs/sphere-sdk';
411
+
412
+ // Convert token to TXF
413
+ const txf = tokenToTxf(token);
414
+ console.log(txf.genesis.data.tokenId);
415
+
416
+ // Build storage data for IPFS
417
+ const storageData = await buildTxfStorageData(tokens, {
418
+ version: 1,
419
+ address: 'alpha1...',
420
+ ipnsName: 'k51...',
421
+ });
422
+ ```
423
+
424
+ ## Token Validation
425
+
426
+ Validate tokens against the aggregator:
427
+
428
+ ```typescript
429
+ import { createTokenValidator } from '@unicitylabs/sphere-sdk';
430
+
431
+ const validator = createTokenValidator({
432
+ aggregatorClient: oracleProvider,
433
+ trustBase: trustBaseData,
434
+ skipVerification: false,
435
+ });
436
+
437
+ // Validate all tokens
438
+ const { validTokens, issues } = await validator.validateAllTokens(tokens);
439
+
440
+ // Check if token state is spent
441
+ const isSpent = await validator.isTokenStateSpent(tokenId, stateHash, publicKey);
442
+
443
+ // Check spent tokens in batch
444
+ const { spentTokens, errors } = await validator.checkSpentTokens(tokens, publicKey);
445
+ ```
446
+
447
+ ## Architecture
448
+
449
+ **Single Identity Model**: L1 and L3 share the same secp256k1 key pair. One mnemonic = one wallet for both layers.
450
+
451
+ ```
452
+ mnemonic → master key → BIP32 derivation → identity
453
+
454
+ ┌─────────────────────┴─────────────────────┐
455
+ │ shared keys │
456
+ │ privateKey: "abc..." (hex secp256k1) │
457
+ │ publicKey: "02def..." (compressed) │
458
+ │ address: "alpha1..." (bech32) │
459
+ └─────────────────────┬─────────────────────┘
460
+
461
+ ┌───────────────────────────────┼───────────────────────────────┐
462
+ ↓ ↓ ↓
463
+ L1 (ALPHA) L3 (Unicity) Nostr
464
+ sphere.payments.l1 sphere.payments sphere.communications
465
+ UTXOs, blockchain Tokens, aggregator P2P messaging
466
+ ```
467
+
468
+ ```
469
+ Sphere (main entry point)
470
+ ├── identity - Wallet identity (address, publicKey, nametag)
471
+ ├── payments - L3 token operations
472
+ │ └── l1 - L1 ALPHA transactions (via sphere.payments.l1)
473
+ └── communications - Direct messages & broadcasts
474
+
475
+ Providers (injectable dependencies)
476
+ ├── StorageProvider - Key-value persistence
477
+ ├── TransportProvider - P2P messaging (Nostr)
478
+ ├── OracleProvider - State validation (Aggregator)
479
+ └── TokenStorageProvider - Token backup (IPFS)
480
+
481
+ Implementation (platform-specific)
482
+ ├── impl/shared/ - Common interfaces & resolvers
483
+ │ ├── config.ts - Base configuration types
484
+ │ └── resolvers.ts - Extend/override pattern utilities
485
+ ├── impl/browser/ - Browser implementations
486
+ │ ├── LocalStorageProvider
487
+ │ ├── IndexedDBTokenStorageProvider
488
+ │ └── createBrowserProviders()
489
+ └── impl/nodejs/ - Node.js implementations
490
+ ├── FileStorageProvider
491
+ ├── FileTokenStorageProvider
492
+ └── createNodeProviders()
493
+
494
+ Core Utilities
495
+ ├── crypto - Key derivation, hashing, signatures
496
+ ├── currency - Amount formatting and conversion
497
+ ├── bech32 - Address encoding (BIP-173)
498
+ └── utils - Base58, patterns, sleep, random
499
+ ```
500
+
501
+ ## Shared Configuration Pattern
502
+
503
+ Both browser and Node.js implementations share common configuration interfaces and resolution logic:
504
+
505
+ ```typescript
506
+ // Base interfaces (impl/shared/config.ts)
507
+ import type {
508
+ BaseTransportConfig, // Common transport options
509
+ BaseOracleConfig, // Common oracle options
510
+ L1Config, // L1 configuration (same for all platforms)
511
+ BaseProviders, // Common result structure
512
+ } from '@unicitylabs/sphere-sdk/impl/shared';
513
+
514
+ // Resolver utilities (impl/shared/resolvers.ts)
515
+ import {
516
+ getNetworkConfig, // Get mainnet/testnet/dev config
517
+ resolveTransportConfig, // Apply extend/override pattern for relays
518
+ resolveOracleConfig, // Resolve oracle URL with fallback
519
+ resolveL1Config, // Resolve L1 with network defaults
520
+ resolveArrayConfig, // Generic array merge helper
521
+ } from '@unicitylabs/sphere-sdk/impl/shared';
522
+ ```
523
+
524
+ ### Extend/Override Pattern
525
+
526
+ The configuration resolution follows a consistent pattern across platforms:
527
+
528
+ ```typescript
529
+ // Priority for arrays: replace > extend > defaults
530
+ const result = resolveArrayConfig(
531
+ networkDefaults, // ['a', 'b']
532
+ config.relays, // If set, replaces entirely
533
+ config.additionalRelays // If set, extends defaults
534
+ );
535
+
536
+ // Examples:
537
+ // No config → ['a', 'b'] (defaults)
538
+ // { relays: ['x'] } → ['x'] (replace)
539
+ // { additionalRelays: ['c'] } → ['a', 'b', 'c'] (extend)
540
+ ```
541
+
542
+ ### Platform-Specific Extensions
543
+
544
+ Each platform extends the base interfaces with platform-specific options:
545
+
546
+ ```typescript
547
+ // Browser: adds reconnectDelay, maxReconnectAttempts
548
+ type TransportConfig = BaseTransportConfig & BrowserTransportExtensions;
549
+
550
+ // Node.js: adds trustBasePath for file-based trust base
551
+ type NodeOracleConfig = BaseOracleConfig & NodeOracleExtensions;
552
+ ```
553
+
554
+ ## Documentation
555
+
556
+ - [Integration Guide](./docs/INTEGRATION.md)
557
+ - [API Reference](./docs/API.md)
558
+
559
+ ## Browser Providers
560
+
561
+ The SDK includes browser-ready provider implementations:
562
+
563
+ | Provider | Description |
564
+ |----------|-------------|
565
+ | `LocalStorageProvider` | Browser localStorage with SSR fallback |
566
+ | `NostrTransportProvider` | Nostr relay messaging with NIP-04 |
567
+ | `UnicityAggregatorProvider` | Unicity aggregator for state proofs |
568
+ | `IpfsStorageProvider` | Helia-based IPFS with HTTP fallback |
569
+
570
+ ## Node.js Providers
571
+
572
+ For CLI and server applications:
573
+
574
+ ```typescript
575
+ import { Sphere } from '@unicitylabs/sphere-sdk';
576
+ import { createNodeProviders } from '@unicitylabs/sphere-sdk/impl/nodejs';
577
+
578
+ // Quick start with testnet
579
+ const providers = createNodeProviders({
580
+ network: 'testnet',
581
+ dataDir: './wallet-data',
582
+ tokensDir: './tokens',
583
+ });
584
+
585
+ const { sphere } = await Sphere.init({
586
+ ...providers,
587
+ autoGenerate: true,
588
+ });
589
+
590
+ // Full configuration
591
+ const providers = createNodeProviders({
592
+ network: 'testnet',
593
+ dataDir: './wallet-data',
594
+ tokensDir: './tokens',
595
+ transport: {
596
+ additionalRelays: ['wss://my-relay.com'],
597
+ timeout: 10000,
598
+ debug: true,
599
+ },
600
+ oracle: {
601
+ apiKey: 'my-api-key',
602
+ trustBasePath: './trustbase.json', // Node.js specific
603
+ },
604
+ l1: {
605
+ enableVesting: true,
606
+ },
607
+ });
608
+ ```
609
+
610
+ ### Manual Provider Creation
611
+
612
+ ```typescript
613
+ import {
614
+ FileStorageProvider,
615
+ FileTokenStorageProvider,
616
+ createNostrTransportProvider,
617
+ createNodeTrustBaseLoader,
618
+ } from '@unicitylabs/sphere-sdk/impl/nodejs';
619
+
620
+ // File-based wallet storage
621
+ const storage = new FileStorageProvider('./wallet-data');
622
+
623
+ // File-based token storage (TXF format)
624
+ const tokenStorage = new FileTokenStorageProvider('./tokens');
625
+
626
+ // Nostr with Node.js WebSocket
627
+ const transport = createNostrTransportProvider({
628
+ relays: ['wss://relay.unicity.network'],
629
+ });
630
+
631
+ // Load trust base from local file
632
+ const trustBaseLoader = createNodeTrustBaseLoader('./trustbase-testnet.json');
633
+ const trustBase = await trustBaseLoader.load();
634
+ ```
635
+
636
+ ## Custom Providers Configuration
637
+
638
+ The SDK uses an **extend/override pattern** for flexible configuration:
639
+
640
+ | Option | Behavior |
641
+ |--------|----------|
642
+ | `relays` | **Replaces** default relays entirely |
643
+ | `additionalRelays` | **Adds** to default relays |
644
+ | `gateways` | **Replaces** default IPFS gateways |
645
+ | `additionalGateways` | **Adds** to default gateways |
646
+ | `url`, `electrumUrl` | **Replaces** default URL (uses network default if not set) |
647
+
648
+ ```typescript
649
+ // Simple: use network preset
650
+ const providers = createBrowserProviders({ network: 'testnet' });
651
+
652
+ // Add extra relays to testnet defaults
653
+ const providers = createBrowserProviders({
654
+ network: 'testnet',
655
+ transport: {
656
+ additionalRelays: ['wss://my-relay.com', 'wss://backup-relay.com'],
657
+ // Result: testnet relay + my-relay + backup-relay
658
+ },
659
+ });
660
+
661
+ // Replace relays entirely (ignores network defaults)
662
+ const providers = createBrowserProviders({
663
+ network: 'testnet',
664
+ transport: {
665
+ relays: ['wss://only-this-relay.com'],
666
+ // Result: only-this-relay (testnet default ignored)
667
+ },
668
+ });
669
+
670
+ // Override aggregator, keep other testnet defaults
671
+ const providers = createBrowserProviders({
672
+ network: 'testnet',
673
+ oracle: {
674
+ url: 'https://my-aggregator.com', // replaces testnet aggregator
675
+ apiKey: 'my-api-key',
676
+ },
677
+ });
678
+
679
+ // Full custom configuration
680
+ const providers = createBrowserProviders({
681
+ network: 'testnet',
682
+ storage: {
683
+ prefix: 'myapp_',
684
+ },
685
+ transport: {
686
+ additionalRelays: ['wss://extra-relay.com'],
687
+ timeout: 15000,
688
+ autoReconnect: true,
689
+ debug: true,
690
+ },
691
+ oracle: {
692
+ url: 'https://custom-aggregator.com',
693
+ apiKey: 'secret',
694
+ timeout: 60000,
695
+ },
696
+ l1: {
697
+ electrumUrl: 'wss://custom-fulcrum.com:50004',
698
+ defaultFeeRate: 5,
699
+ enableVesting: true,
700
+ },
701
+ tokenSync: {
702
+ ipfs: {
703
+ enabled: true,
704
+ additionalGateways: ['https://my-ipfs-gateway.com'],
705
+ },
706
+ },
707
+ });
708
+
709
+ // Enable multiple sync backends
710
+ const providers = createBrowserProviders({
711
+ network: 'mainnet',
712
+ tokenSync: {
713
+ ipfs: { enabled: true, useDht: true },
714
+ cloud: { enabled: true, provider: 'aws', bucket: 'my-backup' }, // future
715
+ },
716
+ });
717
+ ```
718
+
719
+ ## Token Sync Backends
720
+
721
+ The SDK supports multiple token sync backends that can be enabled independently:
722
+
723
+ | Backend | Status | Description |
724
+ |---------|--------|-------------|
725
+ | `ipfs` | ✅ Ready | Decentralized IPFS/IPNS with Helia browser DHT |
726
+ | `mongodb` | ✅ Ready | MongoDB for centralized token storage |
727
+ | `file` | 🚧 Planned | Local file system (Node.js) |
728
+ | `cloud` | 🚧 Planned | Cloud storage (AWS S3, GCP, Azure) |
729
+
730
+ ```typescript
731
+ // Enable IPFS sync with custom gateways
732
+ const providers = createBrowserProviders({
733
+ network: 'testnet',
734
+ tokenSync: {
735
+ ipfs: {
736
+ enabled: true,
737
+ additionalGateways: ['https://my-gateway.com'],
738
+ useDht: true, // Enable browser DHT (Helia)
739
+ },
740
+ },
741
+ });
742
+
743
+ // Enable MongoDB sync
744
+ const providers = createBrowserProviders({
745
+ network: 'mainnet',
746
+ tokenSync: {
747
+ mongodb: {
748
+ enabled: true,
749
+ uri: 'mongodb://localhost:27017',
750
+ database: 'sphere_wallet',
751
+ collection: 'tokens',
752
+ },
753
+ },
754
+ });
755
+
756
+ // Multiple backends for redundancy
757
+ const providers = createBrowserProviders({
758
+ tokenSync: {
759
+ ipfs: { enabled: true },
760
+ mongodb: { enabled: true, uri: 'mongodb://localhost:27017', database: 'wallet' },
761
+ file: { enabled: true, directory: './tokens', format: 'txf' },
762
+ cloud: { enabled: true, provider: 'aws', bucket: 'wallet-backup' },
763
+ },
764
+ });
765
+ ```
766
+
767
+ ## Custom Token Storage Provider
768
+
769
+ You can implement your own `TokenStorageProvider` for custom storage backends:
770
+
771
+ ```typescript
772
+ import type { TokenStorageProvider, TxfStorageDataBase, SaveResult, LoadResult, SyncResult } from '@unicitylabs/sphere-sdk/storage';
773
+ import type { FullIdentity, ProviderStatus } from '@unicitylabs/sphere-sdk/types';
774
+
775
+ class MyCustomStorageProvider implements TokenStorageProvider<TxfStorageDataBase> {
776
+ readonly id = 'my-storage';
777
+ readonly name = 'My Custom Storage';
778
+ readonly type = 'remote' as const;
779
+
780
+ private status: ProviderStatus = 'disconnected';
781
+ private identity: FullIdentity | null = null;
782
+
783
+ setIdentity(identity: FullIdentity): void {
784
+ this.identity = identity;
785
+ }
786
+
787
+ async initialize(): Promise<boolean> {
788
+ // Connect to your storage backend
789
+ this.status = 'connected';
790
+ return true;
791
+ }
792
+
793
+ async shutdown(): Promise<void> {
794
+ this.status = 'disconnected';
795
+ }
796
+
797
+ async connect(): Promise<void> {
798
+ await this.initialize();
799
+ }
800
+
801
+ async disconnect(): Promise<void> {
802
+ await this.shutdown();
803
+ }
804
+
805
+ isConnected(): boolean {
806
+ return this.status === 'connected';
807
+ }
808
+
809
+ getStatus(): ProviderStatus {
810
+ return this.status;
811
+ }
812
+
813
+ async load(): Promise<LoadResult<TxfStorageDataBase>> {
814
+ // Load tokens from your storage
815
+ return {
816
+ success: true,
817
+ data: { _meta: { version: 1, address: this.identity?.address ?? '', formatVersion: '2.0', updatedAt: Date.now() } },
818
+ source: 'remote',
819
+ timestamp: Date.now(),
820
+ };
821
+ }
822
+
823
+ async save(data: TxfStorageDataBase): Promise<SaveResult> {
824
+ // Save tokens to your storage
825
+ return { success: true, timestamp: Date.now() };
826
+ }
827
+
828
+ async sync(localData: TxfStorageDataBase): Promise<SyncResult<TxfStorageDataBase>> {
829
+ // Merge local and remote data
830
+ await this.save(localData);
831
+ return { success: true, merged: localData, added: 0, removed: 0, conflicts: 0 };
832
+ }
833
+ }
834
+
835
+ // Use your custom provider
836
+ const myProvider = new MyCustomStorageProvider();
837
+
838
+ const { sphere } = await Sphere.init({
839
+ ...providers,
840
+ tokenStorage: myProvider,
841
+ autoGenerate: true,
842
+ });
843
+ ```
844
+
845
+ ## Dynamic Provider Management (Runtime)
846
+
847
+ After `Sphere.init()` is called, you can add/remove token storage providers dynamically through UI:
848
+
849
+ ```typescript
850
+ import { createMongoDbStorageProvider } from './my-mongodb-provider';
851
+
852
+ // Add a new provider at runtime (e.g., user enables MongoDB sync in settings)
853
+ const mongoProvider = createMongoDbStorageProvider({
854
+ uri: 'mongodb://localhost:27017',
855
+ database: 'sphere_wallet',
856
+ });
857
+
858
+ await sphere.addTokenStorageProvider(mongoProvider);
859
+
860
+ // Provider is now active and will be used in sync operations
861
+
862
+ // Check if provider exists
863
+ if (sphere.hasTokenStorageProvider('mongodb-token-storage')) {
864
+ console.log('MongoDB sync is enabled');
865
+ }
866
+
867
+ // Get all active providers
868
+ const providers = sphere.getTokenStorageProviders();
869
+ console.log('Active providers:', Array.from(providers.keys()));
870
+
871
+ // Remove a provider (e.g., user disables MongoDB sync)
872
+ await sphere.removeTokenStorageProvider('mongodb-token-storage');
873
+
874
+ // Listen for per-provider sync events
875
+ sphere.on('sync:provider', (event) => {
876
+ console.log(`Provider ${event.providerId}: ${event.success ? 'synced' : 'failed'}`);
877
+ if (event.success) {
878
+ console.log(` Added: ${event.added}, Removed: ${event.removed}`);
879
+ } else {
880
+ console.log(` Error: ${event.error}`);
881
+ }
882
+ });
883
+
884
+ // Trigger sync (syncs with all active providers)
885
+ await sphere.payments.sync();
886
+ ```
887
+
888
+ ### Multiple Providers Example
889
+
890
+ ```typescript
891
+ // User configures multiple sync backends via UI
892
+ const ipfsProvider = createIpfsStorageProvider({ gateways: ['https://ipfs.io'] });
893
+ const mongoProvider = createMongoDbStorageProvider({ uri: 'mongodb://...' });
894
+ const s3Provider = createS3StorageProvider({ bucket: 'wallet-backup' });
895
+
896
+ // Add all providers
897
+ await sphere.addTokenStorageProvider(ipfsProvider);
898
+ await sphere.addTokenStorageProvider(mongoProvider);
899
+ await sphere.addTokenStorageProvider(s3Provider);
900
+
901
+ // Sync syncs with ALL active providers
902
+ // If one fails, others continue (fault-tolerant)
903
+ const result = await sphere.payments.sync();
904
+ console.log(`Synced: +${result.added} -${result.removed}`);
905
+ ```
906
+
907
+ ## Dynamic Relay Management
908
+
909
+ Nostr relays can be added or removed at runtime through the transport provider:
910
+
911
+ ```typescript
912
+ const transport = sphere.getTransport();
913
+
914
+ // Get current relays
915
+ const configuredRelays = transport.getRelays(); // All configured
916
+ const connectedRelays = transport.getConnectedRelays(); // Currently connected
917
+
918
+ // Add a new relay (connects immediately if provider is connected)
919
+ await transport.addRelay('wss://new-relay.com');
920
+
921
+ // Remove a relay (disconnects if connected)
922
+ await transport.removeRelay('wss://old-relay.com');
923
+
924
+ // Check relay status
925
+ transport.hasRelay('wss://relay.com'); // Is configured?
926
+ transport.isRelayConnected('wss://relay.com'); // Is connected?
927
+ ```
928
+
929
+ ### Relay Events
930
+
931
+ ```typescript
932
+ // Listen for relay changes
933
+ sphere.on('transport:relay_added', (event) => {
934
+ console.log(`Relay added: ${event.data.relay}`);
935
+ console.log(`Connected: ${event.data.connected}`);
936
+ });
937
+
938
+ sphere.on('transport:relay_removed', (event) => {
939
+ console.log(`Relay removed: ${event.data.relay}`);
940
+ });
941
+
942
+ sphere.on('transport:error', (event) => {
943
+ console.log(`Transport error: ${event.data.error}`);
944
+ });
945
+ ```
946
+
947
+ ### UI Integration Example
948
+
949
+ ```typescript
950
+ // User adds relay via settings UI
951
+ async function handleAddRelay(relayUrl: string) {
952
+ const transport = sphere.getTransport();
953
+
954
+ if (transport.hasRelay(relayUrl)) {
955
+ showError('Relay already configured');
956
+ return;
957
+ }
958
+
959
+ const success = await transport.addRelay(relayUrl);
960
+ if (success) {
961
+ showSuccess(`Added ${relayUrl}`);
962
+ } else {
963
+ showWarning(`Added but failed to connect to ${relayUrl}`);
964
+ }
965
+ }
966
+
967
+ // User removes relay via settings UI
968
+ async function handleRemoveRelay(relayUrl: string) {
969
+ const transport = sphere.getTransport();
970
+ await transport.removeRelay(relayUrl);
971
+ showSuccess(`Removed ${relayUrl}`);
972
+ }
973
+
974
+ // Display relay status in UI
975
+ function getRelayStatuses() {
976
+ const transport = sphere.getTransport();
977
+ return transport.getRelays().map(relay => ({
978
+ url: relay,
979
+ connected: transport.isRelayConnected(relay),
980
+ }));
981
+ }
982
+ ```
983
+
984
+ ## Nametags
985
+
986
+ Nametags provide human-readable addresses (e.g., `@alice`) for receiving payments.
987
+
988
+ ### Registering a Nametag
989
+
990
+ ```typescript
991
+ // During wallet creation
992
+ const { sphere } = await Sphere.init({
993
+ ...providers,
994
+ mnemonic: 'your twelve words...',
995
+ nametag: 'alice', // Will register @alice
996
+ });
997
+
998
+ // Or after creation
999
+ await sphere.registerNametag('alice');
1000
+
1001
+ // Mint on-chain nametag token (required for receiving via PROXY addresses)
1002
+ const result = await sphere.mintNametag('alice');
1003
+ if (result.success) {
1004
+ console.log('Nametag minted:', result.nametagData?.name);
1005
+ }
1006
+ ```
1007
+
1008
+ ### Common Pitfall: Nametag Already Taken
1009
+
1010
+ If you see this error:
1011
+ ```
1012
+ Failed to register nametag. It may already be taken.
1013
+ [NostrTransportProvider] Nametag already taken: myname - owner: f124f93ae6946ffd...
1014
+ ```
1015
+
1016
+ This means the nametag is registered to a **different public key**. Common causes:
1017
+
1018
+ 1. **Storage cleared or not persisting**:
1019
+ - `Sphere.exists()` returns `false` because storage is empty/inaccessible
1020
+ - SDK creates a new wallet with new keypair
1021
+ - Nametag registration fails because old pubkey owns it on Nostr
1022
+
1023
+ 2. **Different mnemonic provided**:
1024
+ ```typescript
1025
+ // ❌ WRONG: Random mnemonic each time
1026
+ const mnemonic = Sphere.generateMnemonic();
1027
+ const { sphere } = await Sphere.init({
1028
+ mnemonic,
1029
+ nametag: 'myservice', // Fails after first run
1030
+ });
1031
+ ```
1032
+
1033
+ **Note:** `autoGenerate: true` does NOT generate a new mnemonic on every restart. It only generates one if `Sphere.exists()` returns `false` (wallet not found in storage).
1034
+
1035
+ ### Solution: Persistent Storage or Fixed Mnemonic
1036
+
1037
+ **Option 1: Persistent file storage** (recommended for backend):
1038
+
1039
+ ```typescript
1040
+ import { FileStorageProvider } from '@unicitylabs/sphere-sdk/impl/nodejs';
1041
+
1042
+ const storage = new FileStorageProvider('./wallet-data'); // Persists to disk
1043
+
1044
+ const { sphere } = await Sphere.init({
1045
+ storage,
1046
+ autoGenerate: true, // OK: mnemonic saved to disk, reused on restart
1047
+ nametag: 'myservice',
1048
+ });
1049
+ ```
1050
+
1051
+ **Option 2: Fixed mnemonic from environment**:
1052
+
1053
+ ```typescript
1054
+ const { sphere } = await Sphere.init({
1055
+ ...providers,
1056
+ mnemonic: process.env.WALLET_MNEMONIC, // Same mnemonic every time
1057
+ nametag: 'myservice',
1058
+ });
1059
+ ```
1060
+
1061
+ ### Debugging Storage Issues
1062
+
1063
+ If nametag fails unexpectedly, check if wallet exists:
1064
+
1065
+ ```typescript
1066
+ const exists = await Sphere.exists(storage);
1067
+ console.log('Wallet exists:', exists); // Should be true after first run
1068
+
1069
+ // If false - storage is not persisting properly
1070
+ ```
1071
+
1072
+ ### Multi-Address Nametags
1073
+
1074
+ Each derived address can have its own independent nametag:
1075
+
1076
+ ```typescript
1077
+ // Address 0: @alice
1078
+ await sphere.registerNametag('alice');
1079
+
1080
+ // Switch to address 1 and register different nametag
1081
+ await sphere.switchToAddress(1);
1082
+ await sphere.registerNametag('bob');
1083
+
1084
+ // Now:
1085
+ // - Address 0 → @alice
1086
+ // - Address 1 → @bob
1087
+
1088
+ // Get nametag for specific address
1089
+ const aliceTag = sphere.getNametagForAddress(0); // 'alice'
1090
+ const bobTag = sphere.getNametagForAddress(1); // 'bob'
1091
+ ```
1092
+
1093
+ ---
1094
+
1095
+ ## Known Limitations / TODO
1096
+
1097
+ ### Wallet Encryption
1098
+
1099
+ Currently, wallet mnemonics are encrypted using a default key (`DEFAULT_ENCRYPTION_KEY` in constants.ts). This provides basic protection but is not secure for production use.
1100
+
1101
+ **Future implementation needed:**
1102
+ - Add user password parameter to `Sphere.create()`, `Sphere.load()`, and `Sphere.init()`
1103
+ - Derive encryption key from user password using PBKDF2/Argon2
1104
+ - Migration strategy for existing wallets:
1105
+ 1. Try decrypting with user-provided password first
1106
+ 2. If decryption fails, fallback to `DEFAULT_ENCRYPTION_KEY`
1107
+ 3. If fallback succeeds, re-encrypt with new user password
1108
+ 4. This ensures backwards compatibility with wallets created before password support
1109
+
1110
+ ## License
1111
+
1112
+ MIT