From cardano-dev-skills
Guides integrating a Cardano wallet into a web dApp using CIP-30. Covers wallet connection, state reading, transaction signing, and governance with Mesh SDK or raw CIP-30.
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/integration/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
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 supporting CIP-95: Eternl, Lace, Flint, Vespr, Typhon.
| 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/integration/connect-wallet/references/cip30-api-reference.md -- Full CIP-30 API referencenpx claudepluginhub cardano-foundation/cardano-dev-skills --plugin cardano-dev-skillsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.