diff --git a/.changeset/legacy-stake-registration.md b/.changeset/legacy-stake-registration.md
new file mode 100644
index 00000000..93272e54
--- /dev/null
+++ b/.changeset/legacy-stake-registration.md
@@ -0,0 +1,5 @@
+---
+"@evolution-sdk/evolution": patch
+---
+
+Add `registerStakeLegacy` and `deregisterStakeLegacy` builder methods for pre-Conway stake certificate support. These create `StakeRegistration` (CDDL tag 0) and `StakeDeregistration` (CDDL tag 1) certificates with no deposit, matching what most wallets use today. Both methods support script-controlled credentials with redeemers.
diff --git a/docs/content/docs/addresses/address-eras.mdx b/docs/content/docs/addresses/address-eras.mdx
new file mode 100644
index 00000000..5f67c870
--- /dev/null
+++ b/docs/content/docs/addresses/address-eras.mdx
@@ -0,0 +1,145 @@
+---
+title: Address Eras
+description: Parse all Cardano address formats including legacy Byron and Pointer addresses
+---
+
+# Address Eras
+
+The `AddressEras` module is a full-spectrum address parser that handles every Cardano address type -- including legacy Byron addresses and deprecated Pointer addresses that the simplified `Address` module does not cover.
+
+## When to Use AddressEras vs Address
+
+| Feature | Address | AddressEras |
+|---------|---------|-------------|
+| Base addresses | Yes | Yes |
+| Enterprise addresses | Yes | Yes |
+| Byron addresses | No | Yes |
+| Pointer addresses | No | Yes |
+| Reward accounts | No | Yes |
+| Simplified API | Yes | No |
+
+Use `Address` for most application code. Use `AddressEras` when parsing UTxOs or transactions that may contain legacy formats.
+
+## Parsing Any Address
+
+`AddressEras.fromBech32` accepts any valid Bech32-encoded Cardano address and returns a discriminated union tagged by `_tag`:
+
+```typescript twoslash
+import { AddressEras } from "@evolution-sdk/evolution"
+
+// Parse any Cardano address format
+const addr1 = AddressEras.fromBech32(
+ "addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"
+)
+const addr2 = AddressEras.fromBech32(
+ "stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw"
+)
+
+// Type narrowing by _tag
+if (addr1._tag === "BaseAddress") {
+ console.log("Payment:", addr1.paymentCredential)
+ console.log("Stake:", addr1.stakeCredential)
+} else if (addr1._tag === "EnterpriseAddress") {
+ console.log("Payment only:", addr1.paymentCredential)
+} else if (addr1._tag === "RewardAccount") {
+ console.log("Stake credential:", addr1.stakeCredential)
+}
+```
+
+## The Five Address Types
+
+`AddressEras` is a union of all five Cardano address types:
+
+| `_tag` | Fields | Bech32 prefix | Use case |
+|--------|--------|---------------|----------|
+| `BaseAddress` | `networkId`, `paymentCredential`, `stakeCredential` | `addr` / `addr_test` | Standard address with payment and staking |
+| `EnterpriseAddress` | `networkId`, `paymentCredential` | `addr` / `addr_test` | Payment only, no staking rewards |
+| `PointerAddress` | `networkId`, `paymentCredential`, `pointer` | `addr` / `addr_test` | Deprecated pointer to on-chain stake registration |
+| `RewardAccount` | `networkId`, `stakeCredential` | `stake` / `stake_test` | Staking reward withdrawal address |
+| `ByronAddress` | `networkId`, `bytes` | N/A (Base58) | Legacy Byron-era address |
+
+Each type is a `Schema.TaggedClass`, so you can use `_tag` for exhaustive pattern matching.
+
+## Format Conversion
+
+`AddressEras` provides symmetric parsing and encoding functions for all three formats:
+
+### Bech32
+
+```typescript twoslash
+import { AddressEras } from "@evolution-sdk/evolution"
+
+const address = AddressEras.fromBech32(
+ "addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"
+)
+
+const bech32 = AddressEras.toBech32(address)
+```
+
+### Hex
+
+```typescript twoslash
+import { AddressEras } from "@evolution-sdk/evolution"
+
+const address = AddressEras.fromHex(
+ "019493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc"
+)
+
+const hex = AddressEras.toHex(address)
+```
+
+### Bytes
+
+```typescript twoslash
+import { AddressEras } from "@evolution-sdk/evolution"
+
+const address = AddressEras.fromHex(
+ "019493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc"
+)
+
+const bytes = AddressEras.toBytes(address)
+const decoded = AddressEras.fromBytes(bytes)
+```
+
+## Handling Legacy Addresses
+
+When iterating UTxOs from the chain, you may encounter Byron addresses that `Address` cannot parse. Use `AddressEras` to handle them:
+
+```typescript twoslash
+import { AddressEras } from "@evolution-sdk/evolution"
+
+type Utxo = { address: string; value: bigint }
+
+function getPaymentCredential(utxo: Utxo) {
+ const address = AddressEras.fromHex(utxo.address)
+
+ switch (address._tag) {
+ case "BaseAddress":
+ case "EnterpriseAddress":
+ case "PointerAddress":
+ return address.paymentCredential
+ case "RewardAccount":
+ return null // Reward accounts have no payment credential
+ case "ByronAddress":
+ return null // Byron addresses store opaque bytes
+ }
+}
+```
+
+## Summary
+
+| Function | Purpose |
+|----------|---------|
+| `fromBech32()` | Parse Bech32 address string (any type) |
+| `fromHex()` | Parse hex-encoded address bytes |
+| `fromBytes()` | Parse raw `Uint8Array` address |
+| `toBech32()` | Encode to Bech32 string |
+| `toHex()` | Encode to hex string |
+| `toBytes()` | Encode to raw `Uint8Array` |
+| `isAddress()` | Type guard for the `AddressEras` union |
+
+## Next Steps
+
+- **[Address](/docs/addresses/address)** - Simplified API for modern address types
+- **[Address Types](/docs/addresses/address-types)** - Overview of all Cardano address types
+- **[Address Conversion](/docs/addresses/conversion)** - Transform between Bech32, hex, and byte formats
diff --git a/docs/content/docs/addresses/address-types/enterprise.mdx b/docs/content/docs/addresses/address-types/enterprise.mdx
index f439f340..5b274ccf 100644
--- a/docs/content/docs/addresses/address-types/enterprise.mdx
+++ b/docs/content/docs/addresses/address-types/enterprise.mdx
@@ -40,7 +40,7 @@ console.log(bech32); // "addr1..."
Parse a Bech32 address string into an `EnterpriseAddress` instance:
```typescript twoslash
-import { AddressEras, EnterpriseAddress, KeyHash, ScriptHash } from "@evolution-sdk/evolution";
+import { AddressEras, EnterpriseAddress } from "@evolution-sdk/evolution";
const bech32 = "addr1vx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz6cevnrgl";
@@ -55,7 +55,7 @@ console.log("Payment:", address.paymentCredential);
Enterprise addresses can use script hashes as payment credentials:
```typescript twoslash
-import { AddressEras, EnterpriseAddress, KeyHash, ScriptHash } from "@evolution-sdk/evolution";
+import { AddressEras, EnterpriseAddress, ScriptHash } from "@evolution-sdk/evolution";
// Script address example
const scriptAddr = new EnterpriseAddress.EnterpriseAddress({
@@ -74,7 +74,7 @@ console.log("Script enterprise address:", bech32);
**Bech32 Prefix**: `addr` (mainnet) or `addr_test` (testnet)
**Length**: 29 bytes raw / ~59 characters Bech32
-**Header Bits**: `0110xxxx` (enterprise address type)
+**Header Bits**: `0110xxxx` (key hash) or `0111xxxx` (script hash)
**Size Advantage**: Half the size of base addresses (29 vs 57 bytes)
## Comparison with Base Addresses
diff --git a/docs/content/docs/addresses/address-types/index.mdx b/docs/content/docs/addresses/address-types/index.mdx
index ed284cfb..0b1dd493 100644
--- a/docs/content/docs/addresses/address-types/index.mdx
+++ b/docs/content/docs/addresses/address-types/index.mdx
@@ -32,10 +32,10 @@ Each address type encodes different credential combinations:
| Address Type | Payment | Staking | On-Chain Size | Bech32 Prefix | Header Bits |
|--------------|---------|---------|---------------|---------------|-------------|
-| **[Base](/docs/addresses/address-types/base)** | ✓ | ✓ | 57 bytes | `addr`/`addr_test` | `0000xxxx` |
-| **[Enterprise](/docs/addresses/address-types/enterprise)** | ✓ | ✗ | 29 bytes | `addr`/`addr_test` | `0110xxxx` |
-| **[Reward](/docs/addresses/address-types/reward)** | ✗ | ✓ | 29 bytes | `stake`/`stake_test` | `1110xxxx` |
-| **[Pointer](/docs/addresses/address-types/pointer)** | ✓ | Pointer | Variable | `addr`/`addr_test` | `0100xxxx` |
+| **[Base](/docs/addresses/address-types/base)** | ✓ | ✓ | 57 bytes | `addr`/`addr_test` | `0000`–`0011` |
+| **[Enterprise](/docs/addresses/address-types/enterprise)** | ✓ | ✗ | 29 bytes | `addr`/`addr_test` | `0110`–`0111` |
+| **[Reward](/docs/addresses/address-types/reward)** | ✗ | ✓ | 29 bytes | `stake`/`stake_test` | `1110`–`1111` |
+| **[Pointer](/docs/addresses/address-types/pointer)** | ✓ | Pointer | Variable | `addr`/`addr_test` | `0100`–`0101` |
## Address Type Documentation
diff --git a/docs/content/docs/addresses/address-types/pointer.mdx b/docs/content/docs/addresses/address-types/pointer.mdx
index e48a491c..dfdb8978 100644
--- a/docs/content/docs/addresses/address-types/pointer.mdx
+++ b/docs/content/docs/addresses/address-types/pointer.mdx
@@ -117,7 +117,7 @@ console.log("Pointer address (deprecated):", pointerAddr);
**Bech32 Prefix**: `addr` (mainnet) or `addr_test` (testnet)
**Length**: Variable (34-48 bytes depending on pointer values)
-**Header Bits**: `0100xxxx` (pointer address type)
+**Header Bits**: `0100xxxx` (key hash) or `0101xxxx` (script hash)
## Related
diff --git a/docs/content/docs/addresses/address-types/reward.mdx b/docs/content/docs/addresses/address-types/reward.mdx
index 73d22ed0..21aa12a8 100644
--- a/docs/content/docs/addresses/address-types/reward.mdx
+++ b/docs/content/docs/addresses/address-types/reward.mdx
@@ -21,7 +21,7 @@ Reward Address = Staking Credential Only
Create reward addresses by instantiating the `RewardAccount` class:
```typescript twoslash
-import { KeyHash, RewardAccount, ScriptHash } from "@evolution-sdk/evolution";
+import { KeyHash, RewardAccount } from "@evolution-sdk/evolution";
const rewardAccount = new RewardAccount.RewardAccount({
networkId: 1, // mainnet
@@ -40,7 +40,7 @@ console.log(bech32); // "stake1..."
Parse a Bech32 stake address string into a `RewardAccount` instance:
```typescript twoslash
-import { KeyHash, RewardAccount, ScriptHash } from "@evolution-sdk/evolution";
+import { RewardAccount } from "@evolution-sdk/evolution";
const bech32 = "stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw";
@@ -55,7 +55,7 @@ console.log("Stake credential:", address.stakeCredential);
Reward addresses can use script hashes for the staking credential:
```typescript twoslash
-import { KeyHash, RewardAccount, ScriptHash } from "@evolution-sdk/evolution";
+import { RewardAccount, ScriptHash } from "@evolution-sdk/evolution";
// Reward address with script credential
const scriptRewardAccount = new RewardAccount.RewardAccount({
@@ -83,7 +83,7 @@ Reward Address: stake1uy... (stake only)
**Bech32 Prefix**: `stake` (mainnet) or `stake_test` (testnet)
**Length**: 29 bytes raw / ~59 characters Bech32
-**Header Bits**: `1110xxxx` (reward address type)
+**Header Bits**: `1110xxxx` (key hash) or `1111xxxx` (script hash)
## Address Components
diff --git a/docs/content/docs/addresses/address.mdx b/docs/content/docs/addresses/address.mdx
index f59ea720..77502c30 100644
--- a/docs/content/docs/addresses/address.mdx
+++ b/docs/content/docs/addresses/address.mdx
@@ -21,7 +21,7 @@ Legacy formats (Byron, Pointer) exist for historical compatibility but are no lo
### Parsing Addresses
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
// Parse from Bech32 (most common format)
const address = Address.fromBech32(
@@ -37,7 +37,7 @@ const address2 = Address.fromHex(
### Converting Formats
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const address = Address.fromBech32(
"addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd"
@@ -52,7 +52,7 @@ const bytes = Address.toBytes(address); // Uint8Array
### Validating User Input
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
function validateAddress(input: string): Address.Address | null {
try {
@@ -76,7 +76,7 @@ function validateAddress(input: string): Address.Address | null {
### Type Checking
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const address = Address.fromBech32(
"addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd"
@@ -99,7 +99,7 @@ if (details?.type === "Base") {
For comprehensive information about an address:
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const details = Address.getAddressDetails(
"addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd"
diff --git a/docs/content/docs/addresses/construction.mdx b/docs/content/docs/addresses/construction.mdx
index 1aafb2a3..41b4d030 100644
--- a/docs/content/docs/addresses/construction.mdx
+++ b/docs/content/docs/addresses/construction.mdx
@@ -22,9 +22,10 @@ const address = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmlu
```typescript twoslash
import { Address } from "@evolution-sdk/evolution"
-// Address hex strings are typically 29 or 57 bytes
-declare const addressHex: string
-const address = Address.fromHex(addressHex)
+// Address hex: 58 chars (29 bytes, enterprise) or 114 chars (57 bytes, base)
+const address = Address.fromHex(
+ "01abc123def456abc123def456abc123def456abc123def456abc123deabc123def456abc123def456abc123def456abc123def456abc123de"
+)
```
## Convert Between Formats
diff --git a/docs/content/docs/addresses/conversion.mdx b/docs/content/docs/addresses/conversion.mdx
index 9ac3e385..dbf1a740 100644
--- a/docs/content/docs/addresses/conversion.mdx
+++ b/docs/content/docs/addresses/conversion.mdx
@@ -30,7 +30,7 @@ Binary format (Uint8Array), used internally and for serialization.
### Bech32 ↔ Address
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const bech32 = "addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd";
@@ -50,7 +50,7 @@ console.log("Bech32:", encoded);
### Hex ↔ Address
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const hexAddress = "019493315cd92eb5d8c4304e67b7e16ae36d61d34502694657811a2c8e32c728d3861e164cab28cb8f006448139c8f1740ffb8e7aa9e5232dc";
@@ -94,7 +94,7 @@ console.log("Decoded:", decoded);
Conversions can fail with invalid input:
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution";
+import { Address } from "@evolution-sdk/evolution";
const invalidBech32 = "invalid_address";
diff --git a/docs/content/docs/addresses/franken.mdx b/docs/content/docs/addresses/franken.mdx
index 99813389..ff7a5842 100644
--- a/docs/content/docs/addresses/franken.mdx
+++ b/docs/content/docs/addresses/franken.mdx
@@ -5,7 +5,9 @@ description: A pattern for combining payment and stake credentials from differen
# Franken Addresses (Hybrid Pattern)
-> **Important**: Franken addresses are **not a separate address type**. They are simply Base addresses where the payment and stake credentials happen to come from different sources. This is a construction pattern, not a distinct address format.
+
+Franken addresses are **not a separate address type**. They are simply Base addresses where the payment and stake credentials come from different sources. This is a construction pattern, not a distinct format.
+
Franken addresses (also called Frankenstein or chimera addresses) are a way of constructing base addresses where the payment credential and stake credential are **cryptographically independent** - they come from different wallets, smart contracts, or key sources.
diff --git a/docs/content/docs/addresses/index.mdx b/docs/content/docs/addresses/index.mdx
index 2e3c4c43..feaf170f 100644
--- a/docs/content/docs/addresses/index.mdx
+++ b/docs/content/docs/addresses/index.mdx
@@ -56,7 +56,7 @@ const address = new Address({
### Parse and Inspect Addresses
```typescript twoslash
-import { Address, KeyHash } from "@evolution-sdk/evolution"
+import { Address } from "@evolution-sdk/evolution"
const bech32 = "addr1qx2kd28nq8ac5prwg32hhvudlwggpgfp8utlyqxu6wqgz62f79qsdmm5dsknt9ecr5w468r9ey0fxwkdrwh08ly3tu9sy0f4qd"
diff --git a/docs/content/docs/addresses/meta.json b/docs/content/docs/addresses/meta.json
index 141488d3..e2122aec 100644
--- a/docs/content/docs/addresses/meta.json
+++ b/docs/content/docs/addresses/meta.json
@@ -7,6 +7,7 @@
"franken",
"construction",
"conversion",
- "validation"
+ "validation",
+ "address-eras"
]
}
diff --git a/docs/content/docs/advanced/custom-providers.mdx b/docs/content/docs/advanced/custom-providers.mdx
index a4e263d9..4db41aab 100644
--- a/docs/content/docs/advanced/custom-providers.mdx
+++ b/docs/content/docs/advanced/custom-providers.mdx
@@ -5,11 +5,13 @@ description: Implement your own blockchain provider
# Custom Providers
-If the built-in providers (Blockfrost, Maestro, Koios, Kupo/Ogmios) don't meet your needs, you can implement the `ProviderEffect` interface to create a custom provider.
+
+The SDK currently provides four built-in providers. Custom provider implementation requires working with the internal `ProviderEffect` interface — this is an advanced topic and the API may change.
+
## Provider Interface
-A provider must implement these methods:
+All providers implement the `ProviderEffect` interface internally. This is what each built-in provider (Blockfrost, Kupmios, Maestro, Koios) satisfies:
```typescript
interface ProviderEffect {
@@ -26,6 +28,8 @@ interface ProviderEffect {
}
```
+Understanding this interface is useful for knowing what data each provider must supply and for debugging provider-related errors.
+
## Built-in Providers
| Provider | Connection | Best For |
diff --git a/docs/content/docs/advanced/error-handling.mdx b/docs/content/docs/advanced/error-handling.mdx
index cb252f12..b20a5e81 100644
--- a/docs/content/docs/advanced/error-handling.mdx
+++ b/docs/content/docs/advanced/error-handling.mdx
@@ -15,9 +15,14 @@ Evolution SDK is built on Effect, providing structured error handling with typed
| **ProviderError** | Provider communication issues (network, API errors) |
| **EvaluationError** | Plutus script evaluation failures |
-## Promise API (Default)
+## Build Modes
-The standard `.build()`, `.sign()`, `.submit()` methods return Promises. Errors throw as exceptions:
+Evolution SDK offers three ways to handle errors from `.build()`:
+
+
+
+
+The standard `.build()` returns a Promise. Errors throw as exceptions:
```typescript twoslash
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
@@ -45,7 +50,8 @@ try {
}
```
-## Effect API
+
+
Use `.buildEffect()` for composable error handling with Effect:
@@ -73,7 +79,8 @@ const program = client
// Effect.runPromise(program).catch(console.error)
```
-## Either API
+
+
Use `.buildEither()` for explicit success/failure without exceptions:
@@ -98,7 +105,244 @@ const result = await client
// result is Either
```
+
+
+
+## All Error Types
+
+Evolution SDK uses tagged errors throughout. Here is the complete reference:
+
+### Transaction Building
+
+| Error | Source | Common Causes |
+| --- | --- | --- |
+| **TransactionBuilderError** | `Client.newTx().build()` | Insufficient funds, invalid parameters, missing required fields |
+| **EvaluationError** | Script evaluation phase | Plutus validator rejected the transaction, exceeded execution limits |
+| **CoinSelectionError** | UTxO selection phase | Wallet has insufficient funds, no UTxOs match required assets |
+
+### Provider
+
+| Error | Source | Common Causes |
+| --- | --- | --- |
+| **ProviderError** | Any provider call | Network timeout, invalid API key, rate limiting, endpoint down |
+
+### Encoding / Decoding
+
+| Error | Source | Common Causes |
+| --- | --- | --- |
+| **DataError** | `Data.withSchema()` codec operations | Schema mismatch, invalid PlutusData structure |
+| **CBORError** | CBOR encoding/decoding | Malformed CBOR bytes, unexpected data format |
+| **UPLCError** | UPLC operations | Invalid flat encoding, corrupted script bytes |
+
+### Wallet / Key
+
+| Error | Source | Common Causes |
+| --- | --- | --- |
+| **WalletError** | Wallet operations | CIP-30 wallet not connected, user rejected, missing API method |
+| **DerivationError** | Key derivation | Invalid mnemonic, bad derivation path |
+| **PrivateKeyError** | Private key operations | Invalid key bytes, signing failure |
+| **Bip32PrivateKeyError** | BIP-32 HD keys | Invalid extended key |
+| **Bip32PublicKeyError** | BIP-32 public keys | Invalid public key derivation |
+
+### Script
+
+| Error | Source | Common Causes |
+| --- | --- | --- |
+| **NativeScriptError** | Native script operations | Invalid timelock, bad multi-sig configuration |
+
+## Debugging Common Errors
+
+Jump to: [CoinSelectionError](#insufficient-funds-coinselectionerror) | [EvaluationError](#script-evaluation-failed-evaluationerror) | [ProviderError](#provider-request-failed-providererror) | [CBORError](#cbor-decoding-failed-cborerror)
+
+### "Insufficient funds" (CoinSelectionError)
+
+Your wallet doesn't have enough ADA or tokens for the transaction:
+
+```typescript
+try {
+ const tx = await client.newTx()
+ .payToAddress({ address, assets: Assets.fromLovelace(1000_000_000n) })
+ .build()
+} catch (e) {
+ // Check: Does the wallet have enough ADA?
+ // Check: Are UTxOs locked at script addresses?
+ // Check: Is there enough ADA for fees + min UTxO?
+ console.error(e)
+}
+```
+
+**Fix:** Query your wallet UTxOs first to verify available balance.
+
+### "Script evaluation failed" (EvaluationError)
+
+A Plutus validator rejected the transaction:
+
+```typescript
+try {
+ const tx = await client.newTx()
+ .collectFrom({ inputs: scriptUtxos, redeemer: Data.constr(0n, []) })
+ .attachScript({ script: validatorScript })
+ .build()
+} catch (e) {
+ // EvaluationError includes `failures` array with details
+ // Check: Is the redeemer correct for your validator?
+ // Check: Does the datum match what the validator expects?
+ // Check: Are required signers included?
+ // Check: Is the validity interval set correctly for time-locked scripts?
+ console.error(e)
+}
+```
+
+**Fix:** Use debug labels (`label: "my-operation"`) on `collectFrom` to identify which script failed. Check your redeemer matches the validator's expected action.
+
+### "Provider request failed" (ProviderError)
+
+Network or API issues:
+
+```typescript
+try {
+ const tx = await client.newTx()
+ .payToAddress({ address, assets })
+ .build()
+} catch (e) {
+ // Check: Is your API key valid?
+ // Check: Is the provider endpoint reachable?
+ // Check: Are you on the correct network (preprod vs mainnet)?
+ console.error(e)
+}
+```
+
+**Fix:** Verify your provider configuration, API key, and network connectivity.
+
+### "CBOR decoding failed" (CBORError)
+
+Malformed binary data:
+
+```typescript
+import { CBOR } from "@evolution-sdk/evolution"
+
+try {
+ const value = CBOR.fromCBORHex("invalid-hex")
+} catch (e) {
+ // Check: Is the hex string valid?
+ // Check: Is the data actually CBOR-encoded?
+ // Check: Are you using the correct encoding level (single vs double CBOR)?
+ console.error(e)
+}
+```
+
+**Fix:** Verify the hex string is valid and the correct encoding level. Use `UPLC.getCborEncodingLevel()` to check script encoding.
+
+## Inspecting Errors
+
+All Evolution SDK errors are tagged errors with structured fields. You can inspect them by checking the `_tag` property.
+
+### Identifying Error Types
+
+```typescript
+try {
+ const tx = await client.newTx()
+ .payToAddress({ address, assets })
+ .build()
+ const signed = await tx.sign()
+ await signed.submit()
+} catch (e: any) {
+ switch (e._tag) {
+ case "TransactionBuilderError":
+ console.error("Build failed:", e.message)
+ break
+ case "EvaluationError":
+ console.error("Script failed:", e.message)
+ // Inspect individual script failures
+ if (e.failures) {
+ for (const f of e.failures) {
+ console.error(` [${f.purpose}] ${f.label ?? "unlabeled"}: ${f.validationError}`)
+ if (f.traces.length > 0) {
+ console.error(" Traces:", f.traces.join(", "))
+ }
+ }
+ }
+ break
+ case "CoinSelectionError":
+ console.error("Insufficient funds:", e.message)
+ break
+ case "ProviderError":
+ console.error("Provider issue:", e.message)
+ break
+ default:
+ console.error("Unknown error:", e)
+ }
+}
+```
+
+### EvaluationError Script Failures
+
+When a Plutus script fails, `EvaluationError` contains a `failures` array with detailed information about each failed script:
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `purpose` | `string` | `"spend"`, `"mint"`, `"withdraw"`, or `"publish"` |
+| `index` | `number` | Index within the purpose category |
+| `label` | `string?` | Your debug label from `collectFrom({ label: "..." })` |
+| `validationError` | `string` | The error message from the validator |
+| `traces` | `string[]` | Execution traces emitted by the script (`trace` calls in Aiken/Plutus) |
+| `scriptHash` | `string?` | Hash of the failed script |
+| `utxoRef` | `string?` | UTxO reference (for spend redeemers) |
+| `policyId` | `string?` | Policy ID (for mint redeemers) |
+| `credential` | `string?` | Credential hash (for withdraw/cert redeemers) |
+
+### Using Labels for Debugging
+
+Add labels to your operations so script failures are easy to identify:
+
+```typescript
+const tx = await client
+ .newTx()
+ .collectFrom({
+ inputs: escrowUtxos,
+ redeemer: Data.constr(0n, []),
+ label: "claim-escrow" // This appears in EvaluationError.failures[].label
+ })
+ .collectFrom({
+ inputs: vestingUtxos,
+ redeemer: Data.constr(1n, []),
+ label: "unlock-vesting" // Different label for each operation
+ })
+ .attachScript({ script: escrowScript })
+ .attachScript({ script: vestingScript })
+ .build()
+```
+
+When a script fails, the error tells you exactly which operation caused it:
+
+```
+EvaluationError: Script evaluation failed
+ [spend] claim-escrow: Validator returned False
+ Traces: "deadline not reached", "current time: 1735600000"
+```
+
+### Safe Parsing with Either
+
+For non-throwing error handling, use `buildEither()`:
+
+```typescript
+const result = await client
+ .newTx()
+ .payToAddress({ address, assets })
+ .buildEither()
+
+if (result._tag === "Left") {
+ // result.left is the error — inspect with _tag
+ console.error("Failed:", result.left)
+} else {
+ // result.right is the SignBuilder
+ const signed = await result.right.sign()
+ await signed.submit()
+}
+```
+
## Next Steps
- [Architecture](/docs/advanced/architecture) — How errors flow through build phases
- [TypeScript Tips](/docs/advanced/typescript) — Type patterns with Effect
+- [UPLC](/docs/encoding/uplc) — UPLC error debugging
diff --git a/docs/content/docs/advanced/meta.json b/docs/content/docs/advanced/meta.json
index 91b4b2b5..bd6b25c8 100644
--- a/docs/content/docs/advanced/meta.json
+++ b/docs/content/docs/advanced/meta.json
@@ -2,9 +2,10 @@
"title": "Advanced",
"pages": [
"index",
- "performance",
- "debugging",
+ "architecture",
+ "custom-providers",
"error-handling",
- "best-practices"
+ "performance",
+ "typescript"
]
}
diff --git a/docs/content/docs/advanced/typescript.mdx b/docs/content/docs/advanced/typescript.mdx
index d3c47e90..48817769 100644
--- a/docs/content/docs/advanced/typescript.mdx
+++ b/docs/content/docs/advanced/typescript.mdx
@@ -46,12 +46,12 @@ import { preprod, Client } from "@evolution-sdk/evolution"
// Provider-only client (no wallet) — can query but not sign
const queryClient = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
// Signing client (wallet + provider) — full capabilities
const signingClient = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
```
## Effect Integration
diff --git a/docs/content/docs/api-overview.mdx b/docs/content/docs/api-overview.mdx
new file mode 100644
index 00000000..b0f4e576
--- /dev/null
+++ b/docs/content/docs/api-overview.mdx
@@ -0,0 +1,46 @@
+---
+title: API Overview
+description: Find the right module for what you need to do
+---
+
+# API Overview
+
+Find the right module by what you want to do. For full API docs, browse the [Module Reference](/docs/modules).
+
+## Find by Task
+
+| I want to... | Module | Key Functions |
+| --- | --- | --- |
+| **Parse or create an address** | [Address](/docs/modules/Address) | `fromBech32`, `fromHex`, `toHex`, `toBech32`, `fromSeed` |
+| **Work with ADA and tokens** | [Assets](/docs/modules/Assets) | `fromLovelace`, `addByHex`, `merge` |
+| **Build a transaction** | [TransactionBuilder](/docs/modules/sdk/builders/TransactionBuilder) | `newTx`, `payToAddress`, `collectFrom`, `build` |
+| **Sign a transaction** | [SignBuilder](/docs/modules/sdk/builders/SignBuilder) | `sign`, `signAndSubmit`, `partialSign` |
+| **Submit a transaction** | [SubmitBuilder](/docs/modules/sdk/builders/SubmitBuilder) | `submit` |
+| **Create PlutusData** | [Data](/docs/modules/Data) | `constr`, `int`, `bytearray`, `list`, `map` |
+| **Define type-safe schemas** | [TSchema](/docs/modules/TSchema) | `Struct`, `Variant`, `Array`, `Map`, `ByteArray`, `Integer` |
+| **Encode/decode CBOR** | [CBOR](/docs/modules/CBOR) | `fromCBORHex`, `toCBORHex`, `match` |
+| **Work with UPLC scripts** | [UPLC](/docs/modules/UPLC) | `applyParamsToScript`, `fromFlatBytes`, `toFlatBytes` |
+| **Connect a provider** | [Client](/docs/modules/sdk/client/Client) | `make`, `withBlockfrost`, `withKupmios` |
+| **Query UTxOs** | [Provider](/docs/modules/sdk/provider/Provider) | `getUtxos`, `getUtxoByUnit`, `getProtocolParameters` |
+| **Manage wallets** | [Wallet](/docs/modules/sdk/wallet/Wallet) | `withSeed`, `withPrivateKey`, `withCip30` |
+| **Derive keys** | [Derivation](/docs/modules/sdk/wallet/Derivation) | `walletFromSeed`, `keysFromSeed`, `addressFromSeed` |
+| **Sign messages (CIP-30)** | [SignData](/docs/modules/cose/SignData) | `signData`, `verifyData` |
+| **Generate from blueprints** | [codegen](/docs/modules/blueprint/codegen) | `generateTypeScript` |
+| **Work with credentials** | [Credential](/docs/modules/Credential) | `makeKeyHash`, `makeScriptHash` |
+| **Handle governance** | [DRep](/docs/modules/DRep), [VotingProcedures](/docs/modules/VotingProcedures) | `fromKeyHash`, `vote`, `propose` |
+
+## Module Categories
+
+### Core Types
+Address, Assets, Credential, Data, TSchema, CBOR, UPLC, Transaction, UTxO, Value
+
+### SDK
+Client, Provider (Blockfrost, Kupmios, Maestro, Koios), Wallet, TransactionBuilder, SignBuilder, SubmitBuilder, CoinSelection
+
+### Domains
+Blueprint (codegen), COSE (message signing), Plutus (on-chain types)
+
+### Primitives
+Bytes, BigInt, Coin, PolicyId, ScriptHash, KeyHash, Slot, UnixTime, and 100+ more
+
+For the complete list, browse the [Module Reference](/docs/modules) sidebar.
diff --git a/docs/content/docs/architecture/deferred-execution.mdx b/docs/content/docs/architecture/deferred-execution.mdx
index d950085e..2769e6fc 100644
--- a/docs/content/docs/architecture/deferred-execution.mdx
+++ b/docs/content/docs/architecture/deferred-execution.mdx
@@ -66,8 +66,6 @@ graph TB
**[3] Second build() Call**: Creates new fresh `Ref`, re-executes all programs, returns independent transaction. No state shared with first execution.
-**[3] Second build() Call**: Creates new fresh `Ref`, re-executes all programs, returns independent transaction. No state shared with first execution.
-
## Execution Sequence
Programs execute sequentially in append order. Each program can observe effects of previous programs within the same build cycle:
@@ -102,8 +100,6 @@ sequenceDiagram
**[3] Transaction Assembly**: Final state is read, transaction body constructed, witnesses prepared.
-**[3] Transaction Assembly**: Final state is read, transaction body constructed, witnesses prepared.
-
## Integration Points
Deferred execution integrates with other architectural layers:
diff --git a/docs/content/docs/architecture/index.mdx b/docs/content/docs/architecture/index.mdx
index a5c48256..9c96fdee 100644
--- a/docs/content/docs/architecture/index.mdx
+++ b/docs/content/docs/architecture/index.mdx
@@ -197,6 +197,6 @@ Architectural patterns provide consistency:
## Related Topics
-- [Getting Started](/docs/getting-started) - Practical usage examples
+- [Getting Started](/docs/introduction/getting-started) - Practical usage examples
- [Client Basics](/docs/clients) - Client API and capabilities
- [Transactions](/docs/transactions) - Transaction building guide
diff --git a/docs/content/docs/architecture/meta.json b/docs/content/docs/architecture/meta.json
index c9ff0c8a..bcaedea6 100644
--- a/docs/content/docs/architecture/meta.json
+++ b/docs/content/docs/architecture/meta.json
@@ -7,6 +7,7 @@
"redeemer-indexing",
"script-evaluation",
"unfrack-optimization",
+ "deferred-execution",
"devnet",
"provider-layer",
"wallet-layer"
diff --git a/docs/content/docs/architecture/provider-layer.mdx b/docs/content/docs/architecture/provider-layer.mdx
index f18680d2..931b1a22 100644
--- a/docs/content/docs/architecture/provider-layer.mdx
+++ b/docs/content/docs/architecture/provider-layer.mdx
@@ -21,8 +21,6 @@ The architecture establishes a thin interface capturing what transaction buildin
The abstraction remains thin by design—it does not attempt to expose every provider's unique features. Focused scope keeps the interface stable and implementations maintainable.
-The abstraction remains thin by design—it does not attempt to expose every provider's unique features. Focused scope keeps the interface stable and implementations maintainable.
-
## Provider Interface Contract
All providers implement the same core operations required for transaction building:
@@ -62,15 +60,13 @@ graph TD
**[1] Provider Interface**: Defines required operations:
- `getUtxos(address)` - Query unspent outputs at address
- `getProtocolParameters()` - Fetch current protocol parameters
-- `submitTx(cbor)` - Submit signed transaction
+- `submitTx(tx)` - Submit signed transaction
- `evaluateTx(tx, utxos)` - Calculate script execution costs
- `getDatum(hash)` - Retrieve datum by hash
- `awaitTx(hash)` - Wait for transaction confirmation
**[2] Provider Implementations**: Each translates interface calls to their native API. Application code depends only on interface, never on specific implementation.
-**[2] Provider Implementations**: Each translates interface calls to their native API. Application code depends only on interface, never on specific implementation.
-
## Type-Driven Configuration
Provider configuration uses discriminated unions to enforce correctness at compile time:
diff --git a/docs/content/docs/architecture/wallet-layer.mdx b/docs/content/docs/architecture/wallet-layer.mdx
index 28cf38a3..baa4ac29 100644
--- a/docs/content/docs/architecture/wallet-layer.mdx
+++ b/docs/content/docs/architecture/wallet-layer.mdx
@@ -21,8 +21,6 @@ The architecture encodes capability in types. A `ReadOnlyWallet` produces a `Rea
This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming.
-This separation provides security by design: applications needing only monitoring cannot accidentally expose signing capability. The type system makes "read-only" genuinely read-only at the language level, not through defensive programming.
-
## Wallet Capability Hierarchy
Wallets separate into two capability levels, with signing wallets further divided by key management approach:
@@ -53,10 +51,10 @@ graph TD
style Api fill:#ecf0f1,stroke:#34495e,stroke-width:3px,color:#2c3e50
`} />
-**[1] Wallet Base**: Common operations all wallets support:
+**[1] Wallet Base**: Common operation all wallets support:
- `address()` - Get wallet address
-- `getUtxos()` - Query UTxOs at wallet address
-- `getBalance()` - Query total balance
+
+UTxO queries (`getWalletUtxos()`) require a provider and are available on the client, not the wallet itself.
**[2] ReadOnlyWallet**: Base operations only. No signing methods exist. Produced from address or credential without keys.
@@ -69,8 +67,6 @@ Three signing implementations:
- **PrivateKeyWallet**: Extended private key (xprv)
- **ApiWallet**: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets)
-- **ApiWallet**: CIP-30 browser wallet API (Nami, Eternl, Flint, hardware wallets)
-
## Client Type Determination
Wallet capability determines the signing side of staged client assembly. The final client type depends on whether a provider is also present:
diff --git a/docs/content/docs/assets/index.mdx b/docs/content/docs/assets/index.mdx
index c079dd05..b89297c8 100644
--- a/docs/content/docs/assets/index.mdx
+++ b/docs/content/docs/assets/index.mdx
@@ -21,7 +21,7 @@ const adaOnly = Assets.fromLovelace(5_000_000n)
let assets = Assets.fromLovelace(2_000_000n)
assets = Assets.addByHex(
assets,
- "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6", // policy ID
+ "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0", // policy ID (56 hex chars)
"", // asset name (empty for fungible tokens)
100n // quantity
)
diff --git a/docs/content/docs/assets/units.mdx b/docs/content/docs/assets/units.mdx
index 5461e984..0644c46a 100644
--- a/docs/content/docs/assets/units.mdx
+++ b/docs/content/docs/assets/units.mdx
@@ -20,7 +20,7 @@ Every native asset on Cardano is identified by two components: a **policy ID** (
```typescript twoslash
import { Assets } from "@evolution-sdk/evolution"
-const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6"
+const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0" // 56 hex chars = 28 bytes
const assetName = "4d79546f6b656e" // "MyToken" in hex
// Add native token to an asset bundle
diff --git a/docs/content/docs/clients/architecture.mdx b/docs/content/docs/clients/architecture.mdx
index 10b104e0..530e18c9 100644
--- a/docs/content/docs/clients/architecture.mdx
+++ b/docs/content/docs/clients/architecture.mdx
@@ -30,7 +30,7 @@ interface ReadOnlyWalletConfig {
### Backend Transaction Building
-```typescript twoslash
+```typescript twoslash title="server/build-tx.ts"
import { Address, Assets, Transaction, mainnet, Client } from "@evolution-sdk/evolution"
// Backend: Create provider client, then attach read-only wallet
@@ -83,7 +83,7 @@ Frontend applications connect to user wallets through CIP-30 but never have prov
### Implementation
-```typescript
+```typescript title="app/sign-tx.ts"
import { Address, Transaction, TransactionWitnessSet, mainnet, Client } from "@evolution-sdk/evolution"
// 1. Connect wallet
@@ -146,7 +146,7 @@ Backend services use read-only wallets configured with user addresses to build u
### Implementation
-```typescript
+```typescript title="server/api/build-tx.ts"
import { Address, Assets, Transaction, mainnet, Client } from "@evolution-sdk/evolution"
// Backend endpoint
@@ -292,8 +292,6 @@ export async function submitSignedTx(signedTxCbor: string): Promise
-
- Learn about API wallet clients for frontend signing
+
+ CIP-30 browser wallet integration for frontend signing
-
- Learn about read-only clients for backend building
+
+ Read-only wallets and backend transaction building
Understand different wallet types used in clients
diff --git a/docs/content/docs/clients/client-basics.mdx b/docs/content/docs/clients/client-basics.mdx
index 27c9f521..e46c70e8 100644
--- a/docs/content/docs/clients/client-basics.mdx
+++ b/docs/content/docs/clients/client-basics.mdx
@@ -39,8 +39,8 @@ Start with `client.newTx()` and chain operations to specify outputs, metadata, o
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
const builder = client.newTx()
@@ -62,8 +62,8 @@ Call `.sign()` on the built transaction to create signatures with your wallet:
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
const builder = client.newTx()
builder.payToAddress({
@@ -85,8 +85,8 @@ Finally, `.submit()` broadcasts the signed transaction to the blockchain and ret
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
const builder = client.newTx()
builder.payToAddress({
diff --git a/docs/content/docs/clients/index.mdx b/docs/content/docs/clients/index.mdx
index 79ecd954..6be581db 100644
--- a/docs/content/docs/clients/index.mdx
+++ b/docs/content/docs/clients/index.mdx
@@ -5,7 +5,7 @@ description: "Client types combining wallets and providers"
# Clients
-Clients assemble chain-scoped capabilities into one runtime surface. Start with `client(chain)`, then add read access, address context, or signing capability with `.withX(...)`.
+Clients assemble chain-scoped capabilities into one runtime surface. Start with `Client.make(chain)`, then add read access, address context, or signing capability with `.withX(...)`.
Submission only appears once a provider stage is present. Wallet-only assembly stages can sign, but they still hand signed transactions to a provider-backed client or backend for broadcast.
@@ -31,17 +31,17 @@ Different combinations create different client stages with distinct capabilities
## Configuration Pattern
-All clients start with `client(chain)` and add capabilities as needed:
+All clients start with `Client.make(chain)` and add capabilities as needed:
```typescript
-const readClient = client(preprod).withBlockfrost({ ... })
-const addressClient = client(preprod).withAddress("addr_test1...")
-const signingClient = client(preprod).withBlockfrost({ ... }).withSeed({ ... })
+const readClient = Client.make(preprod).withBlockfrost({ ... })
+const addressClient = Client.make(preprod).withAddress("addr_test1...")
+const signingClient = Client.make(preprod).withBlockfrost({ ... }).withSeed({ ... })
```
**Rules**:
-- `client(chain)` is the empty assembly stage
+- `Client.make(chain)` is the empty assembly stage
- Add a provider first when you need blockchain reads or submission
- Add `.withAddress()` when you only need wallet context
- Add `.withSeed()`, `.withPrivateKey()`, or `.withCip30()` when you need signing
diff --git a/docs/content/docs/clients/providers.mdx b/docs/content/docs/clients/providers.mdx
index 73cded0a..cc867325 100644
--- a/docs/content/docs/clients/providers.mdx
+++ b/docs/content/docs/clients/providers.mdx
@@ -78,21 +78,21 @@ import { preprod, Client } from "@evolution-sdk/evolution"
const blockfrostClient = Client.make(preprod)
.withBlockfrost({
baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
- projectId: "key1"
+ projectId: process.env.BLOCKFROST_API_KEY!
})
.withSeed({
- mnemonic: "",
+ mnemonic: process.env.WALLET_MNEMONIC!,
accountIndex: 0
})
-// Switch to Kupmios
+// Switch to Kupmios — same wallet, different provider
const kupmiosClient = Client.make(preprod)
.withKupmios({
kupoUrl: "http://localhost:1442",
ogmiosUrl: "http://localhost:1337"
})
.withSeed({
- mnemonic: "",
+ mnemonic: process.env.WALLET_MNEMONIC!,
accountIndex: 0
})
```
diff --git a/docs/content/docs/common-patterns.mdx b/docs/content/docs/common-patterns.mdx
new file mode 100644
index 00000000..4eedccd2
--- /dev/null
+++ b/docs/content/docs/common-patterns.mdx
@@ -0,0 +1,299 @@
+---
+title: Common Patterns
+description: Quick recipes for frequent tasks — copy, paste, and adapt
+---
+
+# Common Patterns
+
+Task-oriented recipes for the most common operations. Each pattern is self-contained — copy, paste, adapt.
+
+For full tutorials, see the dedicated guide sections linked from each recipe.
+
+---
+
+## Send ADA to an Address
+
+```typescript twoslash
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
+ assets: Assets.fromLovelace(5_000_000n) // 5 ADA
+ })
+ .build()
+
+const signed = await tx.sign()
+const hash = await signed.submit()
+```
+
+**More:** [Simple Payment](/docs/transactions/simple-payment) | [Multi-Output](/docs/transactions/multi-output)
+
+---
+
+## Query Wallet UTxOs
+
+```typescript twoslash
+import { Address, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// Get all UTxOs at your wallet address
+const utxos = await client.getWalletUtxos()
+// utxos → UTxO[] with .txHash, .outputIndex, .assets, .datum
+
+// Get UTxOs at a specific address
+const addr = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")
+const addrUtxos = await client.getUtxos(addr)
+```
+
+**More:** [Querying UTxOs](/docs/querying/utxos)
+
+---
+
+## Lock Funds to a Script
+
+```typescript twoslash
+import { Address, Assets, Data, InlineDatum, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const scriptAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu")
+
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: scriptAddress,
+ assets: Assets.fromLovelace(10_000_000n),
+ datum: new InlineDatum.InlineDatum({ data: Data.constr(0n, []) })
+ })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+**More:** [Locking to Script](/docs/smart-contracts/locking) | [Datums](/docs/smart-contracts/datums)
+
+---
+
+## Spend from a Script
+
+```typescript twoslash
+import { Data, preprod, type UTxO, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
+
+const tx = await client
+ .newTx()
+ .collectFrom({
+ inputs: scriptUtxos,
+ redeemer: Data.constr(0n, [])
+ })
+ .attachScript({ script: validatorScript })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+**More:** [Spending from Script](/docs/smart-contracts/spending) | [Redeemers](/docs/smart-contracts/redeemers)
+
+---
+
+## Apply Parameters to a Compiled Script
+
+```typescript twoslash
+import { Data, UPLC } from "@evolution-sdk/evolution"
+
+declare const compiledScript: string
+
+const applied = UPLC.applyParamsToScript(compiledScript, [
+ Data.bytearray("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ Data.int(1735689600000n),
+])
+// applied → double-CBOR hex string ready for transaction use
+```
+
+**More:** [Parameterized Scripts](/docs/smart-contracts/apply-params)
+
+---
+
+## Define a Type-Safe Datum
+
+```typescript twoslash
+import { Bytes, Data, TSchema } from "@evolution-sdk/evolution"
+
+const EscrowDatum = TSchema.Struct({
+ beneficiary: TSchema.ByteArray,
+ deadline: TSchema.Integer,
+ amount: TSchema.Integer,
+})
+
+type EscrowDatum = typeof EscrowDatum.Type
+
+const Codec = Data.withSchema(EscrowDatum)
+
+// toData → PlutusData (Constr with 3 fields)
+const datum = Codec.toData({
+ beneficiary: Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ deadline: 1735689600000n,
+ amount: 25_000_000n,
+})
+
+// toCBORHex → CBOR hex string for on-chain use
+const cbor = Codec.toCBORHex({
+ beneficiary: Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ deadline: 1735689600000n,
+ amount: 25_000_000n,
+})
+// cbor → "d8799f4e...1a017d7840ff" (ready for datum field)
+```
+
+**More:** [TSchema](/docs/encoding/tschema) | [Datums](/docs/smart-contracts/datums)
+
+---
+
+## Register a Stake Key
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const stakeCredential: Credential.Credential
+
+const tx = await client
+ .newTx()
+ .registerStake({ stakeCredential })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+**More:** [Staking Registration](/docs/staking/registration) | [Delegation](/docs/staking/delegation)
+
+---
+
+## Set Transaction Validity Window
+
+```typescript twoslash
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const now = BigInt(Date.now())
+
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
+ assets: Assets.fromLovelace(2_000_000n)
+ })
+ .setValidity({
+ from: now,
+ to: now + 300_000n // 5 minutes
+ })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+**More:** [Validity Ranges](/docs/time/validity-ranges)
+
+---
+
+## Sign and Verify a Message (CIP-30)
+
+```typescript
+import { COSE, PrivateKey, Address } from "@evolution-sdk/evolution"
+
+declare const privateKey: PrivateKey.PrivateKey
+declare const myAddress: Address.Address
+
+const payload = COSE.Utils.fromText("Login to MyDApp")
+
+const signed = COSE.SignData.signData(
+ Address.toHex(myAddress),
+ payload,
+ privateKey
+)
+// signed.signature — CBOR-encoded COSE_Sign1
+// signed.key — CBOR-encoded COSE_Key
+```
+
+**More:** [Message Signing](/docs/wallets/message-signing)
+
+---
+
+## Handle Errors
+
+```typescript
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+try {
+ const tx = await client
+ .newTx()
+ .payToAddress({
+ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
+ assets: Assets.fromLovelace(2_000_000n)
+ })
+ .build()
+
+ const signed = await tx.sign()
+ await signed.submit()
+} catch (error) {
+ // TransactionBuilderError — build phase failure
+ // EvaluationError — script evaluation failure
+ // ProviderError — network/API error
+ // CoinSelectionError — insufficient funds
+ console.error(error)
+}
+```
+
+**More:** [Error Handling](/docs/advanced/error-handling)
diff --git a/docs/content/docs/devnet/configuration.mdx b/docs/content/docs/devnet/configuration.mdx
index 346311cf..70572c9e 100644
--- a/docs/content/docs/devnet/configuration.mdx
+++ b/docs/content/docs/devnet/configuration.mdx
@@ -30,7 +30,7 @@ import { Address,
} from "@evolution-sdk/evolution"
// Derive address from seed (synchronous, no network required)
-const address = Address.fromSeed("your twenty-four word mnemonic phrase here", {
+const address = Address.fromSeed("test test test test test test test test test test test test test test test test test test test test test test test sauce", {
accountIndex: 0,
networkId: 0 // testnet address format
})
@@ -107,7 +107,9 @@ This pattern enables testing multi-party protocols, delegation scenarios, and wa
After creating a genesis configuration, calculate the resulting UTxOs to verify addresses and amounts.
-**Important**: Genesis UTxOs do NOT appear in Kupo's index because Kupo reads chain events starting from block 1, while genesis UTxOs exist in block 0 (the genesis block itself). You must use `calculateUtxosFromConfig` to derive these UTxOs and provide them to your transaction builder via the `availableUtxos` parameter. After spending a genesis UTxO, the resulting outputs will be indexed normally by Kupo.
+
+**Genesis UTxOs do NOT appear in Kupo's index** — Kupo reads from block 1, but genesis UTxOs are in block 0. Use `calculateUtxosFromConfig` to derive these UTxOs and pass them via `availableUtxos`. After spending a genesis UTxO, resulting outputs are indexed normally.
+
```typescript twoslash
import { Config, Genesis } from "@evolution-sdk/devnet"
@@ -224,7 +226,9 @@ Common protocol parameter customizations:
- **collateralPercentage**: Collateral % for script transactions (default: 150)
- **maxCollateralInputs**: Max collateral inputs (default: 3)
-**Important**: The devnet runs Conway era. At runtime, the node uses `coinsPerUtxoByte` (from Alonzo's `lovelacePerUTxOWord`), not Shelley's legacy `minUTxOValue`.
+
+The devnet runs Conway era. At runtime, the node uses `coinsPerUtxoByte` (from Alonzo's `lovelacePerUTxOWord`), not Shelley's legacy `minUTxOValue`.
+
See the exported types `Config.ShelleyGenesis`, `Config.AlonzoGenesis`, and `Config.ConwayGenesis` for complete definitions.
diff --git a/docs/content/docs/devnet/integration.mdx b/docs/content/docs/devnet/integration.mdx
index f115e4e1..9260422f 100644
--- a/docs/content/docs/devnet/integration.mdx
+++ b/docs/content/docs/devnet/integration.mdx
@@ -37,7 +37,7 @@ import { Address,
Client,
} from "@evolution-sdk/evolution"
-const MNEMONIC = "your twenty-four word mnemonic phrase here"
+const MNEMONIC = "test test test test test test test test test test test test test test test test test test test test test test test sauce"
async function completeWorkflow() {
// Step 1: Derive sender address (synchronous, no network required)
diff --git a/docs/content/docs/encoding/index.mdx b/docs/content/docs/encoding/index.mdx
index b6ad672a..ac5377e5 100644
--- a/docs/content/docs/encoding/index.mdx
+++ b/docs/content/docs/encoding/index.mdx
@@ -9,6 +9,27 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
Evolution SDK handles multiple encoding formats used across the Cardano ecosystem — hex for raw bytes, Bech32 for addresses, CBOR for on-chain data, and JSON for interchange. The encoding modules provide type-safe conversion between these formats.
+## How They Connect
+
+import { Mermaid } from "@/components/mdx/mermaid"
+
+(string, bigint, Uint8Array)"] --> B["TSchema
Type-safe schema"]
+ B --> C["PlutusData
(Constr, Int, Bytes, List, Map)"]
+ C --> D["CBOR
Binary encoding"]
+ D --> E["Hex
Hex string"]
+
+ F["Bech32
addr_test1..."] -.-> G["Address
Bytes"]
+ G -.-> E
+
+ style B fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
+ style C fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f
+ style D fill:#f3e8ff,stroke:#7c3aed,stroke-width:2px,color:#4c1d95
+`} />
+
+**Typical flow:** Define a schema with TSchema → encode values to PlutusData → serialize to CBOR → get hex for transactions. Addresses use a separate Bech32 path.
+
## Encoding Formats
| Format | Use Case | Example |
diff --git a/docs/content/docs/encoding/meta.json b/docs/content/docs/encoding/meta.json
index f3d36b50..48eeb63c 100644
--- a/docs/content/docs/encoding/meta.json
+++ b/docs/content/docs/encoding/meta.json
@@ -8,6 +8,7 @@
"json",
"data",
"tschema",
- "plutus"
+ "plutus",
+ "uplc"
]
}
diff --git a/docs/content/docs/encoding/plutus.mdx b/docs/content/docs/encoding/plutus.mdx
index 508dc074..095c40e4 100644
--- a/docs/content/docs/encoding/plutus.mdx
+++ b/docs/content/docs/encoding/plutus.mdx
@@ -413,7 +413,7 @@ const orderDatum: DexOrderDatum = {
[new Uint8Array(), new Map([[new Uint8Array(), 100000000n]])] // 100 ADA
]),
requested_value: new Map([
- [Bytes.fromHex("token_policy_id_28_bytes_hex_encoded_here_abcd"), new Map([
+ [Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8"), new Map([
[Text.toBytes("USDC"), 10000000n] // 10 USDC (6 decimals)
])]
]),
diff --git a/docs/content/docs/encoding/tschema.mdx b/docs/content/docs/encoding/tschema.mdx
index 8c652161..6580e4a8 100644
--- a/docs/content/docs/encoding/tschema.mdx
+++ b/docs/content/docs/encoding/tschema.mdx
@@ -234,6 +234,162 @@ const person2: Person = {
}
```
+### NullOr
+
+Alternative to `UndefinedOr` — represents optional values as `T | null`:
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const ConfigSchema = TSchema.Struct({
+ timeout: TSchema.Integer,
+ retryLimit: TSchema.NullOr(TSchema.Integer)
+})
+
+type Config = typeof ConfigSchema.Type
+// {
+// timeout: bigint
+// retryLimit: bigint | null
+// }
+
+const Codec = Data.withSchema(ConfigSchema)
+
+const config: Config = {
+ timeout: 30000n,
+ retryLimit: null // No retry limit
+}
+```
+
+### Boolean
+
+Maps `true`/`false` to Plutus constructors (`True = Constr(1, [])`, `False = Constr(0, [])`):
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const SettingsSchema = TSchema.Struct({
+ isActive: TSchema.Boolean,
+ amount: TSchema.Integer
+})
+
+const Codec = Data.withSchema(SettingsSchema)
+
+const settings = Codec.toData({ isActive: true, amount: 100n })
+```
+
+### PlutusData
+
+Escape hatch for arbitrary PlutusData — use when a field can hold any Plutus data:
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const FlexibleDatumSchema = TSchema.Struct({
+ version: TSchema.Integer,
+ payload: TSchema.PlutusData // Accepts any PlutusData
+})
+
+const Codec = Data.withSchema(FlexibleDatumSchema)
+
+const datum = Codec.toData({
+ version: 1n,
+ payload: Data.constr(0n, [Data.int(42n)])
+})
+```
+
+### Literal
+
+For enum-like constructors with no fields:
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const UnitSchema = TSchema.Literal("Unit" as const)
+
+const Codec = Data.withSchema(UnitSchema)
+
+const unit = Codec.toData("Unit" as const)
+```
+
+### Tuple
+
+For fixed-length positional data (encoded as a Plutus constructor with indexed fields):
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const PairSchema = TSchema.Tuple([TSchema.ByteArray, TSchema.Integer])
+
+type Pair = typeof PairSchema.Type
+// readonly [Uint8Array, bigint]
+
+const Codec = Data.withSchema(PairSchema)
+
+const pair: Pair = [new Uint8Array(28), 42n]
+const cbor = Codec.toCBORHex(pair)
+```
+
+### TaggedStruct
+
+Like `Struct` but includes an Effect `_tag` field for pattern matching:
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const ClaimAction = TSchema.TaggedStruct("Claim", {})
+const UpdateAction = TSchema.TaggedStruct("Update", {
+ newValue: TSchema.Integer
+})
+
+type Claim = typeof ClaimAction.Type
+// { readonly _tag: "Claim" }
+
+type Update = typeof UpdateAction.Type
+// { readonly _tag: "Update"; readonly newValue: bigint }
+```
+
+### Union (Direct)
+
+Combine multiple schemas into a discriminated union — useful for complex redeemer types:
+
+```typescript twoslash
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+const ActionSchema = TSchema.Union(
+ TSchema.TaggedStruct("Claim", {}),
+ TSchema.TaggedStruct("Cancel", {}),
+ TSchema.TaggedStruct("Update", { amount: TSchema.Integer })
+)
+
+type Action = typeof ActionSchema.Type
+
+const Codec = Data.withSchema(ActionSchema)
+
+const claim: Action = { _tag: "Claim" }
+const update: Action = { _tag: "Update", amount: 500n }
+```
+
+## Utility Functions
+
+TSchema re-exports several Effect Schema utilities:
+
+```typescript
+import { TSchema } from "@evolution-sdk/evolution"
+
+// Type guard
+TSchema.is(MySchema)(someValue) // boolean
+
+// Compose schemas
+const Composed = TSchema.compose(SchemaA, SchemaB)
+
+// Filter with refinement
+const Positive = TSchema.filter(TSchema.Integer, (n) => n > 0n)
+
+// Structural equality
+const eq = TSchema.equivalence(MySchema)
+eq(a, b) // boolean
+```
+
## Creating Codecs
Use `Data.withSchema()` to create a codec from any schema:
diff --git a/docs/content/docs/encoding/uplc.mdx b/docs/content/docs/encoding/uplc.mdx
new file mode 100644
index 00000000..63beb126
--- /dev/null
+++ b/docs/content/docs/encoding/uplc.mdx
@@ -0,0 +1,201 @@
+---
+title: UPLC
+description: Working with Untyped Plutus Lambda Calculus programs
+---
+
+# UPLC
+
+UPLC (Untyped Plutus Lambda Calculus) is the low-level language that Plutus smart contracts compile to. Evolution SDK provides a complete UPLC module for parsing, constructing, encoding, and manipulating UPLC programs.
+
+Most developers interact with UPLC indirectly through `applyParamsToScript` (see [Parameterized Scripts](/docs/smart-contracts/apply-params)). This guide covers the UPLC module in depth for advanced use cases.
+
+## Program Structure
+
+A UPLC program consists of a version and a body (a term):
+
+```typescript
+import { UPLC } from "@evolution-sdk/evolution"
+
+// Parse a program from flat-encoded bytes
+declare const flatBytes: Uint8Array
+const program = UPLC.fromFlatBytes(flatBytes)
+
+console.log(program.version) // e.g., "1.1.0"
+console.log(program.body) // The root Term
+```
+
+## CBOR Encoding Levels
+
+Plutus scripts on-chain are typically **double CBOR-encoded**: the Flat-encoded bytes are wrapped in CBOR bytes, then wrapped again. Evolution SDK handles all encoding levels:
+
+```typescript
+import { UPLC } from "@evolution-sdk/evolution"
+
+declare const scriptHex: string
+
+// Detect encoding level
+const level = UPLC.getCborEncodingLevel(scriptHex)
+// "double" | "single" | "none"
+
+// Convert between encoding levels
+const singleEncoded = UPLC.applySingleCborEncoding(scriptHex)
+const doubleEncoded = UPLC.applyDoubleCborEncoding(scriptHex)
+
+// Decode double-CBOR to flat bytes
+const flatBytes = UPLC.decodeDoubleCborHexToFlat(scriptHex)
+
+// Parse directly from CBOR hex to a Program
+const program = UPLC.fromCborHexToProgram(scriptHex)
+```
+
+## Flat Encoding
+
+Flat is the binary serialization format for UPLC. Convert between programs and flat bytes:
+
+```typescript
+import { UPLC } from "@evolution-sdk/evolution"
+
+declare const program: UPLC.Program
+
+// Program → flat bytes
+const flatBytes = UPLC.toFlatBytes(program)
+const flatHex = UPLC.toFlatHex(program)
+
+// Flat bytes → Program
+const parsed = UPLC.fromFlatBytes(flatBytes)
+const parsedFromHex = UPLC.fromFlatHex(flatHex)
+```
+
+## Constructing Terms
+
+Build UPLC terms programmatically using the term constructors:
+
+```typescript
+import { UPLC, Data, CBOR } from "@evolution-sdk/evolution"
+
+// Variable reference
+const x = UPLC.varTerm(0n)
+
+// Lambda abstraction
+const identity = UPLC.lambdaTerm(0n, UPLC.varTerm(0n))
+
+// Function application
+const applied = UPLC.applyTerm(identity, UPLC.constantTerm("integer", 42n))
+
+// Builtin function
+const addInteger = UPLC.builtinTerm("addInteger")
+
+// Delay and Force (for lazy evaluation)
+const delayed = UPLC.delayTerm(UPLC.constantTerm("integer", 1n))
+const forced = UPLC.forceTerm(delayed)
+
+// Constructor (Plutus V3)
+const constr = UPLC.constrTerm(0n, [
+ UPLC.constantTerm("integer", 100n)
+])
+
+// Case expression (Plutus V3)
+const caseExpr = UPLC.caseTerm(constr, [
+ UPLC.constantTerm("integer", 1n),
+ UPLC.constantTerm("integer", 2n)
+])
+
+// Error term
+const err = UPLC.errorTerm
+```
+
+## Data Constants
+
+Create UPLC constant terms from PlutusData — this is what `applyParamsToScript` uses internally:
+
+```typescript
+import { UPLC, Data } from "@evolution-sdk/evolution"
+
+// Create a data constant from PlutusData
+const term = UPLC.dataConstant(Data.int(42n))
+
+// With custom CBOR options (default is Aiken-compatible)
+const aikenTerm = UPLC.dataConstant(
+ Data.constr(0n, [Data.int(1n)]),
+)
+```
+
+## Applying Parameters
+
+The most common UPLC operation — apply runtime parameters to a compiled script:
+
+```typescript
+import { UPLC, Data, CBOR } from "@evolution-sdk/evolution"
+
+declare const compiledScript: string // Double-CBOR hex from Aiken
+
+// Apply parameters
+const applied = UPLC.applyParamsToScript(compiledScript, [
+ Data.bytearray("abc123"),
+ Data.int(1000000n)
+])
+
+// With typed schemas
+const typedApplied = UPLC.applyParamsToScriptWithSchema(
+ compiledScript,
+ [{ owner: new Uint8Array(28), deadline: 1000000n }],
+ (params) => Data.constr(0n, [
+ Data.bytearray(params.owner.toString()),
+ Data.int(params.deadline)
+ ])
+)
+```
+
+See [Parameterized Scripts](/docs/smart-contracts/apply-params) for a complete tutorial.
+
+## Builtin Functions
+
+UPLC includes a fixed set of builtin functions matching Plutus V3. The full list is available via `UPLC.BuiltinFunctions`:
+
+| Category | Examples |
+| --- | --- |
+| Arithmetic | `addInteger`, `subtractInteger`, `multiplyInteger`, `divideInteger` |
+| Comparison | `equalsInteger`, `lessThanInteger`, `lessThanEqualsInteger` |
+| ByteString | `appendByteString`, `sliceByteString`, `lengthOfByteString` |
+| Cryptography | `sha2_256`, `sha3_256`, `blake2b_256`, `verifyEd25519Signature` |
+| Data | `constrData`, `mapData`, `listData`, `iData`, `bData`, `unConstrData` |
+| String | `appendString`, `equalsString`, `encodeUtf8`, `decodeUtf8` |
+| BLS | `bls12_381_G1_add`, `bls12_381_G2_add`, `bls12_381_millerLoop` |
+
+## Version Management
+
+UPLC programs carry a semantic version:
+
+```typescript
+import { UPLC } from "@evolution-sdk/evolution"
+
+// Create a version
+const version = UPLC.makeSemVer(1, 1, 0)
+
+// Parse version components
+const parts = UPLC.parseSemVer("1.1.0")
+// { major: 1, minor: 1, patch: 0 }
+```
+
+## Error Handling
+
+UPLC operations throw `UPLC.UPLCError` on failure:
+
+```typescript
+import { UPLC } from "@evolution-sdk/evolution"
+
+try {
+ const program = UPLC.fromFlatHex("invalid")
+} catch (e) {
+ if (e instanceof UPLC.UPLCError) {
+ console.error("UPLC error:", e.message)
+ }
+}
+```
+
+## Next Steps
+
+- [Parameterized Scripts](/docs/smart-contracts/apply-params) — Apply parameters to validators
+- [Blueprint Codegen](/docs/smart-contracts/blueprint-codegen) — Generate types from CIP-57 blueprints
+- [CBOR Encoding](/docs/encoding/cbor) — Low-level CBOR operations
+- [Data Encoding](/docs/encoding/data) — PlutusData construction
diff --git a/docs/content/docs/governance/committee.mdx b/docs/content/docs/governance/committee.mdx
new file mode 100644
index 00000000..209a4c4a
--- /dev/null
+++ b/docs/content/docs/governance/committee.mdx
@@ -0,0 +1,122 @@
+---
+title: Committee Operations
+description: Authorize hot credentials and resign from the Constitutional Committee
+---
+
+# Committee Operations
+
+The Constitutional Committee is a group of members who verify that governance actions are consistent with the constitution. Committee operations use a cold/hot credential model to keep the primary signing key secure while allowing routine voting.
+
+## What Committee Members Do
+
+Constitutional Committee members review governance proposals and vote on whether each one is constitutional. Their votes are required alongside DRep and SPO votes for most governance actions to pass. The committee does not propose actions itself — it acts as a constitutional check on proposals submitted by others.
+
+## Cold vs Hot Credentials
+
+| | Cold Credential | Hot Credential |
+| --- | --- | --- |
+| **Purpose** | Identifies the committee member on-chain | Used for day-to-day voting |
+| **Storage** | Kept offline or in secure hardware | Available on a signing machine |
+| **Usage** | Authorize/revoke hot credentials, resign | Cast votes on governance actions |
+| **Exposure** | Rarely used, minimal attack surface | Regularly used, higher exposure |
+| **If compromised** | Committee seat is at risk | Revoke and authorize a new hot credential |
+
+
+The cold credential controls the committee seat. If it is compromised, the only recourse is resignation. Keep it in cold storage or a hardware security module.
+
+
+## Authorize a Hot Credential
+
+`authCommitteeHot` links a hot credential to a cold one, enabling the hot credential to vote on behalf of the committee member. This transaction must be signed with the cold credential.
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const coldCredential: Credential.Credential
+declare const hotCredential: Credential.Credential
+
+const tx = await client
+ .newTx()
+ .authCommitteeHot({ coldCredential, hotCredential })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+You can re-authorize a different hot credential at any time by submitting a new `authCommitteeHot` transaction. The previous hot credential is automatically revoked.
+
+## Resign from Committee
+
+`resignCommitteeCold` removes a committee member. An optional anchor can provide a URL and content hash explaining the resignation rationale.
+
+```typescript twoslash
+import { Anchor, Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const coldCredential: Credential.Credential
+declare const anchor: Anchor.Anchor
+
+const tx = await client
+ .newTx()
+ .resignCommitteeCold({ coldCredential, anchor })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+Resignation takes effect immediately. Once resigned, the cold credential can no longer authorize hot credentials or participate in governance.
+
+## Script-Controlled Committee
+
+Both `authCommitteeHot` and `resignCommitteeCold` support script-controlled credentials. When the cold credential is a script hash, provide a redeemer and attach the script:
+
+```typescript twoslash
+import { Credential, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const coldCredential: Credential.Credential // script hash credential
+declare const hotCredential: Credential.Credential
+declare const committeeScript: any // compiled Plutus script
+
+const tx = await client
+ .newTx()
+ .authCommitteeHot({
+ coldCredential,
+ hotCredential,
+ redeemer: Data.constr(0n, [])
+ })
+ .attachScript({ script: committeeScript })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+Script-controlled credentials allow multi-sig or on-chain logic to govern committee operations, adding an extra layer of security for the cold credential.
+
+## Next Steps
+
+- [Voting](/docs/governance/voting) — Cast votes on governance actions
+- [Proposals](/docs/governance/proposals) — Submit governance actions for the community to vote on
+- [DRep Registration](/docs/governance/drep-registration) — Register as a Delegated Representative
diff --git a/docs/content/docs/governance/meta.json b/docs/content/docs/governance/meta.json
index 4141f03f..85febbbb 100644
--- a/docs/content/docs/governance/meta.json
+++ b/docs/content/docs/governance/meta.json
@@ -2,8 +2,10 @@
"title": "Governance",
"pages": [
"index",
- "actions",
+ "drep-registration",
+ "vote-delegation",
"voting",
- "proposals"
+ "proposals",
+ "committee"
]
}
diff --git a/docs/content/docs/governance/proposals.mdx b/docs/content/docs/governance/proposals.mdx
index da77bede..e99a312f 100644
--- a/docs/content/docs/governance/proposals.mdx
+++ b/docs/content/docs/governance/proposals.mdx
@@ -19,16 +19,19 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+// Governance action — e.g. new GovernanceAction.InfoAction() for informational proposals.
+// Other types: ParameterChangeAction, HardForkInitiationAction,
+// TreasuryWithdrawalsAction, NoConfidenceAction, UpdateCommitteeAction, NewConstitutionAction
declare const governanceAction: GovernanceAction.GovernanceAction
-declare const rewardAccount: RewardAccount.RewardAccount
-declare const anchor: Anchor.Anchor
+declare const rewardAccount: RewardAccount.RewardAccount // your stake reward account — deposit refunded here
+declare const anchor: Anchor.Anchor // metadata URL + content hash describing the proposal
const tx = await client
.newTx()
.propose({
governanceAction,
- rewardAccount, // Deposit refunded here when finalized
- anchor // Metadata URL + hash (or null)
+ rewardAccount,
+ anchor
})
.build()
diff --git a/docs/content/docs/governance/vote-delegation.mdx b/docs/content/docs/governance/vote-delegation.mdx
index 7ce4ee52..55187289 100644
--- a/docs/content/docs/governance/vote-delegation.mdx
+++ b/docs/content/docs/governance/vote-delegation.mdx
@@ -19,8 +19,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const drepKeyHash: any
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
+declare const drepKeyHash: any // DRep's key hash (28 bytes, from a governance explorer or DRep registry)
const tx = await client
.newTx()
@@ -53,7 +53,7 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
// Abstain from all governance votes
const tx = await client
@@ -82,9 +82,9 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const poolKeyHash: any
-declare const drepKeyHash: any
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
+declare const poolKeyHash: any // pool key hash (from a stake pool explorer)
+declare const drepKeyHash: any // DRep's key hash (from a governance explorer)
const tx = await client
.newTx()
@@ -113,8 +113,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const drepKeyHash: any
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
+declare const drepKeyHash: any // DRep's key hash (28 bytes, from a governance explorer or DRep registry)
const tx = await client
.newTx()
diff --git a/docs/content/docs/governance/voting.mdx b/docs/content/docs/governance/voting.mdx
index 36e9eb0b..cec076c4 100644
--- a/docs/content/docs/governance/voting.mdx
+++ b/docs/content/docs/governance/voting.mdx
@@ -9,8 +9,10 @@ DReps, Constitutional Committee members, and Stake Pool Operators can vote on go
## Casting a Vote
+Build a `VotingProcedures` object with a voter, the governance action to vote on, and your vote (yes/no/abstain):
+
```typescript twoslash
-import { VotingProcedures, preprod, Client } from "@evolution-sdk/evolution"
+import { DRep, GovernanceAction, TransactionHash, VotingProcedures, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
.withBlockfrost({
@@ -19,14 +21,39 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const votingProcedures: VotingProcedures.VotingProcedures
+// 1. Create a voter (DRep identified by key hash, script hash, or built-in)
+declare const drep: DRep.DRep // from DRep.fromKeyHash() or DRep.fromScriptHash()
+const voter = new VotingProcedures.DRepVoter({ drep })
-const tx = await client.newTx().vote({ votingProcedures }).build()
+// 2. Identify the governance action to vote on (from chain query or governance explorer)
+declare const govActionTxHash: TransactionHash.TransactionHash
+const govActionId = new GovernanceAction.GovActionId({
+ transactionId: govActionTxHash,
+ govActionIndex: 0n,
+})
+
+// 3. Create a voting procedure — yes(), no(), or abstain()
+const procedure = new VotingProcedures.VotingProcedure({
+ vote: VotingProcedures.yes(),
+ anchor: null, // optional metadata URL + hash
+})
+
+// 4. Combine into VotingProcedures and submit
+const votingProcedures = VotingProcedures.singleVote(voter, govActionId, procedure)
+const tx = await client.newTx().vote({ votingProcedures }).build()
const signed = await tx.sign()
await signed.submit()
```
+### Vote Options
+
+| Function | Meaning |
+| --- | --- |
+| `VotingProcedures.yes()` | Vote in favor |
+| `VotingProcedures.no()` | Vote against |
+| `VotingProcedures.abstain()` | Explicitly abstain |
+
## Voter Types
| Voter | Credential Type | Script-Controlled? |
@@ -49,8 +76,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const votingProcedures: VotingProcedures.VotingProcedures
-declare const votingScript: any
+declare const votingProcedures: VotingProcedures.VotingProcedures // built with singleVote() as shown above
+declare const votingScript: any // compiled Plutus voting script (from Aiken build or Blueprint codegen)
const tx = await client
.newTx()
diff --git a/docs/content/docs/index.mdx b/docs/content/docs/index.mdx
index f0a2fe27..42e30c6d 100644
--- a/docs/content/docs/index.mdx
+++ b/docs/content/docs/index.mdx
@@ -41,8 +41,6 @@ import { Card, Cards } from 'fumadocs-ui/components/card'
## Core Concepts
-## Core Concepts
-
Learn the fundamentals of working with Evolution SDK.
diff --git a/docs/content/docs/introduction/changelog.mdx b/docs/content/docs/introduction/changelog.mdx
new file mode 100644
index 00000000..1520d14f
--- /dev/null
+++ b/docs/content/docs/introduction/changelog.mdx
@@ -0,0 +1,105 @@
+---
+title: Changelog
+description: Version history and breaking changes
+---
+
+# Changelog
+
+Track what changed in each release. For the full changelog with commit links, see the [CHANGELOG.md on GitHub](https://github.com/IntersectMBO/evolution-sdk/blob/main/packages/evolution/CHANGELOG.md).
+
+## v0.5.1
+
+**Patch** — Restore Certificate.ts as self-contained module, fix docgen compatibility.
+
+- Removed convenience re-exports from Time.ts
+- Updated stale `@example` import paths for flat module structure
+
+## v0.5.0
+
+**Minor** — Flatten module structure to eliminate webpack casing conflicts.
+
+This release changes how modules are organized internally. The public API is preserved, but import paths changed.
+
+**Breaking changes:**
+- Concept-folder subpath imports like `@evolution-sdk/evolution/address` are removed
+- Use the root barrel (`import { Address } from "@evolution-sdk/evolution"`) or wildcard (`import * as Address from "@evolution-sdk/evolution/Address"`)
+- `MessageSigning` renamed to `COSE` in the root barrel export
+- `./plutus`, `./cose`, and `./blueprint` available as subpath imports
+- Blueprint barrel uses `export * as` instead of `export *`
+
+**Migration — step by step:**
+
+1. **Update subpath imports** (most common change):
+
+```typescript
+// Before (v0.4.x) — lowercase folder subpath
+import { Address } from "@evolution-sdk/evolution/address"
+import { Assets } from "@evolution-sdk/evolution/assets"
+
+// After (v0.5.0) — root barrel or PascalCase wildcard
+import { Address, Assets } from "@evolution-sdk/evolution"
+// or
+import * as Address from "@evolution-sdk/evolution/Address"
+```
+
+2. **Rename MessageSigning → COSE**:
+
+```typescript
+// Before
+import { MessageSigning } from "@evolution-sdk/evolution"
+
+// After
+import { COSE } from "@evolution-sdk/evolution"
+```
+
+3. **Update Blueprint imports**:
+
+```typescript
+// Before — direct exports
+import { codegen } from "@evolution-sdk/evolution/blueprint"
+
+// After — namespaced exports
+import { Blueprint } from "@evolution-sdk/evolution"
+Blueprint.codegen.generateTypeScript(...)
+```
+
+4. **Domain subpaths still work** (unchanged):
+
+```typescript
+// These still work in v0.5.0
+import { Address } from "@evolution-sdk/evolution/plutus"
+import { COSESign1 } from "@evolution-sdk/evolution/cose"
+import { Codegen } from "@evolution-sdk/evolution/blueprint"
+```
+
+## v0.4.0
+
+**Minor** — Reorganize flat module structure into semantic concept folders.
+
+- Moved ~130 flat root-level files into 24 concept folders following Effect v4 conventions
+- Merged `datum/` into `data/`
+- Extracted `certificate/` from `governance/`
+- Moved byte primitives to `bytes/`, numeric types to `numeric/`
+- Moved CBOR and Codec to `encoding/`
+- Deleted dead code: `Combinator.ts`, `FormatError.ts`, `NativeScriptsOLD.ts`, `Function.ts`
+- Conway-era certificate encoding fix: tag 258 for nonempty ordered sets
+
+**All public API exports preserved** via barrel files and package.json exports map.
+
+## Versioning Policy
+
+Evolution SDK follows [Semantic Versioning](https://semver.org/):
+
+| Version bump | Meaning |
+| --- | --- |
+| **Major** (1.0.0) | Breaking API changes |
+| **Minor** (0.X.0) | New features, possible breaking changes (pre-1.0) |
+| **Patch** (0.0.X) | Bug fixes, no API changes |
+
+During the pre-1.0 phase, minor versions may include breaking changes. Always check this page before upgrading.
+
+## Staying Updated
+
+- Watch the [GitHub releases](https://github.com/IntersectMBO/evolution-sdk/releases) for notifications
+- Check this page before upgrading to a new minor version
+- See the [Migration Guide](/docs/introduction/migration-from-lucid) if coming from Lucid
diff --git a/docs/content/docs/introduction/community.mdx b/docs/content/docs/introduction/community.mdx
new file mode 100644
index 00000000..1a327ee3
--- /dev/null
+++ b/docs/content/docs/introduction/community.mdx
@@ -0,0 +1,51 @@
+---
+title: Community & Support
+description: Get help, report bugs, and contribute to Evolution SDK
+---
+
+# Community & Support
+
+## Get Help
+
+| Channel | Use For |
+| --- | --- |
+| [GitHub Issues](https://github.com/IntersectMBO/evolution-sdk/issues) | Bug reports and feature requests |
+| [GitHub Discussions](https://github.com/IntersectMBO/evolution-sdk/discussions) | Questions, ideas, and general discussion |
+| [API Reference](/docs/modules) | Full module documentation |
+
+## Report a Bug
+
+1. Check [existing issues](https://github.com/IntersectMBO/evolution-sdk/issues) to avoid duplicates
+2. Include: SDK version, Node.js version, minimal reproduction code
+3. Describe expected vs actual behavior
+
+## Request a Feature
+
+Open a [GitHub Discussion](https://github.com/IntersectMBO/evolution-sdk/discussions) to discuss the idea before submitting a PR. This helps align on API design before implementation work begins.
+
+## Contributing
+
+We welcome contributions. See the full [Contributing Guide](https://github.com/IntersectMBO/evolution-sdk/blob/main/CONTRIBUTING.md) for details.
+
+**Quick start:**
+
+```bash
+git clone https://github.com/IntersectMBO/evolution-sdk.git
+cd evolution-sdk
+pnpm install
+pnpm build
+pnpm test
+```
+
+**Before submitting a PR:**
+- Fork the repo and branch from `main`
+- Add tests for new code
+- Update docs if you changed APIs
+- Ensure `pnpm build`, `pnpm test`, and `pnpm lint` pass
+
+## Useful Links
+
+- [GitHub Repository](https://github.com/IntersectMBO/evolution-sdk)
+- [npm Package](https://www.npmjs.com/package/@evolution-sdk/evolution)
+- [Changelog](/docs/introduction/changelog)
+- [Migration Guide](/docs/introduction/migration-from-lucid)
diff --git a/docs/content/docs/introduction/getting-started.mdx b/docs/content/docs/introduction/getting-started.mdx
index 68728462..741b6a46 100644
--- a/docs/content/docs/introduction/getting-started.mdx
+++ b/docs/content/docs/introduction/getting-started.mdx
@@ -11,12 +11,39 @@ This guide walks you through building your first transaction—from creating a c
Perfect for developers new to the SDK or those migrating from other Cardano libraries looking for a working foundation to build upon.
+## Choose Your Setup
+
+This guide uses **Blockfrost** + **seed phrase** for the quickest path. If your setup is different, jump to the right guide:
+
+| Your Setup | Guide |
+| --- | --- |
+| **Backend / server-side** | This page (Blockfrost + seed phrase) |
+| **Frontend / dApp** | [API Wallets](/docs/wallets/api-wallet) (CIP-30 browser wallet) |
+| **Self-hosted infra** | [Kupmios Provider](/docs/providers/provider-types) (Ogmios + Kupo) |
+| **Local development** | [Devnet](/docs/devnet/getting-started) (Docker-based local chain) |
+| **Multiple providers** | [Provider Types](/docs/providers/provider-types) (compare all options) |
+
+## Key Concept: Provider + Wallet = Client
+
+Before diving in, understand the two halves of a client:
+
+- **Provider** — reads from the blockchain (query UTxOs, protocol parameters, submit transactions). Think of it as your connection to the network.
+- **Wallet** — signs transactions (holds keys or connects to a browser extension). Without a wallet, you can build but not sign.
+
+A **signing client** has both: it can build, sign, and submit. A **read-only client** has only a provider + address: it can build unsigned transactions but cannot sign. See [Client Architecture](/docs/clients/architecture) for full details.
+
+This guide creates a signing client. If you're building a frontend dApp where the user's browser wallet signs, see [API Wallets](/docs/wallets/api-wallet) instead.
+
## Prerequisites
- Node.js 18+ or browser environment with ES modules support
- Basic TypeScript knowledge
- A Cardano testnet wallet with some tADA (get from [faucet](https://docs.cardano.org/cardano-testnet/tools/faucet/))
+
+This guide uses **preprod testnet**. Never use real mainnet funds while learning. Get free test ADA from the [Cardano faucet](https://docs.cardano.org/cardano-testnet/tools/faucet/).
+
+
## Your First Transaction
### 1. Installation
@@ -127,6 +154,8 @@ console.log("Transaction submitted:", hash)
## What's Next?
+- **[Common Patterns](/docs/common-patterns)** - Quick recipes for frequent tasks
+- **[API Overview](/docs/api-overview)** - Find the right module by task
- **[Clients](/docs/clients)** - Connect to different blockchains and providers
- **[Wallets](/docs/wallets)** - Explore all wallet types and key management
- **[Transactions](/docs/transactions)** - Build complex multi-output transactions
diff --git a/docs/content/docs/introduction/important-defaults.mdx b/docs/content/docs/introduction/important-defaults.mdx
new file mode 100644
index 00000000..bf64414f
--- /dev/null
+++ b/docs/content/docs/introduction/important-defaults.mdx
@@ -0,0 +1,140 @@
+---
+title: Important Defaults
+description: Non-obvious behaviors you should know about before building
+---
+
+# Important Defaults
+
+Evolution SDK ships with sensible defaults that handle complexity for you. But if you don't know they exist, they can be surprising. This page documents the most important implicit behaviors.
+
+## Transaction Builder Defaults
+
+### Automatic Coin Selection
+
+When you call `.build()`, the SDK automatically selects UTxOs from your wallet to cover the transaction's requirements. You don't need to manually pick inputs.
+
+```typescript
+// You write this:
+const tx = await client.newTx()
+ .payToAddress({ address, assets: Assets.fromLovelace(5_000_000n) })
+ .build()
+
+// The builder automatically:
+// 1. Queries your wallet's UTxOs
+// 2. Selects the largest UTxOs first (largest-first algorithm)
+// 3. Covers the payment amount + fees + min UTxO for change
+```
+
+**Override:** Pass `availableUtxos` to `.build()` to provide your own UTxO set.
+
+### Automatic Fee Calculation
+
+Fees are calculated automatically based on transaction size, script execution costs, and current protocol parameters. You never set fees manually.
+
+**Override:** Use `FeeValidation.assertValidFee()` after building to verify fees are within expected bounds.
+
+### Automatic Collateral Selection
+
+For transactions involving Plutus scripts, the builder automatically selects collateral UTxOs. Collateral is required by the protocol in case script execution fails.
+
+**Override:** Not typically needed — the builder handles this correctly.
+
+### Automatic Script Evaluation
+
+When you attach a script and provide a redeemer, the builder evaluates the script to compute execution units (memory + CPU). This happens during `.build()`.
+
+```typescript
+// You provide:
+.collectFrom({ inputs: scriptUtxos, redeemer: Data.constr(0n, []) })
+.attachScript({ script: validatorScript })
+
+// The builder automatically:
+// 1. Evaluates the script with the redeemer
+// 2. Computes exact execution units (memory + CPU)
+// 3. Sets execution budgets on the redeemer
+// 4. Includes execution costs in fee calculation
+```
+
+### Automatic Redeemer Indexing
+
+After coin selection changes the input order, redeemer indices must be recalculated. The builder does this automatically — you never manually set redeemer indices.
+
+### Fresh State Per Build
+
+
+Builders are **safe to reuse**. Each `.build()` call creates independent state — no contamination from previous builds.
+
+
+```typescript
+const template = client.newTx()
+ .payToAddress({ address, assets })
+
+// Safe: each build() creates independent state
+const tx1 = await template.build()
+const tx2 = await template.build() // No contamination from tx1
+```
+
+## Encoding Defaults
+
+### CBOR: Aiken-Compatible by Default
+
+`UPLC.applyParamsToScript()` uses Aiken-compatible CBOR encoding (indefinite-length arrays/maps) by default. If you're using a different Plutus toolchain, you may need different options.
+
+```typescript
+// Default: Aiken encoding
+UPLC.applyParamsToScript(script, params)
+
+// For CML-compatible encoding:
+UPLC.applyParamsToScript(script, params, CBOR.CML_DATA_DEFAULT_OPTIONS)
+```
+
+### Scripts Are Double-CBOR Encoded
+
+Compiled Plutus scripts from Aiken/Plutarch are typically double-CBOR encoded (flat bytes → CBOR bytes → CBOR bytes). The SDK handles this automatically, but if you're working with raw script bytes, be aware of the encoding level.
+
+```typescript
+// Check encoding level
+const level = UPLC.getCborEncodingLevel(scriptHex)
+// "double" | "single" | "none"
+```
+
+## Provider Defaults
+
+### Protocol Parameters Fetched Automatically
+
+The builder fetches protocol parameters from your provider during `.build()`. Parameters are used for fee calculation, min UTxO requirements, and cost model selection.
+
+### Network Is Set at Client Creation
+
+The network (mainnet, preprod, preview) is fixed when you create the client. All operations use this network — there's no per-transaction network override.
+
+```typescript
+import { preprod, mainnet, Client } from "@evolution-sdk/evolution"
+
+// Network is locked to preprod for all operations
+const client = Client.make(preprod).withBlockfrost({...})
+```
+
+## Error Defaults
+
+### Errors Are Tagged
+
+All SDK errors have a `_tag` field for pattern matching. This is an Effect convention — use it instead of `instanceof`:
+
+```typescript
+catch (e: any) {
+ if (e._tag === "EvaluationError") { /* script failed */ }
+ if (e._tag === "CoinSelectionError") { /* insufficient funds */ }
+}
+```
+
+### Build Returns Promise by Default
+
+`.build()` returns a `Promise`. For Effect-based error handling, use `.buildEffect()`. For Either-based, use `.buildEither()`.
+
+## Next Steps
+
+- [Getting Started](/docs/introduction/getting-started) — Build your first transaction
+- [Common Patterns](/docs/common-patterns) — Copy-paste recipes
+- [Error Handling](/docs/advanced/error-handling) — All error types and debugging
+- [Architecture](/docs/architecture/transaction-flow) — How the build pipeline works
diff --git a/docs/content/docs/introduction/installation.mdx b/docs/content/docs/introduction/installation.mdx
index 8cae20df..f879fab3 100644
--- a/docs/content/docs/introduction/installation.mdx
+++ b/docs/content/docs/introduction/installation.mdx
@@ -11,23 +11,23 @@ Install it for any Cardano project: dApps, wallets, transaction tools, or when m
## Quick Install
-### npm
-
+
+
```bash
npm install @evolution-sdk/evolution
```
-
-### yarn
-
+
+
```bash
yarn add @evolution-sdk/evolution
```
-
-### pnpm
-
+
+
```bash
pnpm add @evolution-sdk/evolution
```
+
+
## Requirements
diff --git a/docs/content/docs/introduction/meta.json b/docs/content/docs/introduction/meta.json
index 560fbc6f..b3b6b4ce 100644
--- a/docs/content/docs/introduction/meta.json
+++ b/docs/content/docs/introduction/meta.json
@@ -3,10 +3,13 @@
"pages": [
"index",
"why-evolution",
- "getting-started",
"installation",
+ "getting-started",
+ "important-defaults",
"imports",
"platform-compatibility",
- "migration-from-lucid"
+ "changelog",
+ "migration-from-lucid",
+ "community"
]
}
diff --git a/docs/content/docs/introduction/migration-from-lucid.mdx b/docs/content/docs/introduction/migration-from-lucid.mdx
index a1bbea46..b9d4a187 100644
--- a/docs/content/docs/introduction/migration-from-lucid.mdx
+++ b/docs/content/docs/introduction/migration-from-lucid.mdx
@@ -86,6 +86,78 @@ const tx = await client
.build()
```
+### Smart Contract Interactions
+
+**Lucid:**
+
+```typescript
+const tx = await lucid
+ .newTx()
+ .collectFrom(scriptUtxos, redeemer)
+ .attachSpendingValidator(validator)
+ .complete()
+```
+
+**Evolution SDK:**
+
+```typescript
+const tx = await client
+ .newTx()
+ .collectFrom({
+ inputs: scriptUtxos,
+ redeemer: Data.constr(0n, []),
+ label: "my-spend" // Optional debug label (new!)
+ })
+ .attachScript({ script: validatorScript })
+ .build()
+```
+
+Key differences:
+- `collectFrom` takes a named object instead of positional args
+- `attachSpendingValidator` → `attachScript` (one method for all script types)
+- Optional `label` field for debugging script failures
+- Redeemer uses `Data.constr()` instead of Lucid's Data class
+
+### Querying
+
+**Lucid:**
+
+```typescript
+const utxos = await lucid.utxosAt(address)
+const utxoWithUnit = await lucid.utxoByUnit(unit)
+```
+
+**Evolution SDK:**
+
+```typescript
+const utxos = await client.getUtxos(address)
+const utxoWithUnit = await client.getUtxoByUnit(unit)
+const walletUtxos = await client.getWalletUtxos() // New: wallet-specific
+```
+
+### Datum Construction
+
+**Lucid:**
+
+```typescript
+import { Data } from "lucid-evolution"
+const datum = Data.to(new Constr(0, [42n]))
+```
+
+**Evolution SDK:**
+
+```typescript
+import { Data, TSchema } from "@evolution-sdk/evolution"
+
+// Option 1: Raw PlutusData
+const datum = Data.constr(0n, [Data.int(42n)])
+
+// Option 2: Type-safe with TSchema (recommended)
+const MyDatum = TSchema.Struct({ value: TSchema.Integer })
+const Codec = Data.withSchema(MyDatum)
+const datum = Codec.toData({ value: 42n })
+```
+
## Common Patterns
Here are the typical changes you'll make throughout your codebase:
@@ -103,9 +175,9 @@ Evolution SDK uses staged capability assembly and explicit method parameters for
## Migration Strategy
-Start by identifying isolated modules or new features where you can adopt Evolution SDK without touching existing Lucid code. This lets you learn the patterns incrementally while keeping your app functional.
-
-Key steps:
+
+**You don't have to migrate everything at once.** Evolution SDK can coexist with Lucid in the same project. Start with one module, test on preprod, then gradually expand.
+
1. Install `@evolution-sdk/evolution` alongside Lucid (they can coexist)
2. Update one module at a time, starting with simpler transaction flows
diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json
index 5bfd59ea..ba0edc90 100644
--- a/docs/content/docs/meta.json
+++ b/docs/content/docs/meta.json
@@ -2,22 +2,24 @@
"title": "Documentation",
"pages": [
"introduction",
- "architecture",
+ "common-patterns",
+ "api-overview",
"clients",
"wallets",
"providers",
- "addresses",
"transactions",
+ "addresses",
"assets",
- "querying",
- "encoding",
- "time",
"smart-contracts",
"staking",
"governance",
+ "querying",
+ "encoding",
+ "time",
"devnet",
"testing",
"advanced",
+ "architecture",
"modules"
]
}
diff --git a/docs/content/docs/modules/Address.mdx b/docs/content/docs/modules/Address.mdx
index 73f31063..ee546f39 100644
--- a/docs/content/docs/modules/Address.mdx
+++ b/docs/content/docs/modules/Address.mdx
@@ -16,6 +16,7 @@ Added in v2.0.0
- [arbitrary](#arbitrary-1)
- [Functions](#functions)
- [fromBech32](#frombech32)
+ - [fromSeed](#fromseed)
- [Model](#model)
- [AddressDetails (interface)](#addressdetails-interface)
- [Schema](#schema)
@@ -75,6 +76,39 @@ export declare const fromBech32: (i: string, overrideOptions?: ParseOptions) =>
Added in v2.0.0
+## fromSeed
+
+Derive an address from a BIP-39 seed phrase.
+
+Pure, synchronous key derivation — no network access or running cluster required.
+Useful for generating addresses before a devnet cluster starts (e.g. for genesis funding).
+
+**Signature**
+
+```ts
+export declare const fromSeed: (
+ seed: string,
+ options?: { password?: string; addressType?: "Base" | "Enterprise"; accountIndex?: number; networkId?: number }
+) => Address
+```
+
+**Example**
+
+```typescript
+import * as Address from "@evolution-sdk/evolution/Address"
+
+const address = Address.fromSeed(
+ "test test test test test test test test test test test test test test test test test test test test test test test sauce",
+ {
+ accountIndex: 0,
+ networkId: 0 // 0 = testnet, 1 = mainnet
+ }
+)
+const hex = Address.toHex(address)
+```
+
+Added in v2.1.0
+
# Model
## AddressDetails (interface)
diff --git a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx
index a68d44a6..bed2a713 100644
--- a/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx
+++ b/docs/content/docs/modules/sdk/builders/TransactionBuilder.mdx
@@ -399,6 +399,21 @@ export interface TransactionBuilderBase {
*/
readonly registerStake: (params: RegisterStakeParams) => this
+ /**
+ * Register a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+ * This is the pre-Conway registration format still accepted on mainnet and
+ * is what most wallets use today.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly registerStakeLegacy: (params: RegisterStakeLegacyParams) => this
+
/**
* Deregister a stake credential from the chain.
*
@@ -417,6 +432,20 @@ export interface TransactionBuilderBase {
*/
readonly deregisterStake: (params: DeregisterStakeParams) => this
+ /**
+ * Deregister a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+ * This is the pre-Conway deregistration format still accepted on mainnet.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly deregisterStakeLegacy: (params: DeregisterStakeLegacyParams) => this
+
/**
* Delegate stake and/or voting power to a pool or DRep.
*
diff --git a/docs/content/docs/modules/sdk/builders/operations/Operations.mdx b/docs/content/docs/modules/sdk/builders/operations/Operations.mdx
index c62c9c59..5327315d 100644
--- a/docs/content/docs/modules/sdk/builders/operations/Operations.mdx
+++ b/docs/content/docs/modules/sdk/builders/operations/Operations.mdx
@@ -32,8 +32,10 @@ parent: Modules
- [~~DelegateToParams~~ (interface)](#delegatetoparams-interface)
- [DelegateToPoolAndDRepParams (interface)](#delegatetopoolanddrepparams-interface)
- [DelegateToPoolParams (interface)](#delegatetopoolparams-interface)
+ - [DeregisterStakeLegacyParams (interface)](#deregisterstakelegacyparams-interface)
- [DeregisterStakeParams (interface)](#deregisterstakeparams-interface)
- [RegisterAndDelegateToParams (interface)](#registeranddelegatetoparams-interface)
+ - [RegisterStakeLegacyParams (interface)](#registerstakelegacyparams-interface)
- [RegisterStakeParams (interface)](#registerstakeparams-interface)
- [WithdrawParams (interface)](#withdrawparams-interface)
- [utils](#utils)
@@ -425,6 +427,28 @@ export interface DelegateToPoolParams {
Added in v2.0.0
+## DeregisterStakeLegacyParams (interface)
+
+Parameters for legacy (pre-Conway) stake credential deregistration.
+
+Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+This is the pre-Conway deregistration format still accepted on mainnet.
+
+**Signature**
+
+```ts
+export interface DeregisterStakeLegacyParams {
+ /** The stake credential to deregister */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+```
+
+Added in v2.0.0
+
## DeregisterStakeParams (interface)
Parameters for deregistering a stake credential.
@@ -473,6 +497,28 @@ export interface RegisterAndDelegateToParams {
Added in v2.0.0
+## RegisterStakeLegacyParams (interface)
+
+Parameters for legacy (pre-Conway) stake credential registration.
+
+Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+This is the pre-Conway registration format still accepted on mainnet.
+
+**Signature**
+
+```ts
+export interface RegisterStakeLegacyParams {
+ /** The stake credential to register (key hash or script hash) */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+```
+
+Added in v2.0.0
+
## RegisterStakeParams (interface)
Parameters for registering a stake credential.
diff --git a/docs/content/docs/modules/sdk/builders/operations/Stake.mdx b/docs/content/docs/modules/sdk/builders/operations/Stake.mdx
index 8fec987c..37409267 100644
--- a/docs/content/docs/modules/sdk/builders/operations/Stake.mdx
+++ b/docs/content/docs/modules/sdk/builders/operations/Stake.mdx
@@ -19,8 +19,10 @@ Added in v2.0.0
- [createDelegateToPoolAndDRepProgram](#createdelegatetopoolanddrepprogram)
- [createDelegateToPoolProgram](#createdelegatetopoolprogram)
- [~~createDelegateToProgram~~](#createdelegatetoprogram)
+ - [createDeregisterStakeLegacyProgram](#createderegisterstakelegacyprogram)
- [createDeregisterStakeProgram](#createderegisterstakeprogram)
- [createRegisterAndDelegateToProgram](#createregisteranddelegatetoprogram)
+ - [createRegisterStakeLegacyProgram](#createregisterstakelegacyprogram)
- [createRegisterStakeProgram](#createregisterstakeprogram)
- [createWithdrawProgram](#createwithdrawprogram)
@@ -94,6 +96,21 @@ export declare const createDelegateToProgram: (
Added in v2.0.0
+## createDeregisterStakeLegacyProgram
+
+Creates a ProgramStep for legacy (pre-Conway) stake deregistration.
+Adds a StakeDeregistration (CDDL tag 1) certificate with no deposit refund.
+
+**Signature**
+
+```ts
+export declare const createDeregisterStakeLegacyProgram: (
+ params: DeregisterStakeLegacyParams
+) => Effect.Effect
+```
+
+Added in v2.0.0
+
## createDeregisterStakeProgram
Creates a ProgramStep for deregisterStake operation.
@@ -135,6 +152,21 @@ export declare const createRegisterAndDelegateToProgram: (
Added in v2.0.0
+## createRegisterStakeLegacyProgram
+
+Creates a ProgramStep for legacy (pre-Conway) stake registration.
+Adds a StakeRegistration (CDDL tag 0) certificate with no deposit.
+
+**Signature**
+
+```ts
+export declare const createRegisterStakeLegacyProgram: (
+ params: RegisterStakeLegacyParams
+) => Effect.Effect
+```
+
+Added in v2.0.0
+
## createRegisterStakeProgram
Creates a ProgramStep for registerStake operation.
diff --git a/docs/content/docs/modules/sdk/wallet/Derivation.mdx b/docs/content/docs/modules/sdk/wallet/Derivation.mdx
index f18eb001..3f0cdee8 100644
--- a/docs/content/docs/modules/sdk/wallet/Derivation.mdx
+++ b/docs/content/docs/modules/sdk/wallet/Derivation.mdx
@@ -68,7 +68,7 @@ export declare function addressFromSeed(
password?: string
addressType?: "Base" | "Enterprise"
accountIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): { address: CoreAddress.Address; rewardAddress: CoreRewardAddress.RewardAddress | undefined }
```
@@ -101,7 +101,7 @@ export declare function walletFromBip32(
options: {
addressType?: "Base" | "Enterprise"
accountIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): SeedDerivationResult
```
@@ -119,7 +119,7 @@ export declare function walletFromPrivateKey(
options: {
stakeKeyBech32?: string
addressType?: "Base" | "Enterprise"
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): Effect.Effect
```
@@ -137,7 +137,7 @@ export declare const walletFromSeed: (
accountIndex?: number
paymentIndex?: number
stakeIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
}
) => Effect.Effect
```
diff --git a/docs/content/docs/providers/index.mdx b/docs/content/docs/providers/index.mdx
index 4de3ccb1..79ae75be 100644
--- a/docs/content/docs/providers/index.mdx
+++ b/docs/content/docs/providers/index.mdx
@@ -18,7 +18,7 @@ The SDK abstracts provider differences through a unified interface. Choose your
Create a provider-only client to query blockchain data:
```typescript
-import { mainnet, Client } from "@evolution-sdk/evolution"
+import { Address, Transaction, mainnet, Client } from "@evolution-sdk/evolution"
const client = Client.make(mainnet)
.withBlockfrost({
@@ -28,15 +28,15 @@ const client = Client.make(mainnet)
// Query protocol parameters
const params = await client.getProtocolParameters()
-console.log("Min fee:", params.minFeeConstant)
+console.log("Min fee A:", params.minFeeA)
// Query any address
-const utxos = await client.getUtxos("addr1...")
+const utxos = await client.getUtxos(Address.fromBech32("addr1..."))
console.log("UTxOs found:", utxos.length)
// Submit pre-signed transaction
-const signedTxCbor = "84a300..." // Signed transaction CBOR
-const txHash = await client.submitTx(signedTxCbor)
+const signedTx = Transaction.fromCBORHex("84a300...") // Signed transaction
+const txHash = await client.submitTx(signedTx)
```
## Available Providers
diff --git a/docs/content/docs/providers/provider-only-client.mdx b/docs/content/docs/providers/provider-only-client.mdx
index 06ba4449..88004c06 100644
--- a/docs/content/docs/providers/provider-only-client.mdx
+++ b/docs/content/docs/providers/provider-only-client.mdx
@@ -386,7 +386,7 @@ const client =
| Feature | Provider-Only Client | Read-Only Client |
| ----------------------- | ------------------------------ | -------------------------------------------------------------------- |
| **Configuration** | Provider only | Provider + address |
-| **Creation** | `client(chain).withBlockfrost(...)` | `client(chain).withBlockfrost(...).withAddress(address)` |
+| **Creation** | `Client.make(chain).withBlockfrost(...)` | `Client.make(chain).withBlockfrost(...).withAddress(address)` |
| **Query any address** | `getUtxos(anyAddress)` | `getUtxos(anyAddress)` |
| **Query own address** | Not available | `getWalletUtxos()` |
| **Build transactions** | `newTx()` with manual context | `newTx()` returns unsigned tx |
diff --git a/docs/content/docs/providers/provider-types.mdx b/docs/content/docs/providers/provider-types.mdx
index c1b55f84..3abac0db 100644
--- a/docs/content/docs/providers/provider-types.mdx
+++ b/docs/content/docs/providers/provider-types.mdx
@@ -84,7 +84,27 @@ const client = Client.make(mainnet)
})
```
-### Setup Requirements
+### With Custom Headers (Demeter / Hosted)
+
+For hosted Kupmios services like [Demeter](https://demeter.run), pass API keys via custom headers:
+
+```typescript twoslash
+import { mainnet, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(mainnet)
+ .withKupmios({
+ ogmiosUrl: "https://ogmios.demeter.run",
+ kupoUrl: "https://kupo.demeter.run",
+ headers: {
+ ogmiosHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! },
+ kupoHeader: { "dmtr-api-key": process.env.DEMETER_API_KEY! },
+ }
+ })
+```
+
+Each service can have its own header — useful when Kupo and Ogmios use different authentication.
+
+### Setup Requirements (Self-Hosted)
Requires running Cardano node, Ogmios, and Kupo services:
diff --git a/docs/content/docs/providers/submission.mdx b/docs/content/docs/providers/submission.mdx
index 2ffbd3f3..1d88e08d 100644
--- a/docs/content/docs/providers/submission.mdx
+++ b/docs/content/docs/providers/submission.mdx
@@ -45,44 +45,20 @@ const signedTxCbor = "84a300..."
const signedTx = Transaction.fromCBORHex(signedTxCbor)
const txHash = await client.submitTx(signedTx)
+// ---cut---
// Wait for confirmation (checks every 5 seconds by default)
const confirmed = await client.awaitTx(txHash)
-if (confirmed) {
- console.log("Transaction confirmed!")
-} else {
- console.log("Transaction not found")
-}
-```
-
-## Custom Check Interval
-
-Specify how often to check for confirmation:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-const txHashHex = "abc123..."
-const txHash = TransactionHash.fromHex(txHashHex)
-
-// Check every 10 seconds
-const confirmed = await client.awaitTx(txHash, 10000)
-
-console.log("Confirmed:", confirmed)
+// Custom interval: check every 10 seconds
+// const confirmed = await client.awaitTx(txHash, 10000)
```
## Transaction Evaluation
-Evaluate transaction before submission to estimate script execution costs:
+Evaluate a transaction before submission to estimate script execution costs:
```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
+import { Transaction, mainnet, Client } from "@evolution-sdk/evolution"
const client = Client.make(mainnet)
.withBlockfrost({
@@ -90,380 +66,50 @@ const client = Client.make(mainnet)
projectId: process.env.BLOCKFROST_PROJECT_ID!
})
-const unsignedTxCbor = "84a300..." // Unsigned transaction
+const unsignedTxCbor = "84a300..." // Unsigned transaction with scripts
const unsignedTx = Transaction.fromCBORHex(unsignedTxCbor)
-// Evaluate script execution
+// ---cut---
+// Evaluate script execution costs
const redeemers = await client.evaluateTx(unsignedTx)
redeemers.forEach((redeemer) => {
- console.log("Redeemer tag:", redeemer.redeemer_tag)
- console.log("Redeemer index:", redeemer.redeemer_index)
- console.log("Memory units:", redeemer.ex_units.mem)
- console.log("CPU steps:", redeemer.ex_units.steps)
+ console.log(`[${redeemer.redeemer_tag}#${redeemer.redeemer_index}]`,
+ `mem: ${redeemer.ex_units.mem}, steps: ${redeemer.ex_units.steps}`)
})
```
-## Submission Service Pattern
-
-Create a transaction submission service with error handling:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-interface SubmissionResult {
- success: boolean
- txHash?: string
- confirmed?: boolean
- error?: string
-}
-
-export async function submitAndWait(signedTxCbor: string, checkInterval = 5000): Promise {
- try {
- const signedTx = Transaction.fromCBORHex(signedTxCbor)
- const txHash = await client.submitTx(signedTx)
-
- console.log("Transaction submitted:", txHash)
-
- const confirmed = await client.awaitTx(txHash, checkInterval)
-
- return {
- success: true,
- txHash: TransactionHash.toHex(txHash),
- confirmed
- }
- } catch (error: any) {
- console.error("Submission failed:", error)
-
- return {
- success: false,
- error: error.message
- }
- }
-}
-```
-
-## Batch Submission
-
-Submit multiple transactions sequentially:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-async function submitBatch(signedTxs: string[]) {
- const results = []
-
- for (const txCbor of signedTxs) {
- try {
- const tx = Transaction.fromCBORHex(txCbor)
- const txHash = await client.submitTx(tx)
- console.log("Submitted:", txHash)
-
- const confirmed = await client.awaitTx(txHash)
-
- results.push({
- success: true,
- txHash: TransactionHash.toHex(txHash),
- confirmed
- })
- } catch (error: any) {
- results.push({
- success: false,
- error: error.message
- })
- }
- }
-
- return results
-}
-```
-
-## Error Handling
-
-Handle common submission errors:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-async function safeSubmit(signedTxCbor: string) {
- try {
- const signedTx = Transaction.fromCBORHex(signedTxCbor)
- const txHash = await client.submitTx(signedTx)
- return { success: true as const, txHash }
- } catch (error: any) {
- // Common errors
- if (error.message.includes("OutsideValidityIntervalUTxO")) {
- return {
- success: false as const,
- error: "Transaction expired (outside validity interval)"
- }
- }
-
- if (error.message.includes("BadInputsUTxO")) {
- return {
- success: false as const,
- error: "UTxO already spent"
- }
- }
-
- if (error.message.includes("ValueNotConservedUTxO")) {
- return {
- success: false as const,
- error: "Input/output value mismatch"
- }
- }
-
- if (error.message.includes("FeeTooSmallUTxO")) {
- return {
- success: false as const,
- error: "Transaction fee too low"
- }
- }
-
- return {
- success: false as const,
- error: error.message
- }
- }
-}
-```
-
-## Retry Logic
-
-Implement retry logic for transient failures:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-async function submitWithRetry(signedTxCbor: string, maxRetries = 3, delayMs = 1000) {
- let lastError: any
-
- for (let attempt = 1; attempt <= maxRetries; attempt++) {
- try {
- const signedTx = Transaction.fromCBORHex(signedTxCbor)
- const txHash = await client.submitTx(signedTx)
- console.log(`Success on attempt ${attempt}:`, txHash)
- return { success: true as const, txHash }
- } catch (error: any) {
- lastError = error
- console.log(`Attempt ${attempt} failed:`, error.message)
-
- // Don't retry on certain errors
- if (error.message.includes("BadInputsUTxO") || error.message.includes("OutsideValidityIntervalUTxO")) {
- break
- }
-
- if (attempt < maxRetries) {
- await new Promise((resolve) => setTimeout(resolve, delayMs))
- }
- }
- }
-
- return {
- success: false as const,
- error: lastError?.message || "Unknown error"
- }
-}
-```
-
-## Monitoring Pattern
-
-Track transaction status with periodic checks:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-async function monitorTransaction(
- txHashHex: string,
- timeout = 300000, // 5 minutes
- checkInterval = 5000
-): Promise {
- const startTime = Date.now()
- const txHash = TransactionHash.fromHex(txHashHex)
-
- while (Date.now() - startTime < timeout) {
- try {
- const confirmed = await client.awaitTx(txHash, checkInterval)
-
- if (confirmed) {
- console.log("Transaction confirmed:", txHashHex)
- return true
- }
- } catch (error: any) {
- console.error("Monitoring error:", error)
- }
-
- await new Promise((resolve) => setTimeout(resolve, checkInterval))
- }
-
- console.log("Transaction confirmation timeout:", txHashHex)
- return false
-}
-```
-
-## Complete Submission Flow
-
-End-to-end transaction submission with all error handling:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-interface TransactionStatus {
- status: "submitted" | "confirmed" | "failed"
- txHash?: string
- error?: string
- attempts?: number
- confirmedAt?: Date
-}
-
-export async function submitTransaction(signedTxCbor: string): Promise {
- const maxRetries = 3
- let attempts = 0
-
- // Submission with retry
- while (attempts < maxRetries) {
- attempts++
-
- try {
- const signedTx = Transaction.fromCBORHex(signedTxCbor)
- const txHash = await client.submitTx(signedTx)
-
- console.log(`Submitted (attempt ${attempts}):`, txHash)
-
- // Wait for confirmation
- const confirmed = await client.awaitTx(txHash, 5000)
-
- if (confirmed) {
- return {
- status: "confirmed",
- txHash: TransactionHash.toHex(txHash),
- attempts,
- confirmedAt: new Date()
- }
- }
-
- return {
- status: "submitted",
- txHash: TransactionHash.toHex(txHash),
- attempts
- }
- } catch (error: any) {
- console.error(`Attempt ${attempts} failed:`, error.message)
-
- // Terminal errors - don't retry
- if (
- error.message.includes("BadInputsUTxO") ||
- error.message.includes("OutsideValidityIntervalUTxO") ||
- error.message.includes("ValueNotConservedUTxO")
- ) {
- return {
- status: "failed",
- error: error.message,
- attempts
- }
- }
-
- // Wait before retry
- if (attempts < maxRetries) {
- await new Promise((resolve) => setTimeout(resolve, 2000))
- }
- }
- }
-
- return {
- status: "failed",
- error: "Max retries exceeded",
- attempts
- }
-}
-```
-
-## Evaluation Before Submission
-
-Validate scripts before submitting:
-
-```typescript twoslash
-import { Transaction, TransactionHash, mainnet, Client } from "@evolution-sdk/evolution"
-
-const client = Client.make(mainnet)
- .withBlockfrost({
- baseUrl: "https://cardano-mainnet.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_PROJECT_ID!
- })
-
-async function evaluateAndSubmit(signedTxCbor: string) {
- try {
- // Evaluate first
- const signedTx = Transaction.fromCBORHex(signedTxCbor)
- const redeemers = await client.evaluateTx(signedTx)
-
- console.log("Script evaluation:")
- redeemers.forEach((r, i) => {
- console.log(` Redeemer ${i}:`, {
- tag: r.redeemer_tag,
- memory: r.ex_units.mem,
- steps: r.ex_units.steps
- })
- })
-
- // Submit if evaluation succeeds
- const txHash = await client.submitTx(signedTx)
- const confirmed = await client.awaitTx(txHash)
-
- return {
- success: true,
- txHash: TransactionHash.toHex(txHash),
- confirmed,
- redeemers
- }
- } catch (error: any) {
- return {
- success: false,
- error: error.message
- }
+## Common Submission Errors
+
+| Error String | Meaning | Retryable? |
+| --- | --- | --- |
+| `OutsideValidityIntervalUTxO` | Transaction expired | No — rebuild with new validity |
+| `BadInputsUTxO` | UTxO already spent | No — rebuild with fresh UTxOs |
+| `ValueNotConservedUTxO` | Input/output value mismatch | No — fix transaction logic |
+| `FeeTooSmallUTxO` | Fee too low | No — rebuild with correct fee |
+| Network timeout | Provider unreachable | Yes — retry after delay |
+
+```typescript
+try {
+ const signedTx = Transaction.fromCBORHex(signedTxCbor)
+ const txHash = await client.submitTx(signedTx)
+ const confirmed = await client.awaitTx(txHash)
+} catch (error: any) {
+ // Terminal errors — don't retry, rebuild the transaction
+ if (error.message.includes("BadInputsUTxO")) {
+ console.error("UTxO already spent — rebuild with fresh UTxOs")
+ } else if (error.message.includes("OutsideValidityIntervalUTxO")) {
+ console.error("Transaction expired — rebuild with new validity window")
+ } else {
+ // Transient errors — safe to retry
+ console.error("Submission failed:", error.message)
}
}
```
## Next Steps
-Explore real-world use cases:
-
-- [Use Cases](/docs/providers/use-cases) - Complete examples and patterns
-- [Provider Types](/docs/providers/provider-types) - Choose the right provider
+- [Use Cases](/docs/providers/use-cases) — Complete real-world examples
+- [Provider Types](/docs/providers/provider-types) — Choose the right provider
+- [Error Handling](/docs/advanced/error-handling) — Full error type reference and debugging
+- [Retry-Safe Transactions](/docs/transactions/retry-safe) — Builder-level retry patterns
diff --git a/docs/content/docs/querying/utxos.mdx b/docs/content/docs/querying/utxos.mdx
index 97f428ad..56aa3548 100644
--- a/docs/content/docs/querying/utxos.mdx
+++ b/docs/content/docs/querying/utxos.mdx
@@ -85,7 +85,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const refs: any[]
+// TransactionInput identifies a specific UTxO by tx hash + output index
+declare const refs: any[] // Array of TransactionInput (from previous tx results or chain queries)
const utxos = await client.getUtxosByOutRef(refs)
```
diff --git a/docs/content/docs/smart-contracts/apply-params.mdx b/docs/content/docs/smart-contracts/apply-params.mdx
new file mode 100644
index 00000000..f3d1609a
--- /dev/null
+++ b/docs/content/docs/smart-contracts/apply-params.mdx
@@ -0,0 +1,164 @@
+---
+title: Parameterized Scripts
+description: Apply parameters to Plutus scripts at runtime
+---
+
+# Parameterized Scripts
+
+Parameterized scripts are Plutus validators that accept configuration at deployment time. Instead of hardcoding values like an owner's key hash or a deadline into the script, you leave them as parameters and apply them before using the script on-chain.
+
+This is the standard pattern for reusable smart contracts — write one validator, deploy it with different parameters for different use cases.
+
+## How It Works
+
+A parameterized Plutus script is compiled with "holes" — lambda abstractions at the top level. When you apply parameters, Evolution SDK:
+
+1. Decodes the double-CBOR-encoded script to UPLC
+2. Wraps each parameter as a UPLC `Constant` node
+3. Creates `Apply` nodes to fill in the lambda parameters
+4. Re-encodes the result back to double-CBOR hex
+
+The result is a fully applied script ready to use in transactions.
+
+## Apply with Raw Data
+
+Use `UPLC.applyParamsToScript` when you have parameters as `Data.Data` values:
+
+```typescript twoslash
+import { Data, UPLC } from "@evolution-sdk/evolution"
+
+// Your compiled parameterized script (double-CBOR hex from Aiken, Plutarch, etc.)
+declare const compiledScript: string
+
+// Apply a key hash and deadline as parameters
+const appliedScript = UPLC.applyParamsToScript(compiledScript, [
+ Data.bytearray("abc123def456abc123def456abc123def456abc123def456abc123de"), // owner key hash
+ Data.int(1735689600000n), // deadline
+])
+
+// appliedScript is now a fully applied script ready for transactions
+```
+
+Each parameter is applied in order, matching the lambda bindings in the compiled script.
+
+## Apply with Type-Safe Schemas
+
+Use `UPLC.applyParamsToScriptWithSchema` for type-safe parameter conversion via a `toData` function:
+
+```typescript twoslash
+import { Bytes, Data, TSchema, UPLC } from "@evolution-sdk/evolution"
+
+// Define the parameter schema
+const ParamsSchema = TSchema.Struct({
+ owner: TSchema.ByteArray,
+ deadline: TSchema.Integer,
+})
+
+const ParamsCodec = Data.withSchema(ParamsSchema)
+
+declare const compiledScript: string
+
+// Apply with type-safe conversion — each struct field becomes one parameter
+const appliedScript = UPLC.applyParamsToScriptWithSchema(
+ compiledScript,
+ [
+ ParamsCodec.toData({
+ owner: Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ deadline: 1735689600000n,
+ }),
+ ],
+ (value) => value, // Already converted to Data
+)
+```
+
+## Full Example: Parameterized Vesting Contract
+
+Here's a complete workflow — apply parameters to a vesting script, then lock funds to it:
+
+```typescript twoslash
+import {
+ Address,
+ Assets,
+ Bytes,
+ Credential,
+ Data,
+ InlineDatum,
+ UPLC,
+ ScriptHash,
+ TSchema,
+ preprod,
+ Client,
+} from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// 1. Start with the compiled parameterized script
+declare const compiledVestingScript: string
+
+// 2. Apply parameters: beneficiary key hash and unlock deadline
+const appliedScript = UPLC.applyParamsToScript(compiledVestingScript, [
+ Data.bytearray("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ Data.int(1735689600000n),
+])
+
+// 3. Use the applied script in a transaction
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu"),
+ assets: Assets.fromLovelace(50_000_000n),
+ datum: new InlineDatum.InlineDatum({ data: Data.constr(0n, []) }),
+ })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## CBOR Encoding Options
+
+By default, `applyParamsToScript` uses Aiken-compatible encoding (indefinite-length arrays and maps). If your script was compiled with a different tool, you can pass a different CBOR preset:
+
+```typescript twoslash
+import { CBOR, Data, UPLC } from "@evolution-sdk/evolution"
+
+declare const compiledScript: string
+
+// With default Aiken encoding (indefinite-length arrays/maps)
+const applied = UPLC.applyParamsToScript(compiledScript, [
+ Data.int(42n),
+])
+
+// With CML-compatible encoding (definite-length)
+const appliedCml = UPLC.applyParamsToScript(
+ compiledScript,
+ [Data.int(42n)],
+ CBOR.CML_DATA_DEFAULT_OPTIONS,
+)
+```
+
+## When to Use Parameterized Scripts
+
+| Scenario | Approach |
+| --- | --- |
+| Same logic, different config per deployment | Parameterized script |
+| One-off validator with fixed logic | Non-parameterized script |
+| Config changes at runtime (per-transaction) | Use datum fields instead |
+
+Common parameters include:
+- **Owner/admin key hashes** — Who can perform admin actions
+- **Deadlines** — Time-based unlock conditions
+- **Token policy IDs** — Which tokens the contract manages
+- **Oracle addresses** — External data feed references
+
+## Next Steps
+
+- [Locking to Script](/docs/smart-contracts/locking) — Lock funds to your parameterized script
+- [Spending from Script](/docs/smart-contracts/spending) — Spend with redeemers
+- [Datums](/docs/smart-contracts/datums) — Attach state to script outputs
+- [TSchema](/docs/encoding/tschema) — Type-safe data encoding
diff --git a/docs/content/docs/smart-contracts/blueprint-codegen.mdx b/docs/content/docs/smart-contracts/blueprint-codegen.mdx
new file mode 100644
index 00000000..b9522727
--- /dev/null
+++ b/docs/content/docs/smart-contracts/blueprint-codegen.mdx
@@ -0,0 +1,211 @@
+---
+title: Blueprint Codegen
+description: Generate type-safe TypeScript schemas from CIP-57 Plutus Blueprints
+---
+
+# Blueprint Codegen
+
+When you compile a smart contract with Aiken (or another toolchain that outputs [CIP-57 Plutus Blueprints](https://cips.cardano.org/cip/CIP-0057)), you get a `plutus.json` file describing your validators, their parameters, datums, and redeemers.
+
+Evolution SDK can generate type-safe TSchema definitions from this blueprint, giving you compile-time type safety for all on-chain data interactions.
+
+## Quick Start
+
+```typescript
+import { Blueprint } from "@evolution-sdk/evolution"
+import * as fs from "fs"
+
+// 1. Load your blueprint
+const blueprint = JSON.parse(fs.readFileSync("plutus.json", "utf-8"))
+
+// 2. Generate TypeScript code
+const code = Blueprint.Codegen.generateTypeScript(blueprint)
+
+// 3. Write to file
+fs.writeFileSync("src/contract-types.ts", code)
+```
+
+The generated file includes:
+- TSchema definitions for every type in your blueprint
+- Validator metadata (script hashes, parameter schemas)
+- Full type safety for datums, redeemers, and parameters
+
+## Configuration
+
+Pass a config object to customize code generation:
+
+```typescript
+import { Blueprint } from "@evolution-sdk/evolution"
+
+declare const blueprint: any
+
+const code = Blueprint.Codegen.generateTypeScript(blueprint, {
+ // How to generate Option
+ optionStyle: "NullOr", // "NullOr" | "UndefinedOr" | "Union"
+
+ // How to generate union types
+ unionStyle: "Variant", // "Variant" | "Struct" | "TaggedStruct"
+
+ // How to generate empty constructors
+ emptyConstructorStyle: "Literal", // "Literal" | "Struct"
+
+ // Module organization
+ moduleStrategy: "flat", // "flat" | "namespaced"
+
+ // Include constructor index in TSchema
+ includeIndex: false,
+
+ // Field naming for unnamed constructor fields
+ fieldNaming: {
+ singleFieldName: "value",
+ multiFieldPattern: "field{index}",
+ },
+
+ // Import paths for generated code
+ imports: {
+ data: 'import { Data } from "@evolution-sdk/evolution"',
+ tschema: 'import { TSchema } from "@evolution-sdk/evolution"',
+ schema: 'import { Schema } from "effect"',
+ },
+
+ indent: " ",
+})
+```
+
+### Option Styles
+
+Controls how `Option` types from your blueprint are represented:
+
+| Style | Generated Code | Use When |
+| --- | --- | --- |
+| `"NullOr"` | `TSchema.NullOr(T)` | Default, idiomatic TypeScript |
+| `"UndefinedOr"` | `TSchema.UndefinedOr(T)` | Prefer undefined over null |
+| `"Union"` | `TSchema.Union(TaggedStruct("Some", ...), TaggedStruct("None", ...))` | Need explicit pattern matching |
+
+### Union Styles
+
+Controls how union types with named constructors are generated:
+
+| Style | Generated Code | Use When |
+| --- | --- | --- |
+| `"Variant"` | `TSchema.Variant({ Tag1: {...}, Tag2: {...} })` | Default, compact |
+| `"Struct"` | `TSchema.Union(TSchema.Struct(...), ...)` | Verbose, same encoding |
+| `"TaggedStruct"` | `TSchema.Union(TaggedStruct("Tag1", ...), ...)` | Effect `_tag` style |
+
+### Module Strategy
+
+Controls how types are organized in the generated file:
+
+- **`"flat"`** (default) — All types at top level: `CardanoAddressCredential`, `CardanoAssetsPolicyId`
+- **`"namespaced"`** — Nested TypeScript namespaces: `Cardano.Address.Credential`, `Cardano.Assets.PolicyId`
+
+### Custom Field Names
+
+When your blueprint has constructors with unnamed fields, you can provide explicit names:
+
+```typescript
+import { Blueprint } from "@evolution-sdk/evolution"
+
+declare const blueprint: any
+
+const code = Blueprint.Codegen.generateTypeScript(blueprint, {
+ optionStyle: "NullOr",
+ unionStyle: "Variant",
+ emptyConstructorStyle: "Literal",
+ moduleStrategy: "flat",
+ includeIndex: false,
+ useRelativeRefs: true,
+ fieldNaming: {
+ singleFieldName: "value",
+ multiFieldPattern: "field{index}",
+ },
+ imports: {
+ data: 'import { Data } from "@evolution-sdk/evolution"',
+ tschema: 'import { TSchema } from "@evolution-sdk/evolution"',
+ schema: 'import { Schema } from "effect"',
+ },
+ indent: " ",
+ variantFieldNames: {
+ "Credential.VerificationKey": ["hash"],
+ "Credential.Script": ["hash"],
+ },
+})
+```
+
+## What Gets Generated
+
+For a blueprint with validators and type definitions, the codegen produces:
+
+**Schema definitions** — TSchema types for every definition in the blueprint:
+```typescript
+// Generated from Blueprint: my-contract
+import { Data } from "@evolution-sdk/evolution"
+import { TSchema } from "@evolution-sdk/evolution"
+
+export const PlutusData = TSchema.PlutusData
+
+export const ByteArray = TSchema.ByteArray
+export const Int = TSchema.Integer
+
+export const Credential = TSchema.Variant({
+ VerificationKey: { hash: TSchema.ByteArray },
+ Script: { hash: TSchema.ByteArray },
+})
+```
+
+**Validator metadata** — Script info for each validator:
+```typescript
+export const myValidator = {
+ title: "my_validator",
+ compiledCode: "5907...",
+ hash: "abc123...",
+}
+```
+
+## Using Generated Types
+
+Once generated, use the schemas with `Data.withSchema` for type-safe encoding/decoding:
+
+```typescript
+import { Data } from "@evolution-sdk/evolution"
+// Import from your generated file
+// import { Credential, myValidator } from "./contract-types"
+
+declare const Credential: any
+declare const myValidator: { compiledCode: string }
+
+const CredentialCodec = Data.withSchema(Credential)
+
+// Type-safe datum creation
+const datum = CredentialCodec.toData({
+ VerificationKey: {
+ hash: new Uint8Array(28), // Type error if wrong shape
+ },
+})
+```
+
+## End-to-End Workflow
+
+1. **Compile** your smart contract (e.g., `aiken build`)
+2. **Generate** types: run the codegen against `plutus.json`
+3. **Import** generated schemas in your app
+4. **Use** with `Data.withSchema` for type-safe transaction building
+
+```typescript
+import { Blueprint } from "@evolution-sdk/evolution"
+import * as fs from "fs"
+
+// Build step: generate types from blueprint
+const blueprint = JSON.parse(fs.readFileSync("plutus.json", "utf-8"))
+const code = Blueprint.Codegen.generateTypeScript(blueprint)
+fs.writeFileSync("src/generated/contract-types.ts", code)
+```
+
+Then in your application code, import and use the generated types with full type safety.
+
+## Next Steps
+
+- [Parameterized Scripts](/docs/smart-contracts/apply-params) — Apply parameters before using a validator
+- [TSchema](/docs/encoding/tschema) — Understand the schema system
+- [Datums](/docs/smart-contracts/datums) — Attach data to script outputs
+- [Spending from Script](/docs/smart-contracts/spending) — Unlock funds with type-safe redeemers
diff --git a/docs/content/docs/smart-contracts/datums.mdx b/docs/content/docs/smart-contracts/datums.mdx
index 06fafd8b..ddbafbd5 100644
--- a/docs/content/docs/smart-contracts/datums.mdx
+++ b/docs/content/docs/smart-contracts/datums.mdx
@@ -57,7 +57,7 @@ const client = Client.make(preprod)
// Compute datum hash from PlutusData
const datum = Data.constr(0n, [5000000n])
-const datumHash = Data.hashData(datum)
+const datumHash = Data.toDatumHash(datum)
const tx = await client
.newTx()
diff --git a/docs/content/docs/smart-contracts/index.mdx b/docs/content/docs/smart-contracts/index.mdx
index 6109df13..d70ee6e1 100644
--- a/docs/content/docs/smart-contracts/index.mdx
+++ b/docs/content/docs/smart-contracts/index.mdx
@@ -11,6 +11,26 @@ Evolution SDK provides full support for Cardano smart contracts — Plutus V1, V
The transaction builder handles script evaluation, redeemer indexing, and collateral selection automatically. You focus on your contract logic.
+## Smart Contract Flow
+
+import { Mermaid } from "@/components/mdx/mermaid"
+
+(Aiken, Plutarch)"] --> B{"Parameterized?"}
+ B -->|Yes| C["⚙️ Apply Params
UPLC.applyParamsToScript"]
+ B -->|No| D["📦 Script Ready"]
+ C --> D
+ D --> E["🔒 Lock
payToAddress + datum"]
+ E --> F["🔓 Spend
collectFrom + redeemer"]
+
+ style A fill:#f0f9ff,stroke:#0284c7,stroke-width:2px,color:#0c4a6e
+ style C fill:#fefce8,stroke:#ca8a04,stroke-width:2px,color:#713f12
+ style D fill:#f0fdf4,stroke:#16a34a,stroke-width:2px,color:#14532d
+ style E fill:#fdf2f8,stroke:#db2777,stroke-width:2px,color:#831843
+ style F fill:#faf5ff,stroke:#9333ea,stroke-width:2px,color:#581c87
+`} />
+
## How It Works
Smart contract interaction in Cardano involves three concepts:
@@ -68,8 +88,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Spend from script with a redeemer
const tx = await client
@@ -96,13 +116,9 @@ const hash = await signed.submit()
## What the Builder Handles
-When you build a transaction with scripts, Evolution SDK automatically:
-
-- **Evaluates scripts** — Computes execution units (memory + CPU) for each script
-- **Indexes redeemers** — Assigns correct indices after coin selection changes input order
-- **Selects collateral** — Sets aside collateral UTxOs required for script transactions
-- **Calculates fees** — Includes script execution costs in fee computation
-- **Attaches cost models** — Includes protocol cost models in the script data hash
+
+**You focus on your contract logic — the builder handles the rest.** When you build a transaction with scripts, Evolution SDK automatically evaluates scripts (computes execution units), indexes redeemers (after coin selection reorders inputs), selects collateral, calculates fees (including script execution costs), and attaches cost models.
+
## Next Steps
@@ -116,10 +132,31 @@ When you build a transaction with scripts, Evolution SDK automatically:
Unlock funds with redeemers
+
+ Mint and burn native tokens with minting policies
+
+
+ Time-locks, multi-sig, and simple minting without Plutus
+
Static, self, and batch redeemer modes
Reduce transaction size with on-chain scripts
+
+ Apply parameters to reusable validators
+
+
+ Generate type-safe schemas from CIP-57 blueprints
+
+
+ End-to-end tutorial — lock, wait, claim
+
+
+ Mint with CIP-25 metadata — name, image, description
+
+
+ 2-of-3 shared funds — no single person controls spending
+
diff --git a/docs/content/docs/smart-contracts/locking.mdx b/docs/content/docs/smart-contracts/locking.mdx
index f98e6050..edb96100 100644
--- a/docs/content/docs/smart-contracts/locking.mdx
+++ b/docs/content/docs/smart-contracts/locking.mdx
@@ -102,7 +102,7 @@ const scriptAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t
// Create assets with ADA + tokens
let assets = Assets.fromLovelace(5_000_000n)
-assets = Assets.addByHex(assets, "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6", "", 100n)
+assets = Assets.addByHex(assets, "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1", "", 100n)
const tx = await client
.newTx()
@@ -131,7 +131,7 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const validatorScript: any
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
const scriptAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu")
diff --git a/docs/content/docs/smart-contracts/meta.json b/docs/content/docs/smart-contracts/meta.json
index e949fb66..5e92ae89 100644
--- a/docs/content/docs/smart-contracts/meta.json
+++ b/docs/content/docs/smart-contracts/meta.json
@@ -2,9 +2,17 @@
"title": "Smart Contracts",
"pages": [
"index",
- "validators",
- "datums-redeemers",
- "minting-policies",
- "testing"
+ "datums",
+ "locking",
+ "spending",
+ "minting",
+ "native-scripts",
+ "redeemers",
+ "reference-scripts",
+ "apply-params",
+ "blueprint-codegen",
+ "vesting",
+ "mint-nft",
+ "multi-sig"
]
}
diff --git a/docs/content/docs/smart-contracts/mint-nft.mdx b/docs/content/docs/smart-contracts/mint-nft.mdx
new file mode 100644
index 00000000..3fec4f88
--- /dev/null
+++ b/docs/content/docs/smart-contracts/mint-nft.mdx
@@ -0,0 +1,180 @@
+---
+title: "Tutorial: Mint an NFT"
+description: Mint a Cardano NFT with CIP-25 metadata — from minting policy to on-chain token
+---
+
+# Tutorial: Mint an NFT
+
+Mint a unique NFT on Cardano with CIP-25 metadata — a name, image, and description stored on-chain. This tutorial uses a native script minting policy (no Plutus required) and attaches standard NFT metadata.
+
+## What You'll Build
+
+- A **native script minting policy** that only you can mint from
+- An **NFT** (quantity 1) with CIP-25 metadata (name, image, description)
+- A **transaction** that mints the token, attaches metadata, and sends it to a recipient
+
+## Prerequisites
+
+- A Blockfrost API key ([get one free](https://blockfrost.io))
+- Basic familiarity with [minting tokens](/docs/smart-contracts/minting) and [native scripts](/docs/smart-contracts/native-scripts)
+
+## Step 1: Create a Minting Policy
+
+A simple native script policy — only your key can mint:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+// Your key hash (28 bytes) — from your wallet
+const myKeyHash = Bytes.fromHex(
+ "abc123def456abc123def456abc123def456abc123def456abc123de"
+)
+
+// Minting policy: only this key can authorize minting
+const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
+const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy })
+
+// The policy ID = blake2b-224 hash of the script CBOR
+// After minting, you can find it in the transaction output or compute it offline
+// For this tutorial, we'll use the known policy ID in subsequent steps
+```
+
+
+For a **one-time mint** (true NFT uniqueness), add a time-lock to the policy. After the deadline passes, nobody can mint more tokens under this policy. See [Native Scripts](/docs/smart-contracts/native-scripts) for time-lock examples.
+
+
+## Step 2: Structure CIP-25 Metadata
+
+CIP-25 defines the standard metadata format for Cardano NFTs. It uses transaction metadata label `721`:
+
+```typescript
+import { TransactionMetadatum } from "@evolution-sdk/evolution"
+
+// Your policy ID (28 bytes = 56 hex chars)
+const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de"
+
+// Asset name in hex — e.g., "MyNFT001" → hex
+const assetNameHex = "4d794e4654303031" // "MyNFT001" in hex
+
+// CIP-25 metadata structure:
+// { 721: { : { : { name, image, ... } } } }
+const nftMetadata = new Map([
+ [policyId, new Map([
+ [assetNameHex, new Map([
+ ["name", "My First NFT"],
+ ["image", "ipfs://QmYourImageHashHere"],
+ ["mediaType", "image/png"],
+ ["description", "My first NFT minted with Evolution SDK"],
+ ])]
+ ])]
+])
+```
+
+### CIP-25 Required Fields
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `name` | string | Display name of the NFT |
+| `image` | string | URI to the image (typically `ipfs://...`) |
+
+### CIP-25 Optional Fields
+
+| Field | Type | Description |
+| --- | --- | --- |
+| `mediaType` | string | MIME type of the image (e.g., `image/png`) |
+| `description` | string | Human-readable description |
+| `files` | array | Additional files (for multi-asset NFTs) |
+
+## Step 3: Mint, Attach Metadata, and Send
+
+Put it all together — mint the NFT, attach CIP-25 metadata, and send to a recipient:
+
+```typescript
+import {
+ Address, Assets, NativeScripts, Bytes, TransactionMetadatum,
+ preprod, Client
+} from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// --- Policy ---
+const myKeyHash = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
+const nativeScript = new NativeScripts.NativeScript({ script: mintingPolicy })
+
+// --- NFT identity ---
+const policyId = "abc123def456abc123def456abc123def456abc123def456abc123de" // script hash
+const assetName = "4d794e4654303031" // "MyNFT001" in hex
+
+// --- Mint assets (quantity 1 = NFT) ---
+let mintAssets = Assets.fromLovelace(0n)
+mintAssets = Assets.addByHex(mintAssets, policyId, assetName, 1n)
+
+// --- Send assets (NFT + min ADA for UTxO) ---
+let sendAssets = Assets.fromLovelace(2_000_000n)
+sendAssets = Assets.addByHex(sendAssets, policyId, assetName, 1n)
+
+// --- CIP-25 metadata ---
+const nftMetadata = new Map([
+ [policyId, new Map([
+ [assetName, new Map([
+ ["name", "My First NFT"],
+ ["image", "ipfs://QmYourImageHashHere"],
+ ["mediaType", "image/png"],
+ ["description", "Minted with Evolution SDK"],
+ ])]
+ ])]
+])
+
+const recipient = Address.fromBech32(
+ "addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"
+)
+
+// --- Build transaction ---
+const tx = await client
+ .newTx()
+ .mintAssets({ assets: mintAssets }) // mint 1 NFT
+ .attachScript({ script: nativeScript }) // attach minting policy
+ .attachMetadata({ label: 721n, metadata: nftMetadata }) // CIP-25 metadata
+ .payToAddress({ address: recipient, assets: sendAssets }) // send NFT to recipient
+ .build()
+
+const signed = await tx.sign()
+const txHash = await signed.submit()
+// txHash → your NFT is now on-chain!
+```
+
+## How It Works
+
+1. **`mintAssets`** — creates 1 token under your policy ID (quantity 1 = non-fungible)
+2. **`attachScript`** — includes the native script so the ledger can verify your minting authority
+3. **`attachMetadata`** — adds CIP-25 metadata under label 721 (the NFT metadata standard)
+4. **`payToAddress`** — sends the minted NFT + min ADA to the recipient
+
+The builder handles fee calculation, coin selection, and change automatically.
+
+## Common Pitfalls
+
+
+**Metadata label must be `721n`** (bigint). Using `721` (number) will cause a type error. CIP-25 requires label 721.
+
+
+| Problem | Cause | Fix |
+| --- | --- | --- |
+| NFT not showing in wallet | Wrong metadata structure | Ensure policy ID and asset name in metadata match the minted token exactly |
+| "Minting not allowed" | Wrong key signed | Ensure the signing wallet's key hash matches the minting policy |
+| Missing image | Invalid IPFS URI | Use full `ipfs://Qm...` format, pin the file first |
+| Type error on label | Using number instead of bigint | Use `721n` not `721` |
+| Min UTxO too low | Not enough ADA with the NFT | Include at least 2 ADA with the NFT output |
+
+## Next Steps
+
+- [Minting Tokens](/docs/smart-contracts/minting) — Plutus minting policies and burning
+- [Native Scripts](/docs/smart-contracts/native-scripts) — Time-locked policies for one-time mints
+- [Asset Metadata](/docs/assets/metadata) — More metadata patterns (CIP-20 messages)
+- [Blueprint Codegen](/docs/smart-contracts/blueprint-codegen) — Generate types from Aiken validators
diff --git a/docs/content/docs/smart-contracts/minting.mdx b/docs/content/docs/smart-contracts/minting.mdx
new file mode 100644
index 00000000..99a565ec
--- /dev/null
+++ b/docs/content/docs/smart-contracts/minting.mdx
@@ -0,0 +1,146 @@
+---
+title: Minting Tokens
+description: Mint and burn native tokens with minting policies
+---
+
+# Minting Tokens
+
+Minting creates new native tokens on Cardano. Every mint requires a minting policy — either a Plutus script or a native script — that authorizes which tokens can be created and under what conditions.
+
+## How It Works
+
+1. Define the tokens to mint (policy ID + asset name + quantity)
+2. Attach the minting policy script
+3. Provide a redeemer (for Plutus policies)
+4. Build, sign, submit — the builder handles the rest
+
+Positive quantities mint tokens. Negative quantities burn them.
+
+## Mint with a Plutus Policy
+
+```typescript twoslash
+import { Assets, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const mintingPolicy: any // compiled Plutus minting policy (from Aiken build or Blueprint codegen)
+
+// Mint 1000 tokens
+const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0"
+const assetName = "4d79546f6b656e" // "MyToken" in hex
+let assets = Assets.fromLovelace(0n)
+assets = Assets.addByHex(assets, policyId, assetName, 1000n)
+
+const tx = await client
+ .newTx()
+ .mintAssets({
+ assets,
+ redeemer: Data.constr(0n, []), // Minting action
+ label: "mint-my-token"
+ })
+ .attachScript({ script: mintingPolicy })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## Mint and Send in One Transaction
+
+Mint tokens and immediately send them to a recipient:
+
+```typescript twoslash
+import { Address, Assets, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const mintingPolicy: any // compiled Plutus minting policy (from Aiken build or Blueprint codegen)
+
+const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0"
+const assetName = "4d79546f6b656e"
+
+let mintAssets = Assets.fromLovelace(0n)
+mintAssets = Assets.addByHex(mintAssets, policyId, assetName, 1n)
+
+let sendAssets = Assets.fromLovelace(2_000_000n) // Min ADA for UTxO
+sendAssets = Assets.addByHex(sendAssets, policyId, assetName, 1n)
+
+const tx = await client
+ .newTx()
+ .mintAssets({
+ assets: mintAssets,
+ redeemer: Data.constr(0n, []),
+ })
+ .attachScript({ script: mintingPolicy })
+ .payToAddress({
+ address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"),
+ assets: sendAssets
+ })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## Burn Tokens
+
+Use negative quantities to burn tokens you hold:
+
+```typescript twoslash
+import { Assets, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const mintingPolicy: any // compiled Plutus minting policy (from Aiken build or Blueprint codegen)
+
+const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0"
+const assetName = "4d79546f6b656e"
+
+// Negative quantity = burn
+let burnAssets = Assets.fromLovelace(0n)
+burnAssets = Assets.addByHex(burnAssets, policyId, assetName, -500n)
+
+const tx = await client
+ .newTx()
+ .mintAssets({
+ assets: burnAssets,
+ redeemer: Data.constr(1n, []), // Burn action
+ label: "burn-tokens"
+ })
+ .attachScript({ script: mintingPolicy })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## What the Builder Handles
+
+When minting, Evolution SDK automatically:
+- Tracks the minting policy via its policy ID
+- Indexes mint redeemers correctly after coin selection
+- Includes the minting policy in the script data hash
+- Evaluates the minting policy to compute execution units
+- Calculates fees including script execution costs
+
+## Next Steps
+
+- [Locking to Script](/docs/smart-contracts/locking) — Lock minted tokens to a script address
+- [Asset Units](/docs/assets/units) — Understanding policy IDs and asset names
+- [Asset Metadata](/docs/assets/metadata) — Attach CIP-25 NFT metadata
+- [Reference Scripts](/docs/smart-contracts/reference-scripts) — Store minting policies on-chain
diff --git a/docs/content/docs/smart-contracts/multi-sig.mdx b/docs/content/docs/smart-contracts/multi-sig.mdx
new file mode 100644
index 00000000..bca42555
--- /dev/null
+++ b/docs/content/docs/smart-contracts/multi-sig.mdx
@@ -0,0 +1,193 @@
+---
+title: "Tutorial: Multi-Sig Treasury"
+description: Build a 2-of-3 multi-signature treasury — shared funds that require multiple signers to spend
+---
+
+# Tutorial: Multi-Sig Treasury
+
+Build a shared treasury where no single person controls the funds. A 2-of-3 multi-sig requires any 2 out of 3 key holders to approve a withdrawal — perfect for team treasuries, DAOs, and escrow arrangements.
+
+## What You'll Build
+
+- A **2-of-3 native script** (ScriptNOfK) from 3 key hashes
+- A **treasury address** derived from the script
+- A **funding transaction** to deposit ADA
+- A **withdrawal transaction** requiring 2 of 3 signers
+
+No Plutus required — native scripts handle multi-sig natively.
+
+## Prerequisites
+
+- A Blockfrost API key ([get one free](https://blockfrost.io))
+- Familiarity with [native scripts](/docs/smart-contracts/native-scripts)
+- 3 wallets (or 3 account indices from the same mnemonic for testing)
+
+## Step 1: Create the Multi-Sig Script
+
+Define a 2-of-3 policy from three key hashes:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+// Three team members' key hashes (28 bytes each)
+const alice = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+const bob = Bytes.fromHex("b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8")
+const carol = Bytes.fromHex("c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8")
+
+// 2-of-3: any two must sign to spend
+// Returns a NativeScript — ready to use with attachScript()
+const treasuryScript = NativeScripts.makeScriptNOfK(2n, [
+ NativeScripts.makeScriptPubKey(alice),
+ NativeScripts.makeScriptPubKey(bob),
+ NativeScripts.makeScriptPubKey(carol),
+])
+```
+
+### How It Works
+
+| Signers Present | Can Spend? |
+|----------------|------------|
+| Alice + Bob | Yes (2 of 3) |
+| Alice + Carol | Yes (2 of 3) |
+| Bob + Carol | Yes (2 of 3) |
+| Alice only | No (1 of 3) |
+| None | No |
+
+## Step 2: Fund the Treasury
+
+Send ADA to the treasury address. Anyone can deposit — no signatures needed:
+
+```typescript
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// The treasury address is derived from the script hash
+// In practice, compute this from the native script
+declare const treasuryAddress: Address.Address // address derived from the multi-sig script
+
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: treasuryAddress,
+ assets: Assets.fromLovelace(100_000_000n), // 100 ADA
+ })
+ .build()
+
+const signed = await tx.sign()
+const depositTxHash = await signed.submit()
+// depositTxHash → funds now locked in the 2-of-3 treasury
+```
+
+
+Anyone can send funds to the treasury address. The multi-sig restriction only applies to **spending** — deposits are unrestricted.
+
+
+## Step 3: Spend from the Treasury (2 of 3 Sign)
+
+To withdraw, the transaction needs signatures from at least 2 of the 3 key holders:
+
+```typescript
+import { Address, Assets, KeyHash, NativeScripts, Bytes, preprod, type UTxO, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const treasuryUtxos: UTxO.UTxO[] // from client.getUtxos(treasuryAddress)
+declare const treasuryScript: NativeScripts.NativeScript // the 2-of-3 script from Step 1
+
+// Key hashes of the two signers approving this withdrawal
+const aliceKeyHash = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+const bobKeyHash = Bytes.fromHex("b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8")
+
+const recipient = Address.fromBech32(
+ "addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"
+)
+
+const tx = await client
+ .newTx()
+ .collectFrom({ inputs: treasuryUtxos })
+ .attachScript({ script: treasuryScript })
+ .addSigner({ keyHash: new KeyHash.KeyHash({ hash: aliceKeyHash }) })
+ .addSigner({ keyHash: new KeyHash.KeyHash({ hash: bobKeyHash }) })
+ .payToAddress({
+ address: recipient,
+ assets: Assets.fromLovelace(50_000_000n), // withdraw 50 ADA
+ })
+ .build()
+
+// Both Alice and Bob must sign
+const signed = await tx.sign()
+const withdrawTxHash = await signed.submit()
+// withdrawTxHash → 50 ADA sent to recipient, remainder stays in treasury
+```
+
+
+The transaction must be signed by the actual private keys of the listed signers. In a real multi-sig flow, the unsigned transaction CBOR is shared between signers, each adds their signature, then the combined transaction is submitted. See [Client Architecture](/docs/clients/architecture) for the frontend/backend signing pattern.
+
+
+## Variations
+
+### 3-of-3 (Unanimous)
+
+All members must agree:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const alice = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+const bob = Bytes.fromHex("b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8")
+const carol = Bytes.fromHex("c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8")
+
+// All 3 must sign (ScriptAll)
+const unanimousScript = NativeScripts.makeScriptAll([
+ NativeScripts.makeScriptPubKey(alice),
+ NativeScripts.makeScriptPubKey(bob),
+ NativeScripts.makeScriptPubKey(carol),
+])
+```
+
+### Time-Locked Treasury
+
+Add a time constraint — funds can only be withdrawn after a specific date:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const alice = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+const bob = Bytes.fromHex("b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8")
+
+// 2-of-2 AND time-locked (can't spend before slot 50000000)
+const timedTreasury = NativeScripts.makeScriptAll([
+ NativeScripts.makeScriptNOfK(2n, [
+ NativeScripts.makeScriptPubKey(alice),
+ NativeScripts.makeScriptPubKey(bob),
+ ]).script, // unwrap to get the variant
+ NativeScripts.makeInvalidBefore(50000000n),
+])
+```
+
+## Common Pitfalls
+
+| Problem | Cause | Fix |
+| --- | --- | --- |
+| "Missing required signer" | Not enough signers added | Add `.addSigner()` for each approving key |
+| "Native script validation failed" | Wrong key hashes | Verify key hashes match those in the script exactly |
+| Transaction rejected | Only 1 of 3 signed (need 2) | Get a second signer to approve |
+| Wrong treasury address | Script hash doesn't match | Ensure you derive the address from the same script |
+
+## Next Steps
+
+- [Native Scripts](/docs/smart-contracts/native-scripts) — All script types and composition
+- [Tutorial: Token Vesting](/docs/smart-contracts/vesting) — Time-locked release with Plutus
+- [Client Architecture](/docs/clients/architecture) — Multi-party signing flow (frontend/backend)
+- [Spending from Script](/docs/smart-contracts/spending) — Required signers and debug labels
diff --git a/docs/content/docs/smart-contracts/native-scripts.mdx b/docs/content/docs/smart-contracts/native-scripts.mdx
new file mode 100644
index 00000000..6b0ecd55
--- /dev/null
+++ b/docs/content/docs/smart-contracts/native-scripts.mdx
@@ -0,0 +1,198 @@
+---
+title: Native Scripts
+description: Time-locks, multi-sig, and simple minting policies without Plutus
+---
+
+# Native Scripts
+
+Native scripts are lightweight validators that don't require Plutus. They check simple conditions — key signatures, time constraints, or combinations of both. Use them for time-locked vesting, multi-sig wallets, and simple minting policies.
+
+No script evaluation costs, no collateral required, smaller transactions.
+
+## Script Types
+
+| Type | What It Checks | Use Case |
+| --- | --- | --- |
+| **ScriptPubKey** | A specific key signed the transaction | Single-signer authorization |
+| **InvalidBefore** | Transaction is after a slot | Time-locked release |
+| **InvalidHereafter** | Transaction is before a slot | Deadline enforcement |
+| **ScriptAll** | ALL sub-scripts pass | Multi-sig (all must sign) |
+| **ScriptAny** | ANY sub-script passes | Multi-sig (one must sign) |
+| **ScriptNOfK** | N of K sub-scripts pass | M-of-N multi-sig |
+
+## Building Native Scripts
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+// Single key requirement
+const singleSigner = NativeScripts.makeScriptPubKey(
+ Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de") // 28-byte key hash
+)
+
+// Time-lock: can't spend before slot 100000
+const timeLock = NativeScripts.makeInvalidBefore(100000n)
+
+// Deadline: can't spend after slot 200000
+const deadline = NativeScripts.makeInvalidHereafter(200000n)
+```
+
+## Multi-Sig: All Must Sign
+
+Require ALL listed keys to sign the transaction:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const keyHash1 = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+const keyHash2 = Bytes.fromHex("def456abc123def456abc123def456abc123def456abc123def456ab")
+const keyHash3 = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+
+// All three must sign
+const multiSig = NativeScripts.makeScriptAll([
+ NativeScripts.makeScriptPubKey(keyHash1),
+ NativeScripts.makeScriptPubKey(keyHash2),
+ NativeScripts.makeScriptPubKey(keyHash3),
+])
+```
+
+## Multi-Sig: Any Can Sign
+
+Require ANY ONE of the listed keys:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const keyHash1 = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+const keyHash2 = Bytes.fromHex("def456abc123def456abc123def456abc123def456abc123def456ab")
+
+// Either key can authorize
+const anyOf = NativeScripts.makeScriptAny([
+ NativeScripts.makeScriptPubKey(keyHash1),
+ NativeScripts.makeScriptPubKey(keyHash2),
+])
+```
+
+## Multi-Sig: M-of-N
+
+Require at least N of K keys to sign:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const keyHash1 = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+const keyHash2 = Bytes.fromHex("def456abc123def456abc123def456abc123def456abc123def456ab")
+const keyHash3 = Bytes.fromHex("a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8")
+
+// 2-of-3 multi-sig
+const twoOfThree = NativeScripts.makeScriptNOfK(2n, [
+ NativeScripts.makeScriptPubKey(keyHash1),
+ NativeScripts.makeScriptPubKey(keyHash2),
+ NativeScripts.makeScriptPubKey(keyHash3),
+])
+```
+
+## Time-Locked Vesting
+
+Combine a key requirement with a time constraint — funds can only be spent after a deadline:
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const beneficiary = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+
+// Can only spend after slot 50000000 AND with beneficiary's signature
+const vestingScript = NativeScripts.makeScriptAll([
+ NativeScripts.makeScriptPubKey(beneficiary),
+ NativeScripts.makeInvalidBefore(50000000n),
+])
+```
+
+## Minting with Native Scripts
+
+Native scripts work as minting policies — no Plutus, no redeemer, no collateral:
+
+```typescript
+import { Assets, NativeScripts, Bytes, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const myKeyHash = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+
+// Simple minting policy: only my key can mint
+const mintingPolicy = NativeScripts.makeScriptPubKey(myKeyHash)
+
+// Wrap in NativeScript for the builder
+const script = new NativeScripts.NativeScript({ script: mintingPolicy })
+
+// The policy ID is the script hash — you'd compute this from the script CBOR
+// For this example, use a placeholder (28 bytes = 56 hex chars)
+declare const policyId: string // hash of the native script
+
+let assets = Assets.fromLovelace(0n)
+assets = Assets.addByHex(assets, policyId, "4d79546f6b656e", 1000n) // "MyToken"
+
+// No redeemer needed for native scripts
+const tx = await client
+ .newTx()
+ .mintAssets({ assets })
+ .attachScript({ script })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## Utility Functions
+
+```typescript
+import { NativeScripts, Bytes } from "@evolution-sdk/evolution"
+
+const keyHash1 = Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de")
+const keyHash2 = Bytes.fromHex("def456abc123def456abc123def456abc123def456abc123def456ab")
+
+const script = NativeScripts.makeScriptAll([
+ NativeScripts.makeScriptPubKey(keyHash1),
+ NativeScripts.makeScriptPubKey(keyHash2),
+ NativeScripts.makeInvalidBefore(100000n),
+])
+
+// Count minimum required signers
+const count = NativeScripts.countRequiredSigners(script)
+// count = 2 (two key requirements in ScriptAll)
+
+// Extract all key hashes from the script tree
+const keyHashes = NativeScripts.extractKeyHashes(script)
+// keyHashes = [keyHash1, keyHash2]
+
+// Serialize to CBOR
+const cbor = NativeScripts.toCBORHex(
+ new NativeScripts.NativeScript({ script })
+)
+
+// Convert to JSON (for debugging or interop)
+const json = NativeScripts.toJSON(script)
+```
+
+## Native Scripts vs Plutus Scripts
+
+| | Native Scripts | Plutus Scripts |
+| --- | --- | --- |
+| **Complexity** | Simple conditions only | Arbitrary logic |
+| **Execution cost** | None (free) | Memory + CPU units |
+| **Collateral** | Not required | Required |
+| **Transaction size** | Smaller | Larger (script included) |
+| **Redeemer** | Not needed | Required |
+| **Use cases** | Multi-sig, time-locks, simple minting | DeFi, auctions, complex validation |
+
+## Next Steps
+
+- [Minting Tokens](/docs/smart-contracts/minting) — Mint with Plutus minting policies
+- [Locking to Script](/docs/smart-contracts/locking) — Lock funds with datums
+- [Spending from Script](/docs/smart-contracts/spending) — Unlock with redeemers
+- [Time / Slots](/docs/time/slots) — Convert between Unix time and slots for time constraints
diff --git a/docs/content/docs/smart-contracts/redeemers.mdx b/docs/content/docs/smart-contracts/redeemers.mdx
index 5e423834..54d0d9ce 100644
--- a/docs/content/docs/smart-contracts/redeemers.mdx
+++ b/docs/content/docs/smart-contracts/redeemers.mdx
@@ -33,7 +33,7 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Static redeemer — the value is fixed
const tx = await client
@@ -68,7 +68,7 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Self redeemer — callback receives { index, utxo }
const tx = await client
@@ -112,7 +112,7 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const orderUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Batch redeemer — callback sees all specified inputs
const tx = await client
diff --git a/docs/content/docs/smart-contracts/reference-scripts.mdx b/docs/content/docs/smart-contracts/reference-scripts.mdx
index 7c56c337..4c4dc54e 100644
--- a/docs/content/docs/smart-contracts/reference-scripts.mdx
+++ b/docs/content/docs/smart-contracts/reference-scripts.mdx
@@ -27,7 +27,7 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const validatorScript: any
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Store script in a UTxO (send to your own address or a permanent holder)
const myAddress = await client.address()
@@ -66,8 +66,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const referenceScriptUtxo: UTxO.UTxO
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const referenceScriptUtxo: UTxO.UTxO // the UTxO where you deployed the script in Step 1
// Reference the UTxO containing the script instead of attaching it
const tx = await client
diff --git a/docs/content/docs/smart-contracts/spending.mdx b/docs/content/docs/smart-contracts/spending.mdx
index 50481340..d9a70c51 100644
--- a/docs/content/docs/smart-contracts/spending.mdx
+++ b/docs/content/docs/smart-contracts/spending.mdx
@@ -28,8 +28,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
// Spend script UTxOs with a "Claim" redeemer
const tx = await client
@@ -67,8 +67,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
declare const myKeyHash: KeyHash.KeyHash
const tx = await client
@@ -104,8 +104,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
const now = BigInt(Date.now())
@@ -147,8 +147,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptUtxos: UTxO.UTxO[]
-declare const validatorScript: any
+declare const scriptUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress)
+declare const validatorScript: any // compiled Plutus script (from Aiken build or Blueprint codegen)
const beneficiary = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")
diff --git a/docs/content/docs/smart-contracts/vesting.mdx b/docs/content/docs/smart-contracts/vesting.mdx
new file mode 100644
index 00000000..1c2285ff
--- /dev/null
+++ b/docs/content/docs/smart-contracts/vesting.mdx
@@ -0,0 +1,247 @@
+---
+title: "Tutorial: Token Vesting"
+description: Build a complete token vesting contract — lock funds with a deadline, then release to a beneficiary after time passes
+---
+
+# Tutorial: Token Vesting
+
+Build a complete vesting flow: lock funds to a script with a deadline datum, then release them to a beneficiary after the deadline passes. This tutorial ties together TSchema, inline datums, validity ranges, and script spending.
+
+## What You'll Build
+
+A time-locked vesting contract where:
+- An **owner** locks ADA to a script address with a deadline
+- A **beneficiary** can withdraw the funds after the deadline passes
+- The Plutus validator checks: (a) the deadline has passed and (b) the beneficiary signed the transaction
+
+## Prerequisites
+
+- A compiled Plutus vesting validator (from Aiken, Plutarch, or similar)
+- A Blockfrost API key ([get one free](https://blockfrost.io))
+- Basic familiarity with [smart contracts](/docs/smart-contracts) and [TSchema](/docs/encoding/tschema)
+
+## Step 1: Define the Vesting Datum
+
+The datum carries the state your validator needs — who the beneficiary is and when the lock expires:
+
+```typescript twoslash
+import { Bytes, Data, TSchema } from "@evolution-sdk/evolution"
+
+// Define the vesting datum schema
+const VestingDatum = TSchema.Struct({
+ beneficiary: TSchema.ByteArray, // beneficiary's key hash (28 bytes)
+ deadline: TSchema.Integer, // POSIX time in milliseconds
+})
+
+type VestingDatum = typeof VestingDatum.Type
+// { beneficiary: Uint8Array; deadline: bigint }
+
+// Create a codec for encoding/decoding
+const VestingCodec = Data.withSchema(VestingDatum)
+
+// Create a datum
+const datum = VestingCodec.toData({
+ beneficiary: Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ deadline: BigInt(new Date("2025-12-31T23:59:59Z").getTime()), // Dec 31, 2025
+})
+// datum → PlutusData (Constr 0 with 2 fields: ByteArray + Integer)
+```
+
+## Step 2: Lock Funds to the Vesting Script
+
+Send ADA to the script address with the vesting datum attached:
+
+```typescript twoslash
+import { Address, Assets, Bytes, Data, InlineDatum, TSchema, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const VestingDatum = TSchema.Struct({
+ beneficiary: TSchema.ByteArray,
+ deadline: TSchema.Integer,
+})
+const VestingCodec = Data.withSchema(VestingDatum)
+
+// ---cut---
+// Create the datum with beneficiary and deadline
+const datum = VestingCodec.toData({
+ beneficiary: Bytes.fromHex("abc123def456abc123def456abc123def456abc123def456abc123de"),
+ deadline: BigInt(new Date("2025-12-31T23:59:59Z").getTime()),
+})
+
+// Lock 50 ADA to the vesting script address
+const scriptAddress = Address.fromBech32(
+ "addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu"
+)
+
+const tx = await client
+ .newTx()
+ .payToAddress({
+ address: scriptAddress,
+ assets: Assets.fromLovelace(50_000_000n), // 50 ADA
+ datum: new InlineDatum.InlineDatum({ data: datum }),
+ })
+ .build()
+
+const signed = await tx.sign()
+const lockTxHash = await signed.submit()
+// lockTxHash → transaction hash of the locking transaction
+```
+
+
+The locked funds are now at the script address with your datum attached. Nobody can spend them without satisfying the validator — which requires the deadline to pass and the beneficiary to sign.
+
+
+## Step 3: Spend After the Deadline
+
+Once the deadline has passed, the beneficiary can claim the funds. The key is setting the **validity interval** to prove to the validator that the current time is past the deadline:
+
+```typescript twoslash
+import { Address, Assets, Bytes, Data, KeyHash, TSchema, preprod, type UTxO, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const vestingUtxos: UTxO.UTxO[] // from client.getUtxos(scriptAddress) — the locked funds
+declare const vestingScript: any // compiled Plutus vesting validator (from Aiken build or Blueprint codegen)
+declare const beneficiaryKeyHash: KeyHash.KeyHash // beneficiary's key hash (must match datum)
+
+// ---cut---
+const now = BigInt(Date.now())
+
+const tx = await client
+ .newTx()
+ .collectFrom({
+ inputs: vestingUtxos,
+ redeemer: Data.constr(0n, []), // "Claim" action
+ label: "claim-vesting",
+ })
+ .attachScript({ script: vestingScript })
+ .addSigner({ keyHash: beneficiaryKeyHash }) // prove beneficiary signed
+ .setValidity({
+ from: now, // valid from now — proves to validator that deadline has passed
+ to: now + 300_000n, // expires in 5 minutes
+ })
+ .build()
+
+const signed = await tx.sign()
+const claimTxHash = await signed.submit()
+// claimTxHash → funds released to beneficiary's wallet
+```
+
+### Why `setValidity` Matters
+
+Plutus validators can't read the current time directly. Instead, they inspect the transaction's **validity interval** — the range `[from, to]` that the ledger guarantees the transaction was submitted within.
+
+By setting `from: now` where `now > deadline`, you're telling the validator: "this transaction is only valid after the deadline, therefore the deadline has passed." The ledger enforces this — if someone tries to submit before the deadline, the transaction is rejected.
+
+## Step 4: Full Working Example
+
+Putting it all together — lock, wait, claim:
+
+```typescript
+import {
+ Address, Assets, Bytes, Data, InlineDatum, KeyHash,
+ TSchema, preprod, Client
+} from "@evolution-sdk/evolution"
+
+// --- Schema ---
+const VestingDatum = TSchema.Struct({
+ beneficiary: TSchema.ByteArray,
+ deadline: TSchema.Integer,
+})
+const VestingCodec = Data.withSchema(VestingDatum)
+
+// --- Client ---
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// --- Config ---
+const scriptAddress = Address.fromBech32("addr_test1wrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qnmqsyu")
+const beneficiaryHash = Bytes.fromHex(
+ "abc123def456abc123def456abc123def456abc123def456abc123de" // 28 bytes
+)
+const deadline = BigInt(new Date("2025-12-31T23:59:59Z").getTime())
+
+declare const vestingScript: any // compiled Plutus validator
+
+// === LOCK ===
+async function lockFunds(amount: bigint) {
+ const datum = VestingCodec.toData({
+ beneficiary: beneficiaryHash,
+ deadline,
+ })
+
+ const tx = await client
+ .newTx()
+ .payToAddress({
+ address: scriptAddress,
+ assets: Assets.fromLovelace(amount),
+ datum: new InlineDatum.InlineDatum({ data: datum }),
+ })
+ .build()
+
+ const signed = await tx.sign()
+ return signed.submit()
+}
+
+// === CLAIM (after deadline) ===
+async function claimFunds() {
+ const utxos = await client.getUtxos(scriptAddress)
+ const now = BigInt(Date.now())
+
+ if (now < deadline) {
+ throw new Error(`Deadline not reached. Wait until ${new Date(Number(deadline))}`)
+ }
+
+ const tx = await client
+ .newTx()
+ .collectFrom({
+ inputs: utxos,
+ redeemer: Data.constr(0n, []),
+ label: "claim-vesting",
+ })
+ .attachScript({ script: vestingScript })
+ .addSigner({ keyHash: new KeyHash.KeyHash({ hash: beneficiaryHash }) })
+ .setValidity({ from: now, to: now + 300_000n })
+ .build()
+
+ const signed = await tx.sign()
+ return signed.submit()
+}
+```
+
+## Common Pitfalls
+
+
+**Validity interval too early** — If `from` is before the deadline, the validator will reject. Always set `from` to a time after the deadline.
+
+
+| Problem | Cause | Fix |
+| --- | --- | --- |
+| "Script evaluation failed" | Validity `from` is before deadline | Set `from` to current time (must be > deadline) |
+| "Missing required signer" | Beneficiary didn't sign | Add `.addSigner({ keyHash })` matching the datum's beneficiary |
+| "Datum mismatch" | Datum schema doesn't match validator | Verify TSchema field order matches your Aiken/Plutarch type |
+| "UTxO already spent" | Funds already claimed | Query UTxOs first to check if they're still there |
+| "Outside validity interval" | Transaction submitted too late | Increase the `to` value or submit faster |
+
+## Next Steps
+
+- [Locking to Script](/docs/smart-contracts/locking) — More locking patterns
+- [Spending from Script](/docs/smart-contracts/spending) — Redeemer modes and debug labels
+- [Validity Ranges](/docs/time/validity-ranges) — Time constraint details
+- [TSchema](/docs/encoding/tschema) — Schema definition reference
+- [Native Scripts](/docs/smart-contracts/native-scripts) — Time-locks without Plutus
diff --git a/docs/content/docs/staking/delegation.mdx b/docs/content/docs/staking/delegation.mdx
index 0afb2d4f..3d2f3ca2 100644
--- a/docs/content/docs/staking/delegation.mdx
+++ b/docs/content/docs/staking/delegation.mdx
@@ -21,8 +21,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const poolKeyHash: any
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
+declare const poolKeyHash: any // pool key hash (from a stake pool explorer)
const tx = await client.newTx().delegateToPool({ stakeCredential, poolKeyHash }).build()
@@ -109,8 +109,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const poolKeyHash: any
+declare const stakeCredential: Credential.Credential // from (await client.address()).stakingCredential!
+declare const poolKeyHash: any // pool key hash (from a stake pool explorer)
declare const drepKeyHash: any
const tx = await client
diff --git a/docs/content/docs/staking/deregistration.mdx b/docs/content/docs/staking/deregistration.mdx
index b68812ab..1761b666 100644
--- a/docs/content/docs/staking/deregistration.mdx
+++ b/docs/content/docs/staking/deregistration.mdx
@@ -19,7 +19,9 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
+// Get your stake credential from your wallet address
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
const tx = await client.newTx().deregisterStake({ stakeCredential }).build()
@@ -41,8 +43,11 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-declare const rewardBalance: bigint
+// Query rewards first, then withdraw + deregister in one transaction
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
+const delegation = await client.getWalletDelegation()
+const rewardBalance = delegation.rewards // accumulated lovelace
const tx = await client
.newTx()
@@ -66,8 +71,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptStakeCredential: Credential.Credential
-declare const stakeScript: any
+declare const scriptStakeCredential: Credential.Credential // script-controlled stake credential
+declare const stakeScript: any // compiled Plutus staking script (from Aiken build or Blueprint codegen)
const tx = await client
.newTx()
diff --git a/docs/content/docs/staking/index.mdx b/docs/content/docs/staking/index.mdx
index c209f4c5..8b364a35 100644
--- a/docs/content/docs/staking/index.mdx
+++ b/docs/content/docs/staking/index.mdx
@@ -57,6 +57,9 @@ The Conway era introduced new delegation capabilities:
Register stake credentials on-chain
+
+ Pre-Conway registration, withdrawal scripts, and coordinator pattern
+
Delegate to pools and DReps
@@ -66,4 +69,7 @@ The Conway era introduced new delegation capabilities:
Remove credentials and reclaim deposit
+
+ Register and retire stake pools
+
diff --git a/docs/content/docs/staking/legacy-registration.mdx b/docs/content/docs/staking/legacy-registration.mdx
new file mode 100644
index 00000000..dd5bc5bd
--- /dev/null
+++ b/docs/content/docs/staking/legacy-registration.mdx
@@ -0,0 +1,352 @@
+---
+title: Legacy Stake Registration
+description: Register stake credentials using the pre-Conway certificate format (no deposit)
+---
+
+# Legacy Stake Registration
+
+The Conway era introduced new registration certificates (`RegCert`, CDDL tag 7) that require a 2 ADA deposit. However, the **legacy** certificates (`StakeRegistration`, CDDL tag 0) are still accepted on mainnet and are what most wallets and tools use today.
+
+Use legacy registration when:
+
+- You want to avoid the 2 ADA deposit
+- You need compatibility with pre-Conway tooling
+- You're building a wallet that supports older node versions
+
+## Legacy vs Conway Registration
+
+| | Legacy (pre-Conway) | Conway |
+| --- | --- | --- |
+| Certificate | `StakeRegistration` (tag 0) | `RegCert` (tag 7) |
+| Deposit | None | 2 ADA (from protocol params) |
+| Builder method | `registerStakeLegacy()` | `registerStake()` |
+| Deregistration | `deregisterStakeLegacy()` | `deregisterStake()` |
+| Combined register + delegate | Not available | `registerAndDelegateTo()` |
+
+
+Both formats are valid on mainnet today. The ledger accepts either. Choose legacy for simplicity or Conway for deposit-based guarantees.
+
+
+## Basic Legacy Registration
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
+
+// Legacy registration — no deposit required
+const tx = await client.newTx().registerStakeLegacy({ stakeCredential }).build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+Compare with Conway registration which fetches `keyDeposit` from protocol parameters:
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const stakeCredential: Credential.Credential
+
+// Conway registration — 2 ADA deposit deducted automatically
+const tx = await client.newTx().registerStake({ stakeCredential }).build()
+```
+
+## Register and Delegate in One Transaction
+
+Legacy registration doesn't have a combined certificate, but you can chain both operations in a single transaction:
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const stakeCredential: Credential.Credential
+declare const poolKeyHash: any // pool key hash from a stake pool explorer
+
+// Register (legacy) + delegate in one transaction
+const tx = await client
+ .newTx()
+ .registerStakeLegacy({ stakeCredential })
+ .delegateToPool({ stakeCredential, poolKeyHash })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## Script-Controlled Legacy Registration
+
+For stake credentials controlled by a Plutus script, provide a redeemer:
+
+```typescript twoslash
+import { Credential, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const scriptStakeCredential: Credential.Credential
+declare const stakeScript: any // compiled Plutus staking script
+
+const tx = await client
+ .newTx()
+ .registerStakeLegacy({
+ stakeCredential: scriptStakeCredential,
+ redeemer: Data.constr(0n, []),
+ label: "legacy-register-script-stake"
+ })
+ .attachScript({ script: stakeScript })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+## Withdrawal with a Stake Script
+
+Stake scripts are Plutus validators that run when you withdraw rewards. This is also the foundation of the **coordinator pattern** used by DeFi protocols — a zero-amount withdrawal triggers the stake validator, which can enforce global invariants across multiple script inputs.
+
+### Basic Script Withdrawal
+
+```typescript twoslash
+import { Credential, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const scriptStakeCredential: Credential.Credential
+declare const stakeScript: any // compiled Plutus staking script
+declare const rewardAmount: bigint
+
+// Withdraw rewards — the stake script validates this transaction
+const tx = await client
+ .newTx()
+ .withdraw({
+ stakeCredential: scriptStakeCredential,
+ amount: rewardAmount,
+ redeemer: Data.constr(0n, []),
+ label: "withdraw-rewards"
+ })
+ .attachScript({ script: stakeScript })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+### Coordinator Pattern (Zero-Amount Withdrawal)
+
+Use `amount: 0n` to trigger a stake validator without withdrawing rewards. The validator runs and can enforce rules across the entire transaction — checking that inputs, outputs, and certificates meet your protocol's invariants:
+
+```typescript twoslash
+import { Credential, Data, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const scriptStakeCredential: Credential.Credential
+declare const stakeScript: any
+
+// Zero withdrawal — triggers the validator without moving funds
+const tx = await client
+ .newTx()
+ .withdraw({
+ stakeCredential: scriptStakeCredential,
+ amount: 0n,
+ redeemer: Data.constr(0n, []),
+ label: "coordinator-trigger"
+ })
+ .attachScript({ script: stakeScript })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+### Batch Redeemer (Coordinated Spend + Withdraw)
+
+The most powerful pattern combines script UTxO spending with a withdrawal validator. The withdrawal redeemer receives the indices of all spent inputs, so the stake validator can verify each one:
+
+```typescript twoslash
+import { Credential, Data, preprod, Client, Cardano } from "@evolution-sdk/evolution"
+import * as Bytes from "@evolution-sdk/evolution/Bytes"
+import * as PlutusV3 from "@evolution-sdk/evolution/PlutusV3"
+import * as ScriptHash from "@evolution-sdk/evolution/ScriptHash"
+import * as InlineDatum from "@evolution-sdk/evolution/InlineDatum"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+// Your compiled multi-validator (spend + withdraw endpoints)
+declare const compiledCode: string
+const stakeScript = new PlutusV3.PlutusV3({ bytes: Bytes.fromHex(compiledCode) })
+const scriptHash = ScriptHash.fromScript(stakeScript)
+const scriptStakeCredential = scriptHash // ScriptHash is a valid Credential
+
+// Redeemer constructors matching your Aiken validator
+const makeSpendRedeemer = (inputIndex: bigint): Data.Data =>
+ Data.int(inputIndex)
+
+const makeWithdrawRedeemer = (inputIndices: Array): Data.Data =>
+ Data.constr(0n, [Data.list(inputIndices.map(Data.int))])
+
+// Fetch UTxOs locked at the script address
+declare const scriptUtxos: Cardano.UTxO.UTxO[]
+const utxosToSpend = scriptUtxos.slice(0, 2)
+
+// Build the coordinated transaction
+let txBuilder = client.newTx()
+
+// Collect from each script UTxO — self-referencing redeemer gets the final input index
+for (const utxo of utxosToSpend) {
+ txBuilder = txBuilder.collectFrom({
+ inputs: [utxo],
+ redeemer: (indexedInput) => makeSpendRedeemer(BigInt(indexedInput.index))
+ })
+}
+
+// Attach the script once (shared by spend + withdraw endpoints)
+txBuilder = txBuilder.attachScript({ script: stakeScript })
+
+// Zero withdrawal with batch redeemer — receives all input indices
+txBuilder = txBuilder.withdraw({
+ stakeCredential: scriptStakeCredential,
+ amount: 0n,
+ redeemer: {
+ all: (indexedInputs) =>
+ makeWithdrawRedeemer(indexedInputs.map((inp) => BigInt(inp.index))),
+ inputs: utxosToSpend
+ },
+ label: "coordinator-withdraw"
+})
+
+const tx = await txBuilder.build()
+const signed = await tx.sign()
+await signed.submit()
+```
+
+
+**Why batch redeemers?** Input indices aren't known until coin selection runs. The `all` callback receives the final sorted indices after the transaction is balanced — solving the chicken-and-egg problem.
+
+
+## Legacy Deregistration
+
+Deregister a legacy-registered stake credential. No deposit is refunded (since none was paid):
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
+
+// Withdraw rewards first, then deregister — in one transaction
+const delegation = await client.getWalletDelegation()
+
+const tx = await client
+ .newTx()
+ .withdraw({ stakeCredential, amount: delegation.rewards })
+ .deregisterStakeLegacy({ stakeCredential })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+
+Always withdraw rewards before deregistering. Rewards are lost after deregistration regardless of which certificate format you used.
+
+
+## Full Lifecycle Example
+
+Register, delegate, withdraw, and deregister — all using legacy certificates:
+
+```typescript twoslash
+import { Credential, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const poolKeyHash: any
+
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
+
+// 1. Register + delegate
+const regTx = await client
+ .newTx()
+ .registerStakeLegacy({ stakeCredential })
+ .delegateToPool({ stakeCredential, poolKeyHash })
+ .build()
+await (await regTx.sign()).submit()
+
+// ... epochs pass, rewards accumulate ...
+
+// 2. Withdraw rewards
+const delegation = await client.getWalletDelegation()
+const withdrawTx = await client
+ .newTx()
+ .withdraw({ stakeCredential, amount: delegation.rewards })
+ .build()
+await (await withdrawTx.sign()).submit()
+
+// 3. Deregister when done
+const deregTx = await client
+ .newTx()
+ .deregisterStakeLegacy({ stakeCredential })
+ .build()
+await (await deregTx.sign()).submit()
+```
+
+## Next Steps
+
+- [Registration (Conway)](/docs/staking/registration) — Conway-era registration with deposit
+- [Delegation](/docs/staking/delegation) — Pool and DRep delegation options
+- [Withdrawal](/docs/staking/withdrawal) — Reward withdrawal patterns
+- [Deregistration](/docs/staking/deregistration) — Conway-era deregistration with refund
diff --git a/docs/content/docs/staking/meta.json b/docs/content/docs/staking/meta.json
index 50774a2c..cee2a68c 100644
--- a/docs/content/docs/staking/meta.json
+++ b/docs/content/docs/staking/meta.json
@@ -2,8 +2,11 @@
"title": "Staking",
"pages": [
"index",
- "certificates",
- "pools",
- "rewards"
+ "registration",
+ "legacy-registration",
+ "delegation",
+ "withdrawal",
+ "deregistration",
+ "pools"
]
}
diff --git a/docs/content/docs/staking/pools.mdx b/docs/content/docs/staking/pools.mdx
new file mode 100644
index 00000000..b232cb01
--- /dev/null
+++ b/docs/content/docs/staking/pools.mdx
@@ -0,0 +1,153 @@
+---
+title: Stake Pool Operations
+description: Register and retire stake pools on Cardano
+---
+
+# Stake Pool Operations
+
+Stake pool operators use `registerPool` to register a new pool on-chain and `retirePool` to announce retirement. Both operations require the pool operator's key to sign the transaction.
+
+## Pool Registration
+
+Register a stake pool with full parameters — operator key, VRF key, pledge, cost, margin, relays, and optional metadata:
+
+```typescript
+import {
+ Coin,
+ KeyHash,
+ PoolKeyHash,
+ PoolMetadata,
+ PoolParams,
+ RewardAccount,
+ SingleHostAddr,
+ UnitInterval,
+ Url,
+ VrfKeyHash,
+ Bytes32,
+ IPv4,
+ preprod,
+ Client,
+} from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const operatorKeyHash: PoolKeyHash.PoolKeyHash // 28-byte pool operator key hash
+declare const vrfKeyHash: VrfKeyHash.VrfKeyHash // 32-byte VRF verification key hash
+declare const ownerKeyHash: KeyHash.KeyHash // 28-byte pool owner key hash
+declare const rewardAccount: RewardAccount.RewardAccount // pool's reward account for collecting fees
+
+const poolParams = new PoolParams.PoolParams({
+ operator: operatorKeyHash,
+ vrfKeyhash: vrfKeyHash,
+ pledge: 500_000_000n , // 500 ADA pledge
+ cost: 340_000_000n , // 340 ADA fixed cost per epoch
+ margin: new UnitInterval.UnitInterval({ numerator: 1n, denominator: 100n }), // 1% margin
+ rewardAccount,
+ poolOwners: [ownerKeyHash],
+ relays: [], // see Relay Configuration below
+ poolMetadata: null // see Pool Metadata below
+})
+
+const tx = await client
+ .newTx()
+ .registerPool({ poolParams })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+### Pool Parameters Reference
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| `operator` | `PoolKeyHash` | Pool operator's key hash (28 bytes) |
+| `vrfKeyhash` | `VrfKeyHash` | VRF verification key hash (32 bytes) |
+| `pledge` | `Coin` | Operator's pledge in lovelace |
+| `cost` | `Coin` | Fixed cost per epoch in lovelace (min 340 ADA on mainnet) |
+| `margin` | `UnitInterval` | Pool margin as a fraction (numerator/denominator) |
+| `rewardAccount` | `RewardAccount` | Where pool fees are collected |
+| `poolOwners` | `KeyHash[]` | Key hashes of all pool owners |
+| `relays` | `Relay[]` | Network relay endpoints (see below) |
+| `poolMetadata` | `PoolMetadata?` | Optional metadata URL + hash |
+
+## Relay Configuration
+
+Relays tell the network how to reach your pool. Use `SingleHostAddr` for IP-based relays:
+
+```typescript
+import { IPv4, SingleHostAddr } from "@evolution-sdk/evolution"
+
+// IP + port relay
+const relay = new SingleHostAddr.SingleHostAddr({
+ port: 3001n,
+ ipv4: new IPv4.IPv4({ inner: new Uint8Array([192, 168, 1, 100]) }),
+ ipv6: undefined,
+})
+```
+
+Pass relays in the `relays` array of `PoolParams`.
+
+## Pool Metadata
+
+Link to off-chain metadata (pool name, description, ticker) hosted at a URL:
+
+```typescript
+import { Bytes32, PoolMetadata, Url } from "@evolution-sdk/evolution"
+
+declare const metadataHash: Bytes32.Bytes32 // SHA-256 hash of metadata JSON file
+
+const metadata = new PoolMetadata.PoolMetadata({
+ url: new Url.Url({ url: "https://my-pool.com/metadata.json" }),
+ hash: metadataHash,
+})
+```
+
+The metadata JSON file must follow the [CIP-6 standard](https://cips.cardano.org/cip/CIP-0006) with fields like `name`, `description`, `ticker`, and `homepage`.
+
+## Pool Retirement
+
+Announce retirement effective at a future epoch:
+
+```typescript
+import { PoolKeyHash, preprod, Client } from "@evolution-sdk/evolution"
+import type { EpochNo } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const poolKeyHash: PoolKeyHash.PoolKeyHash // the pool to retire
+declare const retirementEpoch: EpochNo.EpochNo // epoch when retirement takes effect
+
+const tx = await client
+ .newTx()
+ .retirePool({
+ poolKeyHash,
+ epoch: retirementEpoch,
+ })
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+Retirement takes effect at the specified epoch. The pool deposit (500 ADA on mainnet) is refunded to the pool's reward account after retirement.
+
+## Update Pool Parameters
+
+To update an existing pool's parameters (cost, margin, relays, metadata), submit a new `registerPool` with the updated `PoolParams`. The chain treats this as an update if the operator key hash matches an existing pool.
+
+## Next Steps
+
+- [Stake Registration](/docs/staking/registration) — Register stake credentials before delegating
+- [Delegation](/docs/staking/delegation) — Delegate stake to a pool
+- [Governance](/docs/governance) — Participate in on-chain governance
diff --git a/docs/content/docs/staking/registration.mdx b/docs/content/docs/staking/registration.mdx
index 1d04c8ab..7f7b7765 100644
--- a/docs/content/docs/staking/registration.mdx
+++ b/docs/content/docs/staking/registration.mdx
@@ -19,7 +19,10 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
+// Get your stake credential from your wallet address
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
+// stakeCredential is a Credential (KeyHash for seed wallets, ScriptHash for script-controlled)
const tx = await client.newTx().registerStake({ stakeCredential }).build()
@@ -27,7 +30,7 @@ const signed = await tx.sign()
await signed.submit()
```
-The deposit amount is fetched automatically from protocol parameters.
+The deposit amount (currently 2 ADA) is fetched automatically from protocol parameters.
## Register and Delegate Together
@@ -44,8 +47,8 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const stakeCredential: Credential.Credential
-declare const poolKeyHash: any
-declare const drep: any
+declare const poolKeyHash: any // pool key hash from a stake pool (e.g. from a pool explorer)
+declare const drep: any // DRep to delegate voting power to (e.g. DRep.fromKeyHash(...))
// Register + delegate to pool in one certificate
const tx = await client
@@ -73,8 +76,8 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const stakeCredential: Credential.Credential
-declare const poolKeyHash: any
-declare const drep: any
+declare const poolKeyHash: any // pool key hash from a stake pool (e.g. from a pool explorer)
+declare const drep: any // DRep to delegate voting power to (e.g. DRep.fromKeyHash(...))
// Register + delegate to both pool and DRep
const tx = await client
@@ -105,7 +108,7 @@ const client = Client.make(preprod)
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
declare const scriptStakeCredential: Credential.Credential
-declare const stakeScript: any
+declare const stakeScript: any // compiled Plutus staking script (from Aiken build or Blueprint codegen)
const tx = await client
.newTx()
diff --git a/docs/content/docs/staking/withdrawal.mdx b/docs/content/docs/staking/withdrawal.mdx
index 1650aad0..8e3b9ef2 100644
--- a/docs/content/docs/staking/withdrawal.mdx
+++ b/docs/content/docs/staking/withdrawal.mdx
@@ -19,9 +19,9 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const stakeCredential: Credential.Credential
-
-// Query current rewards via wallet
+// Get your stake credential and query rewards
+const address = await client.address()
+const stakeCredential = address.stakingCredential!
const delegation = await client.getWalletDelegation()
console.log("Available rewards:", delegation.rewards, "lovelace")
@@ -52,8 +52,8 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptStakeCredential: Credential.Credential
-declare const stakeScript: any
+declare const scriptStakeCredential: Credential.Credential // script-controlled stake credential
+declare const stakeScript: any // compiled Plutus staking script (from Aiken build or Blueprint codegen)
// Trigger the stake validator with zero withdrawal
const tx = await client
@@ -85,9 +85,9 @@ const client = Client.make(preprod)
})
.withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
-declare const scriptStakeCredential: Credential.Credential
-declare const stakeScript: any
-declare const rewardAmount: bigint
+declare const scriptStakeCredential: Credential.Credential // script-controlled stake credential
+declare const stakeScript: any // compiled Plutus staking script (from Aiken build or Blueprint codegen)
+declare const rewardAmount: bigint // from client.getWalletDelegation().rewards
const tx = await client
.newTx()
diff --git a/docs/content/docs/testing/integration-tests.mdx b/docs/content/docs/testing/integration-tests.mdx
index a4833246..be43e1d0 100644
--- a/docs/content/docs/testing/integration-tests.mdx
+++ b/docs/content/docs/testing/integration-tests.mdx
@@ -9,7 +9,7 @@ Integration tests run against a local devnet cluster, validating the full transa
## Test Setup Pattern
-```typescript
+```typescript title="test/integration.test.ts"
import {
describe,
it,
@@ -23,13 +23,12 @@ import {
Address,
Assets,
preprod,
- type SigningClient,
Client,
} from "@evolution-sdk/evolution"
describe("Transaction Tests", () => {
let cluster: Cluster.Cluster
- let client: SigningClient
+ let client: Client.SigningClient
let genesisConfig: any
beforeAll(async () => {
diff --git a/docs/content/docs/testing/meta.json b/docs/content/docs/testing/meta.json
index de7a8a2c..6d0749ce 100644
--- a/docs/content/docs/testing/meta.json
+++ b/docs/content/docs/testing/meta.json
@@ -2,7 +2,8 @@
"title": "Testing",
"pages": [
"index",
- "emulator",
- "mocking"
+ "unit-tests",
+ "integration-tests",
+ "emulator"
]
}
diff --git a/docs/content/docs/testing/unit-tests.mdx b/docs/content/docs/testing/unit-tests.mdx
index 66a3d019..8fd4e08e 100644
--- a/docs/content/docs/testing/unit-tests.mdx
+++ b/docs/content/docs/testing/unit-tests.mdx
@@ -9,7 +9,7 @@ Unit tests validate individual modules — schema encoding/decoding, address par
## Testing Schema Round-Trips
-```typescript twoslash
+```typescript twoslash title="test/schema.test.ts"
import { describe, it, expect } from "vitest"
import { Bytes, Data, TSchema } from "@evolution-sdk/evolution"
diff --git a/docs/content/docs/time/slots.mdx b/docs/content/docs/time/slots.mdx
index ab687606..98d20420 100644
--- a/docs/content/docs/time/slots.mdx
+++ b/docs/content/docs/time/slots.mdx
@@ -55,6 +55,36 @@ const tx = await client
})
```
+## Manual Slot Conversion
+
+The builder handles slot conversion automatically, but you can also convert manually using `Time.unixTimeToSlot` and `Time.slotToUnixTime`:
+
+```typescript
+import { Time, SlotConfig } from "@evolution-sdk/evolution"
+
+// Use built-in network configs
+const preprodConfig = SlotConfig.SLOT_CONFIG_NETWORK.Preprod
+const mainnetConfig = SlotConfig.SLOT_CONFIG_NETWORK.Mainnet
+
+// Unix time → slot
+const now = BigInt(Date.now())
+const currentSlot = Time.unixTimeToSlot(now, preprodConfig)
+console.log("Current slot:", currentSlot)
+
+// Slot → Unix time
+const targetSlot = 50000000n
+const targetTime = Time.slotToUnixTime(targetSlot, preprodConfig)
+console.log("Slot 50M is at:", new Date(Number(targetTime)))
+```
+
+### Available Network Configs
+
+| Network | `SlotConfig.SLOT_CONFIG_NETWORK.X` | Zero Time | Zero Slot | Slot Length |
+| --- | --- | --- | --- | --- |
+| Mainnet | `.Mainnet` | 1596059091000 (Shelley start) | 4492800 | 1000ms |
+| Preprod | `.Preprod` | 1655769600000 | 0 | 1000ms |
+| Preview | `.Preview` | 1666656000000 | 0 | 1000ms |
+
## Next Steps
- [POSIX Time](/docs/time/posix) — Unix timestamp utilities
diff --git a/docs/content/docs/transactions/airdrop.mdx b/docs/content/docs/transactions/airdrop.mdx
new file mode 100644
index 00000000..eb8441ab
--- /dev/null
+++ b/docs/content/docs/transactions/airdrop.mdx
@@ -0,0 +1,209 @@
+---
+title: "Tutorial: Token Airdrop"
+description: Distribute tokens to many recipients using batched transactions and chaining
+---
+
+# Tutorial: Token Airdrop
+
+Distribute tokens to dozens or hundreds of recipients efficiently. This tutorial covers batching payments to stay within transaction size limits and chaining batches for sequential submission.
+
+## What You'll Build
+
+- A **batch payment function** that sends to multiple recipients in one transaction
+- A **chunking strategy** to split large recipient lists into transaction-sized batches
+- **Transaction chaining** so batches submit sequentially without waiting for confirmations
+
+## Prerequisites
+
+- A Blockfrost API key ([get one free](https://blockfrost.io))
+- Tokens already in your wallet (or see [Minting Tokens](/docs/smart-contracts/minting) to create them)
+- Familiarity with [multi-output transactions](/docs/transactions/multi-output) and [transaction chaining](/docs/transactions/chaining)
+
+## Step 1: Define the Recipient List
+
+```typescript
+import { Address, Assets } from "@evolution-sdk/evolution"
+
+interface Recipient {
+ address: Address.Address
+ lovelace: bigint
+}
+
+// Your airdrop recipients
+const recipients: Recipient[] = [
+ { address: Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63"), lovelace: 5_000_000n },
+ { address: Address.fromBech32("addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3n0d3vllmyqwsx5wktcd8cc3sq835lu7drv2xwl2wywfgs68faae"), lovelace: 10_000_000n },
+ // ... potentially hundreds more
+]
+```
+
+## Step 2: Chunk Into Batches
+
+A single Cardano transaction has a max size (~16KB). Each output adds ~60-100 bytes, so you can fit roughly **20-30 recipients per transaction**. Chunk the list:
+
+```typescript
+function chunk(array: T[], size: number): T[][] {
+ const chunks: T[][] = []
+ for (let i = 0; i < array.length; i += size) {
+ chunks.push(array.slice(i, i + size))
+ }
+ return chunks
+}
+
+const BATCH_SIZE = 25 // conservative — adjust based on output size
+const batches = chunk(recipients, BATCH_SIZE)
+// e.g., 100 recipients → 4 batches of 25
+```
+
+
+**Why 25?** Each output needs ~80 bytes for address + amount. With overhead, 25 outputs keeps you well under the 16KB limit. If outputs include native tokens, reduce the batch size (tokens add ~50-100 bytes each).
+
+
+## Step 3: Build and Submit Batches
+
+### Simple Approach: Sequential Submission
+
+Wait for each batch to confirm before submitting the next:
+
+```typescript
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const batches: Recipient[][]
+
+async function submitBatches() {
+ for (let i = 0; i < batches.length; i++) {
+ const batch = batches[i]
+
+ // Build multi-output transaction
+ let builder = client.newTx()
+ for (const recipient of batch) {
+ builder = builder.payToAddress({
+ address: recipient.address,
+ assets: Assets.fromLovelace(recipient.lovelace),
+ })
+ }
+
+ const tx = await builder.build()
+ const signed = await tx.sign()
+ const txHash = await signed.submit()
+
+ console.log(`Batch ${i + 1}/${batches.length} submitted:`, txHash)
+
+ // Wait for confirmation before next batch
+ await client.awaitTx(txHash, 3000)
+ console.log(`Batch ${i + 1} confirmed`)
+ }
+}
+```
+
+### Fast Approach: Transaction Chaining
+
+Don't wait for confirmations — chain batches using `chainResult()`:
+
+```typescript
+import { Address, Assets, TransactionHash, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!,
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const batches: Recipient[][]
+
+async function chainedAirdrop() {
+ const txHashes: TransactionHash.TransactionHash[] = []
+
+ // Get initial UTxOs
+ let availableUtxos = await client.getWalletUtxos()
+
+ for (let i = 0; i < batches.length; i++) {
+ const batch = batches[i]
+
+ let builder = client.newTx()
+ for (const recipient of batch) {
+ builder = builder.payToAddress({
+ address: recipient.address,
+ assets: Assets.fromLovelace(recipient.lovelace),
+ })
+ }
+
+ // Build with available UTxOs from previous batch
+ const signBuilder = await builder.build({ availableUtxos })
+ const signed = await signBuilder.sign()
+ const txHash = await signed.submit()
+
+ txHashes.push(txHash)
+ console.log(`Batch ${i + 1}/${batches.length} submitted:`, txHash)
+
+ // Chain: use remaining UTxOs + new outputs for next batch
+ const chainResult = signBuilder.chainResult()
+ availableUtxos = [...chainResult.available]
+ }
+
+ // Wait for last batch to confirm (all prior batches will be confirmed too)
+ const lastHash = txHashes[txHashes.length - 1]
+ await client.awaitTx(lastHash, 3000)
+ console.log("All batches confirmed!")
+
+ return txHashes
+}
+```
+
+
+**Submit in order.** Each chained transaction depends on outputs from the previous one. If batch 2 arrives at the node before batch 1, it gets rejected. The sequential `for` loop ensures correct ordering.
+
+
+## Step 4: Airdrop with Native Tokens
+
+Distribute tokens (not just ADA) — each recipient gets tokens + min ADA:
+
+```typescript
+import { Address, Assets } from "@evolution-sdk/evolution"
+
+interface TokenRecipient {
+ address: Address.Address
+ lovelace: bigint
+ policyId: string // 56 hex chars
+ assetName: string // hex-encoded token name
+ quantity: bigint
+}
+
+function buildTokenOutput(recipient: TokenRecipient): { address: Address.Address; assets: Assets.Assets } {
+ let assets = Assets.fromLovelace(recipient.lovelace) // min ADA for UTxO
+ assets = Assets.addByHex(assets, recipient.policyId, recipient.assetName, recipient.quantity)
+ return { address: recipient.address, assets }
+}
+
+// Use in the batch loop:
+// builder = builder.payToAddress(buildTokenOutput(recipient))
+```
+
+
+When distributing native tokens, each output needs **more ADA** for the min UTxO requirement (tokens increase UTxO size). Use at least 2 ADA per output with tokens. The builder calculates the exact minimum automatically.
+
+
+## Common Pitfalls
+
+| Problem | Cause | Fix |
+| --- | --- | --- |
+| "Transaction too large" | Too many outputs per batch | Reduce `BATCH_SIZE` (try 15-20) |
+| "Insufficient funds" | Not enough ADA for all outputs + fees | Ensure wallet has total amount + fees for all batches |
+| Batch 2 rejected | Submitted before batch 1 | Use sequential loop, not parallel |
+| "Min UTxO not met" | Output has too little ADA | Increase lovelace per output (2+ ADA with tokens) |
+| Chaining fails | availableUtxos not updated | Pass `chainResult.available` to next build |
+
+## Next Steps
+
+- [Multi-Output Transactions](/docs/transactions/multi-output) — Basic multi-recipient patterns
+- [Transaction Chaining](/docs/transactions/chaining) — How chainResult works
+- [Minting Tokens](/docs/smart-contracts/minting) — Mint tokens before distributing
+- [Tutorial: Mint an NFT](/docs/smart-contracts/mint-nft) — Mint + send in one transaction
diff --git a/docs/content/docs/transactions/chaining.mdx b/docs/content/docs/transactions/chaining.mdx
index 685cdee2..915e7366 100644
--- a/docs/content/docs/transactions/chaining.mdx
+++ b/docs/content/docs/transactions/chaining.mdx
@@ -177,6 +177,50 @@ for (const tx of [tx1, tx2, tx3]) {
- **`chainResult()` is memoized.** It's computed once from the build result and cached. Calling it multiple times is free but you always get the same snapshot.
- **The outputs in `available` are not yet on-chain.** They exist only as pre-computed UTxOs. Don't pass them to any provider call (e.g. `getUtxos`) — they won't be there yet.
+## Composing Builders
+
+Use `.compose()` to merge multiple builder configurations into a single transaction. This enables reusable, modular transaction patterns:
+
+```typescript
+import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(preprod)
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+
+declare const recipientAddress: Address.Address
+
+// Define reusable builder fragments
+const paymentBuilder = client.newTx().payToAddress({
+ address: recipientAddress,
+ assets: Assets.fromLovelace(5_000_000n)
+})
+
+const validityBuilder = client.newTx().setValidity({
+ to: BigInt(Date.now()) + 300_000n // 5 minutes
+})
+
+// Compose them into one transaction
+const tx = await client
+ .newTx()
+ .compose(paymentBuilder)
+ .compose(validityBuilder)
+ .build()
+
+const signed = await tx.sign()
+await signed.submit()
+```
+
+Each `.compose(other)` copies the other builder's queued operations into the current builder. The composed builder is captured at compose-time — later changes to the source builder don't affect the composed result.
+
+**Use cases:**
+- Reusable payment templates shared across different flows
+- Separating concerns: one builder for payments, another for validity, another for metadata
+- Testing: compose mock operations in test setups
+
## Next Steps
diff --git a/docs/content/docs/transactions/first-transaction.mdx b/docs/content/docs/transactions/first-transaction.mdx
index 5b882883..9f0b0ec1 100644
--- a/docs/content/docs/transactions/first-transaction.mdx
+++ b/docs/content/docs/transactions/first-transaction.mdx
@@ -72,9 +72,13 @@ Chain operations to specify what the transaction should do. Call `.build()` when
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
+// ---cut---
const tx = await client
.newTx()
.payToAddress({
@@ -94,8 +98,11 @@ Authorize the transaction with your wallet's private keys:
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
const tx = await client
.newTx()
@@ -105,6 +112,7 @@ const tx = await client
})
.build()
+// ---cut---
const signed = await tx.sign()
```
@@ -116,8 +124,11 @@ Broadcast the signed transaction to the blockchain and get the transaction hash:
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+ })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
const tx = await client
.newTx()
@@ -129,6 +140,7 @@ const tx = await client
const signed = await tx.sign()
+// ---cut---
const txHash = await signed.submit()
console.log("Transaction hash:", txHash)
```
diff --git a/docs/content/docs/transactions/meta.json b/docs/content/docs/transactions/meta.json
index fa6385d4..becf8cc7 100644
--- a/docs/content/docs/transactions/meta.json
+++ b/docs/content/docs/transactions/meta.json
@@ -6,6 +6,7 @@
"simple-payment",
"multi-output",
"chaining",
- "retry-safe"
+ "retry-safe",
+ "airdrop"
]
}
diff --git a/docs/content/docs/transactions/simple-payment.mdx b/docs/content/docs/transactions/simple-payment.mdx
index b15870b9..92928840 100644
--- a/docs/content/docs/transactions/simple-payment.mdx
+++ b/docs/content/docs/transactions/simple-payment.mdx
@@ -75,7 +75,7 @@ const client = Client.make(preprod)
})
// Send 2 ADA plus 100 tokens
-const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1b0c9d8e7f6"
+const policyId = "7edb7a2d9fbc4d2a68e4c9e9d3d7a5c8f2d1e9f8a7b6c5d4e3f2a1"
const assetName = "" // empty for fungible tokens
let assets = Assets.fromLovelace(2_000_000n)
assets = Assets.addByHex(assets, policyId, assetName, 100n)
@@ -104,8 +104,8 @@ Make your code more readable with descriptive variable names:
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
// ---cut---
const recipientAddress = Address.fromBech32("addr_test1vrm9x2dgvdau8vckj4duc89m638t8djmluqw5pdrFollw8qd9k63")
@@ -128,8 +128,8 @@ Adjust transaction values based on network or configuration:
import { Address, Assets, preprod, Client } from "@evolution-sdk/evolution"
const client = Client.make(preprod)
- .withBlockfrost({ baseUrl: "", projectId: "" })
- .withSeed({ mnemonic: "", accountIndex: 0 })
+ .withBlockfrost({ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0", projectId: process.env.BLOCKFROST_API_KEY! })
+ .withSeed({ mnemonic: process.env.WALLET_MNEMONIC!, accountIndex: 0 })
// ---cut---
const isMainnet = process.env.NETWORK === "mainnet"
diff --git a/docs/content/docs/wallets/message-signing.mdx b/docs/content/docs/wallets/message-signing.mdx
new file mode 100644
index 00000000..be125601
--- /dev/null
+++ b/docs/content/docs/wallets/message-signing.mdx
@@ -0,0 +1,172 @@
+---
+title: Message Signing
+description: Sign and verify messages using CIP-30 COSE signatures
+---
+
+# Message Signing
+
+Evolution SDK implements the CIP-30 message signing standard using COSE (CBOR Object Signing and Encryption). This allows wallets to sign arbitrary data and prove ownership of an address without submitting a transaction.
+
+Common use cases:
+- **Authentication** — Prove you control an address (login with wallet)
+- **Data attestation** — Sign off-chain data with your key
+- **Message verification** — Verify a signature came from a specific address
+
+## Sign a Message
+
+Use `COSE.SignData.signData` to sign arbitrary data with a private key:
+
+```typescript
+import { COSE, PrivateKey, Address } from "@evolution-sdk/evolution"
+
+declare const privateKey: PrivateKey.PrivateKey
+declare const myAddress: Address.Address
+
+// Create a payload from text
+const payload = COSE.Utils.fromText("Hello, I'm signing this message!")
+
+// Sign with your private key
+const signedMessage = COSE.SignData.signData(
+ Address.toHex(myAddress), // Address as hex
+ payload, // Payload bytes
+ privateKey // Your private key
+)
+
+// signedMessage contains:
+// - signature: Uint8Array (CBOR-encoded COSE_Sign1)
+// - key: Uint8Array (CBOR-encoded COSE_Key with public key)
+```
+
+## Verify a Signature
+
+Use `COSE.SignData.verifyData` to verify a signed message:
+
+```typescript
+import { COSE, Address, KeyHash } from "@evolution-sdk/evolution"
+
+declare const myAddress: Address.Address
+declare const signerKeyHash: KeyHash.KeyHash
+declare const signedMessage: COSE.SignData.SignedMessage
+
+// The original payload that was signed
+const payload = COSE.Utils.fromText("Hello, I'm signing this message!")
+
+// Verify the signature
+const isValid = COSE.SignData.verifyData(
+ Address.toHex(myAddress), // Expected signer address
+ KeyHash.toHex(signerKeyHash), // Expected signer key hash
+ payload, // Original payload
+ signedMessage // The signed message to verify
+)
+
+if (isValid) {
+ console.log("Signature is valid!")
+}
+```
+
+Verification checks:
+1. Payload matches the signed data
+2. Address in the signature matches the expected address
+3. Algorithm is EdDSA
+4. Public key hash matches the expected key hash
+5. Ed25519 signature is cryptographically valid
+
+## Payload Utilities
+
+The `COSE.Utils` module provides helpers for creating payloads:
+
+```typescript
+import { COSE } from "@evolution-sdk/evolution"
+
+// From text string
+const textPayload = COSE.Utils.fromText("Sign this message")
+
+// Back to text
+const text = COSE.Utils.toText(textPayload)
+
+// From hex string
+const hexPayload = COSE.Utils.fromHex("deadbeef")
+
+// Back to hex
+const hex = COSE.Utils.toHex(hexPayload)
+```
+
+## Low-Level COSE API
+
+For advanced use cases, you can work with COSE structures directly.
+
+### COSE_Sign1
+
+The single-signer signature structure:
+
+```typescript
+import { COSE } from "@evolution-sdk/evolution"
+
+// Decode a COSE_Sign1 from CBOR bytes
+declare const cborBytes: Uint8Array
+const coseSign1 = COSE.Sign1.coseSign1FromCBORBytes(cborBytes)
+
+// Access fields
+const payload = coseSign1.payload // Signed payload
+const signature = coseSign1.signature // Ed25519 signature
+const headers = coseSign1.headers // Protected + unprotected headers
+
+// Encode back to CBOR
+const encoded = COSE.Sign1.coseSign1ToCBORBytes(coseSign1)
+```
+
+### COSE_Key
+
+Represents a public key in COSE format:
+
+```typescript
+import { COSE, PrivateKey, VKey } from "@evolution-sdk/evolution"
+import { Schema } from "effect"
+
+declare const privateKey: PrivateKey.PrivateKey
+
+// Build a COSE_Key from a private key
+const vkey = VKey.fromPrivateKey(privateKey)
+const ed25519Key = new COSE.Key.EdDSA25519Key(
+ { privateKey: undefined, publicKey: vkey },
+ { disableValidation: true }
+)
+const coseKey = ed25519Key.build()
+
+// Encode to CBOR
+const keyBytes = Schema.encodeSync(COSE.Key.COSEKeyFromCBORBytes())(coseKey)
+```
+
+### Headers
+
+COSE headers carry metadata about the signature:
+
+```typescript
+import { COSE } from "@evolution-sdk/evolution"
+
+// Create headers
+const protectedHeaders = COSE.Header.headerMapNew()
+ .setAlgorithmId(COSE.Label.AlgorithmId.EdDSA)
+
+const unprotectedHeaders = COSE.Header.headerMapNew()
+
+const headers = COSE.Header.headersNew(protectedHeaders, unprotectedHeaders)
+```
+
+## How It Works
+
+The CIP-30 signing process follows this flow:
+
+1. **Protected headers** are created with the algorithm (EdDSA) and signer's address
+2. **Unprotected headers** mark the payload as not pre-hashed
+3. A **COSE_Sign1 builder** creates the `Sig_structure1` (the data to be signed)
+4. The private key signs the `Sig_structure1` with Ed25519
+5. The result is CBOR-encoded as a `COSE_Sign1` + `COSE_Key` pair
+
+This matches the `api.signData()` specification from CIP-30, making it compatible with all CIP-30 compliant wallets.
+
+## Next Steps
+
+- [Wallets](/docs/wallets) — Wallet types and setup
+- [Private Key](/docs/wallets/private-key) — Working with private keys
+- [API Wallet](/docs/wallets/api-wallet) — CIP-30 browser wallet integration
diff --git a/docs/content/docs/wallets/meta.json b/docs/content/docs/wallets/meta.json
index c7fa2c92..47410e4a 100644
--- a/docs/content/docs/wallets/meta.json
+++ b/docs/content/docs/wallets/meta.json
@@ -5,6 +5,7 @@
"seed-phrase",
"private-key",
"api-wallet",
- "security"
+ "security",
+ "message-signing"
]
}
diff --git a/docs/content/docs/wallets/patterns.mdx b/docs/content/docs/wallets/patterns.mdx
deleted file mode 100644
index 639f29c9..00000000
--- a/docs/content/docs/wallets/patterns.mdx
+++ /dev/null
@@ -1,10 +0,0 @@
----
-title: "Patterns"
-description: "Frontend/backend architecture and read-only wallets"
----
-
-# Wallet Patterns
-
-This page has moved to the clients section: see **[Client architecture & patterns](/docs/clients/architecture)** for full frontend/backend patterns, read-only wallet examples, and client wiring examples.
-
-Wallet pages should focus on wallet types and configuration only. For concrete examples that combine a wallet and a provider to make a client (and submit transactions), see the clients documentation.
diff --git a/docs/content/docs/wallets/private-key.mdx b/docs/content/docs/wallets/private-key.mdx
index cbf81d10..a84b1c65 100644
--- a/docs/content/docs/wallets/private-key.mdx
+++ b/docs/content/docs/wallets/private-key.mdx
@@ -25,7 +25,9 @@ Eliminate derivation overhead, integrate with hardware security modules (HSMs) a
## How to Secure
-**Critical**: You manage raw cryptographic material with no backup mechanism.
+
+You manage raw cryptographic material with **no backup mechanism**. If you lose the key, the funds are gone.
+
Requirements for production:
@@ -123,6 +125,8 @@ Log all key access events without logging the keys themselves. Alert on unusual
## AWS Secrets Manager Example
+Uses [`@aws-sdk/client-secrets-manager`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/secrets-manager/) — see the [official quickstart](https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets-javascript.html).
+
```typescript twoslash
import { mainnet, Client } from "@evolution-sdk/evolution"
@@ -157,6 +161,8 @@ async function createProductionClient() {
## Azure Key Vault Example
+Uses [`@azure/keyvault-secrets`](https://learn.microsoft.com/en-us/javascript/api/@azure/keyvault-secrets/secretclient) + [`@azure/identity`](https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-node) — see the [official quickstart](https://learn.microsoft.com/en-us/azure/key-vault/secrets/quick-create-node).
+
```typescript twoslash
import { mainnet, Client } from "@evolution-sdk/evolution"
diff --git a/docs/content/docs/wallets/security.mdx b/docs/content/docs/wallets/security.mdx
index d51d52a3..bbd3c6f5 100644
--- a/docs/content/docs/wallets/security.mdx
+++ b/docs/content/docs/wallets/security.mdx
@@ -11,7 +11,11 @@ Complete security guide covering essential rules, environment setup, common mist
### Never Do
-Never hardcode private keys or mnemonics in your source code. This exposes them to anyone with access to your repository. Never commit keys to version control, even in environment files. Never log secrets to console or logging systems. Never expose keys in frontend code where they can be bundled into client-side JavaScript. Never share wallet instances across user requests, as this creates a shared wallet that all users would access.
+
+**Never** hardcode private keys or mnemonics in source code. **Never** commit keys to version control. **Never** log secrets. **Never** expose keys in frontend bundles. **Never** share wallet instances across user requests.
+
+
+These practices expose credentials to anyone with repository access, leak secrets to logging systems, bundle keys into client-side JavaScript, or create shared wallets that all users access.
```typescript
// Error: Hardcoded credentials exposed in source
@@ -343,7 +347,7 @@ Use private key wallets for automated operations. Store keys in vault solutions
## Next Steps
-- **[Patterns](/docs/wallets/patterns)** - Frontend/backend architecture
+- **[Client Architecture](/docs/clients/architecture)** - Frontend/backend patterns
- **[API Wallets](/docs/wallets/api-wallet)** - CIP-30 integration
- **[Private Key](/docs/wallets/private-key)** - Vault integration
- **[Seed Phrase](/docs/wallets/seed-phrase)** - Development wallets
diff --git a/docs/content/docs/wallets/seed-phrase.mdx b/docs/content/docs/wallets/seed-phrase.mdx
index 8f094896..c252d198 100644
--- a/docs/content/docs/wallets/seed-phrase.mdx
+++ b/docs/content/docs/wallets/seed-phrase.mdx
@@ -24,7 +24,9 @@ Mnemonics provide human-friendly backup and recovery. The same 24 words reconstr
## How to Secure
-**Never use in production with real funds.** Anyone with the phrase controls the funds.
+
+**Never use seed phrase wallets in production with real funds.** Anyone with the mnemonic controls all derived keys and funds. Use [Private Key Wallets](/docs/wallets/private-key) with a vault or [API Wallets](/docs/wallets/api-wallet) for production.
+
- Store in environment variables
- Use distinct mnemonics per environment (dev/staging/prod)
diff --git a/docs/mdx-components.tsx b/docs/mdx-components.tsx
index e145e508..e523448c 100644
--- a/docs/mdx-components.tsx
+++ b/docs/mdx-components.tsx
@@ -1,4 +1,5 @@
import defaultMdxComponents from "fumadocs-ui/mdx"
+import * as TabsComponents from "fumadocs-ui/components/tabs"
import * as Twoslash from "fumadocs-twoslash/ui"
import { Mermaid } from "@/components/mdx/mermaid"
import type { MDXComponents } from "mdx/types"
@@ -7,6 +8,7 @@ import type { MDXComponents } from "mdx/types"
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
+ ...TabsComponents,
...Twoslash,
Mermaid,
...components
diff --git a/packages/evolution/docs/modules/Address.ts.md b/packages/evolution/docs/modules/Address.ts.md
index 4cab5f50..5a1b3929 100644
--- a/packages/evolution/docs/modules/Address.ts.md
+++ b/packages/evolution/docs/modules/Address.ts.md
@@ -16,6 +16,7 @@ Added in v2.0.0
- [arbitrary](#arbitrary-1)
- [Functions](#functions)
- [fromBech32](#frombech32)
+ - [fromSeed](#fromseed)
- [Model](#model)
- [AddressDetails (interface)](#addressdetails-interface)
- [Schema](#schema)
@@ -75,6 +76,39 @@ export declare const fromBech32: (i: string, overrideOptions?: ParseOptions) =>
Added in v2.0.0
+## fromSeed
+
+Derive an address from a BIP-39 seed phrase.
+
+Pure, synchronous key derivation — no network access or running cluster required.
+Useful for generating addresses before a devnet cluster starts (e.g. for genesis funding).
+
+**Signature**
+
+```ts
+export declare const fromSeed: (
+ seed: string,
+ options?: { password?: string; addressType?: "Base" | "Enterprise"; accountIndex?: number; networkId?: number }
+) => Address
+```
+
+**Example**
+
+```typescript
+import * as Address from "@evolution-sdk/evolution/Address"
+
+const address = Address.fromSeed(
+ "test test test test test test test test test test test test test test test test test test test test test test test sauce",
+ {
+ accountIndex: 0,
+ networkId: 0 // 0 = testnet, 1 = mainnet
+ }
+)
+const hex = Address.toHex(address)
+```
+
+Added in v2.1.0
+
# Model
## AddressDetails (interface)
diff --git a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md
index fc12c163..c75cae15 100644
--- a/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md
+++ b/packages/evolution/docs/modules/sdk/builders/TransactionBuilder.ts.md
@@ -399,6 +399,21 @@ export interface TransactionBuilderBase {
*/
readonly registerStake: (params: RegisterStakeParams) => this
+ /**
+ * Register a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+ * This is the pre-Conway registration format still accepted on mainnet and
+ * is what most wallets use today.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly registerStakeLegacy: (params: RegisterStakeLegacyParams) => this
+
/**
* Deregister a stake credential from the chain.
*
@@ -417,6 +432,20 @@ export interface TransactionBuilderBase {
*/
readonly deregisterStake: (params: DeregisterStakeParams) => this
+ /**
+ * Deregister a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+ * This is the pre-Conway deregistration format still accepted on mainnet.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly deregisterStakeLegacy: (params: DeregisterStakeLegacyParams) => this
+
/**
* Delegate stake and/or voting power to a pool or DRep.
*
diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md
index 66ce1243..1522cb73 100644
--- a/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md
+++ b/packages/evolution/docs/modules/sdk/builders/operations/Operations.ts.md
@@ -32,8 +32,10 @@ parent: Modules
- [~~DelegateToParams~~ (interface)](#delegatetoparams-interface)
- [DelegateToPoolAndDRepParams (interface)](#delegatetopoolanddrepparams-interface)
- [DelegateToPoolParams (interface)](#delegatetopoolparams-interface)
+ - [DeregisterStakeLegacyParams (interface)](#deregisterstakelegacyparams-interface)
- [DeregisterStakeParams (interface)](#deregisterstakeparams-interface)
- [RegisterAndDelegateToParams (interface)](#registeranddelegatetoparams-interface)
+ - [RegisterStakeLegacyParams (interface)](#registerstakelegacyparams-interface)
- [RegisterStakeParams (interface)](#registerstakeparams-interface)
- [WithdrawParams (interface)](#withdrawparams-interface)
- [utils](#utils)
@@ -425,6 +427,28 @@ export interface DelegateToPoolParams {
Added in v2.0.0
+## DeregisterStakeLegacyParams (interface)
+
+Parameters for legacy (pre-Conway) stake credential deregistration.
+
+Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+This is the pre-Conway deregistration format still accepted on mainnet.
+
+**Signature**
+
+```ts
+export interface DeregisterStakeLegacyParams {
+ /** The stake credential to deregister */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+```
+
+Added in v2.0.0
+
## DeregisterStakeParams (interface)
Parameters for deregistering a stake credential.
@@ -473,6 +497,28 @@ export interface RegisterAndDelegateToParams {
Added in v2.0.0
+## RegisterStakeLegacyParams (interface)
+
+Parameters for legacy (pre-Conway) stake credential registration.
+
+Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+This is the pre-Conway registration format still accepted on mainnet.
+
+**Signature**
+
+```ts
+export interface RegisterStakeLegacyParams {
+ /** The stake credential to register (key hash or script hash) */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+```
+
+Added in v2.0.0
+
## RegisterStakeParams (interface)
Parameters for registering a stake credential.
diff --git a/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md b/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md
index d6bf2618..406d5af3 100644
--- a/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md
+++ b/packages/evolution/docs/modules/sdk/builders/operations/Stake.ts.md
@@ -19,8 +19,10 @@ Added in v2.0.0
- [createDelegateToPoolAndDRepProgram](#createdelegatetopoolanddrepprogram)
- [createDelegateToPoolProgram](#createdelegatetopoolprogram)
- [~~createDelegateToProgram~~](#createdelegatetoprogram)
+ - [createDeregisterStakeLegacyProgram](#createderegisterstakelegacyprogram)
- [createDeregisterStakeProgram](#createderegisterstakeprogram)
- [createRegisterAndDelegateToProgram](#createregisteranddelegatetoprogram)
+ - [createRegisterStakeLegacyProgram](#createregisterstakelegacyprogram)
- [createRegisterStakeProgram](#createregisterstakeprogram)
- [createWithdrawProgram](#createwithdrawprogram)
@@ -94,6 +96,21 @@ export declare const createDelegateToProgram: (
Added in v2.0.0
+## createDeregisterStakeLegacyProgram
+
+Creates a ProgramStep for legacy (pre-Conway) stake deregistration.
+Adds a StakeDeregistration (CDDL tag 1) certificate with no deposit refund.
+
+**Signature**
+
+```ts
+export declare const createDeregisterStakeLegacyProgram: (
+ params: DeregisterStakeLegacyParams
+) => Effect.Effect
+```
+
+Added in v2.0.0
+
## createDeregisterStakeProgram
Creates a ProgramStep for deregisterStake operation.
@@ -135,6 +152,21 @@ export declare const createRegisterAndDelegateToProgram: (
Added in v2.0.0
+## createRegisterStakeLegacyProgram
+
+Creates a ProgramStep for legacy (pre-Conway) stake registration.
+Adds a StakeRegistration (CDDL tag 0) certificate with no deposit.
+
+**Signature**
+
+```ts
+export declare const createRegisterStakeLegacyProgram: (
+ params: RegisterStakeLegacyParams
+) => Effect.Effect
+```
+
+Added in v2.0.0
+
## createRegisterStakeProgram
Creates a ProgramStep for registerStake operation.
diff --git a/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md b/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md
index 695939dd..2afdd380 100644
--- a/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md
+++ b/packages/evolution/docs/modules/sdk/wallet/Derivation.ts.md
@@ -68,7 +68,7 @@ export declare function addressFromSeed(
password?: string
addressType?: "Base" | "Enterprise"
accountIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): { address: CoreAddress.Address; rewardAddress: CoreRewardAddress.RewardAddress | undefined }
```
@@ -101,7 +101,7 @@ export declare function walletFromBip32(
options: {
addressType?: "Base" | "Enterprise"
accountIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): SeedDerivationResult
```
@@ -119,7 +119,7 @@ export declare function walletFromPrivateKey(
options: {
stakeKeyBech32?: string
addressType?: "Base" | "Enterprise"
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
} = {}
): Effect.Effect
```
@@ -137,7 +137,7 @@ export declare const walletFromSeed: (
accountIndex?: number
paymentIndex?: number
stakeIndex?: number
- network?: "Mainnet" | "Testnet" | "Custom"
+ networkId?: number
}
) => Effect.Effect
```
diff --git a/packages/evolution/src/Address.ts b/packages/evolution/src/Address.ts
index 4d7b4e63..6d922588 100644
--- a/packages/evolution/src/Address.ts
+++ b/packages/evolution/src/Address.ts
@@ -243,7 +243,7 @@ export const toBytes = Schema.encodeSync(FromBytes)
* ```typescript
* import * as Address from "@evolution-sdk/evolution/Address"
*
- * const address = Address.fromSeed("your twenty-four word mnemonic ...", {
+ * const address = Address.fromSeed("test test test test test test test test test test test test test test test test test test test test test test test sauce", {
* accountIndex: 0,
* networkId: 0 // 0 = testnet, 1 = mainnet
* })
diff --git a/packages/evolution/src/sdk/builders/TransactionBuilder.ts b/packages/evolution/src/sdk/builders/TransactionBuilder.ts
index 2e22c01b..d7d7952b 100644
--- a/packages/evolution/src/sdk/builders/TransactionBuilder.ts
+++ b/packages/evolution/src/sdk/builders/TransactionBuilder.ts
@@ -65,6 +65,7 @@ import type {
DelegateToPoolAndDRepParams,
DelegateToPoolParams,
DeregisterDRepParams,
+ DeregisterStakeLegacyParams,
DeregisterStakeParams,
MintTokensParams,
PayToAddressParams,
@@ -73,6 +74,7 @@ import type {
RegisterAndDelegateToParams,
RegisterDRepParams,
RegisterPoolParams,
+ RegisterStakeLegacyParams,
RegisterStakeParams,
ResignCommitteeColdParams,
RetirePoolParams,
@@ -957,6 +959,21 @@ export interface TransactionBuilderBase {
*/
readonly registerStake: (params: RegisterStakeParams) => this
+ /**
+ * Register a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+ * This is the pre-Conway registration format still accepted on mainnet and
+ * is what most wallets use today.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly registerStakeLegacy: (params: RegisterStakeLegacyParams) => this
+
/**
* Deregister a stake credential from the chain.
*
@@ -975,6 +992,20 @@ export interface TransactionBuilderBase {
*/
readonly deregisterStake: (params: DeregisterStakeParams) => this
+ /**
+ * Deregister a stake credential using the legacy (pre-Conway) certificate format.
+ *
+ * Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+ * This is the pre-Conway deregistration format still accepted on mainnet.
+ *
+ * Queues a deferred operation that will be executed when build() is called.
+ * Returns the same builder for method chaining.
+ *
+ * @since 2.0.0
+ * @category staking-methods
+ */
+ readonly deregisterStakeLegacy: (params: DeregisterStakeLegacyParams) => this
+
/**
* Delegate stake and/or voting power to a pool or DRep.
*
diff --git a/packages/evolution/src/sdk/builders/internal/factory.ts b/packages/evolution/src/sdk/builders/internal/factory.ts
index c3e909f7..5ccdd07c 100644
--- a/packages/evolution/src/sdk/builders/internal/factory.ts
+++ b/packages/evolution/src/sdk/builders/internal/factory.ts
@@ -65,10 +65,18 @@ export const makeTxBuilder = (
programs.push(Stake.createRegisterStakeProgram(params))
return txBuilder
},
+ registerStakeLegacy: (params: Operations.RegisterStakeLegacyParams) => {
+ programs.push(Stake.createRegisterStakeLegacyProgram(params))
+ return txBuilder
+ },
deregisterStake: (params: Operations.DeregisterStakeParams) => {
programs.push(Stake.createDeregisterStakeProgram(params))
return txBuilder
},
+ deregisterStakeLegacy: (params: Operations.DeregisterStakeLegacyParams) => {
+ programs.push(Stake.createDeregisterStakeLegacyProgram(params))
+ return txBuilder
+ },
delegateTo: (params: Operations.DelegateToParams) => {
programs.push(Stake.createDelegateToProgram(params))
return txBuilder
diff --git a/packages/evolution/src/sdk/builders/operations/Operations.ts b/packages/evolution/src/sdk/builders/operations/Operations.ts
index 1fcb47a0..d15204a7 100644
--- a/packages/evolution/src/sdk/builders/operations/Operations.ts
+++ b/packages/evolution/src/sdk/builders/operations/Operations.ts
@@ -113,6 +113,24 @@ export interface RegisterStakeParams {
readonly label?: string
}
+/**
+ * Parameters for legacy (pre-Conway) stake credential registration.
+ *
+ * Creates a StakeRegistration certificate (CDDL tag 0) with no deposit.
+ * This is the pre-Conway registration format still accepted on mainnet.
+ *
+ * @since 2.0.0
+ * @category staking
+ */
+export interface RegisterStakeLegacyParams {
+ /** The stake credential to register (key hash or script hash) */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+
/**
* Parameters for deregistering a stake credential.
*
@@ -131,6 +149,24 @@ export interface DeregisterStakeParams {
readonly label?: string
}
+/**
+ * Parameters for legacy (pre-Conway) stake credential deregistration.
+ *
+ * Creates a StakeDeregistration certificate (CDDL tag 1) with no deposit refund.
+ * This is the pre-Conway deregistration format still accepted on mainnet.
+ *
+ * @since 2.0.0
+ * @category staking
+ */
+export interface DeregisterStakeLegacyParams {
+ /** The stake credential to deregister */
+ readonly stakeCredential: Credential.Credential
+ /** Redeemer for script-controlled stake credentials */
+ readonly redeemer?: RedeemerBuilder.RedeemerArg
+ /** Optional label for debugging script failures - identifies this operation in error messages */
+ readonly label?: string
+}
+
/**
* Parameters for delegating stake and/or voting power.
*
diff --git a/packages/evolution/src/sdk/builders/operations/Stake.ts b/packages/evolution/src/sdk/builders/operations/Stake.ts
index 7ae1e2e3..4686440f 100644
--- a/packages/evolution/src/sdk/builders/operations/Stake.ts
+++ b/packages/evolution/src/sdk/builders/operations/Stake.ts
@@ -17,8 +17,10 @@ import type {
DelegateToParams,
DelegateToPoolAndDRepParams,
DelegateToPoolParams,
+ DeregisterStakeLegacyParams,
DeregisterStakeParams,
RegisterAndDelegateToParams,
+ RegisterStakeLegacyParams,
RegisterStakeParams,
WithdrawParams
} from "./Operations.js"
@@ -113,6 +115,74 @@ export const createRegisterStakeProgram = (
yield* Effect.logDebug(`[RegisterStake] Added RegCert certificate with deposit ${keyDeposit}`)
})
+/**
+ * Creates a ProgramStep for legacy (pre-Conway) stake registration.
+ * Adds a StakeRegistration (CDDL tag 0) certificate with no deposit.
+ *
+ * @since 2.0.0
+ * @category programs
+ */
+export const createRegisterStakeLegacyProgram = (
+ params: RegisterStakeLegacyParams
+): Effect.Effect =>
+ Effect.gen(function* () {
+ const ctx = yield* TxContext
+
+ // Check if script-controlled
+ const isScriptControlled = params.stakeCredential._tag === "ScriptHash"
+
+ if (isScriptControlled && !params.redeemer) {
+ return yield* Effect.fail(
+ new TransactionBuilderError({
+ message: "Redeemer required for script-controlled stake credential registration"
+ })
+ )
+ }
+
+ // Create legacy StakeRegistration certificate (no deposit)
+ const certificate = new Certificate.StakeRegistration({
+ stakeCredential: params.stakeCredential
+ })
+
+ yield* Ref.update(ctx, (state) => {
+ let newRedeemers = state.redeemers
+ let newDeferredRedeemers = state.deferredRedeemers
+
+ // Track redeemer if script-controlled
+ if (params.redeemer && isScriptControlled) {
+ const deferred = RedeemerBuilder.toDeferredRedeemer(params.redeemer)
+ const certKey = `cert:${Bytes.toHex(params.stakeCredential.hash)}`
+
+ if (deferred._tag === "static") {
+ newRedeemers = new Map(state.redeemers)
+ newRedeemers.set(certKey, {
+ tag: "cert",
+ data: deferred.data,
+ exUnits: undefined,
+ label: params.label
+ })
+ } else {
+ newDeferredRedeemers = new Map(state.deferredRedeemers)
+ newDeferredRedeemers.set(certKey, {
+ tag: "cert",
+ deferred,
+ exUnits: undefined,
+ label: params.label
+ })
+ }
+ }
+
+ return {
+ ...state,
+ certificates: [...state.certificates, certificate],
+ redeemers: newRedeemers,
+ deferredRedeemers: newDeferredRedeemers
+ }
+ })
+
+ yield* Effect.logDebug(`[RegisterStakeLegacy] Added StakeRegistration certificate (no deposit)`)
+ })
+
/**
* Creates a ProgramStep for delegateTo operation.
* Delegates stake and/or voting power based on parameters provided.
@@ -597,6 +667,74 @@ export const createDeregisterStakeProgram = (
yield* Effect.logDebug(`[DeregisterStake] Added UnregCert certificate with deposit refund ${keyDeposit}`)
})
+/**
+ * Creates a ProgramStep for legacy (pre-Conway) stake deregistration.
+ * Adds a StakeDeregistration (CDDL tag 1) certificate with no deposit refund.
+ *
+ * @since 2.0.0
+ * @category programs
+ */
+export const createDeregisterStakeLegacyProgram = (
+ params: DeregisterStakeLegacyParams
+): Effect.Effect =>
+ Effect.gen(function* () {
+ const ctx = yield* TxContext
+
+ // Check if script-controlled
+ const isScriptControlled = params.stakeCredential._tag === "ScriptHash"
+
+ if (isScriptControlled && !params.redeemer) {
+ return yield* Effect.fail(
+ new TransactionBuilderError({
+ message: "Redeemer required for script-controlled stake credential deregistration"
+ })
+ )
+ }
+
+ // Create legacy StakeDeregistration certificate (no deposit refund)
+ const certificate = new Certificate.StakeDeregistration({
+ stakeCredential: params.stakeCredential
+ })
+
+ yield* Ref.update(ctx, (state) => {
+ let newRedeemers = state.redeemers
+ let newDeferredRedeemers = state.deferredRedeemers
+
+ // Track redeemer if script-controlled
+ if (params.redeemer && isScriptControlled) {
+ const deferred = RedeemerBuilder.toDeferredRedeemer(params.redeemer)
+ const certKey = `cert:${Bytes.toHex(params.stakeCredential.hash)}`
+
+ if (deferred._tag === "static") {
+ newRedeemers = new Map(state.redeemers)
+ newRedeemers.set(certKey, {
+ tag: "cert",
+ data: deferred.data,
+ exUnits: undefined,
+ label: params.label
+ })
+ } else {
+ newDeferredRedeemers = new Map(state.deferredRedeemers)
+ newDeferredRedeemers.set(certKey, {
+ tag: "cert",
+ deferred,
+ exUnits: undefined,
+ label: params.label
+ })
+ }
+ }
+
+ return {
+ ...state,
+ certificates: [...state.certificates, certificate],
+ redeemers: newRedeemers,
+ deferredRedeemers: newDeferredRedeemers
+ }
+ })
+
+ yield* Effect.logDebug(`[DeregisterStakeLegacy] Added StakeDeregistration certificate (no deposit refund)`)
+ })
+
/**
* Creates a ProgramStep for withdraw operation.
* Adds a withdrawal entry to the transaction.