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 🙏

© 2024 – Pkg Stats / Ryan Hefner

universal-wallet-connector

v0.0.9

Published

<div align="center"> <a href="https://github.com/othneildrew/Best-README-Template"> <img src="images/logo.png" alt="Logo" width="150" height="120"> </a>

Downloads

5

Readme

Table of Contents

Introduction

Why Another Wallet Connector?

As developers, we aim to concentrate on our applications and objectives. However, while working with Cardano's dApps, we encountered several issues, such as:

  • Account changes causing crashes
  • Network changes occurring without the front-end's awareness
  • Most wallets not handling disconnections properly
  • The hassle of tracking the wallet address, stake address, and network in real-time
  • Ultimately, we want a solution that simply works

What's New?

We aim to bridge the gap between our ideal dApp functionalities and the current market offerings by:

  • Standardizing usage across different wallets
  • Implementing reactive variables for addresses, stake address, network, and balance
  • Dispatching custom events upon relevant wallet changes

What's not new

Basic functions remain accessible through the library. Our goal is not to complicate the process, but to streamline the implementation of the default wallet API. By integrating reactive variables and custom events, we aim to enhance clarity and transparency regarding the activities on the dApp side.

Get started

Weld provider

After installing the library, wrap your App within the WeldProvider.

import React from "react";
import ReactDOM from "react-dom/client";
import { WeldProvider } from "@/lib/react/contexts/weld.context";
import { App } from "./app";

const root = document.querySelector("#root");

if (root) {
  ReactDOM.createRoot(root).render(
    <React.StrictMode>
      <WeldProvider>
        <App />
      </WeldProvider>
    </React.StrictMode>,
  );
}

Initialization

In certain situations, a dApp may require specific initialization. This is managed through the wallet.handler.initialize method. It should be invoked as early as possible within a component that falls under the Weld provider tree.

const { wallet } = useWalletContext();

useEffect(() => {
  if (wallet.isConnected) {
    wallet.handler.initialize();
  }
}, [wallet]);

Usage

Here are common use cases for an app utilizing this library.

Wallet connection

Use the exported SUPPORTED_WALLETS constant to display the available wallets. Additionally, you can use the getInstalledExtensions method or the useInstalledExtensionsContext to identify installed extensions, even if they are not officially supported.

export const App = () => {
  const { wallet, connectWallet } = useWalletContext();

  useEffect(() => {
    if (wallet.isConnected) {
      wallet.handler.initialize();
    }
  }, [wallet]);

  return (
    <ExampleContainer>
      <article className="card bg-base-100 shadow-xl">
        <div className="card-body">
          <div className="flex flex-wrap gap-4">
            {SUPPORTED_WALLETS.map(({ key, displayName }) => (
              <button
                key={key}
                type="button"
                className="btn btn-primary"
                onClick={() => connectWallet(key)}
              >
                {displayName}
              </button>
            ))}
          </div>
        </div>
      </article>
    </ExampleContainer>
  );
};

Error handling

When using Weld, two types of errors can occur: synchronous errors and asynchronous ones.

Synchronous errors

Synchronous errors are the ones that get thrown by functions that you call explicitly, like connect and disconnect. They are regular rejections that can be caught and handled by using normal language constructs:

try {
  const handler = await connect(key);
} catch (error) {
  // handle connection error
}

Synchronous errors using React

You can use one of two functions to connect a wallet using the useWallet React hook. If you just want to trigger the connection flow and don't care about the result, use the connectWallet function, which is guaranteed to never throw:

// Doesn't return the wallet and never throws
connectWallet(key);

You can pass callbacks to the connectWallet function to handle success and error cases:

connectWallet(key, {
  onSuccess(wallet) {
    console.log("wallet", wallet);
  },
  onError(error) {
    console.log("error", error);
  },
});

Alternatively, you can use the connectWalletAsync function, which returns a promise containing the wallet handler and throws errors when they occur.

try {
  const wallet = await connectWalletAsync(key);
  console.log("wallet", wallet);
} catch (error) {
  console.log("error", error);
}

