npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eternl/hub-dapp-sdk

v0.31.3

Published

Eternl Hub SDK for dApp developers.

Readme

@eternl/hub-dapp-sdk

Connect your dApp to Cardano wallets through the Eternl Hub — no browser extension required. Pairing happens over a relayed, end-to-end-encrypted channel using a short connection code (or QR / deep link), and the resulting session exposes a CIP-30 compatible API plus a few Eternl extensions.

The SDK ships a framework-agnostic core plus thin, state-only helpers for React and Vue.

Installation

npm install @eternl/hub-dapp-sdk
# or
pnpm add @eternl/hub-dapp-sdk
# or
yarn add @eternl/hub-dapp-sdk

react and vue are optional peer dependencies — install only the one your app uses. The core entry point (@eternl/hub-dapp-sdk) needs neither.

How connecting works

There is no EternlConnect.connect() facade. You drive the flow yourself with two core functions:

| Function | Role | Returns | | --- | --- | --- | | createDAppConnection(options) | Offerer. The dApp generates a code and waits for a wallet to join. | a DAppConnectionHandle ({ code, dappId, expiresAt, channel, waitForWallet, cancel }) | | joinWithCode(code, options) | Verifier. The dApp enters a code shown by the wallet. | a Promise<EternlSession> |

Most dApps use createDAppConnection: show handle.code to the user, then await handle.waitForWallet() to receive an EternlSession.

The security model: you MUST display the SAS6

During waitForWallet, the connection reaches a status: 'sas_ready' step that carries a 6-digit Short Authentication String (sas6). Your dApp must render sas6 to the user so they can compare it against the digits shown in their wallet.

If the two codes match, no man-in-the-middle substituted the handshake keys. This comparison is the core defense of the pairing protocol — do not skip rendering it. The same sas6 is also present on the subsequent 'confirmed' event.

Quick Start (framework-agnostic core)

import { createDAppConnection } from '@eternl/hub-dapp-sdk';
import type { ConnectionStatusEvent, EternlSession } from '@eternl/hub-dapp-sdk';

async function connect(): Promise<EternlSession> {
  const handle = await createDAppConnection({
    appName: 'My DApp',
    appUrl: 'https://mydapp.com',
    appIcon: 'https://mydapp.com/icon.png',
    permissions: ['read', 'sign'],
    network: 'mainnet',
    // Required against production hubs — see "NATS authentication" below.
    getAuthenticator: myGetAuthenticator,
  });

  // 1. Show the connection code to the user. They enter it in their wallet.
  console.log('Enter this code in Eternl:', handle.code);
  console.log('Code expires at:', new Date(handle.expiresAt).toLocaleTimeString());

  // 2. Wait for the wallet. The onStatus callback drives your UI, and it is
  //    where the SAS6 verification step surfaces.
  const session = await handle.waitForWallet((event: ConnectionStatusEvent) => {
    switch (event.status) {
      case 'waiting_for_wallet':
        console.log('Waiting for a wallet to enter the code…');
        break;
      case 'wallet_connected':
        console.log('Wallet connected, exchanging keys…');
        break;
      case 'sas_ready':
        // SECURITY-CRITICAL: render event.sas6 to the user so they can compare
        // it with the 6 digits shown in their wallet before continuing.
        console.log('Verify these digits match your wallet:', event.sas6);
        break;
      case 'awaiting_user':
        console.log(`${event.walletName ?? 'Wallet'}: user is selecting accounts…`);
        break;
      case 'confirmed':
        console.log('Connection confirmed.');
        break;
      case 'error':
      case 'cancelled':
      case 'timeout':
        console.log('Connection ended:', event.status, event.message);
        break;
    }
  });

  // 3. Use the session (CIP-30 + Eternl extensions).
  const networkId = await session.getNetworkId();
  const ada = await session.getAdaBalance();
  console.log('Connected:', session.activeAccount.name, networkId, `${ada} ADA`);

  return session;
}

To abort before a wallet joins, call handle.cancel().

Joining a code shown by the wallet

If your flow instead has the wallet present a code, use joinWithCode:

