diff --git a/.changeset/blockchain-applied-provider.md b/.changeset/blockchain-applied-provider.md
new file mode 100644
index 000000000..d5a2c3bbc
--- /dev/null
+++ b/.changeset/blockchain-applied-provider.md
@@ -0,0 +1,5 @@
+---
+"@evolution-sdk/evolution": patch
+---
+
+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 f9c35650f..52289af96 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/README.md b/README.md
index cfec5d487..0ab8e09df 100644
--- a/README.md
+++ b/README.md
@@ -280,6 +280,7 @@ Join our thriving community of Cardano developers:
- [x] `Blockfrost` - Blockfrost API provider
- [x] `Koios` - Koios API provider
- [x] `KupoOgmios` - Kupo/Ogmios provider
+ - [x] `BlockchainApplied` - Blockchain Applied API provider
- [ ] `UtxoRpc` - UTXO RPC provider
- [x] Provider abstraction layer
- [ ] Failover and load balancing
@@ -333,7 +334,7 @@ Join our thriving community of Cardano developers:
- [x] Schema types from Plutus blueprint types (codegen)
### Current Focus
-The core SDK covers transaction building, smart contracts (Plutus V1/V2/V3), providers (Blockfrost, Maestro, Koios, Kupo/Ogmios), CIP-30 wallet integration, Conway governance, and staking. Current priorities include additional coin selection algorithms, UTXO RPC provider support, hardware wallet integration, and Hydra integration.
+The core SDK covers transaction building, smart contracts (Plutus V1/V2/V3), providers (Blockfrost, Maestro, Koios, Kupo/Ogmios, Blockchain Applied), CIP-30 wallet integration, Conway governance, and staking. Current priorities include additional coin selection algorithms, UTXO RPC provider support, hardware wallet integration, and Hydra integration.
## Contributing
diff --git a/docs/content/docs/advanced/custom-providers.mdx b/docs/content/docs/advanced/custom-providers.mdx
index 4db41aabb..905a9b13a 100644
--- a/docs/content/docs/advanced/custom-providers.mdx
+++ b/docs/content/docs/advanced/custom-providers.mdx
@@ -6,12 +6,12 @@ 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 +32,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 | Hosted REST API, bearer token |
## Using a Provider
@@ -57,6 +58,13 @@ const kupmiosClient = Client.make(preprod)
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/docs/content/docs/providers/index.mdx b/docs/content/docs/providers/index.mdx
index 79ae75be5..ade84792d 100644
--- a/docs/content/docs/providers/index.mdx
+++ b/docs/content/docs/providers/index.mdx
@@ -41,12 +41,13 @@ const txHash = await client.submitTx(signedTx)
## Available Providers
-| Provider | Infrastructure | Best For |
-| -------------- | -------------- | ---------------------------------- |
-| **Blockfrost** | Hosted API | Production apps, rapid development |
-| **Kupmios** | Ogmios + Kupo | Self-hosted, full control |
-| **Maestro** | Hosted API | Advanced features, analytics |
-| **Koios** | Community API | Decentralized infrastructure |
+| Provider | Infrastructure | Best For |
+| ---------------------- | -------------- | ---------------------------------- |
+| **Blockfrost** | Hosted API | Production apps, rapid development |
+| **Kupmios** | Ogmios + Kupo | Self-hosted, full control |
+| **Maestro** | Hosted API | Advanced features, analytics |
+| **Koios** | Community API | Decentralized infrastructure |
+| **Blockchain Applied** | Hosted API | Hosted REST API, bearer token |
See [Provider Types](/docs/providers/provider-types) for detailed comparison and configuration.
@@ -88,7 +89,7 @@ Understanding the difference helps choose the right architecture:
- Compare Blockfrost, Kupmios, Maestro, and Koios configurations
+ Compare Blockfrost, Kupmios, Maestro, Koios, and Blockchain Applied configurations
Learn to use provider without wallet for generic queries
diff --git a/docs/content/docs/providers/provider-types.mdx b/docs/content/docs/providers/provider-types.mdx
index 3abac0dbb..186f5b2f9 100644
--- a/docs/content/docs/providers/provider-types.mdx
+++ b/docs/content/docs/providers/provider-types.mdx
@@ -1,11 +1,11 @@
---
title: Provider Types
-description: Compare and configure Blockfrost, Kupmios, Maestro, and Koios providers
+description: Compare and configure Blockfrost, Kupmios, Maestro, Koios, and Blockchain Applied providers
---
# Provider Types
-The Evolution SDK supports four provider types, each connecting to different Cardano infrastructure. Choose based on your deployment model, feature requirements, and infrastructure preferences.
+The Evolution SDK supports five provider types, each connecting to different Cardano infrastructure. Choose based on your deployment model, feature requirements, and infrastructure preferences.
## Blockfrost
@@ -276,19 +276,65 @@ Applications prioritizing decentralization, community infrastructure, or wanting
---
+## Blockchain Applied
+
+Hosted REST API by [Blockchain Applied](https://blockchain-applied.com), backed by cardano-db-sync and a Cardano node.
+
+### Configuration
+
+```typescript twoslash
+import { mainnet, Client } from "@evolution-sdk/evolution"
+
+const client = Client.make(mainnet)
+ .withBlockchainApplied({
+ baseUrl: "https://api.blockchain-applied.com/api_ada/v1",
+ token: process.env.BCA_API_KEY!
+ })
+```
+
+### Network Endpoints
+
+```typescript
+// Mainnet
+baseUrl: "https://api.blockchain-applied.com/api_ada/v1"
+
+// Preprod testnet
+baseUrl: "https://api.blockchain-applied.com/api_preprod/v1"
+
+// Preview testnet
+baseUrl: "https://api.blockchain-applied.com/api_preview/v1"
+```
+
+### Best For
+
+Applications that want a hosted REST API with a bearer token.
+
+**Advantages:**
+
+- Simple setup
+- UTxO queries by address or payment credential
+- Transaction submission and script evaluation included
+
+**Considerations:**
+
+- API token required for every endpoint
+- Rate limits per token
+
+---
+
## Provider Comparison Matrix
-| Feature | Blockfrost | Kupmios | Maestro | Koios |
-| --------------------- | ---------------- | -------------------- | ---------- | ---------------- |
-| **Setup Complexity** | Low | High | Low | Low |
-| **Infrastructure** | Hosted | Self-hosted | Hosted | Community-hosted |
-| **API Key Required** | Yes | No | Yes | No |
-| **Rate Limits** | Yes | No | Yes | Varies |
-| **Cost** | Free tier + paid | Infrastructure costs | Paid tiers | Free |
-| **Data Sovereignty** | No | Yes | No | No |
-| **Decentralization** | Low | High | Low | Medium |
-| **Advanced Features** | Standard | Full control | Extended | Standard |
-| **Uptime SLA** | Commercial | Self-managed | Commercial | Best effort |
+| Feature | Blockfrost | Kupmios | Maestro | Koios | Blockchain Applied |
+| --------------------- | ---------------- | -------------------- | ---------- | ---------------- | ------------------ |
+| **Setup Complexity** | Low | High | Low | Low | Low |
+| **Infrastructure** | Hosted | Self-hosted | Hosted | Community-hosted | Hosted |
+| **API Key Required** | Yes | No | Yes | No | Yes |
+| **Rate Limits** | Yes | No | Yes | Varies | Yes |
+| **Cost** | Free tier + paid | Infrastructure costs | Paid tiers | Free | Paid tiers |
+| **Data Sovereignty** | No | Yes | No | No | No |
+| **Decentralization** | Low | High | Low | Medium | Low |
+| **Advanced Features** | Standard | Full control | Extended | Standard | Standard |
+| **Uptime SLA** | Commercial | Self-managed | Commercial | Best effort | Commercial |
## Switching Providers
diff --git a/examples/with-vite-react/README.md b/examples/with-vite-react/README.md
index 774e5b1e2..afd578668 100644
--- a/examples/with-vite-react/README.md
+++ b/examples/with-vite-react/README.md
@@ -130,6 +130,16 @@ const tx = await Client.make(chain)
`pnpm dev` serves the API from the Vite dev server. In production, `server/index.ts` serves it with
the built app. The API is public, so add rate limiting or an origin check before deploying it.
+To use the Blockchain Applied (BCA) provider instead, swap the provider on the server:
+
+```typescript
+// Server (server/payments.ts): the BCA provider, with the token
+const tx = await Client.make(chain)
+ .withBlockchainApplied({ baseUrl: "https://api.blockchain-applied.com/api_preprod/v1", token: process.env.BCA_TOKEN })
+ .withAddress(from)
+ // ...
+```
+
## Development
### Building for Production
diff --git a/packages/evolution/src/sdk/client/Client.ts b/packages/evolution/src/sdk/client/Client.ts
index d986405cf..850538dd9 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 378a6c0ec..46cae6950 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
}
}
@@ -155,6 +157,7 @@ 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)),
+ 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)),
diff --git a/packages/evolution/src/sdk/client/internal/Providers.ts b/packages/evolution/src/sdk/client/internal/Providers.ts
index 2cabf6977..8b9097c73 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 000000000..fb7f97c98
--- /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.
+ * Every endpoint requires a Bearer token.
+ *
+ * @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 000000000..500477c06
--- /dev/null
+++ b/packages/evolution/src/sdk/provider/internal/BlockchainApplied.ts
@@ -0,0 +1,257 @@
+/**
+ * @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.
+ *
+ * HttpUtils hands every JSON number to the schemas as its source text, so integer fields
+ * decode with `Schema.BigInt` (from a bare number or a digit string) and decimal fields
+ * with `Schema.NumberFromString`.
+ */
+
+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) 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.BigInt,
+ page_size: Schema.BigInt,
+ total_count: Schema.optional(Schema.BigInt),
+ total_pages: Schema.optional(Schema.BigInt),
+ 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.BigInt
+})
+export interface UtxoAsset extends Schema.Schema.Type {}
+
+export const UtxoResponse = Schema.Struct({
+ tx_hash: Schema.String,
+ output_index: Schema.BigInt,
+ address: Schema.String,
+ lovelace: Schema.BigInt,
+ 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}) — awaitTx only needs to know the tx is indexed.
+// ============================================================================
+
+export const TxHashResponse = Schema.Struct({
+ hash: Schema.String
+})
+
+// ============================================================================
+// 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 {}
+
+// ============================================================================
+// 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.BigInt,
+ total_withdrawn: Schema.BigInt,
+ withdrawable_rewards: Schema.BigInt,
+ active_epoch: Schema.optional(Schema.NullOr(Schema.BigInt)),
+ 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.BigInt,
+ min_fee_a: Schema.BigInt,
+ min_fee_b: Schema.BigInt,
+ max_tx_size: Schema.BigInt,
+ max_val_size: Schema.BigInt,
+ key_deposit: Schema.BigInt,
+ pool_deposit: Schema.BigInt,
+ drep_deposit: Schema.BigInt,
+ gov_action_deposit: Schema.BigInt,
+ price_mem: Schema.NumberFromString,
+ price_step: Schema.NumberFromString,
+ max_tx_ex_mem: Schema.BigInt,
+ max_tx_ex_steps: Schema.BigInt,
+ coins_per_utxo_size: Schema.BigInt,
+ collateral_percentage: Schema.BigInt,
+ max_collateral_inputs: Schema.BigInt,
+ min_fee_ref_script_cost_per_byte: Schema.NumberFromString,
+ cost_models: Schema.Struct({
+ PlutusV1: Schema.Array(Schema.BigInt),
+ PlutusV2: Schema.Array(Schema.BigInt),
+ PlutusV3: Schema.Array(Schema.BigInt)
+ })
+})
+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.BigInt,
+ steps: Schema.BigInt
+})
+
+export const EvalRedeemerResponse = Schema.Struct({
+ redeemer_tag: Redeemer.RedeemerTag,
+ redeemer_index: Schema.BigInt,
+ ex_units: ExUnits
+})
+
+/**
+ * A UTxO as sent in the `additional_utxos` of POST /tx/evaluate
+ */
+export interface AdditionalUtxo {
+ readonly tx_hash: string
+ readonly output_index: number
+ readonly address: string
+ readonly lovelace: string
+ readonly assets: ReadonlyArray<{ readonly policy_id: string; readonly asset_name: string; readonly quantity: string }>
+ readonly datum_hash?: string
+ readonly inline_datum?: string
+ readonly reference_script_cbor?: string
+ readonly reference_script_type?: string
+}
+
+// ============================================================================
+// Transformation utilities
+// ============================================================================
+
+const toCostModel = (costs: ReadonlyArray): Record =>
+ Object.fromEntries(costs.map((value, index) => [index.toString(), Number(value)]))
+
+export const transformProtocolParameters = (response: ProtocolParametersResponse): Provider.ProtocolParameters => ({
+ minFeeA: Number(response.min_fee_a),
+ minFeeB: Number(response.min_fee_b),
+ maxTxSize: Number(response.max_tx_size),
+ maxValSize: Number(response.max_val_size),
+ keyDeposit: response.key_deposit,
+ poolDeposit: response.pool_deposit,
+ drepDeposit: response.drep_deposit,
+ govActionDeposit: response.gov_action_deposit,
+ priceMem: response.price_mem,
+ priceStep: response.price_step,
+ maxTxExMem: response.max_tx_ex_mem,
+ maxTxExSteps: response.max_tx_ex_steps,
+ coinsPerUtxoByte: response.coins_per_utxo_size,
+ collateralPercentage: Number(response.collateral_percentage),
+ maxCollateralInputs: Number(response.max_collateral_inputs),
+ minFeeRefScriptCostPerByte: response.min_fee_ref_script_cost_per_byte,
+ costModels: {
+ PlutusV1: toCostModel(response.cost_models.PlutusV1),
+ PlutusV2: toCostModel(response.cost_models.PlutusV2),
+ PlutusV3: toCostModel(response.cost_models.PlutusV3)
+ }
+})
+
+export const transformAssets = (lovelace: bigint, assets: ReadonlyArray): CoreAssets.Assets => {
+ let result = CoreAssets.fromLovelace(lovelace)
+ for (const asset of assets) {
+ result = CoreAssets.addByHex(result, asset.policy_id, asset.asset_name, 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: response.withdrawable_rewards
+})
+
+export const transformEvalRedeemer = (redeemer: Schema.Schema.Type): EvalRedeemer => ({
+ redeemer_tag: redeemer.redeemer_tag,
+ redeemer_index: Number(redeemer.redeemer_index),
+ ex_units: new Redeemer.ExUnits({
+ mem: redeemer.ex_units.mem,
+ steps: 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 000000000..341d3b6c5
--- /dev/null
+++ b/packages/evolution/src/sdk/provider/internal/BlockchainAppliedEffect.ts
@@ -0,0 +1,325 @@
+/**
+ * @fileoverview Effect-based Blockchain Applied (BCA) provider functions.
+ * Internal module implementing all provider operations using the Effect pattern.
+ */
+
+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 * as NativeScripts from "../../../NativeScripts.js"
+import * as OriginalBytes from "../../../OriginalBytes.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 => ({ Authorization: `Bearer ${token}` })
+
+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 HttpUtils.HttpResponseError && error.status === 404
+
+// ============================================================================
+// UTxO resolution
+// ============================================================================
+// BCA's UtxoResponse carries the inline datum and the reference script CBOR
+// (rebuilt server-side for native scripts, which db-sync stores no CBOR for),
+// so no follow-up lookups are needed.
+
+const decodeScriptRef = (item: BCA.UtxoResponse): Effect.Effect => {
+ if (!item.reference_script_hash && !item.reference_script_cbor) return Effect.succeed(undefined)
+ const { reference_script_cbor: bytes, reference_script_type: type } = item
+ return Effect.try({
+ try: () => (bytes && type ? BCA.transformScript({ type, bytes }) : undefined),
+ catch: (cause) => new Provider.ProviderError({ message: "Failed to decode reference script", cause })
+ }).pipe(
+ // Without its reference script the UTxO would be left out of the reference
+ // script fee, and the node would reject the transaction.
+ Effect.filterOrFail(
+ (script): script is Script.Script => script !== undefined,
+ () =>
+ new Provider.ProviderError({
+ message: `No usable reference script for ${item.tx_hash}#${item.output_index}`,
+ cause: { reference_script_hash: item.reference_script_hash, reference_script_type: type }
+ })
+ )
+ )
+}
+
+const resolveUtxo = (item: BCA.UtxoResponse): Effect.Effect =>
+ decodeScriptRef(item).pipe(
+ Effect.flatMap((scriptRef) =>
+ Effect.try({
+ try: () =>
+ new CoreUTxO.UTxO({
+ transactionId: TransactionHash.fromHex(item.tx_hash),
+ index: item.output_index,
+ address: CoreAddress.fromBech32(item.address),
+ assets: BCA.transformAssets(item.lovelace, item.assets),
+ datumOption: item.inline_datum
+ ? BCA.inlineDatumFromCBORHex(item.inline_datum)
+ : item.datum_hash
+ ? BCA.datumHashFromHex(item.datum_hash)
+ : undefined,
+ scriptRef
+ }),
+ catch: (cause) => new Provider.ProviderError({ message: "Failed to decode UTxO", cause })
+ })
+ )
+ )
+
+// ============================================================================
+// Paginated UTxO fetch (GET /utxos/{address}, GET /assets/{unit}/utxos)
+// ============================================================================
+
+const fetchAllUtxoPages = (url: string, headers: Record) =>
+ 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)),
+ 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)),
+ 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(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),
+ 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 })),
+ 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: () => {
+ // Keep the fetched bytes: a witness set writes the datum with them
+ const bytes = Bytes.fromHex(response.details.bytes)
+ const data = PlutusData.fromCBORBytes(bytes)
+ return typeof data === "bigint" ? data : OriginalBytes.record(data, 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. Any
+ // other failure (e.g. a 401) ends the wait at once.
+ const checkTx = HttpUtils.get(`${baseUrl}/tx/${txHashHex}`, BCA.TxHashResponse, headers).pipe(
+ Effect.timeout(TIMEOUT)
+ )
+
+ return Effect.retry(checkTx, { schedule: Schedule.spaced(`${checkInterval} millis`), while: is404 }).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.AdditionalUtxo => ({
+ 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/BlockchainApplied.test.ts b/packages/evolution/test/provider/BlockchainApplied.test.ts
new file mode 100644
index 000000000..c5be272d2
--- /dev/null
+++ b/packages/evolution/test/provider/BlockchainApplied.test.ts
@@ -0,0 +1,420 @@
+/**
+ * Offline tests for the Blockchain Applied (BCA) provider. `fetch` is stubbed with response
+ * bodies recorded from the BCA preprod API (cost models shortened, `/tx` trimmed to its header
+ * fields), so no test needs a BCA token.
+ */
+import { Effect } from "effect"
+import { afterEach, describe, expect, it, vi } from "vitest"
+
+import * as Address from "../../src/Address.js"
+import * as Assets from "../../src/Assets.js"
+import * as Bytes from "../../src/Bytes.js"
+import * as Credential from "../../src/Credential.js"
+import * as Data from "../../src/Data.js"
+import * as DatumHash from "../../src/DatumHash.js"
+import * as InlineDatum from "../../src/InlineDatum.js"
+import * as KeyHash from "../../src/KeyHash.js"
+import * as NativeScripts from "../../src/NativeScripts.js"
+import * as PoolKeyHash from "../../src/PoolKeyHash.js"
+import * as BlockchainAppliedEffect from "../../src/sdk/provider/internal/BlockchainAppliedEffect.js"
+import * as Transaction from "../../src/Transaction.js"
+import * as TransactionHash from "../../src/TransactionHash.js"
+import * as TransactionInput from "../../src/TransactionInput.js"
+import {
+ PREPROD_ADDRESS_BECH32,
+ PREPROD_DATUM_HASH_HEX,
+ PREPROD_STAKE_ADDRESS_BECH32,
+ PREPROD_TX_HASH_HEX,
+ PREPROD_UNIT
+} from "./fixtures/constants.js"
+import { evalSample1 } from "./fixtures/evaluateTx.js"
+
+const BASE_URL = "https://api.blockchain-applied.com/api_preprod/v1"
+const TOKEN = "test-token"
+
+type Route = string | { readonly status: number; readonly body: string }
+
+/**
+ * Route each request URL to a raw JSON body, so numbers reach the parser exactly as written.
+ * An array answers successive requests in order (the last entry repeats).
+ */
+const stubFetch = (routes: Record>) => {
+ const served: Record = {}
+ const fetchMock = vi.fn((url: string, _init?: RequestInit) => {
+ const entry = routes[url]
+ const route = Array.isArray(entry) ? entry[Math.min((served[url] = (served[url] ?? -1) + 1), entry.length - 1)] : entry
+ if (route === undefined) return Promise.resolve(new Response('{"error":"not found"}', { status: 404 }))
+ return Promise.resolve(
+ typeof route === "string" ? new Response(route) : new Response(route.body, { status: route.status })
+ )
+ })
+ vi.stubGlobal("fetch", fetchMock)
+ return fetchMock
+}
+
+const page = (url: string, n: number) => `${url}?page=${n}&pagesize=100`
+
+const fail = (effect: Effect.Effect) => Effect.runPromise(Effect.flip(effect))
+
+afterEach(() => {
+ vi.unstubAllGlobals()
+})
+
+// Recorded from GET /utxos/{PREPROD_ADDRESS_BECH32}
+const UTXO_ADA_ONLY = `{"tx_hash":"2c8f45e2aa58c7bb25739da28786cce97a97cd346107bb372807fabde323ab96","output_index":1,"address":"${PREPROD_ADDRESS_BECH32}","lovelace":"2186901580","assets":[],"datum_hash":null,"reference_script_hash":null,"inline_datum":null,"reference_script_cbor":null,"reference_script_type":null}`
+const UTXO_TOKENS = `{"tx_hash":"e0568183b00d1a7625a97ba1792a7506aafc96719c0643fbb021ae75c9d815a4","output_index":1,"address":"${PREPROD_ADDRESS_BECH32}","lovelace":"2425818","assets":[{"policy_id":"2b172b4b0141058c3a5ee855cfa0f52f85808e3d3aba47f5a44cbd5e","asset_name":"69455448","quantity":"1"},{"policy_id":"d088875616229b83868ac5596e77288d8b5c9dffc2f748d8df6332b6","asset_name":"74494e4459","quantity":"9997000000"}],"datum_hash":null,"reference_script_hash":null,"inline_datum":null,"reference_script_cbor":null,"reference_script_type":null}`
+const TOKEN_UNIT = "d088875616229b83868ac5596e77288d8b5c9dffc2f748d8df6332b674494e4459"
+
+// Recorded from GET /utxo/{PREPROD_TX_HASH_HEX}/0: an inline datum, whose hash is set too
+const INLINE_DATUM_CBOR = "d8799fd8799f1a001a8ea5ff1b0000018ea6888e58ff"
+const UTXO_INLINE_DATUM = `{"tx_hash":"${PREPROD_TX_HASH_HEX}","output_index":0,"address":"addr_test1wz7uytdxstxe4nhdtl2gj9rcnlyce99tc707mz6qewxyx9qac0urr","lovelace":"2000000","assets":[{"policy_id":"ec0f50624331d0df45f40ab0e6081e5f1ff577c5c4dc767079d4fe59","asset_name":"695553443230323430343033313833343534","quantity":"1"}],"datum_hash":"${PREPROD_DATUM_HASH_HEX}","reference_script_hash":null,"inline_datum":"${INLINE_DATUM_CBOR}","reference_script_cbor":null,"reference_script_type":null}`
+// The same output with only a datum hash
+const UTXO_DATUM_HASH = UTXO_INLINE_DATUM.replace(`"inline_datum":"${INLINE_DATUM_CBOR}"`, '"inline_datum":null')
+
+// A native reference script, its CBOR rebuilt by the API from db-sync's script JSON
+const NATIVE_SCRIPT = NativeScripts.makeScriptPubKey(Bytes.fromHex("e09d36c79dec9bd1b3d9e152247701cd0bb860b5ebfd1de8abb6735a"))
+const NATIVE_SCRIPT_HASH = "208bdcaf2d83ae026964e23659c703a377473168a39cbdc2b0241115"
+const UTXO_NATIVE_SCRIPT = UTXO_ADA_ONLY.replace(
+ '"reference_script_hash":null',
+ `"reference_script_hash":"${NATIVE_SCRIPT_HASH}"`
+)
+ .replace('"reference_script_cbor":null', `"reference_script_cbor":"${NativeScripts.toCBORHex(NATIVE_SCRIPT)}"`)
+ .replace('"reference_script_type":null', '"reference_script_type":"timelock"')
+// A reference script the API returned no CBOR for
+const UTXO_SCRIPT_NO_CBOR = UTXO_NATIVE_SCRIPT.replace(/"reference_script_cbor":"[0-9a-f]+"/, '"reference_script_cbor":null')
+
+const paged = (items: ReadonlyArray, n: number, hasNext: boolean) =>
+ `{"data":[${items.join(",")}],"page":${n},"page_size":100,"has_next":${hasNext}}`
+
+describe("BlockchainApplied getProtocolParameters", () => {
+ // Recorded from preprod epoch 321, cost models shortened
+ const body = `{"epoch_no":321,"min_fee_a":44,"min_fee_b":155381,"max_tx_size":16384,"max_val_size":5000,"key_deposit":"2000000","pool_deposit":"500000000","drep_deposit":"500000000","gov_action_deposit":"1000000000","price_mem":0.0577,"price_step":0.0000721,"max_tx_ex_mem":"17500000","max_tx_ex_steps":"18446744073709551615","coins_per_utxo_size":"4310","collateral_percentage":150,"max_collateral_inputs":3,"min_fee_ref_script_cost_per_byte":15,"cost_models":{"PlutusV1":[100788,420,1,1],"PlutusV2":[100788,420,1,1,1000],"PlutusV3":[100788,420,1,1,1000,173]}}`
+ const url = `${BASE_URL}/protocol_parameters/latest`
+
+ it("decodes the parameters and keeps the cost model order", async () => {
+ stubFetch({ [url]: body })
+
+ const params = await Effect.runPromise(BlockchainAppliedEffect.getProtocolParameters(BASE_URL, TOKEN))
+
+ expect(params).toEqual({
+ minFeeA: 44,
+ minFeeB: 155381,
+ maxTxSize: 16384,
+ maxValSize: 5000,
+ keyDeposit: 2000000n,
+ poolDeposit: 500000000n,
+ drepDeposit: 500000000n,
+ govActionDeposit: 1000000000n,
+ priceMem: 0.0577,
+ priceStep: 0.0000721,
+ maxTxExMem: 17500000n,
+ maxTxExSteps: 18446744073709551615n,
+ coinsPerUtxoByte: 4310n,
+ collateralPercentage: 150,
+ maxCollateralInputs: 3,
+ minFeeRefScriptCostPerByte: 15,
+ costModels: {
+ PlutusV1: { 0: 100788, 1: 420, 2: 1, 3: 1 },
+ PlutusV2: { 0: 100788, 1: 420, 2: 1, 3: 1, 4: 1000 },
+ PlutusV3: { 0: 100788, 1: 420, 2: 1, 3: 1, 4: 1000, 5: 173 }
+ }
+ })
+ })
+
+ it("fails when coins_per_utxo_size is missing", async () => {
+ stubFetch({ [url]: body.replace('"coins_per_utxo_size":"4310",', "") })
+
+ const error = await fail(BlockchainAppliedEffect.getProtocolParameters(BASE_URL, TOKEN))
+
+ expect(error._tag).toBe("ProviderError")
+ })
+
+ it("fails when min_fee_ref_script_cost_per_byte is null", async () => {
+ stubFetch({ [url]: body.replace('"min_fee_ref_script_cost_per_byte":15', '"min_fee_ref_script_cost_per_byte":null') })
+
+ const error = await fail(BlockchainAppliedEffect.getProtocolParameters(BASE_URL, TOKEN))
+
+ expect(error._tag).toBe("ProviderError")
+ })
+
+ it("fails on cost models keyed by parameter name", async () => {
+ stubFetch({ [url]: body.replace('"PlutusV1":[100788,420,1,1]', '"PlutusV1":{"addInteger-cpu-arguments-intercept":100788}') })
+
+ const error = await fail(BlockchainAppliedEffect.getProtocolParameters(BASE_URL, TOKEN))
+
+ expect(error._tag).toBe("ProviderError")
+ })
+})
+
+describe("BlockchainApplied UTxO queries", () => {
+ const utxosUrl = `${BASE_URL}/utxos/${PREPROD_ADDRESS_BECH32}`
+ const pages = {
+ [page(utxosUrl, 0)]: paged([UTXO_ADA_ONLY], 0, true),
+ [page(utxosUrl, 1)]: paged([UTXO_TOKENS], 1, false)
+ }
+
+ it("getUtxos follows has_next across pages", async () => {
+ const fetchMock = stubFetch(pages)
+
+ const utxos = await Effect.runPromise(
+ BlockchainAppliedEffect.getUtxos(BASE_URL, TOKEN)(Address.fromBech32(PREPROD_ADDRESS_BECH32))
+ )
+
+ expect(fetchMock).toHaveBeenCalledTimes(2)
+ expect(utxos.map((u) => TransactionHash.toHex(u.transactionId))).toEqual([
+ "2c8f45e2aa58c7bb25739da28786cce97a97cd346107bb372807fabde323ab96",
+ "e0568183b00d1a7625a97ba1792a7506aafc96719c0643fbb021ae75c9d815a4"
+ ])
+ expect(utxos[0].assets).toEqual(Assets.fromLovelace(2186901580n))
+ expect(utxos[1].assets).toEqual(
+ Assets.addByHex(
+ Assets.addByHex(Assets.fromLovelace(2425818n), "2b172b4b0141058c3a5ee855cfa0f52f85808e3d3aba47f5a44cbd5e", "69455448", 1n),
+ "d088875616229b83868ac5596e77288d8b5c9dffc2f748d8df6332b6",
+ "74494e4459",
+ 9997000000n
+ )
+ )
+ })
+
+ it("getUtxos queries a payment credential by its bech32 form", async () => {
+ const credential = Credential.makeKeyHash(KeyHash.fromHex("c035d6e2a3c89bf7aa0f22b1bf5fe2d8a51f7e2f3e8b1a8db9d7e844").hash)
+ const credentialUrl = `${BASE_URL}/utxos/${Credential.toBech32(credential)}`
+ stubFetch({ [page(credentialUrl, 0)]: paged([UTXO_ADA_ONLY], 0, false) })
+
+ const utxos = await Effect.runPromise(BlockchainAppliedEffect.getUtxos(BASE_URL, TOKEN)(credential))
+
+ expect(credentialUrl).toContain("/utxos/addr_vkh1")
+ expect(utxos).toHaveLength(1)
+ })
+
+ it("getUtxosWithUnit keeps only UTxOs holding the unit", async () => {
+ stubFetch(pages)
+
+ const utxos = await Effect.runPromise(
+ BlockchainAppliedEffect.getUtxosWithUnit(BASE_URL, TOKEN)(Address.fromBech32(PREPROD_ADDRESS_BECH32), TOKEN_UNIT)
+ )
+
+ expect(utxos.map((u) => u.index)).toEqual([1n])
+ expect(TransactionHash.toHex(utxos[0].transactionId)).toBe(
+ "e0568183b00d1a7625a97ba1792a7506aafc96719c0643fbb021ae75c9d815a4"
+ )
+ })
+
+ it("getUtxoByUnit returns the single holder", async () => {
+ stubFetch({ [page(`${BASE_URL}/assets/${PREPROD_UNIT}/utxos`, 0)]: paged([UTXO_INLINE_DATUM], 0, false) })
+
+ const utxo = await Effect.runPromise(BlockchainAppliedEffect.getUtxoByUnit(BASE_URL, TOKEN)(PREPROD_UNIT))
+
+ expect(utxo.index).toBe(0n)
+ })
+
+ it("getUtxoByUnit fails when several UTxOs hold the unit", async () => {
+ stubFetch({ [page(`${BASE_URL}/assets/${PREPROD_UNIT}/utxos`, 0)]: paged([UTXO_ADA_ONLY, UTXO_TOKENS], 0, false) })
+
+ const error = await fail(BlockchainAppliedEffect.getUtxoByUnit(BASE_URL, TOKEN)(PREPROD_UNIT))
+
+ expect(error._tag).toBe("ProviderError")
+ })
+
+ it("getUtxosByOutRef skips out-refs that 404", async () => {
+ stubFetch({ [`${BASE_URL}/utxo/${PREPROD_TX_HASH_HEX}/0`]: UTXO_INLINE_DATUM })
+ const outRef = (index: bigint) =>
+ new TransactionInput.TransactionInput({ transactionId: TransactionHash.fromHex(PREPROD_TX_HASH_HEX), index })
+
+ const utxos = await Effect.runPromise(
+ BlockchainAppliedEffect.getUtxosByOutRef(BASE_URL, TOKEN)([outRef(0n), outRef(7n)])
+ )
+
+ expect(utxos.map((u) => u.index)).toEqual([0n])
+ })
+})
+
+describe("BlockchainApplied datums and reference scripts on UTxOs", () => {
+ const outRefUrl = `${BASE_URL}/utxo/${PREPROD_TX_HASH_HEX}/0`
+ const resolve = () =>
+ BlockchainAppliedEffect.getUtxosByOutRef(
+ BASE_URL,
+ TOKEN
+ )([new TransactionInput.TransactionInput({ transactionId: TransactionHash.fromHex(PREPROD_TX_HASH_HEX), index: 0n })])
+
+ it("an inline datum becomes an InlineDatum", async () => {
+ stubFetch({ [outRefUrl]: UTXO_INLINE_DATUM })
+
+ const [utxo] = await Effect.runPromise(resolve())
+
+ expect(utxo.datumOption).toEqual(new InlineDatum.InlineDatum({ data: Data.fromCBORHex(INLINE_DATUM_CBOR) }))
+ })
+
+ it("a hash-only datum stays a DatumHash, without fetching the datum", async () => {
+ const fetchMock = stubFetch({ [outRefUrl]: UTXO_DATUM_HASH })
+
+ const [utxo] = await Effect.runPromise(resolve())
+
+ expect(utxo.datumOption).toEqual(DatumHash.fromHex(PREPROD_DATUM_HASH_HEX))
+ expect(fetchMock).toHaveBeenCalledTimes(1)
+ })
+
+ it("decodes a native reference script", async () => {
+ stubFetch({ [outRefUrl]: UTXO_NATIVE_SCRIPT })
+
+ const [utxo] = await Effect.runPromise(resolve())
+
+ expect(utxo.scriptRef).toEqual(NATIVE_SCRIPT)
+ })
+
+ it("fails when a reference script comes without its CBOR", async () => {
+ stubFetch({ [outRefUrl]: UTXO_SCRIPT_NO_CBOR })
+
+ const error = await fail(resolve())
+
+ expect(error._tag).toBe("ProviderError")
+ })
+})
+
+describe("BlockchainApplied getDelegation", () => {
+ const url = `${BASE_URL}/staking/${PREPROD_STAKE_ADDRESS_BECH32}`
+
+ it("returns the pool and the withdrawable rewards", async () => {
+ // Recorded
+ stubFetch({
+ [url]: `{"address":"${PREPROD_STAKE_ADDRESS_BECH32}","pool_id":"pool1mp96jpc2dtaruz0cazmljh03dev0969c4rq3wr6hnc4rjdxn8aw","active_epoch":30,"delegation_tx":"a7412e82b9d556162230faa0090535b499160c45766ea429e53e829ed8fe0c04","total_rewards":"147911340884","total_withdrawn":"94929392428","withdrawable_rewards":"52981948456"}`
+ })
+
+ const delegation = await Effect.runPromise(
+ BlockchainAppliedEffect.getDelegation(BASE_URL, TOKEN)(PREPROD_STAKE_ADDRESS_BECH32)
+ )
+
+ expect(delegation).toEqual({
+ poolId: PoolKeyHash.fromBech32("pool1mp96jpc2dtaruz0cazmljh03dev0969c4rq3wr6hnc4rjdxn8aw"),
+ rewards: 52981948456n
+ })
+ })
+
+ it("returns no pool and no rewards for an unknown stake address", async () => {
+ stubFetch({})
+
+ const delegation = await Effect.runPromise(
+ BlockchainAppliedEffect.getDelegation(BASE_URL, TOKEN)(PREPROD_STAKE_ADDRESS_BECH32)
+ )
+
+ expect(delegation).toEqual({ poolId: null, rewards: 0n })
+ })
+})
+
+describe("BlockchainApplied getDatum", () => {
+ it("decodes the datum bytes", async () => {
+ // Recorded
+ stubFetch({
+ [`${BASE_URL}/datum/${PREPROD_DATUM_HASH_HEX}`]: `{"summary":"datum","details":{"hash":"${PREPROD_DATUM_HASH_HEX}","tx_hash":"${PREPROD_TX_HASH_HEX}","json":{"constructor":0,"fields":[{"constructor":0,"fields":[{"int":1740453}]},{"int":1712190951000}]},"bytes":"${INLINE_DATUM_CBOR}"}}`
+ })
+
+ const datum = await Effect.runPromise(
+ BlockchainAppliedEffect.getDatum(BASE_URL, TOKEN)(DatumHash.fromHex(PREPROD_DATUM_HASH_HEX))
+ )
+
+ expect(datum).toEqual(Data.fromCBORHex(INLINE_DATUM_CBOR))
+ })
+})
+
+describe("BlockchainApplied awaitTx", () => {
+ const url = `${BASE_URL}/tx/${PREPROD_TX_HASH_HEX}`
+ // Recorded, trimmed to the header fields
+ const confirmed = `{"hash":"${PREPROD_TX_HASH_HEX}","block_index":1,"epoch_no":129,"fee":"190713","inputs":[],"outputs":[]}`
+ const notFound = { status: 404, body: '{"error":"Transaction not found"}' }
+
+ it("polls while the transaction 404s, then returns", async () => {
+ const fetchMock = stubFetch({ [url]: [notFound, notFound, confirmed] })
+
+ const result = await Effect.runPromise(
+ BlockchainAppliedEffect.awaitTx(BASE_URL, TOKEN)(TransactionHash.fromHex(PREPROD_TX_HASH_HEX), 10, 5_000)
+ )
+
+ expect(result).toBe(true)
+ expect(fetchMock).toHaveBeenCalledTimes(3)
+ })
+
+ it("fails at once on a 401 instead of polling until the timeout", async () => {
+ const fetchMock = stubFetch({ [url]: { status: 401, body: '{"error":"Unauthorized"}' } })
+
+ const error = await fail(
+ BlockchainAppliedEffect.awaitTx(BASE_URL, TOKEN)(TransactionHash.fromHex(PREPROD_TX_HASH_HEX), 10, 5_000)
+ )
+
+ expect(error._tag).toBe("ProviderError")
+ expect(fetchMock).toHaveBeenCalledTimes(1)
+ })
+})
+
+describe("BlockchainApplied submitTx and evaluateTx", () => {
+ const tx = Transaction.fromCBORHex(evalSample1.transaction)
+
+ it("submitTx posts the CBOR bytes and returns the hash", async () => {
+ const txHash = "7e6378266748b08a44c24682af0e46384e7a58b1a7b71b44e84a24f35444ffba"
+ const fetchMock = stubFetch({ [`${BASE_URL}/tx/submit`]: `{"tx_hash":"${txHash}"}` })
+
+ const result = await Effect.runPromise(BlockchainAppliedEffect.submitTx(BASE_URL, TOKEN)(tx))
+
+ expect(TransactionHash.toHex(result)).toBe(txHash)
+ const init = fetchMock.mock.calls[0][1]!
+ expect(new Headers(init.headers).get("Content-Type")).toBe("application/cbor")
+ expect(init.body).toEqual(Transaction.toCBORBytes(tx))
+ })
+
+ it("evaluateTx sends additional_utxos and decodes the redeemers", async () => {
+ const fetchMock = stubFetch({
+ [`${BASE_URL}/tx/evaluate`]:
+ '[{"redeemer_tag":"spend","redeemer_index":0,"ex_units":{"mem":"1700","steps":"9007199254740993"}},{"redeemer_tag":"reward","redeemer_index":1,"ex_units":{"mem":"476468","steps":"133296322"}},{"redeemer_tag":"cert","redeemer_index":2,"ex_units":{"mem":"1","steps":"2"}}]'
+ })
+
+ const result = await Effect.runPromise(BlockchainAppliedEffect.evaluateTx(BASE_URL, TOKEN)(tx, evalSample1.utxos))
+
+ expect(result.map((r) => [r.redeemer_tag, r.redeemer_index, r.ex_units.mem, r.ex_units.steps])).toEqual([
+ ["spend", 0, 1700n, 9007199254740993n],
+ ["reward", 1, 476468n, 133296322n],
+ ["cert", 2, 1n, 2n]
+ ])
+ const body = JSON.parse(fetchMock.mock.calls[0][1]!.body as string)
+ expect(body.cbor).toBe(Transaction.toCBORHex(tx))
+ expect(body.additional_utxos).toHaveLength(evalSample1.utxos.length)
+ expect(body.additional_utxos[0]).toMatchObject({
+ tx_hash: TransactionHash.toHex(evalSample1.utxos[0].transactionId),
+ output_index: Number(evalSample1.utxos[0].index),
+ address: Address.toBech32(evalSample1.utxos[0].address),
+ lovelace: evalSample1.utxos[0].assets.lovelace.toString()
+ })
+ })
+
+ it("evaluateTx fails on an unknown redeemer tag", async () => {
+ stubFetch({
+ [`${BASE_URL}/tx/evaluate`]: '[{"redeemer_tag":"withdraw","redeemer_index":0,"ex_units":{"mem":"1","steps":"2"}}]'
+ })
+
+ const error = await fail(BlockchainAppliedEffect.evaluateTx(BASE_URL, TOKEN)(tx))
+
+ expect(error._tag).toBe("ProviderError")
+ })
+})
+
+describe("BlockchainApplied authentication", () => {
+ it("sends the bearer token on every request", async () => {
+ const fetchMock = stubFetch({
+ [`${BASE_URL}/utxo/${PREPROD_TX_HASH_HEX}/0`]: UTXO_INLINE_DATUM,
+ [`${BASE_URL}/tx/evaluate`]: "[]"
+ })
+
+ await Effect.runPromise(
+ BlockchainAppliedEffect.getUtxosByOutRef(
+ BASE_URL,
+ TOKEN
+ )([new TransactionInput.TransactionInput({ transactionId: TransactionHash.fromHex(PREPROD_TX_HASH_HEX), index: 0n })])
+ )
+ await Effect.runPromise(
+ BlockchainAppliedEffect.evaluateTx(BASE_URL, TOKEN)(Transaction.fromCBORHex(evalSample1.transaction))
+ )
+
+ expect(fetchMock).toHaveBeenCalledTimes(2)
+ for (const [, init] of fetchMock.mock.calls) {
+ expect(new Headers(init!.headers).get("Authorization")).toBe(`Bearer ${TOKEN}`)
+ }
+ })
+})
diff --git a/packages/evolution/test/provider/providers.test.ts b/packages/evolution/test/provider/providers.test.ts
index d7f7c70ea..e67acb233 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
@@ -71,6 +74,11 @@ 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!), { supportsCredentialQueries: true })
+})
+
// ── Kupmios (self-hosted or Demeter) ──────────────────────────────────────────
describe.skipIf(
!isConfigured(KUPO_URL, "https://your-kupo-endpoint") ||