@unicitylabs/sphere-sdk 0.1.9 → 0.2.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.
@@ -22,8 +22,10 @@ var STORAGE_KEYS_GLOBAL = {
22
22
  WALLET_EXISTS: "wallet_exists",
23
23
  /** Current active address index */
24
24
  CURRENT_ADDRESS_INDEX: "current_address_index",
25
- /** Index of address nametags (JSON: { "0": "alice", "1": "bob" }) - for discovery */
26
- ADDRESS_NAMETAGS: "address_nametags"
25
+ /** Nametag cache per address (separate from tracked addresses registry) */
26
+ ADDRESS_NAMETAGS: "address_nametags",
27
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
28
+ TRACKED_ADDRESSES: "tracked_addresses"
27
29
  };
28
30
  var STORAGE_KEYS_ADDRESS = {
29
31
  /** Pending transfers for this address */
@@ -213,6 +215,19 @@ var FileStorageProvider = class {
213
215
  }
214
216
  await this.save();
215
217
  }
218
+ async saveTrackedAddresses(entries) {
219
+ await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify({ version: 1, addresses: entries }));
220
+ }
221
+ async loadTrackedAddresses() {
222
+ const data = await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
223
+ if (!data) return [];
224
+ try {
225
+ const parsed = JSON.parse(data);
226
+ return parsed.addresses ?? [];
227
+ } catch {
228
+ return [];
229
+ }
230
+ }
216
231
  /**
217
232
  * Get full storage key with address prefix for per-address keys
218
233
  */
@@ -296,7 +311,12 @@ var FileTokenStorageProvider = class {
296
311
  }
297
312
  };
