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/connectReact 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.
The connect modal
The modal shows the Asentum browser extension by default. The other two options are opt in, so you decide how many ways into your dapp you want to offer:
- Browser extension. Always shown. If the visitor does not have it installed, the modal links them to the download.
- Telegram wallet. Shown when you pass
telegramBot. The user is given a six digit code to paste to the bot, and the modal waits for them to approve the pairing. - Create a new wallet. Shown when you pass
onCreateWallet, which runs your own sign up flow.
<AsentumProvider
rpc="https://testnet.asentum.com"
telegramBot="AsentumWalletBot"
onCreateWallet={() => router.push('/signup')}
>Customising the button
Every visual choice is a prop, so the button can match your design without a stylesheet. These are all of them:
| Prop | Default | What it does |
|---|---|---|
| text | "Connect Wallet" | Label while disconnected. Once connected the label becomes the short address |
| bg | purple gradient | Any CSS background, including a gradient |
| color | "#fff" | Text colour |
| border | "none" | Any CSS border |
| radius | 12 | Corner radius in pixels |
| menu | true | Dropdown with copy address and disconnect. With it off, clicking disconnects directly |
| className | none | Your own class, for Tailwind or a design system |
| style | none | Inline 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.
| Hook | Returns |
|---|---|
| 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
| Prop | Meaning |
|---|---|
| rpc | Public RPC used for reads. Defaults to https://testnet.asentum.com |
| dappName | Name shown to the user in the wallet approval prompt |
| telegramBot | Bot username without the @. Adds the Telegram option to the modal |
| onCreateWallet | Callback that runs your sign up flow. Adds the create new option |
| onConnect | Called with the address once a wallet connects |
| onDisconnect | Called when the user disconnects |
| persist | Remember the connected address across page loads. On by default |
| extensionStatus | "ready" by default. Pass "soon" to show the extension option disabled |
| botApi | Telegram 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
connectedbefore calling a write, and send the user toopenConnectif they are not. - The extension option looks disabled. You are on an older release, or
extensionStatusis 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.