From cardano-dev-skills
Guides integrating Cardano browser wallets into web dApps using the CIP-30 standard. Handles wallet discovery, connection, state reading, and transaction signing.
How this skill is triggered — by the user, by Claude, or both
Slash command
/cardano-dev-skills:connect-walletThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
<!-- Documentation lookup path: ${CLAUDE_SKILL_DIR}/../../docs/sources/ -->
Help the developer integrate a Cardano browser wallet into their web application using the CIP-30 standard.
suggest-tooling skill)governance-guide skill)Ask the developer (if not already clear):
Search the bundled documentation for relevant content:
${CLAUDE_SKILL_DIR}/../../docs/sources/mesh-sdk/ - Mesh SDK docs${CLAUDE_SKILL_DIR}/../../docs/sources/evolution-sdk/ - Evolution SDK docs${CLAUDE_SKILL_DIR}/../../docs/sources/cips/ - CIP specifications (CIP-30, CIP-95)Reference the CIP-30 API reference for the full specification:
File: skills/connect-wallet/references/cip30-api-reference.md
window.cardano.<walletName>enable() method that requests user permission// List all available Cardano wallets
const availableWallets = [];
for (const key in window.cardano) {
if (window.cardano[key]?.enable && window.cardano[key]?.name) {
availableWallets.push({
id: key,
name: window.cardano[key].name,
icon: window.cardano[key].icon,
});
}
}
npm install @meshsdk/react @meshsdk/core
// App.tsx - Wrap with MeshProvider
import { MeshProvider } from "@meshsdk/react";
function App() {
return (
<MeshProvider>
<MyDApp />
</MeshProvider>
);
}
// WalletConnect.tsx
import { CardanoWallet, useWallet } from "@meshsdk/react";
function WalletConnect() {
const { connected, wallet } = useWallet();
const readBalance = async () => {
if (!connected) return;
const balance = await wallet.getBalance();
console.log("Balance:", balance);
};
return (
<div>
<CardanoWallet />
{connected && <button onClick={readBalance}>Get Balance</button>}
</div>
);
}
npm install @meshsdk/react @meshsdk/core
Important: CIP-30 requires window, so wallet code must be client-side only.
// components/WalletButton.tsx
"use client";
import dynamic from "next/dynamic";
const CardanoWallet = dynamic(
() => import("@meshsdk/react").then((mod) => mod.CardanoWallet),
{ ssr: false }
);
export default function WalletButton() {
return <CardanoWallet />;
}
// app/layout.tsx or _app.tsx
"use client";
import { MeshProvider } from "@meshsdk/react";
export default function Layout({ children }) {
return <MeshProvider>{children}</MeshProvider>;
}
<script>
let wallet = null;
let connected = false;
let availableWallets = [];
import { onMount } from "svelte";
onMount(() => {
// Discover wallets after DOM load
for (const key in window.cardano) {
if (window.cardano[key]?.enable) {
availableWallets.push(key);
}
}
availableWallets = availableWallets;
});
async function connect(walletName) {
try {
wallet = await window.cardano[walletName].enable();
connected = true;
} catch (e) {
console.error("Connection refused:", e);
}
}
</script>
{#each availableWallets as w}
<button on:click={() => connect(w)}>{w}</button>
{/each}
<template>
<div>
<button v-for="w in wallets" :key="w" @click="connect(w)">
{{ w }}
</button>
<p v-if="connected">Connected!</p>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const wallets = ref([]);
const wallet = ref(null);
const connected = ref(false);
onMounted(() => {
for (const key in window.cardano) {
if (window.cardano[key]?.enable) {
wallets.value.push(key);
}
}
});
async function connect(name) {
try {
wallet.value = await window.cardano[name].enable();
connected.value = true;
} catch (e) {
console.error("Connection refused:", e);
}
}
</script>
<div id="wallet-buttons"></div>
<script>
window.addEventListener("load", () => {
const container = document.getElementById("wallet-buttons");
for (const key in window.cardano) {
if (window.cardano[key]?.enable) {
const btn = document.createElement("button");
btn.textContent = window.cardano[key].name || key;
btn.onclick = async () => {
try {
const api = await window.cardano[key].enable();
console.log("Connected:", api);
// Use api.getBalance(), api.getUtxos(), etc.
} catch (e) {
console.error("Refused:", e);
}
};
container.appendChild(btn);
}
}
});
</script>
Once connected (via SDK or raw API):
// Using raw CIP-30 API object
const networkId = await api.getNetworkId(); // 0 = testnet, 1 = mainnet
const balance = await api.getBalance(); // CBOR-encoded Value
const utxos = await api.getUtxos(); // Array of CBOR-encoded UTxOs
const usedAddresses = await api.getUsedAddresses(); // Array of CBOR addresses
const changeAddress = await api.getChangeAddress(); // CBOR address
// Using Mesh SDK (already decoded)
const balance = await wallet.getBalance(); // Array of { unit, quantity }
const utxos = await wallet.getUtxos(); // Decoded UTxO objects
const addresses = await wallet.getUsedAddresses(); // Bech32 addresses
Note: these Mesh examples target the released
@meshsdk/*1.9.x (npmlatest). Mesh 2.0 (currently beta-only) renames the wallet API (MeshCardanoBrowserWallet,getBalanceMesh(),getChangeAddressBech32(), …) — don't mix the two API generations.
import { MeshTxBuilder, MeshWallet } from "@meshsdk/core";
const txBuilder = new MeshTxBuilder({ fetcher: provider });
// Simple ADA transfer
const tx = await txBuilder
.txOut(recipientAddress, [{ unit: "lovelace", quantity: "5000000" }])
.changeAddress(await wallet.getChangeAddress())
.selectUtxosFrom(await wallet.getUtxos())
.complete();
const signedTx = await wallet.signTx(tx);
const txHash = await wallet.submitTx(signedTx);
Evolution SDK's CIP-30 client is signing-only by design — it carries no provider, so the browser cannot build or submit transactions itself. A provider-backed backend builds the unsigned transaction and submits the signed one; the wallet only signs. (See wallets/api-wallet.mdx in the bundled docs.)
import { Client, Transaction, TransactionWitnessSet, mainnet } from "@evolution-sdk/evolution";
// Connect the browser wallet, then create a signing-only client (no provider).
const walletApi = await window.cardano.eternl.enable();
const client = Client.make(mainnet).withCip30(walletApi);
// `unsignedTxCbor` comes from your backend's provider-backed transaction builder.
const witnessSet = await client.signTx(unsignedTxCbor); // prompts the user
const signedTxCbor = Transaction.addVKeyWitnessesHex(
unsignedTxCbor,
TransactionWitnessSet.toCBORHex(witnessSet),
);
// POST signedTxCbor back to the backend for provider-backed submission.
// Build tx using any serialization library, get CBOR hex
const unsignedTxCbor = buildTransaction(/* ... */);
// Sign via wallet
const witnessSetCbor = await api.signTx(unsignedTxCbor, false);
// Assemble and submit
const signedTxCbor = assembleTx(unsignedTxCbor, witnessSetCbor);
const txHash = await api.submitTx(signedTxCbor);
Wallets can sign arbitrary data — not just transactions — to prove the user controls an address. This backs "sign in with wallet" logins, attestations, and off-chain authorization. No transaction, no fee.
// Raw CIP-30 — returns a COSE_Sign1 signature + key
const { signature, key } = await api.signData(addressHex, payloadHex);
Evolution SDK exposes this as client.signMessage(payload) on a CIP-30 client, and ships COSE.SignData.verifyData(...) to verify a signature server-side (see wallets/message-signing.mdx in the bundled docs). Always verify server-side — a signature proves key ownership only once you check it against the claimed address.
For dApps that need governance features (DRep registration, voting, delegation):
// Enable with CIP-95 extensions
const api = await window.cardano[walletName].enable({
extensions: [{ cip: 95 }]
});
// CIP-95 methods (if supported by wallet)
const pubDRepKey = await api.cip95.getPubDRepKey();
const registeredPubStakeKeys = await api.cip95.getRegisteredPubStakeKeys();
const unregisteredPubStakeKeys = await api.cip95.getUnregisteredPubStakeKeys();
Not all wallets support CIP-95 yet. Check wallet compatibility before relying on it. Wallets listed as CIP-95 implementors in the spec: Eternl, GeroWallet, Lace, NuFi, Typhon, Vespr, Yoroi — but verify against the spec's Implementors list (docs/sources/cips/CIP-0095/README.md) rather than trusting a static list.
| Issue | Cause | Solution |
|---|---|---|
window.cardano is undefined | No wallet installed, or SSR | Check for window existence; use dynamic imports in Next.js |
enable() throws error | User rejected connection | Show friendly message, allow retry |
| Wrong network | Wallet on different network | Check getNetworkId() and show warning |
signTx() fails | Invalid transaction CBOR | Validate tx before sending to wallet; check collateral |
| UTxOs empty after tx | Node not synced | Wait a block, re-query; some providers lag |
| Multiple wallets conflict | Wallet detection order | Let user explicitly choose which wallet |
| CORS errors | API proxy needed | Use backend proxy for chain data; wallet calls are local |
| Balance shows CBOR | Using raw CIP-30 | Decode CBOR with a serialization library or use an SDK |
submitTx() fails | Insufficient funds, bad fee | Build tx with proper fee estimation; ensure enough UTxOs |
| Wallet not detected on page load | Extension loads asynchronously | Add a short delay or poll for window.cardano |
skills/connect-wallet/references/cip30-api-reference.md -- Full CIP-30 API referencenpx claudepluginhub cardano-foundation/cardano-dev-skills --plugin cardano-dev-skillsBuilds Cardano transactions: send ADA, mint NFTs/tokens, interact with smart contracts, delegate stake, register DReps, and vote on-chain using Mesh SDK, Evolution SDK, PyCardano, or cardano-client-lib.
Guides Web3 wallet integration for DApps: wallet connectivity, transaction signing, account management, and blockchain UX patterns. Activates on wallet-related queries.
Builds Stellar dApp frontends: wallet connection (Freighter, Stellar Wallets Kit), transaction building/signing/submission, smart contract invocation, passkey smart accounts. For React/Next.js/Node.js apps.