@unicitylabs/sphere-sdk 0.1.9 → 0.2.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.
@@ -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");
@@ -1414,6 +1450,28 @@ var NostrTransportProvider = class {
1414
1450
  this.paymentRequestResponseHandlers.add(handler);
1415
1451
  return () => this.paymentRequestResponseHandlers.delete(handler);
1416
1452
  }
1453
+ /**
1454
+ * Resolve any identifier to full peer information.
1455
+ * Routes to the appropriate specific resolve method based on identifier format.
1456
+ */
1457
+ async resolve(identifier) {
1458
+ if (identifier.startsWith("@")) {
1459
+ return this.resolveNametagInfo(identifier.slice(1));
1460
+ }
1461
+ if (identifier.startsWith("DIRECT:") || identifier.startsWith("PROXY:")) {
1462
+ return this.resolveAddressInfo(identifier);
1463
+ }
1464
+ if (identifier.startsWith("alpha1") || identifier.startsWith("alphat1")) {
1465
+ return this.resolveAddressInfo(identifier);
1466
+ }
1467
+ if (/^0[23][0-9a-f]{64}$/i.test(identifier)) {
1468
+ return this.resolveAddressInfo(identifier);
1469
+ }
1470
+ if (/^[0-9a-f]{64}$/i.test(identifier)) {
1471
+ return this.resolveTransportPubkeyInfo(identifier);
1472
+ }
1473
+ return this.resolveNametagInfo(identifier);
1474
+ }
1417
1475
  async resolveNametag(nametag) {
1418
1476
  this.ensureReady();
1419
1477
  const hashedNametag = hashNametag(nametag);
@@ -1510,6 +1568,77 @@ var NostrTransportProvider = class {
1510
1568
  };
1511
1569
  }
1512
1570
  }
