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

@asseteragmbh/wallet-connector

v0.2.8

Published

Connect a partner site to the Assetera Wallet pop-up: an EIP-1193 provider, a wagmi connector and a RainbowKit wallet entry (ADR-0054 D3).

Readme

@asseteragmbh/wallet-connector

The partner side of the Assetera Wallet pop-up: an EIP-1193 provider that talks to the wallet at wallet.{env}.assetera.com, plus a wagmi v2 connector and a RainbowKit wallet entry. It has no dependency on Dynamic or on @asseteragmbh/wallet (ADR-0054 D3), so a partner site can add it without pulling in the wallet app.

The pop-up protocol — message types, the ready handshake, SUPPORTED_METHODS, error codes and connectUrl — is the contract in src/protocol.ts. This package is the opener side; the wallet's /connect page is the pop-up side and imports the same types from @asseteragmbh/wallet-connector/protocol.

Before you start: allow-list the partner origin

The wallet only answers a pop-up whose origin query parameter is on its allow-list (ADR-0054 D4). Ask Assetera to list your origin (for example https://app.partner.example) before shipping. An unlisted origin gets no ready message, so the connection simply never completes.

Also note:

  • connect must be triggered by a click. eth_requestAccounts calls window.open synchronously inside request, and browsers only allow that during user activation. Call connect() / eth_requestAccounts from a button handler, never on page load. If the pop-up is blocked, the request rejects with code 4900.
  • Mobile opens a tab. A pop-up (window.open) becomes a normal browser tab on mobile; the user switches back to the partner page when done.

1. Raw EIP-1193 provider

import { createAsseteraWalletProvider } from "@asseteragmbh/wallet-connector";

const provider = createAsseteraWalletProvider({
  walletUrl: "https://wallet.wallet-staging.assetera.com",
  defaultChainId: 1,
});

button.addEventListener("click", async () => {
  const accounts = await provider.request({ method: "eth_requestAccounts" });
  console.log(accounts);
});

provider.on("accountsChanged", (accounts) => console.log(accounts));
provider.on("chainChanged", (chainId) => console.log(chainId));
provider.on("disconnect", (error) => console.log(error));
provider.on("assetera_security", (status) => console.log(status));
provider.on("assetera_accounts", (accounts) => console.log(accounts));

request supports eth_requestAccounts, eth_accounts, eth_chainId, wallet_switchEthereumChain, personal_sign, eth_signTypedData_v4, eth_sendTransaction and wallet_activateVault. eth_accounts and eth_chainId are answered locally without opening the pop-up. Anything else rejects with 4200; read-only chain calls go through your own RPC transports. Errors are thrown as ProviderRpcError with an EIP-1193 code (4001, 4100, 4200, 4900, …).

While a wallet pop-up is open, the wallet reports how well it is protected. The provider emits assetera_security with a WalletSecurityStatus:

interface WalletSecurityStatus {
  protection: "unknown" | "add-factor" | "save-codes" | "create-codes" | "protected";
  passkeys: number;
  authenticatorApps: number;
}

The partner cannot ask for the status, it is only sent while the pop-up is open. add-factor means the wallet has no passkey and no authenticator app yet, save-codes that recovery codes exist but are not confirmed saved, create-codes that a factor exists but there are no recovery codes, and protected that both exist. The provider keeps the last value; provider.lastSecurityStatus() returns it, also after the pop-up closed. Use it to show a nudge such as "Secure your wallet with a passkey".

While a pop-up is open, the wallet may also name the accounts the client has already shared with your site. The provider emits assetera_accounts with a WalletAccountName[]:

interface WalletAccountName {
  address: `0x${string}`;
  name: string;
}

The wallet never sends an account your site does not already know, and it sends the message only while a pop-up is open, never on request. Use it to show "Savings · 0x65fa…3cd5" instead of a bare address. The provider keeps the last list; provider.lastAccountNames() returns it, also after the pop-up closed.

Both createAsseteraWalletProvider and asseteraWallet accept an optional signIn hint. It tells the wallet how the client signed in at the partner so it can try a silent sign-in and otherwise fall back to its own login page:

const provider = createAsseteraWalletProvider({
  walletUrl: "https://wallet.playground.assetera.com",
  defaultChainId: 1,
  signIn: "assetera-marketplace",
});

The hint is only a hint. The wallet checks it against its own list and may ignore it. It never carries a token or an identity.

Signing after an await (a quote, a fee)

The pop-up must open inside the user's click. A request that is sent after an await (for example after the app fetched a firm quote) would open it outside the click, and the browser blocks it. Open it first, in the click handler, then send the request when the data is there. The request reuses the open pop-up.

async function onConfirm() {
  const prepared = provider.prepareSigning(); // synchronous, still inside the click
  if (!prepared.opened) return showBlockedHint(); // the browser blocked it: ask for another click
  try {
    const quote = await fetchFirmQuote(); // the pop-up waits meanwhile
    await walletClient.signTypedData(quote.typedData); // reuses the open pop-up
  } catch (err) {
    prepared.release(); // nothing was sent: close the pop-up this call opened
    throw err;
  }
}

release() closes the pop-up only when prepareSigning() opened it and no request followed; a pop-up that was already open stays open. While no request has followed, the handle also holds the pop-up open, so an already open pop-up does not count down its idle close during the wait. With wagmi, take the provider from the connector: (await connector.getProvider()) as AsseteraWalletProvider. Get it BEFORE the click (for example when the dialog opens), because getProvider() is async.

Holding the pop-up open across a multi-step flow

One click often starts several wallet requests with waits between them: approve a token, wait for the approval to mine, fetch a firm price, sign the terms, send the transaction. The wallet closes its pop-up a short time after the last answered request. A request that comes later would then open a new window outside a click, and the browser would block it. holdPopup() asks the wallet to keep the current pop-up open between those requests.

async function onBuy() {
  const hold = provider.holdPopup(); // first statement of the click handler
  try {
    if (!hold.opened) return showPopupBlocked();
    await approve();
    await fetchQuote();
    await signAndSend();
  } finally {
    hold.release();
  }
}

The call is synchronous and never throws. It opens the pop-up inside the click, or reuses the one that is already open. hold.release() ends the hold; call it in a finally block so a failure does not leave the pop-up held. prepareSigning() holds the pop-up in the same way until its first request settles or release() runs.

The hold is a request to the wallet, not a guarantee. An older wallet ignores the hold message, and the wallet caps a hold at HOLD_MAX_MS (5 minutes) since the last hold: true or request. Keep a fallback for a closed pop-up, for example a button that retries the request from a click and shows a blocked hint when the browser refuses again.

Activating a vault (EIP-7702)

wallet_activateVault asks the wallet to activate a portfolio vault for the connected account. The client approves in the pop-up, the wallet signs an EIP-7702 authorization for Assetera's vault delegate and sends the type-4 transaction from the connected account. It resolves with the transaction hash.

const txHash = await provider.request({
  method: "wallet_activateVault",
  params: [{ chainId: "0xaa36a7", delegate: "0x…" }],
});

chainId is the EIP-155 chain id in the eth_chainId hex form and delegate is the vault delegate contract for that chain. The wallet decides which delegate is allowed: it accepts ONLY the Assetera vault delegate it knows for that chain, never a contract the partner names, so the parameter cannot point the delegation at a partner contract. The request needs an approved connection like every signature and focuses the pop-up. It rejects with 4001 when the client declines, 4100 when there is no connection, 4902 (or another unsupported-chain error) for a chain the wallet does not offer, and 4200 or a named error for a delegate the wallet does not accept. A wallet that does not know the method refuses it with 4200.

2. wagmi v2 connector

import { createConfig, http } from "wagmi";
import { mainnet } from "wagmi/chains";
import { asseteraWallet } from "@asseteragmbh/wallet-connector/wagmi";

const config = createConfig({
  chains: [mainnet],
  connectors: [asseteraWallet({ walletUrl: "https://wallet.wallet-staging.assetera.com" })],
  transports: { [mainnet.id]: http() },
});

@wagmi/core and viem are optional peer dependencies. The connector's isAuthorized() always returns false on purpose: a pop-up cannot be opened without a click, so wagmi must not try to auto-reconnect on load.

connect takes an optional expectedAddress, for a client that has several wallets and should sign in with the one the partner knows:

import { connect } from "@wagmi/core";

await connect(config, {
  connector: config.connectors[0]!,
  expectedAddress: "0x65fa…3cd5",
});

The connector sends it as eth_requestAccounts params: [{ address }], with the address in its checksummed form. Optional [{ address }]: the wallet the partner expects; the pop-up switches to it only when the signed-in client owns it (and, without a fresh approval, only when this partner was given that wallet before). Without it, or with an address viem's isAddress rejects, the request carries no params, exactly as before. A wallet that does not know the parameter ignores it and answers as it always did.

3. RainbowKit

import { connectorsForWallets } from "@rainbow-me/rainbowkit";
import { asseteraRainbowWallet } from "@asseteragmbh/wallet-connector/rainbowkit";

const connectors = connectorsForWallets(
  [
    {
      groupName: "Assetera",
      wallets: [asseteraRainbowWallet({ walletUrl: "https://wallet.wallet-staging.assetera.com" })],
    },
    // …other groups
  ],
  { appName: "My Partner App", projectId: "…" },
);

@rainbow-me/rainbowkit is an optional peer dependency. The wallet entry has id assetera-wallet, name Assetera Wallet and renders the same inline icon as the wagmi connector. asseteraRainbowWallet accepts the same signIn hint as asseteraWallet and passes it through.

Onboarding a new client (pop-up wizard)

A tied-agent partner can give a client an Assetera Wallet in one guided pop-up. Call openAsseteraOnboarding from a button click: it opens ${walletUrl}/onboard?origin=<partner origin>&v=1&chain=<CAIP-2 chain> in a pop-up. The wizard shows the wallet terms and creates the wallet (or returns the one the client already has), then resolves with the address while the pop-up stays open.

The wallet does not register itself with any compliance service. A regulated partner registers it like any other wallet, with an ownership signature that the client approves in the same pop-up. Then it calls close().

import { OnboardingError, openAsseteraOnboarding } from "@asseteragmbh/wallet-connector";

button.addEventListener("click", async () => {
  try {
    // No await before this call: it opens the pop-up and must run inside the click.
    const wallet = await openAsseteraOnboarding({
      walletUrl: "https://wallet.wallet-staging.assetera.com",
      chain: "eip155:80002",
      locale: "en",
      partnerName: "Musterbank",
      // signIn: "assetera-marketplace" is an optional sign-in hint for the wallet.
      // addWallet: true asks for one more wallet even when the client already has one.
    });
    try {
      const challenge = await fetchChallengeFromYourServer(wallet.address, wallet.chain);
      const signature = await wallet.signMessage(challenge.message);
      await registerWithYourComplianceService(wallet.address, wallet.chain, challenge.message, signature);
    } finally {
      wallet.close();
    }
  } catch (error) {
    if (error instanceof OnboardingError) console.error(error.code, error.message);
  }
});

wallet.created is true when the wallet was created in this run and false when the client already had it. If your registry already holds wallet.address, skip the signature and call close().

After the wizard reported the address the pop-up may send accounts messages. Pass an onAccounts option to receive each list, and call wallet.lastAccounts() for the last one.

While the pop-up is open the partner page shows a dimming overlay (overlay: true is the default) with a "Show the wallet window" button and a "Cancel" button. Set overlay: false to render your own, or pass overlay: { title, body, focus, cancel, blocked } to translate the built-in text; partnerName only shows up in that text.

The overlay is plain DOM with inline styles, so it needs no React and no stylesheet, and it never reads your page CSS. Give it your colours and radii with overlay.theme. A dark host takes the "dark" base palette and overrides what it needs:

await openAsseteraOnboarding({
  walletUrl: "https://wallet.wallet-staging.assetera.com",
  chain: "eip155:80002",
  overlay: {
    title: "Continue in the Assetera Wallet",
    body: "Finish the wallet setup in the Assetera window, then come back here.",
    focus: "Open the wallet window",
    cancel: "Not now",
    theme: { scheme: "dark", surface: "#1d1f27", primary: "#7b61ff", radius: "36px" },
  },
});

Every theme key is optional. scheme picks the base palette, "light" (today's look, unchanged) or "dark"; any other key the host passes wins over that palette. So the example above only changes the card colour, the focus button colour and the card radius.

| Key | Default (light) | Contract | | -------------- | -------------------- | -------------------------------------------------- | | scheme | "light" | Base palette, "light" or "dark". | | backdrop | rgba(15,20,30,0.6) | The dimming layer, a CSS colour. | | surface | #ffffff | The card background, a CSS colour. | | text | #101828 | The title colour, a CSS colour. | | mutedText | #475467 | The body text and the Cancel button, a CSS colour. | | primary | #101828 | The focus button background, a CSS colour. | | primaryText | #ffffff | The focus button text, a CSS colour. | | border | "none" | The card border, CSS border shorthand. | | radius | "16px" | The card corner radius, for example "36px". | | buttonRadius | "10px" | The button corner radius, for example "9999px". | | fontFamily | system-ui stack | The overlay font, CSS font-family. |

The "dark" preset is a neutral card: surface #1b1d24, white title, #9aa2b1 body text and a white focus button with dark text. Its text keeps WCAG AA contrast on that surface.

Each host value is applied to its own CSS property with style.setProperty, never concatenated into the style text. A value the browser cannot parse is dropped and the rest of the overlay stays as it is. All overlay text still goes through textContent.

Rendering your own dialog (overlay: false with onControls)

Set overlay: false and pass onControls to drive the run from your own dialog. The callback runs once, synchronously, right after the pop-up opened, whether or not an overlay is shown:

import type { OnboardingControls } from "@asseteragmbh/wallet-connector";

let controls: OnboardingControls | undefined;
const wallet = await openAsseteraOnboarding({
  walletUrl: "https://wallet.wallet-staging.assetera.com",
  chain: "eip155:80002",
  overlay: false,
  onControls: (handles) => {
    controls = handles;
    showMyOwnDialog();
  },
});

controls.focus() brings the pop-up to the front. controls.cancel() behaves like the overlay's Cancel button: it rejects with OnboardingError code "cancelled" before the address arrived, and calls close() after it. Both are safe to call once the run finished.

The promise rejects with an OnboardingError; branch on code:

  • invalid_options: the chain is not a CAIP-2 EVM id (for example eip155:80002) or the wallet URL is invalid.
  • popup_blocked: the browser blocked the pop-up. No overlay is shown; ask the user to allow pop-ups.
  • closed: the pop-up was closed before the wizard finished (or, from signMessage, before the signature).
  • cancelled: the user cancelled in the wizard, in the overlay or through controls.cancel(), or declined the signature.
  • refused: the wallet refused the onboarding.
  • failed: something else went wrong in the wallet (the optional message carries the detail).

The wizard checks the origin query parameter against the wallet's partner list, so your origin must be listed by Assetera (the same allow-list as connect, see above). On success the pop-up posts onboarded (or onboard-cancelled) to window.opener with targetOrigin = your origin, and the opener accepts it only when event.origin is the wallet origin and event.source is the window it opened. After onboarded the pop-up answers personal_sign requests for the onboarded address with the same request and response messages as connect. The overlay stays until close(), or until the client closes the pop-up.

Managing the wallet (pop-up)

A partner page can offer a "Manage wallet" button. openAsseteraWalletManager opens ${walletUrl}/manage?origin=<partner origin>&v=1[&lang=<locale>][&signIn=<hint>] from the click. In the pop-up the client sees the wallet accounts and the wallet security (passkeys, authenticator app, recovery codes) and can change them. The pop-up does not answer RPC requests.

import { ManagerError, openAsseteraWalletManager } from "@asseteragmbh/wallet-connector";

button.addEventListener("click", () => {
  try {
    // No await before this call: it opens the pop-up and must run inside the click.
    const manager = openAsseteraWalletManager({
      walletUrl: "https://wallet.playground.assetera.com",
      locale: "en",
      signIn: "assetera-marketplace",
      onSecurity: (status) => {
        if (status.protection === "add-factor") showSecureWalletNudge();
      },
      onAccounts: (accounts) => {
        for (const account of accounts) label(account.address, account.name);
      },
    });
    void manager.closed.then(() => console.log("manage pop-up closed"));
  } catch (error) {
    if (error instanceof ManagerError) console.error(error.code, error.message);
  }
});

The result carries close(), closed, lastSecurity() and lastAccounts(). close() closes the pop-up and removes the listener and the close timer; closed resolves when the client closes the pop-up too. The manage pop-up uses its own window name, so it never replaces a /connect pop-up.

openAsseteraWalletManager throws a ManagerError when the browser blocked the pop-up (popup_blocked) or when the wallet URL is invalid (invalid_options). The manage pop-up checks the origin query parameter against the wallet's partner list (the same allow-list as connect), so your origin must be listed. It posts ready to the opener and then security and accounts messages; messages are accepted only from the wallet origin and from the pop-up it opened.

Protocol summary

  1. On a click, the partner opens ${walletUrl}/connect?origin=<partner origin>&v=1[&signIn=<hint>][&chainId=<0x chain id>]. chainId is an EIP-155 chain id (the 0x-prefixed hex form eth_chainId returns) that asks the pop-up to start on the partner's current chain; if the wallet does not offer it, the pop-up stays on its own default and reports its chain with a chainChanged event.
  2. The pop-up checks the claimed origin against its allow-list, then posts ready to window.opener with targetOrigin = that origin.
  3. The partner waits for ready from the wallet origin, then posts { type: "assetera-wallet:request", v: 1, id, method, params } with targetOrigin = the wallet origin. The pop-up answers with assetera-wallet:response (matched by id) and forwards accountsChanged, chainChanged and disconnect events.
  4. Messages are accepted only when event.origin is the wallet origin and event.source is the current pop-up. The wallet may close the /connect pop-up after it connected or answered a request; the provider keeps the last accounts and chainId and opens the pop-up again on the next request from a click. While a pop-up is open the wallet also posts security messages (never on request); the provider emits them as assetera_security and keeps the last value in lastSecurityStatus(). The accounts messages work the same way: the provider emits assetera_accounts and keeps the last list in lastAccountNames().

See src/protocol.ts for the exact types, guards and error codes.