Asentum

Build on Asentum

Wallet Connect for Dapps

React drop-in · Estimated read time: 8 minutes

What it gives you

@asentum/connect is the wallet layer for Asentum dapps. It is one React provider, one button, and four hooks. It is what the airdrop dashboard, the social app, the IDE and AuraSwap all use, so the connect experience is identical everywhere on Asentum.

  • A connect button with a built in modal offering the browser extension, the Telegram wallet, or creating a new wallet.
  • Hooks for the connected address, the native ASE balance, and any contract.
  • Reads go straight to the public RPC and need no wallet at all. Writes are signed by the user's wallet.
  • No CSS to import and no heavy dependencies. React is the only peer dependency.

Private keys never reach this library. Signing happens inside the browser extension or the Telegram wallet, which prompt the user to approve each transaction.

Install

npm install @asentum/connect

React 17 or newer is required and is the only peer dependency. The package ships ESM and CommonJS builds plus TypeScript types, so it works in Next.js, Vite and Create React App without configuration.

Quick start

Wrap your app once in the provider, then drop the button anywhere inside it.

import { AsentumProvider, ConnectButton } from '@asentum/connect';

export default function App({ children }) {
  return (
    <AsentumProvider
      rpc="https://testnet.asentum.com"
      dappName="My Dapp"
      onConnect={(address) => console.log('connected', address)}
    >
      <ConnectButton />
      {children}
    </AsentumProvider>
  );
}

That is a working integration. The button opens the modal, the user picks a wallet, and everything inside the provider can read the connected address. The address is remembered across page loads, so a returning visitor stays connected until they disconnect.

In Next.js the provider is a client component. Put it in your root layout with 'use client' at the top of the file, or inside a small client wrapper component.

Customising the button

Every visual choice is a prop, so the button can match your design without a stylesheet. These are all of them:

PropDefaultWhat it does
text"Connect Wallet"Label while disconnected. Once connected the label becomes the short address
bgpurple gradientAny CSS background, including a gradient
color"#fff"Text colour
border"none"Any CSS border
radius12Corner radius in pixels
menutrueDropdown with copy address and disconnect. With it off, clicking disconnects directly
classNamenoneYour own class, for Tailwind or a design system
stylenoneInline styles, merged last so they win over every prop above

Match your brand. A solid fill, square corners and your own label:

<ConnectButton
  text="Sign in"
  bg="#111318"
  color="#ffffff"
  border="1px solid #333333"
  radius={8}
/>

A ghost button. Transparent with an outline, and no dropdown, so a second click disconnects:

<ConnectButton
  text="Connect"
  bg="transparent"
  color="#A6A6FF"
  border="1px solid #A6A6FF"
  menu={false}
/>

Tailwind, or your own design system. Pass a class and neutralise the built in look so your utilities are not fighting inline styles:

<ConnectButton
  className="px-5 py-2.5 rounded-full font-semibold bg-indigo-600 hover:bg-indigo-500 transition"
  bg="transparent"
  border="none"
  radius={0}
/>

The style prop is merged after everything else, so it is the reliable way to override one specific thing:

<ConnectButton style={{ width: '100%', padding: '14px 0', fontSize: 16 }} />

The modal itself is not themeable. It is deliberately identical across every Asentum dapp so users recognise it. If you need a different look entirely, build your own trigger and flow with the hooks below.

Your own connect button

If you would rather use your own markup, open the modal yourself with openConnect from useWallet. In that case you must mount <ConnectModal /> once inside the provider, because the modal is normally rendered by the button you are replacing. Forget it and clicking your button appears to do nothing.

import { ConnectModal, useWallet } from '@asentum/connect';

function Header() {
  const { connected, ase1Address, openConnect, disconnect } = useWallet();
  return (
    <>
      {connected
        ? <button onClick={disconnect}>{ase1Address.slice(0, 10)}…</button>
        : <button onClick={openConnect}>Connect</button>}
      <ConnectModal />
    </>
  );
}

Hooks

All four hooks must be called inside the provider.

HookReturns
useWallet()ase1Address (show this to users), address (internal, for calls), connected, connecting, error, hasWallet, connect, disconnect, openConnect
useAsentum()The AsentumClient: view, call, transfer, deploy, balanceOf, waitReceipt
useContract(addr)view(method, args) and call(method, args, value) bound to one contract
useBalance(addr?)balance, loading, refresh. Native ASE of the connected wallet by default

useBalance returns a wei decimal string and refreshes every twelve seconds. Pass a second argument to change the interval, and call refresh() to update it immediately after a transaction lands.

Reading and writing contracts

A read is free, needs no wallet, and returns the value. A write returns a transaction hash as soon as the user approves it, and you wait for the receipt to know it landed.

import { useAsentum, useContract, useWallet } from '@asentum/connect';

