Evolution SDK
Clients

Client architecture & patterns

Frontend/backend architecture and client wiring

Client architecture & patterns

Complete architecture patterns showing proper separation between frontend signing and backend transaction building. This page documents how to wire wallets and providers into clients for frontend and backend applications.

Read-Only Wallets

Read-only wallets observe addresses and build transactions but cannot sign. They enable transaction construction on backends using the user's address without any signing capability.

What they are: Wallets configured with an address but no private keys—can build, cannot sign.

When to use: Backend transaction building in web applications. Server builds unsigned transactions for frontend to sign.

Why they work: Provides transaction builder with proper UTxO selection and fee calculation for a specific address, without security risk of keys on backend.

How to secure: Backend never sees keys. Frontend sends user address, backend builds transaction as that user, frontend signs with actual wallet.

Configuration

interface ReadOnlyWalletConfig {
  : "read-only"
  : string          // Cardano address to observe (required)
  ?: string   // Stake address for rewards (optional)
}

Backend Transaction Building

import { ,  } from "@evolution-sdk/evolution";

// Backend: Create provider client, then attach read-only wallet
const  = ({
  : "mainnet",
  : {
    : "blockfrost",
    : "https://cardano-mainnet.blockfrost.io/api/v0",
    : ..!
  }
});

// Attach user's address as read-only wallet (expects bech32 string)
const  = .({
  : "read-only",
  : "addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"
});

// Build unsigned transaction
const  = .();
.({
  : ..("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"),
  : ..(5_000_000n)
});

// Build returns result, get transaction and serialize
const  = await .();
const  = await .();
const  = ..();

// Return to frontend for signing
// Frontend: wallet.signTx(txCbor) → wallet.submitTx(signedTxCbor)

Frontend: Signing Only

Frontend applications connect to user wallets through CIP-30 but never have provider access. They retrieve addresses, sign transactions built by backends, and submit signed transactions.

Architecture: API wallet client (no provider) → can sign, cannot build.

Workflow:

  1. User connects browser extension wallet
  2. Frontend gets user's address
  3. Send address to backend
  4. Backend builds unsigned transaction
  5. Frontend receives unsigned CBOR
  6. Frontend requests signature from wallet
  7. User approves in wallet interface
  8. Frontend submits signed transaction

Implementation

import { createClient } from "@evolution-sdk/evolution";

// 1. Connect wallet
declare const cardano: any;
const walletApi = await cardano.eternl.enable();

// 2. Create signing-only client
const client = createClient({
  network: "mainnet",
  wallet: { type: "api", api: walletApi }
});

// 3. Get user address for backend
const address = await client.address();

// 4. Send address to backend, receive unsigned tx CBOR
const unsignedTxCbor = await fetch("/api/build-tx", {
  method: "POST",
  body: JSON.stringify({ userAddress: address })
}).then(r => r.json()).then(data => data.txCbor);

// 5. Sign with user wallet
const witnessSet = await client.signTx(unsignedTxCbor);

// 6. Submit
const txHash = await client.submitTx(unsignedTxCbor);
console.log("Transaction submitted:", txHash);

Security: Provider API keys never exposed to frontend. User approves every signature. Keys stay on the user's device. Cannot build transactions without provider access.


Backend: Building Only

Backend services use read-only wallets configured with user addresses to build unsigned transactions. They have provider access for blockchain queries but zero signing capability.

Architecture: Read-only wallet + provider → can build, cannot sign.

Workflow:

  1. Receive user address from frontend
  2. Create read-only wallet with that address
  3. Build transaction with proper UTxO selection
  4. Calculate fees, validate outputs
  5. Return unsigned transaction CBOR
  6. Frontend handles signing and submission

Implementation

import { createClient } from "@evolution-sdk/evolution";

// Backend endpoint
export async function buildTransaction(userAddress: string) {
  // Create read-only client with user's address
  const client = createClient({
    network: "mainnet",
    provider: {
      type: "blockfrost",
      baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
      projectId: process.env.BLOCKFROST_PROJECT_ID!
    },
    wallet: {
      type: "read-only",
      address: userAddress
    }
  });

  // Build unsigned transaction
  const builder = client.newTx();
  builder.payToAddress({
    address: Core.Address.fromBech32("addr1qz8eg0aknl96hd3v6x3qfmmz5zhtrq5hn8hmq0x4qd6m2qdppx88rnw3eumv9zv2ctjns05c8jhsqwg98qaxcz2qh45qhjv39c"),
    assets: Core.Assets.fromLovelace(5_000_000n)
  });

  // Returns unsigned transaction
  const unsignedTx = await builder.build();
  const txCbor = unsignedTx.toCBOR();

  return { txCbor };
}