import { joinWithCode } from '@eternl/hub-dapp-sdk';

const session = await joinWithCode(userEnteredCode, {
  appName: 'My DApp',
  permissions: ['read', 'sign'],
  getAuthenticator: myGetAuthenticator,
});

Rendering a QR code / deep link

handle.code is the canonical thing to show. If you want to encode it as a scannable QR or a mobile deep link, build a PairingV2Payload and use the encoders:

import { generateQRData, generateDeepLink } from '@eternl/hub-dapp-sdk';
import type { PairingV2Payload } from '@eternl/hub-dapp-sdk';

const payload: PairingV2Payload = {
  v: 2,
  type: 'dapp',
  pin: handle.code,
  expiresAt: handle.expiresAt,
};

const qrString = generateQRData(payload);     // "eternl-pair:<base64url(JSON)>" — feed to any QR encoder
const deepLink = generateDeepLink(payload);   // "eternl://pair?p=<base64url(JSON)>"

NATS authentication (required in production)

Pairing traffic flows over a hardened NATS broker that denies anonymous connections by default. You must supply an authenticator:

  • Provide getAuthenticator — an async () => Authenticator | undefined from @nats-io/nats-core.
  • If getAuthenticator is omitted (or resolves to undefined) and unsafeAllowAnonymousNats is not set, the connect call throws EternlConnectError with code === ErrorCode.AUTH_REQUIRED.
import { createDAppConnection, EternlConnectError, ErrorCode } from '@eternl/hub-dapp-sdk';

try {
  const handle = await createDAppConnection({
    appName: 'My DApp',
    getAuthenticator: async () => myAuthenticator, // from @nats-io/nats-core
  });
  // …
} catch (err) {
  if (err instanceof EternlConnectError && err.code === ErrorCode.AUTH_REQUIRED) {
    console.error('No NATS authenticator was supplied.');
  }
}

unsafeAllowAnonymousNats: true is an escape hatch for local/dev brokers only. Against production brokers an anonymous connection silently downgrades — the session looks paired but cannot publish — so the SDK refuses it by default.

Note: in standard production setups the SDK can mint a scoped token internally, so you may not need getAuthenticator at all. Confirm with your hub deployment which mode applies. Either way, the rule is: a real connection needs auth, or it throws AUTH_REQUIRED.

React

The React helpers are state-only. useEternl() does not connect — it holds the session and exposes reactive state. Your app calls createDAppConnection / joinWithCode itself, then hands the resulting session to setSession.

useEternl() returns:

{
  session: EternlSession | null;
  error: EternlConnectError | null;
  isConnected: boolean;
  activeAccount: AccountInfo | null;
  accounts: readonly AccountInfo[];
  setSession: (session: EternlSession) => void;   // install a connected session
  disconnect: () => Promise<void>;
  switchAccount: (accountId: string) => Promise<void>;
}

The driver pattern — connect with the core, then setSession:

import { useState } from 'react';
import { useEternl, EternlConnectionBadge } from '@eternl/hub-dapp-sdk/react';
import { createDAppConnection } from '@eternl/hub-dapp-sdk';
import type { ConnectionStatusEvent } from '@eternl/hub-dapp-sdk';

function App() {
  const { isConnected, activeAccount, setSession, disconnect } = useEternl();
  const [code, setCode] = useState<string | null>(null);
  const [sas6, setSas6] = useState<string | null>(null);

  async function handleConnect() {
    const handle = await createDAppConnection({
      appName: 'My DApp',
      permissions: ['read', 'sign'],
      getAuthenticator: myGetAuthenticator,
    });

    setCode(handle.code); // show the code to the user

    const session = await handle.waitForWallet((event: ConnectionStatusEvent) => {
      if (event.status === 'sas_ready') {
        // SECURITY-CRITICAL: show this so the user can compare with their wallet.
        setSas6(event.sas6 ?? null);
      }
    });

    setSession(session); // hand the connected session to the hook
    setCode(null);
    setSas6(null);
  }

  if (!isConnected) {
    return (
      <div>
        <button onClick={handleConnect}>Connect Wallet</button>
        {code && <p>Enter this code in Eternl: <strong>{code}</strong></p>}
        {sas6 && <p>Verify these digits match your wallet: <strong>{sas6}</strong></p>}
      </div>
    );
  }

  return (
    <div>
      <p>Connected: {activeAccount?.name}</p>
      <button onClick={disconnect}>Disconnect</button>
      {/* Floating badge with account switcher + disconnect */}
      <EternlConnectionBadge
        position="top-right"
        onAccountChange={(account) => console.log('Switched to:', account.name)}
        onDisconnect={() => console.log('Disconnected')}
      />
    </div>
  );
}

