@unicitylabs/sphere-sdk 0.15.0-dev.1 → 0.16.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +15 -5
  2. package/dist/connect/index.cjs +23 -25
  3. package/dist/connect/index.cjs.map +1 -1
  4. package/dist/connect/index.d.cts +4 -0
  5. package/dist/connect/index.d.ts +4 -0
  6. package/dist/connect/index.js +23 -25
  7. package/dist/connect/index.js.map +1 -1
  8. package/dist/core/index.cjs +726 -225
  9. package/dist/core/index.cjs.map +1 -1
  10. package/dist/core/index.d.cts +172 -21
  11. package/dist/core/index.d.ts +172 -21
  12. package/dist/core/index.js +726 -224
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/impl/browser/connect/index.cjs +23 -25
  15. package/dist/impl/browser/connect/index.cjs.map +1 -1
  16. package/dist/impl/browser/connect/index.js +23 -25
  17. package/dist/impl/browser/connect/index.js.map +1 -1
  18. package/dist/impl/browser/index.cjs +331 -504
  19. package/dist/impl/browser/index.cjs.map +1 -1
  20. package/dist/impl/browser/index.js +331 -504
  21. package/dist/impl/browser/index.js.map +1 -1
  22. package/dist/impl/nodejs/connect/index.cjs +22 -24
  23. package/dist/impl/nodejs/connect/index.cjs.map +1 -1
  24. package/dist/impl/nodejs/connect/index.js +22 -24
  25. package/dist/impl/nodejs/connect/index.js.map +1 -1
  26. package/dist/impl/nodejs/index.cjs +298 -569
  27. package/dist/impl/nodejs/index.cjs.map +1 -1
  28. package/dist/impl/nodejs/index.d.cts +90 -13
  29. package/dist/impl/nodejs/index.d.ts +90 -13
  30. package/dist/impl/nodejs/index.js +298 -569
  31. package/dist/impl/nodejs/index.js.map +1 -1
  32. package/dist/impl/shared/wallet-api/index.d.cts +54 -2
  33. package/dist/impl/shared/wallet-api/index.d.ts +54 -2
  34. package/dist/index.cjs +728 -225
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.cts +227 -108
  37. package/dist/index.d.ts +227 -108
  38. package/dist/index.js +728 -224
  39. package/dist/index.js.map +1 -1
  40. package/dist/modules/payments-v2/index.cjs +11 -7
  41. package/dist/modules/payments-v2/index.cjs.map +1 -1
  42. package/dist/modules/payments-v2/index.d.cts +56 -3
  43. package/dist/modules/payments-v2/index.d.ts +56 -3
  44. package/dist/modules/payments-v2/index.js +11 -7
  45. package/dist/modules/payments-v2/index.js.map +1 -1
  46. package/dist/token-engine/index.cjs +46 -0
  47. package/dist/token-engine/index.cjs.map +1 -1
  48. package/dist/token-engine/index.js +46 -0
  49. package/dist/token-engine/index.js.map +1 -1
  50. package/package.json +2 -1