function Swap() {
  const client = useAsentum();
  const dex = useContract('ase1…yourContract');
  const { connected, openConnect } = useWallet();

  async function trade(amountIn) {
    if (!connected) return openConnect();

    // Read: free, no wallet, no approval prompt.
    const quote = await dex.view('quote', [amountIn]);

    // Write: the wallet prompts the user to sign.
    const txHash = await dex.call('swapExactIn', [amountIn, quote], '0');

    // Wait for it to be included.
    const receipt = await client.waitReceipt(txHash);
    if (!receipt.success) throw new Error('swap reverted');
  }
}

The third argument to call is the native ASE sent with the call, in wei, as a string. Leave it off for calls that send nothing. To send ASE without calling a contract, use client.transfer(to, amount).

Provider options

PropMeaning
rpcPublic RPC used for reads. Defaults to https://testnet.asentum.com
dappNameName shown to the user in the wallet approval prompt
telegramBotBot username without the @. Adds the Telegram option to the modal
onCreateWalletCallback that runs your sign up flow. Adds the create new option
onConnectCalled with the address once a wallet connects
onDisconnectCalled when the user disconnects
persistRemember the connected address across page loads. On by default
extensionStatus"ready" by default. Pass "soon" to show the extension option disabled
botApiTelegram wallet API base. Defaults to https://wallet.asentum.com

Without React

AsentumClient is a plain class and can be used on its own, in a script or on a server, for anything read only.

import { AsentumClient } from '@asentum/connect';

const client = new AsentumClient({ rpc: 'https://testnet.asentum.com' });
const total = await client.view('ase1…yourContract', 'totalSupply', []);
const balance = await client.balanceOf('ase1…');

Writes still need a signer, so they only work in a browser where the extension is present or a Telegram session has been paired. For server side signing use the @asentum/sdk instead, which holds a key directly.

A complete example

One file, everything wired: the provider, a branded button, a live balance, a contract read on load, and a signed write with the receipt waited on and errors surfaced. This is a Next.js client component, and the same code works in Vite or Create React App without the first line.

'use client';

import { useEffect, useState } from 'react';
import {
  AsentumProvider,
  ConnectButton,
  useAsentum,
  useBalance,
  useContract,
  useWallet,
} from '@asentum/connect';

const COUNTER = 'ase1…yourContract';

function Counter() {
  const client = useAsentum();
  const counter = useContract(COUNTER);
  const { connected, ase1Address, openConnect } = useWallet();
  const { balance, refresh } = useBalance();

  const [count, setCount] = useState(null);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState(null);

  // Reads are free and need no wallet, so this runs for every visitor.
  useEffect(() => {
    counter.view('get', []).then(setCount).catch(() => setCount(null));
  }, []);

  async function increment() {
    if (!connected) return openConnect();
    setError(null);
    setBusy(true);
    try {
      const txHash = await counter.call('increment', []);
      const receipt = await client.waitReceipt(txHash);
      if (!receipt.success) throw new Error('the transaction reverted');
      setCount(await counter.view('get', []));
      refresh();
    } catch (err) {
      // The user rejecting the prompt lands here too.
      setError(err.message || String(err));
    } finally {
      setBusy(false);
    }
  }

  const ase = balance ? (Number(balance) / 1e18).toFixed(4) : null;

  return (
    <main style={{ padding: 32, fontFamily: 'system-ui' }}>
      <header style={{ display: 'flex', justifyContent: 'space-between' }}>
        <h1>Counter</h1>
        <ConnectButton text="Sign in" bg="#111318" border="1px solid #333" radius={8} />
      </header>

      <p>Count: {count ?? 'loading…'}</p>
      {connected && <p>{ase1Address} holds {ase} ASE</p>}

      <button onClick={increment} disabled={busy}>
        {busy ? 'Confirming…' : connected ? 'Increment' : 'Connect to increment'}
      </button>

      {error && <p style={{ color: '#ff9db0' }}>{error}</p>}
    </main>
  );
}

export default function Page() {
  return (
    <AsentumProvider
      rpc="https://testnet.asentum.com"
      dappName="Counter Demo"
      telegramBot="AsentumWalletBot"
      onConnect={(addr) => console.log('connected', addr)}
    >
      <Counter />
    </AsentumProvider>
  );
}

Three things in there are worth copying into any dapp. Read on load so visitors see real data before they connect. Send anyone who is not connected to openConnect rather than disabling your buttons. Always wait for the receipt and check success, because a transaction hash only means the user signed, not that it worked.

Troubleshooting

  • Clicking my own button does nothing. You are using a custom trigger without mounting <ConnectModal />. See Your own connect button.
  • A hook throws about missing context. The component is outside AsentumProvider. In Next.js this usually means the provider is in a server component.
  • Writes fail but reads work. Reads go to the RPC and writes need a wallet. Check connected before calling a write, and send the user to openConnect if they are not.
  • The extension option looks disabled. You are on an older release, or extensionStatus is set to "soon". Upgrade to the current version and remove the prop.
  • The balance never changes after a transaction. It polls every twelve seconds. Call refresh() once the receipt lands.

The package is MIT licensed and the source is on GitHub. Once a wallet is connected, the frontend tutorial walks through building a full dapp against your contracts.