EternlConnectionBadge (React) props

interface EternlConnectionBadgeProps {
  position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; // default 'top-right'
  className?: string;
  style?: React.CSSProperties;
  onAccountChange?: (account: AccountInfo) => void;
  onDisconnect?: () => void;
}

The badge reads its state from the same useEternl() singleton, so it renders the current account and offers an account switcher / disconnect once setSession has been called.

Vue

The Vue composable is likewise state-onlyuseEternl() does not connect. You drive the connection with the core functions and call setSession.

useEternl() returns (refs are reactive):

{
  session: Readonly<Ref<EternlSession | null>>;
  error: Readonly<Ref<EternlConnectError | null>>;
  isConnected: ComputedRef<boolean>;
  activeAccount: ComputedRef<AccountInfo | null>;
  accounts: ComputedRef<readonly AccountInfo[]>;
  setSession: (session: EternlSession) => void;   // install a connected session
  disconnect: () => Promise<void>;
  switchAccount: (accountId: string) => Promise<void>;
}

The driver pattern in a SFC:

<script setup lang="ts">
import { ref } from 'vue';
import { useEternl, EternlConnectionBadge } from '@eternl/hub-dapp-sdk/vue';
import { createDAppConnection } from '@eternl/hub-dapp-sdk';
import type { ConnectionStatusEvent } from '@eternl/hub-dapp-sdk';

const { isConnected, activeAccount, setSession, disconnect } = useEternl();
const code = ref<string | null>(null);
const sas6 = ref<string | null>(null);

async function handleConnect() {
  const handle = await createDAppConnection({
    appName: 'My DApp',
    permissions: ['read', 'sign'],
    getAuthenticator: myGetAuthenticator,
  });

  code.value = handle.code; // show the code to the user

  const session = await handle.waitForWallet((event: ConnectionStatusEvent) => {
    if (event.status === 'sas_ready') {
      // SECURITY-CRITICAL: show this so the user can compare with their wallet.
      sas6.value = event.sas6 ?? null;
    }
  });

  setSession(session); // hand the connected session to the composable
  code.value = null;
  sas6.value = null;
}
</script>

<template>
  <div v-if="!isConnected">
    <button @click="handleConnect">Connect Wallet</button>
    <p v-if="code">Enter this code in Eternl: <strong>{{ code }}</strong></p>
    <p v-if="sas6">Verify these digits match your wallet: <strong>{{ sas6 }}</strong></p>
  </div>

  <div v-else>
    <p>Connected: {{ activeAccount?.name }}</p>
    <button @click="disconnect">Disconnect</button>

    <!-- Floating badge with account switcher + disconnect -->
    <EternlConnectionBadge
      position="top-right"
      @account-change="(account) => console.log('Switched to:', account.name)"
      @disconnect="() => console.log('Disconnected')"
    />
  </div>
</template>

EternlConnectionBadge (Vue) props & events

  • Props: position ('top-left' | 'top-right' | 'bottom-left' | 'bottom-right', default 'top-right'), className (string).
  • Events: account-change (emits the new AccountInfo), disconnect.

EternlSession reference

Once connected, the session is your CIP-30 surface plus a few Eternl extensions. All methods are async unless noted.

State (read-only properties)