@@ -73,6 +73,25 @@ interface TrackedAddressEntry {
73
73
  * All operations are async for platform flexibility
74
74
  */
75
75
  interface StorageProvider extends BaseProvider {
76
+ /**
77
+ * Stable identity of the BACKING STORE this provider addresses — not of this
78
+ * object, and not of the class (`id` is a class constant like `'file-storage'`,
79
+ * which is exactly the wrong granularity).
80
+ *
81
+ * Two providers that return the SAME value address the same data, so erasing
82
+ * through one erases through the other: `Sphere.clear({ storage })` tears down
83
+ * the live Spheres of every provider sharing this value, not merely those built
84
+ * on this object. Compose it from everything that selects the store (file path,
85
+ * database name, key prefix) behind a scheme prefix, so two kinds of store can
86
+ * never collide on one string.
87
+ *
88
+ * It must not change over the provider's lifetime — it is read again on teardown,
89
+ * and a value that moved would strand the entry it was registered under.
90
+ *
91
+ * Optional: omit it and liveness falls back to per-object identity, i.e. a
92
+ * second provider over the same data is treated as unrelated.
93
+ */
94
+ readonly backingStoreId?: string;
76
95
  /**
77
96
  * Set identity for scoped storage
78
97
  */
@@ -102,11 +121,44 @@ interface StorageProvider extends BaseProvider {
102
121
  */
103
122
  clear(prefix?: string): Promise<void>;
104
123
  /**
105
- * Save tracked addresses (only user state: index, hidden, timestamps)
124
+ * Save tracked addresses (only user state: index, hidden, timestamps).
125
+ *
126
+ * MUST MERGE, NEVER REPLACE (#766 item 5). `entries` is ONE writer's snapshot,
127
+ * not the whole truth: every Sphere sharing this storage keeps its own copy of
128
+ * the registry and persists all of it, so writing the argument verbatim is a
129
+ * lost update — A activates index 1, B (whose snapshot predates that) activates
130
+ * index 2, and B's write erases index 1 while A still reports it. This happens
131
+ * on a single network with a single provider; do NOT "fix" it by renaming or
132
+ * network-scoping the key.
133
+ *
134
+ * The contract, implemented by `storage/tracked-addresses.ts` — reuse those
135
+ * helpers rather than re-deriving this:
136
+ * - read the stored registry, union it with `entries` BY `index`;
137
+ * - on a conflicting index, the entry with the greater `updatedAt` supplies
138
+ * `hidden`, and `createdAt` keeps the earlier value;
139
+ * - serialize concurrent calls on the provider instance, so one call's read
140
+ * cannot interleave with another's write;
141
+ * - a failed write must not brick later writes, and must still reject to its
142
+ * own caller.
143
+ *
144
+ * An `index` must be a UINT32 — a BIP32 child number. `deriveKeyAtPath` parseInt()s
145
+ * that path segment, so `1.5` derives index 1's keys and the row aliases a real
146
+ * address. The ceiling matters too: `deriveChildKey` pads the child number to 8 hex
147
+ * digits, so anything above `0xffffffff` emits extra bytes and derives off-standard.
148
+ * An `entries` row that is not one must REJECT the whole call (`mergeTrackedAddresses`
149
+ * throws `VALIDATION_ERROR`); dropping it silently on a write reports a save that
150
+ * never happened. Already-stored rows are dropped on READ instead, so one bad row
151
+ * cannot brick every later write. Validate before opening the write transaction if
152
+ * your platform would otherwise replace the reason with a generic abort.
153
+ *
154
+ * A union is safe because there is no delete path: entries are only ever added,
155
+ * and wiping the wallet removes the key itself (`Sphere.clear()`). Adding a
156
+ * per-entry delete would require revisiting this contract.
106
157
  */
107
158
  saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
108
159
  /**
109
- * Load tracked addresses
160
+ * Load tracked addresses. Tolerant: unusable/corrupt storage reads as `[]`
161
+ * (see `parseTrackedAddresses` in `storage/tracked-addresses.ts`).
110
162
  */
111
163
  loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
112
164
  }
@@ -115,10 +167,11 @@ interface StorageProvider extends BaseProvider {
115
167
  declare const NETWORKS: {
116
168
  readonly mainnet: {
117
169
  readonly name: "Mainnet";
118
- readonly aggregatorUrl: "https://aggregator.unicity.network/rpc";
119
- readonly nostrRelays: readonly ["wss://relay.unicity.network", "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
170
+ readonly networkId: 1;
171
+ readonly aggregatorUrl: "https://gateway.mainnet.unicity.network";
172
+ readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
120
173
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
121
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
174
+ readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json";
122
175
  };
123
176
  readonly testnet: {
124
177
  readonly name: "Testnet2";
@@ -136,13 +189,6 @@ declare const NETWORKS: {
136
189
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
137
190
  readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json";
138
191
  };
139
- readonly dev: {
140
- readonly name: "Development";
141
- readonly aggregatorUrl: "https://dev-aggregator.dyndns.org/rpc";
142
- readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
143
- readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
144
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
145
- };
146
192
  };
147
193
  type NetworkType = keyof typeof NETWORKS;
148
194
 
@@ -166,6 +212,8 @@ declare class FileStorageProvider implements StorageProvider {
166
212
  readonly id = "file-storage";
167
213
  readonly name = "File Storage";
168
214
  readonly type: "local";
215
+ /** The resolved wallet file — two providers over one path share erasure (#766). */
216
+ readonly backingStoreId: string;
169
217
  private dataDir;
170
218
  private filePath;
171
219
  private isTxtMode;
@@ -186,6 +234,26 @@ declare class FileStorageProvider implements StorageProvider {
186
234
  has(key: string): Promise<boolean>;
187
235
  keys(prefix?: string): Promise<string[]>;
188
236
  clear(prefix?: string): Promise<void>;
237
+ /** Serializes the read-merge-write below, per provider instance. */
238
+ private trackedWrites;
239
+ /**
240
+ * Persist the tracked-address registry by MERGING, never replacing.
241
+ *
242
+ * Every Sphere over this storage holds its own snapshot and writes it in
243
+ * full, so a wholesale write drops the addresses this writer never saw
244
+ * (#766 item 5 — a lost update, reproducible on one network). Concurrent
245
+ * calls are serialized on `trackedWrites` so a read can never interleave
246
+ * with another call's write.
247
+ *
248
+ * Deliberately PER OBJECT, unlike the browser providers. Two objects over one
249
+ * `dataDir` share a `backingStoreId` but not this cache, and `save()` rewrites the
250
+ * WHOLE file from it — so a sibling's *unrelated* `set()` rolls the registry back
251
+ * regardless of how this one write is serialized. Sharing the chain here would make
252
+ * the cross-object contract case pass while leaving the provider unsafe. The real
253
+ * fix is #771 (refresh from disk under a per-file lock, on every write); until then
254
+ * `backingStoreId` scopes TEARDOWN only. Reviewers keep re-finding this — see the
255
+ * `unsupported:` note in tests/unit/storage/tracked-addresses-providers.test.ts.
256
+ */
189
257
  saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
190
258
  loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
191
259
  /**
@@ -584,6 +652,15 @@ declare class NostrTransportProvider implements TransportProvider {
584
652
  suppressSubscriptions(): void;
585
653
  private _subscriptionsSuppressed;
586
654
  connect(): Promise<void>;
655
+ /**
656
+ * Race one client's relay connect against the configured timeout.
657
+ *
658
+ * The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
659
+ * cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
660
+ * for the whole `config.timeout` after the call has already returned (and
661
+ * then rejects a promise nobody is listening to any more).
662
+ */
663
+ private connectWithDeadline;
587
664
  disconnect(): Promise<void>;
588
665
  isConnected(): boolean;
589
666
  getStatus(): ProviderStatus;
@@ -1545,7 +1622,7 @@ type NodeTransportConfig = BaseTransportConfig;
1545
1622
  */
1546
1623
  type NodeOracleConfig = BaseOracleConfig & NodeOracleExtensions;
1547
1624
  interface NodeProvidersConfig {
1548
- /** Network preset: mainnet, testnet, or dev */
1625
+ /** Network preset: mainnet, testnet or testnet2 */
1549
1626
  network?: NetworkType;
1550
1627
  /** Enable debug logging globally for all providers (default: false). Per-provider debug flags override this. */
1551
1628
  debug?: boolean;
@@ -73,6 +73,25 @@ interface TrackedAddressEntry {
73
73
  * All operations are async for platform flexibility
74
74
  */
75
75
  interface StorageProvider extends BaseProvider {
76
+ /**
77
+ * Stable identity of the BACKING STORE this provider addresses — not of this
78
+ * object, and not of the class (`id` is a class constant like `'file-storage'`,
79
+ * which is exactly the wrong granularity).
80
+ *
81
+ * Two providers that return the SAME value address the same data, so erasing
82
+ * through one erases through the other: `Sphere.clear({ storage })` tears down
83
+ * the live Spheres of every provider sharing this value, not merely those built
84
+ * on this object. Compose it from everything that selects the store (file path,
85
+ * database name, key prefix) behind a scheme prefix, so two kinds of store can
86
+ * never collide on one string.
87
+ *
88
+ * It must not change over the provider's lifetime — it is read again on teardown,
89
+ * and a value that moved would strand the entry it was registered under.
90
+ *
91
+ * Optional: omit it and liveness falls back to per-object identity, i.e. a
92
+ * second provider over the same data is treated as unrelated.
93
+ */
94
+ readonly backingStoreId?: string;
76
95
  /**
77
96
  * Set identity for scoped storage
78
97
  */
@@ -102,11 +121,44 @@ interface StorageProvider extends BaseProvider {
102
121
  */
103
122
  clear(prefix?: string): Promise<void>;
104
123
  /**
105
- * Save tracked addresses (only user state: index, hidden, timestamps)
124
+ * Save tracked addresses (only user state: index, hidden, timestamps).
125
+ *
126
+ * MUST MERGE, NEVER REPLACE (#766 item 5). `entries` is ONE writer's snapshot,
127
+ * not the whole truth: every Sphere sharing this storage keeps its own copy of
128
+ * the registry and persists all of it, so writing the argument verbatim is a
129
+ * lost update — A activates index 1, B (whose snapshot predates that) activates
130
+ * index 2, and B's write erases index 1 while A still reports it. This happens
131
+ * on a single network with a single provider; do NOT "fix" it by renaming or
132
+ * network-scoping the key.
133
+ *
134
+ * The contract, implemented by `storage/tracked-addresses.ts` — reuse those
135
+ * helpers rather than re-deriving this:
136
+ * - read the stored registry, union it with `entries` BY `index`;
137
+ * - on a conflicting index, the entry with the greater `updatedAt` supplies
138
+ * `hidden`, and `createdAt` keeps the earlier value;
139
+ * - serialize concurrent calls on the provider instance, so one call's read
140
+ * cannot interleave with another's write;
141
+ * - a failed write must not brick later writes, and must still reject to its
142
+ * own caller.
143
+ *
144
+ * An `index` must be a UINT32 — a BIP32 child number. `deriveKeyAtPath` parseInt()s
145
+ * that path segment, so `1.5` derives index 1's keys and the row aliases a real
146
+ * address. The ceiling matters too: `deriveChildKey` pads the child number to 8 hex
147
+ * digits, so anything above `0xffffffff` emits extra bytes and derives off-standard.
148
+ * An `entries` row that is not one must REJECT the whole call (`mergeTrackedAddresses`
149
+ * throws `VALIDATION_ERROR`); dropping it silently on a write reports a save that
150
+ * never happened. Already-stored rows are dropped on READ instead, so one bad row
151
+ * cannot brick every later write. Validate before opening the write transaction if
152
+ * your platform would otherwise replace the reason with a generic abort.
153
+ *
154
+ * A union is safe because there is no delete path: entries are only ever added,
155
+ * and wiping the wallet removes the key itself (`Sphere.clear()`). Adding a
156
+ * per-entry delete would require revisiting this contract.
106
157
  */
107
158
  saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
108
159
  /**
109
- * Load tracked addresses
160
+ * Load tracked addresses. Tolerant: unusable/corrupt storage reads as `[]`
161
+ * (see `parseTrackedAddresses` in `storage/tracked-addresses.ts`).
110
162
  */
111
163
  loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
112
164
  }
@@ -115,10 +167,11 @@ interface StorageProvider extends BaseProvider {
115
167
  declare const NETWORKS: {
116
168
  readonly mainnet: {
117
169
  readonly name: "Mainnet";
118
- readonly aggregatorUrl: "https://aggregator.unicity.network/rpc";
119
- readonly nostrRelays: readonly ["wss://relay.unicity.network", "wss://relay.damus.io", "wss://nos.lol", "wss://relay.nostr.band"];
170
+ readonly networkId: 1;
171
+ readonly aggregatorUrl: "https://gateway.mainnet.unicity.network";
172
+ readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
120
173
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
121
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
174
+ readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.mainnet.json";
122
175
  };
123
176
  readonly testnet: {
124
177
  readonly name: "Testnet2";
@@ -136,13 +189,6 @@ declare const NETWORKS: {
136
189
  readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
137
190
  readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet2.json";
138
191
  };
139
- readonly dev: {
140
- readonly name: "Development";
141
- readonly aggregatorUrl: "https://dev-aggregator.dyndns.org/rpc";
142
- readonly nostrRelays: readonly ["wss://nostr-relay.testnet.unicity.network"];
143
- readonly groupRelays: readonly ["wss://sphere-relay.unicity.network"];
144
- readonly tokenRegistryUrl: "https://raw.githubusercontent.com/unicitynetwork/unicity-ids/refs/heads/main/unicity-ids.testnet.json";
145
- };
146
192
  };
147
193
  type NetworkType = keyof typeof NETWORKS;
148
194
 
@@ -166,6 +212,8 @@ declare class FileStorageProvider implements StorageProvider {
166
212
  readonly id = "file-storage";
167
213
  readonly name = "File Storage";
168
214
  readonly type: "local";
215
+ /** The resolved wallet file — two providers over one path share erasure (#766). */
216
+ readonly backingStoreId: string;
169
217
  private dataDir;
170
218
  private filePath;
171
219
  private isTxtMode;
@@ -186,6 +234,26 @@ declare class FileStorageProvider implements StorageProvider {
186
234
  has(key: string): Promise<boolean>;
187
235
  keys(prefix?: string): Promise<string[]>;
188
236
  clear(prefix?: string): Promise<void>;
237
+ /** Serializes the read-merge-write below, per provider instance. */
238
+ private trackedWrites;
239
+ /**
240
+ * Persist the tracked-address registry by MERGING, never replacing.
241
+ *
242
+ * Every Sphere over this storage holds its own snapshot and writes it in
243
+ * full, so a wholesale write drops the addresses this writer never saw
244
+ * (#766 item 5 — a lost update, reproducible on one network). Concurrent
245
+ * calls are serialized on `trackedWrites` so a read can never interleave
246
+ * with another call's write.
247
+ *
248
+ * Deliberately PER OBJECT, unlike the browser providers. Two objects over one
249
+ * `dataDir` share a `backingStoreId` but not this cache, and `save()` rewrites the
250
+ * WHOLE file from it — so a sibling's *unrelated* `set()` rolls the registry back
251
+ * regardless of how this one write is serialized. Sharing the chain here would make
252
+ * the cross-object contract case pass while leaving the provider unsafe. The real
253
+ * fix is #771 (refresh from disk under a per-file lock, on every write); until then
254
+ * `backingStoreId` scopes TEARDOWN only. Reviewers keep re-finding this — see the
255
+ * `unsupported:` note in tests/unit/storage/tracked-addresses-providers.test.ts.
256
+ */
189
257
  saveTrackedAddresses(entries: TrackedAddressEntry[]): Promise<void>;
190
258
  loadTrackedAddresses(): Promise<TrackedAddressEntry[]>;
191
259
  /**
@@ -584,6 +652,15 @@ declare class NostrTransportProvider implements TransportProvider {
584
652
  suppressSubscriptions(): void;
585
653
  private _subscriptionsSuppressed;
586
654
  connect(): Promise<void>;
655
+ /**
656
+ * Race one client's relay connect against the configured timeout.
657
+ *
658
+ * The timer is cleared in a `finally` on EVERY path. `Promise.race` does not
659
+ * cancel the loser: an un-cleared `setTimeout` keeps Node's event loop pinned
660
+ * for the whole `config.timeout` after the call has already returned (and
661
+ * then rejects a promise nobody is listening to any more).
662
+ */
663
+ private connectWithDeadline;
587
664
  disconnect(): Promise<void>;
588
665
  isConnected(): boolean;
589
666
  getStatus(): ProviderStatus;
@@ -1545,7 +1622,7 @@ type NodeTransportConfig = BaseTransportConfig;
1545
1622
  */
1546
1623
  type NodeOracleConfig = BaseOracleConfig & NodeOracleExtensions;
1547
1624
  interface NodeProvidersConfig {
1548
- /** Network preset: mainnet, testnet, or dev */
1625
+ /** Network preset: mainnet, testnet or testnet2 */
1549
1626
  network?: NetworkType;
1550
1627
  /** Enable debug logging globally for all providers (default: false). Per-provider debug flags override this. */
1551
1628
  debug?: boolean;