1571
+ /**
1572
+ * Resolve a DIRECT://, PROXY://, or L1 address to full peer info.
1573
+ * Performs reverse lookup: hash(address) → query '#t' tag → parse binding event.
1574
+ * Works with both new identity binding events and legacy nametag binding events.
1575
+ */
1576
+ async resolveAddressInfo(address) {
1577
+ this.ensureReady();
1578
+ const addressHash = hashAddressForTag(address);
1579
+ const events = await this.queryEvents({
1580
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1581
+ "#t": [addressHash],
1582
+ limit: 1
1583
+ });
1584
+ if (events.length === 0) return null;
1585
+ const bindingEvent = events[0];
1586
+ try {
1587
+ const content = JSON.parse(bindingEvent.content);
1588
+ return {
1589
+ nametag: content.nametag || void 0,
1590
+ transportPubkey: bindingEvent.pubkey,
1591
+ chainPubkey: content.public_key || "",
1592
+ l1Address: content.l1_address || "",
1593
+ directAddress: content.direct_address || "",
1594
+ proxyAddress: content.proxy_address || void 0,
1595
+ timestamp: bindingEvent.created_at * 1e3
1596
+ };
1597
+ } catch {
1598
+ return {
1599
+ transportPubkey: bindingEvent.pubkey,
1600
+ chainPubkey: "",
1601
+ l1Address: "",
1602
+ directAddress: "",
1603
+ timestamp: bindingEvent.created_at * 1e3
1604
+ };
1605
+ }
1606
+ }
1607
+ /**
1608
+ * Resolve transport pubkey (Nostr pubkey) to full peer info.
1609
+ * Queries binding events authored by the given pubkey.
1610
+ */
1611
+ async resolveTransportPubkeyInfo(transportPubkey) {
1612
+ this.ensureReady();
1613
+ const events = await this.queryEvents({
1614
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1615
+ authors: [transportPubkey],
1616
+ limit: 5
1617
+ });
1618
+ if (events.length === 0) return null;
1619
+ events.sort((a, b) => b.created_at - a.created_at);
1620
+ const bindingEvent = events[0];
1621
+ try {
1622
+ const content = JSON.parse(bindingEvent.content);
1623
+ return {
1624
+ nametag: content.nametag || void 0,
1625
+ transportPubkey: bindingEvent.pubkey,
1626
+ chainPubkey: content.public_key || "",
1627
+ l1Address: content.l1_address || "",
1628
+ directAddress: content.direct_address || "",
1629
+ proxyAddress: content.proxy_address || void 0,
1630
+ timestamp: bindingEvent.created_at * 1e3
1631
+ };
1632
+ } catch {
1633
+ return {
1634
+ transportPubkey: bindingEvent.pubkey,
1635
+ chainPubkey: "",
1636
+ l1Address: "",
1637
+ directAddress: "",
1638
+ timestamp: bindingEvent.created_at * 1e3
1639
+ };
1640
+ }
1641
+ }
1513
1642
  /**
1514
1643
  * Recover nametag for the current identity by searching for encrypted nametag events
1515
1644
  * Used after wallet import to recover associated nametag
@@ -1553,6 +1682,63 @@ var NostrTransportProvider = class {
1553
1682
  this.log("Could not decrypt nametag from any event");
1554
1683
  return null;
1555
1684
  }
1685
+ /**
1686
+ * Publish identity binding event on Nostr.
1687
+ * Without nametag: publishes base binding (chainPubkey, l1Address, directAddress).
1688
+ * With nametag: also publishes nametag hash, proxy address, encrypted nametag for recovery.
1689
+ *
1690
+ * Uses kind 30078 parameterized replaceable event with d=SHA256('unicity:identity:' + nostrPubkey).
1691
+ * Each HD address index has its own Nostr key → its own binding event.
1692
+ *
1693
+ * @returns true if successful, false if nametag is taken by another pubkey
1694
+ */
1695
+ async publishIdentityBinding(chainPubkey, l1Address, directAddress, nametag) {
1696
+ this.ensureReady();
1697
+ if (!this.identity) {
1698
+ throw new Error("Identity not set");
1699
+ }
1700
+ const nostrPubkey = this.getNostrPubkey();
1701
+ const dTagBytes = new TextEncoder().encode("unicity:identity:" + nostrPubkey);
1702
+ const dTag = Buffer2.from(sha256(dTagBytes)).toString("hex");
1703
+ const contentObj = {
1704
+ public_key: chainPubkey,
1705
+ l1_address: l1Address,
1706
+ direct_address: directAddress
1707
+ };
1708
+ const tags = [
1709
+ ["d", dTag],
1710
+ ["t", hashAddressForTag(chainPubkey)],
1711
+ ["t", hashAddressForTag(directAddress)],
1712
+ ["t", hashAddressForTag(l1Address)]
1713
+ ];
1714
+ if (nametag) {
1715
+ const existing = await this.resolveNametag(nametag);
1716
+ if (existing && existing !== nostrPubkey) {
1717
+ this.log("Nametag already taken:", nametag, "- owner:", existing);
1718
+ return false;
1719
+ }
1720
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1721
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1722
+ const proxyAddress = proxyAddr.toString();
1723
+ const encryptedNametag = await encryptNametag(nametag, this.identity.privateKey);
1724
+ const hashedNametag = hashNametag(nametag);
1725
+ contentObj.nametag = nametag;
1726
+ contentObj.encrypted_nametag = encryptedNametag;
1727
+ contentObj.proxy_address = proxyAddress;
1728
+ tags.push(["t", hashedNametag]);
1729
+ tags.push(["t", hashAddressForTag(proxyAddress)]);
1730
+ }
1731
+ const content = JSON.stringify(contentObj);
1732
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1733
+ await this.publishEvent(event);
1734
+ if (nametag) {
1735
+ this.log("Published identity binding with nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...");
1736
+ } else {
1737
+ this.log("Published identity binding (no nametag) for pubkey:", nostrPubkey.slice(0, 16) + "...");
1738
+ }
1739
+ return true;
1740
+ }
1741
+ /** @deprecated Use publishIdentityBinding instead */
1556
1742
  async publishNametag(nametag, address) {
1557
1743
  this.ensureReady();
1558
1744
  const hashedNametag = hashNametag(nametag);
@@ -1579,6 +1765,9 @@ var NostrTransportProvider = class {
1579
1765
  const compressedPubkey = getPublicKey(privateKeyHex, true);
1580
1766
  const l1Address = publicKeyToAddress(compressedPubkey, "alpha");
1581
1767
  const encryptedNametag = await encryptNametag(nametag, privateKeyHex);
1768
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1769
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1770
+ const proxyAddress = proxyAddr.toString();
1582
1771
  const hashedNametag = hashNametag(nametag);
1583
1772
  const content = JSON.stringify({
1584
1773
  nametag_hash: hashedNametag,
@@ -1588,17 +1777,20 @@ var NostrTransportProvider = class {
1588
1777
  encrypted_nametag: encryptedNametag,
1589
1778
  public_key: compressedPubkey,
1590
1779
  l1_address: l1Address,
1591
- direct_address: directAddress
1780
+ direct_address: directAddress,
1781
+ proxy_address: proxyAddress
1592
1782
  });
1593
- const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, [
1783
+ const tags = [
1594
1784
  ["d", hashedNametag],
1595
1785
  ["nametag", hashedNametag],
1596
1786
  ["t", hashedNametag],
1787
+ ["t", hashAddressForTag(directAddress)],
1788
+ ["t", hashAddressForTag(proxyAddress)],
1597
1789
  ["address", nostrPubkey],
1598
- // Extended tags for indexing
1599
1790
  ["pubkey", compressedPubkey],
1600
1791
  ["l1", l1Address]
1601
- ]);
1792
+ ];
1793
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1602
1794
  await this.publishEvent(event);
1603
1795
  this.log("Registered nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...", "l1:", l1Address.slice(0, 12) + "...");
1604
1796
  return true;
@@ -2550,6 +2742,113 @@ function createUnicityAggregatorProvider(config) {
2550
2742
  }
2551
2743
  var createUnicityOracleProvider = createUnicityAggregatorProvider;
2552
2744
 
2745
+ // price/CoinGeckoPriceProvider.ts
2746
+ var CoinGeckoPriceProvider = class {
2747
+ platform = "coingecko";
2748
+ cache = /* @__PURE__ */ new Map();
2749
+ apiKey;
2750
+ cacheTtlMs;
2751
+ timeout;
2752
+ debug;
2753
+ baseUrl;
2754
+ constructor(config) {
2755
+ this.apiKey = config?.apiKey;
2756
+ this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
2757
+ this.timeout = config?.timeout ?? 1e4;
2758
+ this.debug = config?.debug ?? false;
2759
+ this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
2760
+ }
2761
+ async getPrices(tokenNames) {
2762
+ if (tokenNames.length === 0) {
2763
+ return /* @__PURE__ */ new Map();
2764
+ }
2765
+ const now = Date.now();
2766
+ const result = /* @__PURE__ */ new Map();
2767
+ const uncachedNames = [];
2768
+ for (const name of tokenNames) {
2769
+ const cached = this.cache.get(name);
2770
+ if (cached && cached.expiresAt > now) {
2771
+ if (cached.price !== null) {
2772
+ result.set(name, cached.price);
2773
+ }
2774
+ } else {
2775
+ uncachedNames.push(name);
2776
+ }
2777
+ }
2778
+ if (uncachedNames.length === 0) {
2779
+ return result;
2780
+ }
2781
+ try {
2782
+ const ids = uncachedNames.join(",");
2783
+ const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
2784
+ const headers = { Accept: "application/json" };
2785
+ if (this.apiKey) {
2786
+ headers["x-cg-pro-api-key"] = this.apiKey;
2787
+ }
2788
+ if (this.debug) {
2789
+ console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
2790
+ }
2791
+ const response = await fetch(url, {
2792
+ headers,
2793
+ signal: AbortSignal.timeout(this.timeout)
2794
+ });
2795
+ if (!response.ok) {
2796
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
2797
+ }
2798
+ const data = await response.json();
2799
+ for (const [name, values] of Object.entries(data)) {
2800
+ if (values && typeof values === "object") {
2801
+ const price = {
2802
+ tokenName: name,
2803
+ priceUsd: values.usd ?? 0,
2804
+ priceEur: values.eur,
2805
+ change24h: values.usd_24h_change,
2806
+ timestamp: now
2807
+ };
2808
+ this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
2809
+ result.set(name, price);
2810
+ }
2811
+ }
2812
+ for (const name of uncachedNames) {
2813
+ if (!result.has(name)) {
2814
+ this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
2815
+ }
2816
+ }
2817
+ if (this.debug) {
2818
+ console.log(`[CoinGecko] Fetched ${result.size} prices`);
2819
+ }
2820
+ } catch (error) {
2821
+ if (this.debug) {
2822
+ console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
2823
+ }
2824
+ for (const name of uncachedNames) {
2825
+ const stale = this.cache.get(name);
2826
+ if (stale?.price) {
2827
+ result.set(name, stale.price);
2828
+ }
2829
+ }
2830
+ }
2831
+ return result;
2832
+ }
2833
+ async getPrice(tokenName) {
2834
+ const prices = await this.getPrices([tokenName]);
2835
+ return prices.get(tokenName) ?? null;
2836
+ }
2837
+ clearCache() {
2838
+ this.cache.clear();
2839
+ }
2840
+ };
2841
+
2842
+ // price/index.ts
2843
+ function createPriceProvider(config) {
2844
+ switch (config.platform) {
2845
+ case "coingecko":
2846
+ return new CoinGeckoPriceProvider(config);
2847
+ default:
2848
+ throw new Error(`Unsupported price platform: ${String(config.platform)}`);
2849
+ }
2850
+ }
2851
+
2553
2852
  // impl/shared/resolvers.ts
2554
2853
  function getNetworkConfig(network = "mainnet") {
2555
2854
  return NETWORKS[network];
@@ -2598,6 +2897,19 @@ function resolveL1Config(network, config) {
2598
2897
  enableVesting: config.enableVesting
2599
2898
  };
2600
2899
  }
2900
+ function resolvePriceConfig(config) {
2901
+ if (config === void 0) {
2902
+ return void 0;
2903
+ }
2904
+ return {
2905
+ platform: config.platform ?? "coingecko",
2906
+ apiKey: config.apiKey,
2907
+ baseUrl: config.baseUrl,
2908
+ cacheTtlMs: config.cacheTtlMs,
2909
+ timeout: config.timeout,
2910
+ debug: config.debug
2911
+ };
2912
+ }
2601
2913
 
2602
2914
  // impl/nodejs/index.ts
2603
2915
  function createNodeProviders(config) {
@@ -2605,6 +2917,7 @@ function createNodeProviders(config) {
2605
2917
  const transportConfig = resolveTransportConfig(network, config?.transport);
2606
2918
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
2607
2919
  const l1Config = resolveL1Config(network, config?.l1);
2920
+ const priceConfig = resolvePriceConfig(config?.price);
2608
2921
  return {
2609
2922
  storage: createFileStorageProvider({
2610
2923
  dataDir: config?.dataDir ?? "./sphere-data"
@@ -2627,7 +2940,8 @@ function createNodeProviders(config) {
2627
2940
  debug: oracleConfig.debug,
2628
2941
  network
2629
2942
  }),
2630
- l1: l1Config
2943
+ l1: l1Config,
2944
+ price: priceConfig ? createPriceProvider(priceConfig) : void 0
2631
2945
  };
2632
2946
  }
2633
2947
  export {