| Property | Type | Notes | | --- | --- | --- | | pairId | string | 64-hex pairing identifier for this session. | | transport | TransportType | Underlying transport in use. | | expiresAt | Date | When the session expires. | | isConnected | boolean | Transport connectivity. | | walletOnline | boolean | Whether the paired wallet is currently reachable. | | accessGranted | boolean | Whether the wallet granted this dApp access (consent layer, distinct from isConnected). | | grantedCategories | ReadonlyArray<'core' \| 'governance' \| 'extension'> | Method categories the wallet granted. | | grantedPermissions | readonly Permission[] | Granted CIP-30/CIP-95 permissions. | | deviceInfo | WalletDeviceInfo \| null | Connected wallet's device info. | | availableAccounts | readonly AccountInfo[] | All accounts shared by the wallet. | | activeAccount | AccountInfo | The currently active account. |

CIP-30 core

session.getNetworkId();                       // Promise<number>
session.getUtxos(amount?, paginate?);         // Promise<string[] | null>
session.getBalance();                         // Promise<string>  (CBOR-encoded value)
session.getUsedAddresses(paginate?);          // Promise<string[]>
session.getUnusedAddresses();                 // Promise<string[]>
session.getChangeAddress();                   // Promise<string>
session.getRewardAddresses();                 // Promise<string[]>
session.signTx(txCbor, partialSign?);         // Promise<string>  (witness set / signed tx CBOR)
session.signData(address, payloadHex);        // Promise<DataSignature>
session.submitTx(txCbor);                     // Promise<string>  (tx hash)

CIP-30 extensions

session.getExtensions();                      // Promise<{ cip: number }[]>
session.getCollateral(params?);               // Promise<string[] | null>

CIP-95 governance

session.getPubDRepKey();                      // Promise<string>
session.getRegisteredPubStakeKeys();          // Promise<string[]>
session.getUnregisteredPubStakeKeys();        // Promise<string[]>

Eternl extensions

session.getAdaBalance();                      // Promise<number>  — balance in ADA, already parsed
session.requestPresenceCheck();               // Promise<boolean> — actively probe wallet reachability
session.switchAccount(accountId);             // Promise<void>
session.disconnect();                         // Promise<void>    — tear down this session
session.remove();                             // Promise<void>    — disconnect and forget the pairing

Eventssession.on(event, listener) returns an unsubscribe function:

const off = session.on('accountChange', (account) => { /* AccountInfo */ });
session.on('accountsChanged', (e) => { /* { accounts, activeAccountId } */ });
session.on('walletStatusChange', (e) => { /* { online: boolean } */ });
session.on('connectionChange', (e) => { /* { isConnected: boolean } */ });
session.on('accessGranted', (e) => { /* { grantedCategories, autoEstablishAllowed, walletName? } */ });
session.on('accessDenied', (e) => { /* { reason: string } */ });
session.on('peerPresenceC', (e) => { /* fast (~5s) presence event; requires enablePresenceC */ });
session.on('error', (err) => { /* Error */ });
off(); // unsubscribe

The peerPresenceC event only fires when you pass enablePresenceC: true in DAppConnectionOptions. It provides faster (~5s) offline detection than the legacy heartbeat path and runs additively alongside it.

DAppConnectionOptions

Shared by createDAppConnection and joinWithCode:

interface DAppConnectionOptions {
  appName: string;                 // required — shown to the user
  appUrl?: string;                 // defaults to window.location.origin
  appIcon?: string;
  permissions?: string[];          // e.g. ['read', 'sign'] (default)
  network?: string;                // defaults to 'mainnet'
  hubUrl?: string;                 // NATS WebSocket URL; defaults to the public hub
  hubRestUrl?: string;             // Hub REST URL; defaults to the public hub
  getAuthenticator?: () => Promise<Authenticator | undefined>; // from @nats-io/nats-core
  unsafeAllowAnonymousNats?: boolean; // dev/local only — see "NATS authentication"
  enablePresenceC?: boolean;       // opt into fast raw-stream presence (peerPresenceC)
}

Error handling

import { EternlConnectError, ErrorCode } from '@eternl/hub-dapp-sdk';

