@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/LICENSE +21 -0
- package/README.md +1112 -0
- package/dist/core/index.cjs +7042 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +2548 -0
- package/dist/core/index.d.ts +2548 -0
- package/dist/core/index.js +6946 -0
- package/dist/core/index.js.map +1 -0
- package/dist/impl/browser/index.cjs +7853 -0
- package/dist/impl/browser/index.cjs.map +1 -0
- package/dist/impl/browser/index.d.cts +3016 -0
- package/dist/impl/browser/index.d.ts +3016 -0
- package/dist/impl/browser/index.js +7841 -0
- package/dist/impl/browser/index.js.map +1 -0
- package/dist/impl/nodejs/index.cjs +1767 -0
- package/dist/impl/nodejs/index.cjs.map +1 -0
- package/dist/impl/nodejs/index.d.cts +1091 -0
- package/dist/impl/nodejs/index.d.ts +1091 -0
- package/dist/impl/nodejs/index.js +1722 -0
- package/dist/impl/nodejs/index.js.map +1 -0
- package/dist/index.cjs +7647 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10533 -0
- package/dist/index.d.ts +10533 -0
- package/dist/index.js +7523 -0
- package/dist/index.js.map +1 -0
- package/dist/l1/index.cjs +1545 -0
- package/dist/l1/index.cjs.map +1 -0
- package/dist/l1/index.d.cts +705 -0
- package/dist/l1/index.d.ts +705 -0
- package/dist/l1/index.js +1455 -0
- package/dist/l1/index.js.map +1 -0
- package/package.json +140 -0
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
|