diff --git a/.changeset/blockchain-applied-provider.md b/.changeset/blockchain-applied-provider.md
new file mode 100644
index 00000000..e6a36ad5
--- /dev/null
+++ b/.changeset/blockchain-applied-provider.md
@@ -0,0 +1,5 @@
+---
+"@evolution-sdk/evolution": minor
+---
+
+Add Blockchain Applied (BCA) as a built-in provider (`Client.make(...).withBlockchainApplied({ baseUrl, token })`, or `BlockchainAppliedProvider` / `mainnet` / `preprod` / `preview` / `custom` directly). Covers the full `ProviderEffect` interface: `getProtocolParameters`, `getUtxos`, `getUtxosWithUnit`, `getUtxoByUnit`, `getUtxosByOutRef`, `getDelegation`, `getDatum`, `awaitTx`, `submitTx`, and `evaluateTx`.
diff --git a/.env.test.local.example b/.env.test.local.example
index f9c35650..52289af9 100644
--- a/.env.test.local.example
+++ b/.env.test.local.example
@@ -21,6 +21,12 @@ MAESTRO_PREPROD_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
# Optional: override the default preprod endpoint
# MAESTRO_PREPROD_URL=https://preprod.gomaestro-api.org/v1
+# ── Blockchain Applied (BCA) ──────────────────────────────────────────────────
+# Required to run BCA tests. Ask the BCA team for a preprod bearer token.
+BCA_PREPROD_KEY=bca_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+# Optional: override the default preprod endpoint
+# BCA_PREPROD_URL=https://staging.blockchain-applied.com/api_preprod/v1
+
# ── Kupmios (self-hosted or Demeter.run) ──────────────────────────────────────
# Both URLs are required to run Kupmios tests.
# If you use Demeter, provide one Kupo key and one Ogmios key.
diff --git a/.gitignore b/.gitignore
index c810abd8..d8ae95ba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,6 @@ CLAUDE.md
AGENTS.md
# Local env overrides — never commit API keys
+**/*.local
.env.local
.env.test.local
diff --git a/docs/content/docs/advanced/custom-providers.mdx b/docs/content/docs/advanced/custom-providers.mdx
index 4db41aab..25bf6763 100644
--- a/docs/content/docs/advanced/custom-providers.mdx
+++ b/docs/content/docs/advanced/custom-providers.mdx
@@ -6,12 +6,13 @@ description: Implement your own blockchain provider
# Custom Providers
-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.
+ The SDK currently provides five 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
-All providers implement the `ProviderEffect` interface internally. This is what each built-in provider (Blockfrost, Kupmios, Maestro, Koios) satisfies:
+All providers implement the `ProviderEffect` interface internally. This is what each built-in provider (Blockfrost, Kupmios, Maestro, Koios, Blockchain Applied) satisfies:
```typescript
interface ProviderEffect {
@@ -32,12 +33,13 @@ Understanding this interface is useful for knowing what data each provider must
## Built-in Providers
-| Provider | Connection | Best For |
-| --------------- | ---------------- | --------------------------------- |
-| **Blockfrost** | REST API | Production apps, simple setup |
-| **Maestro** | REST API | Advanced queries, data analytics |
-| **Koios** | REST API | Open source, community maintained |
-| **Kupo/Ogmios** | WebSocket + REST | Self-hosted, devnet, full control |
+| Provider | Connection | Best For |
+| ---------------------- | ---------------- | -------------------------------------- |
+| **Blockfrost** | REST API | Production apps, simple setup |
+| **Maestro** | REST API | Advanced queries, data analytics |
+| **Koios** | REST API | Open source, community maintained |
+| **Kupo/Ogmios** | WebSocket + REST | Self-hosted, devnet, full control |
+| **Blockchain Applied** | REST API | Full-featured Cardano indexer/node API |
## Using a Provider
@@ -45,18 +47,22 @@ Understanding this interface is useful for knowing what data each provider must
import { preprod, Client } from "@evolution-sdk/evolution"
// Blockfrost
-const blockfrostClient = Client.make(preprod)
- .withBlockfrost({
- baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
- projectId: process.env.BLOCKFROST_API_KEY!
- })
+const blockfrostClient = Client.make(preprod).withBlockfrost({
+ baseUrl: "https://cardano-preprod.blockfrost.io/api/v0",
+ projectId: process.env.BLOCKFROST_API_KEY!
+})
// Kupo/Ogmios
-const kupmiosClient = Client.make(preprod)
- .withKupmios({
- kupoUrl: "http://localhost:1442",
- ogmiosUrl: "http://localhost:1337"
- })
+const kupmiosClient = Client.make(preprod).withKupmios({
+ kupoUrl: "http://localhost:1442",
+ ogmiosUrl: "http://localhost:1337"
+})
+
+// Blockchain Applied
+const bcaClient = Client.make(preprod).withBlockchainApplied({
+ baseUrl: "https://api.blockchain-applied.com/api_preprod/v1",
+ token: process.env.BCA_API_KEY!
+})
```
## Next Steps
diff --git a/examples/with-vite-react/README.md b/examples/with-vite-react/README.md
index 371c111d..583ea297 100644
--- a/examples/with-vite-react/README.md
+++ b/examples/with-vite-react/README.md
@@ -126,11 +126,22 @@ const txHash = await sdk
.then(tx => tx.submit());
```
+Or with the Blockchain Applied (BCA) provider:
+
+```typescript
+const sdk = client(preprod)
+ .withBlockchainApplied({
+ baseUrl: "https://api.blockchain-applied.com/api_preprod/v1",
+ token: "your_bca_token"
+ })
+ .withCip30(walletApi);
+```
+
### Key Concepts
- **Client Assembly**: Start with `client(chain)` and add capabilities with `.withX(...)`
- **Wallet Capability**: Connect a CIP-30 wallet with `.withCip30(walletApi)`
-- **Provider Capability**: Add Blockfrost, Maestro, Kupmios, or Koios with `.withBlockfrost(...)` and the related methods
+- **Provider Capability**: Add Blockfrost, Maestro, Kupmios, Koios, or Blockchain Applied with `.withBlockfrost(...)` and the related methods
- **Transaction Building**: Chain operations like `payToAddress()`, `collectFrom()`, etc.
- **Signing & Submission**: Build → Sign → Submit pipeline
diff --git a/packages/evolution/src/sdk/client/Client.ts b/packages/evolution/src/sdk/client/Client.ts
index d986405c..a7fa9421 100644
--- a/packages/evolution/src/sdk/client/Client.ts
+++ b/packages/evolution/src/sdk/client/Client.ts
@@ -90,6 +90,17 @@ export interface KoiosConfig {
readonly token?: string
}
+/**
+ * Configuration for the Blockchain Applied (BCA) provider.
+ *
+ * @since 2.1.0
+ * @category model
+ */
+export interface BlockchainAppliedConfig {
+ readonly baseUrl: string
+ readonly token?: string
+}
+
/**
* Configuration for the Kupmios provider (Kupo + Ogmios).
*
@@ -156,6 +167,7 @@ export interface ClientAssembly {
readonly withKoios: (config: KoiosConfig) => ReadClient
readonly withKupmios: (config: KupmiosConfig) => ReadClient
readonly withMaestro: (config: MaestroConfig) => ReadClient
+ readonly withBlockchainApplied: (config: BlockchainAppliedConfig) => ReadClient
readonly withAddress: (address: string, rewardAddress?: string) => AddressClient
readonly withSeed: (config: SeedWalletConfig) => OfflineSignerClient
readonly withPrivateKey: (config: PrivateKeyWalletConfig) => OfflineSignerClient
@@ -190,6 +202,7 @@ export type AddressClient = EffectToPromiseAPI & {
readonly withKoios: (config: KoiosConfig) => ReadOnlyClient
readonly withKupmios: (config: KupmiosConfig) => ReadOnlyClient
readonly withMaestro: (config: MaestroConfig) => ReadOnlyClient
+ readonly withBlockchainApplied: (config: BlockchainAppliedConfig) => ReadOnlyClient
readonly effect: AddressClientEffect
}
@@ -205,6 +218,7 @@ export type OfflineSignerClient = EffectToPromiseAPI
readonly withKoios: (config: KoiosConfig) => SigningClient
readonly withKupmios: (config: KupmiosConfig) => SigningClient
readonly withMaestro: (config: MaestroConfig) => SigningClient
+ readonly withBlockchainApplied: (config: BlockchainAppliedConfig) => SigningClient
readonly effect: OfflineSignerClientEffect
}
diff --git a/packages/evolution/src/sdk/client/internal/Client.ts b/packages/evolution/src/sdk/client/internal/Client.ts
index 378a6c0e..7dbaf31a 100644
--- a/packages/evolution/src/sdk/client/internal/Client.ts
+++ b/packages/evolution/src/sdk/client/internal/Client.ts
@@ -33,6 +33,7 @@ const createAddressClient = (chain: Chain, wallet: Wallet.ReadOnlyWallet): Addre
withKoios: (config) => createReadOnlyClient(chain, Providers.koios(config), wallet),
withKupmios: (config) => createReadOnlyClient(chain, Providers.kupmios(config), wallet),
withMaestro: (config) => createReadOnlyClient(chain, Providers.maestro(config), wallet),
+ withBlockchainApplied: (config) => createReadOnlyClient(chain, Providers.blockchainApplied(config), wallet),
effect: wallet.effect satisfies AddressClientEffect
})
@@ -58,6 +59,7 @@ const createOfflineSignerClient = (chain: Chain, wallet: Signing.ResolvedSignerW
withKoios: (config) => createSigningClient(chain, Providers.koios(config), wallet),
withKupmios: (config) => createSigningClient(chain, Providers.kupmios(config), wallet),
withMaestro: (config) => createSigningClient(chain, Providers.maestro(config), wallet),
+ withBlockchainApplied: (config) => createSigningClient(chain, Providers.blockchainApplied(config), wallet),
effect: effectInterface
}
}
@@ -141,7 +143,8 @@ const createSigningClient = (
const createReadClient = (chain: Chain, provider: Provider.Provider): ReadClient => ({
...provider,
chain,
- withAddress: (address, rewardAddress) => createReadOnlyClient(chain, provider, Wallets.readOnlyWallet(address, rewardAddress)(chain)),
+ withAddress: (address, rewardAddress) =>
+ createReadOnlyClient(chain, provider, Wallets.readOnlyWallet(address, rewardAddress)(chain)),
withSeed: (config) => createSigningClient(chain, provider, Wallets.seedWallet(config)(chain)),
withPrivateKey: (config) => createSigningClient(chain, provider, Wallets.privateKeyWallet(config)(chain)),
withCip30: (api) => createSigningClient(chain, provider, Wallets.cip30Wallet(api)(chain)),
@@ -155,8 +158,10 @@ export const client = (chain: Chain = mainnet): ClientAssembly => ({
withKoios: (config) => createReadClient(chain, Providers.koios(config)),
withKupmios: (config) => createReadClient(chain, Providers.kupmios(config)),
withMaestro: (config) => createReadClient(chain, Providers.maestro(config)),
- withAddress: (address, rewardAddress) => createAddressClient(chain, Wallets.readOnlyWallet(address, rewardAddress)(chain)),
+ withBlockchainApplied: (config) => createReadClient(chain, Providers.blockchainApplied(config)),
+ withAddress: (address, rewardAddress) =>
+ createAddressClient(chain, Wallets.readOnlyWallet(address, rewardAddress)(chain)),
withSeed: (config) => createOfflineSignerClient(chain, Wallets.seedWallet(config)(chain)),
withPrivateKey: (config) => createOfflineSignerClient(chain, Wallets.privateKeyWallet(config)(chain)),
withCip30: (api) => createOfflineSignerClient(chain, Wallets.cip30Wallet(api)(chain))
-})
\ No newline at end of file
+})
diff --git a/packages/evolution/src/sdk/client/internal/Providers.ts b/packages/evolution/src/sdk/client/internal/Providers.ts
index 2cabf697..8b9097c7 100644
--- a/packages/evolution/src/sdk/client/internal/Providers.ts
+++ b/packages/evolution/src/sdk/client/internal/Providers.ts
@@ -1,9 +1,10 @@
+import * as BlockchainApplied from "../../provider/BlockchainApplied.js"
import * as Blockfrost from "../../provider/Blockfrost.js"
import * as Koios from "../../provider/Koios.js"
import * as Kupmios from "../../provider/Kupmios.js"
import * as Maestro from "../../provider/Maestro.js"
import type { Provider } from "../../provider/Provider.js"
-import type { BlockfrostConfig, KoiosConfig, KupmiosConfig, MaestroConfig } from "../Client.js"
+import type { BlockchainAppliedConfig, BlockfrostConfig, KoiosConfig, KupmiosConfig, MaestroConfig } from "../Client.js"
// ── Blockfrost ────────────────────────────────────────────────────────────────
export const blockfrost = (config: BlockfrostConfig): Provider => Blockfrost.custom(config.baseUrl, config.projectId)
@@ -18,3 +19,7 @@ export const kupmios = (config: KupmiosConfig): Provider =>
// ── Maestro ───────────────────────────────────────────────────────────────────
export const maestro = (config: MaestroConfig): Provider =>
new Maestro.MaestroProvider(config.baseUrl, config.apiKey, config.turboSubmit)
+
+// ── Blockchain Applied ───────────────────────────────────────────────────────
+export const blockchainApplied = (config: BlockchainAppliedConfig): Provider =>
+ BlockchainApplied.custom(config.baseUrl, config.token)
diff --git a/packages/evolution/src/sdk/provider/BlockchainApplied.ts b/packages/evolution/src/sdk/provider/BlockchainApplied.ts
new file mode 100644
index 00000000..76f4cc76
--- /dev/null
+++ b/packages/evolution/src/sdk/provider/BlockchainApplied.ts
@@ -0,0 +1,104 @@
+import { Effect } from "effect"
+
+import * as BlockchainAppliedEffect from "./internal/BlockchainAppliedEffect.js"
+import type { Provider, ProviderEffect } from "./Provider.js"
+
+const HOST = "https://api.blockchain-applied.com"
+
+/**
+ * Blockchain Applied (BCA) provider for Cardano blockchain data access.
+ * Supports Bearer token authentication.
+ *
+ * @since 2.0.0
+ * @category constructors
+ */
+export class BlockchainAppliedProvider implements Provider {
+ readonly effect: ProviderEffect
+ readonly baseUrl: string
+ readonly token?: string
+
+ constructor(baseUrl: string, token?: string) {
+ this.baseUrl = baseUrl
+ this.token = token
+
+ this.effect = {
+ getProtocolParameters: () => BlockchainAppliedEffect.getProtocolParameters(baseUrl, token),
+ getUtxos: BlockchainAppliedEffect.getUtxos(baseUrl, token),
+ getUtxosWithUnit: BlockchainAppliedEffect.getUtxosWithUnit(baseUrl, token),
+ getUtxoByUnit: BlockchainAppliedEffect.getUtxoByUnit(baseUrl, token),
+ getUtxosByOutRef: BlockchainAppliedEffect.getUtxosByOutRef(baseUrl, token),
+ getDelegation: BlockchainAppliedEffect.getDelegation(baseUrl, token),
+ getDatum: BlockchainAppliedEffect.getDatum(baseUrl, token),
+ awaitTx: BlockchainAppliedEffect.awaitTx(baseUrl, token),
+ submitTx: BlockchainAppliedEffect.submitTx(baseUrl, token),
+ evaluateTx: BlockchainAppliedEffect.evaluateTx(baseUrl, token)
+ }
+ }
+
+ getProtocolParameters = () => Effect.runPromise(this.effect.getProtocolParameters())
+
+ getUtxos = (addressOrCredential: Parameters[0]) =>
+ Effect.runPromise(this.effect.getUtxos(addressOrCredential))
+
+ getUtxosWithUnit = (
+ addressOrCredential: Parameters[0],
+ unit: Parameters[1]
+ ) => Effect.runPromise(this.effect.getUtxosWithUnit(addressOrCredential, unit))
+
+ getUtxoByUnit = (unit: Parameters[0]) => Effect.runPromise(this.effect.getUtxoByUnit(unit))
+
+ getUtxosByOutRef = (outRefs: Parameters[0]) =>
+ Effect.runPromise(this.effect.getUtxosByOutRef(outRefs))
+
+ getDelegation = (rewardAddress: Parameters[0]) =>
+ Effect.runPromise(this.effect.getDelegation(rewardAddress))
+
+ getDatum = (datumHash: Parameters[0]) => Effect.runPromise(this.effect.getDatum(datumHash))
+
+ awaitTx = (
+ txHash: Parameters[0],
+ checkInterval?: Parameters[1],
+ timeout?: Parameters[2]
+ ) => Effect.runPromise(this.effect.awaitTx(txHash, checkInterval, timeout))
+
+ submitTx = (tx: Parameters[0]) => Effect.runPromise(this.effect.submitTx(tx))
+
+ evaluateTx = (tx: Parameters[0], additionalUTxOs?: Parameters[1]) =>
+ Effect.runPromise(this.effect.evaluateTx(tx, additionalUTxOs))
+}
+
+/**
+ * Pre-configured Blockchain Applied provider for Cardano mainnet.
+ *
+ * @since 2.0.0
+ * @category constructors
+ */
+export const mainnet = (token?: string): BlockchainAppliedProvider =>
+ new BlockchainAppliedProvider(`${HOST}/api_ada/v1`, token)
+
+/**
+ * Pre-configured Blockchain Applied provider for Cardano preprod testnet.
+ *
+ * @since 2.0.0
+ * @category constructors
+ */
+export const preprod = (token?: string): BlockchainAppliedProvider =>
+ new BlockchainAppliedProvider(`${HOST}/api_preprod/v1`, token)
+
+/**
+ * Pre-configured Blockchain Applied provider for Cardano preview testnet.
+ *
+ * @since 2.0.0
+ * @category constructors
+ */
+export const preview = (token?: string): BlockchainAppliedProvider =>
+ new BlockchainAppliedProvider(`${HOST}/api_preview/v1`, token)
+
+/**
+ * Create a custom Blockchain Applied provider with a custom base URL.
+ *
+ * @since 2.0.0
+ * @category constructors
+ */
+export const custom = (baseUrl: string, token?: string): BlockchainAppliedProvider =>
+ new BlockchainAppliedProvider(baseUrl, token)
diff --git a/packages/evolution/src/sdk/provider/internal/BlockchainApplied.ts b/packages/evolution/src/sdk/provider/internal/BlockchainApplied.ts
new file mode 100644
index 00000000..d1703913
--- /dev/null
+++ b/packages/evolution/src/sdk/provider/internal/BlockchainApplied.ts
@@ -0,0 +1,272 @@
+/**
+ * @fileoverview Blockchain Applied (BCA) API schemas and transformation utilities.
+ *
+ * Schemas mirror https://api.blockchain-applied.com/api_ada/v1/openapi.json.
+ * Swagger UI on https://api.blockchain-applied.com/api_ada/v1/docs/
+ * BCA is backed by cardano-db-sync, so field naming (block_index, out_sum, valid_contract, ...)
+ * follows db-sync conventions rather than Blockfrost/Koios/Maestro's own vocabularies.
+ */
+
+import { Schema } from "effect"
+
+import * as CoreAssets from "../../../Assets.js"
+import * as Bytes from "../../../Bytes.js"
+import * as PlutusData from "../../../Data.js"
+import * as DatumHash from "../../../DatumHash.js"
+import type { DatumOption } from "../../../DatumOption.js"
+import * as InlineDatum from "../../../InlineDatum.js"
+import * as NativeScripts from "../../../NativeScripts.js"
+import * as PlutusV1 from "../../../PlutusV1.js"
+import * as PlutusV2 from "../../../PlutusV2.js"
+import * as PlutusV3 from "../../../PlutusV3.js"
+import * as PoolKeyHash from "../../../PoolKeyHash.js"
+import * as Redeemer from "../../../Redeemer.js"
+import type { Script } from "../../../Script.js"
+import type { EvalRedeemer } from "../../EvalRedeemer.js"
+import type * as Provider from "../Provider.js"
+
+// ============================================================================
+// Shared envelope
+// ============================================================================
+
+/**
+ * Some BCA resource endpoints (datum, script) wrap their payload as
+ * { summary, details }. `summary` is a resource-category label, not a
+ * union discriminator.
+ */
+export const Envelope = (details: Schema.Schema) =>
+ Schema.Struct({
+ summary: Schema.String,
+ details
+ })
+
+export const PaginatedResponse = (item: Schema.Schema) =>
+ Schema.Struct({
+ data: Schema.Array(item),
+ page: Schema.Number,
+ page_size: Schema.Number,
+ total_count: Schema.Number,
+ total_pages: Schema.Number,
+ has_next: Schema.Boolean
+ })
+
+// ============================================================================
+// UTxOs (GET /utxos/{address}, GET /assets/{unit}/utxos)
+// ============================================================================
+
+export const UtxoAsset = Schema.Struct({
+ policy_id: Schema.String,
+ asset_name: Schema.String,
+ quantity: Schema.String
+})
+export interface UtxoAsset extends Schema.Schema.Type {}
+
+export const UtxoResponse = Schema.Struct({
+ tx_hash: Schema.String,
+ output_index: Schema.Number,
+ address: Schema.String,
+ lovelace: Schema.String,
+ assets: Schema.Array(UtxoAsset),
+ datum_hash: Schema.optional(Schema.NullOr(Schema.String)),
+ inline_datum: Schema.optional(Schema.NullOr(Schema.String)),
+ reference_script_hash: Schema.optional(Schema.NullOr(Schema.String)),
+ reference_script_cbor: Schema.optional(Schema.NullOr(Schema.String)),
+ reference_script_type: Schema.optional(Schema.NullOr(Schema.String))
+})
+export interface UtxoResponse extends Schema.Schema.Type {}
+
+export const PaginatedUtxoResponse = PaginatedResponse(UtxoResponse)
+
+// ============================================================================
+// Transaction (GET /tx/{hash}) — inputs/outputs are UtxoResponse-shaped, so
+// getUtxosByOutRef can reuse the same UTxO resolution path as GET /utxos/{address}.
+// ============================================================================
+
+export const TransactionResponse = Schema.Struct({
+ hash: Schema.String,
+ inputs: Schema.Array(UtxoResponse),
+ outputs: Schema.Array(UtxoResponse)
+})
+export interface TransactionResponse extends Schema.Schema.Type {}
+
+// ============================================================================
+// Datum (GET /datum/{datumhash})
+// ============================================================================
+
+export const DatumDetails = Schema.Struct({
+ hash: Schema.String,
+ tx_hash: Schema.String,
+ json: Schema.Unknown,
+ bytes: Schema.String
+})
+export interface DatumDetails extends Schema.Schema.Type {}
+
+// ============================================================================
+// Script (GET /script/{scripthash})
+// ============================================================================
+
+export const ScriptDetails = Schema.Struct({
+ hash: Schema.String,
+ size: Schema.Number,
+ type: Schema.String,
+ json: Schema.Unknown,
+ bytes: Schema.String,
+ tx_hash: Schema.String
+})
+export interface ScriptDetails extends Schema.Schema.Type {}
+
+// ============================================================================
+// Staking (GET /staking/{stakeaddr}) — current pool delegation and reward
+// balance in one flat object.
+// ============================================================================
+
+export const StakingResponse = Schema.Struct({
+ address: Schema.String,
+ pool_id: Schema.optional(Schema.NullOr(Schema.String)),
+ total_rewards: Schema.String,
+ total_withdrawn: Schema.String,
+ withdrawable_rewards: Schema.String,
+ active_epoch: Schema.optional(Schema.NullOr(Schema.Number)),
+ delegation_tx: Schema.optional(Schema.NullOr(Schema.String))
+})
+export interface StakingResponse extends Schema.Schema.Type {}
+
+// ============================================================================
+// Protocol parameters (GET /protocol_parameters/latest)
+// ============================================================================
+
+export const ProtocolParametersResponse = Schema.Struct({
+ epoch_no: Schema.Number,
+ min_fee_a: Schema.Number,
+ min_fee_b: Schema.Number,
+ max_tx_size: Schema.Number,
+ max_val_size: Schema.Number,
+ key_deposit: Schema.String,
+ pool_deposit: Schema.String,
+ price_mem: Schema.Number,
+ price_step: Schema.Number,
+ max_tx_ex_mem: Schema.String,
+ max_tx_ex_steps: Schema.String,
+ cost_models: Schema.Unknown,
+ coins_per_utxo_size: Schema.optional(Schema.NullOr(Schema.String)),
+ collateral_percentage: Schema.optional(Schema.NullOr(Schema.Number)),
+ drep_deposit: Schema.optional(Schema.NullOr(Schema.String)),
+ gov_action_deposit: Schema.optional(Schema.NullOr(Schema.String)),
+ max_collateral_inputs: Schema.optional(Schema.NullOr(Schema.Number)),
+ min_fee_ref_script_cost_per_byte: Schema.optional(Schema.NullOr(Schema.Number))
+})
+export interface ProtocolParametersResponse extends Schema.Schema.Type {}
+
+// ============================================================================
+// Tx submit / evaluate
+// ============================================================================
+
+export const SubmitResponse = Schema.Struct({
+ tx_hash: Schema.String
+})
+
+export const ExUnits = Schema.Struct({
+ mem: Schema.String,
+ steps: Schema.String
+})
+
+export const EvalRedeemerResponse = Schema.Struct({
+ redeemer_tag: Schema.Literal("spend", "mint", "cert", "reward", "vote", "propose"),
+ redeemer_index: Schema.Number,
+ ex_units: ExUnits
+})
+
+// ============================================================================
+// Transformation utilities
+// ============================================================================
+
+/** BCA declares `cost_models` as an opaque object — normalize either array or object shapes. */
+const normalizeCostModel = (raw: unknown): Record => {
+ if (Array.isArray(raw)) {
+ return Object.fromEntries(raw.map((value, index) => [index.toString(), Number(value)]))
+ }
+ if (raw && typeof raw === "object") {
+ return Object.fromEntries(
+ Object.entries(raw as Record).map(([key, value]) => [key, Number(value)])
+ )
+ }
+ return {}
+}
+
+const extractCostModel = (costModels: unknown, keys: ReadonlyArray): Record => {
+ if (costModels && typeof costModels === "object") {
+ for (const key of keys) {
+ const value = (costModels as Record)[key]
+ if (value !== undefined) return normalizeCostModel(value)
+ }
+ }
+ return {}
+}
+
+export const transformProtocolParameters = (response: ProtocolParametersResponse): Provider.ProtocolParameters => ({
+ minFeeA: response.min_fee_a,
+ minFeeB: response.min_fee_b,
+ maxTxSize: response.max_tx_size,
+ maxValSize: response.max_val_size,
+ keyDeposit: BigInt(response.key_deposit),
+ poolDeposit: BigInt(response.pool_deposit),
+ drepDeposit: BigInt(response.drep_deposit ?? "0"),
+ govActionDeposit: BigInt(response.gov_action_deposit ?? "0"),
+ priceMem: response.price_mem,
+ priceStep: response.price_step,
+ maxTxExMem: BigInt(response.max_tx_ex_mem),
+ maxTxExSteps: BigInt(response.max_tx_ex_steps),
+ coinsPerUtxoByte: BigInt(response.coins_per_utxo_size ?? "0"),
+ collateralPercentage: response.collateral_percentage ?? 0,
+ maxCollateralInputs: response.max_collateral_inputs ?? 0,
+ minFeeRefScriptCostPerByte: response.min_fee_ref_script_cost_per_byte ?? 0,
+ costModels: {
+ PlutusV1: extractCostModel(response.cost_models, ["PlutusV1", "plutus_v1"]),
+ PlutusV2: extractCostModel(response.cost_models, ["PlutusV2", "plutus_v2"]),
+ PlutusV3: extractCostModel(response.cost_models, ["PlutusV3", "plutus_v3"])
+ }
+})
+
+export const transformAssets = (lovelace: string, assets: ReadonlyArray): CoreAssets.Assets => {
+ let result = CoreAssets.fromLovelace(BigInt(lovelace))
+ for (const asset of assets) {
+ result = CoreAssets.addByHex(result, asset.policy_id, asset.asset_name, BigInt(asset.quantity))
+ }
+ return result
+}
+
+export const transformScript = (details: { type: string; bytes: string }): Script | undefined => {
+ const bytes = Bytes.fromHex(details.bytes)
+ switch (details.type) {
+ case "plutusV1":
+ return new PlutusV1.PlutusV1({ bytes })
+ case "plutusV2":
+ return new PlutusV2.PlutusV2({ bytes })
+ case "plutusV3":
+ return new PlutusV3.PlutusV3({ bytes })
+ case "timelock":
+ case "multisig":
+ return NativeScripts.fromCBORHex(details.bytes)
+ default:
+ return undefined
+ }
+}
+
+export const transformDelegation = (response: StakingResponse): Provider.Delegation => ({
+ poolId: response.pool_id ? PoolKeyHash.fromBech32(response.pool_id) : null,
+ rewards: BigInt(response.withdrawable_rewards)
+})
+
+export const transformEvalRedeemer = (redeemer: Schema.Schema.Type): EvalRedeemer => ({
+ redeemer_tag: redeemer.redeemer_tag as Redeemer.RedeemerTag,
+ redeemer_index: redeemer.redeemer_index,
+ ex_units: new Redeemer.ExUnits({
+ mem: BigInt(redeemer.ex_units.mem),
+ steps: BigInt(redeemer.ex_units.steps)
+ })
+})
+
+export const inlineDatumFromCBORHex = (cborHex: string): DatumOption =>
+ new InlineDatum.InlineDatum({ data: PlutusData.fromCBORHex(cborHex) })
+
+export const datumHashFromHex = (hex: string): DatumOption => DatumHash.fromHex(hex)
diff --git a/packages/evolution/src/sdk/provider/internal/BlockchainAppliedEffect.ts b/packages/evolution/src/sdk/provider/internal/BlockchainAppliedEffect.ts
new file mode 100644
index 00000000..a94d6964
--- /dev/null
+++ b/packages/evolution/src/sdk/provider/internal/BlockchainAppliedEffect.ts
@@ -0,0 +1,332 @@
+/**
+ * @fileoverview Effect-based Blockchain Applied (BCA) provider functions.
+ * Internal module implementing all provider operations using the Effect pattern.
+ */
+
+import { HttpClientError } from "@effect/platform"
+import { Effect, Option, Schedule, Schema } from "effect"
+
+import * as CoreAddress from "../../../Address.js"
+import * as AssetName from "../../../AssetName.js"
+import * as CoreAssets from "../../../Assets.js"
+import * as Bytes from "../../../Bytes.js"
+import * as Credential from "../../../Credential.js"
+import * as PlutusData from "../../../Data.js"
+import type * as DatumHash from "../../../DatumHash.js"
+import type * as DatumOption from "../../../DatumOption.js"
+import * as NativeScripts from "../../../NativeScripts.js"
+import * as PolicyId from "../../../PolicyId.js"
+import type * as RewardAddress from "../../../RewardAddress.js"
+import type * as Script from "../../../Script.js"
+import * as Transaction from "../../../Transaction.js"
+import * as TransactionHash from "../../../TransactionHash.js"
+import type * as TransactionInput from "../../../TransactionInput.js"
+import * as CoreUTxO from "../../../UTxO.js"
+import * as Provider from "../Provider.js"
+import * as BCA from "./BlockchainApplied.js"
+import * as HttpUtils from "./HttpUtils.js"
+
+const PAGE_SIZE = 100
+const TIMEOUT = 10_000
+
+const bearerHeaders = (token?: string): Record | undefined =>
+ token ? { Authorization: `Bearer ${token}` } : undefined
+
+const wrapError = (operation: string) => (cause: unknown) =>
+ Effect.fail(
+ new Provider.ProviderError({
+ message: `BlockchainApplied ${operation} failed`,
+ cause
+ })
+ )
+
+const getAddressPath = (addressOrCredential: CoreAddress.Address | Credential.Credential): string =>
+ "hash" in addressOrCredential ? Credential.toBech32(addressOrCredential) : CoreAddress.toBech32(addressOrCredential)
+
+const is404 = (error: unknown): boolean =>
+ error instanceof HttpClientError.ResponseError && error.response.status === 404
+
+// ============================================================================
+// Script / datum resolution
+// ============================================================================
+// BCA's UtxoResponse carries inline_datum / reference_script_cbor directly
+// when available; only fall back to a separate lookup by hash for older
+// rows that predate those fields.
+
+const fetchScript =
+ (baseUrl: string, headers: Record | undefined) =>
+ (scriptHash: string): Effect.Effect =>
+ HttpUtils.get(`${baseUrl}/script/${scriptHash}`, BCA.Envelope(BCA.ScriptDetails), headers).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.map((response) => BCA.transformScript(response.details))
+ )
+
+const fetchDatumOption =
+ (baseUrl: string, headers: Record | undefined) =>
+ (datumHash: string): Effect.Effect =>
+ HttpUtils.get(`${baseUrl}/datum/${datumHash}`, BCA.Envelope(BCA.DatumDetails), headers).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.map((response) => BCA.inlineDatumFromCBORHex(response.details.bytes))
+ )
+
+const resolveUtxo =
+ (baseUrl: string, headers: Record | undefined) =>
+ (item: BCA.UtxoResponse): Effect.Effect => {
+ const scriptEffect =
+ item.reference_script_cbor && item.reference_script_type
+ ? Effect.succeed(BCA.transformScript({ type: item.reference_script_type, bytes: item.reference_script_cbor }))
+ : item.reference_script_hash
+ ? fetchScript(
+ baseUrl,
+ headers
+ )(item.reference_script_hash).pipe(Effect.catchAll(() => Effect.succeed(undefined)))
+ : Effect.succeed(undefined)
+
+ const datumEffect = item.inline_datum
+ ? Effect.succeed(BCA.inlineDatumFromCBORHex(item.inline_datum))
+ : item.datum_hash
+ ? fetchDatumOption(
+ baseUrl,
+ headers
+ )(item.datum_hash).pipe(Effect.catchAll(() => Effect.succeed(BCA.datumHashFromHex(item.datum_hash!))))
+ : Effect.succeed(undefined)
+
+ return Effect.all([scriptEffect, datumEffect]).pipe(
+ Effect.map(
+ ([scriptRef, datumOption]) =>
+ new CoreUTxO.UTxO({
+ transactionId: TransactionHash.fromHex(item.tx_hash),
+ index: BigInt(item.output_index),
+ address: CoreAddress.fromBech32(item.address),
+ assets: BCA.transformAssets(item.lovelace, item.assets),
+ datumOption,
+ scriptRef
+ })
+ )
+ )
+ }
+
+// ============================================================================
+// Paginated UTxO fetch (GET /utxos/{address}, GET /assets/{unit}/utxos)
+// ============================================================================
+
+const fetchAllUtxoPages = (url: string, headers: Record | undefined) =>
+ Effect.gen(function* () {
+ let all: Array = []
+ // BCA pages are 0-indexed (CIP-30 Paginate convention).
+ let page = 0
+ while (true) {
+ const response = yield* HttpUtils.get(
+ `${url}${url.includes("?") ? "&" : "?"}page=${page}&pagesize=${PAGE_SIZE}`,
+ BCA.PaginatedUtxoResponse,
+ headers
+ ).pipe(Effect.timeout(TIMEOUT))
+ all = all.concat(response.data)
+ if (!response.has_next) break
+ page += 1
+ }
+ return all
+ })
+
+// ============================================================================
+// Protocol parameters
+// ============================================================================
+
+export const getProtocolParameters = (baseUrl: string, token?: string) =>
+ HttpUtils.get(`${baseUrl}/protocol_parameters/latest`, BCA.ProtocolParametersResponse, bearerHeaders(token)).pipe(
+ Effect.map(BCA.transformProtocolParameters),
+ Effect.timeout(TIMEOUT),
+ Effect.catchAll(wrapError("getProtocolParameters"))
+ )
+
+// ============================================================================
+// UTxOs
+// ============================================================================
+
+export const getUtxos =
+ (baseUrl: string, token?: string) => (addressOrCredential: CoreAddress.Address | Credential.Credential) => {
+ const headers = bearerHeaders(token)
+ const addressPath = getAddressPath(addressOrCredential)
+ return fetchAllUtxoPages(`${baseUrl}/utxos/${addressPath}`, headers).pipe(
+ Effect.flatMap((utxos) => Effect.forEach(utxos, resolveUtxo(baseUrl, headers), { concurrency: 10 })),
+ Effect.catchAll(wrapError("getUtxos"))
+ )
+ }
+
+export const getUtxosWithUnit =
+ (baseUrl: string, token?: string) =>
+ (addressOrCredential: CoreAddress.Address | Credential.Credential, unit: string) => {
+ const headers = bearerHeaders(token)
+ const addressPath = getAddressPath(addressOrCredential)
+ return fetchAllUtxoPages(`${baseUrl}/utxos/${addressPath}`, headers).pipe(
+ Effect.map((utxos) => utxos.filter((utxo) => utxo.assets.some((a) => `${a.policy_id}${a.asset_name}` === unit))),
+ Effect.flatMap((utxos) => Effect.forEach(utxos, resolveUtxo(baseUrl, headers), { concurrency: 10 })),
+ Effect.catchAll(wrapError("getUtxosWithUnit"))
+ )
+ }
+
+export const getUtxoByUnit = (baseUrl: string, token?: string) => (unit: string) => {
+ const headers = bearerHeaders(token)
+ return fetchAllUtxoPages(`${baseUrl}/assets/${unit}/utxos`, headers).pipe(
+ Effect.flatMap((utxos) => {
+ if (utxos.length === 0) {
+ return Effect.fail(new Provider.ProviderError({ message: "No UTxO found for unit", cause: "Not found" }))
+ }
+ if (utxos.length > 1) {
+ return Effect.fail(
+ new Provider.ProviderError({
+ message: "Unit needs to be an NFT or only held by one address.",
+ cause: "Multiple UTxOs found"
+ })
+ )
+ }
+ return resolveUtxo(baseUrl, headers)(utxos[0])
+ }),
+ Effect.catchAll(wrapError("getUtxoByUnit"))
+ )
+}
+
+export const getUtxosByOutRef =
+ (baseUrl: string, token?: string) => (inputs: ReadonlyArray) => {
+ const headers = bearerHeaders(token)
+
+ return Effect.forEach(
+ inputs,
+ (input) => {
+ const txHash = TransactionHash.toHex(input.transactionId)
+ return HttpUtils.get(`${baseUrl}/utxo/${txHash}/${Number(input.index)}`, BCA.UtxoResponse, headers).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.flatMap(resolveUtxo(baseUrl, headers)),
+ Effect.map((utxo) => Option.some(utxo)),
+ Effect.catchIf(is404, () => Effect.succeed(Option.none())),
+ Effect.catchAll(wrapError("getUtxosByOutRef"))
+ )
+ },
+ { concurrency: 10 }
+ ).pipe(Effect.map((results) => results.filter(Option.isSome).map((result) => result.value)))
+ }
+
+// ============================================================================
+// Delegation
+// ============================================================================
+
+export const getDelegation = (baseUrl: string, token?: string) => (rewardAddress: RewardAddress.RewardAddress) =>
+ HttpUtils.get(`${baseUrl}/staking/${rewardAddress}`, BCA.StakingResponse, bearerHeaders(token)).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.map(BCA.transformDelegation),
+ // 404 — stake address not registered/never delegated.
+ Effect.catchIf(is404, () => Effect.succeed({ poolId: null, rewards: 0n } as Provider.Delegation)),
+ Effect.catchAll(wrapError("getDelegation"))
+ )
+
+// ============================================================================
+// Datum
+// ============================================================================
+
+export const getDatum = (baseUrl: string, token?: string) => (datumHash: DatumHash.DatumHash) => {
+ const datumHashHex = Bytes.toHex(datumHash.hash)
+ return HttpUtils.get(`${baseUrl}/datum/${datumHashHex}`, BCA.Envelope(BCA.DatumDetails), bearerHeaders(token)).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.flatMap((response) =>
+ Effect.try({
+ try: () => Schema.decodeSync(PlutusData.FromCBORHex())(response.details.bytes),
+ catch: (error) => new Provider.ProviderError({ message: "Failed to parse datum CBOR", cause: error })
+ })
+ ),
+ Effect.catchAll(wrapError("getDatum"))
+ )
+}
+
+// ============================================================================
+// awaitTx
+// ============================================================================
+
+export const awaitTx =
+ (baseUrl: string, token?: string) =>
+ (txHash: TransactionHash.TransactionHash, checkInterval = 20_000, timeout = 160_000) => {
+ const txHashHex = TransactionHash.toHex(txHash)
+ const headers = bearerHeaders(token)
+
+ // GET /tx/{hash} 404s until the tx is indexed, 200s once confirmed.
+ const checkTx = HttpUtils.get(`${baseUrl}/tx/${txHashHex}`, BCA.TransactionResponse, headers).pipe(
+ Effect.timeout(TIMEOUT)
+ )
+
+ return Effect.retry(checkTx, Schedule.spaced(`${checkInterval} millis`)).pipe(
+ Effect.timeout(timeout),
+ Effect.as(true),
+ Effect.catchAllCause((cause) =>
+ Effect.fail(new Provider.ProviderError({ cause, message: "BlockchainApplied awaitTx failed" }))
+ )
+ )
+ }
+
+// ============================================================================
+// submitTx / evaluateTx
+// ============================================================================
+
+export const submitTx = (baseUrl: string, token?: string) => (tx: Transaction.Transaction) =>
+ HttpUtils.postUint8Array(
+ `${baseUrl}/tx/submit`,
+ Transaction.toCBORBytes(tx),
+ BCA.SubmitResponse,
+ bearerHeaders(token)
+ ).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.map((response) => TransactionHash.fromHex(response.tx_hash)),
+ Effect.catchAll(wrapError("submitTx"))
+ )
+
+const toAdditionalUtxo = (utxo: CoreUTxO.UTxO): BCA.UtxoResponse => ({
+ tx_hash: TransactionHash.toHex(utxo.transactionId),
+ output_index: Number(utxo.index),
+ address: CoreAddress.toBech32(utxo.address),
+ lovelace: utxo.assets.lovelace.toString(),
+ assets: CoreAssets.flatten(utxo.assets)
+ .filter(([, , quantity]) => quantity !== 0n)
+ .map(([policyId, assetName, quantity]) => ({
+ policy_id: PolicyId.toHex(policyId),
+ asset_name: AssetName.toHex(assetName),
+ quantity: quantity.toString()
+ })),
+ datum_hash: utxo.datumOption?._tag === "DatumHash" ? Bytes.toHex(utxo.datumOption.hash) : undefined,
+ inline_datum: utxo.datumOption?._tag === "InlineDatum" ? PlutusData.toCBORHex(utxo.datumOption.data) : undefined,
+ ...toReferenceScriptFields(utxo.scriptRef)
+})
+
+const toReferenceScriptFields = (
+ script: Script.Script | undefined
+): { reference_script_cbor: string; reference_script_type: string } | Record => {
+ if (!script) return {}
+ switch (script._tag) {
+ case "PlutusV1":
+ return { reference_script_cbor: Bytes.toHex(script.bytes), reference_script_type: "plutusV1" }
+ case "PlutusV2":
+ return { reference_script_cbor: Bytes.toHex(script.bytes), reference_script_type: "plutusV2" }
+ case "PlutusV3":
+ return { reference_script_cbor: Bytes.toHex(script.bytes), reference_script_type: "plutusV3" }
+ case "NativeScript":
+ return { reference_script_cbor: NativeScripts.toCBORHex(script), reference_script_type: "timelock" }
+ }
+}
+
+export const evaluateTx =
+ (baseUrl: string, token?: string) => (tx: Transaction.Transaction, additionalUTxOs?: Array) => {
+ const body = {
+ cbor: Transaction.toCBORHex(tx),
+ ...(additionalUTxOs && additionalUTxOs.length > 0
+ ? { additional_utxos: additionalUTxOs.map(toAdditionalUtxo) }
+ : {})
+ }
+
+ return HttpUtils.postJson(
+ `${baseUrl}/tx/evaluate`,
+ body,
+ Schema.Array(BCA.EvalRedeemerResponse),
+ bearerHeaders(token)
+ ).pipe(
+ Effect.timeout(TIMEOUT),
+ Effect.map((response) => response.map(BCA.transformEvalRedeemer)),
+ Effect.catchAll(wrapError("evaluateTx"))
+ )
+ }
diff --git a/packages/evolution/test/provider/conformance.ts b/packages/evolution/test/provider/conformance.ts
index c37ab214..1e1ed935 100644
--- a/packages/evolution/test/provider/conformance.ts
+++ b/packages/evolution/test/provider/conformance.ts
@@ -17,7 +17,7 @@ import {
preprodScriptAddress,
preprodStakeAddress,
preprodTxHash,
- previewTxHash,
+ previewTxHash
} from "./fixtures/constants.js"
import { evalSample1, evalSample2, evalSample3, evalSample4 } from "./fixtures/evaluateTx.js"
@@ -126,9 +126,7 @@ export function registerConformanceTests(factory: () => Provider, options?: Conf
})
it("awaitTx rejects for preview-only tx on preprod", { timeout: 10_000 }, async () => {
- await expect(provider.awaitTx(previewTxHash(), 1000, 5000)).rejects.toThrow(
- "awaitTx failed"
- )
+ await expect(provider.awaitTx(previewTxHash(), 1000, 5000)).rejects.toThrow("awaitTx failed")
})
// submitTx and evaluateTx require a valid signed CBOR tx — skipped until we have tx fixtures
diff --git a/packages/evolution/test/provider/providers.test.ts b/packages/evolution/test/provider/providers.test.ts
index 311a8a32..424dcfc7 100644
--- a/packages/evolution/test/provider/providers.test.ts
+++ b/packages/evolution/test/provider/providers.test.ts
@@ -10,6 +10,7 @@
*/
import { describe, expect, it } from "vitest"
+import { BlockchainAppliedProvider } from "../../src/sdk/provider/BlockchainApplied.js"
import { BlockfrostProvider } from "../../src/sdk/provider/Blockfrost.js"
import { Koios } from "../../src/sdk/provider/Koios.js"
import { KupmiosProvider } from "../../src/sdk/provider/Kupmios.js"
@@ -35,6 +36,8 @@ const BLOCKFROST_URL = process.env.BLOCKFROST_PREPROD_URL ?? "https://cardano-pr
const BLOCKFROST_KEY = process.env.BLOCKFROST_PREPROD_KEY
const MAESTRO_URL = process.env.MAESTRO_PREPROD_URL ?? "https://preprod.gomaestro-api.org/v1"
const MAESTRO_KEY = process.env.MAESTRO_PREPROD_KEY
+const BCA_URL = process.env.BCA_PREPROD_URL ?? "https://staging.blockchain-applied.com/api_preprod/v1"
+const BCA_KEY = process.env.BCA_PREPROD_KEY
const KUPO_URL = process.env.KUPMIOS_KUPO_URL
const OGMIOS_URL = process.env.KUPMIOS_OGMIOS_URL
const KUPMIOS_KUPO_KEY = process.env.KUPMIOS_KUPO_KEY
@@ -63,7 +66,9 @@ describe.skipIf(!process.env.KOIOS_PREVIEW_ENABLED)("Koios (preview)", () => {
// ── Blockfrost ────────────────────────────────────────────────────────────────
describe.skipIf(!isConfigured(BLOCKFROST_KEY, "preprodXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))("Blockfrost", () => {
- registerConformanceTests(() => new BlockfrostProvider(BLOCKFROST_URL, BLOCKFROST_KEY), { supportsCredentialQueries: true })
+ registerConformanceTests(() => new BlockfrostProvider(BLOCKFROST_URL, BLOCKFROST_KEY), {
+ supportsCredentialQueries: true
+ })
})
// ── Maestro ───────────────────────────────────────────────────────────────────
@@ -71,10 +76,14 @@ describe.skipIf(!isConfigured(MAESTRO_KEY, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))(
registerConformanceTests(() => new MaestroProvider(MAESTRO_URL, MAESTRO_KEY!))
})
+// ── Blockchain Applied (BCA) ──────────────────────────────────────────────────
+describe.skipIf(!isConfigured(BCA_KEY, "bca_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"))("BlockchainApplied", () => {
+ registerConformanceTests(() => new BlockchainAppliedProvider(BCA_URL, BCA_KEY))
+})
+
// ── Kupmios (self-hosted or Demeter) ──────────────────────────────────────────
describe.skipIf(
- !isConfigured(KUPO_URL, "https://your-kupo-endpoint") ||
- !isConfigured(OGMIOS_URL, "https://your-ogmios-endpoint")
+ !isConfigured(KUPO_URL, "https://your-kupo-endpoint") || !isConfigured(OGMIOS_URL, "https://your-ogmios-endpoint")
)("Kupmios", () => {
registerConformanceTests(
() =>