@unicitylabs/sphere-sdk 0.1.8 → 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.
@@ -21,8 +21,10 @@ var STORAGE_KEYS_GLOBAL = {
21
21
  WALLET_EXISTS: "wallet_exists",
22
22
  /** Current active address index */
23
23
  CURRENT_ADDRESS_INDEX: "current_address_index",
24
- /** Index of address nametags (JSON: { "0": "alice", "1": "bob" }) - for discovery */
25
- ADDRESS_NAMETAGS: "address_nametags"
24
+ /** Nametag cache per address (separate from tracked addresses registry) */
25
+ ADDRESS_NAMETAGS: "address_nametags",
26
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
27
+ TRACKED_ADDRESSES: "tracked_addresses"
26
28
  };
27
29
  var STORAGE_KEYS_ADDRESS = {
28
30
  /** Pending transfers for this address */
@@ -215,6 +217,19 @@ var LocalStorageProvider = class {
215
217
  await this.remove(key);
216
218
  }
217
219
  }
220
+ async saveTrackedAddresses(entries) {
221
+ await this.set(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES, JSON.stringify({ version: 1, addresses: entries }));
222
+ }
223
+ async loadTrackedAddresses() {
224
+ const data = await this.get(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
225
+ if (!data) return [];
226
+ try {
227
+ const parsed = JSON.parse(data);
228
+ return parsed.addresses ?? [];
229
+ } catch {
230
+ return [];
231
+ }
232
+ }
218
233
  // ===========================================================================
219
234
  // Helpers
220
235
  // ===========================================================================
@@ -1182,6 +1197,10 @@ function defaultUUIDGenerator() {
1182
1197
 
1183
1198
  // transport/NostrTransportProvider.ts
1184
1199
  var EVENT_KINDS = NOSTR_EVENT_KINDS;
1200
+ function hashAddressForTag(address) {
1201
+ const bytes = new TextEncoder().encode("unicity:address:" + address);
1202
+ return Buffer2.from(sha256(bytes)).toString("hex");
1203
+ }
1185
1204
  function deriveNametagEncryptionKey(privateKeyHex) {
1186
1205
  const privateKeyBytes = Buffer2.from(privateKeyHex, "hex");
1187
1206
  const saltInput = new TextEncoder().encode("sphere-nametag-salt");
@@ -1590,6 +1609,28 @@ var NostrTransportProvider = class {
1590
1609
  this.paymentRequestResponseHandlers.add(handler);
1591
1610
  return () => this.paymentRequestResponseHandlers.delete(handler);
1592
1611
  }
1612
+ /**
1613
+ * Resolve any identifier to full peer information.
1614
+ * Routes to the appropriate specific resolve method based on identifier format.
1615
+ */
1616
+ async resolve(identifier) {
1617
+ if (identifier.startsWith("@")) {
1618
+ return this.resolveNametagInfo(identifier.slice(1));
1619
+ }
1620
+ if (identifier.startsWith("DIRECT:") || identifier.startsWith("PROXY:")) {
1621
+ return this.resolveAddressInfo(identifier);
1622
+ }
1623
+ if (identifier.startsWith("alpha1") || identifier.startsWith("alphat1")) {
1624
+ return this.resolveAddressInfo(identifier);
1625
+ }
1626
+ if (/^0[23][0-9a-f]{64}$/i.test(identifier)) {
1627
+ return this.resolveAddressInfo(identifier);
1628
+ }
1629
+ if (/^[0-9a-f]{64}$/i.test(identifier)) {
1630
+ return this.resolveTransportPubkeyInfo(identifier);
1631
+ }
1632
+ return this.resolveNametagInfo(identifier);
1633
+ }
1593
1634
  async resolveNametag(nametag) {
1594
1635
  this.ensureReady();
1595
1636
  const hashedNametag = hashNametag(nametag);
@@ -1633,15 +1674,17 @@ var NostrTransportProvider = class {
1633
1674
  const bindingEvent = events[0];
1634
1675
  try {
1635
1676
  const content = JSON.parse(bindingEvent.content);
1677
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1678
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1679
+ const proxyAddress = proxyAddr.toString();
1636
1680
  if (content.public_key && content.l1_address) {
1637
- const l3Address = `PROXY:${hashedNametag}`;
1638
1681
  return {
1639
1682
  nametag,
1640
1683
  transportPubkey: bindingEvent.pubkey,
1641
1684
  chainPubkey: content.public_key,
1642
1685
  l1Address: content.l1_address,
1643
1686
  directAddress: content.direct_address || "",
1644
- proxyAddress: l3Address,
1687
+ proxyAddress,
1645
1688
  timestamp: bindingEvent.created_at * 1e3
1646
1689
  };
1647
1690
  }
@@ -1649,14 +1692,13 @@ var NostrTransportProvider = class {
1649
1692
  const pubkeyTag = bindingEvent.tags.find((t) => t[0] === "pubkey");
1650
1693
  const l1Tag = bindingEvent.tags.find((t) => t[0] === "l1");
1651
1694
  if (pubkeyTag?.[1] && l1Tag?.[1]) {
1652
- const l3Address = `PROXY:${hashedNametag}`;
1653
1695
  return {
1654
1696
  nametag,
1655
1697
  transportPubkey: bindingEvent.pubkey,
1656
1698
  chainPubkey: pubkeyTag[1],
1657
1699
  l1Address: l1Tag[1],
1658
1700
  directAddress: "",
1659
- proxyAddress: l3Address,
1701
+ proxyAddress,
1660
1702
  timestamp: bindingEvent.created_at * 1e3
1661
1703
  };
1662
1704
  }
@@ -1668,17 +1710,90 @@ var NostrTransportProvider = class {
1668
1710
  l1Address: "",
1669
1711
  // Cannot derive without 33-byte pubkey
1670
1712
  directAddress: "",
1671
- proxyAddress: `PROXY:${hashedNametag}`,
1713
+ proxyAddress,
1672
1714
  timestamp: bindingEvent.created_at * 1e3
1673
1715
  };
1674
1716
  } catch {
1717
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1718
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1675
1719
  return {
1676
1720
  nametag,
1677
1721
  transportPubkey: bindingEvent.pubkey,
1678
1722
  chainPubkey: "",
1679
1723
  l1Address: "",
1680
1724
  directAddress: "",
1681
- proxyAddress: `PROXY:${hashedNametag}`,
1725
+ proxyAddress: proxyAddr.toString(),
1726
+ timestamp: bindingEvent.created_at * 1e3
1727
+ };
1728
+ }
1729
+ }
1730
+ /**
1731
+ * Resolve a DIRECT://, PROXY://, or L1 address to full peer info.
1732
+ * Performs reverse lookup: hash(address) → query '#t' tag → parse binding event.
1733
+ * Works with both new identity binding events and legacy nametag binding events.
1734
+ */
1735
+ async resolveAddressInfo(address) {
1736
+ this.ensureReady();
1737
+ const addressHash = hashAddressForTag(address);
1738
+ const events = await this.queryEvents({
1739
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1740
+ "#t": [addressHash],
1741
+ limit: 1
1742
+ });
1743
+ if (events.length === 0) return null;
1744
+ const bindingEvent = events[0];
1745
+ try {
1746
+ const content = JSON.parse(bindingEvent.content);
1747
+ return {
1748
+ nametag: content.nametag || void 0,
1749
+ transportPubkey: bindingEvent.pubkey,
1750
+ chainPubkey: content.public_key || "",
1751
+ l1Address: content.l1_address || "",
1752
+ directAddress: content.direct_address || "",
1753
+ proxyAddress: content.proxy_address || void 0,
1754
+ timestamp: bindingEvent.created_at * 1e3
1755
+ };
1756
+ } catch {
1757
+ return {
1758
+ transportPubkey: bindingEvent.pubkey,
1759
+ chainPubkey: "",
1760
+ l1Address: "",
1761
+ directAddress: "",
1762
+ timestamp: bindingEvent.created_at * 1e3
1763
+ };
1764
+ }
1765
+ }
1766
+ /**
1767
+ * Resolve transport pubkey (Nostr pubkey) to full peer info.
1768
+ * Queries binding events authored by the given pubkey.
1769
+ */
1770
+ async resolveTransportPubkeyInfo(transportPubkey) {
1771
+ this.ensureReady();
1772
+ const events = await this.queryEvents({
1773
+ kinds: [EVENT_KINDS.NAMETAG_BINDING],
1774
+ authors: [transportPubkey],
1775
+ limit: 5
1776
+ });
1777
+ if (events.length === 0) return null;
1778
+ events.sort((a, b) => b.created_at - a.created_at);
1779
+ const bindingEvent = events[0];
1780
+ try {
1781
+ const content = JSON.parse(bindingEvent.content);
1782
+ return {
1783
+ nametag: content.nametag || void 0,
1784
+ transportPubkey: bindingEvent.pubkey,
1785
+ chainPubkey: content.public_key || "",
1786
+ l1Address: content.l1_address || "",
1787
+ directAddress: content.direct_address || "",
1788
+ proxyAddress: content.proxy_address || void 0,
1789
+ timestamp: bindingEvent.created_at * 1e3
1790
+ };
1791
+ } catch {
1792
+ return {
1793
+ transportPubkey: bindingEvent.pubkey,
1794
+ chainPubkey: "",
1795
+ l1Address: "",
1796
+ directAddress: "",
1682
1797
  timestamp: bindingEvent.created_at * 1e3
1683
1798
  };
1684
1799
  }
@@ -1726,6 +1841,63 @@ var NostrTransportProvider = class {
1726
1841
  this.log("Could not decrypt nametag from any event");
1727
1842
  return null;
1728
1843
  }
1844
+ /**
1845
+ * Publish identity binding event on Nostr.
1846
+ * Without nametag: publishes base binding (chainPubkey, l1Address, directAddress).
1847
+ * With nametag: also publishes nametag hash, proxy address, encrypted nametag for recovery.
1848
+ *
1849
+ * Uses kind 30078 parameterized replaceable event with d=SHA256('unicity:identity:' + nostrPubkey).
1850
+ * Each HD address index has its own Nostr key → its own binding event.
1851
+ *
1852
+ * @returns true if successful, false if nametag is taken by another pubkey
1853
+ */
1854
+ async publishIdentityBinding(chainPubkey, l1Address, directAddress, nametag) {
1855
+ this.ensureReady();
1856
+ if (!this.identity) {
1857
+ throw new Error("Identity not set");
1858
+ }
1859
+ const nostrPubkey = this.getNostrPubkey();
1860
+ const dTagBytes = new TextEncoder().encode("unicity:identity:" + nostrPubkey);
1861
+ const dTag = Buffer2.from(sha256(dTagBytes)).toString("hex");
1862
+ const contentObj = {
1863
+ public_key: chainPubkey,
1864
+ l1_address: l1Address,
1865
+ direct_address: directAddress
1866
+ };
1867
+ const tags = [
1868
+ ["d", dTag],
1869
+ ["t", hashAddressForTag(chainPubkey)],
1870
+ ["t", hashAddressForTag(directAddress)],
1871
+ ["t", hashAddressForTag(l1Address)]
1872
+ ];
1873
+ if (nametag) {
1874
+ const existing = await this.resolveNametag(nametag);
1875
+ if (existing && existing !== nostrPubkey) {
1876
+ this.log("Nametag already taken:", nametag, "- owner:", existing);
1877
+ return false;
1878
+ }
1879
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1880
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1881
+ const proxyAddress = proxyAddr.toString();
1882
+ const encryptedNametag = await encryptNametag(nametag, this.identity.privateKey);
1883
+ const hashedNametag = hashNametag(nametag);
1884
+ contentObj.nametag = nametag;
1885
+ contentObj.encrypted_nametag = encryptedNametag;
1886
+ contentObj.proxy_address = proxyAddress;
1887
+ tags.push(["t", hashedNametag]);
1888
+ tags.push(["t", hashAddressForTag(proxyAddress)]);
1889
+ }
1890
+ const content = JSON.stringify(contentObj);
1891
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1892
+ await this.publishEvent(event);
1893
+ if (nametag) {
1894
+ this.log("Published identity binding with nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...");
1895
+ } else {
1896
+ this.log("Published identity binding (no nametag) for pubkey:", nostrPubkey.slice(0, 16) + "...");
1897
+ }
1898
+ return true;
1899
+ }
1900
+ /** @deprecated Use publishIdentityBinding instead */
1729
1901
  async publishNametag(nametag, address) {
1730
1902
  this.ensureReady();
1731
1903
  const hashedNametag = hashNametag(nametag);
@@ -1752,6 +1924,9 @@ var NostrTransportProvider = class {
1752
1924
  const compressedPubkey = getPublicKey(privateKeyHex, true);
1753
1925
  const l1Address = publicKeyToAddress(compressedPubkey, "alpha");
1754
1926
  const encryptedNametag = await encryptNametag(nametag, privateKeyHex);
1927
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
1928
+ const proxyAddr = await ProxyAddress.fromNameTag(nametag);
1929
+ const proxyAddress = proxyAddr.toString();
1755
1930
  const hashedNametag = hashNametag(nametag);
1756
1931
  const content = JSON.stringify({
1757
1932
  nametag_hash: hashedNametag,
@@ -1761,17 +1936,20 @@ var NostrTransportProvider = class {
1761
1936
  encrypted_nametag: encryptedNametag,
1762
1937
  public_key: compressedPubkey,
1763
1938
  l1_address: l1Address,
1764
- direct_address: directAddress
1939
+ direct_address: directAddress,
1940
+ proxy_address: proxyAddress
1765
1941
  });
1766
- const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, [
1942
+ const tags = [
1767
1943
  ["d", hashedNametag],
1768
1944
  ["nametag", hashedNametag],
1769
1945
  ["t", hashedNametag],
1946
+ ["t", hashAddressForTag(directAddress)],
1947
+ ["t", hashAddressForTag(proxyAddress)],
1770
1948
  ["address", nostrPubkey],
1771
- // Extended tags for indexing
1772
1949
  ["pubkey", compressedPubkey],
1773
1950
  ["l1", l1Address]
1774
- ]);
1951
+ ];
1952
+ const event = await this.createEvent(EVENT_KINDS.NAMETAG_BINDING, content, tags);
1775
1953
  await this.publishEvent(event);
1776
1954
  this.log("Registered nametag:", nametag, "for pubkey:", nostrPubkey.slice(0, 16) + "...", "l1:", l1Address.slice(0, 12) + "...");
1777
1955
  return true;
@@ -2776,6 +2954,113 @@ async function readFileAsUint8Array(file) {
2776
2954
  return new Uint8Array(buffer);
2777
2955
  }
2778
2956
 
2957
+ // price/CoinGeckoPriceProvider.ts
2958
+ var CoinGeckoPriceProvider = class {
2959
+ platform = "coingecko";
2960
+ cache = /* @__PURE__ */ new Map();
2961
+ apiKey;
2962
+ cacheTtlMs;
2963
+ timeout;
2964
+ debug;
2965
+ baseUrl;
2966
+ constructor(config) {
2967
+ this.apiKey = config?.apiKey;
2968
+ this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
2969
+ this.timeout = config?.timeout ?? 1e4;
2970
+ this.debug = config?.debug ?? false;
2971
+ this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
2972
+ }
2973
+ async getPrices(tokenNames) {
2974
+ if (tokenNames.length === 0) {
2975
+ return /* @__PURE__ */ new Map();
2976
+ }
2977
+ const now = Date.now();
2978
+ const result = /* @__PURE__ */ new Map();
2979
+ const uncachedNames = [];
2980
+ for (const name of tokenNames) {
2981
+ const cached = this.cache.get(name);
2982
+ if (cached && cached.expiresAt > now) {
2983
+ if (cached.price !== null) {
2984
+ result.set(name, cached.price);
2985
+ }
2986
+ } else {
2987
+ uncachedNames.push(name);
2988
+ }
2989
+ }
2990
+ if (uncachedNames.length === 0) {
2991
+ return result;
2992
+ }
2993
+ try {
2994
+ const ids = uncachedNames.join(",");
2995
+ const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
2996
+ const headers = { Accept: "application/json" };
2997
+ if (this.apiKey) {
2998
+ headers["x-cg-pro-api-key"] = this.apiKey;
2999
+ }
3000
+ if (this.debug) {
3001
+ console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
3002
+ }
3003
+ const response = await fetch(url, {
3004
+ headers,
3005
+ signal: AbortSignal.timeout(this.timeout)
3006
+ });
3007
+ if (!response.ok) {
3008
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
3009
+ }
3010
+ const data = await response.json();
3011
+ for (const [name, values] of Object.entries(data)) {
3012
+ if (values && typeof values === "object") {
3013
+ const price = {
3014
+ tokenName: name,
3015
+ priceUsd: values.usd ?? 0,
3016
+ priceEur: values.eur,
3017
+ change24h: values.usd_24h_change,
3018
+ timestamp: now
3019
+ };
3020
+ this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
3021
+ result.set(name, price);
3022
+ }
3023
+ }
3024
+ for (const name of uncachedNames) {
3025
+ if (!result.has(name)) {
3026
+ this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
3027
+ }
3028
+ }
3029
+ if (this.debug) {
3030
+ console.log(`[CoinGecko] Fetched ${result.size} prices`);
3031
+ }
3032
+ } catch (error) {
3033
+ if (this.debug) {
3034
+ console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
3035
+ }
3036
+ for (const name of uncachedNames) {
3037
+ const stale = this.cache.get(name);
3038
+ if (stale?.price) {
3039
+ result.set(name, stale.price);
3040
+ }
3041
+ }
3042
+ }
3043
+ return result;
3044
+ }
3045
+ async getPrice(tokenName) {
3046
+ const prices = await this.getPrices([tokenName]);
3047
+ return prices.get(tokenName) ?? null;
3048
+ }
3049
+ clearCache() {
3050
+ this.cache.clear();
3051
+ }
3052
+ };
3053
+
3054
+ // price/index.ts
3055
+ function createPriceProvider(config) {
3056
+ switch (config.platform) {
3057
+ case "coingecko":
3058
+ return new CoinGeckoPriceProvider(config);
3059
+ default:
3060
+ throw new Error(`Unsupported price platform: ${String(config.platform)}`);
3061
+ }
3062
+ }
3063
+
2779
3064
  // impl/shared/resolvers.ts
2780
3065
  function getNetworkConfig(network = "mainnet") {
2781
3066
  return NETWORKS[network];
@@ -2824,6 +3109,19 @@ function resolveL1Config(network, config) {
2824
3109
  enableVesting: config.enableVesting
2825
3110
  };
2826
3111
  }
3112
+ function resolvePriceConfig(config) {
3113
+ if (config === void 0) {
3114
+ return void 0;
3115
+ }
3116
+ return {
3117
+ platform: config.platform ?? "coingecko",
3118
+ apiKey: config.apiKey,
3119
+ baseUrl: config.baseUrl,
3120
+ cacheTtlMs: config.cacheTtlMs,
3121
+ timeout: config.timeout,
3122
+ debug: config.debug
3123
+ };
3124
+ }
2827
3125
  function resolveArrayConfig(defaults, replace, additional) {
2828
3126
  if (replace) {
2829
3127
  return replace;
@@ -2891,6 +3189,7 @@ function createBrowserProviders(config) {
2891
3189
  const oracleConfig = resolveOracleConfig(network, config?.oracle);
2892
3190
  const l1Config = resolveL1Config(network, config?.l1);
2893
3191
  const tokenSyncConfig = resolveTokenSyncConfig(network, config?.tokenSync);
3192
+ const priceConfig = resolvePriceConfig(config?.price);
2894
3193
  return {
2895
3194
  storage: createLocalStorageProvider(config?.storage),
2896
3195
  transport: createNostrTransportProvider({
@@ -2911,6 +3210,7 @@ function createBrowserProviders(config) {
2911
3210
  }),
2912
3211
  tokenStorage: createIndexedDBTokenStorageProvider(),
2913
3212
  l1: l1Config,
3213
+ price: priceConfig ? createPriceProvider(priceConfig) : void 0,
2914
3214
  tokenSyncConfig
2915
3215
  };
2916
3216
  }