Effect API
Midday SDK is built on Effect, a powerful TypeScript library for building robust, composable applications. The SDK follows the Hybrid API Pattern where Effect is the source of truth.
Why Effect?
Effect provides:
- Type-safe errors - Errors are part of the type signature
- Composability - Build complex workflows from simple operations
- Dependency injection - Swap implementations for testing
- Resource management - Automatic cleanup of resources
- Concurrency - Built-in support for parallel and sequential operations
API Styles Comparison
Simplest approach, ideal for quick prototypes. Uses method chaining:
const client = await Midday.Client.create(config);const loaded = await client.loadContract({ module, zkConfig, privateStateId: 'my-id' });const deployed = await loaded.deploy();await deployed.actions.increment();const state = await deployed.ledgerState();Functional style with Effect composition. Access via .effect namespace:
const program = Effect.gen(function* () { const client = yield* Midday.Client.effect.create(config); const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); yield* deployed.effect.actions.increment(); const state = yield* deployed.effect.ledgerState(); return state;});Full dependency injection with layer composition and automatic resource management:
import { Cluster, ClusterService } from '@no-witness-labs/midday-sdk/devnet';
// Layer composition — all lifecycle is automaticconst ClusterLayer = Cluster.managedLayer({ clusterName: 'my-app' });
const ClientFromCluster = Layer.scoped( Midday.Client.MiddayClientService, Effect.gen(function* () { const cluster = yield* ClusterService; return yield* Midday.Client.effect.createScoped({ seed: 'your-seed', networkConfig: cluster.networkConfig, privateStateProvider: Midday.PrivateState.inMemoryPrivateStateProvider(), }); }),);
const AppLayer = ClientFromCluster.pipe(Layer.provide(ClusterLayer));
const program = Effect.gen(function* () { const client = yield* Midday.Client.MiddayClientService; const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); yield* deployed.effect.actions.increment(); return yield* deployed.effect.ledgerState();});
// Zero lifecycle code — cleanup is automatic (LIFO order)await Effect.runPromise(program.pipe(Effect.provide(AppLayer)));The Hybrid Pattern
The SDK follows these principles:
- Effect is source of truth - All logic is implemented as Effect functions
- Client is the hub - Everything flows from the client
- Two interfaces -
.effect.method()for Effect users,.method()for Promise users - Two-handle pattern -
LoadedContractandDeployedContractare distinct types
// LoadedContract — returned by loadContract()interface LoadedContract<TLedger, TCircuits, TActions> { readonly module: LoadedContractModule; readonly providers: ContractProviders;
deploy(options?: DeployOptions): Promise<DeployedContract<TLedger, TCircuits, TActions>>; join(address: string, options?: JoinOptions): Promise<DeployedContract<TLedger, TCircuits, TActions>>;
readonly effect: { deploy(options?: DeployOptions): Effect.Effect<DeployedContract, ContractError | TxTimeoutError>; join(address: string, options?: JoinOptions): Effect.Effect<DeployedContract, ContractError | TxTimeoutError>; };}
// DeployedContract — returned by deploy() or join()interface DeployedContract<TLedger, TCircuits, TActions> { readonly address: string; readonly actions: TActions; // typed action methods
call(action: TCircuits, ...args: unknown[]): Promise<CallResult>; ledgerState(): Promise<TLedger>; ledgerStateAt(blockHeight: number): Promise<TLedger>; getState(): Promise<unknown>; getStateAt(blockHeight: number): Promise<unknown>; onStateChange(callback: (state: TLedger) => void): Unsubscribe; onRawStateChange(callback: (state: unknown) => void): Unsubscribe; watchState(): AsyncIterableIterator<TLedger>; watchRawState(): AsyncIterableIterator<unknown>;
readonly effect: { readonly actions: ToEffectActions<TActions>; call(action: TCircuits, ...args: unknown[]): Effect.Effect<CallResult, ContractError>; ledgerState(): Effect.Effect<TLedger, ContractError>; ledgerStateAt(blockHeight: number): Effect.Effect<TLedger, ContractError>; getState(): Effect.Effect<unknown, ContractError>; getStateAt(blockHeight: number): Effect.Effect<unknown, ContractError>; watchState(): Stream.Stream<TLedger, ContractError>; watchRawState(): Stream.Stream<unknown, ContractError>; };}Running Effects
With runEffectPromise
The simplest way to run an Effect:
const result = await Midday.Runtime.runEffectPromise(program);With Effect.runPromise
For more control:
const result = await Effect.runPromise( program.pipe(Effect.provide(ClientLayer)));Available Services
| Service | Layer Factory | Description |
|---|---|---|
Client.MiddayClientService | Client.layer(config) | Pre-initialized client (scoped — auto-closes) |
Client.ClientService | Client.ClientLive | Client factory service |
ClusterService | Cluster.managedLayer(config?) | Managed devnet cluster (auto-starts/removes) |
Using Layers
Pre-configured Client Layer (Recommended)
For most applications, use Client.layer() to create a scoped client layer.
The client is automatically closed when the scope ends:
import * as Midday from '@no-witness-labs/midday-sdk';import { Effect } from 'effect';
// Create scoped client layer — auto-closes when doneconst ClientLayer = Midday.Client.layer({ seed: 'your-64-char-hex-seed', networkConfig: Midday.Config.NETWORKS.local, privateStateProvider: Midday.PrivateState.inMemoryPrivateStateProvider(),});
const program = Effect.gen(function* () { const client = yield* Midday.Client.MiddayClientService; const loaded = yield* client.effect.loadContract({ module: CounterContract, zkConfig: Midday.ZkConfig.fromPath('./contracts/counter'), privateStateId: 'my-counter', }); const deployed = yield* loaded.effect.deploy(); return deployed;});
await Effect.runPromise(program.pipe(Effect.provide(ClientLayer)));Layer Composition with Devnet
For applications that also manage a local devnet cluster, compose layers so cleanup happens automatically in LIFO order:
import { Cluster, ClusterService } from '@no-witness-labs/midday-sdk/devnet';import { Layer } from 'effect';
// Managed cluster — auto-starts on creation, auto-removes on releaseconst ClusterLayer = Cluster.managedLayer({ clusterName: 'my-app' });
// Derives a client from the cluster serviceconst ClientFromCluster = Layer.scoped( Midday.Client.MiddayClientService, Effect.gen(function* () { const cluster = yield* ClusterService; return yield* Midday.Client.effect.createScoped({ seed: Midday.Config.DEV_WALLET_SEED, networkConfig: cluster.networkConfig, privateStateProvider: Midday.PrivateState.inMemoryPrivateStateProvider(), }); }),);
// Compose: Cluster feeds into Clientconst AppLayer = ClientFromCluster.pipe(Layer.provide(ClusterLayer));
// Run — zero lifecycle codeawait Effect.runPromise(program.pipe(Effect.provide(AppLayer)));Client Factory Service
For creating multiple clients with different configs:
const program = Effect.gen(function* () { const clientService = yield* Midday.Client.ClientService;
// Create different clients const localClient = yield* clientService.create(localConfig); const testnetClient = yield* clientService.create(testnetConfig);
return { localClient, testnetClient };});
await Effect.runPromise(program.pipe(Effect.provide(Midday.Client.ClientLive)));Error Handling
Effect makes errors explicit in the type system:
const program = Effect.gen(function* () { const client = yield* Midday.Client.effect.create(config); const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); const result = yield* deployed.effect.call('increment'); return result;}).pipe( Effect.catchTag('ContractError', (error) => { console.error('Contract operation failed:', error.message); return Effect.succeed({ status: 'failed' }); }), Effect.catchTag('TxTimeoutError', (error) => { console.error('Timeout:', error.message); return Effect.succeed({ status: 'timeout' }); }), Effect.catchAll((error) => { console.error('Unexpected error:', error); return Effect.fail(error); }));State Watching with Effect Streams
Effect users get Stream variants for state watching:
import { Stream } from 'effect';
const program = Effect.gen(function* () { const deployed = /* ... */;
// Watch as Effect Stream const stream = deployed.effect.watchState();
// Take first 5 state changes yield* stream.pipe( Stream.take(5), Stream.runForEach((state) => Effect.sync(() => console.log('Counter:', state.counter)) ), );});Testing with Effect
Replace services with test implementations:
import { Layer, Effect } from 'effect';
// Create mock client matching the MiddayClient interfaceconst mockClient: Midday.Client.MiddayClient = { networkConfig: Midday.Config.NETWORKS.local, providers: mockProviders, wallet: null, relayerWallet: null, loadContract: () => Promise.resolve(mockLoadedContract), waitForTx: () => Promise.resolve({ txHash: '0x123', blockHeight: 1, blockHash: '0xabc' }), close: () => Promise.resolve(), [Symbol.asyncDispose]: async () => {}, effect: { loadContract: () => Effect.succeed(mockLoadedContract), waitForTx: () => Effect.succeed({ txHash: '0x123', blockHeight: 1, blockHash: '0xabc' }), close: () => Effect.void, },};
const TestClientLayer = Layer.succeed(Midday.Client.MiddayClientService, mockClient);
// Run tests with mock clientconst result = await Effect.runPromise( program.pipe(Effect.provide(TestClientLayer)));Parallel Operations
Effect makes concurrent operations easy:
const program = Effect.gen(function* () { // Run multiple contract calls in parallel const [result1, result2] = yield* Effect.all([ contract1.effect.call('action1'), contract2.effect.call('action2'), ], { concurrency: 'unbounded' });
return { result1, result2 };});Resource Management
Effect handles cleanup automatically with scoped resources:
Scoped Layers (Recommended)
When using Client.layer() or Cluster.managedLayer(), resources are cleaned up
automatically when the Effect scope ends — no manual close() needed:
const ClientLayer = Midday.Client.layer(config);
// Client is auto-closed when runPromise completesawait Effect.runPromise(program.pipe(Effect.provide(ClientLayer)));Scoped Resources
For fine-grained control, use createScoped() and makeScoped() directly:
const program = Effect.scoped( Effect.gen(function* () { const client = yield* Midday.Client.effect.createScoped(config); // client is auto-closed when this scope exits const loaded = yield* client.effect.loadContract({ module, zkConfig, privateStateId: 'my-id' }); const deployed = yield* loaded.effect.deploy(); yield* deployed.effect.actions.increment(); }),);Manual Close (Promise API)
When using the Promise API, always close the client explicitly:
const client = await Midday.Client.create(config);try { // ...} finally { await client.close();}