try {
  const handle = await createDAppConnection({ appName: 'My DApp', getAuthenticator });
  const session = await handle.waitForWallet();
} catch (error) {
  if (error instanceof EternlConnectError) {
    switch (error.code) {
      case ErrorCode.AUTH_REQUIRED: /* no NATS authenticator supplied */ break;
      case ErrorCode.TIMEOUT:       /* code expired before a wallet joined */ break;
      case ErrorCode.REJECTED:      /* user rejected the connection */ break;
      case ErrorCode.CANCELLED:     /* connection was cancelled */ break;
      case ErrorCode.TRANSPORT_ERROR: /* network / NATS error */ break;
      default: console.error(error.code, error.message);
    }
  }
}

Balance utilities

import {
  parseLovelaceBalance,
  lovelaceToAda,
  formatAda,
  parseAndFormatAda,
} from '@eternl/hub-dapp-sdk';

const lovelace = parseLovelaceBalance(await session.getBalance()); // CBOR → lovelace
const ada = lovelaceToAda(lovelace);                                // 1_000_000 → 1
const formatted = formatAda(lovelace);                              // "1.234567 ADA"
const display = parseAndFormatAda(await session.getBalance());      // CBOR → "1.234567 ADA"

(session.getAdaBalance() does the parse-and-convert for you in a single call.)

Session persistence & paired wallets

The core also exports helpers to persist and restore connections without re-pairing:

import {
  hasStoredSession,
  getStoredSessionInfo,
  restoreSession,
  clearStoredSession,
  getPairedWallets,
  reconnectToWallet,
  removePairedWallet,
  clearPairedWallets,
} from '@eternl/hub-dapp-sdk';

if (hasStoredSession()) {
  const session = await restoreSession({ getAuthenticator: myGetAuthenticator });
}

const wallets = getPairedWallets(); // PairedWallet[]
const session = await reconnectToWallet(wallets[0].walletId, { getAuthenticator: myGetAuthenticator });

restoreSession and reconnectToWallet accept the same getAuthenticator / unsafeAllowAnonymousNats auth options described above, and likewise throw AUTH_REQUIRED without them.

Subpath exports

| Import path | Contents | | --- | --- | | @eternl/hub-dapp-sdk | Core: createDAppConnection, joinWithCode, generateQRData, generateDeepLink, session/paired-wallet helpers, Permission, EternlConnectError, ErrorCode, balance utilities, and all types. | | @eternl/hub-dapp-sdk/react | useEternl, EternlConnectionBadge (state-only React helpers). | | @eternl/hub-dapp-sdk/vue | useEternl, EternlConnectionBadge (state-only Vue 3 helpers). |

TypeScript

The package is fully typed. Real exported types include:

import type {
  // Core connection
  DAppConnectionOptions,
  DAppConnectionHandle,
  DAppSession,
  ConnectionStatus,
  ConnectionStatusEvent,
  PairingV2Payload,

  // Session
  EternlSession,
  AccountInfo,
  ConnectionState,

  // Events
  SessionEventType,
  SessionEventListener,
  ConnectionChangeEvent,
  WalletStatusEvent,
  AccessGrantedEvent,
  AccessDeniedEvent,
  AccountsChangedEvent,

  // Wallet device
  WalletDeviceInfo,
  WalletPlatform,

  // Paired wallets
  PairedWallet,

  // CIP-30
  NetworkType,
  TransportType,
  Paginate,
  DataSignature,
  CollateralParams,
} from '@eternl/hub-dapp-sdk';

// Value exports
import { Permission, EternlConnectError, ErrorCode } from '@eternl/hub-dapp-sdk';

The previously documented EternlConnectConfig and ConnectionRequest types do not exist and are not exported.

License

Business Source License 1.1 (BUSL-1.1). See LICENSE.

The Licensor is Tastenkunst GmbH. Additional Use Grant: None — you may copy, modify, and make non-production use of this package. Production/commercial use requires a separate license from Tastenkunst. On the Change Date (the fourth anniversary of each version's first publication) that version converts to GPL-2.0-or-later.

Contact: https://tastenkunst.com/