Asynchronous errors

Asynchronous errors are the ones that occur during side effects like polling updates. Since they can occur anywhere and at any point, these errors cannot be caught by a try catch so we don't throw them as errors to prevent uncaught failure rejections. Instead, we send events which contain the errors and that can be listened for using our event system:

subscribe("weld:wallet.update.error.*", (event) => {
  handleError(event.data.error);
});

The useWallet React hook wraps the asynchronous error events that are related to the current wallet and allows you to pass callbacks to handle them without having to manage the event subscriptions manually:

// If using the weld context
<WeldProvider config={{ wallet: { onUpdateError: error => handleError(error) }}}>{children}</WeldProvider>

// If using the wallet provider
<WalletProvider config={{ onUpdateError: error => handleError(error)}}>{children}</WalletProvider>

// If using the useWallet hook directly
const { wallet } = useWallet({ onUpdateError: error => handleError(error) });

Reactive variable

This very simple example would not be a real use case. But it shows that those values would be automatically updated if they are changing. This might seems trivial, but right now the default wallets API does not allow to achieve this easily. A valid use case for reactive variables are the connect button on a website header where the wallet icon is usally displayed as well as the balance.

This simple example may not reflect a practical use case, yet it demonstrates how these values are automatically updated upon change. While this may seem trivial, achieving this is not straightforward with the current default wallets API.

A pertinent application for reactive variables would be the connect button on a website's header, where the wallet icon and balance are typically displayed.

import { useWalletContext } from "@/lib/react/contexts/wallet.context";

export const App = () => {
  const { wallet } = useWalletContext();

  if (!wallet.isConnected) return <></>;

  return (
    <>
      <div>Connected to {wallet.handler.info.displayName}</div>
      <div>Stake address: {wallet.rewardAddress}</div>
      <div>Change address: {wallet.changeAddress}</div>
      <div>Network: {wallet.networkId}</div>
      <div>Lovelace: {wallet.balanceLovelace}</div>
    </>
  );
};

Other methods

All default API functions are accessible and can be utilized via the wallet.handler class. If a function is unavailable, you can retrieve the default API by invoking the wallet.handler.getDefaultApi method.

export const App = () => {
  const { wallet } = useWalletContext();

  useEffect(() => {
    if (wallet.isConnected) {
      wallet.handler.initialize();
    }
  }, [wallet]);

  return (
    <ExampleContainer>
      <article className="card bg-base-100 shadow-xl max-w-[800px] mx-auto">
        <div className="card-body text-center">
          {!wallet.isConnected ? (
            <h2>Connect your wallet</h2>
          ) : (
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => wallet.handler.signTx("YOUR_TX", true)}
            >
              Sign
            </button>
          )}
        </div>
      </article>
    </ExampleContainer>
  );
};

Events

Events are a crucial feature of this library, enabling continuous synchronization between the dApp connector and the front-end. They trigger updates as necessary, such as when the wallet balance changes or when the user switches wallet accounts.

Semantic

Events follow the specific naming convention scope.namespace.type, where the wildcard * can be used at the end of an event name to capture all corresponding events.

Naming

Scopes: wallet

Namespaces: connection, balance, reward-address, change-address, network

Types: update, initiate, success, error

Events table

| Scope | Namespace | Type | Parameters | | ------ | -------------- | -------- | --------------------------------------------------------------------------------- | | wallet | connection | initiate | undefined | | wallet | connection | success | { handler: WalletHandler; } | | wallet | connection | error | { error: unknown; } | | wallet | update | error | { error: unknown; } | | wallet | balance | update | { handler: WalletHandler; cbor: string; balanceLovelace: number \| undefined; } | | wallet | reward-address | update | { handler: WalletHandler; rewardAddress: string; } | | wallet | change-address | update | { handler: WalletHandler; changeAddress: string; } | | wallet | network | update | { handler: WalletHandler; networkId: NetworkId; } |

Wildcard usage

weld:*

Listen to every events triggered.

