@guardian-sdk/cardano
v1.1.0
Published
Guardian SDK for Cardano
Downloads
329
Maintainers
Readme
@guardian-sdk/cardano — Cardano
Native staking support for Cardano, part of the Guardian SDK.
Abstracts Blockfrost API calls and CBOR transaction construction behind a clean, type-safe API so you can build staking features without dealing with bech32 decoding, stake certificate encoding, UTXO coin selection, or Cardano-specifics.
Limitation — pure-ADA wallets only (native token support coming in a future release). This release only supports wallets whose UTXOs contain ADA exclusively. If your wallet holds native tokens (NFTs, fungible tokens) and there is not enough pure-ADA to cover the transaction, signing and fee estimation will throw
ValidationError("UNSUPPORTED_OPERATION"). Move native tokens to a separate address before staking, or use a dedicated ADA-only wallet for staking operations.
Table of Contents
- How Cardano Native Staking Works
- Blockfrost Setup
- Installation
- Quick Start
- API Reference
- Transaction Flows
- Signing Flows
- Logging
- Error Handling
- Supported Chains
- Roadmap
How Cardano Native Staking Works
Cardano uses Ouroboros — a proof-of-stake consensus protocol where block production rights are proportional to the amount of ADA delegated to a stake pool. ADA holders delegate their stake to pools to participate in network security and earn rewards without locking or transferring any tokens.
Stake Pools
A stake pool is an always-on server that participates in block production on behalf of its delegators. Pools are identified by a bech32 pool ID (e.g. pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy).
Each pool sets two fee parameters that reduce delegator rewards:
- Fixed cost — a flat ADA amount taken from rewards each epoch (currently minimum 170 ADA on mainnet). Pools must cover their operating costs before distributing to delegators.
- Margin — a percentage of the remaining rewards the pool operator keeps (0–100%). The rest goes to delegators.
The return on stake (ROS) a delegator earns is approximately:
delegator ROS ≈ protocol_yield × (1 − margin) − fixed_cost / pool_active_stakeThe protocol targets roughly 4–5% annual yield across all stake. Actual returns depend on pool performance (blocks produced vs expected) and pool saturation.
Saturation: Each pool has a target size determined by the k parameter (currently 500 optimal pools, each ideally holding 1/500 of total ADA staked). Pools above saturation earn reduced rewards — this design incentivises delegators to spread ADA across many pools rather than concentrating in a few.
As of 2026, the network has approximately 3,000 pools, of which ~2,400 are active.
Stake Keys and Addresses
Every Cardano wallet has two independent key pairs derived from the same root seed:
root seed (mnemonic)
├── payment key (m/1852'/1815'/0'/0/0) ──► addr1... (payment addresses)
└── staking key (m/1852'/1815'/0'/2/0) ──► stake1... (stake address)| Key | Authorises | Derived address |
|---|---|---|
| Payment key | Spending UTXOs; paying fees | addr1... payment addresses |
| Staking key | Delegation certificates; reward withdrawals | stake1... stake address |
The stake address (stake1...) is a single identifier for your staking account regardless of how many payment addresses you use. It is the handle for all staking operations: registering the key, delegating to a pool, and withdrawing rewards.
Both keys are required to sign any staking transaction. The SDK accepts them as separate paymentPrivateKey and stakingPrivateKey fields in sign().
Key format: The SDK accepts standard Ed25519 private keys — 32 bytes as 64 hex characters. BIP32-Ed25519 HD wallet keys are 64-byte extended keys; pass only the first 32 bytes (the private scalar). Use
deriveCardanoKeys(rootKeyHex)to derive both keys correctly from a BIP32 root key.
UTXO Model — Delegation Without Locking
Cardano uses an Unspent Transaction Output (UTXO) model. Every ADA sits in a specific UTXO, not in an account balance. When you delegate, you are not moving or locking any of your UTXOs — you are registering a preference on-chain that says "assign the stake weight of all UTXOs controlled by my staking key to pool X".
This has two important consequences:
Nothing is locked. Your ADA is fully spendable at all times, even while actively delegating. Receiving, sending, or spending ADA does not interrupt delegation — the pool earns rewards on whatever ADA you hold at each epoch snapshot.
No unbonding period. Changing or stopping delegation takes effect in the next epoch with no waiting period. You pay only the regular transaction fee.
getDelegations() reports amount as your total controlled ADA (including any rewards currently in the reward account) — the economic weight your delegation carries. Available and Staked in getBalances() are derived by subtracting withdrawable_amount from controlled_amount, so they reflect the actual spendable UTXO balance. Staked equals Available while actively delegating to a pool, and drops to zero when the stake key is not delegating — only delegated ADA earns rewards.
Rewards
Rewards do not auto-compound into your staked balance. They accumulate separately in your reward account and must be explicitly withdrawn using a ClaimRewards transaction.
Rewards become spendable two epochs after they are earned (see Epochs and Reward Timing). The Rewards balance in getBalances() is the amount available to withdraw right now.
When you withdraw rewards, the ADA moves from the reward account to a payment address you control. You can withdraw and redelegate simultaneously in the same transaction, or withdraw separately.
Lifecycle of a Stake
redelegate()
┌─────────────────────────────────┐
│ ▼
register + delegate() ──► [ Delegating ] [ Delegating — new pool ]
│
undelegate()
│
▼
[ Deregistered ] ──► 2 ADA deposit returned to wallet [ Delegating ] ──► rewards accumulate each epoch ──► [ Rewards available ]
│
claim()
│
▼
ADA in payment address| Stage | Description |
|---|---|
| Unregistered | Stake key does not exist on-chain. A 2 ADA deposit is required to register it. |
| Registered, not delegating | Stake key registered but no pool selected. Key deposit is locked on-chain. No rewards earned. |
| Delegating | ADA earns rewards every epoch. Delegation is active for the leader schedule as of epoch N+2 after the delegation transaction. |
| Rewards | Rewards distributed at the end of each epoch, available to withdraw from epoch N+4 onward (visible as the Rewards balance). |
| Deregistered | Stake key removed from chain. The 2 ADA deposit is refunded in the same transaction. Any unclaimed rewards are lost — always claim before deregistering. |
Delegation cycle in detail
First-time delegation (most common path):
1. Submit delegation tx with:
├── StakeRegistration certificate → 2 ADA deposit deducted from wallet
└── StakeDelegation certificate → pool assignment recorded on-chain
2. End of current epoch N:
└── On-chain snapshot taken. Your ADA counted toward the pool.
3. Epoch N+1:
└── Snapshot is used to determine the leader schedule for epoch N+2.
4. Epoch N+2:
└── Your stake participates in the leader schedule.
5. Epoch N+3:
└── Pool produces blocks (if selected) using the snapshot that includes your stake.
Rewards are earned for this epoch's performance.
6. Epoch N+4:
└── Rewards for epoch N+3 are calculated and distributed.
Your `Rewards` balance increases.
7. Repeat from step 2 every epoch (~5 days).Redelegation (switching pool):
1. Submit redelegate tx with:
└── StakeDelegation certificate to new pool (no deposit needed, no fee beyond tx fee)
2. Change takes effect at end of current epoch.
Old pool: earns rewards until snapshot.
New pool: earns rewards from next epoch.
3. No rewards are lost. No unbonding period.Undelegation (stop staking):
1. Claim any unclaimed rewards first — they are lost on deregistration.
2. Submit undelegate tx with:
└── StakeDeregistration certificate → 2 ADA deposit refunded in same tx
3. Pool stops earning on your behalf from next epoch.
Your ADA remains fully spendable throughout.
4. If you want to delegate again later, pay the 2 ADA deposit again.Claiming rewards:
1. Check Rewards balance: getBalances() → `Rewards` amount
2. Submit claim tx with:
└── Withdrawal entry: reward account → lovelace amount
ADA moves from reward account to your payment address.
3. Delegation continues uninterrupted. Claiming does not affect your pool assignment.Epochs and Reward Timing
A Cardano epoch spans 5 days (432,000 slots at 1 second per slot). Epochs are numbered sequentially and the exact reward schedule is deterministic:
Epoch N Epoch N+1 Epoch N+2 Epoch N+3 Epoch N+4
─────────────────────────────────────────────────────────────────────────────
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Tx confirmed Snapshot Stake in Blocks Rewards
(delegation taken leader produced distributed
included) schedule (rewards (visible in
earned) `withdrawable_amount`)| Event | Timing | |---|---| | Delegation submitted | Epoch N (any slot) | | Snapshot taken | End of Epoch N | | Stake participates in leader schedule | Epoch N+2 | | Pool produces blocks with your stake | Epoch N+3 | | First rewards calculated | End of Epoch N+3 | | First rewards spendable | Epoch N+4 (15–20 days after delegation) | | Subsequent rewards | Every epoch (~5 days) |
The first reward after first-time delegation therefore arrives 15–20 days after the transaction is confirmed, depending on where in the epoch you delegated. After that, rewards are added to your Rewards balance at the start of each new epoch.
Fee Model
Cardano fees are calculated from the size of the transaction, not from gas:
fee = minFeeA × txSizeInBytes + minFeeBCurrent mainnet parameters (protocol version 9):
| Parameter | Value | Description |
|---|---|---|
| minFeeA | 44 lovelaces/byte | Per-byte coefficient |
| minFeeB | 155,381 lovelaces | Base fee |
| Typical tx size | 300–400 bytes | Staking transactions |
| Typical fee | ~0.17–0.18 ADA | For a delegation tx |
Because the fee depends on the transaction size and the transaction size depends on the fee (it's a field in the body), the SDK estimates the size using mock 32/64-byte witnesses (same byte length as real signatures) and applies the formula once. No iteration is needed.
There is no fee priority mechanism. All valid transactions with the correct minimum fee are treated equally by block producers. Transactions cannot be accelerated by offering a higher fee.
Gas estimation is not used. Every operation has a predictable, deterministic fee based on its serialised size.
Typical fees observed on mainnet:
| Operation | Approx. fee | Notes | |---|---|---| | Delegate (first time) | ~0.18 ADA + 2 ADA deposit | Registration + delegation certificates | | Redelegate | ~0.17 ADA | Delegation certificate only | | Undelegate | ~0.17 ADA − 2 ADA deposit returned | Net cost is negative (deposit refunded) | | Claim rewards | ~0.17 ADA | Withdrawal only |
Key Protocol Parameters
| Parameter | Value | |---|---| | Stake key registration deposit | 2 ADA (refunded on deregistration) | | Unbonding period | None | | Redelegate fee | None (standard tx fee only) | | Min delegation amount | No protocol minimum (deposit is the effective minimum) | | Epoch duration | 5 days | | Target number of pools (k) | 500 | | Approximate protocol yield | ~4–5% annually | | Active pools (approx.) | ~2,400 | | Total registered pools | ~3,000 | | Mainnet staking explorer | https://cardanoscan.io | | Preprod staking explorer | https://preprod.cardanoscan.io |
Blockfrost Setup
All on-chain queries go through Blockfrost — the most widely used Cardano API service. No local node is required.
- Register at blockfrost.io
- Create a Cardano mainnet project
- Copy the
project_idkey (format:mainnet...)
cardano({ apiKey: "mainnetXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" })Rate limits:
| Tier | Requests/day | |---|---| | Free | 50,000 | | Starter | 500,000 | | Professional | 5,000,000 |
Installation
npm install @guardian-sdk/cardano \
@guardian-sdk/sdk \
@cardano-sdk/[email protected] \
@cardano-sdk/[email protected] \
@cardano-sdk/[email protected]If you already have @guardian-sdk/bsc installed, @guardian-sdk/sdk is already present — you only need to add the Cardano-specific peers:
npm install @guardian-sdk/cardano \
@cardano-sdk/[email protected] \
@cardano-sdk/[email protected] \
@cardano-sdk/[email protected]Dependencies
| Package | Version | Role |
|---|---|---|
| @guardian-sdk/sdk | workspace:^ | Peer — chain-agnostic core, shared types and interfaces |
| @cardano-sdk/core | 0.46.12 | Peer — Cardano primitives: addresses, transactions, certificates |
| @cardano-sdk/crypto | 0.4.5 | Peer — Ed25519 key operations and Blake2b hashing |
| @cardano-sdk/util | 0.17.1 | Peer — shared utilities for the Cardano SDK family |
Why exact peer versions? The
@cardano-sdkfamily has no stability guarantees between minor versions and CBOR serialisation is sensitive to the exact release. The versions above are the ones this package was built and tested against. These pins will be loosened to^in a future release.
Quick Start
import { GuardianSDK } from "@guardian-sdk/sdk";
import { cardano, chains } from "@guardian-sdk/cardano";
const sdk = new GuardianSDK([
cardano({ apiKey: "mainnetXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }),
]);
const PAYMENT_ADDRESS = "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae";
const STAKE_ADDRESS = "stake1ux3g2c9dx2nhhehyrezy4uvtyvgmndp3v4kplasjan2fcgfv7jyfa";
// Derive your payment and staking keys from a BIP32 root key (192 hex chars).
// See deriveCardanoKeys() — never hardcode or log these values.
import { deriveCardanoKeys } from "@guardian-sdk/cardano";
const { paymentPrivateKey: PAYMENT_KEY, stakingPrivateKey: STAKING_KEY } =
deriveCardanoKeys(process.env.CARDANO_ROOT_KEY!);
// 1. Browse stake pools
const { data: pools } = await sdk.getValidators(chains.cardanoMainnet);
console.log(`${pools.length} pools found, top pool: ${pools[0].name}`);
// 2. Check current delegation
const { delegations, stakingSummary } = await sdk.getDelegations(chains.cardanoMainnet, STAKE_ADDRESS);
console.log(`Max APY: ${stakingSummary.maxApy.toFixed(2)}%`);
// 3. Check balances
const balances = await sdk.getBalances(chains.cardanoMainnet, STAKE_ADDRESS);
for (const b of balances) {
console.log(b.type, Number(b.amount) / 1e6, "ADA");
}
// Available 9.95 ADA ← controlled minus rewards (spendable UTXOs)
// Staked 9.95 ADA ← equals Available while delegating
// Pending 0 ADA ← never used on Cardano
// Rewards 2.1 ADA ← withdrawable rewards
// 4. Delegate to the top pool
import type { CardanoSigningWithPrivateKey } from "@guardian-sdk/cardano";
const transaction = {
type: "Delegate" as const,
chain: chains.cardanoMainnet,
amount: 0n,
account: PAYMENT_ADDRESS,
isMaxAmount: false,
validator: pools[0],
};
const fee = await sdk.estimateFee(transaction);
console.log(`Fee: ${Number(fee.total) / 1e6} ADA`);
const signingArgs: CardanoSigningWithPrivateKey = {
transaction,
fee,
nonce: 0,
paymentPrivateKey: PAYMENT_KEY,
stakingPrivateKey: STAKING_KEY,
};
const rawTx = await sdk.sign(signingArgs);
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);
console.log(`Delegated! https://cardanoscan.io/transaction/${txHash}`);API Reference
cardano()
Factory function. Returns a GuardianServiceContract for the Cardano mainnet.
function cardano(config: CardanoConfig): GuardianServiceContract
interface CardanoConfig {
apiKey: string; // Blockfrost project_id for mainnet
logger?: Logger; // optional; defaults to NoopLogger
}getValidators
Returns a page of stake pools registered on Cardano. Pagination is passed directly to Blockfrost — only the requested page is fetched. Pool metadata (name, ticker, description) is batch-fetched in parallel for that page.
// First page, default 20 pools
const { data, pagination } = await sdk.getValidators(chains.cardanoMainnet);
// Paginate
const page2 = await sdk.getValidators(chains.cardanoMainnet, { page: 2, pageSize: 50 });
console.log(page2.pagination.hasNextPage); // true/false — use this to drive paginationReturns: Promise<ValidatorsPage>
interface ValidatorsPage {
data: Validator[];
pagination: {
page: number;
pageSize: number;
total: undefined; // Not available — Blockfrost does not return total count
totalPages: undefined; // Not available
hasNextPage: boolean; // true when the page returned a full pageSize of results
};
}
interface Validator {
id: string; // bech32 pool ID — pool1...
status: ValidatorStatus; // "Active" | "Inactive"
name: string; // Pool name from metadata, or ticker, or truncated pool ID
description: string; // Pool description from metadata
image: undefined; // Cardano pools have no standard logo URL format
apy: number; // Estimated annual yield (%)
delegators: number | undefined; // Live delegator count
operatorAddress: string; // bech32 pool ID — use in transaction.validator
creditAddress: string; // Same as operatorAddress (no separate credit contract)
}
type ValidatorStatus = "Active" | "Inactive";
// Active — pool is registered and not in retirement
// Inactive — pool has submitted a retirement certificate (retiring next epoch)APY estimation: Uses the Cardano ledger reward formula (Shelley spec §11.8). APY is derived from live on-chain parameters — monetary expansion rate (ρ), treasury rate (τ), pledge influence (a0), and pool saturation — fetched alongside pools on each call. Actual earned rewards depend on pool luck and real-time block production. For precise historical ROA, call Blockfrost's
/pools/{id}/historydirectly.
getDelegations
Returns the current delegation state and a summary of the staking protocol. Accepts either a stake address (stake1...) or a base payment address (addr1q...) — the stake credential is extracted automatically from a payment address.
const { delegations, stakingSummary } = await sdk.getDelegations(
chains.cardanoMainnet,
"stake1ux3g2c9dx2nhhehyrezy4uvtyvgmndp3v4kplasjan2fcgfv7jyfa"
);Returns: Promise<Delegations>
interface Delegations {
delegations: Delegation[]; // At most one entry on Cardano
stakingSummary: StakingSummary;
}
interface Delegation {
id: string;
validator: Validator; // Full pool details
amount: bigint; // Total ADA controlled by the stake key, in lovelaces
status: "Active"; // Cardano: always "Active" when delegating
delegationIndex: 0n; // Not used in Cardano
pendingUntil: 0; // No unbonding period
}
interface StakingSummary {
totalProtocolStake: number; // Total ADA staked network-wide (in ADA, not lovelaces)
maxApy: number; // Highest estimated APY across the first page of pools
minAmountToStake: 2_000_000n; // 2 ADA stake key registration deposit (in lovelaces)
unboundPeriodInMillis: 0; // No unbonding period
redelegateFeeRate: 0; // No fee to switch pools
activeValidators: undefined; // Not available from getDelegations — use getValidators()
totalValidators: undefined; // Not available from getDelegations — use getValidators()
}Cardano on-chain supports only one active delegation per stake key. Multi-pool delegation (CIP-17) is a metadata-layer convention implemented by wallets across multiple stake keys — there is no native multi-pool delegation.
delegationstherefore contains at most one entry.
If the stake key is not registered or not currently delegating,
delegationsis an empty array.
getBalances
Returns balance information for an address (Cardano surfaces Available, Staked, and Rewards). Accepts either a stake address (stake1...) or a base payment address (addr1q...) — the stake credential is extracted automatically from a payment address. Balances reflect the total ADA controlled by the stake key across all associated payment addresses.
const balances = await sdk.getBalances(
chains.cardanoMainnet,
"stake1ux3g2c9dx2nhhehyrezy4uvtyvgmndp3v4kplasjan2fcgfv7jyfa"
);Returns: Promise<Balance[]>
All amounts are in lovelaces (1 ADA = 1,000,000 lovelaces).
| Type | What it represents on Cardano |
|---|---|
| Available | Spendable UTXO balance (controlled_amount minus withdrawable_amount). Fully spendable at all times regardless of delegation. |
| Staked | Equals Available when the stake key is actively delegated to a pool; 0 otherwise. Only delegated ADA earns staking rewards. |
| Rewards | withdrawable_amount from Blockfrost — rewards that have been distributed and are sitting in the reward account, ready to withdraw via a ClaimRewards transaction |
type BalanceType = "Available" | "Staked" | "Pending" | "Rewards";Cardano does not use the
Pendingtype (it is present for BSC compatibility).getBalances()returnsAvailable,Staked, andRewards.
```typescript
const balances = await sdk.getBalances(chains.cardanoMainnet, STAKE_ADDRESS);
for (const b of balances) {
console.log(b.type, (Number(b.amount) / 1e6).toFixed(6), "ADA");
}
// Available 9.950000 ADA ← controlled_amount minus withdrawable_amount
// Staked 9.950000 ADA ← equals Available while delegating
// Pending 0.000000 ADA ← never used on Cardano
// Rewards 2.100000 ADA ← withdrawable_amount
Rewardsreflects rewards that have already been distributed and are available to withdraw. Rewards currently being earned in the active epoch are not yet included.
getNonce
Cardano uses a UTXO model — transactions reference specific UTXOs as inputs, not account nonces. This always returns 0. The double-spend protection is handled internally by UTXO input selection during signing.
const nonce = await sdk.getNonce(chains.cardanoMainnet, address); // always 0estimateFee
Estimates the transaction fee by fetching protocol parameters, UTXOs, and the stake account (registration status + reward balance) from Blockfrost, building a draft transaction that mirrors the one sign() submits — distinct payment/staking witnesses, a TTL, the real certificate set, and any full-balance reward withdrawal — then applying the fee formula plus a 10% safety buffer.
transaction.account must be set to a base payment address (addr1q...) — it carries the stake credential used to fetch UTXOs and the reward account. Enterprise/pointer addresses are rejected with ValidationError("INVALID_ADDRESS").
const fee = await sdk.estimateFee(transaction);Returns: Promise<UtxoFee>
interface UtxoFee {
type: "UtxoFee";
txSizeBytes: number; // Estimated serialised transaction size in bytes
total: bigint; // Total fee in lovelaces
}Accepts all four transaction types:
// Delegate — register stake key and set pool
const fee = await sdk.estimateFee({
type: "Delegate",
chain: chains.cardanoMainnet,
amount: 0n, // amount is unused for delegation
account: PAYMENT_ADDRESS, // required for UTXO fetch
isMaxAmount: false,
validator: pools[0],
});
// Redelegate — change pool
const fee = await sdk.estimateFee({
type: "Redelegate",
chain: chains.cardanoMainnet,
amount: 0n,
account: PAYMENT_ADDRESS,
isMaxAmount: false,
fromValidator: currentDelegation.validator,
toValidator: newPool,
});
// Undelegate — deregister stake key
const fee = await sdk.estimateFee({
type: "Undelegate",
chain: chains.cardanoMainnet,
amount: 0n,
account: PAYMENT_ADDRESS,
isMaxAmount: false,
validator: currentDelegation.validator,
});
// ClaimRewards — withdraw rewards
const fee = await sdk.estimateFee({
type: "ClaimRewards",
chain: chains.cardanoMainnet,
amount: claimableAmount, // validated (> 0 and <= on-chain `Rewards`); the full reward balance is always withdrawn
account: PAYMENT_ADDRESS,
validator: currentDelegation.validator,
index: 0n, // not used in Cardano; required by interface
});sign
Signs a Cardano transaction and returns the CBOR hex string ready to broadcast.
Cardano staking requires two Ed25519 keys — pass them as paymentPrivateKey and stakingPrivateKey in the signing args. Use deriveCardanoKeys() to obtain them from a BIP32 root key:
import { deriveCardanoKeys } from "@guardian-sdk/cardano";
import type { CardanoSigningWithPrivateKey } from "@guardian-sdk/cardano";
const { paymentPrivateKey, stakingPrivateKey } = deriveCardanoKeys(rootKeyHex);
const signingArgs: CardanoSigningWithPrivateKey = {
transaction,
fee,
nonce: 0, // always 0 for Cardano
paymentPrivateKey,
stakingPrivateKey,
};
const rawTx = await sdk.sign(signingArgs);CardanoSigningWithPrivateKey extends BaseSignArgs and is accepted wherever the SDK expects signing arguments.
The SDK:
- Derives the verification key from each private key
- Fetches UTXOs and protocol parameters from Blockfrost
- Builds and CBOR-encodes the transaction body
- Hashes the body with Blake2b-256
- Signs the hash with both keys
- Returns the fully assembled transaction as CBOR hex
preHash and compile
For MPC wallets, hardware signers, or custody setups where private keys are not available in the application process. Splits signing into two steps:
Your App SDK External Signer (MPC/HSM)
│ │ │
│ preHash(tx,fee,0) │ │
│ ─────────────────────►│ │
│ │ │
│ { serializedTx, │ │
│ signArgs } │ │
│ ◄─────────────────────│ │
│ │ │
│ serializedTx (32-byte body hash/digest) │
│ ──────────────────────────────────────────────────►
│ │ │
│ paymentSig, stakingVKey, stakingSig, paymentVKey │
│ ◄──────────────────────────────────────────────────
│ │ │
│ compile({signArgs, │ │
│ signature: "a:b:c:d"}) │
│ ─────────────────────►│ │
│ │ │
│ rawTx (CBOR hex) │ │
│ ◄─────────────────────│ │Step 1 — serialise the transaction body:
const { serializedTransaction, signArgs } = await sdk.preHash({
transaction,
fee,
nonce: 0,
});
// For Cardano, `serializedTransaction` is the Blake2b-256 **hash** of the tx body
// (32 bytes / 64 hex chars) — this is the exact value the Ed25519 signer must sign.
// The full CBOR body is carried inside `signArgs._txBodyCbor` for use in compile().
// Send `serializedTransaction` to your external signer. The signer must:
// 1. Treat it as the 32-byte digest to sign
// 2. Sign with payment Ed25519 key → paymentSig (64 bytes → 128 hex)
// 3. Sign with staking Ed25519 key → stakingSig (64 bytes → 128 hex)
// 4. Return both signatures and both verification keys (vkeys)Step 2 — compile the signed transaction:
// Encode all witness components into the single signature field using ":" delimiter
const rawTx = await sdk.compile({
signArgs,
signature: `${paymentSig}:${stakingVKey}:${stakingSig}:${paymentVKey}`,
// Format: paymentSigHex:stakingVKeyHex:stakingSigHex:paymentVKeyHex
// Each component is a hex string. : is the separator.
});Step 3 — broadcast:
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);deriveCardanoKeys
Derives the payment and staking private keys from a BIP32 root key. Both keys are required for every signing operation.
import { deriveCardanoKeys } from "@guardian-sdk/cardano";
const { paymentPrivateKey, stakingPrivateKey } = deriveCardanoKeys(rootKeyHex);Parameters
| Parameter | Type | Description |
|---|---|---|
| rootKeyHex | string | BIP32-Ed25519 root private key — 96 bytes as a 192-character hex string |
Returns: { paymentPrivateKey: string, stakingPrivateKey: string } — both are 32-byte Ed25519 scalars (64 hex chars), ready to pass to sign().
Derivation paths (CIP-1852):
| Key | Path |
|---|---|
| Payment | m / 1852' / 1815' / 0' / 0 / 0 |
| Staking | m / 1852' / 1815' / 0' / 2 / 0 |
Obtaining a root key from a mnemonic:
deriveCardanoKeys accepts a root key hex, not a mnemonic directly. To convert a mnemonic, use a BIP39 library alongside @cardano-sdk/crypto (already a bundled dependency):
import { Bip32PrivateKey, ready } from "@cardano-sdk/crypto";
import { mnemonicToEntropy } from "@scure/bip39"; // npm install @scure/bip39
import { wordlist } from "@scure/bip39/wordlists/english.js";
await ready(); // initialise WASM module
const entropy = mnemonicToEntropy("your twenty four word mnemonic ...", wordlist);
const rootKeyHex = Bip32PrivateKey.fromBip39Entropy(Buffer.from(entropy), "").hex();
const { paymentPrivateKey, stakingPrivateKey } = deriveCardanoKeys(rootKeyHex);Store
rootKeyHexthe same way you would store a mnemonic — it is equivalent in sensitivity. Never log or commit it.
Transaction Flows
Full working examples for each staking operation. All examples share this setup:
import { GuardianSDK } from "@guardian-sdk/sdk";
import { cardano, chains } from "@guardian-sdk/cardano";
import type { CardanoSigningWithPrivateKey } from "@guardian-sdk/cardano";
const sdk = new GuardianSDK([
cardano({ apiKey: "mainnetXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }),
]);
const PAYMENT_ADDRESS = "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae";
const STAKE_ADDRESS = "stake1ux3g2c9dx2nhhehyrezy4uvtyvgmndp3v4kplasjan2fcgfv7jyfa";
const { paymentPrivateKey: PAYMENT_KEY, stakingPrivateKey: STAKING_KEY } =
deriveCardanoKeys(process.env.CARDANO_ROOT_KEY!);Delegate — register and start staking
Registers the stake key on-chain (2 ADA deposit) and sets the pool to delegate to. The registration and delegation certificates are bundled in a single transaction.
const { data: pools } = await sdk.getValidators(chains.cardanoMainnet);
const selectedPool = pools[0];
const transaction = {
type: "Delegate" as const,
chain: chains.cardanoMainnet,
amount: 0n, // delegation carries no token amount
account: PAYMENT_ADDRESS, // required: used to fetch UTXOs for fee payment
isMaxAmount: false,
validator: selectedPool,
};
const fee = await sdk.estimateFee(transaction);
console.log(`Fee: ~${(Number(fee.total) / 1e6).toFixed(4)} ADA`);
console.log(`Deposit: 2 ADA (returned when you deregister)`);
// Wallet needs at least: fee.total + 2_000_000n lovelaces
const signingArgs: CardanoSigningWithPrivateKey = {
transaction, fee, nonce: 0,
paymentPrivateKey: PAYMENT_KEY,
stakingPrivateKey: STAKING_KEY,
};
const rawTx = await sdk.sign(signingArgs);
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);
console.log(`Delegated! https://cardanoscan.io/transaction/${txHash}`);
// First rewards arrive in ~15–20 days (N+4)If the stake key is already registered (you have delegated before), the
StakeRegistrationcertificate is still included but is harmless on-chain. No second deposit is charged for an already-registered key.
Redelegate — switch pool
Changes the delegation to a different pool. Takes effect at the next epoch boundary. No waiting period, no fee beyond the standard transaction fee.
const { delegations } = await sdk.getDelegations(chains.cardanoMainnet, STAKE_ADDRESS);
const currentDelegation = delegations[0];
const { data: pools } = await sdk.getValidators(chains.cardanoMainnet);
const newPool = pools.find((p) => p.operatorAddress !== currentDelegation.validator.operatorAddress)!;
const transaction = {
type: "Redelegate" as const,
chain: chains.cardanoMainnet,
amount: 0n,
account: PAYMENT_ADDRESS,
isMaxAmount: false,
fromValidator: currentDelegation.validator,
toValidator: newPool,
};
const fee = await sdk.estimateFee(transaction);
const signingArgs: CardanoSigningWithPrivateKey = {
transaction, fee, nonce: 0,
paymentPrivateKey: PAYMENT_KEY,
stakingPrivateKey: STAKING_KEY,
};
const rawTx = await sdk.sign(signingArgs);
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);
console.log(`Pool switched! https://cardanoscan.io/transaction/${txHash}`);
// Old pool earns for remainder of current epoch.
// New pool earns from next epoch.Undelegate — stop staking and recover deposit
Deregisters the stake key. The 2 ADA registration deposit is returned in the same transaction. Stops earning rewards from the next epoch.
Claim rewards before undelegating. Any unclaimed rewards in the reward account are lost permanently when the stake key is deregistered.
// Step 1: claim any pending rewards first (see Claim flow below)
const balances = await sdk.getBalances(chains.cardanoMainnet, STAKE_ADDRESS);
const rewards = balances.find((b) => b.type === "Rewards")!;
if (rewards.amount > 0n) {
// claim first — rewards are lost on deregistration
}
// Step 2: deregister
const { delegations } = await sdk.getDelegations(chains.cardanoMainnet, STAKE_ADDRESS);
const transaction = {
type: "Undelegate" as const,
chain: chains.cardanoMainnet,
amount: 0n,
account: PAYMENT_ADDRESS,
isMaxAmount: false,
validator: delegations[0].validator,
};
const fee = await sdk.estimateFee(transaction);
// Net cost = fee.total − 2_000_000n (deposit returned)
console.log(`Net effect: +${((2_000_000n - fee.total) / 1_000_000n).toString()} ADA returned to wallet`);
const signingArgs: CardanoSigningWithPrivateKey = {
transaction, fee, nonce: 0,
paymentPrivateKey: PAYMENT_KEY,
stakingPrivateKey: STAKING_KEY,
};
const rawTx = await sdk.sign(signingArgs);
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);
console.log(`Deregistered! https://cardanoscan.io/transaction/${txHash}`);ClaimRewards — withdraw accumulated rewards
Withdraws accumulated rewards from the reward account to your payment address. Delegation continues uninterrupted.
const balances = await sdk.getBalances(chains.cardanoMainnet, STAKE_ADDRESS);
const rewards = balances.find((b) => b.type === "Rewards")!;
if (rewards.amount === 0n) {
console.log("No rewards available yet. Check back after the next epoch.");
} else {
const { delegations } = await sdk.getDelegations(chains.cardanoMainnet, STAKE_ADDRESS);
const transaction = {
type: "ClaimRewards" as const,
chain: chains.cardanoMainnet,
amount: rewards.amount, // exact lovelace amount to withdraw
account: PAYMENT_ADDRESS,
validator: delegations[0].validator,
index: 0n, // unused in Cardano; required by interface
};
const fee = await sdk.estimateFee(transaction);
console.log(`Claiming ${(Number(rewards.amount) / 1e6).toFixed(6)} ADA, fee: ~${(Number(fee.total) / 1e6).toFixed(4)} ADA`);
const signingArgs: CardanoSigningWithPrivateKey = {
transaction, fee, nonce: 0,
paymentPrivateKey: PAYMENT_KEY,
stakingPrivateKey: STAKING_KEY,
};
const rawTx = await sdk.sign(signingArgs);
const txHash = await sdk.broadcast(chains.cardanoMainnet, rawTx);
console.log(`Rewards claimed! https://cardanoscan.io/transaction/${txHash}`);
}Signing Flows
See the main README signing flows diagram for a visual reference of the direct and MPC signing paths.
Cardano signing always involves two keys and produces two witness entries in the transaction:
| Key | Signs | Why | |---|---|---| | Payment key | Transaction body hash | Authorises UTXO consumption (fee payment) | | Staking key | Transaction body hash | Authorises delegation certificates and reward withdrawals |
Both verification keys are included in the transaction's witness set so validators can verify the signatures on-chain.
Logging
Logging is opt-in — pass a logger to cardano() to enable it:
import { ConsoleLogger } from "@guardian-sdk/sdk";
import { cardano } from "@guardian-sdk/cardano";
const sdk = new GuardianSDK([
cardano({
apiKey: "mainnetXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
logger: new ConsoleLogger("debug"), // "debug" | "info" | "warn" | "error"
}),
]);Plug in any logger that implements the Logger interface (debug, info, warn, error methods). See the main README Logging section for full details including bring-your-own-logger examples.
Private keys and signatures are never logged at any level.
Error Handling
Every error thrown by the SDK extends GuardianError. See the main README Error Handling section for the catch pattern and base class reference.
ValidationError
Thrown when the caller provides invalid input, before any network call is made.
import { ValidationError } from "@guardian-sdk/sdk";| Code | Thrown when |
|---|---|
| INVALID_ADDRESS | An address string is not valid bech32 — e.g. a pool ID passed to getDelegations, or an enterprise/pointer address passed where a base address or stake address is required |
| INVALID_AMOUNT | Insufficient UTXOs to cover the required fee and deposit |
| INVALID_PRIVATE_KEY | A key is not 32 bytes (64 hex characters) |
| UNSUPPORTED_OPERATION | The wallet contains UTXOs with native tokens and ADA-only UTXOs are insufficient — move tokens to a separate address before staking |
SigningError
Thrown when signing arguments are malformed.
import { SigningError } from "@guardian-sdk/sdk";| Code | Thrown when |
|---|---|
| INVALID_SIGNING_ARGS | paymentPrivateKey or stakingPrivateKey missing from signing args |
| INVALID_FEE_TYPE | fee.type is not "UtxoFee" — use estimateFee() to get a Cardano fee |
| INVALID_SIGNING_ARGS | The signature string passed to compile() does not contain exactly four : delimited components |
ConfigError
import { ConfigError } from "@guardian-sdk/sdk";| Code | Thrown when |
|---|---|
| UNSUPPORTED_CHAIN | The chain passed to any method has no registered service |
Catching by code
import { ValidationError } from "@guardian-sdk/sdk";
try {
await sdk.getBalances(chains.cardanoMainnet, rawInput);
} catch (err) {
if (err instanceof ValidationError && err.code === "INVALID_ADDRESS") {
showError("Please enter a valid stake address (stake1...) or base payment address (addr1q...).");
}
}Supported Chains
import { chains } from "@guardian-sdk/cardano";| Chain | Symbol | Explorer | |---|---|---| | Cardano Mainnet | ADA | https://cardanoscan.io |
import { SUPPORTED_CHAINS } from "@guardian-sdk/cardano";
// [{ id: "cardano-mainnet", symbol: "ADA", decimals: 6, ... }]Roadmap
| Feature | Status | Issue | |---|---|---| | Native-token UTXOs — staking currently spends pure-ADA UTXOs only; wallets holding native tokens in the same address are not yet supported. | Planned (v1.1.0) | — |
← Back to Guardian SDK