298
313
  try {
299
- const files = fs2.readdirSync(this.tokensDir).filter((f) => f.endsWith(".json") && f !== "_meta.json");
314
+ const files = fs2.readdirSync(this.tokensDir).filter(
315
+ (f) => f.endsWith(".json") && f !== "_meta.json" && f !== "_tombstones.json" && !f.startsWith("archived_") && // Skip archived tokens
316
+ !f.startsWith("token-") && // Skip legacy token format
317
+ !f.startsWith("nametag-")
318
+ // Skip nametag files (not tokens)
319
+ );
300
320
  for (const file of files) {
301
321
  try {
302
322
  const basename2 = path2.basename(file, ".json");
@@ -314,6 +334,14 @@ var FileTokenStorageProvider = class {
314
334
  } catch {
315
335
  }
316
336
  }
337
+ const tombstonesPath = path2.join(this.tokensDir, "_tombstones.json");
338
+ if (fs2.existsSync(tombstonesPath)) {
339
+ try {
340
+ const content = fs2.readFileSync(tombstonesPath, "utf-8");
341
+ data._tombstones = JSON.parse(content);
342
+ } catch {
343
+ }
344
+ }
317
345
  return {
318
346
  success: true,
319
347
  data,
@@ -358,6 +386,10 @@ var FileTokenStorageProvider = class {
358
386
  fs2.unlinkSync(filePath);
359
387
  }
360
388
  }
389
+ fs2.writeFileSync(
390
+ path2.join(this.tokensDir, "_tombstones.json"),
391
+ JSON.stringify(data._tombstones, null, 2)
392
+ );
361
393
  }
362
394
  return {
363
395
  success: true,
@@ -1006,6 +1038,10 @@ function defaultUUIDGenerator() {
1006
1038
 
1007
1039
  // transport/NostrTransportProvider.ts
1008
1040
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
1041
+ function hashAddressForTag(address) {
1042
+ const bytes = new TextEncoder().encode("unicity:address:" + address);
1043
+ return Buffer2.from(sha256(bytes)).toString("hex");
1044
+ }
1009
1045
  function deriveNametagEncryptionKey(privateKeyHex) {
1010
1046
  const privateKeyBytes = Buffer2.from(privateKeyHex, "hex");
1011
1047
  const saltInput = new TextEncoder().encode("sphere-nametag-salt");
@@ -1259,7 +1295,7 @@ var NostrTransportProvider = class {
1259
1295
  // ===========================================================================
1260
1296
  // TransportProvider Implementation
1261
1297
  // ===========================================================================
1262
- setIdentity(identity) {
1298
+ async setIdentity(identity) {
1263
1299
  this.identity = identity;
1264
1300
  const secretKey = Buffer2.from(identity.privateKey, "hex");
1265
1301
  this.keyManager = NostrKeyManager.fromPrivateKey(secretKey);
@@ -1289,12 +1325,9 @@ var NostrTransportProvider = class {
1289
1325
  this.log("NostrClient reconnected to relay:", url);
1290
1326
  }
1291
1327
  });
1292
- this.nostrClient.connect(...this.config.relays).then(() => {
1293
- this.subscribeToEvents();
1294
- oldClient.disconnect();
1295
- }).catch((err) => {
1296
- this.log("Failed to reconnect with new identity:", err);
1297
- });
1328
+ await this.nostrClient.connect(...this.config.relays);
1329
+ this.subscribeToEvents();
1330
+ oldClient.disconnect();
1298
1331
  } else if (this.isConnected()) {
1299
1332
  this.subscribeToEvents();
1300
1333
  }
@@ -1414,6 +1447,28 @@ var NostrTransportProvider = class {
1414
1447
  this.paymentRequestResponseHandlers.add(handler);
1415
1448
  return () => this.paymentRequestResponseHandlers.delete(handler);
1416
1449
  }
1450
+ /**
1451
+ * Resolve any identifier to full peer information.
1452
+ * Routes to the appropriate specific resolve method based on identifier format.
1453
+ */
1454
+ async resolve(identifier) {
1455
+ if (identifier.startsWith("@")) {
1456
+ return this.resolveNametagInfo(identifier.slice(1));
1457
+ }
1458
+ if (identifier.startsWith("DIRECT:") || identifier.startsWith("PROXY:")) {
1459
+ return this.resolveAddressInfo(identifier);
1460
+ }
1461
+ if (identifier.startsWith("alpha1") || identifier.startsWith("alphat1")) {
1462
+ return this.resolveAddressInfo(identifier);
1463
+ }
1464
+ if (/^0[23][0-9a-f]{64}$/i.test(identifier)) {
1465
+ return this.resolveAddressInfo(identifier);
1466
+ }
1467
+ if (/^[0-9a-f]{64}$/i.test(identifier)) {
1468
+ return this.resolveTransportPubkeyInfo(identifier);
1469
+ }
1470
+ return this.resolveNametagInfo(identifier);
1471
+ }
1417
1472
  async resolveNametag(nametag) {
1418
1473
  this.ensureReady();
1419
1474
  const hashedNametag = hashNametag(nametag);
@@ -1510,6 +1565,77 @@ var NostrTransportProvider = class {
1510
1565
  };
1511
1566
  }
1512
1567
  }
1568
+ /**
1569
+ * Resolve a DIRECT://, PROXY://, or L1 address to full peer info.
1570
+ * Performs reverse lookup: hash(address) → query '#t' tag → parse binding event.
1571
+ * Works with both new identity binding events and legacy nametag binding events.
1572
+ */
1573
+ async resolveAddressInfo(address) {
1574
+ this.ensureReady();
1575
+ const addressHash = hashAddressForTag(address);
1576
+ const events = await this.queryEvents({
1577
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1578
+ "#t": [addressHash],
1579
+ limit: 1
1580
+ });
1581
+ if (events.length === 0) return null;
1582
+ const bindingEvent = events[0];
1583
+ try {
1584
+ const content = JSON.parse(bindingEvent.content);
1585
+ return {
1586
+ nametag: content.nametag || void 0,
1587
+ transportPubkey: bindingEvent.pubkey,
1588
+ chainPubkey: content.public_key || "",
1589
+ l1Address: content.l1_address || "",
1590
+ directAddress: content.direct_address || "",
1591
+ proxyAddress: content.proxy_address || void 0,
1592
+ timestamp: bindingEvent.created_at * 1e3
1593
+ };
1594
+ } catch {
1595
+ return {
1596
+ transportPubkey: bindingEvent.pubkey,
1597
+ chainPubkey: "",
1598
+ l1Address: "",
1599
+ directAddress: "",
1600
+ timestamp: bindingEvent.created_at * 1e3
1601
+ };
1602
+ }
1603
+ }
1604
+ /**
1605
+ * Resolve transport pubkey (Nostr pubkey) to full peer info.
1606
+ * Queries binding events authored by the given pubkey.
1607
+ */
1608
+ async resolveTransportPubkeyInfo(transportPubkey) {
1609
+ this.ensureReady();
1610
+ const events = await this.queryEvents({
1611
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1612
+ authors: [transportPubkey],
1613
+ limit: 5
1614
+ });
1615
+ if (events.length === 0) return null;
1616
+ events.sort((a, b) => b.created_at - a.created_at);
1617
+ const bindingEvent = events[0];
1618
+ try {
1619
+ const content = JSON.parse(bindingEvent.content);
1620
+ return {
1621
+ nametag: content.nametag || void 0,
1622
+ transportPubkey: bindingEvent.pubkey,
1623
+ chainPubkey: content.public_key || "",
1624
+ l1Address: content.l1_address || "",
1625
+ directAddress: content.direct_address || "",
1626
+ proxyAddress: content.proxy_address || void 0,
1627
+ timestamp: bindingEvent.created_at * 1e3
1628
+ };
1629
+ } catch {
1630
+ return {
1631
+ transportPubkey: bindingEvent.pubkey,
1632
+ chainPubkey: "",
1633
+ l1Address: "",
1634
+ directAddress: "",
1635
+ timestamp: bindingEvent.created_at * 1e3
1636
+ };
1637
+ }
1638
+ }
1513
1639
  /**
1514
1640
  * Recover nametag for the current identity by searching for encrypted nametag events
1515
1641
  * Used after wallet import to recover associated nametag
@@ -1553,6 +1679,63 @@ var NostrTransportProvider = class {
1553
1679
  this.log("Could not decrypt nametag from any event");
1554
1680
  return null;
1555
1681
  }
1682
+ /**
1683
+ * Publish identity binding event on Nostr.
1684
+ * Without nametag: publishes base binding (chainPubkey, l1Address, directAddress).
1685
+ * With nametag: also publishes nametag hash, proxy address, encrypted nametag for recovery.
1686
+ *
1687
+ * Uses kind 30078 parameterized replaceable event with d=SHA256('unicity:identity:' + nostrPubkey).
1688
+ * Each HD address index has its own Nostr key → its own binding event.
1689
+ *
1690
+ * @returns true if successful, false if nametag is taken by another pubkey
1691
+ */
1692
+ async publishIdentityBinding(chainPubkey, l1Address, directAddress, nametag) {
1693
+ this.ensureReady();
1694
+ if (!this.identity) {
1695
+ throw new Error("Identity not set");
1696
+ }
1697
+ const nostrPubkey = this.getNostrPubkey();
1698
+ const dTagBytes = new TextEncoder().encode("unicity:identity:" + nostrPubkey);
1699
+ const dTag = Buffer2.from(sha256(dTagBytes)).toString("hex");
1700
+ const contentObj = {
1701
+ public_key: chainPubkey,
1702
+ l1_address: l1Address,
1703
+ direct_address: directAddress
1704
+ };
1705
+ const tags = [
1706
+ ["d", dTag],
1707
+ ["t", hashAddressForTag(chainPubkey)],
1708
+ ["t", hashAddressForTag(directAddress)],
1709
+ ["t", hashAddressForTag(l1Address)]
1710
+ ];
1711
+ if (nametag) {
1712
+ const existing = await this.resolveNametag(nametag);
1713
+ if (existing && existing !== nostrPubkey) {
1714
+ this.log("Nametag already taken:", nametag, "- owner:", existing);
1715
+ return false;
1716
+ }
1717
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1718
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1719
+ const proxyAddress = proxyAddr.toString();
1720
+ const encryptedNametag = await encryptNametag(nametag, this.identity.privateKey);
1721
+ const hashedNametag = hashNametag(nametag);
1722
+ contentObj.nametag = nametag;
1723
+ contentObj.encrypted_nametag = encryptedNametag;
1724
+ contentObj.proxy_address = proxyAddress;
1725
+ tags.push(["t", hashedNametag]);
1726
+ tags.push(["t", hashAddressForTag(proxyAddress)]);
1727
+ }
1728
+ const content = JSON.stringify(contentObj);
1729
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1730
+ await this.publishEvent(event);
1731
+ if (nametag) {
1732
+ this.log("Published identity binding with nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...");
1733
+ } else {
1734
+ this.log("Published identity binding (no nametag) for pubkey:", nostrPubkey.slice(0, 16) + "...");
1735
+ }
1736
+ return true;
1737
+ }
1738
+ /** @deprecated Use publishIdentityBinding instead */
1556
1739
  async publishNametag(nametag, address) {
1557
1740
  this.ensureReady();
1558
1741
  const hashedNametag = hashNametag(nametag);
@@ -1579,6 +1762,9 @@ var NostrTransportProvider = class {
1579
1762
  const compressedPubkey = getPublicKey(privateKeyHex, true);
1580
1763
  const l1Address = publicKeyToAddress(compressedPubkey, "alpha");
1581
1764
  const encryptedNametag = await encryptNametag(nametag, privateKeyHex);
1765
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1766
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1767
+ const proxyAddress = proxyAddr.toString();
1582
1768
  const hashedNametag = hashNametag(nametag);
1583
1769
  const content = JSON.stringify({
1584
1770
  nametag_hash: hashedNametag,
@@ -1588,17 +1774,20 @@ var NostrTransportProvider = class {
1588
1774
  encrypted_nametag: encryptedNametag,
1589
1775
  public_key: compressedPubkey,
1590
1776
  l1_address: l1Address,
1591
- direct_address: directAddress
1777
+ direct_address: directAddress,
1778
+ proxy_address: proxyAddress
1592
1779
  });
1593
- const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, [
1780
+ const tags = [
1594
1781
  ["d", hashedNametag],
1595
1782
  ["nametag", hashedNametag],
1596
1783
  ["t", hashedNametag],
1784
+ ["t", hashAddressForTag(directAddress)],
1785
+ ["t", hashAddressForTag(proxyAddress)],
1597
1786
  ["address", nostrPubkey],
1598
- // Extended tags for indexing
1599
1787
  ["pubkey", compressedPubkey],
1600
1788
  ["l1", l1Address]
1601
- ]);
1789
+ ];
1790
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1602
1791
  await this.publishEvent(event);
1603
1792
  this.log("Registered nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...", "l1:", l1Address.slice(0, 12) + "...");
1604
1793
  return true;
@@ -2550,6 +2739,113 @@ function createUnicityAggregatorProvider(config) {
2550
2739
  }
2551
2740
  var createUnicityOracleProvider = createUnicityAggregatorProvider;
2552
2741
 
2742
+ // price/CoinGeckoPriceProvider.ts
2743
+ var CoinGeckoPriceProvider = class {
2744
+ platform = "coingecko";
2745
+ cache = /* @__PURE__ */ new Map();
2746
+ apiKey;
2747
+ cacheTtlMs;
2748
+ timeout;
2749
+ debug;
2750
+ baseUrl;
2751
+ constructor(config) {
2752
+ this.apiKey = config?.apiKey;
2753
+ this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
2754
+ this.timeout = config?.timeout ?? 1e4;
2755
+ this.debug = config?.debug ?? false;
2756
+ this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
2757
+ }
2758
+ async getPrices(tokenNames) {
2759
+ if (tokenNames.length === 0) {
2760
+ return /* @__PURE__ */ new Map();
2761
+ }
2762
+ const now = Date.now();
2763
+ const result = /* @__PURE__ */ new Map();
2764
+ const uncachedNames = [];
2765
+ for (const name of tokenNames) {
2766
+ const cached = this.cache.get(name);
2767
+ if (cached && cached.expiresAt > now) {
2768
+ if (cached.price !== null) {
2769
+ result.set(name, cached.price);
2770
+ }
2771
+ } else {
2772
+ uncachedNames.push(name);
2773
+ }
2774
+ }
2775
+ if (uncachedNames.length === 0) {
2776
+ return result;
2777
+ }
2778
+ try {
2779
+ const ids = uncachedNames.join(",");
2780
+ const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
2781
+ const headers = { Accept: "application/json" };
2782
+ if (this.apiKey) {
2783
+ headers["x-cg-pro-api-key"] = this.apiKey;
2784
+ }
2785
+ if (this.debug) {
2786
+ console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
2787
+ }
2788
+ const response = await fetch(url, {
2789
+ headers,
2790
+ signal: AbortSignal.timeout(this.timeout)
2791
+ });
2792
+ if (!response.ok) {
2793
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
2794
+ }
2795
+ const data = await response.json();
2796
+ for (const [name, values] of Object.entries(data)) {
2797
+ if (values && typeof values === "object") {
2798
+ const price = {
2799
+ tokenName: name,
2800
+ priceUsd: values.usd ?? 0,
2801
+ priceEur: values.eur,
2802
+ change24h: values.usd_24h_change,
2803
+ timestamp: now
2804
+ };
2805
+ this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
2806
+ result.set(name, price);
2807
+ }
2808
+ }
2809
+ for (const name of uncachedNames) {
2810
+ if (!result.has(name)) {
2811
+ this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
2812
+ }
2813
+ }
2814
+ if (this.debug) {
2815
+ console.log(`[CoinGecko] Fetched ${result.size} prices`);
2816
+ }
2817
+ } catch (error) {
2818
+ if (this.debug) {
2819
+ console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
2820
+ }
2821
+ for (const name of uncachedNames) {
2822
+ const stale = this.cache.get(name);
2823
+ if (stale?.price) {
2824
+ result.set(name, stale.price);
2825
+ }
2826
+ }
2827
+ }
2828
+ return result;
2829
+ }
2830
+ async getPrice(tokenName) {
2831
+ const prices = await this.getPrices([tokenName]);
2832
+ return prices.get(tokenName) ?? null;
2833
+ }
2834
+ clearCache() {
2835
+ this.cache.clear();
2836
+ }
2837
+ };
2838
+
2839
+ // price/index.ts
2840
+ function createPriceProvider(config) {
2841
+ switch (config.platform) {
2842
+ case "coingecko":
2843
+ return new CoinGeckoPriceProvider(config);
2844
+ default:
2845
+ throw new Error(`Unsupported price platform: ${String(config.platform)}`);
2846
+ }
2847
+ }
2848
+
2553
2849
  // impl/shared/resolvers.ts
2554
2850
  function getNetworkConfig(network = "mainnet") {
2555
2851
  return NETWORKS[network];
@@ -2598,6 +2894,19 @@ function resolveL1Config(network, config) {
2598
2894
  enableVesting: config.enableVesting
2599
2895
  };
2600
2896
  }
2897
+ function resolvePriceConfig(config) {
2898
+ if (config === void 0) {
2899
+ return void 0;
2900
+ }
2901
+ return {
2902
+ platform: config.platform ?? "coingecko",
2903
+ apiKey: config.apiKey,
2904
+ baseUrl: config.baseUrl,
2905
+ cacheTtlMs: config.cacheTtlMs,
2906
+ timeout: config.timeout,
2907
+ debug: config.debug
2908
+ };
2909
+ }
2601
2910
 
2602
2911
  // impl/nodejs/index.ts
2603
2912
  function createNodeProviders(config) {
@@ -2605,6 +2914,7 @@ function createNodeProviders(config) {
2605
2914
  const transportConfig = resolveTransportConfig(network, config?.transport);
2606
2915
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
2607
2916
  const l1Config = resolveL1Config(network, config?.l1);
2917
+ const priceConfig = resolvePriceConfig(config?.price);
2608
2918
  return {
2609
2919
  storage: createFileStorageProvider({
2610
2920
  dataDir: config?.dataDir ?? "./sphere-data"
@@ -2627,7 +2937,8 @@ function createNodeProviders(config) {
2627
2937
  debug: oracleConfig.debug,
2628
2938
  network
2629
2939
  }),
2630
- l1: l1Config
2940
+ l1: l1Config,
2941
+ price: priceConfig ? createPriceProvider(priceConfig) : void 0
2631
2942
  };
2632
2943
  }
2633
2944
  export {