Cardano Wallet As A Service

Ideal for both client and server-side applications, or any JavaScript/TypeScript environment where you need programmatic wallet access.

💡
TLDR: Minimal required code to integrate wallet as a service
import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
 
// Configure UTXOS wallet options
const options: EnableWeb3WalletOptions = {
  networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: preprod, 1: mainnet
  projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID, // https://utxos.dev/dashboard
};
 
// Enable the wallet
const wallet = await Web3Wallet.enable(options);

1. Install Dependencies

Install the required packages:

npm install @meshsdk/web3-sdk @meshsdk/provider

2. Set Up Environment Variables

Edit your .env file with your project id, blockchain provider api key, and the network:

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id # https://utxos.dev/dashboard
BLOCKFROST_API_KEY_PREPROD=your_blockfrost_api_key # https://blockfrost.io/dashboard
NEXT_PUBLIC_NETWORK_ID=0  # 0 for preprod, 1 for mainnet

3. Blockchain Data Provider

This is boilerplate to work with Cardano and not specific to UTXOS usage

  1. Get a free API key from Blockfrost or any another supported provider
  2. Edit your .env file to expose new BLOCKFROST_API_KEY to the server
  3. Find out how to setup blockfrost

4. Initialize The Wallet

Create a wallet instance with proper error handling:

import { EnableWeb3WalletOptions, Web3Wallet } from "@meshsdk/web3-sdk";
import { BlockfrostProvider } from "@meshsdk/provider";
 
async function initializeWallet() {
  try {
    // Initialize the blockchain data provider with secure api endpoint
    // const provider = new BlockfrostProvider(process.env.BLOCKFROST_API_KEY_PREPROD); // quick start method (insecure)
    const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
 
    // Configure UTXOS wallet options
    const options: EnableWeb3WalletOptions = {
      networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID) || 0, // 0: preprod, 1: mainnet
      projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID, // https://utxos.dev/dashboard
      fetcher: provider,
      submitter: provider,
    };
 
    // Enable the wallet
    const wallet = await Web3Wallet.enable(options);
    return wallet;
  } catch (error) {
    console.error("Failed to initialize wallet:", error);
    throw error;
  }
}
 
// Usage
const wallet = await initializeWallet();

5. Verify Wallet Connection

Test that your wallet is working correctly, for example by fetching the wallet address:

const address = await wallet.cardano.getChangeAddress();
console.log("Change Address:", address);
 
const utxos = await wallet.cardano.getUtxos();
console.log("UTXOs:", utxos);

Wallet API Reference

The wallet provides a comprehensive API for blockchain interactions. See all available methods in the SDK documentation.

Here are some common operations you can perform with the wallet:

Address Management

// Get change address for transaction outputs
const changeAddress = await wallet.cardano.getChangeAddress();
 
// Get wallet addresses
const addresses = await wallet.cardano.getUsedAddresses();
 
// Get unused addresses for receiving funds
const unusedAddresses = await wallet.cardano.getUnusedAddresses();

UTXO And Assets

// Get all UTXOs for the wallet
const utxos = await wallet.cardano.getUtxos();
 
// Get collateral UTXOs
const collateral = await wallet.cardano.getCollateral();

Sign Operations

// Sign a transaction
const signedTx = await wallet.cardano.signTx(
  unsignedTx,
);
 
// Sign arbitrary data (for authentication/verification)
const signature = await wallet.cardano.signData(address, message);
// Submit a signed transaction to the Cardano network
const txHash = await wallet.cardano.submitTx(signedTx);

Asset Information

// Get wallet balance (including native assets)
const balance = await wallet.cardano.getBalance();
 
// Get specific asset balance
const assetBalance = await wallet.cardano.getAssets();
 
// Get policy IDs of assets in wallet
const policyIds = await wallet.cardano.getPolicyIds();
 
// Get assets from a specific policy ID
const assets = await wallet.cardano.getPolicyIdAssets("asset_policy_id");

Wallet Export

Allow user to view their private keys for backup or migration purposes:

// Export wallet data
await wallet.exportWallet("cardano");

Wallet Disable

// Logs user out of their JWT
await wallet.disable();

Cache wallet

It is common to first connect to and store a users wallet, making it globally accessible in frontend applications. The preliminary “connect wallet” step used by many today.

Connect, store, and pass around

// connect, close window, and store
const wallet = await initializeWallet();
const MyContext = createContext(wallet);
 
// build transactions here
 
// open window to sign
const wallet = useContext(MyContext);
const signedTx = await wallet.cardano.signData("unsigned-data");

Keep Window Open

It’s also possible to flow directly from unauthenticated -> sign tx in a single iframe, reducing the need to first connect. Simply add keepWindowOpen to the enable options and immediately use Web3Wallet in pure functions.

// step 1: enable, do things, and sign
const wallet = await Web3Wallet.enable({
    networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID)
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    keepWindowOpen: true // default = false
  });
// iframe will stay open while we perform async operations expecting a follow up operation.
const signedData = await wallet.cardano.signData('unsigned-data');

Hello world example

Here’s a complete example of sending ADA from the wallet:

import { Web3Wallet, EnableWeb3WalletOptions } from "@meshsdk/web3-sdk";
import { BlockfrostProvider, MeshTxBuilder } from "@meshsdk/core";
 
async function enableWallet(){
  const provider = new BlockfrostProvider(`/api/blockfrost/preprod/`);
 
  const options: EnableWeb3WalletOptions = {
    projectId: process.env.NEXT_PUBLIC_UTXOS_PROJECT_ID,
    networkId: parseInt(process.env.NEXT_PUBLIC_NETWORK_ID),
    fetcher: provider,
    submitter: provider,
  };
 
  const wallet = await Web3Wallet.enable(options);
  return wallet;
}
 