weld:wallet.*

Listen to every events triggered on the wallet scope.

weld:wallet.balance.*

Listen to every events triggered on the wallet scope and the balance namespace.

weld:wallet.balance.update.*

Listen to every events triggered on the wallet scope, the balance namespace and the update type.

weld:wallet.balance.update.nami

Listen to every events triggered on the wallet scope, the balance namespace, the update type and the specific nami key.

Persistence

Weld provides a flexible interface to handle wallet connection persistence.

Automatic reconnection

When using the useWallet React hook, an attempt will be made to reconnect the persisted wallet on first mount.

If you are not using the useWallet hook, you can use the getPersistedValue helper function to retrieve the persisted wallet and connect it during the initialization of your app:

function initApp() {
  const persistedWalletKey = getPersistedValue("connectedWallet");
  if (persistedWalletKey) {
    connect(persistedWalletKey).then((handler) => {
      console.log("handler", handler);
    });
  }
}

Note: getPersistedValue always returns undefined when persistence is disabled.

Configuration

By default, the user's wallet connection is persisted to local storage. This behavior can be customized by providing a different Storage interface to the global configuration object:

defaults.persistence.storage = {
  get(key) {
    if (typeof window !== "undefined") {
      return window.localStorage.getItem(key) ?? undefined;
    }
  },
  set(key, value) {
    if (typeof window !== "undefined") {
      window.localStorage.setItem(key, value);
    }
  },
  remove(key) {
    if (typeof window !== "undefined") {
      window.localStorage.removeItem(key);
    }
  },
};

The persistence features can be disabled through the global configuration object:

defaults.persistence.enabled = false;

Note: When using a SSR framework, make sure to set configuration options inside a client side file

Usage with Next.js

When using Next.js, you can prevent hydration errors by retrieving the connected wallet cookie in a server component and passing it as initial value to the useWallet hook:

import { cookies } from "next/headers";
import { STORAGE_KEYS } from "@ada-anvil/weld/server";
import { WeldProvider } from "@ada-anvil/weld/react";

export default function RootLayout({ children }) {
  const isConnectingTo = cookies().get(STORAGE_KEYS.connectedWallet)?.value;
  return (
    <WeldProvider
      config={{
        wallet: {
          initialState: { isConnectingTo },
        },
      }}
    >
      {children}
    </WeldProvider>
  );
}

Examples

A recent version of Node.js is required for this project.

To run the examples, navigate to the project's root directory and execute npm install, then npm run dev.

Alternatively, you can directly explore the code by browsing the examples folder.

Methods

initialize

Initializes the handler when it is needed for certain wallets.

  • Returns: Promise<boolean>

getChangeAddress

Gets the change address for the wallet.

  • Returns: Promise<AddressBech32>

getStakeAddress

Gets the stake address for the wallet.

  • Returns: Promise<AddressBech32>

getNetworkId

Gets the network ID of the wallet.

  • Returns: Promise<NetworkId>

getBalance

Gets the balance of the wallet in CBOR format.

  • Returns: Promise<Cbor>

getBalanceLovelace

Gets the balance of the wallet in Lovelace.

  • Returns: Promise<Lovelace>

getBalanceAssets

Gets the balance of assets for the wallet categorized by policies.

  • Returns: Promise<BalanceByPolicies>

getDefaultApi

Gets the default API for the wallet.

  • Returns: DefaultWalletApi

isConnected

Checks if the wallet is connected.

  • Returns: Promise<boolean>

isConnectedTo

Checks if the wallet is connected to a specific wallet key.

  • Returns: Promise<boolean>

getUtxos

Gets the UTXOs for the wallet.

  • Returns: Promise<string[] | undefined>

signTx

Signs a transaction.

  • Parameters: tx: string, partialSign: boolean = true
  • Returns: Promise<string>

submitTx

Submits a transaction to the network.

  • Parameters: tx: string
  • Returns: Promise<string>

signData

Signs data with the wallet's stake address.

  • Parameters: payload: string
  • Returns: Promise<Signature>