Skip to main content

Cardano

EffectStream connects to Cardano to leverage its UTXO model and native assets. Syncing is typically handled via high-performance indexers.

1. Configuration (Read)

Network Definition

.buildNetworks(builder =>
builder.addNetwork({
name: "cardano",
type: ConfigNetworkType.CARDANO,
network: "yaci", // or "preview", "mainnet"
nodeUrl: "http://127.0.0.1:10000", // e.g. Yaci Devkit
})
)

Sync Protocol

EffectStream supports syncing via UTXO-RPC (using Dolos/Yaci) or Carp.

// Example using UTXO-RPC (Dolos)
.addParallel(
(networks) => networks.cardano,
(network, deployments) => ({
name: "parallelUtxoRpc",
type: ConfigSyncProtocolType.CARDANO_UTXORPC_PARALLEL,
rpcUrl: "http://127.0.0.1:50051", // UTXO-RPC endpoint
startChainPoint: { slot: 1, hash: "abc123..." }, // slot and block hash to start syncing from
})
)

Primitives

All Cardano primitives use Dolos via the UTxORPC Watch module for real-time transaction streaming. Each primitive specifies a UTxORPC predicate that filters transactions at the source, so only relevant data reaches your application.

import {
PrimitiveTypeCardanoDelayedAsset,
PrimitiveTypeCardanoMintBurn,
PrimitiveTypeCardanoPoolDelegation,
PrimitiveTypeCardanoProjectedNFT,
PrimitiveTypeCardanoTransfer,
} from "@effectstream/sm/builtin";

Delayed Asset (PrimitiveTypeCardanoDelayedAsset)

Tracks the creation and spending of native asset UTxOs. "Delayed" refers to asset ownership only becoming final after on-chain confirmation (unlike account-model chains with instant balance updates). The primitive uses the moves_asset UTxORPC predicate filtered by policy ID.

Maintains an IVM materialized view (cardano_asset_utxos_view_<name>) with current unspent asset holdings — INSERTs on UTxO creation, DELETEs on spending.

.buildPrimitives(builder =>
builder.addPrimitive(
(sp) => sp.parallelUtxoRpc,
() => ({
name: "MyNativeAsset",
type: PrimitiveTypeCardanoDelayedAsset,
startBlockHeight: 1,
stateMachinePrefix: "cardano-asset",
policyIds: ["abcdef1234..."], // filter by policy ID
network: "yaci",
}),
)
)

Payload fields:

FieldDescription
addressRecipient/owner address
txIdTransaction hash
outputIndexOutput position in the transaction
cip14FingerprintCIP-14 asset fingerprint (policy ID + asset name)
amountQuantity (null signals the UTxO was spent)
policyIdNative asset policy ID
assetNameAsset name (hex-encoded)

Mint/Burn (PrimitiveTypeCardanoMintBurn)

Captures native token minting and burning events. Positive quantities indicate mints; negative indicate burns. Uses the mints_asset UTxORPC predicate filtered by policy ID.

.buildPrimitives(builder =>
builder.addPrimitive(
(sp) => sp.parallelUtxoRpc,
() => ({
name: "MyTokenMints",
type: PrimitiveTypeCardanoMintBurn,
startBlockHeight: 1,
stateMachinePrefix: "cardano-mint",
policyIds: ["abcdef1234..."],
network: "yaci",
}),
)
)

Payload fields:

FieldDescription
txIdTransaction hash
metadataJSON-stringified transaction metadata
assetsJSON array of {amount, policyId, assetName}
inputAddressesJSON array of addresses that signed inputs
outputAddressesJSON array of recipient addresses

Pool Delegation (PrimitiveTypeCardanoPoolDelegation)

Tracks stake pool delegation changes. Monitors both pre-Conway (stakeDelegation) and Conway-era (stakeRegDelegCert, stakeVoteDelegCert) delegation certificates using the has_certificate UTxORPC predicate. Supports an optional pools allowlist to filter by specific stake pool key hashes.

Maintains an IVM materialized view (cardano_pool_delegation_view_<name>) with the current delegation per staking credential — UPSERTs on each delegation change.

.buildPrimitives(builder =>
builder.addPrimitive(
(sp) => sp.parallelUtxoRpc,
() => ({
name: "CardanoPoolDelegation",
type: PrimitiveTypeCardanoPoolDelegation,
startBlockHeight: 1,
stateMachinePrefix: "cardano-pool-delegation",
pools: ["7301761068762f..."], // optional: only watch these pools
network: "yaci",
}),
)
)

Payload fields:

FieldDescription
addressStaking credential hash
poolTarget stake pool key hash
epochNetwork epoch at delegation time

See the cardano-delegation template for a complete working example including a React frontend.

Projected NFT (PrimitiveTypeCardanoProjectedNFT)

Tracks the lock → unlock → claim lifecycle of NFTs locked at a Plutus script (hololocker). This enables "projecting" on-chain NFTs into off-chain game state while they remain provably locked on-chain. Uses the has_address UTxORPC predicate filtered by the script address.