Security: Backend never sees private keys. Cannot sign even if server is compromised. Provider keys protected on server. Cannot submit transactions without signing from frontend.


Full Flow: Build → Sign → Submit

Complete architecture showing frontend/backend separation with proper security model.

Components:

  • Frontend: API wallet (CIP-30) for signing
  • Backend: Read-only wallet for building
  • Security: Keys stay on user device, provider keys stay on server

Complete Example

// @filename: shared.ts
export type  = { : string };

// @filename: frontend.ts
// ===== Frontend (Browser) =====
import { ,  } from "@evolution-sdk/evolution";
import type {  } from "./shared";

declare const : any;

async function (: string, : bigint) {
  // 1. Connect user wallet
  const  = await .eternl.enable();
  const  = ({
    : "mainnet",
    : { : "api", :  }
  });

  // 2. Get user address (returns Core Address, convert to bech32 for backend)
  const  = ..(await .());

  // 3. Request backend to build transaction
  const  = await ("/api/build-payment", {
    : "POST",
    : { "Content-Type": "application/json" },
    : .({
      ,
      ,
      : .()
    })
  });
  const {  } = await .() as ;

  // 4. Sign with user wallet (prompts user approval)
  const  = await .();

  // 5. Submit signed transaction
  const  = await .();

  return ;
}

// @filename: backend.ts
// ===== Backend (Server) =====
import { ,  } from "@evolution-sdk/evolution";

export async function (
  : string,
  : string,
  : bigint
) {
  // Convert bech32 addresses from frontend to Core Address
  const  = ..();

  // Create provider client first, then attach read-only wallet
  const  = ({
    : "mainnet",
    : {
      : "blockfrost",
      : "https://cardano-mainnet.blockfrost.io/api/v0",
      : ..!
    }
  });

  // Attach user's address as read-only wallet
  const  = .({
    : "read-only",
    : 
  });

  // Build unsigned transaction
  const  = .();
  .({
    : ,
    : ..()
  });

  // Return unsigned CBOR for frontend to sign
  const  = await .();
  const  = await .();
  const  = ..();

  return {  };
}

Why This Works

Why This Works

User keys never leave their device. Provider API keys never exposed to frontend. Backend cannot sign transactions (compromised server results in no fund loss). Frontend cannot build transactions alone (no provider access). Both components required for complete transactions. Clean separation of concerns provides scalable architecture.


Method Matrix

MethodSeedPrivate KeyAPI WalletRead-Only
address()YesYesYesYes
rewardAddress()YesYesYesYes
newTx()YesYesNoYes
sign()YesYesYesNo
signTx()YesYesYesNo
submitTx()YesYesYesYes

Common Mistakes

Error: Frontend trying to build:

// WRONG - Frontend has no provider
const client = createClient({
  network: "mainnet",
  wallet: { type: "api", api: walletApi }
  // No provider!
});

const builder = client.newTx(); // Error: Cannot build without provider

Correct - Frontend signs only:

// CORRECT - Frontend signs, backend builds
const client = createClient({
  network: "mainnet",
  wallet: { type: "api", api: walletApi }
});

// Get tx from backend, then sign
const witnessSet = await client.signTx(txCborFromBackend);

Error: Backend trying to sign:

// WRONG - Backend has no private keys
const client = createClient({
  network: "mainnet",
  provider: { /* ... */ },
  wallet: {
    type: "read-only",
    address: userAddress
  }
});

await client.sign(); // Error: Cannot sign with read-only wallet

Correct - Backend builds only:

// CORRECT - Backend builds, frontend signs
const client = createClient({
  network: "mainnet",
    provider: { /* ... */ },
  wallet: {
    type: "read-only",
    address: userAddress
  }
});

const unsignedTx = await builder.build(); // Returns unsigned CBOR

Next Steps