async function sendADA(wallet, recipientAddress, amountADA) {
  try {
    // Build transaction
    const tx = new MeshTxBuilder({
      fetcher: provider,
    });
 
    const amountLovelace = (amountADA * 1_000_000).toString();
 
    // Build the asset unit (policy ID + asset name)
    const assetUnit = policyId + assetName;
 
    tx.txOut(recipientAddress, [
      { unit: "lovelace", quantity: amountLovelace }
    ])
    tx.txOut(recipientAddress, [
      { unit: "lovelace", quantity: "1500000" },
      { unit: assetUnit, quantity: "1" }, // NFT
    ])
    .changeAddress(await wallet.cardano.getChangeAddress())
    .selectUtxosFrom(await wallet.cardano.getUtxos());
 
    // Complete, sign, and submit
    const unsignedTx = await tx.complete();
 
    const signedTx = await wallet.cardano.signTx(unsignedTx);
 
    const txHash = await wallet.cardano.submitTx(signedTx);
    console.log("Transaction submitted:", txHash);
 
    return txHash;
  } catch (error) {
    console.error("Transaction failed:", error);
    throw error;
  }
}
 
 
// Usage: button onclick to execute each function
const wallet = await enableWallet();
const txHash = await sendADA(wallet, "addr_test1...", 5); // Send 5 ADA

Security Considerations

Never expose your API keys in client-side code

Always use server-side environment variables and proxy requests through secure API routes to keep your keys safe.

Set Up Blockchain Data Provider on server-side

This is boilerplate to work with Cardano and not specific to UTXOS usage

Edit your .env file to expose new BLOCKFROST_API_KEY to the server

# .env
NEXT_PUBLIC_UTXOS_PROJECT_ID=your_project_id
BLOCKFROST_API_KEY_PREPROD=your_blockfrost_api_key # never expose on client
  1. We need a safe way to consume BLOCKFROST_API_KEY without exposing it’s value. The solution varies slightly for your environment. In Next.js (App directory) we create a new file called app/api/blockfrost/[...slug]/route.ts and paste in this function.
async function handleBlockfrostRequest(
  request,
  context,
) {
  try {
    const { params } = context;
    const slug = params.slug || [];
    const network = slug[0];
 
    // Network configuration
    const networkConfig = getNetworkConfig(network);
    if (!networkConfig.key) {
      return {
        status: 500,
        headers: { "Content-Type": "application/json" },
        body: { error: `Missing Blockfrost API key for network: ${network}` },
      };
    }
 
    // Construct endpoint
    const endpointPath = slug.slice(1).join("/") || "";
    const queryString = getQueryString(request.url);
    const endpoint = endpointPath + queryString;
 
    // Set headers
    const headers = {
      project_id: networkConfig.key,
    };
 
    if (endpointPath === "tx/submit" || endpointPath === "utils/txs/evaluate") {
      headers["Content-Type"] = "application/cbor";
    } else {
      headers["Content-Type"] = "application/json";
    }
 
    // Forward request to Blockfrost
    const url = `${networkConfig.baseUrl}/${endpoint}`;
    const blockfrostResponse = await fetch(url, {
      method: request.method,
      headers,
      body: request.method !== "GET" ? request.body : undefined,
    });
 
    // Handle 404 for UTXOs as empty wallet
    if (blockfrostResponse.status === 404 && endpointPath.includes("/utxos")) {
      return {
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: [],
      };
    }
 
    // Handle errors
    if (!blockfrostResponse.ok) {
      const errorBody = await blockfrostResponse.text();
      return {
        status: blockfrostResponse.status,
        headers: { "Content-Type": "application/json" },
        body: {
          error: `Blockfrost API error: ${blockfrostResponse.status} ${blockfrostResponse.statusText}`,
          details: errorBody,
        },
      };
    }
 
    // Handle CBOR endpoints
    if (endpointPath === "utils/txs/evaluate" || endpointPath === "tx/submit") {
      const responseData = await blockfrostResponse.text();
      return {
        status: blockfrostResponse.status,
        headers: { "Content-Type": "application/json" },
        body: responseData,
      };
    }
 
    // Handle JSON responses
    const responseData = await blockfrostResponse.json();
    return {
      status: 200,
      headers: { "Content-Type": "application/json" },
      body: responseData,
    };
  } catch (error: unknown) {
    console.error("Blockfrost API route error:", error);
    const errorMessage = error instanceof Error ? error.message : String(error);
 
    return {
      status: 500,
      headers: { "Content-Type": "application/json" },
      body: { error: errorMessage },
    };
  }
}
 
// Helper functions
function getQueryString(url) {
  const qIndex = url.indexOf("?");
  return qIndex !== -1 ? url.substring(qIndex) : "";
}
 
function getNetworkConfig(network) {
  switch (network) {
    case "mainnet":
      return {
        key: process.env.BLOCKFROST_API_KEY_MAINNET,
        baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
      };
    // add different networks
    default: // preprod
      return {
        key: process.env.BLOCKFROST_API_KEY_PREPROD,
        baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
      };
  }
}
 
// Next.js App router specific exports
export async function GET(
  request,
  { params },
) {
  return createAppRouterHandler(request, params);
}
 
export async function POST(
  request,
  { params },
) {
  return createAppRouterHandler(request, params);
}

Now we can initialize BlockfrostProvider with our hosted api route and keep our credentials secure!