Maintains an IVM materialized view (cardano_projected_nft_view_<name>) — UPSERTs on Lock/Unlocking transitions, DELETEs on Claims.

.buildPrimitives(builder =>
builder.addPrimitive(
(sp) => sp.parallelUtxoRpc,
() => ({
name: "GameNFTLocker",
type: PrimitiveTypeCardanoProjectedNFT,
startBlockHeight: 1,
stateMachinePrefix: "cardano-projected-nft",
scriptHash: "abc123...", // hololocker script hash
network: "yaci",
}),
)
)

Payload fields:

FieldDescription
ownerAddressOwner hash parsed from datum (PKH/NFT/Receipt)
previousTxIdPrevious transaction hash (for state transitions)
previousOutputIndexPrevious output index
currentTxIdCurrent transaction hash
currentOutputIndexCurrent output index
policyIdNative asset policy ID
assetNameNative asset name
statusLock, Unlocking, or Claim
forHowLongTime-lock expiry (for Unlocking status)

Transfer (PrimitiveTypeCardanoTransfer)

Captures ADA and native asset transfers. Tracks outputs (destination UTxOs) and input credentials (payment authorization). The UTxORPC predicate is user-provided, typically has_address to watch specific addresses.

.buildPrimitives(builder =>
builder.addPrimitive(
(sp) => sp.parallelUtxoRpc,
() => ({
name: "WatchMyAddress",
type: PrimitiveTypeCardanoTransfer,
startBlockHeight: 1,
stateMachinePrefix: "cardano-transfer",
predicate: {
match: { cardano: { has_address: { exact_address: "addr_test1..." } } },
},
network: "yaci",
}),
)
)

Payload fields:

FieldDescription
txIdTransaction hash
metadataJSON-stringified transaction metadata
inputCredentialsJSON array of verification key hashes that signed inputs
outputsJSON array of {index, address, coin, assets[]}

2. Batcher Adapters (Write)

To write to Cardano, you must implement a BlockchainAdapter that constructs Cardano transactions.

Since Cardano uses a UTXO model, the adapter typically needs to:

  1. Query available UTXOs for the batcher wallet.
  2. Construct a transaction using a library like lucid or mesh.
  3. Sign and submit via the node or an API like Ogmios.
// Conceptual Custom Adapter
class CardanoAdapter implements BlockchainAdapter {
async submitBatch(data: any) {
// 1. Build TX with data embedded in Metadata or Datum
// 2. Sign with stored private key
// 3. Submit
}
}

3. Browser Wallets (Connect)

Use WalletMode.Cardano to connect to Cardano wallets (Nami, Eternl, Flint, etc.).

import { walletLogin, WalletMode } from "@effectstream/wallets";

const result = await walletLogin({
mode: WalletMode.Cardano,
// Optional: prefer a specific wallet extension
preference: { name: "nami" },
});

if (result.success) {
const wallet = result.result;
console.log("Connected Cardano Address:", wallet.walletAddress);
}

4. Cryptography (Verify)

EffectStream includes the logic to verify CIP-30/CIP-8 Data Signatures. This allows you to authenticate user actions signed by Cardano wallets.

Signing Messages

import { signMessage } from "@effectstream/wallets";

// Note: Cardano wallets often sign hex payloads or structured data
const signature = await signMessage(wallet, "Hello Cardano");

Verifying Signatures

import { CryptoManager } from "@effectstream/crypto";
import { AddressType } from "@effectstream/utils";

const crypto = CryptoManager.getCryptoManager(AddressType.CARDANO);

// 1. Verify Cardano Address (Bech32)
const isValidAddr = crypto.verifyAddress("addr_test1...");

// 2. Verify Data Signature (CIP-30)
// Note: 'signatureStruct' usually contains the signature + key (e.g., "sig+key")
const isValidSig = await crypto.verifySignature(
userAddress,
"Hello Cardano",
signatureStruct
);

5. Orchestration

Use launchCardano from @effectstream/orchestrator/start-cardano to launch a local environment using Yaci Devkit and Dolos.

// in start.ts
processesToLaunch: [
...launchCardano("@my-project/cardano-contracts"),
]

NOTE: To use this launcher you need to implement some deno task in your project. A working implementation is provided in the template generator, templates or e2e tests.

{
"name": "@e2e/cardano-contracts",
...
"tasks": {
"devkit:start": "deno run -A --node-modules-dir npm:@bloxbean/yaci-devkit up",
"devkit:wait": "wait-on tcp:3001",
"dolos:fill-template": "deno run -A ./fill-template.ts",
"dolos:start": "rm -rf ./data && rm -rf ./dolos.socket && deno task dolos:fill-template && deno run -A --node-modules-dir npm:@txpipe/dolos bootstrap relay && deno run -A --node-modules-dir npm:@txpipe/dolos daemon",
"dolos:wait": "wait-on tcp:50051" // utxorpc port
}
}